Add Linux/Raspberry Pi setup and port the clicker to X11

The clicker had no Linux support at all: pygetwindow has no X11 backend, so
_other_activate returned "window management is unsupported" and every step
failed before it could click. focus.py now has a third backend that raises the
browser with xdotool and reads the focused window's WM_CLASS to verify it came
forward - the same activate-then-confirm shape as the macOS and Windows paths.

setup-linux.sh mirrors setup-windows.bat with apt instead of winget. Three
things are specific to this platform rather than incidental:

- Node comes from NodeSource. Pi OS ships one too old for Next 16, and the LTS
  line is also what better-sqlite3 publishes prebuilt arm64 binaries for.
- Python dependencies go in a virtualenv. Pi OS Bookworm enforces PEP 668, so
  pip into the system interpreter fails with externally-managed-environment.
- Autostart uses an XDG ~/.config/autostart entry, the direct analogue of the
  Windows Startup folder: no sudo, and it runs inside the graphical session,
  which the clicker needs for DISPLAY.

Wayland is called out in four places - the session guard, diagnose.py, the
installer and both READMEs - because Pi OS on a Pi 5 defaults to it and the
clicker simply cannot work there. Wayland does not let one client synthesise
input into another, so this is a switch-to-X11 situation, not a bug to fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-08-30 18:53:25 -05:00
co-authored by Claude Opus 5
parent 3cc632ac40
commit c5c946d3cc
9 changed files with 436 additions and 5 deletions
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
#
# Register AutoFirmer to start at login and keep itself updated. Linux/Pi
# counterpart of install-autostart.ps1 - the same two mechanisms, expressed the
# way this desktop does them:
#
# * a ~/.config/autostart entry runs scripts/start-all.sh at login
# * PM2's own cron restart drives the update checks
#
# No sudo, nothing system-wide. Everything runs inside the graphical login
# session, which is not incidental: the clicker synthesises real X11 input and
# needs a desktop with DISPLAY set. A systemd system service has neither.
#
# ./scripts/install-autostart.sh [--interval-minutes 5] [--skip-clicker]
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
INTERVAL=5
SKIP_CLICKER=0
while [ $# -gt 0 ]; do
case "$1" in
--interval-minutes) INTERVAL="$2"; shift 2 ;;
--skip-clicker) SKIP_CLICKER=1; shift ;;
*) echo "unknown option: $1" >&2; exit 1 ;;
esac
done
info() { printf ' %s\n' "$1"; }
warn() { printf ' ! %s\n' "$1" >&2; }
info "project root: $ROOT"
# ── PM2 ─────────────────────────────────────────────────────────────────────
if ! command -v pm2 >/dev/null 2>&1; then
info 'installing PM2 globally...'
npm install -g pm2
command -v pm2 >/dev/null 2>&1 || { echo "PM2 installed but not on PATH" >&2; exit 1; }
fi
info "pm2: $(command -v pm2)"
cd "$ROOT"
# Run Next directly rather than through `npm start`, so PM2 supervises the
# server itself instead of an npm wrapper that spawns it.
NEXT_BIN="$ROOT/node_modules/next/dist/bin/next"
[ -f "$NEXT_BIN" ] || { echo "next not found at $NEXT_BIN - run npm install first" >&2; exit 1; }
pm2 delete autofirmer >/dev/null 2>&1 || true
pm2 start "$NEXT_BIN" --name autofirmer --interpreter node -- start
info 'pm2: autofirmer registered'
if [ "$SKIP_CLICKER" -eq 0 ]; then
PY_CMD_FILE="$ROOT/scripts/.python-cmd"
PY_CMD="$( [ -f "$PY_CMD_FILE" ] && cat "$PY_CMD_FILE" || echo python3 )"
PY_EXE="$($PY_CMD -c 'import sys;print(sys.executable)' 2>/dev/null || true)"
if [ -n "$PY_EXE" ] && [ -x "$PY_EXE" ]; then
pm2 delete clicker >/dev/null 2>&1 || true
if pm2 start "$ROOT/clicker/runner.py" --name clicker --interpreter "$PY_EXE"; then
info "pm2: clicker registered ($PY_EXE)"
else
warn 'pm2 start clicker failed - the dashboard is unaffected'
fi
else
warn 'python not found - skipping the clicker'
fi
fi
# ── Update checks (PM2 cron) ────────────────────────────────────────────────
pm2 delete autofirmer-update >/dev/null 2>&1 || true
if pm2 start "$ROOT/scripts/update-check.mjs" --name autofirmer-update \
--no-autorestart --cron-restart "*/$INTERVAL * * * *"; then
info "update checks: every $INTERVAL minutes (*/$INTERVAL * * * *)"
else
warn 'could not register the update checker - run scripts/update-check.mjs by hand'
fi
pm2 save >/dev/null
info 'pm2: process list saved'
# ── Start at login (XDG autostart) ──────────────────────────────────────────
# The desktop equivalent of the Windows Startup folder: runs inside the
# graphical session, so DISPLAY is set and the clicker can reach the X server.
AUTOSTART_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/autostart"
DESKTOP_FILE="$AUTOSTART_DIR/autofirmer.desktop"
mkdir -p "$AUTOSTART_DIR"
cat > "$DESKTOP_FILE" <<DESKTOP
[Desktop Entry]
Type=Application
Name=AutoFirmer
Comment=Start the AutoFirmer dashboard, clicker and update checker
Exec=$ROOT/scripts/start-all.sh
Terminal=false
X-GNOME-Autostart-enabled=true
DESKTOP
info "login entry: $DESKTOP_FILE"
# ── Wayland warning ─────────────────────────────────────────────────────────
# Worth saying now rather than letting every automation fail later.
if [ -n "${WAYLAND_DISPLAY:-}" ] && [ -z "${DISPLAY:-}" ]; then
echo
warn 'This is a Wayland session. The dashboard is fine, but the clicker cannot'
warn 'work: xdotool and pyautogui both speak X11, which Wayland does not expose.'
warn 'Switch with: sudo raspi-config -> Advanced Options -> Wayland -> X11, then reboot.'
fi
echo
info 'Done. No sudo was needed and nothing system-wide was changed.'
info 'Running now, and again at every login.'
info "Updates checked every $INTERVAL minutes; see scripts/update.log"
info 'Useful: pm2 list | pm2 logs autofirmer | pm2 logs clicker'
info "To disable autostart: rm $DESKTOP_FILE"