a73x

c76c27a2

docs: Ctrl-\ w in the key table, and why the wall is a child process

a73x   2026-08-19 17:17

Commit message
docs: Ctrl-\ w in the key table, and why the wall is a child process

README.md
Old New
@@ -39,6 +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 | `Shift+PageUp` / `Shift+PageDown` | scrollback (any other key returns to live) | 43 | `Shift+PageUp` / `Shift+PageDown` | scrollback (any other key returns to live) |
43 44
44 `mux` again reattaches — full TUI screens included. Kill the client with 45 `mux` again reattaches — full TUI screens included. Kill the client with
@@ -139,6 +140,9 @@ cursor), attached the way `muxa` attaches — no size claim, so watching
139 never resizes anyone. `q` or `Ctrl-\` leaves. To type into a session, 140 never resizes anyone. `q` or `Ctrl-\` leaves. To type into a session,
140 attach to it: `mux HOST --session b`. 141 attach to it: `mux HOST --session b`.
141 142
143 The same wall, from inside a session: `Ctrl-\` `w` shows every session on
144 the daemon you are attached to, and `q` puts you back where you were.
145
142 ## Everything else 146 ## Everything else
143 147
144 ```sh 148 ```sh
docs/decisions.md
Old New
@@ -3959,3 +3959,65 @@ chord has to remember; the arm picks a name by intent and everything after the
3959 pick — write `.detach`, return `.switch_to` — is one path. The intent is spent 3959 pick — write `.detach`, return `.switch_to` — is one path. The intent is spent
3960 on arrival whichever name it picks, which is what keeps an unasked-for reply 3960 on arrival whichever name it picks, which is what keeps an unasked-for reply
3961 harmless. 3961 harmless.
3962
3963 **The wall reached from inside a session is a CHILD PROCESS, not a call.**
3964 `Ctrl-\ w` asks the daemon the same `sessions_req` question the switching
3965 chords ask, then spawns `/proc/self/exe wall SPELLING...` with stdio inherited
3966 and waits for it. The reason is wallview's teardown: it detaches its per-tile
3967 pump threads and calls `exit(0)` rather than joining them, because a tile
3968 blocked in `readFrame` is not joinable and the window between the last paint
3969 and process death must contain no free. That is correct for a process whose
3970 whole job is the wall, and unusable inside a client that has to survive the
3971 wall and go back to a session — in-process, leaving the wall would end the
3972 session with it. As a child, the existing teardown is right by construction and
3973 none of it had to change. What the child paints over is repaired by the
3974 re-attach that follows, which asks for a fresh snapshot the way every attach
3975 does; the child also restores the termios and the alternate screen it found,
3976 and the re-attach re-establishes both regardless.
3977
3978 Two consequences fall out of that shape. The reply arm now reads an optional
3979 ACTION out of `sessions_reply` rather than an optional NAME — a wall ends the
3980 run without naming a session to go to — and the whole payload has to travel by
3981 value, like `SessionName` and for the same reason: the frame is freed before
3982 the child is spawned. The carrier is sized from this client's own bound on how
3983 many stripes it will show, deliberately not from the daemon's `max_sessions`,
3984 which the client cannot watch change; a reply too long for it shows no wall
3985 rather than a wall that silently omits sessions.
3986
3987 **`--via` has no wall.** The wall grammar spells a socket, a host, and a
3988 `quic://` endpoint, and has no form at all for "an arbitrary command's stdio" —
3989 so for a `--via` target there is no spelling to write down and nothing to hand
3990 the child. That is refused where it is discovered, with one line, and the
3991 client re-attaches to the session the chord was typed in: the same shape a
3992 refused switch already had, because the fact is the same one — nothing failed,
3993 and there is somewhere to go back to.
3994
3995 **A chord waits 2s, because an old daemon answers by saying nothing.** A daemon
3996 that predates `sessions_req` (0x0c) does not refuse the frame — its dispatch
3997 ends in `else => {}`, so the question is dropped and the connection stays
3998 perfectly healthy. An intent with no deadline then outlives the universe: it
3999 stays armed for the rest of the session, every later `Ctrl-\ c/n/p/w` is a
4000 silent no-op, and nothing in the picture suggests the daemon rather than the
4001 keyboard. So the intent carries an expiry, which is the whole reason it is a
4002 struct and not the bare enum. 2s is far longer than any round trip a switch is
4003 usable over and short enough that the keystroke is still in the user's head
4004 when the answer arrives; the poll's 100ms cap is what makes the deadline
4005 reachable at all, since the case being caught is exactly a daemon gone quiet.
4006 Expiry is self-clearing, so one silence is reported once rather than on every
4007 poll for the rest of the session.
4008
4009 It is reported as a corner banner, `[no session list: upgrade muxd]`, not on
4010 stderr: the terminal is in raw mode on the alternate screen, and a print there
4011 lands mid-grid with no newline discipline. `[reconnecting]` already sits in
4012 that corner and is read the same way — it survives until the next full repaint
4013 paints over it, which is the right lifetime for a marker the user has to
4014 actually notice. ASCII only, because the banner is placed by byte length.
4015
4016 A reconnect CLEARS the pending chord instead of expiring it. The two are
4017 different facts: an expiry means the daemon heard nothing and the user is owed
4018 an explanation, while a reconnect means the daemon that was asked no longer
4019 has the question — the user presses the chord again rather than being switched
4020 by a reply that outlived its connection. And the silence already has an
4021 explanation they have been shown, since the reconnect painted its own banner.
4022 Reporting both would put two markers in one corner about one event.
4023
src/client.zig
Old New
@@ -7,8 +7,10 @@
7 //! Keybinding layer: Ctrl-\ (0x1c) is a command prefix in a live session — 7 //! Keybinding layer: Ctrl-\ (0x1c) is a command prefix in a live session —
8 //! `Ctrl-\ d` or `Ctrl-\ Ctrl-\` detaches, `Ctrl-\ c` creates a new session 8 //! `Ctrl-\ d` or `Ctrl-\ Ctrl-\` detaches, `Ctrl-\ c` creates a new session
9 //! and switches to it in place, `Ctrl-\ n` / `Ctrl-\ p` step to the next or 9 //! and switches to it in place, `Ctrl-\ n` / `Ctrl-\ p` step to the next or
10 //! previous session in the daemon's list. While dialling or reconnecting 10 //! previous session in the daemon's list, `Ctrl-\ w` shows them all as a
11 //! there is no session to command and a bare Ctrl-\ still aborts. 11 //! read-only wall (a `mux wall` child on this terminal) and returns here
12 //! when it leaves. While dialling or reconnecting there is no session to
13 //! command and a bare Ctrl-\ still aborts.
12 const std = @import("std"); 14 const std = @import("std");
13 const Engine = @import("engine").Engine; 15 const Engine = @import("engine").Engine;
14 const Replica = @import("replica").Replica; 16 const Replica = @import("replica").Replica;