Files
socktop-swipe/notes/PLAN-v2.md
T
jasonwitty ad8ebcaa71 docs: record the LattePanda deployment and the traps it turned up
The rack display has been running v2 since today and passed a physical swipe
test and a cold boot, so the hardware table now says what is actually in
service and what is merely intended: the Wyse 3040 has not been tried yet and
should not read as though it has.

notes/HARDWARE-NOTES.md gains the two traps that cost the most time during the
deployment, both of which will recur:

lightdm autologin fires when a seat starts, not after a logout, so `i3-msg
exit` does not test the autostart -- it strands a keyboard-less wall display at
a greeter that nobody standing at the rack can get past. Only a real reboot
tests the boot path.

And `ssh host 'sudo ...'` allocates no TTY, so sudo has nowhere to prompt and
fails; chaining the rest of the recovery behind `&&` then swallows it silently.
Use `ssh -t`, and `;` for steps that must run regardless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 14:03:30 -07:00

24 KiB
Raw Blame History

socktop-swipe v2 — release plan

Status: implemented and in service, 2026-09-09. Milestones 1-7 done on branch v2-rust (PR #1); the LattePanda rack display has been running v2 since, and passed a physical swipe test and a cold boot. Milestone 8, the Wyse 3040 install following only the README, is the remaining acceptance gate.

Where the built thing differs from this plan, notes/DESIGN.md is authoritative.

v1 is three POSIX shell scripts driving lisgd and tmux. It works and is running on the rack display today, but it hard-codes a single linear carousel of socktop hosts, and every new kind of screen is a special case (SOCKTOP_AUX_CMD is the aux screen because there was nowhere else to put it). v2 makes the layout a first-class, user-authored thing and rewrites the logic in Rust.


1. Decisions locked in

Question Decision
Rust scope One Rust binary owns config, the grid model, gesture input (evdev, replacing lisgd) and drives tmux. tmux stays the pane engine; zellij shelved (§4.3)
Grid coordinates Sparse ordinals — they define order and relative position, not physical slots
socktop group One cell, regardless of host count; expands into a sub-sequence internally
Payload TUIs Installer offers socktop / uptime-kuma-status / unifly, never requires them
Distribution Source only for now. Clone from Gitea, cargo build --release. No prebuilt binaries, no apt repo yet — revisit after feeling the pain on the Wyse
v1 config Clean break. No .env reader, no migrate command. Rack display's YAML gets hand-written once
Canonical repo gt.wittyoneoff.com/jason/socktop-swipe

2. The grid model

2.1 Coordinates

Every screen declares at: "<row>x<col>".

  • row increases downward: -1x0 is above 0x0, 1x0 is below it.
  • col increases rightward: 0x1 is right of 0x0.
  • 0x0 is where you start.

Coordinates are sparse ordinals. Only the ordering matters:

  • Rows sort ascending. A vertical swipe moves to the next defined row in that direction, not literally row ± 1. Rows -1, 0, 1 and rows -7, 0, 42 behave identically.
  • Within a row, cells sort by column ascending. A horizontal swipe moves to the next defined cell. 0x1 and 0x5 are interchangeable as long as the ordering is right.

This is the "ezpz for users" property: you never have to count how many screens a socktop group will produce in order to place the thing next to it.

2.2 Horizontal movement

A socktop cell with four hosts is one cell containing a sub-sequence of five screens:

  cell 0x0 (socktop, 4 hosts)            cell 0x1 (socktop, 2 hosts)
  ┌───────────────────────────────────┐  ┌──────────────────────┐
  │ tiled → host1 → host2 → host3 → h4│→ │ tiled → host1 → host2│
  └───────────────────────────────────┘  └──────────────────────┘

Swiping forward walks the sub-sequence, and only leaves the cell after its last sub-screen. Non-socktop cells have a sub-sequence of length 1, so they are entered and left in a single swipe.

Entry direction sets the landing sub-screen:

  • entering a cell from the left → its first sub-screen (the tiled overview)
  • entering a cell from the right → its last sub-screen

So swiping back left from 0x1 lands you on 0x0's last host, not its overview. That is what v1 does today and it is what makes the carousel feel continuous.

No wrap-around at either end of a row — same reasoning as v1: on a wall display, wrapping makes it impossible to tell where you are.

2.3 Vertical movement

  1. Find the next defined row in that direction. If there is none, do nothing.
  2. If you have been in that row before, go back to exactly where you were — same cell, same sub-screen. No calculation.
  3. Only if the row has never been visited: go to the cell at the same column if one exists, otherwise snap to the nearest defined column in that row. (Ties break toward the lower column — see open question Q1.)

Return memory always wins over snapping. Each row remembers its last position, and only a horizontal move within that row updates it. This is what makes "swipe up to unifly, swipe down, you're back on the same Pi" work — and it holds for wider grids too: from 0x1, up to -1x0, then down returns you to 0x1, not 0x0.

Snapping therefore only runs on the first entry into a row, and a layout where every row has a cell in the same column never runs it at all.

2.4 The default layout, walked through

                        ┌──────────────┐
              row -1    │    unifly    │            -1x0
                        └──────────────┘
                                ↕
      ┌───────────────────────────────────┐  ┌──────────────────────┐
row 0 │ 4 Pis: tiled → each Pi zoomed     │→ │ orangepi + trixie    │
      └───────────────────────────────────┘  └──────────────────────┘
                     0x0                              0x1
                                ↕
                        ┌──────────────┐
              row 1     │ uptime-kuma  │             1x0
                        └──────────────┘
  • Swipe up from any of the five 0x0 screens → unifly.
  • Left/right on unifly → nothing; it is the only cell in row -1.
  • Swipe down from unifly → back into 0x0, on the exact Pi you left.
  • Swipe down again → uptime-kuma-status.

From 0x1 (orangepi + trixie) the same holds: up snaps to -1x0 the first time, and down brings you back to 0x1 on the host you left.


3. Configuration

3.1 Format and location

YAML. Resolution order:

  1. --config <path>
  2. $XDG_CONFIG_HOME/socktop-swipe/config.yaml (i.e. ~/.config/socktop-swipe/config.yaml)
  3. /etc/socktop-swipe/config.yaml

Per-user first, because the display box runs as one dedicated user and that keeps the file editable without sudo. /etc stays supported so a future .deb has somewhere sensible to drop a default.

socktop-swipe validate parses the file, resolves the grid, checks every referenced binary exists, and prints the resulting map. It is the thing to run after every edit, and the README will say so.

3.2 Schema

# ── how the session is built ────────────────────────────────────────────────
session: socktop-swipe          # tmux session name
terminal: alacritty             # used only by the autostart the installer writes

# Full paths solve the "i3 has no ~/.cargo/bin on PATH" problem in one place.
# ~ is expanded. Anything already on PATH can be given as a bare name.
binaries:
  socktop:            ~/.cargo/bin/socktop
  uptime-kuma-status: ~/.cargo/bin/uptime-kuma-status
  unifly:             ~/Documents/GitHub/unifly/target/release-small/unifly

# ── touch panel ─────────────────────────────────────────────────────────────
touch:
  device: /dev/input/by-id/usb-ILITEK_ILITEK-TOUCH-event-if00
  width: 1280                   # the PANEL's resolution, not the X screen
  height: 720
  grab: true                    # exclusive grab; see §5
  threshold: 80                 # px of travel before a drag counts as a swipe
  leniency: 30                  # degrees off-axis tolerated
  fingers: [1, 2, 3]            # contact counts accepted

gestures:
  forward: RL                   # right-to-left finger motion moves forward
  back:    LR
  up:      DU
  down:    UD

# ── the grid ────────────────────────────────────────────────────────────────
screens:
  - at: -1x0
    type: unifly

  - at: 0x0
    type: socktop
    socktop_group: [rpi-master, rpi-worker-1, rpi-worker-2, rpi-worker-3]
    layout: tiled

  - at: 0x1
    type: socktop
    socktop_group: "orangepi, trixie"      # CSV string also accepted
    layout: even-vertical

  - at: 1x0
    type: uptime-kuma-status
    url: https://status.wittyoneoff.com/status/wittyoneoff

3.3 Monitor types

Each type knows how to turn its parameters into one or more tmux panes. Every type also accepts command: to override the generated command outright, and title: to override the pane border label — so nobody is ever blocked by a type not yet supporting a parameter they need.

Type Parameters Panes Generated command
socktop socktop_group (list or CSV, required), layout (default tiled) one per profile, plus the tiled overview as sub-screen 0 <socktop> -P <profile>
uptime-kuma-status url 1 <uptime-kuma-status> <url>
unifly (none yet)site/controller carved out for later 1 <unifly> tui
generic command (required), title 1 as given

layout accepts the tmux layout names (tiled, even-horizontal, even-vertical, main-horizontal, main-vertical).

Only socktop produces a multi-screen sub-sequence today. The internal representation is a plain Vec<Pane> per cell plus an optional overview, so if a future type wants the same treatment it is a data change, not a redesign.


4. Architecture

One binary, one process at runtime.

src/
  main.rs           CLI dispatch
  config/
    mod.rs          serde types, defaults, path resolution
    coord.rs        Coord parsing ("0x-1"), Grid construction, neighbour lookup
  grid.rs           navigation state machine — pure, no I/O, unit tested
  monitor.rs        MonitorType → panes + command lines
  input.rs          evdev reader → Swipe { direction, fingers }
  session/mod.rs    Multiplexer trait — the ONLY module that knows tmux vs zellij (§4.3)
  session/tmux.rs   build the session, select window/pane, zoom
  doctor.rs         diagnostics (replaces tools/diag.sh and tools/find-device.sh)
  ui.rs             optional on-screen position indicator (§8, cuttable)

4.1 Why this is better than v1 even before the grid

v1 runs lisgd as a daemon which shells out to socktop-swipe next per gesture. That script has no memory, so on every single swipe it re-derives "where am I" by interrogating tmux (list-windows, display-message, window_zoomed_flag). v2 holds the cursor in memory: one gesture becomes two or three tmux calls instead of six, and the navigation logic becomes a pure function over a struct that can be unit tested without a display, a touch panel or a tmux server. That test suite is the main reason the grid model is worth attempting at all.

4.2 CLI

Command What it does
socktop-swipe run Build the tmux session, spawn the terminal attached to it, run the gesture loop, exit when the session exits. The single autostart line.
socktop-swipe daemon Gesture loop only, against an existing session (for a systemd-user split)
socktop-swipe attach Build and attach the session only
socktop-swipe validate Parse config, resolve the grid, check binaries, print the map
socktop-swipe doctor Interactive touch diagnostics; list candidate devices; report what the panel actually sends
socktop-swipe next|back|up|down Drive a running instance over its control socket — for keybindings and testing without a panel

run as one process also kills the v1 double-instance footgun by construction: the second one fails to grab the device and exits with a clear message.

4.3 Multiplexer: tmux, with zellij shelved

tmux stays. session/ is a Multiplexer trait with a tmux implementation behind it, so the question can be reopened cheaply, but it is not being evaluated for v2.

Shelved rather than rejected. The reasoning, recorded in notes/DESIGN.md so it is not re-litigated: "available as a crate" does not buy in-process integration — zellij-server/zellij-utils are workspace crates for the binary, not a library surface, so it would still be a subprocess driven over a CLI exactly like tmux. The blocking issue is addressing: the grid needs "focus cell 0x1, sub-screen 3, zoomed" as one deterministic call, which tmux gives directly as select-pane -t session:window.3 plus resize-pane -Z, whereas zellij's CLI is direction-oriented (move-focus left). Footprint also runs the wrong way on 2 GB boxes, and a zellij source build on an Atom is a much worse story for the install guide than apt install tmux.

The one idea worth keeping on the shelf: running the navigation state machine as a WASM plugin inside zellij via zellij-tile, which would give real event subscriptions instead of CLI polling. Revisit only if tmux becomes the bottleneck.


5. Input: evdev instead of lisgd

input.rs opens the event device and reads multitouch protocol B (ABS_MT_SLOT, ABS_MT_TRACKING_ID, ABS_MT_POSITION_X/Y), tracking per-slot start and end points. A swipe fires when the dominant axis exceeds threshold, the off-axis angle is within leniency, and the peak simultaneous contact count is in fingers.

What this buys:

  • Drops the C toolchain from the install path. No libinput-dev, no libX11-dev, no git clone git.sr.ht/~mil/lisgd, no make. On the Wyse's eMMC that is real.
  • grab: true replaces --xignore. EVIOCGRAB takes the device exclusively, so X never sees the touches at all — which is what the Option "Ignore" InputClass was faking. That removes an /etc/X11/xorg.conf.d file and the logout/login step it required. The X rule stays documented in notes/ as a fallback for anyone who wants touch to reach X for other apps.
  • Compositor-agnostic for free. Reading /dev/input directly means nothing in the gesture path is X11-specific. This is not a Wayland port — the terminal and tmux still are what they are — but it removes one of the reasons v1 could not be one.
  • The fingers: [1,2,3] workaround becomes honest. Instead of binding three separate lisgd gestures per direction, the peak contact count is just a field on the detected swipe, and doctor can print it.

Still required: read access to the device. Installer offers the input group (needs a relogin) or a udev rule granting the display user access to that one device (no relogin). Prefer the udev rule and say why.


6. Installer

install.sh stays POSIX sh — it has to run before any Rust exists.

curl -fsSL https://gt.wittyoneoff.com/jason/socktop-swipe/raw/branch/main/install.sh | sh

Piping to sh leaves no stdin for prompts, so the script reads answers from /dev/tty explicitly. If there is no tty it falls back to defaults and says so. --yes takes every default non-interactively; --no-<thing> declines individually.

6.1 Dependency preflight

Every dependency is checked before anything is installed, and the whole report is printed at once so you can see the full cost up front rather than discovering it one prompt at a time. Each missing item comes with a concrete offered fix; anything with no known fix on the detected distro is reported as such rather than silently skipped, with the manual step spelled out.

Checked How If missing
distro + package manager /etc/os-release, which of apt-get/dnf/pacman/zypper unsupported → print manual package list and continue in degraded mode
tmux (or zellij, per §4.3) command -v + version offer the distro package
terminal emulator probe alacritty, foot, kitty, xterm offer to install one; explain that it must be GPU/GL-capable-ish and legible at panel size
cargo / rustup command -v cargo offer rustup, stating the disk cost (~1.2 GB toolchain + target dir) before asking
C toolchain only if something still needs building offer build-essential equivalent
window manager probe i3, detect a running WM needed only for the --i3-equivalent autostart; skip that step cleanly if absent
display manager /etc/lightdm, /etc/gdm3, /etc/sddm.conf autologin is only offered for the one detected; unknown DM → print the manual snippet
screen locker probe xss-lock, light-locker, i3lock, xscreensaver offer to disable, naming the specific unit or process found
touch device enumerate /dev/input/by-id/*event* with touchscreen capabilities none found → point at socktop-swipe doctor and stop before writing a broken config
device read access try opening the chosen device offer udev rule (default) or input group
git command -v needed only for the unifly source build
disk space df on /usr/local, $HOME, and the cargo target location warn before starting a build that will not fit — this is the Wyse's 8 GB eMMC case specifically

The report ends with a single summary — "3 things will be installed, 2 files changed, ~1.4 GB of disk used. Continue? [y/N]" — and nothing before that point has modified the system.

6.2 Flow

  1. Preflight (§6.1). Print the full report, get one confirmation.
  2. Toolchain — if no cargo: "Rust is needed to build from source. Install rustup? [Y/n]"
  3. Build and install socktop-swipe from source to /usr/local/bin.
  4. Touch panel — enumerate /dev/input/by-id/* touch devices, show them, ask which, write it into the config. Offer a live confirm ("swipe now") before committing.
  5. Optional payload TUIs, each a separate prompt, all skippable:
    • socktopcargo install socktop
    • uptime-kuma-statuscargo install uptime-kuma-status
    • unifly — clone github.com/jasonwitty/unifly, cargo build --profile release-small -p unifly, record the resulting path in binaries:
  6. Device access — udev rule (default) or input group.
  7. Autostart? — i3 / systemd user unit / none.
  8. Autologin? — lightdm drop-in, with the physical-access warning stated at the prompt, not buried in the README.
  9. Never blank, never sleep, never lock? — Xorg ServerFlags snippet + xset in the autostart + disable any detected screen locker. (Keeping v1's hard-won note that sudo xset silently targets root's X connection and does nothing.)
  10. Write a starter config if none exists — one socktop group with a placeholder profile, commented, and a pointer to socktop-swipe validate.
  11. Print exactly what changed, file by file, and what to do next.

uninstall.sh reverses each of the above and, as in v1, prints what it deliberately left alone.

Cheap future-proofing: add [package.metadata.deb] to Cargo.toml now (paths /usr/bin, /etc/socktop-swipe/, /lib/udev/rules.d/). It costs about fifteen lines and means the apt-repo follow-up is cargo deb rather than a packaging project.


7. README rewrite

Written for someone who has just found the repo and does not know what socktop is. Current README leads with mechanism ("the carousel is tmux pane zoom, not extra instances") — correct, and it moves to notes/DESIGN.md.

New order:

  1. What this is — one paragraph, then the rack photo and the swipe video.
  2. What you're looking at — annotated: the panel, the 19" adapter, what is on each screen and why.
  3. Hardware — tested table (LattePanda, Wyse 3040), the GeeekPi 9" panel, and a link across to cad/ for the adapter that mounts it in a 19" rack.
  4. Quick start — the curl one-liner and what it will ask you.
  5. Your first config — build it up: one socktop group → add unifly above → add uptime-kuma above/below. Each step shows the YAML and the resulting map.
  6. The grid — §2 of this document, with the diagram.
  7. Monitor types — the §3.3 table.
  8. Configuration reference — full key table.
  9. Unattended operation — autologin, blanking, phantom outputs, does it come back after a power cut.
  10. Troubleshooting — lead with socktop-swipe doctor, then the specific failures.
  11. Uninstall.
  12. Related projects — socktop, uptime-kuma-status, unifly, Uptime Kuma.

The Wyse 3040 install is done by following this README and nothing else, and every place it is wrong or incomplete is a bug fixed before release.


8. Wayfinding indicator (proposed, cuttable)

A 2D grid is harder to stay oriented in than v1's single line, and there is no wrap to tell you you have reached an end. Proposal: a small persistent indicator in the tmux status line or pane border showing position, e.g. unifly ▲ · rpi-worker-2 (3/5) · ▼ kuma — the current screen plus what is above and below. Off by default, indicator: true to enable. Flagging it as a distinct decision rather than assuming it.


9. Repo layout after v2

socktop-swipe/
├── README.md                 rewritten (§7)
├── Cargo.toml / Cargo.lock
├── src/                      §4
├── config.example.yaml       the default rack layout, heavily commented
├── install.sh  uninstall.sh
├── packaging/
│   ├── socktop-swipe.service systemd user unit
│   └── 70-socktop-swipe.rules udev
├── cad/                      unchanged
├── media/                    unchanged
└── notes/
    ├── PLAN-v2.md            this document
    ├── DESIGN.md             why tmux, why evdev, the grid model's reasoning
    ├── HARDWARE-NOTES.md     ILITEK contact counts, phantom DSI-1, the sudo-xset
    │                         trap, multi-monitor SCREEN_W, LattePanda specifics,
    │                         and: screen size and rack size are separate measurements
    │                         (9" screen, 10" mini-rack mount, 19" rack via the adapter)
    ├── V1-BASH.md            what v1 did and where to find it (tag v1.2)
    └── TODO.md

Tag the current tree v1.2 before deleting the shell scripts, so the working bash version stays recoverable.


10. Work order

Each milestone ends somewhere testable.

# Milestone Done when
1 Tag v1.2. Scaffold the crate. Config types + coord.rs + validate socktop-swipe validate prints the rack layout map from YAML
2 grid.rs navigation state machine, no I/O Unit tests cover: sparse columns, sub-sequence entry from left vs right, vertical snap, cursor persistence, both ends of every row
3 session/tmux.rs + attach Session builds correctly from YAML on the desktop; drive it with next/back/up/down by hand — no touch panel needed
4 input.rs evdev + doctor doctor correctly reports direction and contact count on the LattePanda's ILITEK panel
5 run wires it together LattePanda runs the full default grid by touch, unifly above and kuma below
6 Installer rewrite Clean-VM install works, prompts behave when piped from curl, --yes works
7 README + notes/ split Reads correctly to someone who has not seen the project
8 Wyse 3040 validation Debian minimal → working rack display, following only the README. Every stumble is a fix, then re-run
9 Release Tag v2.0, config.example.yaml matches the rack, cargo clippy clean with no #[allow]

Milestone 5 is the point at which the LattePanda switches over. Its v1 install stays untouched until then — v2 uses a different config path and a different tmux session name, so both can be installed side by side during milestone 35 testing.


11. Open questions

Q1 — column tie-break on snap. From 0x2, if the row above has cells at -1x1 and -1x3, both are distance 1. Plan says lower column (left) wins. Fine? This only ever fires on the first entry into a row, so it is low-stakes.

Q2 — the indicator (§8). Build it, or cut it from v2?

Q3 — repo visibility. Distribution is Gitea-source-only for now, which is settled. But the README is being written for a public audience and the Wyse is reference hardware for a public guide. Is the Gitea repo publicly readable without a login? If not, the curl one-liner in the README will not work for anyone but you.