Files
DeeJanuzandClaude Opus 5.5 8b854fd424 Uninstall cleans up after itself; a shorter install command
- desktops.sh uninstall also removes Launch as Standalone's app copies
  and the title bar decoration, and Display Settings' uninstall the
  profiles' launcher entries; its install writes them back from the
  saved profiles (new: ft-layout launchers)
- the README says which settings an uninstall leaves
- the install command is now curl -fsSL https://deejanuz.github.io/
  frametop/get.sh | bash (GitHub Pages, from main)
- ft-gaze logs "action manifest ...: ok" instead of "error 0"

Found in a clean install test on the Frame (2026-10-01): uninstall,
then get.sh from the headset into a fresh clone; everything installed
and doctor.sh passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:42:37 -06:00

106 lines
6.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Start, stop, or inspect the multi-screen Plasma desktop in VR on the Frame.
# Usage: desktops.sh start [screens] | stop | restart | status | log [lines]
# desktops.sh install # make the VR launcher's "Desktop" entry start Frametop
# desktops.sh uninstall # give the launcher back the stock SteamOS desktop
# desktops.sh screens N # set the default screen count in ~/.config/frametop.conf
# desktops.sh remote on|off|info # VNC access over the tailnet (applies on next start)
# desktops.sh relay install|uninstall|status|log # input relay service (see input/input-relay.py)
# start without a count uses the Frame's config. FT_WIDTH, FT_HEIGHT, FT_PHYS_WIDTH pass through.
set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
. "$root/scripts/_env.sh"
frame="$root/scripts/frame.sh"
action=${1:-start}
screens=${2:-${FT_SCREENS:-}}
session=$FRAME_REPO/session
override=.local/share/applications/deckard-nested-desktop.desktop
log=/tmp/frametop-session.log
# Bracketed first letter so pgrep/pkill never match the ssh shell running them.
match='[v]r-overlay-key frametop '
# Is either backend's desktop running? (gamescope, or ft-screens)
running="{ pgrep -f '$match' >/dev/null || pgrep -x ft-screens >/dev/null; }"
case $action in
start)
# In its own systemd unit, so it outlives this shell (SSH, the settings app's restart).
"$root/scripts/sync.sh" >/dev/null
"$frame" --host "$running && { echo 'already running'; exit 0; }
systemctl --user reset-failed frametop-desktop 2>/dev/null
systemd-run --user --collect --quiet --unit frametop-desktop \
${screens:+--setenv=FT_SCREENS=$screens} ${FT_WIDTH:+--setenv=FT_WIDTH=$FT_WIDTH} ${FT_HEIGHT:+--setenv=FT_HEIGHT=$FT_HEIGHT} \
${FT_PHYS_WIDTH:+--setenv=FT_PHYS_WIDTH=$FT_PHYS_WIDTH} ${FT_BACKEND:+--setenv=FT_BACKEND=$FT_BACKEND} \
bash -c 'exec $session/frametop-session.sh > $log 2>&1'
sleep 12; echo \"plasmashell processes: \$(pgrep -c plasmashell)\"
$running && echo 'started' || { echo 'failed:'; tail -20 $log; exit 1; }" ;;
install)
"$root/scripts/sync.sh" >/dev/null
"$frame" --host "set -e; mkdir -p ~/.local/share/applications
sed 's|@SESSION@|$session/frametop-session.sh|' $session/deckard-nested-desktop.desktop > ~/$override
[ -f ~/.config/frametop.conf ] || cp $session/frametop.conf.example ~/.config/frametop.conf
echo \"installed ~/$override\"; grep ^Exec= ~/$override; echo; cat ~/.config/frametop.conf" ;;
uninstall)
# Also what the session puts in place at each start: Launch as Standalone's app copies and
# the title bar decoration (float/ft_apps.py, decoration/).
"$frame" --host "rm -f ~/$override
rm -rf ~/.local/share/frametop/apps ~/.local/share/kwin/decorations/kwin4_decoration_qml_frametop
rmdir ~/.local/share/frametop 2>/dev/null; echo 'removed; the launcher uses the stock desktop again'" ;;
screens)
[[ ${2:-} =~ ^[1-9]$ ]] || { echo "usage: $0 screens N (1-9)" >&2; exit 2; }
"$frame" --host "set -e; f=~/.config/frametop.conf
[ -f \$f ] || cp $session/frametop.conf.example \$f
sed -i 's/^SCREENS=[0-9]*/SCREENS=$2/' \$f; grep ^SCREENS \$f" ;;
remote)
case ${2:-info} in
on|off)
v=$([ "$2" = on ] && echo 1 || echo 0)
"$frame" --host "set -e; f=~/.config/frametop.conf
[ -f \$f ] || cp $session/frametop.conf.example \$f
grep -q '^REMOTE=' \$f || echo 'REMOTE=0 # 1 = serve the desktop over VNC on the tailnet (port 5900)' >> \$f
sed -i 's/^REMOTE=[01]/REMOTE=$v/' \$f; grep ^REMOTE \$f; echo 'applies the next time the desktop starts'" ;;
info)
"$frame" --host "grep ^REMOTE ~/.config/frametop.conf 2>/dev/null || echo 'REMOTE not set'
ip=\$(ip -4 -o addr show tailscale0 | awk '{print \$4}' | cut -d/ -f1)
echo \"VNC: \$(hostname):5900 on the tailnet (\$ip) password \$(cat ~/.config/frametop-remote/vnc-password 2>/dev/null || echo '(created on first start)')\"
if pgrep -f '[X]vnc :20 ' >/dev/null; then echo 'vnc: running'; else echo 'vnc: not running'; fi
if pgrep -f '[k]rdpserver --plasma' >/dev/null; then echo 'capture (krdp, 127.0.0.1 only): running'; else echo 'capture (krdp): not running'; fi" ;;
*) echo "usage: $0 remote on|off|info" >&2; exit 2 ;;
esac ;;
relay)
unit=frametop-input-relay.service
case ${2:-status} in
install)
"$root/scripts/sync.sh" >/dev/null
# Enabled, not started: started under a running SteamVR it would grab the
# mouse away from it. It comes up before SteamVR on the next start.
fill_template "$root/input/$unit" | on_frame "mkdir -p ~/.config/systemd/user && cat > ~/.config/systemd/user/$unit"
"$frame" --host "set -e
systemctl --user daemon-reload; systemctl --user enable $unit
echo 'enabled; starts before SteamVR on the next reboot or SteamVR restart'" ;;
uninstall) "$frame" --host "systemctl --user disable --now $unit 2>/dev/null; systemctl --user clean --what=fdstore $unit 2>/dev/null; rm -f ~/.config/systemd/user/$unit; systemctl --user daemon-reload; echo removed" ;;
status) "$frame" --host "systemctl --user is-enabled $unit 2>/dev/null; systemctl --user is-active $unit 2>/dev/null
echo \"fd store: \$(systemctl --user show -p NFileDescriptorStore --value $unit)\"
p=\$(pgrep -x vrserver | head -1); [ -n \"\$p\" ] && for e in \$(ls -l /proc/\$p/fd 2>/dev/null | grep -oE 'event[0-9]+( \\(deleted\\))?' | sort -u | tr ' ' '_'); do n=\${e%%_*}; echo \"vrserver has \$e: \$(cat /sys/class/input/\$n/device/name 2>/dev/null)\"; done; true" ;;
log) "$frame" --host "journalctl --user -u $unit --no-pager -n ${3:-30}" ;;
*) echo "usage: $0 relay install|uninstall|status|log" >&2; exit 2 ;;
esac ;;
stop)
# ft-screens: ending it ends KWin and the session. gamescope can take a while to exit
# on SIGTERM. Wait, then force it. First, programs started in the desktop move out of
# its unit (session/keep-apps.sh), so background work in them outlives the restart.
"$frame" --host "$running || { echo 'not running'; exit 0; }
$session/keep-apps.sh
systemctl --user stop frametop-desktop 2>/dev/null; pkill -x ft-screens; pkill -f '$match'
for i in \$(seq 20); do $running || { echo stopped; exit 0; }; sleep 0.5; done
pkill -KILL -x ft-screens
pkill -KILL -f '$match'; sleep 1
pkill -f '[m]ultidesk-session.sh --inner' 2>/dev/null; pkill -f '[k]rdpserver --plasma' 2>/dev/null
pkill -f '[X]vnc :20 ' 2>/dev/null; pkill -f '[x]freerdp /v:.*:3390' 2>/dev/null
$running && { echo 'still running'; exit 1; } || echo 'stopped (forced)'" ;;
restart) "$0" stop; sleep 3; exec "$0" start ${screens:+"$screens"} ;;
status) "$frame" --host "if $running; then pgrep -af '$match|[m]d-screens --socket' | cut -c1-120; else echo 'not running'; fi" ;;
log) "$frame" --host "grep -vE '^\s*$' $log | tail -n ${2:-40}" ;;
*) echo "usage: $0 start [screens] | stop | restart | status | log [lines] | install | uninstall | screens N | remote on|off|info | relay install|uninstall|status|log" >&2; exit 2 ;;
esac