diff --git a/README.md b/README.md index ead79df..902048d 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,19 @@ Swipe-to-zoom touchscreen navigation for a wall-mounted [socktop](https://crates.io/crates/socktop) dashboard. -One tmux window shows every monitored host tiled together. Swiping right-to-left -on the panel zooms into each host full-screen, one at a time; swiping -left-to-right walks back out to the overview. +Hosts are arranged in **groups**. Each group is one tmux window showing its +hosts tiled together. Swiping right-to-left on the panel zooms into each host +full-screen, one at a time, then moves on to the next group's overview; swiping +left-to-right walks back the same way. ``` - overview -> host 1 -> host 2 -> host 3 -> host 4 - (tiled) zoomed zoomed zoomed zoomed - <-------------- swipe left-to-right -------------- + group A -> A1 -> A2 -> A3 -> A4 -> group B -> B1 -> B2 + (quarters) zoomed zoomed zoomed zoomed (stacked) zoomed zoomed + <------------------------- swipe left-to-right ------------------------- ``` +With a single group this is simply overview -> each host -> back. + Built for a 10" touch panel on a server rack driving four Raspberry Pis, on a low-power x86 box running i3 on X11. @@ -63,6 +66,7 @@ fresh session. | `--xignore` | Also tell X to ignore the touch panel (see below — usually wanted) | | `--i3` | Also add i3 autostart lines, with a backup and `i3 -C` validation | | `--noblank` | Also stop the screen blanking / DPMS power-down (wall displays) | +| `--autologin` | Also make lightdm log this user straight into i3 at boot (see [security note](#related-does-the-display-come-back-by-itself)) | Re-running is safe: an existing config file is never overwritten, and the i3 edit is skipped if already present. @@ -92,7 +96,8 @@ blame the config change. | Option | Default | What it does | | --- | --- | --- | -| `SOCKTOP_HOSTS` | four Pi profiles | socktop profile names, **in swipe order**. Any count works; layout and carousel adapt. | +| `SOCKTOP_GROUPS` | Pis quartered `;` Orange Pi + Debian stacked | Groups separated by `;`, each a list of socktop profile names **in swipe order**, plus an optional `@layout` (`tiled` default, `even-vertical` stacks, `even-horizontal` side-by-side, `main-*` also work). Any count of groups and hosts works. | +| `SOCKTOP_HOSTS` | empty | Legacy single-group form; used only when `SOCKTOP_GROUPS` is empty | | `SOCKTOP_SESSION` | `socktop4` | tmux session name | | `SOCKTOP_BIN` | `$HOME/.cargo/bin/socktop` | Full path — i3 does not have `~/.cargo/bin` on `PATH` | | `TOUCH_DEV` | ILITEK by-id path | Touch device. **Always use a `/dev/input/by-id/` path**; `eventN` numbers change on reboot. | @@ -104,8 +109,14 @@ blame the config change. ### Adding or removing hosts -Edit `SOCKTOP_HOSTS` and run `socktop-rack`. Nothing else needs touching — the -carousel derives its length from the pane count at runtime. +Edit `SOCKTOP_GROUPS` and run `socktop-rack`. Nothing else needs touching — the +carousel derives its length from the windows and panes at runtime. Every host +named must exist in `~/.config/socktop/profiles.json` on the display host. + +```sh +# four Pis as quarters, then two more boxes stacked one above the other +SOCKTOP_GROUPS="rpi-master rpi-worker-1 rpi-worker-2 rpi-worker-3 ; orangepi trixie @even-vertical" +``` ## Autostart @@ -152,8 +163,11 @@ autostarts. Check with: grep -E '^autologin-(user|session)' /etc/lightdm/lightdm.conf ``` -No output means no autologin. Configuring it is out of scope here — it weakens -the machine's security posture and is a deliberate choice, not a default. +No output means no autologin. `./install.sh --autologin` writes +`/etc/lightdm/lightdm.conf.d/50-autologin-socktop-swipe.conf` for the current +user into the `i3` session. It is opt-in, not a default: **anyone with physical +access to the panel gets that user's session**, so only do it where the display +user is nothing but the dashboard. Delete the file to get the login prompt back. ## Uninstall diff --git a/config.env b/config.env index 914c511..0d6a724 100644 --- a/config.env +++ b/config.env @@ -12,10 +12,24 @@ # What to monitor # --------------------------------------------------------------------------- -# socktop profile names (from ~/.config/socktop/profiles.json), in swipe order. -# The first is what you land on after one right-to-left swipe from the overview. -# Any count works; the tiled layout and the carousel adapt automatically. -SOCKTOP_HOSTS="${SOCKTOP_HOSTS:-rpi-master rpi-worker-1 rpi-worker-2 rpi-worker-3}" +# What to show, as GROUPS separated by ";". Each group is one screen of the +# carousel: a tiled overview of its hosts, then each host zoomed full-screen in +# turn, then on to the next group. Host names are socktop profile names from +# ~/.config/socktop/profiles.json. +# +# overview(A) -> A1 -> A2 -> ... -> overview(B) -> B1 -> B2 -> ... +# +# An optional "@layout" token in a group sets its tmux overview layout +# (tiled, even-horizontal, even-vertical, main-horizontal, main-vertical). +# Default is tiled: 4 hosts become quarters, 2 become side-by-side. +# "@even-vertical" stacks the panes one above the other instead. +# +# The example below is the rack display: the four Pis quartered and zoomable, +# then the Orange Pi and the Debian box stacked and zoomable. +SOCKTOP_GROUPS="${SOCKTOP_GROUPS:-rpi-master rpi-worker-1 rpi-worker-2 rpi-worker-3 ; orangepi trixie @even-vertical}" + +# Legacy single-group form. Used only when SOCKTOP_GROUPS is empty. +SOCKTOP_HOSTS="${SOCKTOP_HOSTS:-}" # tmux session name. Change only if it collides with something else. SOCKTOP_SESSION="${SOCKTOP_SESSION:-socktop4}" diff --git a/install.sh b/install.sh index 82fe635..7357dee 100755 --- a/install.sh +++ b/install.sh @@ -5,7 +5,8 @@ # ./install.sh --xignore ...and tell X to ignore the touch panel # ./install.sh --i3 ...and add i3 autostart lines # ./install.sh --noblank ...and stop the screen blanking -# ./install.sh --xignore --i3 --noblank full wall-display setup +# ./install.sh --autologin ...and make lightdm log this user straight into i3 +# ./install.sh --xignore --i3 --noblank --autologin full wall-display setup # # Safe to re-run: an existing config file is never overwritten, and the i3 edit # is skipped if already present. @@ -17,17 +18,20 @@ ETC="$PREFIX/etc" CONF="$ETC/socktop-swipe.env" XCONF=/etc/X11/xorg.conf.d/99-ignore-touch-socktop-swipe.conf BCONF=/etc/X11/xorg.conf.d/10-no-blanking-socktop-swipe.conf +LCONF=/etc/lightdm/lightdm.conf.d/50-autologin-socktop-swipe.conf SRC="$HOME/src/lisgd" here=$(cd "$(dirname "$0")" && pwd) do_xignore=no do_i3=no do_noblank=no +do_autologin=no for a in "$@"; do case "$a" in --xignore) do_xignore=yes ;; --i3) do_i3=yes ;; --noblank) do_noblank=yes ;; + --autologin) do_autologin=yes ;; *) echo "unknown option: $a" >&2; exit 2 ;; esac done @@ -147,6 +151,33 @@ EOF relogin=yes fi +# --- optional: lightdm autologin -------------------------------------------- +if [ "$do_autologin" = yes ]; then + # A wall display must come back by itself after a power cut. Without this a + # reboot leaves the panel at the greeter with nothing autostarted. + # SECURITY: anyone with physical access to the screen gets this user's + # session. Only use it on a display whose user account is nothing but the + # dashboard. Debian's lightdm-autologin PAM stack needs no extra group. + if [ ! -d /etc/lightdm ]; then + echo "!! /etc/lightdm not found; skipping autologin (not lightdm?)" >&2 + else + session=i3 + [ -e /usr/share/xsessions/$session.desktop ] || + echo "!! /usr/share/xsessions/$session.desktop missing; check autologin-session in $LCONF" >&2 + echo "==> enabling lightdm autologin for $(id -un) into '$session'" + sudo mkdir -p /etc/lightdm/lightdm.conf.d + sudo tee "$LCONF" >/dev/null </dev/null || true -first=1 -for h in $SOCKTOP_HOSTS; do - if [ "$first" = 1 ]; then - tmux new-session -d -s "$SOCKTOP_SESSION" -n rack "$SOCKTOP_BIN -P $h" - first=0 - else - # Split the most recently created pane so creation order == index order. - tmux split-window -t "$WIN" "$SOCKTOP_BIN -P $h" - fi - # Label the pane with the host it is showing. - tmux select-pane -t "$WIN.$(tmux display-message -t "$WIN" -p '#{pane_index}')" -T "$h" -done +# Pane titles are set on the session, so do it up front; windows inherit. +build_group() { + # $1 = window name, remaining args = host names and an optional @layout + name=$1 + shift + layout=tiled + hosts= + for tok in "$@"; do + case "$tok" in + @*) layout=${tok#@} ;; + *) hosts="$hosts $tok" ;; + esac + done + # shellcheck disable=SC2086 + set -- $hosts + [ $# -gt 0 ] || return 0 -tmux select-layout -t "$WIN" tiled -tmux select-pane -t "$WIN.0" + if ! tmux has-session -t "$SOCKTOP_SESSION" 2>/dev/null; then + tmux new-session -d -s "$SOCKTOP_SESSION" -n "$name" "$SOCKTOP_BIN -P $1" + else + tmux new-window -d -t "$SOCKTOP_SESSION" -n "$name" "$SOCKTOP_BIN -P $1" + fi + win="$SOCKTOP_SESSION:$name" + tmux select-pane -t "$win.0" -T "$1" + shift + for h in "$@"; do + # Split the most recently created pane so creation order == index order. + tmux split-window -t "$win" "$SOCKTOP_BIN -P $h" + tmux select-pane -t "$win.$(tmux display-message -t "$win" -p '#{pane_index}')" -T "$h" + done + tmux select-layout -t "$win" "$layout" + tmux select-pane -t "$win.0" +} + +n=0 +old_ifs=$IFS +IFS=';' +for group in $SOCKTOP_GROUPS; do + IFS=$old_ifs + # shellcheck disable=SC2086 + build_group "g$n" $group + n=$((n + 1)) + IFS=';' +done +IFS=$old_ifs + +tmux select-window -t "$SOCKTOP_SESSION:g0" tmux set-option -t "$SOCKTOP_SESSION" pane-border-status top tmux set-option -t "$SOCKTOP_SESSION" pane-border-format ' #{pane_title} ' diff --git a/socktop-swipe b/socktop-swipe index 31c25a1..639ef5d 100755 --- a/socktop-swipe +++ b/socktop-swipe @@ -1,14 +1,15 @@ #!/bin/sh # socktop-swipe next|prev # -# Walks a linear "zoom carousel" over the tiled socktop window: +# Walks a linear "zoom carousel" over the socktop session. Each tmux window is +# one group of hosts; within a group the line is the tiled overview, then each +# pane zoomed full-screen in index order; then the next window's overview: # -# overview <-> pane 0 <-> pane 1 <-> ... <-> pane N-1 -# (all hosts) host 1 host 2 last host +# overview(g0) <-> g0.0 <-> ... <-> g0.N <-> overview(g1) <-> g1.0 <-> ... # -# "next" moves right along that line and stops at the last pane. -# "prev" moves back and stops at the overview. Deliberately no wrap-around: -# on a wall display, wrapping makes it impossible to tell where you are. +# "next" moves right and stops at the last pane of the last window. "prev" +# moves back and stops at the first overview. Deliberately no wrap-around: on +# a wall display, wrapping makes it impossible to tell where you are. # # This is tmux pane zoom, NOT extra socktop instances. All panes keep running # and stay connected while hidden, so swiping back shows current data with no @@ -22,10 +23,16 @@ done dir=${1:?usage: socktop-swipe next|prev} -# Resolve the session's active window to its window id (@N) rather than assuming -# a window name, so this works against any socktop session however it was built. -WIN=$(tmux display-message -t "$SOCKTOP_SESSION" -p '#{window_id}' 2>/dev/null) || exit 0 -[ -n "$WIN" ] || exit 0 +# Ordered window ids (@N) of the session, and the currently active one. +wins=$(tmux list-windows -t "$SOCKTOP_SESSION" -F '#{window_id}' 2>/dev/null) || exit 0 +[ -n "$wins" ] || exit 0 +WIN=$(tmux display-message -t "$SOCKTOP_SESSION" -p '#{window_id}') || exit 0 + +prev_win= next_win= seen=no +for w in $wins; do + if [ "$seen" = yes ]; then next_win=$w; break; fi + if [ "$w" = "$WIN" ]; then seen=yes; else prev_win=$w; fi +done state=$(tmux display-message -t "$WIN" -p '#{window_zoomed_flag} #{pane_index} #{window_panes}') || exit 0 zoomed=${state%% *} @@ -35,24 +42,47 @@ count=${rest##* } last=$((count - 1)) zoom_to() { + # $1 = window id, $2 = pane index. # Selecting a different pane auto-unzooms, so zoom explicitly afterwards. - tmux select-pane -t "$WIN.$1" - tmux resize-pane -Z -t "$WIN.$1" + tmux select-window -t "$1" + tmux select-pane -t "$1.$2" + tmux resize-pane -Z -t "$1.$2" +} + +overview() { + # Show window $1 unzoomed. + tmux select-window -t "$1" + if [ "$(tmux display-message -t "$1" -p '#{window_zoomed_flag}')" = 1 ]; then + tmux resize-pane -Z -t "$1" + fi } if [ "$zoomed" = 0 ]; then case "$dir" in - next) zoom_to 0 ;; - prev) : ;; # already at the overview; nothing further out + next) zoom_to "$WIN" 0 ;; + prev) + # Back out of this group's overview onto the previous group's last pane. + if [ -n "$prev_win" ]; then + pl=$(tmux display-message -t "$prev_win" -p '#{window_panes}') + zoom_to "$prev_win" $((pl - 1)) + fi + ;; esac else case "$dir" in - next) [ "$idx" -lt "$last" ] && zoom_to $((idx + 1)) || : ;; + next) + if [ "$idx" -lt "$last" ]; then + zoom_to "$WIN" $((idx + 1)) + elif [ -n "$next_win" ]; then + tmux resize-pane -Z -t "$WIN.$idx" # unzoom before leaving + overview "$next_win" + fi + ;; prev) if [ "$idx" -gt 0 ]; then - zoom_to $((idx - 1)) + zoom_to "$WIN" $((idx - 1)) else - tmux resize-pane -Z -t "$WIN.$idx" # unzoom -> overview + tmux resize-pane -Z -t "$WIN.$idx" # unzoom -> this group's overview fi ;; esac diff --git a/uninstall.sh b/uninstall.sh index d97f318..58154df 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -12,6 +12,7 @@ BIN="$PREFIX/bin" CONF="$PREFIX/etc/socktop-swipe.env" XCONF=/etc/X11/xorg.conf.d/99-ignore-touch-socktop-swipe.conf BCONF=/etc/X11/xorg.conf.d/10-no-blanking-socktop-swipe.conf +LCONF=/etc/lightdm/lightdm.conf.d/50-autologin-socktop-swipe.conf keep_config=no [ "${1:-}" = "--keep-config" ] && keep_config=yes @@ -42,6 +43,11 @@ if [ -e "$BCONF" ]; then echo "removed $BCONF (screen blanking returns after the next X restart)" fi +if [ -e "$LCONF" ]; then + sudo rm -f "$LCONF" + echo "removed $LCONF (login prompt returns at next boot)" +fi + echo echo "Left in place on purpose:" echo " * lisgd -- remove with: sudo make -C ~/src/lisgd uninstall"