a73x

f3100d58

docs: the chord list stops describing zoom

a73x   2026-08-24 11:32

Commit message
docs: the chord list stops describing zoom

README, --help and the comments in interact/wallview/mux_main now say
focus where they said zoom. Ctrl-\ w survives with a new meaning: fold
the saved wall into this one, which is the only road from `mux TARGET`
to the full wall.

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

README.md
Old New
@@ -43,45 +43,35 @@ mux # attach
43 ``` 43 ```
44 44
45 **`mux` IS the wall.** There is one navigation axis and one program behind 45 **`mux` IS the wall.** There is one navigation axis and one program behind
46 it: zoomed IN is a session, full screen; zoomed OUT is the wall. `mux` and 46 it: every tile is a live session on its own rectangle, and one tile is
47 `mux HOST` open zoomed into the session they just attached to; `mux wall` 47 focused — it receives your keystrokes. `mux` and `mux HOST` open on a wall
48 opens on the wall. `Ctrl-\` `w` moves between them, in place — no child 48 of one tile whose rect is the whole terminal; `mux wall` opens on the saved
49 process, no second screen, and every tile's replica stays hot, so coming 49 wall. `Ctrl-\` `1`-`9` focuses a tile, `Ctrl-\` `d` leaves — no child
50 back is a repaint and not a re-attach. Bare `mux` still feels like a plain 50 process, no second screen, and every tile's replica stays hot, so moving
51 focus is a repaint and not a re-attach. Bare `mux` still feels like a plain
51 client, because a wall of one tile is what a plain client always was. 52 client, because a wall of one tile is what a plain client always was.
52 53
53 Zoomed in — the session has the terminal, and only the prefix is held back: 54 The focused tile has the terminal, and only the prefix is held back:
54 55
55 | Key | Effect | 56 | Key | Effect |
56 |---|---| 57 |---|---|
57 | `Ctrl-\` | command prefix — press it, then a command key | 58 | `Ctrl-\` | command prefix — press it, then a command key |
58 | `Ctrl-\` `d` (or `Ctrl-\` `Ctrl-\`) | detach and leave mux (session keeps running) | 59 | `Ctrl-\` `d` (or `Ctrl-\` `Ctrl-\`) | detach and leave mux (session keeps running) |
59 | `Ctrl-\` `w` | zoom out to the wall | 60 | `Ctrl-\` `c` | create a session, add a tile, focus it |
60 | `Ctrl-\` `c` | create a session, put it on the wall, zoom it | 61 | `Ctrl-\` `n` / `Ctrl-\` `p` | focus the next / previous session of this daemon, wrapping |
61 | `Ctrl-\` `n` / `Ctrl-\` `p` | move the zoom to the next / previous session of this daemon, wrapping | 62 | `Ctrl-\` `l` | skip back to the last tile you focused |
62 | `Ctrl-\` `l` | skip back to the last tile you zoomed (out, if that was this one) | 63 | `Ctrl-\` `1`-`9` | focus tile N |
64 | `Ctrl-\` `x` | forget the focused tile (off the wall file; the session keeps running) |
63 | `Shift+PageUp` / `Shift+PageDown` | scrollback, a screen at a time (any other key returns to live) | 65 | `Shift+PageUp` / `Shift+PageDown` | scrollback, a screen at a time (any other key returns to live) |
64 | mouse wheel | scrollback, three rows a notch (arrow keys to a full-screen app) | 66 | mouse wheel | scrollback, three rows a notch (arrow keys to a full-screen app) |
65 | drag with the left button | select what it crosses, copy on release | 67 | drag with the left button | select what it crosses, copy on release |
66 | any key not behind `Ctrl-\` | goes to the session | 68 | any key not behind `Ctrl-\` | goes to the focused tile |
67 69
68 Zoomed out — the wall reads the keys and nothing reaches a session: 70 Behind the `Ctrl-\` prefix, `n`, `p` and `c` are about SESSIONS, not tiles:
69 71 they walk the daemon's own session ring. A session that already has a tile
70 | Key | Effect | 72 is an instant focus move; one that does not gets a tile, because visiting it
71 |---|---| 73 is attaching to it and every attach is recorded (see "the wall is your
72 | `j` / `k` (or `n` / `p`), `1`-`9` | select a tile | 74 attach history" below). The digits are what walk tiles.
73 | click a stripe | select that tile |
74 | `Enter` | zoom the selected tile |
75 | `x` | forget the selected tile (off the wall file; the session keeps running) |
76 | `q` (or `Ctrl-\`) | leave mux |
77
78 Behind the `Ctrl-\` prefix — which is to say, zoomed in — `n`, `p` and `c`
79 are about SESSIONS, not tiles: they walk the daemon's own
80 session ring. (Bare `n`/`p` on the wall are just aliases of `j`/`k`, because
81 there is no session to leave.) A session that already has a tile is an instant zoom move; one
82 that does not gets a tile, because visiting it is attaching to it and every
83 attach is recorded (see "the wall is your attach history" below). The wall's
84 `j`/`k` and the digits are what walk tiles.
85 75
86 A session whose shell exits ends `mux` with that shell's exit code when it 76 A session whose shell exits ends `mux` with that shell's exit code when it
87 was the only tile on the wall — so `mux` stays something you can put in a 77 was the only tile on the wall — so `mux` stays something you can put in a
@@ -114,7 +104,7 @@ does worse.
114 `kill -9` if you like; the session doesn't care. 104 `kill -9` if you like; the session doesn't care.
115 105
116 One daemon hosts up to four named sessions; the first is called `0`. 106 One daemon hosts up to four named sessions; the first is called `0`.
117 `Ctrl-\` `c` creates one named after the lowest free number and zooms it, 107 `Ctrl-\` `c` creates one named after the lowest free number and focuses it,
118 `Ctrl-\` `n` and `Ctrl-\` `p` step through them in the daemon's order and 108 `Ctrl-\` `n` and `Ctrl-\` `p` step through them in the daemon's order and
119 wrap at the ends, and `mux --session NAME` attaches to any of them by name 109 wrap at the ends, and `mux --session NAME` attaches to any of them by name
120 (creating it if there is room). With only one session open `n` and `p` do 110 (creating it if there is room). With only one session open `n` and `p` do
@@ -254,7 +244,7 @@ muxweb HOST quic://OTHER --sock /tmp/s.sock#b # ...or state it: shows these, A
254 244
255 Each tile is a live session, dialed the way `mux` dials it — the same 245 Each tile is a live session, dialed the way `mux` dials it — the same
256 `HOST`, `quic://HOST[:PORT]` and `--sock PATH` spellings, with `#SESSION` 246 `HOST`, `quic://HOST[:PORT]` and `--sock PATH` spellings, with `#SESSION`
257 naming the session on that daemon. Click a tile to zoom it and type in it; 247 naming the session on that daemon. Click a tile to focus it and type in it;
258 click the shade to come back. The wall itself is editable from the page: the 248 click the shade to come back. The wall itself is editable from the page: the
259 box adds a tile, `×` removes one (which detaches — the session and 249 box adds a tile, `×` removes one (which detaches — the session and
260 everything in it keep running), `+` starts a new session on that tile's host, 250 everything in it keep running), `+` starts a new session on that tile's host,
@@ -298,13 +288,13 @@ as `HOST#build` and as `quic://HOST#build` is two tiles, deliberately.
298 adds one either, because an arbitrary command is not an address and the wall 288 adds one either, because an arbitrary command is not an address and the wall
299 grammar has no line to write for it — the session is real, it is just not 289 grammar has no line to write for it — the session is real, it is just not
300 addressable as a tile, and `Ctrl-\` `w` labels it `--via CMD#NAME` on a bar 290 addressable as a tile, and `Ctrl-\` `w` labels it `--via CMD#NAME` on a bar
301 that `x` will find nothing in the file to forget; and neither do the wall's 291 that `Ctrl-\` `x` will find nothing in the file to forget; and neither do the wall's
302 own stripes. Writing the file is best effort — if it cannot be 292 own stripes. Writing the file is best effort — if it cannot be
303 written you get one warning line and the attach happens anyway. 293 written you get one warning line and the attach happens anyway.
304 294
305 That file is what `Ctrl-\` `w` shows you: zooming out from any session puts 295 That file is what `mux wall` opens on, and what `Ctrl-\` `w` folds in
306 up the saved wall with the tile you are standing on among them. There is one 296 one-shot — the saved wall with the tile you are standing on among them.
307 wall, and no way to end up looking at a second one by accident. 297 There is one wall, and no way to end up looking at a second one by accident.
308 298
309 `mux wall add`/`rm` are the same edit without an attach: file operations 299 `mux wall add`/`rm` are the same edit without an attach: file operations
310 only, so neither dials, and `rm` never touches the session. A wall or hub 300 only, so neither dials, and `rm` never touches the session. A wall or hub
@@ -316,55 +306,48 @@ wedging every command that touches the wall.
316 306
317 `muxweb TILE...` ADDS its tiles to that file rather than replacing it — 307 `muxweb TILE...` ADDS its tiles to that file rather than replacing it —
318 naming a wall on the command line changes what this run shows, not what 308 naming a wall on the command line changes what this run shows, not what
319 you have recorded. Forgetting stays explicit: the page's `×`, the wall's 309 you have recorded. Forgetting stays explicit: the page's `×`, `Ctrl-\` `x`,
320 `x`, `mux wall rm`. 310 `mux wall rm`.
321 311
322 Watching is read-only: each stripe is a live session (label bar + the rows 312 Watching is read-only: each stripe is a live session (label bar + the rows
323 around its cursor), attached the way `muxa` attaches — no size claim, so 313 around its cursor), attached the way `muxa` attaches — no size claim, so
324 watching never resizes anyone. Resize the terminal and the stripes are 314 watching never resizes anyone.
325 re-cut the next time you unzoom — a live re-layout would have to stop every
326 tile mid-paint — so a wall you never zoom keeps its old widths.
327 315
328 The keys are the ones listed under "Quick start, local" — `mux wall` and 316 The keys are the ones listed under "Quick start, local" — `mux wall` and
329 `mux` are the same program, entered through different doors, so there is 317 `mux` are the same program, entered through different doors, so there is
330 one key table and not two. Zoomed out, `j`/`k` (or `n`/`p`) and `1`-`9` 318 one key table and not two. Every typed key goes to the focused tile; only
331 select — as does clicking a stripe — `Enter` zooms, `x` forgets, `q` 319 the `Ctrl-\` prefix is held back. `Ctrl-\` `1`-`9` focuses a tile (as does
332 leaves. Zoomed in, the session has 320 clicking one), `Ctrl-\` `n`/`p`/`c` walk the daemon's session ring, `Ctrl-\` `l`
333 the terminal and `Ctrl-\` is the only thing held back: `w` zooms out, `d` 321 skips back to the last tile you focused, `Ctrl-\` `x` forgets the focused tile,
334 detaches and leaves, `n`/`p`/`c` walk the daemon's session ring, `l` skips 322 `Ctrl-\` `d` leaves. Any other command key after the prefix is swallowed along with it,
335 back to the last tile you zoomed. Any other command key after the prefix is 323 as in a plain client.
336 swallowed along with it, as in a plain client. 324
337 325 The focused tile's label bar carries a `>` marker. Focus is client-local:
338 The selected stripe's label bar carries a `>` marker. `Enter` zooms it in 326 moving it sends nothing on the wire, so `Ctrl-\` `n` and `Ctrl-\` `l` move
339 place: the tile's own connection is resized from 0×0 to this terminal's 327 between sessions instantly — no reconnect, no snapshot, no flash. Every
340 size and starts carrying your keystrokes, so the client that types is a 328 tile keeps replicating at its own rect whether you are looking at it or
341 full-size one, claiming the grid the ordinary way. Nothing is dialled and 329 not, so nothing reflows when focus moves away.
342 nothing new attaches. Every other tile keeps replicating behind the zoom, 330
343 so `Ctrl-\` `n` and `Ctrl-\` `l` move between sessions instantly — no 331 Typed input at the focused tile is predicted locally, and the wheel and the
344 reconnect, no snapshot, no flash. Zooming out stops the keystrokes and puts 332 mouse work exactly as they do in a plain client, because a focused tile *is*
345 the stripes back; the session keeps the grid the zoom claimed, so nothing 333 a plain client's loop. The focused tile's Core owns the mouse modes; a tile
346 reflows when you look away. 334 that is not focused holds none, so nothing it armed can report while you
347 335 are looking elsewhere.
348 Typed input at a zoomed tile is predicted locally, and the wheel and the
349 mouse work exactly as they do in a plain client, because a zoomed tile *is*
350 a plain client's loop. Unzoomed, the wall asks its own terminal for no
351 mouse mode at all, so nothing a stripe's session armed can report while you
352 are looking at the wall.
353 336
354 A tile whose host is not answering says `reconnecting` and keeps retrying, 337 A tile whose host is not answering says `reconnecting` and keeps retrying,
355 forever and on purpose — a wall is a thing you leave up, and a laptop that 338 forever and on purpose — a wall is a thing you leave up, and a laptop that
356 wakes an hour later should find its sessions. The cost is that a tile 339 wakes an hour later should find its sessions. The cost is that a tile
357 pointing at a daemon that is never coming back looks identical to one whose 340 pointing at a daemon that is never coming back looks identical to one whose
358 network is merely down. Nothing times it out; `x`, or `mux wall rm 341 network is merely down. Nothing times it out; `Ctrl-\` `x`, or `mux wall rm
359 SPELLING`, is how a line leaves. 342 SPELLING`, is how a line leaves.
360 343
361 `x` forgets the selected tile: its line leaves the wall file, its stripe 344 `Ctrl-\` `x` forgets the focused tile: its line leaves the wall file, its stripe
362 leaves the screen, and the remaining stripes are re-cut over the space. 345 leaves the screen, and the remaining stripes are re-cut over the space.
363 Forgetting is never killing — the session goes on running and `mux` reaches 346 Forgetting is never killing — the session goes on running and `mux` reaches
364 it again by name. Forget them all and the wall says so. 347 it again by name. Forget them all and the wall says so.
365 348
366 The same wall, from inside a session: `Ctrl-\` `w` shows every session on 349 `Ctrl-\` `w` folds in the saved wall's tiles one-shot — the wall you are
367 the daemon you are attached to, and `q` puts you back where you were. 350 already on, plus every session recorded in the file. `Ctrl-\` `d` leaves.
368 351
369 ## Everything else 352 ## Everything else
370 353
docs/superpowers/specs/2026-08-23-retire-zoom-design.md
Old New
@@ -76,7 +76,7 @@ holds — it is the wall file that defines the wall, not a claim.
76 76
77 | chord | today | after | 77 | chord | today | after |
78 |---|---|---| 78 |---|---|---|
79 | `Ctrl-\ w` | unzoom to the wall | **removed** | 79 | `Ctrl-\ w` | unzoom to the wall | fold the saved wall into this one — the road from `mux TARGET` to the full wall |
80 | `Ctrl-\ n` / `p` | next/prev session on this daemon, adds a tile | unchanged; new tile appends, focus moves to it | 80 | `Ctrl-\ n` / `p` | next/prev session on this daemon, adds a tile | unchanged; new tile appends, focus moves to it |
81 | `Ctrl-\ c` | new session, adds a tile | unchanged | 81 | `Ctrl-\ c` | new session, adds a tile | unchanged |
82 | `Ctrl-\ l` | last zoom | last focus | 82 | `Ctrl-\ l` | last zoom | last focus |
src/interact.zig
Old New
@@ -81,7 +81,7 @@ pub const detach_key: u8 = 0x1c;
81 /// as input (see `carry`), because there was no session to command yet when 81 /// as input (see `carry`), because there was no session to command yet when
82 /// they were typed. 82 /// they were typed.
83 /// 83 ///
84 /// Public because the CLI wall's ZOOMED tile needs the same layer over the 84 /// Public because the CLI wall's focused tile needs the same layer over the
85 /// same keys (wallview.zig): a focused tile is a session on this terminal, 85 /// same keys (wallview.zig): a focused tile is a session on this terminal,
86 /// and a second copy of this table would be a twin that drifts. What each 86 /// and a second copy of this table would be a twin that drifts. What each
87 /// action MEANS is the caller's — in the wall `.detach` detaches and ends 87 /// action MEANS is the caller's — in the wall `.detach` detaches and ends
@@ -119,12 +119,9 @@ pub const PrefixFilter = struct {
119 /// wrong shell, and sending them back to the old one races the detach 119 /// wrong shell, and sending them back to the old one races the detach
120 /// that is already on its way. 120 /// that is already on its way.
121 /// 121 ///
122 /// `.wall` is the variant where the loss is visible: the user comes 122 /// `.wall` shows the saved wall — `Ctrl-\ w` hydrates the wall file's
123 /// BACK to this same session, so bytes dropped behind `Ctrl-\ w` are 123 /// tiles one-shot. It ends the chunk like every chord above, so bytes
124 /// bytes they will look for and not find. The rule stays as it is 124 /// typed behind it are dropped for the same reason.
125 /// anyway — those bytes were typed before the wall took the terminal,
126 /// and delivering them after it gives them back would replay them into
127 /// a shell whose prompt has moved on.
128 pub fn feed(self: *PrefixFilter, buf: []u8) Out { 125 pub fn feed(self: *PrefixFilter, buf: []u8) Out {
129 var kept: usize = 0; 126 var kept: usize = 0;
130 for (buf) |b| { 127 for (buf) |b| {
@@ -181,13 +178,13 @@ const wheel_rows: u32 = 3;
181 /// into scrollback movement. 178 /// into scrollback movement.
182 /// 179 ///
183 /// TWO ROLES, and they are not alternatives. Every tile's `Core` owns a 180 /// TWO ROLES, and they are not alternatives. Every tile's `Core` owns a
184 /// filter for the bytes a ZOOMED session's terminal delivers; it runs only 181 /// filter for the bytes a focused session's terminal delivers; it runs only
185 /// while no application in that session has asked for the mouse, because 182 /// while no application in that session has asked for the mouse, because
186 /// when one has, the bytes are its own and the filter is bypassed entirely 183 /// when one has, the bytes are its own and the filter is bypassed entirely
187 /// (and reset, so a report split across that transition cannot be 184 /// (and reset, so a report split across that transition cannot be
188 /// half-eaten). The CLI wall owns one more, in front of its UNZOOMED key 185 /// half-eaten). The CLI wall owns one more, in its keyboard loop, to
189 /// loop, where there is no session to bypass it for — the modes are the 186 /// hit-test a press for focus — the report bytes themselves pass through to
190 /// wall's own and every report is the wall's. 187 /// the focused tile, so the Core that owns the drag sees them.
191 /// 188 ///
192 /// Only the SGR form (`ESC [ < b ; x ; y M|m`) is recognised, because it is 189 /// Only the SGR form (`ESC [ < b ; x ; y M|m`) is recognised, because it is
193 /// the only form the client ever asks its terminal for (`client_mouse_setup`). 190 /// the only form the client ever asks its terminal for (`client_mouse_setup`).
@@ -398,7 +395,7 @@ fn onWinch(_: c_int) callconv(.c) void {
398 /// Arm SIGWINCH, so `winchRaised` has something to answer. 395 /// Arm SIGWINCH, so `winchRaised` has something to answer.
399 /// 396 ///
400 /// The keyboard thread polls the flag and relayouts; there is no single 397 /// The keyboard thread polls the flag and relayouts; there is no single
401 /// promoted pump that owns the screen, so no pump consumes the signal. 398 /// pump that owns the screen, so no pump consumes the signal.
402 pub fn watchWinch() void { 399 pub fn watchWinch() void {
403 var sa: std.posix.Sigaction = .{ 400 var sa: std.posix.Sigaction = .{
404 .handler = .{ .handler = onWinch }, 401 .handler = .{ .handler = onWinch },
@@ -448,7 +445,7 @@ pub fn ttySize(fd: std.posix.fd_t) ?proto.Size {
448 /// 445 ///
449 /// Written once for a driver's whole LIFETIME rather than per session, 446 /// Written once for a driver's whole LIFETIME rather than per session,
450 /// because the driver is the wall (`wall_setup`): it holds the screen while 447 /// because the driver is the wall (`wall_setup`): it holds the screen while
451 /// its tiles promote and demote underneath it, and a tile writes only 448 /// focus moves between its tiles, and a tile writes only
452 /// `session_claim`. 449 /// `session_claim`.
453 /// 450 ///
454 /// Autowrap (`?7l`) goes off with the alternate screen and for its sake: an 451 /// Autowrap (`?7l`) goes off with the alternate screen and for its sake: an
@@ -458,7 +455,7 @@ const terminal_frame_setup = "\x1b[22;0t\x1b[?1049h\x1b[?25l\x1b[?7l";
458 455
459 /// The half that is about holding a SESSION on somebody's terminal: the 456 /// The half that is about holding a SESSION on somebody's terminal: the
460 /// mouse modes its wheel is read out of, and nothing else. A wall tile's 457 /// mouse modes its wheel is read out of, and nothing else. A wall tile's
461 /// promote writes this first (`Core.claimTerminal`), then the session's own 458 /// focus claim writes this first (`Core.claimTerminal`), then the session's own
462 /// modes on top — the wall owns the screen for its whole life and a tile 459 /// modes on top — the wall owns the screen for its whole life and a tile
463 /// may not touch it. 460 /// may not touch it.
464 /// 461 ///
@@ -561,17 +558,18 @@ const terminal_teardown = session_release ++ "\x1b[?7h\x1b[?25h\x1b[23;0t\x1b[?1
561 /// Undoes `session_claim`, and everything a SESSION can have asked this 558 /// Undoes `session_claim`, and everything a SESSION can have asked this
562 /// terminal for while it held it: `?2004l` because the session armed 559 /// terminal for while it held it: `?2004l` because the session armed
563 /// bracketed paste, the whole mouse table because the session's own modes 560 /// bracketed paste, the whole mouse table because the session's own modes
564 /// were mirrored onto this terminal (`appendMouseModes`) and a promoted 561 /// were mirrored onto this terminal (`appendMouseModes`) and a focused
565 /// tile's application can have asked for modes the client never wanted. 562 /// tile's application can have asked for modes the client never wanted.
566 /// 563 ///
567 /// It leads `terminal_teardown` — the exit path — and it is inside 564 /// It leads `terminal_teardown` — the exit path — and it is inside
568 /// `wall_teardown`. Every route out of a terminal claim writes these 565 /// `wall_teardown`. Every route out of a terminal claim writes these
569 /// bytes; that is the pairing, and one test pins all four. 566 /// bytes; that is the pairing, and one test pins all four.
570 /// 567 ///
571 /// Public because the DEMOTE does not write it from here. A wall's zoom 568 /// Public because the focus move does not write it from here. A wall's
572 /// transition and a tile's pump are different threads, and the release has 569 /// focus change and a tile's pump are different threads, and the release
573 /// to be ordered against the NEXT tile's claim rather than merely happen — 570 /// has to be ordered against the NEXT tile's claim rather than merely
574 /// so the thread that moves the zoom writes it, before the store that lets 571 /// happen — so the thread that moves the focus writes it, before the store
572 /// that lets
575 /// the next pump see the move (wallview's `setFocus`). Exported rather than 573 /// the next pump see the move (wallview's `setFocus`). Exported rather than
576 /// duplicated, for `wall_setup`/`wall_teardown`'s reason: a mouse mode 574 /// duplicated, for `wall_setup`/`wall_teardown`'s reason: a mouse mode
577 /// added to the claim must not need finding in four places. 575 /// added to the claim must not need finding in four places.
@@ -579,24 +577,24 @@ pub const session_release = "\x1b[?2004l" ++ mouse_teardown;
579 577
580 /// What a driver that lends its screen to one session at a time writes on 578 /// What a driver that lends its screen to one session at a time writes on
581 /// the way in and on the way out. The CLI wall is the driver: it holds the 579 /// the way in and on the way out. The CLI wall is the driver: it holds the
582 /// alternate screen for its whole life while its tiles promote and demote 580 /// alternate screen for its whole life while focus moves between its
583 /// underneath it, so the screen half is written once here and never by a 581 /// tiles, so the screen half is written once here and never by a
584 /// tile. 582 /// tile.
585 /// 583 ///
586 /// The teardown is `terminal_teardown` itself, and that identity is the 584 /// The teardown is `terminal_teardown` itself, and that identity is the
587 /// design rather than a coincidence. The wall can be left while a tile is 585 /// design rather than a coincidence. The wall can be left while a tile is
588 /// still PROMOTED — stdin closes under a zoom, and `q` is not reachable 586 /// still holds the terminal — `Ctrl-\ d` ends the run from any state —
589 /// from inside one — so every mode a session set through that tile 587 /// so every mode a session set through that tile (bracketed paste, the
590 /// (bracketed paste, the mouse modes it asked for) has to come off here as 588 /// mouse modes it asked for) has to come off here as well as at the focus
591 /// well as at the demote. A wall exit and a client exit are the same 589 /// handover. A wall exit and a client exit are the same
592 /// terminal, restored the same way; the only difference is that the wall 590 /// terminal, restored the same way; the only difference is that the wall
593 /// wrote the screen half once for N sessions. 591 /// wrote the screen half once for N sessions.
594 /// 592 ///
595 /// The TITLE is the one thing that is NOT symmetric, deliberately. A 593 /// The TITLE is the one thing that is NOT symmetric, deliberately. A
596 /// promoted tile sets it and a demote leaves it standing: the wall pushed 594 /// focused tile sets it and a focus move leaves it standing: the wall pushed
597 /// one title for its whole life and pops it here, so a user who zooms 595 /// one title for its whole life and pops it here, so a user who focuses
598 /// through four sessions sees four titles and gets their own back when the 596 /// through four sessions sees four titles and gets their own back when the
599 /// wall exits. Restoring at each demote would need a title to restore TO, 597 /// wall exits. Restoring at each focus move would need a title to restore TO,
600 /// and mux cannot read one back — the tmux behaviour, and the right one. 598 /// and mux cannot read one back — the tmux behaviour, and the right one.
601 /// 599 ///
602 /// That push (`22;0t`, "icon name and window title", matching the OSC 0 600 /// That push (`22;0t`, "icon name and window title", matching the OSC 0
@@ -993,7 +991,7 @@ pub const Routed = enum {
993 /// The replica took state and then refused what arrived, and can no 991 /// The replica took state and then refused what arrived, and can no
994 /// longer be trusted. The driver answers by re-attaching from scratch, 992 /// longer be trusted. The driver answers by re-attaching from scratch,
995 /// quoting no seq and no epoch — the whole problem is that what we hold 993 /// quoting no seq and no epoch — the whole problem is that what we hold
996 /// is untrusted — at whatever size its zoom claims 994 /// is untrusted — at whatever size its rect claims
997 /// (`wallview.sendAttach`). 995 /// (`wallview.sendAttach`).
998 resync, 996 resync,
999 /// Not the Core's. `exit_status` and `taken_over` are the session's 997 /// Not the Core's. `exit_status` and `taken_over` are the session's
@@ -1006,8 +1004,8 @@ pub const Routed = enum {
1006 /// `selection_reply` is here for a narrower reason and is the one the 1004 /// `selection_reply` is here for a narrower reason and is the one the
1007 /// driver must call BACK about: `selectionCopy` correlates it, tests the 1005 /// driver must call BACK about: `selectionCopy` correlates it, tests the
1008 /// watermark and refuses an oversized one, but the terminal it copies 1006 /// watermark and refuses an oversized one, but the terminal it copies
1009 /// to is the wall's and the tile that asked is usually a demoted 1007 /// to is the wall's and the tile that asked is usually not the focused
1010 /// stripe. Same shape as `sessions_reply`, which the driver reads with 1008 /// one. Same shape as `sessions_reply`, which the driver reads with
1011 /// `client.ringNeighbour`. 1009 /// `client.ringNeighbour`.
1012 not_mine, 1010 not_mine,
1013 }; 1011 };
@@ -1016,7 +1014,7 @@ pub const Routed = enum {
1016 /// 1014 ///
1017 /// A wall tile's terminal is shared with N stripes on N threads, so every 1015 /// A wall tile's terminal is shared with N stripes on N threads, so every
1018 /// paint asks first and the answer is held until the paint is finished: a 1016 /// paint asks first and the answer is held until the paint is finished: a
1019 /// zoom that moved mid-paint would put one session's rows on another's 1017 /// focus move mid-paint would put one session's rows on another's
1020 /// screen. Side channels are gated on `Core.claim` instead. 1018 /// screen. Side channels are gated on `Core.claim` instead.
1021 pub const Sink = struct { 1019 pub const Sink = struct {
1022 ctx: ?*anyopaque = null, 1020 ctx: ?*anyopaque = null,
@@ -1065,7 +1063,7 @@ pub const Claim = enum { none, session };
1065 /// whatever terminal claim it still holds. `deinit` puts all three 1063 /// whatever terminal claim it still holds. `deinit` puts all three
1066 /// back; see there for the order. 1064 /// back; see there for the order.
1067 /// * `claimTerminal` / `releaseTerminal` — the terminal a tile BORROWS 1065 /// * `claimTerminal` / `releaseTerminal` — the terminal a tile BORROWS
1068 /// for as long as it is promoted. Raw mode and the SIGWINCH handler 1066 /// for as long as it is focused. Raw mode and the SIGWINCH handler
1069 /// belong to the driver that owns the screen (the wall arms both); a 1067 /// belong to the driver that owns the screen (the wall arms both); a
1070 /// Core only ever holds a session's claim. See `Claim`. 1068 /// Core only ever holds a session's claim. See `Claim`.
1071 /// * per pass: `winch`, then `idle`. 1069 /// * per pass: `winch`, then `idle`.
@@ -1088,11 +1086,11 @@ pub const Claim = enum { none, session };
1088 /// Nothing here is a singleton. A wall tile brings its own transport and 1086 /// Nothing here is a singleton. A wall tile brings its own transport and
1089 /// its own Core, one per tile from birth — there is never a second replica 1087 /// its own Core, one per tile from birth — there is never a second replica
1090 /// or a second applier for one tile. `out_fd`/`size` say where a Core 1088 /// or a second applier for one tile. `out_fd`/`size` say where a Core
1091 /// paints and `sink` says whether it may right now; a demoted tile's says 1089 /// paints and `sink` says whether it may right now; a gone tile's says
1092 /// no, and the wall paints the stripe from `grid()` instead. 1090 /// no, and the wall paints its dead-tile label instead.
1093 pub const Core = struct { 1091 pub const Core = struct {
1094 /// The tile index a zoomed `Core` selects under: it is the only 1092 /// The tile index a `Core` selects under: it is the only
1095 /// session on its screen, so there is exactly one. Named rather than 1093 /// session on its rect, so there is exactly one. Named rather than
1096 /// written three times because the drag machine's `tile` is what 1094 /// written three times because the drag machine's `tile` is what
1097 /// confines a selection to one pane — `hitTest` stamps it, `highlight` 1095 /// confines a selection to one pane — `hitTest` stamps it, `highlight`
1098 /// hands it to the painter, and `paintDragChange` decides which rows 1096 /// hands it to the painter, and `paintDragChange` decides which rows
@@ -1137,9 +1135,9 @@ pub const Core = struct {
1137 /// them back out, across read boundaries. 1135 /// them back out, across read boundaries.
1138 mouse: MouseFilter = .{}, 1136 mouse: MouseFilter = .{},
1139 /// The mux selection over THIS session, while this Core holds the 1137 /// The mux selection over THIS session, while this Core holds the
1140 /// terminal. A demoted wall tile has `claim == .none` and so never 1138 /// terminal. A wall tile that is not focused has `claim == .none` and so never
1141 /// sees a report at all: unzoomed, the drag belongs to the wall's own 1139 /// sees a report at all: the drag belongs to the focused tile, on its
1142 /// keyboard loop, over its own hit-test. 1140 /// own hit-test.
1143 drag: select.Drag = .{}, 1141 drag: select.Drag = .{},
1144 /// The last id this Core put on the wire. Per-Core and not global: one 1142 /// The last id this Core put on the wire. Per-Core and not global: one
1145 /// link carries one conversation, and `client_core` correlates against 1143 /// link carries one conversation, and `client_core` correlates against
@@ -1154,7 +1152,7 @@ pub const Core = struct {
1154 sel_watermark: u32 = 0, 1152 sel_watermark: u32 = 0,
1155 /// The selection the request was taken from, compared against what is 1153 /// The selection the request was taken from, compared against what is
1156 /// still held when the answer comes back. A reply that outlived its own 1154 /// still held when the answer comes back. A reply that outlived its own
1157 /// highlight — a relayout, a forget, a zoom, a resync — is text for 1155 /// highlight — a relayout, a forget, a focus move, a resync — is text for
1158 /// rows nobody is looking at any more. 1156 /// rows nobody is looking at any more.
1159 sel_range: ?select.Range = null, 1157 sel_range: ?select.Range = null,
1160 /// Scroll mode: 0 = live; N = viewing the screenful whose bottom sits N 1158 /// Scroll mode: 0 = live; N = viewing the screenful whose bottom sits N
@@ -1197,7 +1195,7 @@ pub const Core = struct {
1197 size: proto.Size, 1195 size: proto.Size,
1198 ) !Core { 1196 ) !Core {
1199 // The driver's layout was cut from ONE reading of the terminal, so 1197 // The driver's layout was cut from ONE reading of the terminal, so
1200 // a second ioctl here would clip a promoted tile to rows the wall 1198 // a second ioctl here would clip a tile to rows the wall
1201 // does not believe in. 1199 // does not believe in.
1202 const eng = try Engine.init(alloc, .{ .cols = size.cols, .rows = size.rows }); 1200 const eng = try Engine.init(alloc, .{ .cols = size.cols, .rows = size.rows });
1203 return .{ 1201 return .{
@@ -1220,7 +1218,7 @@ pub const Core = struct {
1220 // is still on it and still using it after this tile's pump has 1218 // is still on it and still using it after this tile's pump has
1221 // gone. 1219 // gone.
1222 // `.write`, always. This is the path a pump takes when its SESSION 1220 // `.write`, always. This is the path a pump takes when its SESSION
1223 // ends under a zoom — nobody moved the zoom, so nobody else wrote 1221 // ends while its tile holds the terminal — nobody moved the focus, so nobody else wrote
1224 // the release, and the wall goes on running with the terminal still 1222 // the release, and the wall goes on running with the terminal still
1225 // in that session's modes. 1223 // in that session's modes.
1226 self.releaseTerminal(.write); 1224 self.releaseTerminal(.write);
@@ -1229,21 +1227,20 @@ pub const Core = struct {
1229 self.rep.eng.deinit(); 1227 self.rep.eng.deinit();
1230 } 1228 }
1231 1229
1232 /// Only the promoted tile sees a SIGWINCH; it adopts the wall's 1230 /// A pump calls this before claiming, to follow a resize.
1233 /// size.
1234 pub fn adoptSize(self: *Core, size: proto.Size) void { 1231 pub fn adoptSize(self: *Core, size: proto.Size) void {
1235 self.size = size; 1232 self.size = size;
1236 } 1233 }
1237 1234
1238 /// Take the terminal for this session ALONE — the wall's promote. 1235 /// Take the terminal for this session ALONE — the wall's claim.
1239 /// 1236 ///
1240 /// Taken under the SINK for ORDER, not painting: the wall writes the 1237 /// Taken under the SINK for ORDER, not painting: the wall writes the
1241 /// previous holder's release under the same lock, before the store that 1238 /// previous holder's release under the same lock, before the store that
1242 /// makes the handover visible (wallview's `setFocus`). A claim taken 1239 /// makes the handover visible (wallview's `setFocus`). A claim taken
1243 /// outside it can land AFTER that release, leaving modes nothing 1240 /// outside it can land AFTER that release, leaving modes nothing
1244 /// undoes. `false` is a promote the zoom moved out from under. 1241 /// undoes. `false` is a claim the focus moved out from under.
1245 /// 1242 ///
1246 /// The session's own modes go on top because a promote's resize is 1243 /// The session's own modes go on top because a claim's resize is
1247 /// answered by `resyncSnapshot`, which carries no modes: otherwise a 1244 /// answered by `resyncSnapshot`, which carries no modes: otherwise a
1248 /// tile whose application asked for the mouse would hold a terminal 1245 /// tile whose application asked for the mouse would hold a terminal
1249 /// that never heard about it. 1246 /// that never heard about it.
@@ -1272,26 +1269,26 @@ pub const Core = struct {
1272 /// 1269 ///
1273 /// `.write` is the ordinary answer and the only one a Core can reach on 1270 /// `.write` is the ordinary answer and the only one a Core can reach on
1274 /// its own. `.already_written` exists because a shared terminal's 1271 /// its own. `.already_written` exists because a shared terminal's
1275 /// handover has to be ORDERED, not merely eventual: the wall's zoom 1272 /// handover has to be ORDERED, not merely eventual: the wall's focus
1276 /// transition writes the release itself, on the thread that moves the 1273 /// change writes the release itself, on the thread that moves the
1277 /// zoom, before the store that lets the next tile's pump see the move. 1274 /// focus, before the store that lets the next tile's pump see the move.
1278 /// The pump that is being demoted then finds the bytes already gone and 1275 /// The outgoing pump then finds the bytes already gone and
1279 /// has only its own state left to drop. Spelled as an argument rather 1276 /// has only its own state left to drop. Spelled as an argument rather
1280 /// than a second method so every call site has to say which it is — 1277 /// than a second method so every call site has to say which it is —
1281 /// silently taking the wrong one is a terminal left reporting clicks, 1278 /// silently taking the wrong one is a terminal left reporting clicks,
1282 /// or two releases racing a claim. 1279 /// or two releases racing a claim.
1283 pub const Undo = enum { write, already_written }; 1280 pub const Undo = enum { write, already_written };
1284 1281
1285 /// Give the terminal back: the demote, and every other way out. 1282 /// Give the terminal back: the release, and every other way out.
1286 /// 1283 ///
1287 /// Nothing goes on the WIRE — a demote is client-local — but plenty 1284 /// Nothing goes on the WIRE — the release is client-local — but plenty
1288 /// comes off the terminal, because the session that was promoted set 1285 /// comes off the terminal, because the session that held it set
1289 /// modes on it. A wall left still reporting clicks into the user's 1286 /// modes on it. A wall left still reporting clicks into the user's
1290 /// shell is the failure this pairs against. 1287 /// shell is the failure this pairs against.
1291 /// 1288 ///
1292 /// The scroll view goes with it whichever way the undo went: the 1289 /// The scroll view goes with it whichever way the undo went: the
1293 /// stripe that resumes paints live state from the same replica, so a 1290 /// stripe that resumes paints live state from the same replica, so a
1294 /// Core still suppressing paints would come back to a zoom showing 1291 /// Core still suppressing paints would come back to a tile showing
1295 /// nothing. 1292 /// nothing.
1296 /// 1293 ///
1297 /// Idempotent, and it says which teardown by what was claimed. 1294 /// Idempotent, and it says which teardown by what was claimed.
@@ -1299,9 +1296,9 @@ pub const Core = struct {
1299 const held = self.claim; 1296 const held = self.claim;
1300 self.claim = .none; 1297 self.claim = .none;
1301 self.dropScrollView(); 1298 self.dropScrollView();
1302 // The highlight goes with the screen it was drawn on. A demoted 1299 // The highlight goes with the screen it was drawn on. A tile that
1303 // tile paints nothing, so an inversion kept here would be invisible 1300 // lost focus paints nothing, so an inversion kept here would be
1304 // until the tile was zoomed again and then reappear over rows the 1301 // invisible until the tile was focused again and then reappear over rows the
1305 // user chose in another session's lifetime — and a reply still in 1302 // user chose in another session's lifetime — and a reply still in
1306 // flight would copy text for them. 1303 // flight would copy text for them.
1307 self.drag.clear(); 1304 self.drag.clear();
@@ -1312,7 +1309,7 @@ pub const Core = struct {
1312 } 1309 }
1313 } 1310 }
1314 1311
1315 /// For the wall's demoted stripe, which crops this grid. 1312 /// The engine this Core's replica paints from.
1316 pub fn grid(self: *Core) *Engine { 1313 pub fn grid(self: *Core) *Engine {
1317 return self.rep.eng; 1314 return self.rep.eng;
1318 } 1315 }
@@ -1334,9 +1331,9 @@ pub const Core = struct {
1334 if (self.sink.end) |e| e(self.sink.ctx); 1331 if (self.sink.end) |e| e(self.sink.ctx);
1335 } 1332 }
1336 1333
1337 /// The whole screen from the replica — the wall's promote, where the 1334 /// The whole screen from the replica — a local repaint at zero round
1338 /// replica has been hot the whole time the tile was a stripe, so the 1335 /// trips, the replica hot the whole time the tile has been painting its
1339 /// zoom is a local repaint at zero round trips. 1336 /// rect.
1340 /// 1337 ///
1341 /// The overlay goes back on top: the rows just drawn have overwritten 1338 /// The overlay goes back on top: the rows just drawn have overwritten
1342 /// predictions still outstanding. 1339 /// predictions still outstanding.
@@ -1497,8 +1494,8 @@ pub const Core = struct {
1497 // the round trip is no longer one. 1494 // the round trip is no longer one.
1498 // 1495 //
1499 // Painted raw rather than through `paintFull`: this arm and 1496 // Painted raw rather than through `paintFull`: this arm and
1500 // the overlay below it are ONE hold of the sink (a zoom that 1497 // the overlay below it are ONE hold of the sink (a focus move
1501 // moved between them would put the two halves of this frame 1498 // between them would put the two halves of this frame
1502 // on two different screens), and the sink is not reentrant. 1499 // on two different screens), and the sink is not reentrant.
1503 const hl = self.highlight(); 1500 const hl = self.highlight();
1504 try paint_mod.renderClipped(self.alloc, self.rep.eng, self.size, hl.sink(), self.row_off, self.out_fd); 1501 try paint_mod.renderClipped(self.alloc, self.rep.eng, self.size, hl.sink(), self.row_off, self.out_fd);
@@ -1546,8 +1543,8 @@ pub const Core = struct {
1546 /// rendered onto the host terminal by this platform's adapters. 1543 /// rendered onto the host terminal by this platform's adapters.
1547 /// 1544 ///
1548 /// The decode happens whatever the sink says and the WRITE does not, 1545 /// The decode happens whatever the sink says and the WRITE does not,
1549 /// which is the same split the claim already makes: a demoted wall tile 1546 /// which is the same split the claim already makes: a wall tile that is not focused
1550 /// tracks its session's modes for the promote that will need them (see 1547 /// tracks its session's modes for the focus claim that will need them (see
1551 /// `claimTerminal`) and puts nothing on a terminal it does not hold. 1548 /// `claimTerminal`) and puts nothing on a terminal it does not hold.
1552 /// 1549 ///
1553 /// Under the sink for a reason the claim gate does NOT cover, and the 1550 /// Under the sink for a reason the claim gate does NOT cover, and the
@@ -1676,7 +1673,7 @@ pub const Core = struct {
1676 .exit_status, .taken_over, .sessions_reply => return .not_mine, 1673 .exit_status, .taken_over, .sessions_reply => return .not_mine,
1677 // The correlation, the watermark and the size refusal are the 1674 // The correlation, the watermark and the size refusal are the
1678 // Core's (`selectionCopy`); WHERE the text goes is not. At the 1675 // Core's (`selectionCopy`); WHERE the text goes is not. At the
1679 // wall the tile that asked is a demoted stripe every time, and 1676 // wall the tile that asked is not the focused one every time, and
1680 // the clipboard it copies to belongs to the wall's screen. 1677 // the clipboard it copies to belongs to the wall's screen.
1681 .selection_reply => return .not_mine, 1678 .selection_reply => return .not_mine,
1682 // The driver owns local fds and the Core owns the replica, so 1679 // The driver owns local fds and the Core owns the replica, so
@@ -1853,10 +1850,10 @@ pub const Core = struct {
1853 // Speculate before sending, so the glyph is on screen while the 1850 // Speculate before sending, so the glyph is on screen while the
1854 // keystroke is still in flight. The bytes that go out are 1851 // keystroke is still in flight. The bytes that go out are
1855 // unchanged either way — and they go out whatever the sink 1852 // unchanged either way — and they go out whatever the sink
1856 // says: a demoted wall tile still owes its session the bytes 1853 // says: a wall tile that lost focus still owes its session the bytes
1857 // that were typed at it while it was the zoom, it just may not 1854 // that were typed at it while it held the terminal, it just may not
1858 // draw them. An overlay glyph on a stripe would be graffiti on 1855 // draw them. An overlay glyph it painted would be graffiti on
1859 // whichever session the zoom moved to. 1856 // whichever session holds the terminal now.
1860 if (self.beginPaint()) { 1857 if (self.beginPaint()) {
1861 defer self.endPaint(); 1858 defer self.endPaint();
1862 offerKeystroke(self.alloc, &self.overlay, self.rep.eng, keys, self.size, self.row_off, self.out_fd); 1859 offerKeystroke(self.alloc, &self.overlay, self.rep.eng, keys, self.size, self.row_off, self.out_fd);
@@ -1871,8 +1868,8 @@ pub const Core = struct {
1871 /// Left button only. Middle is the terminal's own paste and right its 1868 /// Left button only. Middle is the terminal's own paste and right its
1872 /// menu. 1869 /// menu.
1873 /// 1870 ///
1874 /// A plain click is a defined no-op: a zoomed tile is the only session 1871 /// A plain click is a defined no-op: a tile is the only session
1875 /// on its screen, so there is nothing for a click to select. 1872 /// on its rect, so there is nothing for a click to select.
1876 /// 1873 ///
1877 /// What comes back is the selection a release FINISHED. One read can 1874 /// What comes back is the selection a release FINISHED. One read can
1878 /// hold more than one release; the last is the answer, which is what 1875 /// hold more than one release; the last is the answer, which is what
@@ -1896,8 +1893,8 @@ pub const Core = struct {
1896 1893
1897 /// Ask the daemon for the text under a finished selection. 1894 /// Ask the daemon for the text under a finished selection.
1898 /// 1895 ///
1899 /// The zoomed driver calls this from `forward`, which already runs on 1896 /// Called from `forward`, which already runs on the focused tile's
1900 /// the tile's pump — the thread that owns this transport. The WALL's 1897 /// pump — the thread that owns this transport. The WALL's
1901 /// keyboard may not: a `Transport` has exactly one owning thread, so it 1898 /// keyboard may not: a `Transport` has exactly one owning thread, so it
1902 /// posts the range and its pump calls this instead 1899 /// posts the range and its pump calls this instead
1903 /// (`wallview`'s `copySelection`). 1900 /// (`wallview`'s `copySelection`).
@@ -1916,8 +1913,8 @@ pub const Core = struct {
1916 } 1913 }
1917 1914
1918 /// What a `selection_reply` is worth, given the selection that is still 1915 /// What a `selection_reply` is worth, given the selection that is still
1919 /// held — `Core.drag` zoomed, `wallview.Shared.drag` at the wall, which 1916 /// held — `Core.drag`, read by the caller under its lock, which is why
1920 /// is why the caller supplies it rather than this reading one. 1917 /// it supplies the range rather than this reading one.
1921 /// 1918 ///
1922 /// The correlation is `client_core`'s, unchanged: a reply that answers 1919 /// The correlation is `client_core`'s, unchanged: a reply that answers
1923 /// no pending request, or answers one this drag already replaced, is 1920 /// no pending request, or answers one this drag already replaced, is
@@ -1954,8 +1951,8 @@ pub const Core = struct {
1954 /// that names none. 1951 /// that names none.
1955 /// 1952 ///
1956 /// The mapping is trivial and that is the whole reason selection lives 1953 /// The mapping is trivial and that is the whole reason selection lives
1957 /// in the Core while zoomed: `renderClipped` is top-anchored, so a 1954 /// in the Core: a terminal row is a grid row less `row_off`, and the
1958 /// terminal row IS a grid row, and the only conversion left is the 1955 /// only conversion left is the
1959 /// history the daemon is holding under it. 1956 /// history the daemon is holding under it.
1960 fn hitTest(self: *Core, ev: MouseFilter.Event) ?select.Hit { 1957 fn hitTest(self: *Core, ev: MouseFilter.Event) ?select.Hit {
1961 // Scrolled back, nothing on this screen is addressable: 1958 // Scrolled back, nothing on this screen is addressable:
@@ -2263,7 +2260,7 @@ test "interact: an event knows where it fell among the keys" {
2263 // `forward` flattens the keys and drops the reports, so a driver 2260 // `forward` flattens the keys and drops the reports, so a driver
2264 // reading the two lists side by side cannot tell whether the click 2261 // reading the two lists side by side cannot tell whether the click
2265 // came before or after the Enter — and at the wall that is the 2262 // came before or after the Enter — and at the wall that is the
2266 // difference between zooming the tile the user chose and zooming the 2263 // difference between focusing the tile the user chose and focusing the
2267 // one their click had just moved the selection to. 2264 // one their click had just moved the selection to.
2268 const key_first = f.feed("\r\x1b[<0;3;2M", &out); 2265 const key_first = f.feed("\r\x1b[<0;3;2M", &out);
2269 try std.testing.expectEqualStrings("\r", key_first.forward); 2266 try std.testing.expectEqualStrings("\r", key_first.forward);
@@ -2966,18 +2963,18 @@ test "interact: the exit teardown unsets every mode mux turned on, and pops the
2966 } 2963 }
2967 2964
2968 test "interact: a borrowed terminal's claim is a session's, and every teardown undoes it" { 2965 test "interact: a borrowed terminal's claim is a session's, and every teardown undoes it" {
2969 // The claim a wall tile takes at a promote is the mouse modes and 2966 // The claim a wall tile takes when it gains focus is the mouse modes and
2970 // nothing else — the SCREEN was taken once, by the wall — and the 2967 // nothing else — the SCREEN was taken once, by the wall — and the
2971 // release is the head of every teardown there is. 2968 // release is the head of every teardown there is.
2972 // 2969 //
2973 // The pairing this pins turns over many times per run — once per zoom, 2970 // The pairing this pins turns over many times per run — once per focus move,
2974 // where the screen's turns over once per process — so a half that only 2971 // where the screen's turns over once per process — so a half that only
2975 // works on the exit path is a wall that leaves a terminal reporting 2972 // works on the exit path is a wall that leaves a terminal reporting
2976 // clicks into a shell the moment somebody unzooms. 2973 // clicks into a shell the moment focus moves on.
2977 try std.testing.expectEqualStrings("\x1b[?1000h\x1b[?1002h\x1b[?1006h", session_claim); 2974 try std.testing.expectEqualStrings("\x1b[?1000h\x1b[?1002h\x1b[?1006h", session_claim);
2978 try std.testing.expect(std.mem.startsWith(u8, terminal_teardown, session_release)); 2975 try std.testing.expect(std.mem.startsWith(u8, terminal_teardown, session_release));
2979 try std.testing.expect(std.mem.indexOf(u8, wall_teardown, session_release) != null); 2976 try std.testing.expect(std.mem.indexOf(u8, wall_teardown, session_release) != null);
2980 // And it is exported, because the DEMOTE is written by a thread that 2977 // And it is exported, because the release is written by a thread that
2981 // holds no Core: wallview's `setFocus`. A release that only a Core could 2978 // holds no Core: wallview's `setFocus`. A release that only a Core could
2982 // write is a release that cannot be ordered against the next Core's 2979 // write is a release that cannot be ordered against the next Core's
2983 // claim. 2980 // claim.
@@ -3016,8 +3013,8 @@ test "interact: a borrowed terminal's claim is a session's, and every teardown u
3016 comptime std.fmt.comptimePrint("\x1b[?{d}l", .{dec}), 3013 comptime std.fmt.comptimePrint("\x1b[?{d}l", .{dec}),
3017 ) != null); 3014 ) != null);
3018 } 3015 }
3019 // ...and so does every mode the DAEMON can ask a promoted tile to 3016 // ...and so does every mode the DAEMON can ask a focused tile to
3020 // mirror, which is the half that drifts: a zoomed tile's application 3017 // mirror, which is the half that drifts: a focused tile's application
3021 // can turn on 1002 or 1003, modes the client's own capture set never 3018 // can turn on 1002 or 1003, modes the client's own capture set never
3022 // names and nothing else would take back off. 3019 // names and nothing else would take back off.
3023 inline for (proto.mouse_modes) |m| { 3020 inline for (proto.mouse_modes) |m| {
@@ -3068,16 +3065,16 @@ test "interact: a promote takes the mouse, a demote gives it back, a demote twic
3068 const claimed = drainPipe(p[0], &buf); 3065 const claimed = drainPipe(p[0], &buf);
3069 try std.testing.expect(std.mem.startsWith(u8, claimed, session_claim)); 3066 try std.testing.expect(std.mem.startsWith(u8, claimed, session_claim));
3070 // A mode the claim never names, so its presence can only be the 3067 // A mode the claim never names, so its presence can only be the
3071 // level-set — the half a promote needs because `resyncSnapshot` answers 3068 // level-set — the half a claim needs because `resyncSnapshot` answers
3072 // a resize with no `term_modes` at all. 1003 rather than 1002: 1002 is 3069 // a resize with no `term_modes` at all. 1003 rather than 1002: 1002 is
3073 // in the client's own capture set now, so the claim writes it and its 3070 // in the client's own capture set now, so the claim writes it and its
3074 // presence would no longer distinguish the two writes. 3071 // presence would no longer distinguish the two writes.
3075 try std.testing.expect(std.mem.indexOf(u8, claimed, "\x1b[?1003l") != null); 3072 try std.testing.expect(std.mem.indexOf(u8, claimed, "\x1b[?1003l") != null);
3076 3073
3077 // A promote onto the tile already zoomed re-asserts the grid, not the 3074 // A claim onto the tile already focused re-asserts the grid, not the
3078 // modes: nothing is written a second time, so nothing has to come off 3075 // modes: nothing is written a second time, so nothing has to come off
3079 // twice either. `false` says so — the same answer a promote gets when 3076 // twice either. `false` says so — the same answer a claim gets when
3080 // the zoom moved out from under it. 3077 // the focus moved out from under it.
3081 try std.testing.expect(!core.claimTerminal()); 3078 try std.testing.expect(!core.claimTerminal());
3082 3079
3083 core.releaseTerminal(.write); 3080 core.releaseTerminal(.write);
@@ -3085,7 +3082,7 @@ test "interact: a promote takes the mouse, a demote gives it back, a demote twic
3085 try std.testing.expectEqualStrings(session_release, drainPipe(p[0], &buf)); 3082 try std.testing.expectEqualStrings(session_release, drainPipe(p[0], &buf));
3086 3083
3087 // A release with nothing claimed writes nothing. That is what makes 3084 // A release with nothing claimed writes nothing. That is what makes
3088 // `deinit` safe on a tile nobody ever zoomed — and what stops a wall 3085 // `deinit` safe on a tile nobody ever claimed — and what stops a wall
3089 // from unsetting a mode the SESSION never set, on a terminal the wall 3086 // from unsetting a mode the SESSION never set, on a terminal the wall
3090 // is about to hand back to the user. 3087 // is about to hand back to the user.
3091 core.releaseTerminal(.write); 3088 core.releaseTerminal(.write);
@@ -3123,12 +3120,12 @@ const SelectionTransport = struct {
3123 } 3120 }
3124 }; 3121 };
3125 3122
3126 /// A zoomed Core on a pipe, with a claim, ready to be dragged over. 3123 /// A Core on a pipe, with a claim, ready to be dragged over.
3127 fn dragFixture(alloc: std.mem.Allocator, out_fd: std.posix.fd_t) !Core { 3124 fn dragFixture(alloc: std.mem.Allocator, out_fd: std.posix.fd_t) !Core {
3128 var core = try Core.initSized(alloc, -1, out_fd, .{ .cols = 20, .rows = 6 }); 3125 var core = try Core.initSized(alloc, -1, out_fd, .{ .cols = 20, .rows = 6 });
3129 core.is_tty = true; 3126 core.is_tty = true;
3130 // A drag only happens on a terminal this Core took over — which is 3127 // A drag only happens on a terminal this Core took over — which is
3131 // exactly the promoted-tile case, and the reason an unzoomed tile 3128 // exactly the focused-tile case, and the reason a non-focused tile
3132 // never reaches any of this. 3129 // never reaches any of this.
3133 _ = core.claimTerminal(); 3130 _ = core.claimTerminal();
3134 // Row 4 carries wide cells. Kept OFF the rows the column assertions 3131 // Row 4 carries wide cells. Kept OFF the rows the column assertions
@@ -3185,7 +3182,7 @@ test "interact: a drag at a zoomed tile inverts what it crossed, and a click doe
3185 // it comes back with no inversion on it. 3182 // it comes back with no inversion on it.
3186 try std.testing.expect(std.mem.indexOf(u8, cleared, "row-one") != null); 3183 try std.testing.expect(std.mem.indexOf(u8, cleared, "row-one") != null);
3187 try std.testing.expect(std.mem.indexOf(u8, cleared, "\x1b[7m") == null); 3184 try std.testing.expect(std.mem.indexOf(u8, cleared, "\x1b[7m") == null);
3188 // ...and that press is a click, which a zoomed tile answers with 3185 // ...and that press is a click, which a focused tile answers with
3189 // nothing: there is one session on this screen to select. 3186 // nothing: there is one session on this screen to select.
3190 _ = try core.forward(&tr, "\x1b[<0;1;4m"); 3187 _ = try core.forward(&tr, "\x1b[<0;1;4m");
3191 try std.testing.expectError(error.WouldBlock, std.posix.read(p[0], &buf)); 3188 try std.testing.expectError(error.WouldBlock, std.posix.read(p[0], &buf));
@@ -3686,7 +3683,7 @@ test "interact: a reply is the text that was asked for, or it is nothing" {
3686 } 3683 }
3687 3684
3688 // A reply that outlived its own highlight. Whatever cleared it — a 3685 // A reply that outlived its own highlight. Whatever cleared it — a
3689 // relayout, a forget, a zoom, a resync — the rows it names are not on 3686 // relayout, a forget, a focus move, a resync — the rows it names are not on
3690 // anybody's screen now, and a copy from it is a copy the user did not 3687 // anybody's screen now, and a copy from it is a copy the user did not
3691 // ask for and cannot see. 3688 // ask for and cannot see.
3692 _ = try askedFor(&core, &tr); 3689 _ = try askedFor(&core, &tr);
@@ -3737,7 +3734,7 @@ test "interact: a demote drops the highlight, and the reply it was waiting for"
3737 var rbuf: [64]u8 = undefined; 3734 var rbuf: [64]u8 = undefined;
3738 const id = core.semantic.pending_selection_id.?; 3735 const id = core.semantic.pending_selection_id.?;
3739 3736
3740 // The zoom moved on. This Core's screen belongs to somebody else now — 3737 // The focus moved on. This Core's screen belongs to somebody else now —
3741 // the wall's stripes, or another tile — so the inversion it was 3738 // the wall's stripes, or another tile — so the inversion it was
3742 // holding is over rows nobody can see, and the answer still in flight 3739 // holding is over rows nobody can see, and the answer still in flight
3743 // is for a highlight that has stopped existing. 3740 // is for a highlight that has stopped existing.
src/mux_main.zig
Old New
@@ -4,7 +4,7 @@
4 //! `mux HOST` runs the ssh→QUIC handoff — ssh fetches the daemon's QUIC 4 //! `mux HOST` runs the ssh→QUIC handoff — ssh fetches the daemon's QUIC
5 //! coordinates and carries the session only if the QUIC dial does not. 5 //! coordinates and carries the session only if the QUIC dial does not.
6 //! 6 //!
7 //! Every one of those is a WALL of one tile, entered focused 7 //! Every one of those is a WALL of one tile whose rect is the whole terminal
8 //! (`wallview.runAttach`); `mux wall` is the same program entered on the 8 //! (`wallview.runAttach`); `mux wall` is the same program entered on the
9 //! wall itself. What lives up here is argv, the refusals that must happen 9 //! wall itself. What lives up here is argv, the refusals that must happen
10 //! before a dial (self-attach, an unbindable socket path), the auto-start, 10 //! before a dial (self-attach, an unbindable socket path), the auto-start,
@@ -46,7 +46,7 @@ const usage =
46 \\ 46 \\
47 \\ mux wall add SPELLING... / mux wall rm SPELLING... edit the saved 47 \\ mux wall add SPELLING... / mux wall rm SPELLING... edit the saved
48 \\ wall without dialling anything. The wall is attach history: `mux` 48 \\ wall without dialling anything. The wall is attach history: `mux`
49 \\ itself adds the tile it attaches to, `x` on the wall forgets one, 49 \\ itself adds the tile it attaches to, `Ctrl-\ x` on the wall forgets one,
50 \\ and neither ever kills a session. 50 \\ and neither ever kills a session.
51 \\ 51 \\
52 ; 52 ;
src/wallview.zig
Old New
@@ -817,7 +817,7 @@ fn redial(
817 transport.close(); 817 transport.close();
818 state.* = .reconnecting; 818 state.* = .reconnecting;
819 paintLabel(t, state.*); 819 paintLabel(t, state.*);
820 // ...and the same news for the tile that is ZOOMED, which has no label 820 // ...and the same news for a one-tile wall, which has no label
821 // bar on screen to read it off. The corner banner is the plain client's 821 // bar on screen to read it off. The corner banner is the plain client's
822 // own, said before the dial rather than inside it for its reason: it is 822 // own, said before the dial rather than inside it for its reason: it is
823 // a PAINT on the session's screen, and the Core is what paints. `banner` 823 // a PAINT on the session's screen, and the Core is what paints. `banner`
@@ -1732,9 +1732,9 @@ fn tileLabel(alloc: std.mem.Allocator, target: client.Target, name: []const u8)
1732 }; 1732 };
1733 } 1733 }
1734 1734
1735 /// What a pump's ending means for the wall. Only the tile under the ZOOM 1735 /// What a pump's ending means for the wall. Only the last present tile
1736 /// can end the program: a stripe that goes quiet narrates itself and the 1736 /// ending can end the program: a tile that goes quiet while others remain
1737 /// wall goes on, which is what the dead-tile paint is for. 1737 /// narrates itself and the wall goes on, which is what the dead-tile paint is for.
1738 const EndAction = union(enum) { 1738 const EndAction = union(enum) {
1739 /// The wall has other tiles: the focus moves to the next present one. 1739 /// The wall has other tiles: the focus moves to the next present one.
1740 /// Its label already says what became of the ended tile. 1740 /// Its label already says what became of the ended tile.
@@ -2843,7 +2843,7 @@ test "a copy too big for OSC 52 is said out loud rather than dropped" {
2843 test "setFocus writes the outgoing tile's release before the store that lets the next pump claim" { 2843 test "setFocus writes the outgoing tile's release before the store that lets the next pump claim" {
2844 // The keyboard thread's half of the terminal handover, and the order is 2844 // The keyboard thread's half of the terminal handover, and the order is
2845 // load-bearing: two pumps are two threads, and A's release and B's claim 2845 // load-bearing: two pumps are two threads, and A's release and B's claim
2846 // race if the release is not on the wire first. A demoted pump writes 2846 // race if the release is not on the wire first. The outgoing pump writes
2847 // nothing (already_written), so if this stops emitting, nothing does — 2847 // nothing (already_written), so if this stops emitting, nothing does —
2848 // and the wall reports clicks into whatever shell the user lands in. 2848 // and the wall reports clicks into whatever shell the user lands in.
2849 // is_tty is true because this leg is about what a focus move WRITES, and 2849 // is_tty is true because this leg is about what a focus move WRITES, and