Files
jasonwitty 9226224507 Set window options per window; label panes where programs cannot clobber them
Two bugs found deploying to the LattePanda, both of which v1 also had.

Window options do not propagate from the session and new windows do not
inherit them. `set-option -t <session> pane-border-status` quietly applies to
whichever window happens to be current, so only ONE window ever got pane
borders -- v1's rack display has had unlabeled borders on two of its three
windows this whole time and nobody noticed, because the window that got them
was the one usually on screen. This is the same trap that makes remain-on-exit
useless here. pane-border-status, pane-border-format and allow-rename are now
set per window in place(), and a test asserts every window has them.

Pane labels no longer use `select-pane -T`. The pane *title* is writable by
whatever runs in the pane: unifly probes for Kitty graphics support on startup
and tmux consumed part of that probe as a title change, so the border read
"Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA" instead of "unifly". Labels now live in a
pane-scoped user option, @socktop_label, which no escape sequence can reach;
pane-border-format falls back to the title if it is somehow unset.

Also: `terminal:` now expands a leading ~/ like `binaries:` already did. A
window manager's PATH rarely includes ~/.cargo/bin, so a full path is the usual
answer there and should not have to be spelled out longhand.

Build cost corrected from guesses to measurements on the LattePanda (Atom
x5-Z8350, 4 cores, 1.9 GB, no swap, toolchain already present): 108 seconds,
peak 1.1 GB, 103 MB target directory, 946 KB binary. The README said twenty
minutes and the installer budgeted 600 MB; both were wrong.

Integration tests now use one tmux session name each -- cargo runs them in
parallel and they were tearing down each other's server state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 13:24:16 -07:00

441 lines
15 KiB
Bash
Executable File

