f3100d58
docs: the chord list stops describing zoom
a73x 2026-08-24 11:32
Commit message
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 |