Files
Frametop--frametop/uninstall.sh
T
DeeJanuzandClaude Opus 5.5 a0e46e4954 gaze: keep SteamVR's eye cameras at 90 fps (gaze/eye-rate.py)
The Frame's SteamVR driver runs the eye cameras at 15 to 90 fps and, with its
defaults, picks 15. Our tracker then sees 8 or 9 frames in a calibration
dot's 0.6 s, where it needs 15, so it can never calibrate (2026-10-10 report:
"the right eye was seen in only 8 frames" on all 27 dots). With
steamvr.gazeDepEyeToHeadEnable on, SteamVR asks for 90 at every start; the
reporter calibrated once it was on.

gaze/run.sh install turns it on (vrcmd with SteamVR running, else the
settings file), and marks it as Frametop's; gaze/run.sh uninstall and
uninstall.sh turn it off only then. update-check.py (doctor) and
gaze-report.py report the cameras' rate from eyetracking.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:58:26 -06:00

293 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# Uninstall Frametop from the Steam Frame. In a terminal on the headset (Konsole in the desktop,
# or over SSH):
#
# curl -fsSL https://frametop.github.io/frametop/uninstall.sh | bash
#
# or ~/frametop/uninstall.sh. It doesn't use the rest of the repo, so it also works when
# ~/frametop is gone or broken.
#
# It never stops what you're using now: the input relay carries the keyboard and mouse, and the
# desktop runs from the repo. So it goes in two steps:
# 1. Frametop stops starting. The launcher's Desktop entry goes back to the stock desktop, and
# Frametop's Native Desktop entry, services, SteamVR driver, menu entries, and system files
# (our eye tracker's frame grabber and the Bluetooth fixes, with sudo) are removed, and so
# are the file capabilities of hand tracking's camera broker (ft-camd, with sudo). What runs
# now keeps running until you restart the headset.
# 2. After the restart, run it again. It deletes the code (~/frametop, or the releases in
# ~/.local/share/frametop/releases with their containers and images) and, if you want, your
# settings, voice dictation's speech models, and the build container.
# When nothing of Frametop is running, one run does both.
#
# Options (piped, they go after "bash -s --"):
# --dir DIR where the repo is, if not ~/frametop
# --dry-run show what it would do, and change nothing
set -euo pipefail
shopt -s nullglob
usage() {
cat <<'EOF'
usage: uninstall.sh [--dir DIR] [--dry-run]
piped: curl -fsSL https://frametop.github.io/frametop/uninstall.sh | bash -s -- [options]
EOF
}
dry=0
apps=$HOME/.local/share/applications
override=$apps/deckard-nested-desktop.desktop
native_copy=$apps/native-deckard-nested-desktop.desktop
relay_unit=$HOME/.config/systemd/user/frametop-input-relay.service
driver=$HOME/.local/share/frametop/ft_pointer
releases=$HOME/.local/share/frametop/releases # pack/install-release.sh
vrpathreg=/opt/steamvr/bin/linuxarm64/vrpathreg
handsctl=$HOME/.local/bin/ft-handsctl
eye_rate=$HOME/.local/state/frametop/gaze/steamvr-eye-rate # gaze/eye-rate.py turned the setting on
eyegrab_files=(/etc/systemd/system/frametop-eyegrab.service /etc/frametop/ft-eyegrab
/etc/atomic-update.conf.d/frametop-eyegrab.conf)
bt_files=(/etc/systemd/system/steamframe-bt-fixups.service /etc/systemd/system/bluetooth.service.d/steamframe.conf
/etc/steamframe/bt-fixups.sh /etc/atomic-update.conf.d/frametop-bluetooth.conf)
step() { printf '\n\033[1m== %s\033[0m\n' "$*"; }
run() { # run a command, or with --dry-run, show it
if [ "$dry" = 1 ]; then printf ' would run: %s\n' "$*"; else "$@"; fi
}
ask() { # ask "question" default(y|n)
local hint answer
if [ "$dry" = 1 ]; then echo "$1 [dry run: yes]"; return 0; fi
hint=$([ "$2" = y ] && echo "Y/n" || echo "y/N")
read -r -p "$1 [$hint] " answer </dev/tty || answer=
answer=${answer:-$2}
[[ $answer =~ ^[Yy] ]]
}
exists() { local f; for f in "$@"; do [ -e "$f" ] && return 0; done; return 1; }
size() { du -shc "$@" 2>/dev/null | tail -1 | cut -f1; }
# Is this folder Frametop's code? Only then is it deleted.
is_repo() { [ "$1" != "$HOME" ] && [ -f "$1/desktops.sh" ] && [ -f "$1/session/frametop-session.sh" ]; }
find_repo() {
local p
[ -n "$dir" ] && { echo "$dir"; return; }
# The launcher entry and the relay's unit point into the repo, until step 1 removes them.
p=$(sed -n 's#^Exec=\(.*\)/session/frametop-session\.sh.*#\1#p' "$override" 2>/dev/null | head -1)
[ -z "$p" ] && p=$(sed -n 's#^ExecStart=/usr/bin/python3 \(.*\)/input/input-relay\.py.*#\1#p' "$relay_unit" 2>/dev/null | head -1)
[ -z "$p" ] && [ -f "${BASH_SOURCE[0]:-}" ] && p=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
echo "${p:-$HOME/frametop}"
}
# Frametop's programs that are running now (step 1 leaves them running until the restart).
running() {
local n out=()
for n in ft-screens ft-pointer ft-powerd ft-eyegrab ft-camd ft-hands ft-voiced whisper-server; do
pgrep -x "$n" >/dev/null && out+=("$n")
done
pgrep -f '[i]nput/input-relay\.py' >/dev/null && out+=("input relay")
pgrep -f '[g]aze/ft-gazed' >/dev/null && out+=("gaze service")
pgrep -f '[f]rametop-session\.sh' >/dev/null && out+=("desktop session")
echo "${out[*]}"
}
main() {
local units=() entries=() names=() sys=0 repo now f
while [ $# -gt 0 ]; do
case $1 in
--dir) dir=${2:?--dir needs a folder}; shift ;;
--dry-run) dry=1 ;;
-h|--help) usage; return 0 ;;
*) echo "unknown option: $1" >&2; usage >&2; return 2 ;;
esac
shift
done
if ! { grep -qx 'ID=steamos' /etc/os-release && grep -qE '^VARIANT_ID="?vr"?$' /etc/os-release; } 2>/dev/null; then
echo "This uninstalls Frametop from a Steam Frame (SteamOS, VR variant). Run it in a terminal on the headset." >&2
return 1
fi
if [ "$dry" = 0 ] && ! { : </dev/tty; } 2>/dev/null; then
echo "This asks questions, and there's no terminal to ask in. Run it in one (over SSH: ssh -t)." >&2
return 1
fi
[ "$dry" = 1 ] && echo "Dry run: nothing changes."
repo=$(find_repo)
camd=$repo/hands/build/ft-camd
camd_caps=$(getcap "$camd" 2>/dev/null || true)
# Step 1: what makes Frametop start. Nothing here stops a running program.
[ -f "$override" ] && grep -q 'Frametop' "$override" || override=
[ -f "$native_copy" ] || native_copy=
units=("$HOME"/.config/systemd/user/frametop-*.service)
for f in ft-input-settings ft-display-settings ft-remote-displays ft-layout-reset ft-screens-toggle ft-remote-settings ft-gazeprobe; do
[ -e "$apps/$f.desktop" ] && entries+=("$apps/$f.desktop")
done
[ -e "$apps/frametop-handrec.desktop" ] && entries+=("$apps/frametop-handrec.desktop")
entries+=("$apps"/frametop-profile-*.desktop) # one per layout profile (layout/ft_layout.py)
[ -L "$handsctl" ] || handsctl=
[ -f "$eye_rate" ] && [ -f "$repo/gaze/eye-rate.py" ] || eye_rate=
exists "${eyegrab_files[@]}" "${bt_files[@]}" && sys=1
if [ -n "$override" ] || [ -n "$native_copy" ] || [ ${#units[@]} -gt 0 ] || [ ${#entries[@]} -gt 0 ] ||
[ -d "$driver" ] || [ -n "$handsctl" ] || [ "$sys" = 1 ] || [ -n "$camd_caps" ] || [ -n "$eye_rate" ]; then
step "Step 1 of 2: stop Frametop from starting"
echo "This removes:"
[ -n "$override" ] && echo " - the launcher's Desktop entry (Launch a program -> Desktop opens the stock desktop again)"
[ -n "$native_copy" ] && echo " - the launcher's Native Desktop entry (Desktop opens the same stock desktop then)"
for f in "${units[@]}"; do echo " - the service $(basename "$f")"; done
[ -d "$driver" ] && echo " - the 3D mouse's SteamVR driver (ft_pointer)"
[ ${#entries[@]} -gt 0 ] && echo " - ${#entries[@]} menu entries (Frametop Display Settings, Input Settings, ...)"
[ -n "$handsctl" ] && echo " - $handsctl"
[ -n "$eye_rate" ] && echo " - gaze mode's SteamVR setting for 90 fps eye cameras (steamvr.gazeDepEyeToHeadEnable)"
[ -n "$camd_caps" ] && echo " - the hand camera broker's file capabilities (needs your password)"
exists "${eyegrab_files[@]}" && echo " - our eye tracker's frame grabber (a system service: needs your password)"
exists "${bt_files[@]}" && echo " - the Bluetooth fixes (system files: needs your password)"
echo "What runs now keeps running until you restart the headset, so your keyboard, mouse, and"
echo "this terminal keep working. Your settings and the code stay for now."
ask "Uninstall Frametop?" n || { echo "Nothing changed."; return 0; }
[ -n "$override" ] && run rm -f "$override"
[ -n "$native_copy" ] && run rm -f "$native_copy"
if [ ${#units[@]} -gt 0 ]; then
for f in "${units[@]}"; do names+=("$(basename "$f")"); done
run systemctl --user disable "${names[@]}" 2>/dev/null || true
run rm -f "${units[@]}"
run systemctl --user daemon-reload
fi
if [ -d "$driver" ]; then
if [ -x "$vrpathreg" ]; then
LD_LIBRARY_PATH=$(dirname "$vrpathreg") run "$vrpathreg" removedriver "$driver" ||
echo "warning: SteamVR's vrpathreg couldn't unregister the driver; SteamVR may log that it's missing" >&2
fi
run rm -rf "$driver"
fi
[ ${#entries[@]} -gt 0 ] && run rm -f "${entries[@]}"
[ -n "$handsctl" ] && run rm -f "$handsctl"
[ -n "$eye_rate" ] && { run python3 "$repo/gaze/eye-rate.py" off ||
echo "warning: SteamVR's eye camera setting stays on. Turn it off with: python3 $repo/gaze/eye-rate.py off" >&2; }
if [ "$sys" = 1 ]; then
echo "The system files need your password (sudo)."
# $1 is ft-camd when it has capabilities to remove, else empty; the system files follow.
if ! run sudo bash -c '
camd=$1; shift
for u in frametop-eyegrab steamframe-bt-fixups; do systemctl disable $u.service 2>/dev/null; done
rm -f "$@"
rmdir /etc/frametop /etc/steamframe /etc/systemd/system/bluetooth.service.d 2>/dev/null
[ -n "$camd" ] && [ -x "$camd" ] && setcap -r "$camd" 2>/dev/null
systemctl daemon-reload; true' sys "${camd_caps:+$camd}" "${eyegrab_files[@]}" "${bt_files[@]}"; then
echo "warning: the system files weren't removed (no password?). Run this again to retry." >&2
fi
elif [ -n "$camd_caps" ]; then
echo "ft-camd's file capabilities need your password (sudo) to remove."
run sudo setcap -r "$camd" ||
echo "warning: ft-camd keeps its capabilities (no password?). Run this again to retry." >&2
fi
[ "$dry" = 1 ] || echo "Frametop no longer starts."
fi
# Step 2: delete what's left, once nothing of Frametop runs.
now=$(running)
if [ -n "$now" ]; then
step "Restart the headset to finish"
echo "Still running from before: $now."
echo "They stop when the headset restarts. After that, run this again to delete the code"
echo "($repo) and, if you want, your settings and the build container:"
echo
if [ "$repo" = "$HOME/frametop" ]; then
echo " curl -fsSL https://frametop.github.io/frametop/uninstall.sh | bash"
else
echo " curl -fsSL https://frametop.github.io/frametop/uninstall.sh | bash -s -- --dir $(printf %q "$repo")"
fi
echo
if ask "Restart the headset now? This closes everything open, in VR and on the desktop." n; then
run systemctl reboot || echo "Couldn't restart it from here: restart the headset from Steam's power menu." >&2
fi
[ "$dry" = 1 ] || return 0
echo "Dry run: after the restart, step 2 would go like this."
fi
step "Step 2 of 2: delete what's left"
if [ -d "$releases" ]; then
local boxes=() images=() b
for f in "$releases"/*/.frametop-release; do
boxes+=("$(sed -n 's/^BOX=//p' "$f")")
images+=("$(sed -n 's/^IMAGE=//p' "$f")")
done
echo "Frametop's releases: their files in $releases ($(size "$releases")), and the container and"
echo "image of each (${#boxes[@]}; an image is about 3 GB). A downloaded Frametop.zip stays where it is."
if ask "Delete the releases, their containers, and their images?" y; then
for b in "${boxes[@]}"; do
[[ $b =~ ^frametop-[A-Za-z0-9_.-]+$ ]] && { run podman rm -f "$b" 2>/dev/null || true; }
done
for b in "${images[@]}"; do
[[ $b == localhost/frametop:* ]] && { run podman rmi "$b" 2>/dev/null || true; }
done
run rm -rf "$releases"
fi
fi
if [[ $repo == "$releases"/* ]]; then
: # a release: above
elif [ -d "$repo" ] && is_repo "$repo"; then
if [ -f "$repo/.git" ]; then
echo "Leaving $repo: it's a git worktree. Remove it with git worktree remove."
else
local def=y
if [ -n "$(git -C "$repo" status --porcelain --untracked-files=no 2>/dev/null)" ] ||
[ -n "$(git -C "$repo" log --branches --not --remotes --oneline 2>/dev/null | head -1)" ]; then
echo "$repo has changes of its own (git -C $repo status)."
def=n
fi
if ask "Delete the Frametop code in $repo ($(size "$repo"))?" "$def"; then
cd "$HOME"
run rm -rf "$repo"
fi
fi
elif [ -e "$repo" ]; then
echo "Leaving $repo: it doesn't look like Frametop's code."
fi
# Rebuilt by the desktop at each start, so nothing to keep.
run rm -rf "$HOME/.local/share/frametop/apps" "$HOME/.local/share/kwin/decorations/kwin4_decoration_qml_frametop" \
"$HOME/.cache/frametop"
local settings=("$HOME"/.config/frametop.conf* "$HOME"/.config/frametop-*.json* "$HOME/.config/frametop-remote"
"$HOME/.config/frametop" "$HOME/.local/state/frametop" "$HOME/.local/share/frametop-stream")
local kept=()
for f in "${settings[@]}"; do [ -e "$f" ] && kept+=("$f"); done
if [ ${#kept[@]} -gt 0 ]; then
echo "Your settings: the screen layout and profiles, button maps, gaze calibration, the remote"
echo "desktop password, the remote displays' pairings and sign-ins (their computers keep Frametop's"
echo "clients and token until you remove them in their Web UI), and the Frametop desktop's own Plasma"
echo "setup (${kept[*]/#$HOME/\~})."
ask "Delete your settings too? Keep them to pick up where you left off if you reinstall." n &&
run rm -rf "${kept[@]}"
fi
# Frametop Voice's speech models (voice/models.sh): downloaded again by an install.
if [ -d "$HOME/.local/share/frametop/voice" ]; then
ask "Delete voice dictation's speech models ($(size "$HOME/.local/share/frametop/voice"); an install downloads them again)?" y &&
run rm -rf "$HOME/.local/share/frametop/voice"
fi
local recs=()
for f in "$HOME/.local/share/frametop/eyes" "$HOME/.local/share/frametop/hands"; do [ -e "$f" ] && recs+=("$f"); done
if [ ${#recs[@]} -gt 0 ]; then
ask "Delete your eye and hand recordings in ~/.local/share/frametop ($(size "${recs[@]}"))?" n &&
run rm -rf "${recs[@]}"
fi
[ "$dry" = 1 ] || rmdir "$HOME/.local/share/frametop" 2>/dev/null || true
if command -v podman >/dev/null && podman container exists dev 2>/dev/null; then
echo "The build container (dev) holds Frametop's compilers and libraries, 1-2 GB. If you put"
echo "anything else in it, that goes too."
if ask "Delete the build container?" n; then
run "$HOME/.local/bin/distrobox" rm --force dev </dev/null
run podman image rm registry.fedoraproject.org/fedora-toolbox:44 >/dev/null 2>&1 || true
echo "distrobox stays in ~/.local/bin, for any other containers. To remove it too:"
echo " ~/dev/src/distrobox/uninstall --prefix ~/.local"
fi
fi
step "Done"
echo "Frametop is uninstalled."
}
dir=
main "$@"