mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 08:00:32 +02:00
Steam Frame from a Mac: research docs and helper scripts
README with the minimum-typing checklist (Developer Mode toggle + Set User Password; the rest runs from the Mac), docs for SSH, streaming, file transfer, and open questions with sourced confidence levels, plus Mac-side zsh helpers and a fallback headset bootstrap. Scripts are UNTESTED against hardware: checked with zsh -n / bash -n / shellcheck only. Two SWE-2 Max read-only review passes (devin -p --model swe-2-max); verified findings fixed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
1f6115b789
11 files changed
+700
No files matched your search
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# Runs ON the Steam Frame (fallback path only; normally Developer Mode's
|
||||
# toggle + "Set User Password" is enough and this is not needed).
|
||||
# Served by scripts/serve-bootstrap.sh, which substitutes the public key.
|
||||
#
|
||||
# UNTESTED against real hardware. Idempotent.
|
||||
set -eu
|
||||
|
||||
KEY='__PUBKEY__'
|
||||
|
||||
mkdir -p "$HOME/.ssh"
|
||||
chmod 700 "$HOME/.ssh"
|
||||
touch "$HOME/.ssh/authorized_keys"
|
||||
chmod 600 "$HOME/.ssh/authorized_keys"
|
||||
if grep -qxF "$KEY" "$HOME/.ssh/authorized_keys"; then
|
||||
echo "key already present"
|
||||
else
|
||||
echo "$KEY" >> "$HOME/.ssh/authorized_keys"
|
||||
echo "key added"
|
||||
fi
|
||||
|
||||
echo "Enabling sshd. If sudo asks for a password you never set, press Ctrl-C,"
|
||||
echo "set one in Steam Settings > Developer > Set User Password (or run: passwd),"
|
||||
echo "then re-run the same one-liner."
|
||||
sudo systemctl enable --now sshd
|
||||
|
||||
echo
|
||||
echo "sshd: $(systemctl is-active sshd) user: $(id -un) host: $(hostname)"
|
||||
ip -4 -brief addr show scope global 2>/dev/null || true
|
||||
echo "Now on the Mac: scripts/connect.sh"
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env zsh
|
||||
# Mac-side: find the Steam Frame, create a key, add a `Host frame` alias to
|
||||
# ~/.ssh/config, copy the key, and optionally disable SSH password logins.
|
||||
#
|
||||
# UNTESTED against real hardware. Idempotent: safe to re-run.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/connect.sh [HOST_OR_IP] # set up key + alias
|
||||
# scripts/connect.sh [HOST_OR_IP] --harden # also disable password auth
|
||||
#
|
||||
# Env: FRAME_USER (default steamos), FRAME_ALIAS (default frame).
|
||||
set -euo pipefail
|
||||
|
||||
FRAME_USER=${FRAME_USER:-steamos}
|
||||
FRAME_ALIAS=${FRAME_ALIAS:-frame}
|
||||
KEY="$HOME/.ssh/id_ed25519_frame"
|
||||
CONFIG="$HOME/.ssh/config"
|
||||
BEGIN_MARK="# >>> steam-frame ($FRAME_ALIAS) >>>"
|
||||
END_MARK="# <<< steam-frame ($FRAME_ALIAS) <<<"
|
||||
|
||||
harden=0
|
||||
host_arg=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--harden) harden=1 ;;
|
||||
-h|--help) sed -n '2,11p' "$0"; exit 0 ;;
|
||||
*) host_arg="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
port_open() {
|
||||
# nc resolves through the system resolver (including mDNS for .local).
|
||||
nc -z -G 3 "$1" 22 >/dev/null 2>&1
|
||||
}
|
||||
|
||||
pick_host() {
|
||||
local candidates=()
|
||||
[[ -n "$host_arg" ]] && candidates+=("$host_arg")
|
||||
candidates+=("$FRAME_ALIAS.local" "$FRAME_ALIAS")
|
||||
local h
|
||||
for h in "${candidates[@]}"; do
|
||||
if port_open "$h"; then
|
||||
print -r -- "$h"; return 0
|
||||
fi
|
||||
print -u2 " - $h: not resolvable or port 22 closed"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
print "==> Looking for the Steam Frame"
|
||||
if ! HOST=$(pick_host); then
|
||||
print -u2 "Could not reach the Frame on port 22."
|
||||
print -u2 "Check: Developer Mode on + user password set; same Wi-Fi; no client isolation."
|
||||
print -u2 "Then re-run with the IP from Quick Settings: scripts/connect.sh 192.168.x.y"
|
||||
exit 1
|
||||
fi
|
||||
print " found: $HOST"
|
||||
|
||||
print "==> SSH key"
|
||||
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
|
||||
if [[ ! -f "$KEY" ]]; then
|
||||
ssh-keygen -q -t ed25519 -N '' -C "mac->steam-frame" -f "$KEY"
|
||||
print " created $KEY"
|
||||
else
|
||||
print " exists: $KEY"
|
||||
fi
|
||||
|
||||
print "==> ~/.ssh/config alias '$FRAME_ALIAS' -> $HOST"
|
||||
touch "$CONFIG" && chmod 600 "$CONFIG"
|
||||
tmp=$(mktemp)
|
||||
# Drop any previous managed block, then PREPEND a fresh one: ssh uses the first
|
||||
# value it sees per option, so this block must precede any other "Host frame"
|
||||
# or "Host *". The trailing "Host *" returns the rest of the file to global scope.
|
||||
awk -v b="$BEGIN_MARK" -v e="$END_MARK" '
|
||||
$0==b {skip=1; next}
|
||||
$0==e {skip=0; next}
|
||||
!skip {print}
|
||||
' "$CONFIG" > "$tmp"
|
||||
{
|
||||
print -r -- "$BEGIN_MARK"
|
||||
print -r -- "Host $FRAME_ALIAS"
|
||||
print -r -- " HostName $HOST"
|
||||
print -r -- " User $FRAME_USER"
|
||||
print -r -- " IdentityFile $KEY"
|
||||
print -r -- " IdentitiesOnly yes"
|
||||
print -r -- " ServerAliveInterval 30"
|
||||
print -r -- "Host *"
|
||||
print -r -- "$END_MARK"
|
||||
cat "$tmp"
|
||||
} > "$CONFIG"
|
||||
rm -f "$tmp"
|
||||
|
||||
print "==> Checking key login"
|
||||
if ssh -o BatchMode=yes -o ConnectTimeout=5 "$FRAME_ALIAS" true 2>/dev/null; then
|
||||
print " key login already works"
|
||||
else
|
||||
print " copying key (enter the Developer Mode password once)"
|
||||
ssh-copy-id -i "$KEY.pub" -o IdentitiesOnly=yes "$FRAME_USER@$HOST"
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=5 "$FRAME_ALIAS" true \
|
||||
|| { print -u2 "Key login still failing after ssh-copy-id."; exit 1; }
|
||||
print " key login OK"
|
||||
fi
|
||||
|
||||
if (( harden )); then
|
||||
print "==> Disabling SSH password auth (sudo password asked on the Frame)"
|
||||
# shellcheck disable=SC2016
|
||||
if ! ssh -t "$FRAME_ALIAS" '
|
||||
set -e
|
||||
grep -Eiq "^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf" /etc/ssh/sshd_config \
|
||||
|| { echo "sshd_config has no sshd_config.d include; not hardening."; exit 1; }
|
||||
printf "PasswordAuthentication no\nKbdInteractiveAuthentication no\n" \
|
||||
| { sudo mkdir -p /etc/ssh/sshd_config.d; sudo tee /etc/ssh/sshd_config.d/01-frame-keys-only.conf >/dev/null; }
|
||||
sudo sshd -t
|
||||
sudo systemctl reload sshd
|
||||
echo "password auth disabled"
|
||||
'; then
|
||||
print -u2 "!! Hardening failed. If the drop-in was written, it will disable password SSH"
|
||||
print -u2 "!! on the next sshd restart. To undo it:"
|
||||
print -u2 "!! ssh $FRAME_ALIAS 'sudo rm -f /etc/ssh/sshd_config.d/01-frame-keys-only.conf'"
|
||||
exit 1
|
||||
fi
|
||||
if ssh -o BatchMode=yes -o ConnectTimeout=5 "$FRAME_ALIAS" true; then
|
||||
print " key login still OK after hardening"
|
||||
else
|
||||
print -u2 "!! Key login FAILED after hardening. Password SSH is now off."
|
||||
print -u2 "!! Recover via RDP or 'adb shell' (USB-C), then run:"
|
||||
print -u2 "!! sudo rm /etc/ssh/sshd_config.d/01-frame-keys-only.conf && sudo systemctl reload sshd"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
print "\nDone. Try: ssh $FRAME_ALIAS"
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env zsh
|
||||
# Mac-side: install Flatpaks on the Steam Frame over SSH (per-user, so they
|
||||
# survive SteamOS updates and need no sudo / steamos-readonly changes).
|
||||
#
|
||||
# UNTESTED against real hardware. Idempotent.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/install-apps.sh remmina [--vnc-host my-mac.local]
|
||||
# scripts/install-apps.sh moonlight
|
||||
# scripts/install-apps.sh org.example.SomeApp # any Flathub app ID
|
||||
#
|
||||
# --vnc-host pre-seeds a Remmina profile pointing at the Mac's built-in
|
||||
# Screen Sharing (VNC, port 5900) so nothing needs typing in the headset.
|
||||
set -euo pipefail
|
||||
|
||||
FRAME_ALIAS=${FRAME_ALIAS:-frame}
|
||||
vnc_host=""
|
||||
apps=()
|
||||
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
--vnc-host) vnc_host=${2:?--vnc-host needs a hostname}; shift 2
|
||||
[[ "$vnc_host" =~ '^[A-Za-z0-9.-]+$' ]] || { print -u2 "Bad hostname: $vnc_host"; exit 2; } ;;
|
||||
-h|--help) sed -n '2,13p' "$0"; exit 0 ;;
|
||||
remmina) apps+=(org.remmina.Remmina); shift ;;
|
||||
moonlight) apps+=(com.moonlight_stream.Moonlight); shift ;;
|
||||
[A-Za-z]*.*[A-Za-z0-9_]) [[ "$1" =~ '^[A-Za-z0-9_.-]+$' ]] || { print -u2 "Bad app ID: $1"; exit 2; }; apps+=("$1"); shift ;;
|
||||
*) print -u2 "Unknown app '$1' (use remmina, moonlight, or a Flathub app ID)"; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if (( ${#apps} == 0 )) && [[ -z "$vnc_host" ]]; then
|
||||
sed -n '2,13p' "$0"; exit 2
|
||||
fi
|
||||
|
||||
if (( ${#apps} )); then
|
||||
print "==> Installing on $FRAME_ALIAS: ${apps[*]}"
|
||||
ssh "$FRAME_ALIAS" "
|
||||
set -e
|
||||
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak install --user -y flathub ${(j: :)${(@q)apps}}
|
||||
"
|
||||
fi
|
||||
|
||||
if [[ -n "$vnc_host" ]]; then
|
||||
print "==> Writing Remmina profile for vnc://$vnc_host"
|
||||
ssh "$FRAME_ALIAS" "
|
||||
set -e
|
||||
d=\$HOME/.var/app/org.remmina.Remmina/data/remmina
|
||||
mkdir -p \"\$d\"
|
||||
cat > \"\$d/mac-screen-sharing.remmina\" <<'EOF'
|
||||
[remmina]
|
||||
name=Mac Screen Sharing
|
||||
protocol=VNC
|
||||
server=$vnc_host:5900
|
||||
colordepth=32
|
||||
quality=9
|
||||
viewonly=0
|
||||
showcursor=1
|
||||
EOF
|
||||
echo \"wrote \$d/mac-screen-sharing.remmina\"
|
||||
"
|
||||
print "On the Mac: System Settings > General > Sharing > Screen Sharing (i) >"
|
||||
print " enable 'VNC viewers may control screen with password' and set one."
|
||||
fi
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env zsh
|
||||
# Mac-side: put text on the Steam Frame desktop clipboard.
|
||||
#
|
||||
# UNTESTED against real hardware. Assumes the in-headset desktop is a Plasma
|
||||
# session owned by the SSH user; prints diagnostics if that assumption fails.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/paste-to-frame.sh # sends the Mac clipboard (pbpaste)
|
||||
# some-cmd | scripts/paste-to-frame.sh -
|
||||
set -euo pipefail
|
||||
|
||||
FRAME_ALIAS=${FRAME_ALIAS:-frame}
|
||||
|
||||
# Runs on the Frame. Clipboard text arrives on stdin. setsid keeps the
|
||||
# clipboard-serving process alive after the SSH session closes.
|
||||
remote=$(cat <<'EOF'
|
||||
set -u
|
||||
tmp=$(mktemp)
|
||||
cat > "$tmp"
|
||||
rt=/run/user/$(id -u)
|
||||
sock=$(ls "$rt" 2>/dev/null | grep -E '^wayland-[0-9]+$' | head -n 1)
|
||||
if [ -n "$sock" ] && command -v wl-copy >/dev/null 2>&1 \
|
||||
&& XDG_RUNTIME_DIR=$rt WAYLAND_DISPLAY=$sock setsid wl-copy < "$tmp" >/dev/null 2>&1; then
|
||||
echo "copied via wl-copy ($sock)"
|
||||
elif command -v xclip >/dev/null 2>&1 \
|
||||
&& DISPLAY=:0 setsid xclip -selection clipboard -i < "$tmp" >/dev/null 2>&1; then
|
||||
echo "copied via xclip (DISPLAY=:0)"
|
||||
else
|
||||
echo "clipboard copy failed; diagnostics:" >&2
|
||||
echo " runtime dir: $(ls "$rt" 2>&1 | tr '\n' ' ')" >&2
|
||||
echo " wl-copy: $(command -v wl-copy || echo missing) xclip: $(command -v xclip || echo missing)" >&2
|
||||
loginctl list-sessions --no-legend 2>&1 | sed 's/^/ session: /' >&2
|
||||
rm -f "$tmp"; exit 2
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
EOF
|
||||
)
|
||||
b64=$(print -rn -- "$remote" | base64)
|
||||
|
||||
if [[ "${1:-}" == "-" ]]; then
|
||||
ssh "$FRAME_ALIAS" "bash -c \"\$(echo $b64 | base64 -d)\""
|
||||
else
|
||||
pbpaste | ssh "$FRAME_ALIAS" "bash -c \"\$(echo $b64 | base64 -d)\""
|
||||
fi
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env zsh
|
||||
# Mac-side: copy a file or folder to the Steam Frame.
|
||||
#
|
||||
# UNTESTED against real hardware.
|
||||
#
|
||||
# Usage: scripts/push.sh SOURCE [REMOTE_DEST] (default dest: ~/Downloads/)
|
||||
set -euo pipefail
|
||||
|
||||
FRAME_ALIAS=${FRAME_ALIAS:-frame}
|
||||
src=${1:?usage: push.sh SOURCE [REMOTE_DEST]}
|
||||
dest=${2:-Downloads/}
|
||||
|
||||
if ssh "$FRAME_ALIAS" 'command -v rsync >/dev/null'; then
|
||||
rsync -a --progress "$src" "$FRAME_ALIAS:${(q)dest}" # remote shell parses the path
|
||||
else
|
||||
print -u2 "rsync not found on the Frame; falling back to scp"
|
||||
scp -r "$src" "$FRAME_ALIAS:$dest" # modern scp uses SFTP: no remote shell parsing
|
||||
fi
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env zsh
|
||||
# Mac-side (fallback only): serve bootstrap-on-frame.sh over plain HTTP on the
|
||||
# LAN, with this Mac's Frame public key embedded, and print the short
|
||||
# one-liner to type in Konsole on the headset. Ctrl-C to stop.
|
||||
#
|
||||
# UNTESTED against real hardware. Serves only a public key; use on a trusted LAN.
|
||||
set -euo pipefail
|
||||
|
||||
PORT=${PORT:-8765}
|
||||
here=${0:A:h}
|
||||
KEY="$HOME/.ssh/id_ed25519_frame"
|
||||
|
||||
if [[ ! -f "$KEY.pub" ]]; then
|
||||
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
|
||||
ssh-keygen -q -t ed25519 -N '' -C "mac->steam-frame" -f "$KEY"
|
||||
fi
|
||||
pub=$(<"$KEY.pub")
|
||||
|
||||
dir=$(mktemp -d)
|
||||
trap 'rm -rf "$dir"' EXIT
|
||||
# index.html so the bare URL works; curl doesn't care about the name.
|
||||
sed "s|__PUBKEY__|$pub|" "$here/bootstrap-on-frame.sh" > "$dir/index.html"
|
||||
|
||||
name="$(scutil --get LocalHostName 2>/dev/null || hostname -s).local"
|
||||
ip=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || true)
|
||||
|
||||
print "Type ONE of these in Konsole on the Frame:"
|
||||
print " curl -fsS $name:$PORT|bash"
|
||||
[[ -n "$ip" ]] && print " curl -fsS $ip:$PORT|bash"
|
||||
print "Serving from $dir on port $PORT (Ctrl-C to stop)..."
|
||||
python3 -m http.server "$PORT" --directory "$dir"
|
||||
Reference in new issue
Block a user