c76c27a2
docs: Ctrl-\ w in the key table, and why the wall is a child process
a73x 2026-08-19 17:17
Commit message
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; |