128 lines
6.2 KiB
Markdown
128 lines
6.2 KiB
Markdown
|
|
# Design notes
|
||
|
|
|
||
|
|
Why the thing is built the way it is. The README says what to do; this says why,
|
||
|
|
so neither has to carry both jobs.
|
||
|
|
|
||
|
|
## The carousel is tmux pane zoom, not extra processes
|
||
|
|
|
||
|
|
There is exactly one monitor process per host. Zooming calls
|
||
|
|
`tmux resize-pane -Z`, which makes one pane fill the window and sends `SIGWINCH`
|
||
|
|
so the program repaints at the new size.
|
||
|
|
|
||
|
|
Three reasons that matters:
|
||
|
|
|
||
|
|
- **Half the processes.** Separate full-screen instances would mean N extra
|
||
|
|
processes and double the polling load on the monitored hosts. On a 1.9 GB
|
||
|
|
display host that is the difference between comfortable and not.
|
||
|
|
- **No reconnect delay.** Hidden panes keep running and stay connected, so
|
||
|
|
swiping back shows current data, not a stale snapshot with a spinner.
|
||
|
|
- **Constant load.** The monitored hosts see the same connections regardless of
|
||
|
|
what is on screen.
|
||
|
|
|
||
|
|
## Why the grid is sparse ordinals
|
||
|
|
|
||
|
|
The obvious design is that a coordinate names a physical slot. It falls apart on
|
||
|
|
the first socktop group: a group of four hosts is five screens, so putting
|
||
|
|
anything to its right means writing `0x5`, and adding a fifth host means
|
||
|
|
renumbering the rest of the row.
|
||
|
|
|
||
|
|
Making coordinates pure ordering removes that entirely. Only the sort order
|
||
|
|
matters, so `0x1` and `0x5` are the same thing, and a group grows without
|
||
|
|
disturbing its neighbours. The cost is that the file is not a literal map of the
|
||
|
|
screen — which is what `socktop-swipe validate` is for.
|
||
|
|
|
||
|
|
## Why return memory beats spatial snapping
|
||
|
|
|
||
|
|
Vertical movement had two plausible rules and they disagree. Purely spatial: from
|
||
|
|
`0x1`, up to `-1x0`, down again lands on `0x0`. Return memory: it lands back on
|
||
|
|
`0x1`.
|
||
|
|
|
||
|
|
Return memory won because the thing you actually do with a wall display is glance
|
||
|
|
away and glance back. Losing your place on every glance is the worse failure, and
|
||
|
|
snapping is only ever needed to decide the *first* entry into a row. A layout
|
||
|
|
where every row has a cell in the same column never snaps at all.
|
||
|
|
|
||
|
|
## Why evdev instead of lisgd
|
||
|
|
|
||
|
|
libinput deliberately emits gesture events only for touchpads, never for
|
||
|
|
touchscreens, so `libinput-gestures` and everything built on it cannot work here
|
||
|
|
at all. Something has to read raw touch events. v1 used
|
||
|
|
[lisgd](https://git.sr.ht/~mil/lisgd); v2 does it in-process.
|
||
|
|
|
||
|
|
What that bought:
|
||
|
|
|
||
|
|
- **No C toolchain in the install path.** No `libinput-dev`, no `libX11-dev`, no
|
||
|
|
`git clone` and `make`. On the Wyse's 8 GB of eMMC that is not a small thing.
|
||
|
|
- **`grab: true` replaces the X ignore rule.** `EVIOCGRAB` takes the device
|
||
|
|
exclusively, so X never sees the touches — which is what v1's
|
||
|
|
`Option "Ignore"` InputClass was faking, except this needs no X restart and no
|
||
|
|
logout. The X rule is still documented in the README as a fallback for anyone
|
||
|
|
who wants touch to reach other applications.
|
||
|
|
- **One process, so the double-instance bug cannot happen.** lisgd does not grab
|
||
|
|
the device, so two copies made every swipe fire twice and the carousel appeared
|
||
|
|
to skip. Now the second copy fails to grab and says so.
|
||
|
|
- **The contact-count workaround became honest.** Instead of binding three
|
||
|
|
separate lisgd gestures per direction, the peak contact count is a field on the
|
||
|
|
detected swipe, and `doctor` prints it in English.
|
||
|
|
|
||
|
|
Averaging rather than summing the contacts' travel matters here: a panel
|
||
|
|
reporting one physical finger as three contacts must not look like three times
|
||
|
|
the displacement. There is a test for exactly that.
|
||
|
|
|
||
|
|
## Why panes are addressed by id, and kept alive
|
||
|
|
|
||
|
|
Pane *indices* renumber when a pane dies. Pane *ids* (`%12`) do not, so every
|
||
|
|
lookup uses them.
|
||
|
|
|
||
|
|
That leaves the question of what happens when a monitor exits — a typo in a
|
||
|
|
`generic` command, a socktop that cannot reach its agent. tmux destroys a window
|
||
|
|
when its last pane goes, which during construction breaks the next
|
||
|
|
`split-window` with a baffling "no current target", and afterwards silently
|
||
|
|
reshuffles the display.
|
||
|
|
|
||
|
|
v1 used `remain-on-exit`, which cannot actually do the job: it is a **per-window**
|
||
|
|
option that new windows do not inherit, so there is always a gap between creating
|
||
|
|
a window and setting it. v2 wraps each command instead:
|
||
|
|
|
||
|
|
```sh
|
||
|
|
<command>; s=$?; printf '\n[%s exited: status %s]\n' <name> "$s"; while :; do sleep 86400; done
|
||
|
|
```
|
||
|
|
|
||
|
|
The pane outlives the command, and the failure is visible *on the wall display*
|
||
|
|
with its exit status — which is what a wall display is for. tmux already runs
|
||
|
|
each command under `sh`, so this costs one shell that stays resident per pane
|
||
|
|
rather than one that execs away.
|
||
|
|
|
||
|
|
## Why `at: "0x0"` must be quoted
|
||
|
|
|
||
|
|
YAML reads an unquoted `0x0` as the hexadecimal number 0. The nasty part is that
|
||
|
|
`1x0` is *not* valid hex and arrives as a string, so only the row-0 entries break
|
||
|
|
and the failure looks arbitrary. Deserialization catches the integer case and
|
||
|
|
prints the fix rather than a type error.
|
||
|
|
|
||
|
|
## Multiplexer: tmux, with zellij shelved
|
||
|
|
|
||
|
|
`src/session/` is a `Multiplexer` trait with a tmux implementation behind it, so
|
||
|
|
the question is cheap to reopen. It was shelved rather than rejected, for reasons
|
||
|
|
worth recording so it is not re-litigated:
|
||
|
|
|
||
|
|
1. **"Available as a crate" is not an embedding API.** `zellij-server`,
|
||
|
|
`zellij-client` and `zellij-utils` are published, but they are workspace
|
||
|
|
crates for the binary, not a supported library surface. Realistic integration
|
||
|
|
is the CLI or a WASM plugin — so it would still be a subprocess driven over a
|
||
|
|
CLI, exactly like tmux.
|
||
|
|
2. **Addressability is what we depend on.** The grid needs *"focus cell 0x1,
|
||
|
|
sub-screen 3, zoomed"* as one deterministic call. tmux gives that directly
|
||
|
|
(`select-pane -t %12`, `resize-pane -Z`). zellij's CLI is direction-oriented
|
||
|
|
(`move-focus left`), which would mean counting relative moves and tracking
|
||
|
|
state we cannot verify.
|
||
|
|
3. **Footprint runs the wrong way.** zellij is a client/server async multiplexer
|
||
|
|
with a wasmtime plugin runtime, heavier at idle than tmux's C implementation.
|
||
|
|
On 2 GB boxes that is the binding constraint, and `apt install tmux` versus a
|
||
|
|
zellij source build on an Atom is a much worse story for the install guide.
|
||
|
|
|
||
|
|
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 driving a CLI. Revisit only if tmux becomes the
|
||
|
|
bottleneck.
|