14b06705
docs: the wall's keys in the README, and why zooming is a real attach
a73x 2026-08-19 17:17
Commit message
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. | ||