9226224507
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>
441 lines
15 KiB
Bash
Executable File
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 "=============================================================="
|