a73x

646ac14b

docs: the wall is the layout

a73x   2026-09-03 05:20

Commit message
docs: the wall is the layout

README: the wall is $XDG_STATE_HOME/mux/layout, not the daemons' live
lists; the three doors a pane comes through; the picker's two levels and
its keys; Ctrl-\ x removes a pane and ends nothing; a new "The layout
file" subsection with the format, the strict loader and the trimmed-seed
no-save rule; the hub serves and writes the same file; the daemon-newer-
than-client caveat for the # holds lines. The [+N not shown] sentence is
gone with the feature.

CLAUDE.md: the "wall file lists DAEMONS" and "layout sidecar is derived"
invariants are replaced by "the layout is the wall; the poll grades it and
adds nothing" and "the hosts file lists DAEMONS and nothing else"; the
picker bullet gains its session level; the Ctrl-\ x bullet says removal and
points ending at the picker.

decisions.md: a dated section with the report, the three shapes considered,
the strictness reversal, the # holds line, the asker-counted end count and
its deferred UX issue, the hub's pre-dial room check, the duplicate-leaf
refusal on both fronts, why keeps_wall stayed, and the pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

CLAUDE.md
Old New
@@ -130,63 +130,116 @@ own. Test fixtures in `test/`:
130 agent, though anything they run inside a session uses that session's 130 agent, though anything they run inside a session uses that session's
131 socket like any other process. Frames, never transport; both ends cap 131 socket like any other process. Frames, never transport; both ends cap
132 a frame at `agent_data_max`. 132 a frame at `agent_data_max`.
133 - **The wall file lists DAEMONS; tiles are their live sessions.** 133 - **The layout is the wall; the poll grades it and adds nothing.**
134 `$XDG_STATE_HOME/mux/layout` is authored intent: a pane tree whose leaves
135 are `HOST#SESSION`, `HOST` a hosts-file line verbatim. Tiles come from it
136 and from three doors only — the file on start (`wall_layout.seedLayout`),
137 the picker, and a chord split — never from a daemon's `sessions_reply`;
138 `mux HOST`'s entry pane goes through the same seat-then-`wall_layout.persist`
139 path. `wall_host.planHostDiff` is the once-a-second GRADE and the whole of
140 it: it binds a pending pane whose session the list names, marks `gone` one
141 the list does not, and vanishes a live pane whose shell ended after the
142 one-list grace. A name in the list that no pane spells is nobody's
143 business. A pane on a host that answers NOTHING stays, wearing
144 `unreachable`, for as long as the box is dark — an eight-pane setup is not
145 worth one quiet machine — and a `gone` pane (the host said no, not
146 nothing) takes Enter to re-create the session in that pane's own rect or
147 `x` to remove it, every other key eaten. A wall of only gone panes is not
148 empty; a wall with no panes says so and opens the picker. A daemon restart
149 re-creates nothing: its panes wear `gone` until an Enter starts them.
150 `wall_layout.persist` is the ONE save path, gated on `Shared.layout_path`,
151 and every change to the pane set or the tree calls it — birth, split,
152 resize, detach, removal, a poll's vanish of an exited shell, a picker add,
153 birth or forget. Only a wall on a TERMINAL (`Shared.is_tty`) has a path at
154 all: a piped `mux` is a wall of one and neither reads nor writes.
155 `seedLayout` is STRICT — `.refused` with the offending line for a host the
156 hosts file lacks, a leaf without `#SESSION` or with a bad name, a repeated
157 leaf (a second leaf spelling the entry included), more leaves than
158 `layout.max_leaves`, or text `layout.parseReporting` gives up on — and the
159 caller prints `mux: layout ignored (PATH): LINE` and starts as if the file
160 were missing, because seating half a wall is how a user loses one.
161 Whitespace-only is `.none`, not a refusal. A missing file is one local
162 pane on session `0` when there is an entry and an empty wall when there is
163 not; a hosts file with lines and no layout reconstructs NOTHING. A run
164 whose seed was trimmed to fit the terminal, or that dropped a leaf naming
165 this shell's own session, sets `layout_path` null for the WHOLE run and
166 says which on the notice line (`[layout not saved: ...]`) — it never
167 rewrites the file, and a save that fails for any other reason is a notice
168 too, never a stderr print into somebody's pane. `focus N` is the one line
169 in the file that is not authored and may go stale between tree changes.
170 Both files are strict now: the 2026-08 asymmetry (strict hosts, lenient
171 sidecar) is reversed, because the layout is no longer derived.
172 - **The hosts file lists DAEMONS and nothing else.**
134 `$XDG_STATE_HOME/mux/hosts`, one spelling per line (`--sock PATH` | 173 `$XDG_STATE_HOME/mux/hosts`, one spelling per line (`--sock PATH` |
135 `HOST` | `quic://HOST[:PORT]`); `#SESSION` is refused, because a host 174 `HOST` | `quic://HOST[:PORT]`); `#SESSION` is refused, because a host
136 line must name nothing that could be resurrected. `hosts.zig` owns it. 175 line must name nothing that could be resurrected — naming a session is
137 Tiles come from each host's own `sessions_req` on a fresh side 176 the layout's job. `hosts.zig` owns it. `--via` and `mux a` record no host.
138 connection, polled once a second — never from the file — so a birth by
139 anyone appears and an exit disappears, and `n`/`p`/digits walk the one
140 list. A host with no live session contributes NOTHING — the wall shows
141 sessions and nothing else, and a wall with none says so; tiles
142 ride out a blip rather than vanishing. Only a host's OWN list may take a
143 pane: a saved pane on a host that answers nothing stays, wearing
144 `unreachable`, for as long as the box is dark — an eight-pane setup is
145 not worth one quiet machine. A saved pane whose REACHABLE host answers
146 without its session stays too, wearing `gone` — the host said no, not
147 nothing — until Enter re-creates the session in that pane's own rect or
148 `x` dismisses it; every other key at a gone pane is eaten. A wall of
149 only gone panes is not empty. `--via` and `mux a` record no host.
150 Nothing re-creates a session: a daemon restart heals to what that daemon
151 now has, which for a fresh daemon is session `0`.
152 `client.hosts.load` is strict — a bad line refuses `mux` with rc 2 and prints 177 `client.hosts.load` is strict — a bad line refuses `mux` with rc 2 and prints
153 it — while `mux hosts` lists and `mux hosts rm` edits VERBATIM, so the 178 it — while `mux hosts` lists and `mux hosts rm` edits VERBATIM, so the
154 repair tool can read what the loader will not. The BROWSER hub is the 179 repair tool can read what the loader will not. The BROWSER hub is the
155 same wall: `mux web [HOST ...]` records its argv into the file and then 180 same wall: `mux web [HOST ...]` records its argv into the hosts file and
156 serves the file, one `client.SessionPoll` per daemon, `/tiles` in host 181 then serves the LAYOUT — `webhub.readLeaves`, which refuses a bad file
157 order then daemon order. Ids are birth order and are never reused. The 182 with `mux web: layout ignored (PATH): LINE` and serves an empty wall —
158 page authors nothing — its `+` births on that tile's daemon, and POST 183 one `client.SessionPoll` per daemon, `/tiles` in tree order carrying
159 /tiles, PUT and DELETE answer 405. The old `wall` file is retired. 184 `id`, `label`, `session` and `state`. Ids are birth order and are never
160 - **Hosts live in the picker, not on the wall.** `Ctrl-\ s` is a MODE of 185 reused. The page authors ONE thing: its `+` (`POST /tiles/<id>`) checks
161 `interact.PrefixFilter` (`picking`), like the spelling editor it now 186 the file for room and for a duplicate under the hub mutex BEFORE it dials
187 (`webhub.roomForLeaf`), births, then `webhub.appendLeaf` writes the leaf
188 beside that pane — 409 `duplicate` for a second `+` inside one poll
189 interval, 502 for `WallFull` or `BadLayout`. Dialling first would leave a
190 live session behind a 502 that no wall names. POST /tiles, PUT and DELETE
191 answer 405. Two writers in one instant lose one update, which the hosts
192 file has always accepted. The old `wall` file is retired.
193 - **Hosts and sessions live in the picker.** `Ctrl-\ s` is a MODE of
194 `interact.PrefixFilter` (`picking`), like the spelling editor it
162 contains: every byte is the popup's, so a key can never reach a session, 195 contains: every byte is the popup's, so a key can never reach a session,
163 and the table is unit-testable without a terminal. Rows are the hosts 196 and both tables are unit-testable without a terminal. It has two levels
164 file's daemons in file order with the POLLER's last answer beside each 197 (`PrefixFilter.pick_level`). HOSTS are the hosts file's daemons in file
165 (`N sessions` / `no sessions` / `unreachable` / `connecting`); Enter 198 order with the POLLER's last answer beside each (`N sessions` / `no
166 births on the selected host — a tile that CREATES at `client.nextFreeName` 199 sessions` / `unreachable` / `connecting`); Enter descends into that
167 off that host's own list, so a host with no tile is still one you can 200 daemon's sessions, `x` forgets the host (the file line, its poller, its
168 start work on — `x` forgets a host (the file line, its poller, its tiles; 201 panes; the sessions keep running), `a` is the old `:` spelling editor.
169 the sessions keep running), `a` is the old `:` editor. Tiles do not paint 202 SESSIONS (`wall_picker.sessionRows`) are one row per name the last answer
170 while it is open (`Shared.picker_open`); the close bumps `repaint_gen`. 203 carried, reading `on this wall` when the layout already has it and
171 An empty wall opens it ONCE by itself, so an Esc there leaves the 204 `K client(s)` from `proto.parseSessionsHolds` — K is EVERY holder, this
172 one-line text; and on a TERMINAL the last tile's exit leaves that empty 205 wall's own pane included, so it is a number to READ and never a verdict;
173 wall rather than ending mux, as does a picker birth the daemon REFUSES 206 an old daemon sends none and the row shows no count. There Enter adds the
174 (`Tile.keeps_wall`, which `born_from` cannot say — the entry tile carries 207 session as a pane and zooms to it (`wall_picker.pickAdd`, which JOINS and
175 the same null) — a piped `mux` still exits with the shell's code, because 208 never creates; a session already on the wall is zoomed to and nothing
176 scripts read it. 209 else), `x` ends it, and Esc backs out one level. `c` births at either
177 - **`Ctrl-\ x` ends a session; the daemon owns the two-step.** `end_req` 210 level — `wall_picker.pickBirth` CREATES at `client.nextFreeName` off that
178 `0x11` / `end_reply` `0x94`, observer verbs. The daemon refuses a first 211 host's own list, so a host with no pane is still one you can start work
179 `x` on a session other clients hold and answers the count; the client 212 on. Tiles do not paint while it is open (`Shared.picker_open`); the close
180 arms 3 s for the forcing second press. An accepted end is BOUNDED — 213 bumps `repaint_gen`. An empty wall opens it ONCE by itself, so an Esc
181 SIGKILL past `Pty.term_grace_ms` — so no shell can refuse to die, and 214 there leaves the one-line text; and on a TERMINAL the last tile's exit
182 `mux d upgrade` is refused while any session is ending rather than carry 215 leaves that empty wall rather than ending mux, as does a picker birth the
183 a masterless pty across the exec. A daemon with no arm answers nothing 216 daemon REFUSES (`Tile.keeps_wall`, which `born_from` cannot say — the
184 and the client banners `[daemon too old to end a session]`. The tile 217 entry tile carries the same null) — a piped `mux` still exits with the
185 leaves on the next list, not on the keypress — EXCEPT a tile that has 218 shell's code, because scripts read it.
186 never come up, a saved pane on a dark host included: `x` there closes the 219 - **`Ctrl-\ x` removes a pane and ends nothing; ending is the picker's, and
187 tile locally, because a pump parked in `dial` reads no ask, a pending pane 220 the daemon owns its two-step.** `wallview.removePane` sets the notice
188 has no pump at all, and birthing onto an `unreachable` row is a designed 221 `[pane removed - the session is still on its daemon]` BEFORE the vanish
189 path. 222 (the vanish hands the focus on, and the incoming pump's claim is what
223 paints the sentence), tells the pump to detach — it writes `.detach` on
224 its way out and the transport close behind it is the fallback — vanishes
225 the tile, relayouts and persists. Every pane removes the same way,
226 including one that never came up. ENDING is `wall_picker.pickEnd`:
227 `end_req` `0x11` / `end_reply` `0x94`, observer verbs, over
228 `client.endSession` on a SIDE connection, because the session may have no
229 pane here to ask through. That connection dials `HostSpec.poll_target` —
230 the poller's batch recipe, never the interactive one, since an end must
231 not start a daemon and must not reach for a terminal. The daemon refuses
232 a first `x` on a session other clients hold and answers the count;
233 `Shared.pick_end` arms 3 s per host AND name for the forcing second
234 press, and every OTHER refusal arms nothing, because only "others
235 attached" is a question a second press answers. An accepted end is
236 BOUNDED — SIGKILL past `Pty.term_grace_ms` — so no shell can refuse to
237 die, and `mux d upgrade` is refused while any session is ending rather
238 than carry a masterless pty across the exec. A daemon that sends no holds
239 line is refused up front with `[daemon too old to end a session]`: it
240 answers an unknown frame with silence, so the press would spend the whole
241 reply budget with the wall frozen. An ended session's pane leaves on the
242 next list, not on the keypress.
190 - **A QUIC client says goodbye.** Every QUIC connection takes one of the 243 - **A QUIC client says goodbye.** Every QUIC connection takes one of the
191 daemon's eight client slots at the handshake, attached or not, and the 244 daemon's eight client slots at the handshake, attached or not, and the
192 wall polls each QUIC host once a second on a connection of its own. 245 wall polls each QUIC host once a second on a connection of its own.
@@ -272,15 +325,6 @@ own. Test fixtures in `test/`:
272 ssh's own tty prompts, polls keep `BatchMode`. A decline SIGTERMs that ssh 325 ssh's own tty prompts, polls keep `BatchMode`. A decline SIGTERMs that ssh
273 (a refused askpass is an empty password to OpenSSH, asked three times over) 326 (a refused askpass is an empty password to OpenSSH, asked three times over)
274 and ends the tile with `[prompt declined]`. 327 and ends the tile with `[prompt declined]`.
275 - **The layout sidecar is derived convenience, not authored intent.**
276 `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from
277 every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall
278 of one and neither saves nor restores. Restore is verbatim; the aspect
279 rule applies only when no saved tree exists. Healing is per-leaf against
280 the hosts' live lists. `client.hosts.load` stays strict (a host line is authored
281 intent) while the sidecar is lenient — every failure degrades silently to
282 the default cut. The asymmetry is deliberate: a bad host line is a user's
283 mistake to fix; a stale layout is a convenience to forget.
284 - **Detach is a goodbye, not a tear.** A pump that has sent its detach frame 328 - **Detach is a goodbye, not a tear.** A pump that has sent its detach frame
285 never redials — redial checks `detach_ack` — so the daemon's close after 329 never redials — redial checks `detach_ack` — so the daemon's close after
286 a detach is final, not a network event to recover from. 330 a detach is final, not a network event to recover from.
README.md
Old New
@@ -42,27 +42,35 @@ mux d start & # the daemon: hosts your shell and the authoritative screen
42 mux # attach 42 mux # attach
43 ``` 43 ```
44 44
45 **`mux` IS the wall, and the wall is a list of daemons.** The file 45 **`mux` IS the wall, and the wall is your layout.** The file
46 `~/.local/state/mux/hosts` names machines — your own and every box you have 46 `~/.local/state/mux/layout` names the panes you have opened — each one a
47 attached to — one spelling per line, and the tiles are the sessions those 47 `HOST#SESSION` leaf, in the tree you arranged them in — and every `mux` on
48 daemons have live *right now*. There is one list and one navigation axis: 48 this machine opens exactly that. `~/.local/state/mux/hosts` is the other
49 `Ctrl-\` `1`-`9` focuses a tile, `Ctrl-\` `n`/`p` walk them all, `Ctrl-\` 49 file: the daemons this machine may browse, your own and every box you have
50 `d` leaves. Every tile's replica stays hot whether you are looking at it or 50 attached to, one spelling per line. There is one list and one navigation
51 not, so moving focus is a repaint and not a re-attach — no child process, 51 axis: `Ctrl-\` `1`-`9` focuses a pane, `Ctrl-\` `n`/`p` walk them all,
52 no second screen. 52 `Ctrl-\` `d` leaves. Every pane's replica stays hot whether you are looking
53 53 at it or not, so moving focus is a repaint and not a re-attach — no child
54 The list is the daemons' answer, not your history: `mux` asks every listed 54 process, no second screen.
55 daemon once a second what it is hosting, so a session a split, a `mux a` or 55
56 somebody else's client births turns up as a tile by itself, and a session 56 Sessions live on daemons, and a daemon may have more of them than your wall
57 that ends takes its tile with it. Nothing on the wall can outlive the shell 57 shows: a session born by `mux a`, by a browser, or by another machine is on
58 behind it, and nothing in the file can bring one back. 58 no wall until you put it there. `mux` still asks every listed daemon once a
59 second what it is hosting, but that answer only GRADES the panes you
60 authored — it wakes one whose session is there, marks `gone` one whose
61 session the daemon no longer lists, and takes away one whose shell exited.
62 It never adds a pane. `Ctrl-\` `s` lists your daemons, Enter on one lists
63 its sessions with who else holds each, Enter on a session adds it as a
64 pane, `c` starts a new one there, `x` ends one (asking first when someone
65 else holds it), Esc backs out a level. On the wall, `Ctrl-\` `x` takes the
66 focused pane off this wall and ends nothing.
59 67
60 `mux` on a machine that has never run it records your own daemon (the line 68 `mux` on a machine that has never run it records your own daemon (the line
61 `--sock <default>`, starting it if nothing is listening) and opens zoomed on 69 `--sock <default>`, starting it if nothing is listening) and opens one pane
62 its session `0` — a first run is still just a shell. After that, bare `mux` 70 on its session `0` — a first run is still just a shell. After that, bare
63 opens on the wall itself, and `mux HOST` opens zoomed on HOST's session `0`, 71 `mux` opens the layout you left, and `mux HOST` opens zoomed on HOST's
64 adding HOST to the file if it is new, with the rest of the list on the wall 72 session `0`, adding HOST to the hosts file and the pane to your layout if
65 behind it. Every host dials at once, in the background, so a slow ssh 73 they were not there. Every host dials at once, in the background, so a slow ssh
66 somewhere else never holds up the one you asked for; `Ctrl-\` `w` zooms back 74 somewhere else never holds up the one you asked for; `Ctrl-\` `w` zooms back
67 out to see them. A wall of one tile still feels like a plain client, because 75 out to see them. A wall of one tile still feels like a plain client, because
68 a wall of one tile is what a plain client always was — except that on a 76 a wall of one tile is what a plain client always was — except that on a
@@ -71,12 +79,15 @@ view keeps the session's name and state in sight. Only a piped `mux` is
71 bare bytes. 79 bare bytes.
72 80
73 A split births a session: not a window onto an existing one, but its own 81 A split births a session: not a window onto an existing one, but its own
74 shell on the focused tile's daemon, laid out beside or below the focus. The 82 shell on the focused pane's daemon, laid out beside or below the focus. A
75 wall places tiles side by side when the terminal is at least twice as wide 83 wall with no layout to open places its panes side by side when the terminal
76 as it is tall, and stacks them otherwise. Fullscreen is a layout where the 84 is at least twice as wide as it is tall, and stacks them otherwise — after
77 focused pane takes the whole terminal; resize mode trades cells between the 85 that the tree is yours. Fullscreen is a layout where the focused pane takes
78 focus and its neighbors, one per keystroke. The layout survives detach and 86 the whole terminal; resize mode trades cells between the focus and its
79 comes back the next time you open a wall on a terminal. 87 neighbors, one per keystroke. Every change to the panes or the tree — a
88 birth, a split, a resize, an `x`, a detach — writes the layout file, so a
89 second terminal on this machine opens what you last arranged and a wall
90 that is killed loses nothing it had committed.
80 91
81 The focused tile has the terminal, and only the prefix is held back: 92 The focused tile has the terminal, and only the prefix is held back:
82 93
@@ -84,48 +95,58 @@ The focused tile has the terminal, and only the prefix is held back:
84 |---|---| 95 |---|---|
85 | `Ctrl-\` | command prefix — press it, then a command key | 96 | `Ctrl-\` | command prefix — press it, then a command key |
86 | `Ctrl-\` `d` (or `Ctrl-\` `Ctrl-\`) | disconnect: leave every tile and leave mux, interrupt nobody (every session keeps running) | 97 | `Ctrl-\` `d` (or `Ctrl-\` `Ctrl-\`) | disconnect: leave every tile and leave mux, interrupt nobody (every session keeps running) |
87 | `Ctrl-\` `c` | create a session on the focused tile's daemon, add a tile, focus it | 98 | `Ctrl-\` `c` | create a session on the focused pane's daemon, add a pane for it, focus it |
88 | `Ctrl-\` `n` / `Ctrl-\` `p` | focus the next / previous tile on the wall, wrapping | 99 | `Ctrl-\` `n` / `Ctrl-\` `p` | focus the next / previous pane on the wall, wrapping |
89 | `Ctrl-\` `h` `j` `k` `l` | focus the pane left / down / up / right | 100 | `Ctrl-\` `h` `j` `k` `l` | focus the pane left / down / up / right |
90 | `Ctrl-\` `\|` (or `\`) | split right: new session in a pane to the right | 101 | `Ctrl-\` `\|` (or `\`) | split right: new session in a pane to the right |
91 | `Ctrl-\` `-` | split below: new session in a pane below | 102 | `Ctrl-\` `-` | split below: new session in a pane below |
92 | `Ctrl-\` `f` | fullscreen the focused pane (toggle) | 103 | `Ctrl-\` `f` | fullscreen the focused pane (toggle) |
93 | `Ctrl-\` `r` | resize mode: `h`/`l` shrink/grow the focus's width, `k`/`j` its height, one cell per press; Esc leaves silently, any other key leaves and types | 104 | `Ctrl-\` `r` | resize mode: `h`/`l` shrink/grow the focus's width, `k`/`j` its height, one cell per press; Esc leaves silently, any other key leaves and types |
94 | `Ctrl-\` `1`-`9` | focus tile N — the digit on that tile's label bar. A new tile takes the lowest free digit, so end 2 of 1 2 3 and the next session is 2 again | 105 | `Ctrl-\` `1`-`9` | focus pane N — the digit on that pane's label bar. A new pane takes the lowest free digit, so remove 2 of 1 2 3 and the next pane is 2 again |
95 | `Ctrl-\` `x` | end the focused session — the daemon hangs up its shell and every attached client sees the exit. With other clients on it the daemon refuses the first press and says how many; a second `x` within 3 seconds forces it | 106 | `Ctrl-\` `x` | take the focused pane off this wall — the layout is written without it and the session keeps running on its daemon for whoever else holds it. Ending a session is the picker's `x` |
96 | `Ctrl-\` `w` | zoom out to the wall | 107 | `Ctrl-\` `w` | zoom out to the wall |
97 | `Ctrl-\` `s` | the host picker: a popup listing every daemon on the wall with what its last poll said — `N sessions`, `no sessions`, `unreachable`, `connecting`. `j`/`k` or the arrows move, `1`-`9` pick a row, Enter or `c` starts a session on the selected machine and closes, `x` forgets it (the file line goes, the sessions keep running), `a` adds one by spelling — `HOST`, `quic://HOST[:PORT]` or `--sock PATH`, Enter adds and Esc returns to the rows. Esc or `s` closes — and an arrow key split across two reads reads as a bare Escape, so it closes too. The spelling editor takes printable ASCII only, so a path or hostname with any other byte in it goes in the hosts file or on argv instead. While the picker is open every key is its own: nothing reaches a session | 108 | `Ctrl-\` `s` | the picker, and the only door a session comes onto the wall through. Level one is every daemon in your hosts file with what its last poll said — `N sessions`, `no sessions`, `unreachable`, `connecting`. `j`/`k` or the arrows move, `1`-`9` pick a row, Enter opens that daemon's SESSIONS, `c` starts a new session there and adds it, `x` forgets the host (the file line, its poller and its panes go; the sessions keep running), `a` adds a host by spelling — `HOST`, `quic://HOST[:PORT]` or `--sock PATH`, Enter adds and Esc returns to the rows. Level two is one row per session that daemon has, reading `on this wall` when it is already a pane and `K client(s)` for everyone holding it, this wall's own pane counted: Enter adds it as a pane (or just zooms to it), `c` still births, `x` ends the session with the daemon's two-step, Esc backs out to the hosts. Esc or `s` at level one closes — and an arrow key split across two reads reads as a bare Escape, so it closes too. The spelling editor takes printable ASCII only, so a path or hostname with any other byte in it goes in the hosts file or on argv instead. While the picker is open every key is its own: nothing reaches a session |
98 | `Shift+PageUp` / `Shift+PageDown` | scrollback, a screen at a time (any other key returns to live) | 109 | `Shift+PageUp` / `Shift+PageDown` | scrollback, a screen at a time (any other key returns to live) |
99 | mouse wheel | scrollback, three rows a notch (arrow keys to a full-screen app) | 110 | mouse wheel | scrollback, three rows a notch (arrow keys to a full-screen app) |
100 | drag with the left button | select what it crosses, copy on release | 111 | drag with the left button | select what it crosses, copy on release |
101 | any key not behind `Ctrl-\` | goes to the focused tile | 112 | any key not behind `Ctrl-\` | goes to the focused tile |
102 113
103 `n`, `p` and the digits walk the same list, because there is only one: 114 `n`, `p` and the digits walk the same list, because there is only one: your
104 the wall's tiles, across every daemon on it, in host order and then in each 115 layout's panes, in tree order, whatever daemons they came from. `c`, `|`
105 daemon's own slot order. `c`, `|` and `-` ask the FOCUSED tile's daemon for 116 and `-` ask the FOCUSED pane's daemon for a new session, so a split on a
106 a new session, so a split on a remote tile births a shell on that machine 117 remote pane births a shell on that machine and not on yours.
107 and not on yours. 118
108 119 Three doors put a pane on the wall and nothing else does: the layout file
109 `x` is the other half of `c`, and it is not a wall edit: it ends the 120 on start, the picker, and a chord split — plus `mux HOST` on the command
110 session. The tile leaves when the daemon's list no longer has it, not when 121 line, which is the entry pane and takes the same path. A session born
111 the key is pressed, so what you see is always what the daemons have. 122 anywhere else is real and running and on no wall of yours until you add it.
112 Because ending interrupts anyone else watching, the daemon refuses a first 123
113 `x` on a session with other clients attached and says how many — the rail 124 `x` on the wall is a wall edit and only that: the pane goes, the layout is
114 reads `[1 other attached - x again to end]` — and a second `x` within three 125 written without it, the client says goodbye to that session, and the shell
115 seconds forces it. A shell that ignores the hangup is killed half a second 126 behind it keeps running for whoever else holds it — the rail reads
116 later; there is no session that cannot be ended. Dropping a whole MACHINE 127 `[pane removed - the session is still on its daemon]`. ENDING a session is
117 is not a chord: it is rarer than ending a shell and must not sit one key 128 the picker's `x` at the session level, which is where you can see who else
118 from `n`. From inside the wall it is the picker's `x` (`Ctrl-\` `s`, then 129 holds it first. Because ending interrupts anyone else watching, the daemon
119 `x`); from the command line it is `mux hosts rm`. 130 refuses a first `x` on a session with other clients attached and says how
120 131 many — `[work: 1 other attached - x again to end]` — and a second `x` on
121 `x` ends a session, never a box. A daemon whose last session ends stays up 132 the same row within three seconds forces it. A shell that ignores the
122 and empty, and the next `mux HOST` is born into it — so emptying a machine 133 hangup is killed half a second later; there is no session that cannot be
123 costs you nothing but the shell you meant to end. Ending the daemon itself 134 ended. That count on the row is EVERY holder including this wall's own
124 is `mux d stop`. 135 pane, so ending a session you alone have open is two presses.
125 136
126 A session whose shell exits takes its tile off the wall. On a terminal the 137 Dropping a whole MACHINE is not a chord either: from inside the wall it is
127 LAST tile's exit leaves you on the empty wall with the picker over it, not 138 the picker's `x` at the host level (`Ctrl-\` `s`, then `x`), and from the
128 back in your shell: `Ctrl-\` `d` is how you leave. A piped `mux` has no wall 139 command line `mux hosts rm`.
140
141 The picker's `x` ends a session, never a box. A daemon whose last session
142 ends stays up and empty, and the next `mux HOST` is born into it — so
143 emptying a machine costs you nothing but the shell you meant to end. Ending
144 the daemon itself is `mux d stop`.
145
146 A session whose shell exits takes its pane off the wall — you ended it, so
147 the layout is written without it. On a terminal the LAST pane's exit leaves
148 you on the empty wall with the picker over it, not back in your shell:
149 `Ctrl-\` `d` is how you leave. A piped `mux` has no wall
129 to leave you on, so it exits with that shell's code — `mux` stays something 150 to leave you on, so it exits with that shell's code — `mux` stays something
130 you can put in a script. 151 you can put in a script.
131 152
@@ -157,13 +178,14 @@ does worse.
157 One daemon hosts up to thirty-two named sessions; the first is called `0`. 178 One daemon hosts up to thirty-two named sessions; the first is called `0`.
158 `Ctrl-\` `c` creates one named after the lowest free number and focuses it, 179 `Ctrl-\` `c` creates one named after the lowest free number and focuses it,
159 and `mux --session NAME` attaches to any of them by name (creating it if 180 and `mux --session NAME` attaches to any of them by name (creating it if
160 there is room). One wall holds up to thirty-two tiles however many daemons 181 there is room). One wall holds up to thirty-two panes however many daemons
161 they came from; past that the rail says `[+N not shown]` and the digits do 182 they came from, and a layout file with more leaves than that is refused
162 not reach them. Not worth designing past until someone has thirty-three 183 rather than half-seated. Not worth designing past until someone has
163 shells. 184 thirty-three shells.
164 185
165 To see which sessions are alive without attaching to one: `mux hosts` prints 186 To see which sessions are alive without attaching to one: `mux hosts` prints
166 every daemon on the wall with its live session count, and `mux d stats` names 187 every daemon in your hosts file with its live session count, `Ctrl-\` `s`
188 then Enter lists one daemon's sessions by name, and `mux d stats` names
167 every session of one daemon with the number of clients watching it. `stats` 189 every session of one daemon with the number of clients watching it. `stats`
168 is the question to ask when `mux` will not let you in — thirty-two sessions 190 is the question to ask when `mux` will not let you in — thirty-two sessions
169 is the table, and the thirty-third name has nowhere to go. 191 is the table, and the thirty-third name has nowhere to go.
@@ -286,11 +308,11 @@ being slow is not what separates them.
286 (or `mux -A quic://HOST`, `mux -A --sock PATH`, or a bare `mux -A` on your 308 (or `mux -A quic://HOST`, `mux -A --sock PATH`, or a bare `mux -A` on your
287 own daemon) forwards on that tile, and a sibling grown from it by chord 309 own daemon) forwards on that tile, and a sibling grown from it by chord
288 (`Ctrl-\ c`, `|`, `-`) inherits it — same daemon, same host, nothing newly 310 (`Ctrl-\ c`, `|`, `-`) inherits it — same daemon, same host, nothing newly
289 exposed. Every OTHER tile on the wall arrives from a daemon's session list 311 exposed. Every OTHER pane on the wall arrives from your layout file or from
290 rather than from your command line, and those never offer an agent: the 312 the picker rather than from your command line, and those never offer an
291 wall is a list of machines, and a machine you have never typed the flag for 313 agent: a machine you have never typed the flag for must not get your keys
292 must not get your keys because it happens to be on it. `mux a` never 314 because a pane of yours happens to sit on it. `mux a` never forwards, under
293 forwards, under any flag. 315 any flag.
294 316
295 ## Quick start, remote over QUIC 317 ## Quick start, remote over QUIC
296 318
@@ -320,24 +342,37 @@ there is no unauthenticated mode to fall back to.
320 ## Quick start, a wall of sessions in a browser 342 ## Quick start, a wall of sessions in a browser
321 343
322 ```sh 344 ```sh
323 mux web # serves your hosts file on 127.0.0.1:7681 345 mux web # serves your layout on 127.0.0.1:7681
324 mux web HOST quic://OTHER --sock /tmp/s.sock # ...adds those daemons first, then serves 346 mux web HOST quic://OTHER --sock /tmp/s.sock # ...adds those daemons first, then serves
325 ``` 347 ```
326 348
327 **The wall is your hosts file, in the browser exactly as in the terminal.** 349 **The wall is your layout, in the browser exactly as in the terminal.**
328 Each line of `$XDG_STATE_HOME/mux/hosts` is a DAEMON — `HOST`, 350 The hub reads the same `$XDG_STATE_HOME/mux/layout` a terminal wall does
329 `quic://HOST[:PORT]` or `--sock PATH` — and the tiles are whatever those 351 and serves its panes in tree order, each labelled with its daemon — `HOST`,
330 daemons have live, polled once a second. A session started anywhere, by 352 `quic://HOST[:PORT]` or `--sock PATH`, a line of `$XDG_STATE_HOME/mux/hosts`
331 anyone, appears; one that ends disappears. A `HOST` on the command line is 353 verbatim — and its session. A session started anywhere else appears on no
332 added to the file (the same thing `mux HOST` does) and then the file is the 354 tile; a pane whose daemon stops listing its session reads `gone` and stays.
333 wall; `#SESSION` is refused, because a host line may not name something that 355 A `HOST` on the command line is added to the hosts file (the same thing
334 could be resurrected. 356 `mux HOST` does) and then the layout is the wall; `#SESSION` on a HOST
357 argument is refused, because a *host* line may not name something that
358 could be resurrected — sessions are named by the layout file instead. A
359 layout the hub cannot parse is reported as `mux web: layout ignored (PATH):
360 LINE` and it serves an empty wall rather than a guess.
335 361
336 Click a tile to focus it and type in it; click the shade to come back. `+` 362 Click a tile to focus it and type in it; click the shade to come back. `+`
337 on a tile starts a new session on that tile's daemon and drops you into it. 363 on a tile starts a new session on that tile's daemon, writes the new leaf
338 That is the whole of what the page can do to the wall: adding and removing 364 into the layout beside that pane, and drops you into it — so a birth in the
339 DAEMONS is `mux hosts add|rm` or the CLI picker's `a`/`x`, and ending a 365 browser is on the next terminal wall and the reverse. It checks the file
340 session is `Ctrl-\ x` in the terminal or `exit` in the shell. 366 for room and for a duplicate before it dials, so a full wall or a repeat
367 never leaves a session nobody asked for behind a 502; a second `+` inside
368 one poll interval is a 409. That is the whole of what the page can do to
369 the wall: adding and removing DAEMONS is `mux hosts add|rm` or the CLI
370 picker's `a`/`x`, removing a pane is `Ctrl-\ x`, and ending a session is
371 the CLI picker's `x` or `exit` in the shell.
372
373 Two writers in the same instant lose one update — the hub and a terminal
374 wall each read, modify and atomically rename the file, exactly as the hosts
375 file has always worked.
341 376
342 Localhost only, deliberately: to see it from another machine, forward it — 377 Localhost only, deliberately: to see it from another machine, forward it —
343 `ssh -L 7681:127.0.0.1:7681 HOST`. 378 `ssh -L 7681:127.0.0.1:7681 HOST`.
@@ -345,18 +380,23 @@ Localhost only, deliberately: to see it from another machine, forward it —
345 An older `~/.local/state/mux/wall` is ignored and can be deleted; the hub 380 An older `~/.local/state/mux/wall` is ignored and can be deleted; the hub
346 kept its own list of sessions there until this version. 381 kept its own list of sessions there until this version.
347 382
348 ## The wall is your hosts 383 ## Two files: your daemons, and your wall
384
385 `$XDG_STATE_HOME/mux/hosts` is the daemons this machine may browse.
386 `$XDG_STATE_HOME/mux/layout` is the wall itself — the panes you opened, in
387 the tree you put them in. The first says what you *can* reach; the second
388 says what you *see*.
349 389
350 ```sh 390 ```sh
351 mux hosts # the daemons on the wall, one per line, with live session counts 391 mux hosts # the daemons you can browse, one per line, with live session counts
352 mux hosts add HOST # put a daemon on the wall without opening it 392 mux hosts add HOST # record a daemon without opening it
353 mux hosts add --sock /tmp/s.sock 393 mux hosts add --sock /tmp/s.sock
354 mux hosts add quic://box:4433 394 mux hosts add quic://box:4433
355 mux hosts rm HOST # take one off (its sessions keep running) 395 mux hosts rm HOST # take one off (its sessions keep running)
356 ``` 396 ```
357 397
358 The file is `~/.local/state/mux/hosts` (`$XDG_STATE_HOME/mux/hosts`), one 398 The file is `~/.local/state/mux/hosts` (`$XDG_STATE_HOME/mux/hosts`), one
359 daemon per line, in wall order: 399 daemon per line, in picker order:
360 400
361 ``` 401 ```
362 --sock /run/user/1000/muxd.sock 402 --sock /run/user/1000/muxd.sock
@@ -365,17 +405,17 @@ quic://gate.example:4433
365 ``` 405 ```
366 406
367 A line names a **machine**, never a session: `#NAME` on a spelling is 407 A line names a **machine**, never a session: `#NAME` on a spelling is
368 refused, because the wall shows every session a daemon has and there is 408 refused, because naming sessions is the layout file's job and a host line
369 nothing left for a `#` to pick. That is also what makes the file safe to 409 must name nothing that anything could resurrect a shell from. Lines are
370 keep — there is no line in it that anything could resurrect a shell from. 410 deduped byte for byte, so the same box written `box` and `quic://box` is
371 Lines are deduped byte for byte, so the same box written `box` and 411 two hosts, deliberately: they are two different ways in.
372 `quic://box` is two hosts, deliberately: they are two different ways in.
373 412
374 Every `mux` that names a target records that target — `mux HOST`, 413 Every `mux` that names a target records that target — `mux HOST`,
375 `mux quic://HOST`, `mux --sock PATH`, and a bare `mux`, which records your 414 `mux quic://HOST`, `mux --sock PATH`, and a bare `mux`, which records your
376 own daemon. `--via CMD` never records one, because an arbitrary command is 415 own daemon. `--via CMD` never records one, because an arbitrary command is
377 not an address and the grammar has no line to write for it; the session is 416 not an address and the grammar has no line to write for it; the session is
378 real, it is just not a machine the wall can name. `mux a` records nothing 417 real, it is just not a machine the hosts file can name — which also means a
418 `--via` session is on no layout and no wall. `mux a` records nothing
379 either (it attaches at 0×0 and claims nothing). Writing the file is best 419 either (it attaches at 0×0 and claims nothing). Writing the file is best
380 effort — if it cannot be written you get one warning line and the attach 420 effort — if it cannot be written you get one warning line and the attach
381 happens anyway. 421 happens anyway.
@@ -402,51 +442,98 @@ absent sockets). `mux hosts rm` reads the file verbatim too, so a line
402 you hand-edited into something no longer legal can be typed back byte for 442 you hand-edited into something no longer legal can be typed back byte for
403 byte and removed instead of wedging every command that touches the wall. 443 byte and removed instead of wedging every command that touches the wall.
404 444
405 A daemon that is not answering contributes **nothing** to the wall: not one 445 A daemon that is not answering keeps **every pane you put on it**, wearing
406 dead tile per session it used to have, and no placeholder of its own either. 446 `unreachable`, for as long as the box is dark — an eight-pane setup is not
407 Its line stays in the file and its poller keeps redialling, forever and on 447 worth one quiet machine. Its line stays in the file and its poller keeps
408 purpose — a wall is a thing you leave up, and a laptop that wakes an hour 448 redialling, forever and on purpose; a wall is a thing you leave up, and a
409 later should find its sessions back on it. The cost is that a daemon that is 449 laptop that wakes an hour later should find its panes back on it. The cost
410 never coming back looks exactly like one whose network is merely down. 450 is that a daemon that is never coming back looks exactly like one whose
411 Nothing times it out; `mux hosts rm SPELLING` is how a line leaves. Your own 451 network is merely down. Nothing times it out; `mux hosts rm SPELLING`, or
412 daemon is the exception that is started rather than waited for: a listed 452 the picker's `x` at the host level, is how a line leaves. Your own daemon is
413 local `--sock <default>` that nothing is serving is started for you, because 453 the exception that is started rather than waited for: a listed local
414 it dies on every reboot while its line lives on. 454 `--sock <default>` that nothing is serving is started for you, because it
415 455 dies on every reboot while its line lives on.
416 A host that answers with an EMPTY list contributes nothing either, and that 456
417 is an ordinary state rather than a remote curiosity: a daemon lives until 457 A host that is REACHABLE and answers without one of your sessions is a
418 `mux d stop`, so one whose last session you ended sits there with nothing to 458 different sentence: that pane wears `gone`, because the daemon said no
419 show until someone is born into it again. A wall on which no host has a live 459 rather than nothing. Enter re-creates the session in that pane's own rect,
420 session is empty, and says so on one line rather than showing a blank screen 460 `x` takes the pane off the wall, and every other key at it is eaten. A wall
421 that reads as hung. 461 of only gone panes is not an empty wall.
422 462
423 That is what the **host picker** is for. The wall shows live sessions and 463 A daemon with no sessions at all is an ordinary state rather than a remote
424 nothing else, so the machines behind them live in a popup over it: `Ctrl-\` 464 curiosity: a daemon lives until `mux d stop`, so one whose last session you
425 `s` lists every daemon in the hosts file with what its last poll said, and 465 ended sits there with nothing to show until someone is born into it again.
426 Enter starts a session on the one you select — including a machine that 466 It contributes no pane, and a host with no pane of yours contributes nothing
427 currently has none, which is the whole point of `no sessions` being a row 467 either — the wall is what you authored, not what the daemons happen to have.
428 you can act on. `x` there forgets a host (the same edit `mux hosts rm` 468
429 makes, and it ends nothing), and `a` adds one. An empty wall opens the 469 A wall with no panes says so on one line rather than showing a blank screen
430 picker by itself, because a blank screen is no place to act from; Esc 470 that reads as hung, and opens the **picker** over it by itself, because a
431 leaves the one-line text standing. 471 blank screen is no place to act from; Esc leaves the one-line text standing.
432 472 That is the state you get on a machine whose hosts file has lines but whose
433 `Ctrl-\` `x` on the last tile of a terminal wall therefore leaves you on 473 layout file does not exist yet — nothing reconstructs a wall for you.
434 that empty wall with the picker open, not back in your shell — `x` ends a 474
435 session, never mux. A piped `mux` is unchanged: it is a wall of one and it 475 `Ctrl-\` `x` on the last pane of a terminal wall therefore leaves you on
436 still exits with the shell's exit code. 476 that empty wall with the picker open, not back in your shell — `x` edits
437 477 the wall, never mux. A piped `mux` is unchanged: it is a wall of one, it
438 **A daemon too old to be asked shows nothing too.** Listing a daemon's live 478 neither saves nor restores a layout, and it still exits with the shell's
439 sessions is a new question on the wire, and a daemon from before this change 479 exit code.
440 answers it with the silence it answers every unknown frame with. It is up, 480
441 `mux --sock PATH` still attaches to it, and none of its sessions reach the 481 **A daemon too old to be asked shows nothing.** Listing a daemon's live
442 wall, so today `mux d upgrade` (or a restart) on every box you put on the 482 sessions is a question a daemon from before that change answers with the
443 wall is what fills it in. 483 silence it answers every unknown frame with. It is up, `mux --sock PATH`
444 484 still attaches to it, and the picker cannot show you its sessions to add,
445 Restarting a daemon re-creates **nothing**. The old wall would have brought 485 so `mux d upgrade` (or a restart) on every box you browse is what fills it
446 your sessions back from the file; this one cannot, because the file has no 486 in. Counting who ELSE holds a session is newer still: a daemon that does
447 session in it to bring back. A host's tiles leave with it and the wall heals 487 not send the count shows none, and its sessions cannot be ended from the
448 to whatever that daemon has when it returns — which, for a daemon someone 488 picker at all (`[daemon too old to end a session]`).
449 just started, is one session called `0`. 489
490 Restarting a daemon re-creates **nothing**. Your panes on it stay, wearing
491 `gone` once it answers without their sessions; Enter on one starts that
492 session again, in the pane's own rect. Nothing does that for you, because a
493 shell that came back empty is not the shell you left.
494
495 ### The layout file
496
497 `~/.local/state/mux/layout` (`$XDG_STATE_HOME/mux/layout`) is the wall.
498 Every leaf is `HOST#SESSION`, where `HOST` is a line of the hosts file
499 spelled exactly as it appears there:
500
501 ```
502 mux-layout 1
503 beside 0
504 leaf 40 --sock /run/user/1000/muxd.sock#0
505 stacked 40
506 leaf 12 box#work
507 leaf 12 quic://gate.example:4433#2
508 focus 0
509 ```
510
511 A first run on a new machine writes the smallest one there is — a hosts
512 file of `--sock <default>` and a layout of `leaf 0 --sock <default>#0` with
513 `focus 0`.
514
515 It is written by any wall on a terminal, on every change — a birth, a
516 split, a resize, an `x`, a detach — so two terminals on this machine see
517 each other's adds on their next start. `focus N` is the one line in it that
518 is not authored: it records where the focus was, and it can go stale between
519 changes to the tree.
520
521 Reading it is strict, the way the hosts file is strict: a leaf naming a host
522 the hosts file does not have, a leaf with no `#SESSION` or a bad session
523 name, a repeated leaf, more leaves than a wall holds, or text that is not a
524 layout REFUSES the whole file. `mux` says `mux: layout ignored (PATH): LINE`
525 with the line it gave up on and then starts as if the file were missing — it
526 does not seat half a wall, because silently dropping panes you authored is
527 how you lose one. An empty file is not a bad file; it is simply no wall.
528
529 There is one thing a wall will not save: a run whose panes it could not all
530 seat. A terminal too small for the tree, or a layout naming the very session
531 this `mux` is running inside, drops those leaves for the run and says
532 `[layout not saved: terminal too small for 2 of its panes]` (or
533 `[layout not saved: it names this shell's own session]`) on the notice line.
534 That run then writes NOTHING — the file you have is the file you keep, and
535 the next start in a window that fits saves again. A save that fails for any
536 other reason says `[layout not saved: <reason>]` in the same place.
450 537
451 `mux hosts add`/`rm` are file operations only: neither dials, and `rm` never 538 `mux hosts add`/`rm` are file operations only: neither dials, and `rm` never
452 touches the session — the shells on that machine go on running and `mux 539 touches the session — the shells on that machine go on running and `mux
@@ -474,7 +561,7 @@ the `Ctrl-\` prefix is held back. `Ctrl-\` `1`-`9` focuses a tile, `Ctrl-\`
474 `n`/`p` walk them, `Ctrl-\` `c` creates a session on the focused tile's 561 `n`/`p` walk them, `Ctrl-\` `c` creates a session on the focused tile's
475 daemon, `Ctrl-\` `h`/`j`/`k`/`l` move focus between panes, `Ctrl-\` `|`/`-` 562 daemon, `Ctrl-\` `h`/`j`/`k`/`l` move focus between panes, `Ctrl-\` `|`/`-`
476 split right or below, `Ctrl-\` `f` fullscreen, `Ctrl-\` `r` resize mode, 563 split right or below, `Ctrl-\` `f` fullscreen, `Ctrl-\` `r` resize mode,
477 `Ctrl-\` `s` opens the host picker, `Ctrl-\` `x` ends the focused session, 564 `Ctrl-\` `s` opens the picker, `Ctrl-\` `x` removes the focused pane,
478 `Ctrl-\` `w` zooms out, `Ctrl-\` `d` leaves. Any other command key after the prefix 565 `Ctrl-\` `w` zooms out, `Ctrl-\` `d` leaves. Any other command key after the prefix
479 is swallowed along with it, as in a plain client. 566 is swallowed along with it, as in a plain client.
480 567
@@ -500,17 +587,17 @@ a plain client's loop. The focused tile's Core owns the mouse modes; a tile
500 that is not focused holds none, so nothing it armed can report while you 587 that is not focused holds none, so nothing it armed can report while you
501 are looking elsewhere. 588 are looking elsewhere.
502 589
503 The pane layout is remembered — where the cuts were, which tile had the 590 The panes, where the cuts were and which pane had the focus live in
504 focus — under `$XDG_STATE_HOME/mux/layout`, saved by any wall you leave from 591 `$XDG_STATE_HOME/mux/layout` (above), written by any wall on a terminal on
505 a terminal and restored over whatever sessions the daemons turn out to have 592 every change and read back verbatim. It is authored intent, not derived
506 when you come back. It is derived convenience and not something you authored: 593 convenience: a file that will not parse is refused with its line rather than
507 every way it can fail (a missing file, a stale tree, a session that is no 594 degraded to a default cut, and the default cut — panes side by side on a
508 longer there) degrades quietly to the default cut, where the wall places 595 wide terminal, stacked on a tall one — is what you get only when there is no
509 tiles side by side on a wide terminal and stacks them on a tall one. 596 file to open.
510 597
511 A scripted `mux TARGET` — stdout on a pipe — is still a wall of one tile 598 A scripted `mux TARGET` — stdout on a pipe — is still a wall of one pane
512 whose rect is the whole terminal, writing exactly what the plain client 599 whose rect is the whole terminal, writing exactly what the plain client
513 wrote, with no other host dialled and no layout saved. A wall of MANY needs 600 wrote, with no other host dialled and no layout read or saved. A wall of MANY needs
514 a terminal to cut stripes from and says `mux: wall needs a terminal` if it 601 a terminal to cut stripes from and says `mux: wall needs a terminal` if it
515 has none. 602 has none.
516 603
@@ -569,8 +656,8 @@ execs the old one back and the old one carries on serving. Scrollback does
569 not survive the handover; the visible grid, the titles, the command marks 656 not survive the handover; the visible grid, the titles, the command marks
570 and the exit codes do. A daemon too old to know the request answers nothing, 657 and the exit codes do. A daemon too old to know the request answers nothing,
571 and `mux d upgrade` says so: `no reply: this daemon predates upgrade — stop 658 and `mux d upgrade` says so: `no reply: this daemon predates upgrade — stop
572 and run`. A session in the middle of being ended (`Ctrl-\` `x`, up to half a 659 and run`. A session in the middle of being ended (the picker's `x`, up to
573 second while its shell is being hung up) refuses the upgrade with `session 660 half a second while its shell is being hung up) refuses the upgrade with `session
574 ending, retry` rather than carrying a shell with no master across the exec. 661 ending, retry` rather than carrying a shell with no master across the exec.
575 662
576 The remote spelling is `mux d upgrade HOST`: one binary means the machine 663 The remote spelling is `mux d upgrade HOST`: one binary means the machine
@@ -591,6 +678,15 @@ execs its candidate as `mux d run`, and the verb is `start` now. `make install`
591 install directory on its way in — they are modes of `mux` now, and a 678 install directory on its way in — they are modes of `mux` now, and a
592 `muxd` left on PATH is a second daemon waiting for someone to start it. 679 `muxd` left on PATH is a second daemon waiting for someone to start it.
593 680
681 One more mixed-version caveat runs the other way, daemon newer than client.
682 A daemon now answers `sessions_req` with a `# holds NAME N` line per session
683 beside the names, which an older client skips as it skips the version line —
684 but a client at v0.0.1-17 or older sized its receive buffer for the names
685 alone (1101 bytes) and reads anything longer as a transport error. A new
686 daemon holding roughly fourteen or more sessions with 32-character names
687 therefore reads `unreachable` on such a client until that side is upgraded.
688 Upgrading the client end is the fix; there is nothing to do on the daemon.
689
594 Copy and paste work through the session: a mux drag and an application's own 690 Copy and paste work through the session: a mux drag and an application's own
595 OSC 52 write both reach your terminal's clipboard (including from a remote box over QUIC, 691 OSC 52 write both reach your terminal's clipboard (including from a remote box over QUIC,
596 where nothing else can), a paste arrives bracketed when the application 692 where nothing else can), a paste arrives bracketed when the application
docs/decisions.md
Old New
@@ -7854,3 +7854,174 @@ whose first draft passed against the bug, because `mux d stats` puts
7854 `session 0 clients=1` on the SAME line and a greedy match read that 7854 `session 0 clients=1` on the SAME line and a greedy match read that
7855 instead of `clients=N attaches=`. The mutant's series, 1 2 3 4 5 6 7 8, 7855 instead of `clients=N attaches=`. The mutant's series, 1 2 3 4 5 6 7 8,
7856 is what the leg now prints when it fails. 7856 is what the leg now prints when it fails.
7857
7858 ## 2026-09-02 — the wall is the layout: panes are authored, the poll only grades
7859
7860 **The report.** "by default wall should just be the layout. I connect to 2
7861 local and 3 remotes; every time I type `mux` I expect to view that. If I go
7862 to a new machine and type `mux`, I expect to view only 1 local session
7863 because I've never connected before." What the product did instead: a wall
7864 listed every live session of every daemon in the hosts file, tiles came from
7865 each daemon's own `sessions_req` once a second, and the layout sidecar was a
7866 derived convenience healed against those lists. A bare `mux` on the laptop
7867 therefore showed the two sessions this box was VIEWING, because they were
7868 the laptop daemon's live sessions, and a session born by `mux a`, a browser
7869 or another device turned up on every wall that listed its daemon.
7870
7871 **The shapes on the table.** Three. (1) Leave the live-list wall and add a
7872 per-device hide list — rejected: it makes the default wrong and asks the
7873 user to keep subtracting from it forever, and a new session on a shared
7874 daemon still arrives unannounced. (2) Keep the live-list wall and add a
7875 `mux HOST#SESSION` argv grammar for the panes you want — rejected on
7876 ergonomics by the user: naming a session on the command line is not how
7877 anyone finds one, and it leaves the default wall untouched anyway. (3) Flip
7878 the layout file from derived to AUTHORED and make it the only source of
7879 tiles. The flip won because it removes a source of tiles rather than adding
7880 a filter over one: with the file authoritative there is nothing to subtract,
7881 and the poll has exactly one job left, which is to grade the panes that are
7882 already there. It also deleted the duplicated poll-planner filed as collab
7883 68d4700e — `planHostDiff`'s `birth` arm and `applyList`'s in the hub both
7884 went, leaving one grading rule per front instead of two birthing ones.
7885
7886 **The strictness reversal.** Until now `hosts` was strict (a bad line
7887 refuses `mux` with rc 2) and the layout sidecar was lenient (every failure
7888 degraded silently to the default cut). The reason was that a host line is
7889 authored intent and a layout was not. The layout is authored intent now, so
7890 it is strict too: `seedLayout` returns `.plan`, `.refused LINE` or `.none`,
7891 and a refusal prints `mux: layout ignored (PATH): LINE` and starts as if the
7892 file were missing. Refused: a leaf naming a host the hosts file does not
7893 have, a leaf with no `#SESSION` or a bad session name, a repeated leaf (a
7894 second leaf spelling the entry counts), more leaves than `layout.max_leaves`
7895 (32, shared with `wallview.max_tiles`), or text `layout.parseReporting`
7896 gives up on — which reports the file line it stopped at, not the header.
7897 Whitespace-only is `.none`: nothing was authored, so there is nothing to
7898 name in a refusal. Seating PART of a wall is the one thing that must not
7899 happen, because a user who loses four of six panes has no undo and no line
7900 to fix.
7901
7902 **The trimmed-seed no-save ruling.** A seed that could not seat everything
7903 the file named — a terminal too small for the tree, or a leaf naming the
7904 very session this `mux` runs inside — sets `Shared.layout_path` null for the
7905 WHOLE run and says which on the notice line (`[layout not saved: terminal
7906 too small for N of its panes]` / `[layout not saved: it names this shell's
7907 own session]`). It does not write a trimmed file. `persist` serializes the
7908 tree it HAS, so the first save of such a run would drop the leaves the wall
7909 left out: the user's other panes gone before a key was pressed. Stopping the
7910 whole run's saving is the conservative half of that trade — a resize you
7911 made in a too-small window is forgotten, and the panes you authored survive.
7912 The two sentences are counted apart (`SeedPlan.dropped` vs `dropped_self`)
7913 because a wall that blamed the terminal's size for a shell's own stripe
7914 would send the user resizing a window that was never the problem. In the
7915 same spirit a save FAILURE is a notice, not a `std.debug.print`: every save
7916 runs under the alternate screen, so a stderr line would land in the middle
7917 of whichever pane the cursor was in and stay there until a repaint.
7918
7919 **`# holds NAME N`, and why a `#` line.** The picker's session rows need to
7920 say who else is on a session before you end it, and that is the only wire
7921 addition in the change. `sessions_reply` was names one per line plus a
7922 `# mux VERSION` meta line; it now carries one `# holds NAME N` line per
7923 session under the SAME version gate as the meta line, so a daemon with no
7924 version to state sends a byte-identical old payload. A `#` line rather than
7925 a new frame or a new field because `sessionsIter` already yields only lines
7926 that are valid session names: an old client skips the holds lines exactly as
7927 it skips the meta line, an old daemon sends none, and `parseSessionsHolds`
7928 reads that as unknown rather than as zero — a zero would read as "safe to
7929 end". The count is EVERY holder, the asker included.
7930
7931 **The count includes this wall's own pane (ruling).** It is a number to
7932 READ, never a verdict. Whether an end goes through is judged by the DAEMON
7933 against the OTHERS — the holders that are not the connection asking — so a
7934 client that decided from this number would refuse to end a session only it
7935 was in. The visible cost: ending, from the picker, a session whose one pane
7936 is on this very wall takes two presses, because the daemon counts that pane
7937 as another client of that session. The alternative considered was
7938 auto-forcing when the only other holder is this wall's own pane; it needs
7939 the client to match a holder to a pane, which is exactly the verdict-making
7940 the number is not for. Deferred to a collab issue as a UX change rather than
7941 smuggled in here.
7942
7943 **`x` moved, and what each one means now.** `Ctrl-\ x` on the wall is
7944 `removePane`: the notice `[pane removed - the session is still on its
7945 daemon]`, the pump told to detach (it writes `.detach` on its way out; the
7946 transport close is the fallback), the tile vanished, the layout persisted.
7947 Nothing is ended, and every pane goes the same way including one that never
7948 came up. ENDING is the picker's `x` at the session level, where the row
7949 already shows the holder count — the question "who else is in here" is
7950 answered before the key is pressed rather than by the refusal after it. That
7951 end runs `client.endSession` on a SIDE connection, because the session may
7952 have no pane on this wall to ask through, and it dials `HostSpec.poll_target`
7953 — the poller's batch recipe, with ssh's `BatchMode=yes`, a connect timeout
7954 and `asked` false. Never the interactive target: an end must not start a
7955 daemon, and a password prompt going to `/dev/tty` under the popup would park
7956 the keyboard thread in the TCP retry schedule. A daemon that sends no holds
7957 line is refused up front (`[daemon too old to end a session]`) rather than
7958 on the wire, because such a box answers an unknown frame with silence and
7959 the press would spend the whole reply budget with the wall frozen. Only
7960 "others attached" arms the 3 s force window (`Shared.pick_end`, per host and
7961 name); every other refusal shows the daemon's own reason and arms nothing,
7962 since arming there would leave the next `x` forcing an end nobody said was
7963 blocked.
7964
7965 **The hub reads and writes the same file.** `mux web` serves the layout of
7966 the machine it runs on: `webhub.readLeaves`, refusing a bad file with
7967 `mux web: layout ignored (PATH): LINE` and serving an empty wall rather than
7968 a guess, `/tiles` in tree order carrying `id`, `label`, `session` and
7969 `state`. The page's `+` (`POST /tiles/<id>`) checks the file for ROOM and
7970 for a DUPLICATE under the hub mutex BEFORE it dials (`roomForLeaf`), births,
7971 then `appendLeaf` writes the leaf beside that pane. Pre-dial because
7972 dialling first and finding the wall full afterwards leaves a live session
7973 nobody asked for, on no wall, that only `mux a` or a terminal could find
7974 again. A second `+` on one tile inside a single poll interval is a 409
7975 `duplicate`, not a repeated leaf; `WallFull` and `BadLayout` are 502. Two
7976 hubs or walls writing in one instant lose one update — each write is a
7977 read-modify-write over an atomic rename, which is what the hosts file has
7978 always done, and the alternative is a lock file nobody would clean up.
7979 Known and accepted: a zoomed `gone` pane in the browser can resurrect its
7980 session across a page RELOAD, because the page latches `exited` only until
7981 the reload throws that state away; the terminal wall's `gone` pane is Enter
7982 to restart by design, and the browser reaching the same place by F5 is the
7983 same door with no keystroke on it.
7984
7985 **Duplicate leaves are refused on both fronts.** The loader refuses a
7986 repeated leaf (including a second leaf spelling the entry — only the first
7987 can be the tile the user is typing in), and the hub refuses one before it
7988 dials. Two panes on one session would each attach, each claim a rect, and
7989 each grade off the same list; the first one to lose its session would take
7990 the other's name with it.
7991
7992 **`keeps_wall` was NOT deleted.** The plan called for it: with the wall no
7993 longer built from births, a refused picker birth should leave the wall as it
7994 was without a flag saying so. It cannot go. `endAction`'s tail is reached
7995 only when the ended tile is the ONLY present pane on a terminal with stdin
7996 open and the reason is not a clean exit, and two panes arrive there with
7997 byte-identical fields and opposite right answers — a picker birth onto an
7998 empty wall must vanish and leave the wall standing, and the ENTRY tile of
7999 `mux TARGET` must finish with rc 1 and the refusal sentence, because that
8000 refusal is the program's. `born_from` is null for both and `creates` is true
8001 for both. The entry tile IS also the one tile with `retry_cold` false, so
8002 that field could distinguish them today — but it answers a different
8003 question (is a link that died before any state arrived worth redialling),
8004 and hanging the wall's survival off it would mean one edit to the retry rule
8005 silently changes what a refused `mux TARGET` does. The flag stays, its doc
8006 comment now says exactly that, and the test that would fail without it
8007 carries the argument. Recorded follow-up, not done and wanting its own
8008 decision: INVERTING the flag — marking the one tile whose ending is the
8009 program's, where `retry_cold = false` is already set — carries the same
8010 information with the safer default and fixes an edge the current polarity
8011 gets wrong, since a SEEDED pane alone on a terminal wall, refused, has
8012 `keeps_wall` false and so ends mux, when under this model a saved pane's
8013 refusal is a pane's and not the program's.
8014
8015 **Cross-version caveat, daemon newer than client.** A client at v0.0.1-17 or
8016 older sizes its `sessions_reply` buffer for the names alone (1101 bytes) and
8017 reads a longer reply as a transport error. A NEW daemon holding roughly
8018 fourteen or more sessions with 32-character names therefore reads
8019 `unreachable` on such a wall until that wall is upgraded. `sessions_reply_max`
8020 is now names + one holds line per session + the meta line for exactly this
8021 reason: a receiver sized to the names alone reads a full daemon that states
8022 its version as an unreachable box.
8023
8024 **The pin.** The e2e suite grades 110 scenario checkpoints and 38
8025 convergence points, up from what the old model needed, and the leg that
8026 states the whole change in one sentence is "two walls on the same daemons
8027 are two layouts; neither learns of the other's panes" in the hosts group.