#!/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 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/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 <