#!/bin/sh
# socktop-swipe installer.
#
# curl -fsSL https://gt.wittyoneoff.com/jason/socktop-swipe/raw/branch/main/install.sh | sh
#
# or, from a checkout:
#
# ./install.sh
#
# Everything is checked BEFORE anything is installed, and the full cost is
# printed once. Nothing touches the system before you answer the summary.
#
# --yes accept every default without asking
# --uninstall hand over to uninstall.sh
# --prefix DIR install to DIR/bin instead of /usr/local/bin
set -eu
REPO=https://gt.wittyoneoff.com/jason/socktop-swipe
PREFIX=/usr/local
ASSUME_YES=no
SRC=""
for a in "$@"; do
case "$a" in
--yes | -y) ASSUME_YES=yes ;;
--prefix=*) PREFIX=${a#--prefix=} ;;
--uninstall) exec sh -c "$(dirname "$0")/uninstall.sh" ;;
-h | --help) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown option: $a" >&2; exit 2 ;;
esac
done
BIN="$PREFIX/bin"
CONF_DIR="$HOME/.config/socktop-swipe"
CONF="$CONF_DIR/config.yaml"
UDEV=/etc/udev/rules.d/70-socktop-swipe.rules
XBLANK=/etc/X11/xorg.conf.d/10-no-blanking-socktop-swipe.conf
LIGHTDM=/etc/lightdm/lightdm.conf.d/50-autologin-socktop-swipe.conf
# --------------------------------------------------------------------------
# Asking
# --------------------------------------------------------------------------
# Piping into sh leaves stdin holding the SCRIPT, not a keyboard, so prompts
# must read the terminal directly. With no terminal at all we take defaults and
# say so rather than silently guessing.
if [ -r /dev/tty ] && [ -t 1 ]; then HAVE_TTY=yes; else HAVE_TTY=no; fi
say() { printf '%s\n' "$*"; }
step() { printf '\n==> %s\n' "$*"; }
warn() { printf ' !! %s\n' "$*" >&2; }
# ask "question" default(y|n) -> 0 for yes
ask() {
_q=$1
_d=$2
if [ "$ASSUME_YES" = yes ] || [ "$HAVE_TTY" = no ]; then
[ "$_d" = y ] && return 0 || return 1
fi
[ "$_d" = y ] && _hint="[Y/n]" || _hint="[y/N]"
while :; do
printf ' %s %s ' "$_q" "$_hint" >/dev/tty
read -r _a </dev/tty || _a=""
[ -z "$_a" ] && _a=$_d
case "$_a" in
[Yy] | [Yy][Ee][Ss]) return 0 ;;
[Nn] | [Nn][Oo]) return 1 ;;
esac
done
}
# askval "question" "default" -> echoes the answer
askval() {
if [ "$ASSUME_YES" = yes ] || [ "$HAVE_TTY" = no ]; then
printf '%s' "$2"
return
fi
printf ' %s [%s] ' "$1" "$2" >/dev/tty
read -r _a </dev/tty || _a=""
[ -z "$_a" ] && _a=$2
printf '%s' "$_a"
}
have() { command -v "$1" >/dev/null 2>&1; }
# --------------------------------------------------------------------------
# Preflight
# --------------------------------------------------------------------------
say "socktop-swipe installer"
say "======================="
OS_NAME=$(. /etc/os-release 2>/dev/null && printf '%s' "${PRETTY_NAME:-unknown}" || true)
[ -n "$OS_NAME" ] || OS_NAME=unknown
ARCH=$(uname -m)
PKG=""
PKG_INSTALL=""
for p in apt-get dnf pacman zypper apk; do
if have $p; then
PKG=$p
break
fi
done
case "$PKG" in
apt-get) PKG_INSTALL="sudo apt-get install -y" ;;
dnf) PKG_INSTALL="sudo dnf install -y" ;;
pacman) PKG_INSTALL="sudo pacman -S --needed --noconfirm" ;;
zypper) PKG_INSTALL="sudo zypper install -y" ;;
apk) PKG_INSTALL="sudo apk add" ;;
esac
# Display manager and window manager, for the autostart and autologin steps.
# NB: written as `if`, not `[ x ] && y`. Under `set -e` an AND-OR list that
# ends up false aborts the script, so a box with no lightdm would exit here.
DM=none
if [ -d /etc/lightdm ]; then DM=lightdm
elif [ -d /etc/gdm3 ] || [ -d /etc/gdm ]; then DM=gdm
elif [ -f /etc/sddm.conf ] || [ -d /etc/sddm.conf.d ]; then DM=sddm
fi
WM=none
if have i3; then WM=i3; fi
TERMINAL=""
for t in alacritty foot kitty xterm; do
if have $t; then
TERMINAL=$t
break
fi
done
LOCKER=""
for l in xss-lock light-locker xscreensaver gnome-screensaver; do
if have $l; then
LOCKER="$LOCKER $l"
fi
done
# Space for the toolchain and the build. Both land under $HOME unless
# CARGO_HOME says otherwise, which is exactly the 8GB-of-eMMC case.
avail_mb() { df -Pm "$1" 2>/dev/null | awk 'NR==2 {print $4}'; }
HOME_FREE=$(avail_mb "$HOME")
PREFIX_FREE=$(avail_mb "$(dirname "$PREFIX")")
step "Found"
say " system $OS_NAME ($ARCH)"
say " packages ${PKG:-none detected}"
say " display mgr $DM"
say " window mgr $WM"
say " terminal ${TERMINAL:-none found}"
say " tmux $(tmux -V 2>/dev/null || echo 'NOT INSTALLED')"
# Not `cargo --version | cut ... || echo`: the pipeline's status is cut's, and
# cut succeeds on empty input, so the fallback would never fire.
if have cargo; then
say " cargo $(cargo --version 2>/dev/null | cut -d' ' -f1-2)"
else
say " cargo NOT INSTALLED"
fi
say " git $(git --version 2>/dev/null || echo 'NOT INSTALLED')"
say " free space ${HOME_FREE:-?} MB in \$HOME, ${PREFIX_FREE:-?} MB on $PREFIX"
if [ -n "$LOCKER" ]; then say " screen locker $LOCKER"; fi
# What we are about to need.
NEED_PKGS=""
NEED_RUSTUP=no
COST_MB=10
if ! have tmux; then NEED_PKGS="$NEED_PKGS tmux"; fi
if ! have git; then NEED_PKGS="$NEED_PKGS git"; fi
if ! have cargo; then
NEED_RUSTUP=yes
COST_MB=$((COST_MB + 1200))
fi
# The build itself. Measured on an Atom x5-Z8350: a 103 MB target directory,
# 108 seconds, peaking at 1.1 GB of RAM. 250 leaves headroom.
COST_MB=$((COST_MB + 250))
if [ -z "$TERMINAL" ]; then
warn "no terminal emulator found. socktop-swipe can attach in an existing"
warn "terminal, but unattended autostart needs one. alacritty is the tested choice."
fi
if [ -z "$PKG" ]; then
warn "no supported package manager. Nothing will be installed for you;"
warn "make sure tmux, git and a Rust toolchain are present."
fi
if [ -n "$HOME_FREE" ] && [ "$HOME_FREE" -lt "$COST_MB" ]; then
warn "only ${HOME_FREE} MB free in \$HOME but about ${COST_MB} MB is needed."
warn "A source build will not fit. Free space first, or set CARGO_HOME and"
warn "CARGO_TARGET_DIR to somewhere with room."
fi
step "This will"
if [ -n "$NEED_PKGS" ]; then say " install packages:$NEED_PKGS"; fi
if [ "$NEED_RUSTUP" = yes ]; then say " install the Rust toolchain via rustup (~1.2 GB)"; fi
say " build socktop-swipe from source (~100 MB of build artifacts)"
say " install the binary to $BIN"
say " write a starter config to $CONF"
say " ...then ask about the touch device, autostart, autologin and blanking."
say ""
say " About ${COST_MB} MB of disk. Nothing has been changed yet."
say ""
if [ "$HAVE_TTY" = no ]; then
say "No terminal available, so every question takes its default."
say "Run the script directly for the interactive version:"
say " curl -fsSLO $REPO/raw/branch/main/install.sh && sh install.sh"
say ""
fi
ask "Continue?" y || { say "Nothing done."; exit 0; }
# --------------------------------------------------------------------------
# Dependencies
# --------------------------------------------------------------------------
if [ -n "$NEED_PKGS" ]; then
step "Installing:$NEED_PKGS"
if [ -z "$PKG_INSTALL" ]; then
warn "no package manager -- install$NEED_PKGS yourself and re-run."
exit 1
fi
if [ "$PKG" = apt-get ]; then sudo apt-get update; fi
# shellcheck disable=SC2086
$PKG_INSTALL $NEED_PKGS
fi
if [ "$NEED_RUSTUP" = yes ]; then
step "Installing the Rust toolchain"
if ask "Install rustup now?" y; then
curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --no-modify-path
# shellcheck disable=SC1091
. "$HOME/.cargo/env"
else
say "Install Rust yourself, then re-run this script."
exit 1
fi
fi
if ! have cargo; then warn "cargo still not on PATH -- open a new shell and re-run."; exit 1; fi
# --------------------------------------------------------------------------
# Source
# --------------------------------------------------------------------------
here=$(CDPATH='' cd -- "$(dirname -- "$0")" 2>/dev/null && pwd || echo "")
if [ -n "$here" ] && [ -f "$here/Cargo.toml" ]; then
SRC=$here
step "Building from this checkout: $SRC"
else
SRC="$HOME/.local/src/socktop-swipe"
step "Fetching the source to $SRC"
mkdir -p "$(dirname "$SRC")"
if [ -d "$SRC/.git" ]; then
git -C "$SRC" pull --ff-only
else
git clone "$REPO" "$SRC"
fi
fi
step "Building (this is the slow part on a low-power box)"
( cd "$SRC" && cargo build --release )
sudo install -d "$BIN"
sudo install -m 755 "$SRC/target/release/socktop-swipe" "$BIN/socktop-swipe"
say " installed $BIN/socktop-swipe"
# --------------------------------------------------------------------------
# Touch device
# --------------------------------------------------------------------------
step "Touch panel"
DEVICE=""
# Prefer the by-id path: eventN numbers get reshuffled on reboot.
for d in /dev/input/by-id/*event*; do
[ -e "$d" ] || continue
if udevadm info --query=property --name="$d" 2>/dev/null | grep -q '^ID_INPUT_TOUCHSCREEN=1'; then
say " found touchscreen: $d"
if [ -z "$DEVICE" ]; then DEVICE=$d; fi
fi
done
if [ -z "$DEVICE" ]; then
warn "no touchscreen found under /dev/input/by-id/."
warn "If the panel is not plugged in yet, that is fine -- set touch.device"
warn "in the config later. 'socktop-swipe doctor --list' will show it."
DEVICE=/dev/input/by-id/CHANGE-ME
else
DEVICE=$(askval "Use which device?" "$DEVICE")
fi
step "Device access"
say " Reading touch events needs permission on the device."
say " A udev rule grants it for touchscreens only and takes effect at once."
say " The 'input' group grants access to every input device, keyboard included,"
say " and needs a full logout."
RELOGIN=no
if ask "Install the udev rule?" y; then
sudo install -m 644 "$SRC/packaging/70-socktop-swipe.rules" "$UDEV"
sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=input
say " wrote $UDEV"
if ! id -nG | tr ' ' '\n' | grep -qx input; then
warn "the rule uses GROUP=input and you are not in it."
if ask "Add $(id -un) to the 'input' group as well?" y; then
sudo usermod -aG input "$(id -un)"
RELOGIN=yes
fi
fi
elif ask "Add $(id -un) to the 'input' group instead?" n; then
sudo usermod -aG input "$(id -un)"
RELOGIN=yes
fi
# --------------------------------------------------------------------------
# Config
# --------------------------------------------------------------------------
step "Configuration"
mkdir -p "$CONF_DIR"
if [ -e "$CONF" ]; then
say " keeping the existing $CONF"
else
sed "s|^ device: .*| device: $DEVICE|" "$SRC/config.example.yaml" >"$CONF"
say " wrote $CONF"
say " EDIT IT: the example screens are the author's rack, not yours."
fi
# --------------------------------------------------------------------------
# Optional monitors
# --------------------------------------------------------------------------
step "Monitor programs (all optional -- socktop-swipe runs whatever you configure)"
if ! have socktop && ask "Install socktop (the point of the project)?" y; then
cargo install socktop
fi
if ! have uptime-kuma-status && ask "Install uptime-kuma-status?" n; then
cargo install uptime-kuma-status
fi
if ! have unifly && ask "Build unifly (UniFi TUI) from source?" n; then
U="$HOME/.local/src/unifly"
mkdir -p "$(dirname "$U")"
if [ -d "$U/.git" ]; then git -C "$U" pull --ff-only; else
git clone https://github.com/jasonwitty/unifly "$U"
fi
( cd "$U" && cargo build --profile release-small -p unifly )
say " built $U/target/release-small/unifly"
say " put that path under 'binaries: unifly:' in $CONF"
fi
# --------------------------------------------------------------------------
# Unattended operation
# --------------------------------------------------------------------------
step "Wall-display behaviour"
if ask "Stop the screen blanking, sleeping and locking?" y; then
sudo mkdir -p /etc/X11/xorg.conf.d
sudo tee "$XBLANK" >/dev/null <<-'EOF'
# Installed by socktop-swipe. This is a wall display: it must never blank.
Section "ServerFlags"
Option "BlankTime" "0"
Option "StandbyTime" "0"
Option "SuspendTime" "0"
Option "OffTime" "0"
EndSection
EOF
say " wrote $XBLANK"
# The Xorg snippet covers every session including the greeter and survives
# reboots; xset covers a session that re-enables blanking after X starts.
# Both are needed, and xset MUST run as the session user -- under sudo it
# targets root's X connection and silently does nothing.
if [ -n "${DISPLAY:-}" ] && have xset; then
xset s off s noblank -dpms 2>/dev/null || true
say " applied to the running session"
fi
for l in $LOCKER; do
warn "$l is installed and may still lock the screen. Disable it in your"
warn "session's autostart."
done
fi
if [ "$WM" = i3 ] && [ -e "$HOME/.config/i3/config" ]; then
if ask "Add the i3 autostart lines?" y; then
i3conf="$HOME/.config/i3/config"
if grep -q socktop-swipe "$i3conf"; then
say " already present; leaving it alone"
else
bak="$i3conf.bak-$(date +%Y%m%d-%H%M%S)"
cp "$i3conf" "$bak"
{
printf '\n# --- socktop-swipe ---\n'
printf '# One process: builds the session, opens it in a terminal and reads the panel.\n'
printf 'exec --no-startup-id %s/socktop-swipe run\n' "$BIN"
printf '# Never blank (the Xorg snippet covers reboots; this covers this session).\n'
printf 'exec --no-startup-id xset s off s noblank -dpms\n'
if have unclutter; then
printf '# With the panel grabbed, nothing ever moves the pointer, so hide it.\n'
printf 'exec --no-startup-id unclutter --timeout 1 --ignore-scrolling\n'
fi
} >>"$i3conf"
if i3 -C -c "$i3conf" >/dev/null 2>&1; then
say " added the autostart (backup: $bak)"
else
cp "$bak" "$i3conf"
warn "i3 rejected the config; restored $bak"
fi
fi
fi
elif [ "$DM" != none ] || [ "$WM" != none ]; then
say " no i3 config found. For a systemd user session instead:"
say " cp $SRC/packaging/socktop-swipe.service ~/.config/systemd/user/"
say " systemctl --user enable --now socktop-swipe"
fi
if [ "$DM" = lightdm ]; then
say ""
say " Autologin makes the display come back by itself after a power cut."
say " ANYONE WITH PHYSICAL ACCESS TO THE PANEL GETS THIS USER'S SESSION."
say " Only do this where the account is nothing but the dashboard."
if ask "Log $(id -un) straight into $WM at boot?" n; then
sudo mkdir -p /etc/lightdm/lightdm.conf.d
sudo tee "$LIGHTDM" >/dev/null <<-EOF
# Installed by socktop-swipe. Delete this file to restore the login prompt.
[Seat:*]
autologin-user=$(id -un)
autologin-user-timeout=0
autologin-session=$WM
EOF
say " wrote $LIGHTDM (takes effect at next boot)"
fi
elif [ "$DM" != none ]; then
say " Autologin is only automated for lightdm; yours is $DM. Configure it there"
say " if the display must come back unattended after a power cut."
fi
# --------------------------------------------------------------------------
say ""
say "=============================================================="
say "Installed."
if [ "$RELOGIN" = yes ]; then say "LOG OUT AND BACK IN before use (group membership changed)."; fi
cat <<EOF
Next:
1. Edit $CONF -- the screens are the author's, not yours.
2. socktop-swipe validate check it, and see the grid map
3. socktop-swipe doctor swipe, and see what the panel reports
4. socktop-swipe run build the session and start swiping
EOF
say "=============================================================="