a73x

14b06705

docs: the wall's keys in the README, and why zooming is a real attach

a73x   2026-08-19 17:17

Commit message
docs: the wall's keys in the README, and why zooming is a real attach

The wall had one documented key (`q`); it now has four, so the README
says what they do. decisions.md records why `Enter` spawns a client
instead of forwarding keystrokes down a tile — typing through a 0x0
attach would fork the latest-wins rule — plus why the selection lives
under the paint mutex and why coming back needs a generation counter.

README.md
Old New
@@ -39,7 +39,7 @@ Inside a session:
39 | `Ctrl-\` `d` (or `Ctrl-\` `Ctrl-\`) | detach (session keeps running) | 39 | `Ctrl-\` `d` (or `Ctrl-\` `Ctrl-\`) | detach (session keeps running) |
40 | `Ctrl-\` `c` | create a session and switch to it (the old one keeps running) | 40 | `Ctrl-\` `c` | create a session and switch to it (the old one keeps running) |
41 | `Ctrl-\` `n` / `Ctrl-\` `p` | switch to the next / previous session, wrapping | 41 | `Ctrl-\` `n` / `Ctrl-\` `p` | switch to the next / previous session, wrapping |
42 | `Ctrl-\` `w` | show every session as a read-only wall; `q` comes back | 42 | `Ctrl-\` `w` | show every session as a wall (`j`/`k` select, `Enter` types, `q` comes back) |
43 | `Shift+PageUp` / `Shift+PageDown` | scrollback (any other key returns to live) | 43 | `Shift+PageUp` / `Shift+PageDown` | scrollback (any other key returns to live) |
44 44
45 `mux` again reattaches — full TUI screens included. Kill the client with 45 `mux` again reattaches — full TUI screens included. Kill the client with
@@ -148,10 +148,24 @@ mux wall HOST '--sock /tmp/s.sock#b' # ...or state it (one spelling per argume
148 mux wall HOST --sock /tmp/s.sock#b # ...`--sock PATH` unquoted works too, as in muxweb 148 mux wall HOST --sock /tmp/s.sock#b # ...`--sock PATH` unquoted works too, as in muxweb
149 ``` 149 ```
150 150
151 Read-only: each stripe is a live session (label bar + the rows around its 151 Watching is read-only: each stripe is a live session (label bar + the rows
152 cursor), attached the way `muxa` attaches — no size claim, so watching 152 around its cursor), attached the way `muxa` attaches — no size claim, so
153 never resizes anyone. `q` or `Ctrl-\` leaves. To type into a session, 153 watching never resizes anyone. The stripes are cut once, from the terminal's
154 attach to it: `mux HOST --session b`. 154 size when the wall opened, so resizing that terminal while the wall is up
155 misplaces them until you leave and reopen it.
156
157 | Key | Effect |
158 |---|---|
159 | `j` / `k` (or `n` / `p`) | select the next / previous stripe, wrapping |
160 | `1`-`9` | select that stripe, counting from the top |
161 | `Enter` | type into the selected session; `Ctrl-\` `d` comes back |
162 | `q` (or `Ctrl-\`) | leave the wall |
163
164 The selected stripe's label bar carries a `>` marker. `Enter` zooms it:
165 the wall hands the terminal to an ordinary `mux` attached to that session,
166 so typing is a full-size client claiming the grid — never a tile, which
167 stays 0×0 and passive. `Ctrl-\` `d` detaches that client and the wall
168 comes back, current.
155 169
156 The same wall, from inside a session: `Ctrl-\` `w` shows every session on 170 The same wall, from inside a session: `Ctrl-\` `w` shows every session on
157 the daemon you are attached to, and `q` puts you back where you were. 171 the daemon you are attached to, and `q` puts you back where you were.
docs/decisions.md
Old New
@@ -4083,3 +4083,57 @@ pass for a client that printed the line and attached anyway. The daemon runs
4083 200 columns wide there, because every assertion greps a MESSAGE out of a grid 4083 200 columns wide there, because every assertion greps a MESSAGE out of a grid
4084 dump and a grid wraps — at 80 the wall refusal, whose length follows `$TMPDIR`, 4084 dump and a grid wraps — at 80 the wall refusal, whose length follows `$TMPDIR`,
4085 split mid-sentence and a correct refusal read as a missing one. 4085 split mid-sentence and a correct refusal read as a missing one.
4086
4087 **The wall's zoom is a real attach, and that is the whole design.** `Enter` on
4088 the selected stripe spawns an ordinary `mux` on this terminal, waits for it, and
4089 takes the terminal back when it detaches. The obvious alternative — forward
4090 keystrokes down the selected tile's transport — forks the latest-wins rule at
4091 its root: a tile attaches 0×0 precisely so it can never claim the grid, and a
4092 client that types is exactly a client that must claim it, so a typing tile is
4093 either a 0×0 client claiming a grid (a rule with an exception is a rule nobody
4094 can reason about) or a session being typed at by a client whose size the daemon
4095 was told to ignore. Spawning keeps both halves honest: the wall stays an
4096 observer, and interaction claims the grid the legitimate way. It is `Ctrl-\ w`
4097 read backwards — that spawns a wall from a client, this spawns a client from a
4098 wall — and the same argument settles it, that a process boundary is what makes
4099 "the terminal belongs to exactly one of you" true by construction.
4100
4101 The selection is under the paint mutex, not atomic. It is read while a label bar
4102 is being drawn, and the bars are drawn by the pump threads: an atomic read would
4103 let a bar paint the marker the selection had when its frame arrived, so a wall
4104 could show two markers, or none, for as long as nothing else painted. One
4105 keystroke moves it and repaints both bars under one hold, so the marker is
4106 single-valued in every frame the terminal ever shows.
4107
4108 Coming back needs a lever, because a correct replica is not a painted screen.
4109 Holding `paint_mu` for the child's whole life does more than stop the stripes
4110 painting over it: a pump blocks ON the mutex at the first frame it would paint
4111 and stops reading its socket there, so the pumps do NOT keep replicating behind
4112 the zoom — each one stalls a frame in and the wall is as far behind as the zoom
4113 was long. So the zoom bumps a generation counter on the way out and each pump
4114 repaints when its own last-painted generation lags — checked on the poll
4115 timeout, so a stripe is back within ~100ms whether or not its session ever
4116 speaks again. Between the two, the wall returns current: the backlog every pump
4117 drains once it is released, and the generation bump for the sessions that said
4118 nothing at all. A tile still inside its reconnect backoff is the one gap: it
4119 repaints when it next narrates a state, which is the same moment it would have
4120 had anything true to say.
4121
4122 The tail of blocked pumps is a drop, and it is accepted rather than fixed. A
4123 tile that stops reading accumulates queue on the daemon side, and past
4124 `pending_cap` (8 MiB) the daemon drops that client — so a chatty session under
4125 a long zoom loses its tile, which reconnects and re-attaches from a snapshot,
4126 re-running the whole ssh→QUIC handshake for a `.hand` or `.quic` target. The
4127 alternative is a pump that keeps reading and applying while it may not paint,
4128 which is a second buffering path through the replica for the sake of a case
4129 that already self-heals; and the cap itself is not negotiable, since it is the
4130 only thing bounding what one wedged client can cost the daemon in memory. The
4131 visible cost is a stripe reading `[reconnecting]` for a moment after a long
4132 zoom, which is what a stripe that fell behind should say.
4133
4134 A failed zoom is said on the label bar, not on stderr — the alternate screen the
4135 wall re-enters would eat the line — and never propagated: a spawn that did not
4136 work out is not a reason to end the wall the user is still standing in. The
4137 `--via` case is `unreachable` rather than an error, because the wall grammar has
4138 no `--via` spelling to parse: no tile can hold that target, and pretending
4139 otherwise would add an error path nothing can reach.