a73x

52e192d6

refactor: wallview's comments name the failure, not the history

a73x   2026-08-30 19:16

Commit message
refactor: wallview's comments name the failure, not the history

63 essays down to none. Field docs now say what the field is and what
breaks when it is wrong; the paragraphs re-deriving how a race was found,
what a previous cut did, and what a measurement once showed are gone.

2497 -> 2214 lines, 1120 -> 806 comment lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSUFuYHqU9wr4J5NC8EkWV

docscheck.blocks
Old New
@@ -54,7 +54,7 @@ wall_test_layout.zig 2
54 wall_test_picker.zig 4 54 wall_test_picker.zig 4
55 wall_test_pump.zig 9 55 wall_test_pump.zig 9
56 wall_test_wall.zig 7 56 wall_test_wall.zig 7
57 wallview.zig 63 57 wallview.zig 0
58 wasm_core.zig 5 58 wasm_core.zig 5
59 webhub_main.zig 4 59 webhub_main.zig 4
60 webhub.zig 29 60 webhub.zig 29
src/tui/wallview.zig
Old New
@@ -1,22 +1,7 @@
1 //! The CLI wall: N sessions in one terminal, the multiattach the browser hub 1 //! The CLI wall: N sessions in one terminal. It lists daemons (hosts.zig)
2 //! gives without a browser. The wall lists daemons (hosts.zig); a host's 2 //! and a host's live sessions are its tiles, polled. Every tile claims its
3 //! live sessions are its tiles, polled. 3 //! rectangle and focus is client-local. One thread per tile owns that tile's
4 //! 4 //! transport both ways, so the keyboard hands it bytes over a mailbox.
5 //! Every tile claims its rectangle: attach sends the rect, relayout resends
6 //! it, and focus is client-local and sends nothing on the wire.
7 //!
8 //! Tiles are never compacted: pump threads hold pointers into the tile array,
9 //! so a forgotten tile is a hole focus steps over (`stepPresent`). A tile
10 //! whose pump has ended keeps its rect and narrates on its label bar.
11 //!
12 //! One thread per tile, owning its transport BOTH directions. A `Transport`
13 //! is single-threaded — `service()` and `writeFrame` share QUIC state, and a
14 //! reconnect swaps the struct out from under the pump — so the keyboard
15 //! hands typed bytes over a mailbox and doorbell pipe. Threads, not one fair
16 //! loop: readFrame blocks and userspace buffering is invisible to poll.
17 //!
18 //! A beside cut needs VT-safe truncation of a grid row, because the
19 //! terminal only clips at ITS edge: `engine.dumpVtRowClipped` is it.
20 const std = @import("std"); 5 const std = @import("std");
21 const proto = @import("term").protocol; 6 const proto = @import("term").protocol;
22 const client = @import("client"); 7 const client = @import("client");
@@ -50,11 +35,8 @@ pub const resolveHost = client.resolveHost;
50 35
51 /// Whether this process has a screen to cut stripes on. 36 /// Whether this process has a screen to cut stripes on.
52 fn headless(out_fd: std.posix.fd_t) bool { 37 fn headless(out_fd: std.posix.fd_t) bool {
53 // Everything a wall does beyond its entry tile — other hosts, their 38 // A `mux` with no measurable terminal is a wall of ONE, writing exactly
54 // pollers, a saved layout — is stripes, so a `mux` with no measurable 39 // what the plain client wrote: everything else a wall does is stripes.
55 // terminal is a wall of ONE and writes what the plain client wrote.
56 // `run` asks this of the same fd as `measured == null`; `runAttach`
57 // asks it before there is a `run` to ask.
58 return interact.ttySize(out_fd) == null; 40 return interact.ttySize(out_fd) == null;
59 } 41 }
60 42
@@ -94,16 +76,8 @@ pub const State = enum {
94 /// between two turns of a pump's poll loop. 76 /// between two turns of a pump's poll loop.
95 pub const mailbox_max = 4096; 77 pub const mailbox_max = 4096;
96 78
97 /// The most tiles one wall can hold. 79 /// The most tiles one wall can hold. A hard bound, not a growable array:
98 /// 80 /// pump threads hold `*Tile` pointers, so growing would move it under them.
99 /// A hard bound rather than a growable array, because the pump threads hold
100 /// `*Tile` pointers for the wall's whole life: growing the array would move
101 /// it under them. The number is generous beside what a terminal can show
102 /// (`wallFloors` refuses a rect too thin for the daemon's row floor
103 /// under a label bar, so an 80x24 terminal caps at eight tiles) and beside
104 /// `max_clients = 8` per daemon. What it really
105 /// bounds is how much a wall of hosts is allowed to cost when the daemons
106 /// on it have grown many sessions.
107 pub const max_tiles: usize = 32; 81 pub const max_tiles: usize = 32;
108 82
109 /// Why a tile's pump stopped, for the keyboard that has to decide what that 83 /// Why a tile's pump stopped, for the keyboard that has to decide what that
@@ -120,47 +94,20 @@ pub const Shared = struct {
120 /// screen being cleared and the terminal changing hands. 94 /// screen being cleared and the terminal changing hands.
121 paint_mu: std.Thread.Mutex = .{}, 95 paint_mu: std.Thread.Mutex = .{},
122 out_fd: std.posix.fd_t, 96 out_fd: std.posix.fd_t,
123 /// Whether there is a terminal here at all. 97 /// Whether there is a terminal here at all. Every WALL-level write is
124 /// 98 /// gated on it: on a pipe mux writes the grid and no terminal state.
125 /// `mux TARGET` is a wall of one tile now, and a scripted `mux`
126 /// with a pipe on both ends is the shape half the suite runs. On a pipe
127 /// it must write exactly what the plain client wrote — the grid, and no
128 /// terminal state whatever: no alternate screen, no raw mode, no
129 /// clears, nothing to undo. So every WALL-level write is gated here.
130 /// A wall of many is unaffected: it still refuses to run without a
131 /// terminal to cut stripes from, and says so.
132 is_tty: bool, 99 is_tty: bool,
133 /// The pumps' doorbell to the KEYBOARD — the mirror of `Tile.wake_w`. 100 /// The pumps' doorbell to the KEYBOARD — the mirror of `Tile.wake_w`.
134 /// 101 /// Without it a shell that exits goes unnoticed until the next keystroke.
135 /// A pump rings it when something only the keyboard can act on has
136 /// happened: a tile went quiet, a `sessions_reply` came back with a
137 /// name to focus, a detach was acknowledged. Without it the keyboard
138 /// sits in read(2) on a stdin that may never say anything again, and a
139 /// shell that exits under the focus would go unnoticed until the next
140 /// keystroke.
141 kb_r: std.posix.fd_t = -1, 102 kb_r: std.posix.fd_t = -1,
142 kb_w: std.posix.fd_t = -1, 103 kb_w: std.posix.fd_t = -1,
143 /// The focused tile's prediction counters, republished on every pass a 104 /// The focused tile's prediction counters. Published OUT to the driver
144 /// tile paints while it holds the terminal. 105 /// that owns the exit, because a pump is a detached thread that never
145 /// 106 /// reaches a `deinit` to print `MUX_PREDICT_STATS` itself.
146 /// `MUX_PREDICT_STATS` used to be printed by `Core.deinit`, and a wall
147 /// tile's Core never reaches one: its pump is a detached thread the
148 /// process exit kills where it stands. So the counters come OUT to the
149 /// driver that does own the exit, which prints them once, after the
150 /// terminal is back — the same line, on the same screen, in the same
151 /// order the plain client put it there. A struct copy under a lock the
152 /// focused pump is already taking on every paint.
153 stats: interact.PredictCounters = .{}, 107 stats: interact.PredictCounters = .{},
154 /// One sentence for the next focused tile to put in its corner. 108 /// One sentence for the next focused tile to put in its corner. The
155 /// 109 /// keyboard has no painter for a LIVE tile — `Core.banner` belongs to a
156 /// Written by the keyboard under `paint_mu`, taken by the pump that 110 /// Core — so it leaves the message where that pump will find it.
157 /// owns the terminal. The keyboard has no painter of its own for a
158 /// LIVE tile — `Core.banner` belongs to a Core, and the Core that
159 /// matters is the one being focused ON — so the message is left where
160 /// that pump will find it when it claims.
161 /// Sized for a corner banner, not for arbitrary text: `setNotice`
162 /// truncates rather than allocate for a message nobody can read at
163 /// that width anyway.
164 notice: [96]u8 = undefined, 111 notice: [96]u8 = undefined,
165 notice_len: usize = 0, 112 notice_len: usize = 0,
166 /// The terminal's shape: measured at startup, and re-measured by the 113 /// The terminal's shape: measured at startup, and re-measured by the
@@ -168,17 +115,11 @@ pub const Shared = struct {
168 /// the thread that lays the wall out may act on it). Stripes are cut 115 /// the thread that lays the wall out may act on it). Stripes are cut
169 /// from it and a one-tile wall claims exactly it. 116 /// from it and a one-tile wall claims exactly it.
170 size: proto.Size, 117 size: proto.Size,
171 /// The selected tile's index — the FOCUSED tile, the one input goes to 118 /// The FOCUSED tile: where input goes, and whose pump holds the terminal.
172 /// and the one whose pump has claimed the terminal. Under `paint_mu` 119 /// Under `paint_mu` so a repaint draws the focus the keyboard has now.
173 /// rather than atomic because it is read while a label bar is being
174 /// drawn: a pump repainting on a frame must draw the focus the keyboard
175 /// has, not the one it had when the frame arrived.
176 sel: usize = 0, 120 sel: usize = 0,
177 /// Where the focused tile's last paint left the cursor on the terminal. 121 /// Where the focused tile's last paint left the cursor. The cursor
178 /// The cursor belongs to the focus, not to whichever pump painted last: 122 /// belongs to the focus: an unfocused paint's last act puts it back here.
179 /// an unfocused paint's last act is to put the cursor back here. Under
180 /// `paint_mu` because the painters that read and write it already stand
181 /// there. Home until the focused tile has painted once.
182 cursor: Engine.CursorPos = .{ .x = 0, .y = 0 }, 123 cursor: Engine.CursorPos = .{ .x = 0, .y = 0 },
183 /// One label bar per tile when the wall holds more than one session; a 124 /// One label bar per tile when the wall holds more than one session; a
184 /// one-tile wall owns every row and draws no bar. Set by `relayout`, so 125 /// one-tile wall owns every row and draws no bar. Set by `relayout`, so
@@ -201,11 +142,8 @@ pub const Shared = struct {
201 /// `f` toggles it; relayout passes `sel` to flatten when set, so the 142 /// `f` toggles it; relayout passes `sel` to flatten when set, so the
202 /// focused tile takes the whole terminal and every other gets 0×0. 143 /// focused tile takes the whole terminal and every other gets 0×0.
203 fullscreen: bool = false, 144 fullscreen: bool = false,
204 /// Bumped when the terminal's contents are no longer anybody's paint — 145 /// Bumped when the terminal's contents are no longer anybody's paint.
205 /// a relayout re-cut the stripes, or the focus moved and the newly 146 /// The only thing that repaints a tile whose session has gone quiet.
206 /// focused tile owes a full repaint. Pumps compare it against what
207 /// they last painted at, which is the only thing that repaints a tile
208 /// whose session sent no new frame meanwhile.
209 repaint_gen: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), 147 repaint_gen: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
210 /// True while the host picker's popup is on the terminal. Every paint 148 /// True while the host picker's popup is on the terminal. Every paint
211 /// asks: a tile redrawing its rect under the box would erase the rows 149 /// asks: a tile redrawing its rect under the box would erase the rows
@@ -221,13 +159,8 @@ pub const Shared = struct {
221 /// arrive with the picker already up. 159 /// arrive with the picker already up.
222 ask_open: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), 160 ask_open: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
223 /// Where this wall answers ssh's prompts, and the binary ssh execs to 161 /// Where this wall answers ssh's prompts, and the binary ssh execs to
224 /// ask. Null is "ssh keeps its own prompts": a piped `mux` has no 162 /// ask. Null is "ssh keeps its own prompts": without `$XDG_RUNTIME_DIR`
225 /// popup to answer with, and a box with no `$XDG_RUNTIME_DIR` has 163 /// the socket would land on a shared /tmp, answerable by any local user.
226 /// nowhere private to put the socket — a prompt on a shared /tmp is
227 /// one any local user could answer.
228 ///
229 /// Named for what arrives on it, not `ask`: `Tile.ask` is the end-key's
230 /// own arm, and two fields called `ask` in one file are a reader's trap.
231 prompts: ?*askpass.Listener = null, 164 prompts: ?*askpass.Listener = null,
232 prompt_exe: []const u8 = "", 165 prompt_exe: []const u8 = "",
233 }; 166 };
@@ -250,15 +183,9 @@ pub fn closeAsk(
250 prefix: *interact.PrefixFilter, 183 prefix: *interact.PrefixFilter,
251 picker_sel: usize, 184 picker_sel: usize,
252 ) void { 185 ) void {
253 // ONE function because the two ends had drifted: `relayout` CLEARS the 186 // ONE function: `relayout` CLEARS the screen, so a picker still open
254 // screen, so a picker still open under the box owes a repaint, and the 187 // under the box owes a repaint. The answer buffer is zeroed here and
255 // hangup path did not know it. A prompt can arrive over an open picker 188 // nowhere else — `feed` cannot, since the action it returned borrows it.
256 // — that is why the answer has a buffer of its own — so a notice whose
257 // helper hung up left `picking` true on a blank screen until the next
258 // key.
259 //
260 // The answer buffer goes here and nowhere else: `feed` cannot zero it,
261 // because the action it returned borrows it.
262 prefix.askClose(); 189 prefix.askClose();
263 shared.ask_open.store(false, .release); 190 shared.ask_open.store(false, .release);
264 // `relayout` bumps `repaint_gen`, which is the only thing that redraws 191 // `relayout` bumps `repaint_gen`, which is the only thing that redraws
@@ -279,10 +206,8 @@ pub fn popupOpen(shared: *const Shared) bool {
279 } 206 }
280 207
281 /// The rect a paint used, in the only terms a click can be resolved in. 208 /// The rect a paint used, in the only terms a click can be resolved in.
282 /// 209 /// Absolute rows count from the oldest row the DAEMON retains, so a grid row
283 /// Absolute rows are counted from the oldest row the DAEMON still retains 210 /// alone names nothing without the history count its frame carried.
284 /// (`protocol.SelectionReply`), so a grid row alone names nothing: it has
285 /// to be read against the history count the same frame carried.
286 pub const Tile = struct { 211 pub const Tile = struct {
287 r: Resolved, 212 r: Resolved,
288 rect: layout.Rect, 213 rect: layout.Rect,
@@ -314,34 +239,17 @@ pub const Tile = struct {
314 /// here (under `paint_mu`) because the KEYBOARD repaints this bar too, 239 /// here (under `paint_mu`) because the KEYBOARD repaints this bar too,
315 /// when the focus moves, and it has no other way to know it. 240 /// when the focus moves, and it has no other way to know it.
316 state: State = .connecting, 241 state: State = .connecting,
317 /// Whether this tile's pump thread is still running. 242 /// Whether this tile's pump thread is still running. Only pumps answer
318 /// 243 /// `repaint_gen`, so a DEAD pump's bar is the one paint the keyboard
319 /// Only pumps answer `repaint_gen`, so a tile whose pump has ENDED — a 244 /// makes for a tile — drawing over a live one would stale it.
320 /// refused attach, a session that exited — has nobody to redraw its
321 /// rect after a relayout clear. The keyboard reads this and paints the
322 /// dead tile's label bar, which is the only paint it ever does on a
323 /// tile's behalf.
324 ///
325 /// Deliberately ONLY for dead pumps. A tile that is merely
326 /// `[reconnecting]` still has a thread that will repaint its hot
327 /// replica within a poll timeout, and letting the keyboard draw over
328 /// that would replace something true with something stale.
329 alive: std.atomic.Value(bool) = std.atomic.Value(bool).init(true), 245 alive: std.atomic.Value(bool) = std.atomic.Value(bool).init(true),
330 /// Whether this slot's pump thread has RETURNED. Stored as the pump's 246 /// Whether this slot's pump thread has RETURNED — the half of "free"
331 /// last act — after `alive`, after the bell that reads `t.shared` — and 247 /// that `present` cannot say. Reusing the slot without it overwrites a
332 /// it is the half of "free" that `present` cannot say: a birth that 248 /// `*Tile` the returning thread is still reading.
333 /// reused a vanished slot on `!present` alone would overwrite a `*Tile`
334 /// the returning thread is still reading.
335 pump_done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), 249 pump_done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
336 /// Forgotten by `Ctrl-\ x`: off the wall, and off the wire as soon as 250 /// Forgotten by `Ctrl-\ x`: off the wall, and off the wire as soon as the
337 /// the pump notices. The pump's answer is to RETURN — which closes its 251 /// pump notices. "Remove is detach" — the session is untouched. Distinct
338 /// transport and frees the daemon slot — and nothing more: "remove is 252 /// from `alive`: a gone tile the user removed, a dead one still narrates.
339 /// detach", so the session itself is untouched and still there for the
340 /// next `mux` that asks for it.
341 ///
342 /// Distinct from `alive`, which says the pump has ENDED (refused,
343 /// exited, never started). A gone tile is one the user removed; a dead
344 /// one is still on the wall, narrating what became of it.
345 gone: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), 253 gone: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
346 /// The doorbell. The keyboard writes one byte here to wake this tile's 254 /// The doorbell. The keyboard writes one byte here to wake this tile's
347 /// pump; the bytes themselves carry nothing, the mailbox does. Both 255 /// pump; the bytes themselves carry nothing, the mailbox does. Both
@@ -349,21 +257,14 @@ pub const Tile = struct {
349 /// ringing free to ring again. 257 /// ringing free to ring again.
350 wake_r: std.posix.fd_t = -1, 258 wake_r: std.posix.fd_t = -1,
351 wake_w: std.posix.fd_t = -1, 259 wake_w: std.posix.fd_t = -1,
352 /// Bytes typed at this tile while it was focused, waiting for its pump. 260 /// Bytes typed at this tile while it was focused. The keyboard writing
353 /// The keyboard is the only writer and it writes ONLY to the focused 261 /// ONLY to the focused tile is the whole enforcement of "input goes to
354 /// tile — which is the whole enforcement of "input goes to the focus". 262 /// the focus", so the pump sends what it finds without re-checking.
355 /// The pump therefore sends whatever it finds here without re-checking
356 /// the focus: bytes typed at a session belong to that session even if
357 /// the focus has moved on since.
358 in_mu: std.Thread.Mutex = .{}, 263 in_mu: std.Thread.Mutex = .{},
359 in: [mailbox_max]u8 = undefined, 264 in: [mailbox_max]u8 = undefined,
360 in_len: usize = 0, 265 in_len: usize = 0,
361 /// A transport somebody else already opened, adopted instead of 266 /// The ENTRY tile's link, adopted instead of dialled: `mux HOST` dials on
362 /// dialled. This is the ENTRY tile's link: `mux HOST` dials on the main 267 /// the main thread while it still has the tty, so ssh can prompt.
363 /// thread while it still has the tty, so ssh can ask for a hostkey or a
364 /// password, and a dial that never came up exits with the message and
365 /// the code the plain client always gave. The pump `adopt`s it, which
366 /// is what moves its QUIC out-queue onto this thread's allocator.
367 pre: ?client.Transport = null, 268 pre: ?client.Transport = null,
368 /// Whether this tile's attach may CREATE the session it names. The 269 /// Whether this tile's attach may CREATE the session it names. The
369 /// daemon reads create-vs-join off the attach's size claim, so a tile 270 /// daemon reads create-vs-join off the attach's size claim, so a tile
@@ -378,22 +279,13 @@ pub const Tile = struct {
378 /// was the one attach failure it did not exit on. Null for tiles that 279 /// was the one attach failure it did not exit on. Null for tiles that
379 /// no tile created. 280 /// no tile created.
380 born_from: ?usize = null, 281 born_from: ?usize = null,
381 /// Whether a wall outlives this tile's ending. `born_from` cannot say 282 /// Whether a wall outlives this tile's ending. `born_from` cannot say it:
382 /// it: a picker birth onto an EMPTY wall has no anchor to name, and so 283 /// a picker birth onto an EMPTY wall carries the entry tile's null, and
383 /// carries the same null the entry tile does — whose ending really is 284 /// the entry tile's ending really is mux's — a script reads its code.
384 /// mux's, because a script reads `mux TARGET`'s code. Set false here and
385 /// a refused Enter on the empty wall quits to the shell instead of
386 /// putting its sentence on the empty-wall line.
387 keeps_wall: bool = false, 285 keeps_wall: bool = false,
388 /// Whether a link that dies before this tile ever saw state is worth 286 /// Whether a link that died before this tile saw state is worth retrying.
389 /// retrying. 287 /// False for the ENTRY tile — a bad host or a typo'd command helps nobody
390 /// 288 /// to retry. True for wall tiles: a wall is a thing you leave up.
391 /// False for the ENTRY tile, which keeps the plain client's rule
392 /// verbatim: a transport that died before any session carried none, so
393 /// it is a bad host, a typo'd command or a `mux d proxy` that exited,
394 /// and retrying any of those helps nobody (`client.lostMsg` is the
395 /// sentence). True for wall tiles, which retry a dead host forever on
396 /// purpose — a wall is a thing you leave up.
397 retry_cold: bool = true, 289 retry_cold: bool = true,
398 /// Why this tile's pump stopped, and with what code. Written BEFORE 290 /// Why this tile's pump stopped, and with what code. Written BEFORE
399 /// `alive` clears, so a keyboard that sees a dead tile can always read 291 /// `alive` clears, so a keyboard that sees a dead tile can always read
@@ -404,13 +296,8 @@ pub const Tile = struct {
404 /// state, so no lock: nothing else reads or writes it. 296 /// state, so no lock: nothing else reads or writes it.
405 end_seen: bool = false, 297 end_seen: bool = false,
406 /// A focus chord's question, posted by the keyboard for this tile's pump 298 /// A focus chord's question, posted by the keyboard for this tile's pump
407 /// to put on the wire. `client.SwitchIntent` as a 299 /// to put on the wire — `client.SwitchIntent` as a u8, taken with a swap
408 /// u8, zero being `.none`; the pump takes it with a swap, so one 300 /// so one keystroke asks one question. The PUMP asks: it owns the link.
409 /// keystroke asks exactly one question.
410 ///
411 /// It is the PUMP that asks because a Transport has one owning thread,
412 /// and the pump that answers because the reply arrives on its link —
413 /// the keyboard only learns the name to focus to.
414 ask: std.atomic.Value(u8) = std.atomic.Value(u8).init(0), 301 ask: std.atomic.Value(u8) = std.atomic.Value(u8).init(0),
415 /// Until when a second `Ctrl-\ x` on this tile FORCES the end. Written 302 /// Until when a second `Ctrl-\ x` on this tile FORCES the end. Written
416 /// by the pump from the daemon's refusal, read by the keyboard on the 303 /// by the pump from the daemon's refusal, read by the keyboard on the
@@ -435,13 +322,9 @@ pub const Tile = struct {
435 /// keystrokes never vanish without the wall admitting to it. 322 /// keystrokes never vanish without the wall admitting to it.
436 in_dropped: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), 323 in_dropped: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
437 324
438 /// The keyboard re-cut the stripes and this tile's rect changed: the 325 /// The keyboard re-cut the stripes: the pump owes the daemon a `.resize`.
439 /// pump owes the daemon a `.resize` at its new content size. The pump 326 /// A plain bool under `paint_mu`, not an atomic — an atomic could be
440 /// is the transport's only writer, so relayout doorbells here and the 327 /// consumed apart from the rect it describes, and that pair is the point.
441 /// pump sends — the same single-writer rule the claim used to keep.
442 // A plain bool under `paint_mu`, not an atomic: an atomic could be
443 // consumed apart from the rect it describes, and that pair is the whole
444 // contract. `takePass` is the only reader.
445 resize_pending: bool = false, 328 resize_pending: bool = false,
446 /// The `repaint_gen` the pump's current pass was taken under; the paint 329 /// The `repaint_gen` the pump's current pass was taken under; the paint
447 /// sink refuses a paint once a relayout has moved past it. Under 330 /// sink refuses a paint once a relayout has moved past it. Under
@@ -467,12 +350,9 @@ pub const Tile = struct {
467 } 350 }
468 }; 351 };
469 352
470 /// The keyboard thread's whole wall, as one value. `liveTiles`/ 353 /// The keyboard thread's whole wall, as one value. `liveTiles`/`livePresent`
471 /// `livePresent` name the prefix walk; a walk of the whole array is a walk 354 /// name the prefix walk; the whole array walks into uninitialised slots.
472 /// into uninitialised slots. 355 /// Keyboard-thread only: a pump gets a `*Tile` and a `*Shared`, never this.
473 ///
474 /// Keyboard-thread only, so `paint_mu`'s ownership of the tree is
475 /// unchanged: a pump is handed a `*Tile` and a `*Shared` and never this.
476 pub const Wall = struct { 356 pub const Wall = struct {
477 alloc: std.mem.Allocator, 357 alloc: std.mem.Allocator,
478 tiles: []Tile, 358 tiles: []Tile,
@@ -493,11 +373,9 @@ pub const Wall = struct {
493 } 373 }
494 }; 374 };
495 375
496 /// Which tile's rectangle a zero-based terminal (row, col) falls in, or 376 /// Which tile's rectangle a zero-based terminal (row, col) falls in, or null
497 /// null when none does. A beside layout gives every tile the same rows, so 377 /// when none does — a click on a rail hits no tile and focus stays put.
498 /// the column is what separates them; a click on a rail column hits no 378 /// Under `paint_mu`, which is what `Tile.rect` is written under.
499 /// tile and focus stays put. Under `paint_mu` because `Tile.rect` is
500 /// written under it.
501 pub fn rectHit(tiles: []Tile, present: []const bool, shared: *Shared, row: u16, col: u16) ?usize { 379 pub fn rectHit(tiles: []Tile, present: []const bool, shared: *Shared, row: u16, col: u16) ?usize {
502 shared.paint_mu.lock(); 380 shared.paint_mu.lock();
503 defer shared.paint_mu.unlock(); 381 defer shared.paint_mu.unlock();
@@ -546,21 +424,13 @@ pub const EndKey = union(enum) {
546 424
547 /// Whether `Ctrl-\ x` has anything to ask, and what. 425 /// Whether `Ctrl-\ x` has anything to ask, and what.
548 pub fn endKey(t: *Tile, now: i64) EndKey { 426 pub fn endKey(t: *Tile, now: i64) EndKey {
549 // A pump that has ended is the only thread that would ever swap `ask` 427 // An ask stored on an ended pump goes nowhere, so the TILE goes instead.
550 // out again, so an ask stored on one goes nowhere for the wall's life: 428 // Nothing is asked of the daemon: the session keeps running and the
551 // the TILE goes instead. Nothing is asked of the daemon — a session it 429 // host's next list births the tile back. A pending pane has no pump.
552 // still holds keeps running, and the host's next list births the tile
553 // back — but a tile whose host no poll can reach (a declined prompt, a
554 // box that is down) was otherwise on the wall for the wall's life with
555 // no key that could remove it.
556 // A pending pane has no pump at all, so it goes the same way — and a
557 // pane closed that turns out to exist births back on the next list.
558 if (!t.alive.load(.acquire)) return .drop; 430 if (!t.alive.load(.acquire)) return .drop;
559 // A pump still on its FIRST dial is alive and parked in `dial`, which 431 // A pump on its FIRST dial is parked in `dial`, which polls `gone` and
560 // polls `gone` and never `ask`: the key would be swallowed for as long 432 // never `ask`: the key would be swallowed until the box came back, then
561 // as the box is down — the picker advertises `unreachable` rows as a 433 // end the session. Birthing onto an `unreachable` row is a designed path.
562 // place to press Enter, so that is a designed path — and would then end
563 // the session the moment the dial landed.
564 if (!t.ever_up.load(.acquire) and awaitingSession(t) != null) return .drop; 434 if (!t.ever_up.load(.acquire) and awaitingSession(t) != null) return .drop;
565 return .{ .ask = intentForEnd(t, now) }; 435 return .{ .ask = intentForEnd(t, now) };
566 } 436 }
@@ -636,10 +506,8 @@ pub fn labelText(
636 } 506 }
637 507
638 /// The label bar: inverse, full width, `N> LABEL [state]`, truncated at the 508 /// The label bar: inverse, full width, `N> LABEL [state]`, truncated at the
639 /// terminal edge. Caller holds `paint_mu` — the marker and the state are 509 /// terminal edge. Caller holds `paint_mu`. Only called when `label_rows` is
640 /// shared state, and this is also what writes the bytes out. Only called 510 /// nonzero — a one-tile wall owns every row and has no bar.
641 /// when `label_rows` is nonzero: a one-tile wall owns every row and has no
642 /// bar to draw.
643 pub fn paintLabelLocked(t: *Tile) void { 511 pub fn paintLabelLocked(t: *Tile) void {
644 if (popupOpen(t.shared)) return; 512 if (popupOpen(t.shared)) return;
645 // ASCII, not an arrow glyph: this bar is byte-truncated, greppable in 513 // ASCII, not an arrow glyph: this bar is byte-truncated, greppable in
@@ -677,15 +545,9 @@ pub fn paintLabelLocked(t: *Tile) void {
677 } 545 }
678 546
679 // The bars of the tiles whose pump has ended. Caller holds `paint_mu`. 547 // The bars of the tiles whose pump has ended. Caller holds `paint_mu`.
680 // A dead pump answers no doorbell and no `repaint_gen`, so the keyboard is 548 // A dead pump answers no doorbell, so the keyboard is the only thread that
681 // the only thread left that can move such a tile's focus marker: without 549 // can move its focus marker. `gone` rather than `present`, because a focus
682 // this, two refused tiles make `Ctrl-\ 1` and `Ctrl-\ 2` change nothing on 550 // move has no `present` slice and a vanished tile's rect is a neighbour's.
683 // screen, and a live tile that gained the focus shows a second `>` beside
684 // a stale one.
685 //
686 // `gone` rather than `present` because a focus move has no `present` slice
687 // to consult; `vanishTile` stores the two together, and a vanished tile
688 // must not paint — its rect belongs to a neighbour now.
689 pub fn paintDeadBarsLocked(tiles: []Tile) void { 551 pub fn paintDeadBarsLocked(tiles: []Tile) void {
690 for (tiles) |*t| { 552 for (tiles) |*t| {
691 if (!t.alive.load(.acquire) and !t.gone.load(.acquire)) paintLabelLocked(t); 553 if (!t.alive.load(.acquire) and !t.gone.load(.acquire)) paintLabelLocked(t);
@@ -728,12 +590,9 @@ fn ringLive(t: *const Tile) bool {
728 return true; 590 return true;
729 } 591 }
730 592
731 // A tile's wake pipe is never closed, not even when its pump ends: the fd 593 // A tile's wake pipe is never closed: the fd NUMBER would be recycled, and a
732 // NUMBER would be recycled, and the pollers open a socket a second, so a 594 // late bell would put a stray byte into somebody's live connection instead of
733 // late bell — `setFocus` rings every tile, present or not — would put a 595 // getting EBADF. Two fds per tile — the process exit is the cheaper owner.
734 // stray byte into somebody's live connection instead of getting EBADF.
735 // Two fds per tile, CLOEXEC, at most `max_tiles` tiles: the process exit
736 // is a cheaper owner than the alternative is to get right.
737 596
738 /// `ring` in the other direction: never blocks, never reports. 597 /// `ring` in the other direction: never blocks, never reports.
739 pub fn ringKeyboard(shared: *const Shared) void { 598 pub fn ringKeyboard(shared: *const Shared) void {
@@ -786,14 +645,9 @@ pub fn takeNotice(shared: *Shared, out: []u8) []const u8 {
786 return takeNoticeLocked(shared, out); 645 return takeNoticeLocked(shared, out);
787 } 646 }
788 647
789 /// Called ONLY from the keyboard loop and ONLY while `t` is the focused tile: 648 /// Called ONLY from the keyboard loop and ONLY while `t` is focused: that
790 /// that restriction IS the enforcement of "an unfocused tile claims no terminal modes". 649 /// restriction IS "an unfocused tile claims no terminal modes". A chunk that
791 /// 650 /// does not fit is dropped WHOLE — a partial copy is a command nobody typed.
792 /// Mouse reports travel it unsplit — the split needs the pump's state — and
793 /// an unfocused tile's never arrive, since this is the focused tile only.
794 ///
795 /// A chunk that does not fit is dropped WHOLE: copying what fits splices two
796 /// reads into a command nobody typed; blocking wedges the wall.
797 pub fn sendKeys(t: *Tile, keys: []const u8) void { 651 pub fn sendKeys(t: *Tile, keys: []const u8) void {
798 { 652 {
799 t.in_mu.lock(); 653 t.in_mu.lock();
@@ -812,28 +666,16 @@ pub fn sendKeys(t: *Tile, keys: []const u8) void {
812 ring(t); 666 ring(t);
813 } 667 }
814 668
815 /// Move the focus to tile `next`. Client-local: decides which pump owns the 669 /// Move the focus to tile `next`. Client-local: it decides which pump owns
816 /// terminal's modes, not which session the daemon hears. The outgoing tile's 670 /// the terminal's modes, not which session the daemon hears. The outgoing
817 /// `session_release` is written HERE, under `paint_mu`, so the handover is 671 /// tile's `session_release` is written HERE under `paint_mu`, because two
818 /// ordered — two pumps are two threads, and A's release and B's claim race 672 /// pumps are two threads and A's release races B's claim.
819 /// (measured on a real pty: A's release landed after B's claim in five of
820 /// six). Written before the doorbell; the released pump writes nothing
821 /// (`.already_written`).
822 ///
823 /// Legacy mouse reporting (1000 without 1006) spells a report `ESC [ M`
824 /// plus ordinary characters — column 81 is `q` (quits), column 88 is `x`
825 /// (forgets the focused tile). So the outgoing session's modes come off
826 /// here, ahead of the focus moving. The claim and release are doorbells;
827 /// no screen clear — every tile paints its own rect.
828 pub fn setFocus(tiles: []Tile, shared: *Shared, next: usize) void { 673 pub fn setFocus(tiles: []Tile, shared: *Shared, next: usize) void {
829 const prev = shared.sel; 674 const prev = shared.sel;
830 shared.paint_mu.lock(); 675 shared.paint_mu.lock();
831 defer shared.paint_mu.unlock(); 676 defer shared.paint_mu.unlock();
832 // The outgoing session's modes come off HERE, on the thread moving the 677 // The outgoing session's modes come off HERE, ahead of the doorbell that
833 // focus, ahead of the doorbell that lets the next pump claim. 678 // lets the next pump claim — and only when there IS a terminal to change.
834 // ...and only when there IS a terminal. A scripted `mux` on a pipe is a
835 // wall of one tile whose chords still work; what it must not do is
836 // write a mode change into a capture the plain client left clean.
837 if (shared.is_tty and prev != next and prev < tiles.len and tiles[prev].alive.load(.acquire)) { 679 if (shared.is_tty and prev != next and prev < tiles.len and tiles[prev].alive.load(.acquire)) {
838 proto.writeAllFd(shared.out_fd, interact.session_release) catch {}; 680 proto.writeAllFd(shared.out_fd, interact.session_release) catch {};
839 tiles[prev].release_pending.store(true, .release); 681 tiles[prev].release_pending.store(true, .release);
@@ -895,11 +737,9 @@ pub fn paintEmptyWallLocked(shared: *Shared) void {
895 proto.writeAllFd(shared.out_fd, fbs.getWritten()) catch {}; 737 proto.writeAllFd(shared.out_fd, fbs.getWritten()) catch {};
896 } 738 }
897 739
898 /// A tile leaves the wall: off the `present` roll, off the tree, and its 740 /// A tile leaves the wall: off the `present` roll, off the tree, and its pump
899 /// pump told to return. The ONE owner — the chord and the poll's diff both 741 /// told to return. The ONE owner — chord and poll diff both come here, so the
900 /// come here, which is why the focus hand-off can only be written once. 742 /// focus hand-off is written once. `to` overrides where the focus goes.
901 /// `to` overrides where a focused tile's focus goes; null takes the next
902 /// present tile. The caller re-cuts.
903 pub fn vanishTile(tiles: []Tile, present: []bool, shared: *Shared, i: usize, to: ?usize) void { 743 pub fn vanishTile(tiles: []Tile, present: []bool, shared: *Shared, i: usize, to: ?usize) void {
904 present[i] = false; 744 present[i] = false;
905 tiles[i].gone.store(true, .release); 745 tiles[i].gone.store(true, .release);
@@ -936,19 +776,14 @@ pub fn showsSelf(
936 // exported variable it cannot unset, and the refusal names unsetting as 776 // exported variable it cannot unset, and the refusal names unsetting as
937 // the way out — both spellings of that have to work. 777 // the way out — both spellings of that have to work.
938 if (es.len == 0 or en.len == 0) return false; 778 if (es.len == 0 or en.len == 0) return false;
939 // Both halves, so session 0 attaching to session 1 of the same daemon 779 // Both halves, so session 0 attaching to session 1 of one daemon works.
940 // keeps working. String equality on the path: a symlinked or relatively 780 // String equality: this guards a mistake, not a security boundary.
941 // spelled `--sock` for the same socket evades this, accepted, because
942 // this guards the mistake people make (typing `mux` in a mux shell) and
943 // is not a security boundary.
944 return std.mem.eql(u8, es, sock) and std.mem.eql(u8, en, proto.resolveName(name)); 781 return std.mem.eql(u8, es, sock) and std.mem.eql(u8, en, proto.resolveName(name));
945 } 782 }
946 783
947 /// Two tiles on the same daemon, as SPELLED. Identity dedup would need an 784 /// Two tiles on the same daemon, as SPELLED. Identity dedup would need an
948 /// endpoint handshake the spec deliberately refuses, so the same session 785 /// endpoint handshake the spec refuses, so one session reached two ways is
949 /// reached as `HOST#S` and as `quic://…#S` is two tiles — and the ring 786 /// two tiles; the ring chords only ask their own daemon and never care.
950 /// chords, which only ever ask the daemon their own tile is already talking
951 /// to, never have to care.
952 fn sameTarget(a: client.Target, b: client.Target) bool { 787 fn sameTarget(a: client.Target, b: client.Target) bool {
953 if (std.meta.activeTag(a) != std.meta.activeTag(b)) return false; 788 if (std.meta.activeTag(a) != std.meta.activeTag(b)) return false;
954 return switch (a) { 789 return switch (a) {
@@ -986,13 +821,9 @@ const Doorbell = enum { fresh, kept };
986 /// tiles are born in two places now — at startup, and whenever a focus chord 821 /// tiles are born in two places now — at startup, and whenever a focus chord
987 /// or the saved wall adds one. 822 /// or the saved wall adds one.
988 fn initTile(t: *Tile, r: Resolved, s: layout.Rect, shared: *Shared, idx: usize, bell: Doorbell) !void { 823 fn initTile(t: *Tile, r: Resolved, s: layout.Rect, shared: *Shared, idx: usize, bell: Doorbell) !void {
989 // The doorbell, before the pump that polls it exists. Non-blocking at 824 // The doorbell, before the pump that polls it exists. Non-blocking both
990 // both ends: the writer must never wedge the keyboard and the reader 825 // ends: neither side may wedge the other. A kept pipe may still hold the
991 // must never wedge the pump. 826 // byte that rang the departing pump; the bytes carry nothing.
992 //
993 // A kept pipe may still hold the byte `vanishTile` rang the departing
994 // pump with; the bytes carry nothing, so the new pump's first poll
995 // wakes once for nobody and reads its empty mailbox.
996 const wake: [2]std.posix.fd_t = switch (bell) { 827 const wake: [2]std.posix.fd_t = switch (bell) {
997 .fresh => try std.posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }), 828 .fresh => try std.posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }),
998 .kept => .{ t.wake_r, t.wake_w }, 829 .kept => .{ t.wake_r, t.wake_w },
@@ -1025,15 +856,9 @@ pub fn spawnPump(t: *Tile) void {
1025 // never dials, so the sidecar cannot re-create a session. 856 // never dials, so the sidecar cannot re-create a session.
1026 if (t.pending) return; 857 if (t.pending) return;
1027 const th = std.Thread.spawn(.{}, wall_pump.pumpTile, .{t}) catch { 858 const th = std.Thread.spawn(.{}, wall_pump.pumpTile, .{t}) catch {
1028 // A tile with no thread is a tile nothing will ever paint — the 859 // A tile with no thread is one nothing will ever paint. Marked here
1029 // same hole `pumpTile`'s exit closes, reached without the pump 860 // so the keyboard paints its bar; `.connecting` would be a lie. The
1030 // having run at all. Marked here so the keyboard paints its bar and 861 // entry tile's link is already open and nobody is left to adopt it.
1031 // narrates a focus into it. `.connecting` would be a lie: nobody is
1032 // going to.
1033 //
1034 // The entry tile's link is already OPEN, and nobody is left to
1035 // adopt it: closed here, or the fd and the daemon slot it holds
1036 // outlive the tile by the whole run.
1037 if (t.pre) |*pre| { 862 if (t.pre) |*pre| {
1038 pre.close(); 863 pre.close();
1039 t.pre = null; 864 t.pre = null;
@@ -1087,14 +912,8 @@ const Birth = struct {
1087 // next poll of that host births a second tile for the same session. 912 // next poll of that host births a second tile for the same session.
1088 host: ?usize = null, 913 host: ?usize = null,
1089 // Whether `r.session` is borrowed and the tile's own copies are owed. 914 // Whether `r.session` is borrowed and the tile's own copies are owed.
1090 // The poll finds a name in the buffer it is about to reuse and must not
1091 // pay for a copy — nor leave one behind — when the wall refuses the
1092 // tile; `r.label` is rebuilt here rather than passed.
1093 //
1094 // FALSE is an ownership claim, not an optimisation: the tile takes 915 // FALSE is an ownership claim, not an optimisation: the tile takes
1095 // `label` and `session` verbatim, and whichever birth takes its digit 916 // `label` and `session` verbatim and a later birth frees both.
1096 // back frees both with `alloc`. Hand it a literal and the abort is that
1097 // later birth, arbitrarily far from the caller that made the mistake.
1098 borrowed: bool = false, 917 borrowed: bool = false,
1099 }; 918 };
1100 919
@@ -1157,12 +976,9 @@ fn birthTileOrRefuse(w: Wall, b: Birth) !usize {
1157 ); 976 );
1158 defer flat.deinit(w.alloc); 977 defer flat.deinit(w.alloc);
1159 const new_rect = flat.rectOf(@intCast(at)) orelse return error.NoRect; 978 const new_rect = flat.rectOf(@intCast(at)) orelse return error.NoRect;
1160 // The real rect, not a placeholder: a creating tile puts its rect on 979 // The real rect, not a placeholder: the daemon refuses creates under
1161 // the first attach frame and the daemon refuses creates under 980 // `min_session_rows`, and a 2-row rect on a live session is destructive
1162 // min_session_rows; a joining tile doorbells a resize on its first 981 // under latest-wins. `label_rows` too, so `viewRows` is right at attach.
1163 // pass, and a 2-row rect on a live session is destructive under
1164 // latest-wins. `label_rows` is set here so `viewRows` is right from
1165 // the first attach; relayout re-flattens every rect and sets it again.
1166 w.shared.label_rows = if (new_live > 1) 1 else 0; 982 w.shared.label_rows = if (new_live > 1) 1 else 0;
1167 // Past every refusal: the tile is the wall's now, so the copies a pump 983 // Past every refusal: the tile is the wall's now, so the copies a pump
1168 // will hold for its whole life are worth making. Made BEFORE the slot 984 // will hold for its whole life are worth making. Made BEFORE the slot
@@ -1223,13 +1039,9 @@ fn addSessionTile(w: Wall, from: usize, name: []const u8, place: Place) FocusTo
1223 if (std.mem.eql(u8, proto.resolveName(t.r.session), want)) return .{ .moved = i }; 1039 if (std.mem.eql(u8, proto.resolveName(t.r.session), want)) return .{ .moved = i };
1224 } 1040 }
1225 const at = birthTile(w, .{ 1041 const at = birthTile(w, .{
1226 // The offer is inherited from the tile this one grew out of. Same 1042 // The offer is inherited: same target, so `-A` exposes nothing new,
1227 // target, so `-A` exposes nothing the user has not already exposed 1043 // and a chord-made tile has no command line to spell the flag on.
1228 // to that host — and a chord-made tile has no command line to spell 1044 // `want` borrows the caller's name — see `Birth.borrowed`.
1229 // the flag on, so not inheriting would silently end the forwarding
1230 // at the first session switch.
1231 // `want` borrows the caller's name: the wall makes its own copies
1232 // only if it keeps the tile — see `Birth.borrowed`.
1233 .r = .{ .target = target, .label = "", .session = want, .agent = w.tiles[from].r.agent }, 1045 .r = .{ .target = target, .label = "", .session = want, .agent = w.tiles[from].r.agent },
1234 .from = from, 1046 .from = from,
1235 .place = place, 1047 .place = place,
@@ -1271,20 +1083,12 @@ pub fn closeNotice(tiles: []Tile, present: []const bool, shared: *Shared) void {
1271 if (pending) showRefusal(tiles, shared, z) else _ = ringLive(&tiles[z]); 1083 if (pending) showRefusal(tiles, shared, z) else _ = ringLive(&tiles[z]);
1272 } 1084 }
1273 1085
1274 /// How a tile spells itself on the wall: the wall grammar's own form, which 1086 /// How a tile spells itself on the wall — the form that keys the layout
1275 /// is what keys the layout sidecar's leaves. 1087 /// sidecar's leaves. `--via` has no form in that grammar, so it gets a label
1276 /// 1088 /// that is honest on a bar and is not a spelling.
1277 /// `--via` has no form in that grammar — an arbitrary command is not an
1278 /// address — so it gets a label that is honest on a bar and is not a
1279 /// spelling: `recordHost` refuses it for the same reason. Naming it anyway
1280 /// is better than a blank bar over a session that is really there.
1281 pub fn tileLabel(alloc: std.mem.Allocator, target: client.Target, name: []const u8) ![]const u8 { 1089 pub fn tileLabel(alloc: std.mem.Allocator, target: client.Target, name: []const u8) ![]const u8 {
1282 // Sized for the form it will actually take. `spellingCap` bounds the 1090 // `spellingCap` counts a `--via` operand as ZERO — the grammar has no
1283 // wall grammar's three forms and deliberately counts a `--via` 1091 // place to put one. A label does, and the command has no length bound.
1284 // command's operand as ZERO, because that grammar has no place to put
1285 // one; a label does, and the command can be any length at all (the
1286 // suite's is a cache path plus a socket path, which overran a cap built
1287 // from the grammar's).
1288 const cap = switch (target) { 1092 const cap = switch (target) {
1289 .via => |cmd| "--via ".len + cmd.len + 1 + proto.session_name_max, 1093 .via => |cmd| "--via ".len + cmd.len + 1 + proto.session_name_max,
1290 else => client.spellingCap(target), 1094 else => client.spellingCap(target),
@@ -1338,14 +1142,9 @@ pub fn endAction(
1338 ) EndAction { 1142 ) EndAction {
1339 const t = &tiles[ended]; 1143 const t = &tiles[ended];
1340 const reason: EndReason = @enumFromInt(t.end.load(.acquire)); 1144 const reason: EndReason = @enumFromInt(t.end.load(.acquire));
1341 // A chord-born tile that never got its session leaves no trace: the tile 1145 // A chord-born tile that never got its session leaves no trace. The
1342 // goes back where the chord was typed, with the sentence a script reads 1146 // sentence names the state: a daemon that answered and said no may free a
1343 // on stderr and a survivor the screen shows. The sentence names the 1147 // slot, while one that never answered has to be started.
1344 // state, because the two states send the user to different places: a
1345 // daemon that answered and said no may free a slot, while one that
1346 // never answered has to be started. `x` on the last local session ends
1347 // the local daemon, so a wall reaching for a new session on its own
1348 // machine is exactly the second case.
1349 const gone_msg: ?[]const u8 = switch (reason) { 1148 const gone_msg: ?[]const u8 = switch (reason) {
1350 // Whatever the tile had been: the user REFUSED this dial, and a 1149 // Whatever the tile had been: the user REFUSED this dial, and a
1351 // wall that retried anyway would ask the same question every two 1150 // wall that retried anyway would ask the same question every two
@@ -1364,16 +1163,10 @@ pub fn endAction(
1364 return .{ .vanish = .{ .back = back, .msg = msg } }; 1163 return .{ .vanish = .{ .back = back, .msg = msg } };
1365 } 1164 }
1366 } 1165 }
1367 // A clean exit is noise once it is over: the tile leaves and the wall 1166 // A clean exit is noise once over: the tile leaves and the wall re-cuts,
1368 // re-cuts. Only when somebody is left to steer it — a piped client 1167 // but only while somebody is left to steer it. On a TERMINAL the LAST
1369 // whose stdin has gone has no keyboard, so its ending is mux's own. 1168 // tile goes the same way and the wall stays up empty; a piped `mux` is a
1370 // The back-target prefers a live pump: a dead tile's mailbox drains 1169 // wall of one and still exits with the shell's code, which scripts read.
1371 // nowhere, and a chord that needs a pump goes silent.
1372 //
1373 // On a TERMINAL the LAST tile goes the same way, and the wall stays up
1374 // empty with the picker over it: `x` ends a session, not the program,
1375 // and the machines are still there to birth on. A piped `mux` is a
1376 // wall of one and still exits with the shell's code — scripts read it.
1377 if (reason == .exited and stdin_open and (is_tty or presentCount(present[0..live]) > 1)) { 1170 if (reason == .exited and stdin_open and (is_tty or presentCount(present[0..live]) > 1)) {
1378 // Null rather than the tile that just went: `stepLive` falls back 1171 // Null rather than the tile that just went: `stepLive` falls back
1379 // to the selection itself when there is nothing else present, and 1172 // to the selection itself when there is nothing else present, and
@@ -1417,21 +1210,16 @@ pub fn endAction(
1417 .declined => .{ .code = 1, .msg = "mux: prompt declined" }, 1210 .declined => .{ .code = 1, .msg = "mux: prompt declined" },
1418 }, 1211 },
1419 }; 1212 };
1420 // A session the PICKER started on a wall that is still there ends as a 1213 // A session the PICKER started ends as a session ends: the sentence goes
1421 // session ends: the sentence goes on the empty-wall line and the 1214 // on the empty-wall line. Off a terminal there is no wall to leave up.
1422 // machines are still there to birth on again. Off a terminal there is
1423 // no wall to leave standing and the code wins, and a closed stdin ends
1424 // mux whatever the tile was.
1425 if (t.keeps_wall and is_tty and stdin_open) 1215 if (t.keeps_wall and is_tty and stdin_open)
1426 return .{ .vanish = .{ .back = null, .msg = gone_msg orelse done.finish.msg } }; 1216 return .{ .vanish = .{ .back = null, .msg = gone_msg orelse done.finish.msg } };
1427 return done; 1217 return done;
1428 } 1218 }
1429 1219
1430 /// Newly dead tiles, drained one per pass. Returns the first newly-dead 1220 /// Newly dead tiles, drained one per pass. Returns the first that is focused
1431 /// tile that is focused or whose reason is `.exited` — both need the 1221 /// or `.exited` — both need the keyboard to vanish them; the rest narrated on
1432 /// keyboard to act (vanish); the rest narrated on their own bars and need 1222 /// their own bars and are marked seen here.
1433 /// nothing, so they are marked seen here. The loop calls this every pass,
1434 /// so a batch of exits drains across passes.
1435 pub fn endedTile(tiles: []Tile, present: []const bool, shared: *Shared) ?usize { 1223 pub fn endedTile(tiles: []Tile, present: []const bool, shared: *Shared) ?usize {
1436 const z = shared.sel; 1224 const z = shared.sel;
1437 var hit: ?usize = null; 1225 var hit: ?usize = null;
@@ -1527,16 +1315,10 @@ pub fn runAttach(
1527 std.debug.print("{s}", .{f.msg}); 1315 std.debug.print("{s}", .{f.msg});
1528 return f.exit; 1316 return f.exit;
1529 }; 1317 };
1530 // The wall this attach lands on is the WHOLE wall: the host just 1318 // `mux HOST` is the whole wall zoomed on HOST, not a wall of one: the
1531 // dialled first, so it is tile 0 and the sidecar's anchor, and every 1319 // dialled host is tile 0 and the sidecar's anchor, every other listed
1532 // other listed host after it. `mux HOST` is the wall zoomed on HOST, 1320 // host after it. Unless there is no terminal — then the specs stop at the
1533 // not a wall of one. 1321 // entry host, so a piped `mux` writes what the plain client wrote.
1534 //
1535 // ...unless there is no terminal, which is what makes the promise
1536 // above true: `headless` is the same question `run` asks, asked here
1537 // because `otherHosts` runs before `run` exists. On a pipe the specs
1538 // stop at the entry host and `run` starts no poller for it, so this
1539 // process writes exactly what the plain client wrote.
1540 var arena_state = std.heap.ArenaAllocator.init(alloc); 1322 var arena_state = std.heap.ArenaAllocator.init(alloc);
1541 defer arena_state.deinit(); 1323 defer arena_state.deinit();
1542 const arena = arena_state.allocator(); 1324 const arena = arena_state.allocator();
@@ -1565,11 +1347,8 @@ pub fn runAttach(
1565 wall_host.otherHosts(arena, &specs, spelling, path, key, idle_ms); 1347 wall_host.otherHosts(arena, &specs, spelling, path, key, idle_ms);
1566 } 1348 }
1567 // The narration was owed to a person at a BARE prompt, and the wall is 1349 // The narration was owed to a person at a BARE prompt, and the wall is
1568 // about to take the screen. Past here fd 2 is the alternate screen, so 1350 // about to take the screen: past here a late line from ssh would land on
1569 // a late line from this ssh — `Connection to box closed by remote 1351 // the alternate screen. A piped `mux` goes on relaying to its script.
1570 // host.` at the link's death — would be the foreign writer this whole
1571 // rule removes. A piped `mux` goes on relaying: a script reads stderr
1572 // and has no paint to corrupt.
1573 if (!headless(std.posix.STDOUT_FILENO)) transport.narrate = false; 1352 if (!headless(std.posix.STDOUT_FILENO)) transport.narrate = false;
1574 return run(alloc, specs.items, .{ 1353 return run(alloc, specs.items, .{
1575 .focus0 = true, 1354 .focus0 = true,
@@ -1618,10 +1397,8 @@ pub const Entry = struct {
1618 key: ?[]const u8 = null, 1397 key: ?[]const u8 = null,
1619 idle_ms: u32 = client.quic_idle_ms_default, 1398 idle_ms: u32 = client.quic_idle_ms_default,
1620 /// Whether this wall insists on a terminal. A wall of many does — there 1399 /// Whether this wall insists on a terminal. A wall of many does — there
1621 /// is nothing to cut stripes from without one, and it says so. A 1400 /// is nothing to cut stripes from without one. A scripted `mux TARGET` is
1622 /// scripted `mux TARGET` on a pipe does not: it is a wall of one tile 1401 /// a wall of one whose rect is the whole terminal, so it does not.
1623 /// whose rect is the whole terminal, and it writes exactly what the
1624 /// plain client wrote.
1625 needs_tty: bool = true, 1402 needs_tty: bool = true,
1626 }; 1403 };
1627 1404
@@ -1638,13 +1415,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1638 // The plain client's fallback, byte for byte: a piped `mux` painted an 1415 // The plain client's fallback, byte for byte: a piped `mux` painted an
1639 // 80x24 grid and this one has to paint the same one. 1416 // 80x24 grid and this one has to paint the same one.
1640 const size = measured orelse proto.Size{ .cols = 80, .rows = 24 }; 1417 const size = measured orelse proto.Size{ .cols = 80, .rows = 24 };
1641 // Whether there is a TERMINAL is a different question from whether it 1418 // Whether there is a TERMINAL and whether it could be MEASURED are two
1642 // could be MEASURED, and the two are answered apart: `is_tty` off 1419 // questions, answered apart: a 1x1 pty is a real terminal reporting a
1643 // stdin, the size off an ioctl (`interact.ttySize`) that gives up below 1420 // size nothing can paint at.
1644 // two columns or rows. A 1x1 pty is a real terminal that reports a size
1645 // nothing can paint at, and the plain client took it over and painted
1646 // 80x24 into it. Conflating the two put that client on no alternate
1647 // screen at all.
1648 const is_tty = std.posix.isatty(stdin_fd); 1421 const is_tty = std.posix.isatty(stdin_fd);
1649 1422
1650 var shared = Shared{ .out_fd = stdout_fd, .size = size, .is_tty = is_tty }; 1423 var shared = Shared{ .out_fd = stdout_fd, .size = size, .is_tty = is_tty };
@@ -1709,39 +1482,25 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1709 var raw = o; 1482 var raw = o;
1710 raw.lflag.ICANON = false; 1483 raw.lflag.ICANON = false;
1711 raw.lflag.ECHO = false; 1484 raw.lflag.ECHO = false;
1712 // ISIG and IXON off because those keys belong to the SESSION, not 1485 // ISIG and IXON off because those keys belong to the SESSION: Ctrl-C
1713 // to mux: Ctrl-C has to reach the remote shell as a byte, and 1486 // must reach the remote shell as a byte. It is also why `Ctrl-\` is
1714 // Ctrl-S must not freeze the local terminal out from under a 1487 // the only way out — nothing else here raises a signal.
1715 // session that wanted it. It is also why `Ctrl-\` is the only way
1716 // out — nothing else here raises a signal any more.
1717 raw.lflag.ISIG = false; 1488 raw.lflag.ISIG = false;
1718 raw.iflag.IXON = false; 1489 raw.iflag.IXON = false;
1719 raw.iflag.ICRNL = false; 1490 raw.iflag.ICRNL = false;
1720 try std.posix.tcsetattr(stdin_fd, .FLUSH, raw); 1491 try std.posix.tcsetattr(stdin_fd, .FLUSH, raw);
1721 // Alternate screen, cursor hidden while no tile has painted, 1492 // Alternate screen, cursor hidden, autowrap off, title pushed — from
1722 // autowrap off (stripe clipping is the terminal's right edge), 1493 // interact's own constant, because the exit must undo what a focused
1723 // title pushed. Written from interact's own constant, because the 1494 // TILE added on top and only that module knows the whole list.
1724 // exit has to undo what a focused TILE can have added on top of it
1725 // and only that module knows the whole list. The wall no longer
1726 // claims the mouse itself — the focused tile's `claimTerminal`
1727 // does, so a click reaches the session that asked for it and a
1728 // click in another tile's rect moves the focus there.
1729 proto.writeAllFd(stdout_fd, interact.wall_setup) catch {}; 1495 proto.writeAllFd(stdout_fd, interact.wall_setup) catch {};
1730 // Armed by the wall and not by any Core: the wall owns this 1496 // Armed by the wall and not by any Core: the wall owns this
1731 // terminal's raw mode. The KEYBOARD thread reads the process-wide 1497 // terminal's raw mode. The KEYBOARD thread reads the process-wide
1732 // winch flag and relayouts; no pump consumes it. 1498 // winch flag and relayouts; no pump consumes it.
1733 interact.watchWinch(); 1499 interact.watchWinch();
1734 } 1500 }
1735 // From here to the key loop every step can fail — a tile array, a 1501 // From here to the key loop every `try` unwinds past the restore below,
1736 // doorbell pipe per tile, the `present` array — and each of them is a 1502 // leaving the user on an alternate screen in raw mode with no prompt.
1737 // `try` that unwinds straight past the restore at the bottom. That 1503 // Only that window: every ordinary exit is the teardown at the bottom.
1738 // leaves the user on an alternate screen, in raw mode, with a title
1739 // pushed and a wall that never appeared: no cursor, no prompt, and
1740 // nothing on screen to say why.
1741 //
1742 // It fires only on that window. Every ordinary way out of this function
1743 // is the explicit teardown below, because the function ends in
1744 // `std.posix.exit` rather than a return.
1745 errdefer if (orig) |o| { 1504 errdefer if (orig) |o| {
1746 proto.writeAllFd(stdout_fd, interact.wall_teardown) catch {}; 1505 proto.writeAllFd(stdout_fd, interact.wall_teardown) catch {};
1747 std.posix.tcsetattr(stdin_fd, .FLUSH, o) catch {}; 1506 std.posix.tcsetattr(stdin_fd, .FLUSH, o) catch {};
@@ -1756,12 +1515,8 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1756 shared.sel = 0; 1515 shared.sel = 0;
1757 1516
1758 // ssh's prompts get somewhere to go, on a TERMINAL only: without one 1517 // ssh's prompts get somewhere to go, on a TERMINAL only: without one
1759 // there is no popup to answer with, so ssh keeps its own prompts and 1518 // there is no popup, so ssh keeps its prompts and fails them visibly.
1760 // fails them onto the row with its reason — half one's path, and a 1519 // `$XDG_RUNTIME_DIR` and no fallback: a shared /tmp socket would be a
1761 // visible one rather than a wait nobody can see.
1762 //
1763 // `$XDG_RUNTIME_DIR` and no fallback, for `sockpath`'s reason turned up
1764 // one notch: a guess lands in a shared /tmp, and a socket there is a
1765 // password prompt any local user could answer. 1520 // password prompt any local user could answer.
1766 var ask_exe_buf: [std.fs.max_path_bytes]u8 = undefined; 1521 var ask_exe_buf: [std.fs.max_path_bytes]u8 = undefined;
1767 if (is_tty) { 1522 if (is_tty) {
@@ -1863,13 +1618,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1863 var over_buf: [64]u8 = undefined; 1618 var over_buf: [64]u8 = undefined;
1864 if (wall_host.hostsOverCapacity(&over_buf, host_specs.len)) |said| setNotice(&shared, said); 1619 if (wall_host.hostsOverCapacity(&over_buf, host_specs.len)) |said| setNotice(&shared, said);
1865 for (host_table[0..hosts_live]) |*h| { 1620 for (host_table[0..hosts_live]) |*h| {
1866 // Not without a terminal (`headless`). A poller here would turn 1621 // Not without a terminal: a poller here would turn every scripted
1867 // every scripted `mux --sock S` into a wall of whatever that daemon 1622 // `mux --sock S` into a wall of whatever that daemon is running.
1868 // happens to be running: measured on a piped attach to a daemon 1623 // Nothing to wait for when nothing was started.
1869 // holding four sessions, four attaches and a capture that opens
1870 // with label bars.
1871 // Nothing to wait for when nothing was started: `spawnPump`'s
1872 // catch marks `pump_done` for the same reason.
1873 if (measured == null) { 1624 if (measured == null) {
1874 h.poller_done.store(true, .release); 1625 h.poller_done.store(true, .release);
1875 continue; 1626 continue;
@@ -1887,12 +1638,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1887 const hosts_path = hosts.statePath(alloc) catch null; 1638 const hosts_path = hosts.statePath(alloc) catch null;
1888 defer if (hosts_path) |p| alloc.free(p); 1639 defer if (hosts_path) |p| alloc.free(p);
1889 1640
1890 // The wall is always "in": every non-chord byte goes to the focused 1641 // The wall is always "in": every non-chord byte goes to the focused tile.
1891 // tile, whose own Core splits mouse reports and owns the drag. The 1642 // The keyboard holds back only the `Ctrl-\` prefix and hit-tests presses
1892 // keyboard holds back only the `Ctrl-\` prefix, and a local MouseFilter 1643 // for focus; the report bytes still go through, so the drag's Core sees them.
1893 // that parses the terminal's SGR reports to hit-test a press for
1894 // focus — the report bytes themselves go through to the focused tile
1895 // unchanged, so the Core that owns the drag sees them.
1896 var input: WallInput = .{}; 1644 var input: WallInput = .{};
1897 // Which HOST the picker's selection rests on, kept across opens so a 1645 // Which HOST the picker's selection rests on, kept across opens so a
1898 // popup reopened by reflex is where it was left. Keyboard-thread 1646 // popup reopened by reflex is where it was left. Keyboard-thread
@@ -2001,11 +1749,8 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2001 }, 1749 },
2002 .vanish => |v| { 1750 .vanish => |v| {
2003 if (v.msg) |msg| { 1751 if (v.msg) |msg| {
2004 // The notice is the banner on the surviving tile's 1752 // Banner on the surviving tile's rect; stderr deferred
2005 // rect; stderr is deferred to after raw-mode restore 1753 // past the raw-mode restore. Both say the same words.
2006 // so it does not land on the alternate screen. Both
2007 // say the same words — a screen that names a cause
2008 // the exit line contradicts is worse than either.
2009 var nb: [128]u8 = undefined; 1754 var nb: [128]u8 = undefined;
2010 setNotice(&shared, noticeText(&nb, msg)); 1755 setNotice(&shared, noticeText(&nb, msg));
2011 vanish_msg = msg; 1756 vanish_msg = msg;
@@ -2029,12 +1774,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2029 }, 1774 },
2030 } 1775 }
2031 } 1776 }
2032 // A box whose ssh has GONE, before a box that is arriving: the 1777 // A box whose ssh has GONE, before one that is arriving: the helper is
2033 // FIDO notifier's helper is SIGTERMed the moment the touch lands, 1778 // SIGTERMed when the touch lands, so the prompt on screen is a question
2034 // and what is on screen then is a question nobody is waiting on. 1779 // nobody waits on. Dismissing it by key would decline a live dial.
2035 // Closing it here rather than on a key is the whole point — the
2036 // user should not have to dismiss it, and dismissing it would put
2037 // a live dial's pid in the decline ring.
2038 if (input.prefix.asking) { 1780 if (input.prefix.asking) {
2039 if (shared.prompts) |l| { 1781 if (shared.prompts) |l| {
2040 if (!l.showing()) closeAsk(w, &shared, &input.prefix, picker_sel); 1782 if (!l.showing()) closeAsk(w, &shared, &input.prefix, picker_sel);
@@ -2097,16 +1839,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2097 }, 1839 },
2098 .leave => {}, 1840 .leave => {},
2099 } 1841 }
2100 // On news, not on the tick: the rows have to follow the pollers 1842 // On news, not on the tick: the rows follow the pollers while the
2101 // while the popup is open, and a repaint every 100ms would rewrite 1843 // popup is open, but a repaint every 100ms rewrites an unchanged
2102 // a screen nothing changed on. 1844 // screen. A zeroed stamp is a screen the box is no longer on —
2103 // The bell too: a tile that ended relayouts under the box, and the 1845 // `relayout` cleared it, and nothing else would repaint the popup.
2104 // cleared screen owes the popup a repaint the pollers must not be
2105 // the first to give it.
2106 // A zeroed stamp is a screen the box is no longer on: `relayout`
2107 // clears it and zeroes the stamp, and a second ended tile drained on
2108 // a pass with no bell, no news and no key would otherwise leave the
2109 // popup wiped until the next list.
2110 var foot_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined; 1846 var foot_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined;
2111 const repair = wall_picker.pickerRepaint(&foot_buf, &input.prefix, picker_opened or host_news or 1847 const repair = wall_picker.pickerRepaint(&foot_buf, &input.prefix, picker_opened or host_news or
2112 winch or fds[1].revents != 0 or shared.picker_stamp == 0); 1848 winch or fds[1].revents != 0 or shared.picker_stamp == 0);
@@ -2146,11 +1882,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2146 wall_picker.paintAsk(&shared, ask_prompt.slice(), &input.prefix); 1882 wall_picker.paintAsk(&shared, ask_prompt.slice(), &input.prefix);
2147 continue; 1883 continue;
2148 } 1884 }
2149 // ssh asks in sequence — a host key, then a password — and the 1885 // ssh asks in sequence and the next helper is already in the
2150 // next helper is already waiting in the backlog. Reopening 1886 // backlog: reopening BEFORE the close saves a screen that flashes
2151 // BEFORE the close saves the user a screen that flashes back to 1887 // back to the wall between two halves of one login.
2152 // the wall between two halves of one login; `askOpen` takes the
2153 // answer buffer back the way `closeAsk` would have.
2154 if (takeAsk(&shared, &input.prefix, &ask_prompt)) { 1888 if (takeAsk(&shared, &input.prefix, &ask_prompt)) {
2155 wall_picker.paintAsk(&shared, ask_prompt.slice(), &input.prefix); 1889 wall_picker.paintAsk(&shared, ask_prompt.slice(), &input.prefix);
2156 continue; 1890 continue;
@@ -2237,17 +1971,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2237 } 1971 }
2238 continue; 1972 continue;
2239 } 1973 }
2240 // An EMPTY wall — no tile present, whether none was ever born or 1974 // An EMPTY wall has no `tiles[z]`, and every branch below reads one.
2241 // the last one has left — has no `tiles[z]` to command, and every 1975 // `live` cannot answer it — a slot high-water never falls — so the
2242 // branch below reads one. `live` cannot answer this: it is the slot 1976 // `present` roll is what `relayout` counted. No sidecar on the way
2243 // high-water and never falls, so it still counts a tile that 1977 // out: writing one would erase the layout the user last left.
2244 // vanished. The `present` roll is what `relayout` counted when it
2245 // decided to paint the empty line, and the screen and the keyboard
2246 // have to agree on which wall this is.
2247 //
2248 // No sidecar on the way out: an empty wall has no tree worth
2249 // remembering, and writing one would erase the layout the user
2250 // last left on a wall that had tiles.
2251 if (presentCount(present[0..live]) == 0) { 1978 if (presentCount(present[0..live]) == 0) {
2252 switch (cmd.action) { 1979 switch (cmd.action) {
2253 .detach => { 1980 .detach => {
@@ -2263,12 +1990,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2263 } 1990 }
2264 continue; 1991 continue;
2265 } 1992 }
2266 // A BARE Ctrl-\ while there is no session to command is the 1993 // A BARE Ctrl-\ with no session to command is the user giving up on
2267 // user giving up on the wait. Judged after the filter rather 1994 // the wait. Judged after the filter, so `\x1c` still holding out for
2268 // than by scanning the raw bytes, because the two readings of 1995 // its command key stays apart from `\x1c w` asking for the wall.
2269 // the same byte have to stay apart: a `\x1c` still holding out
2270 // for its command key is the abort, while `\x1c w` is somebody
2271 // asking for the wall.
2272 if (cmd.action == .none and input.prefix.pending) { 1996 if (cmd.action == .none and input.prefix.pending) {
2273 if (awaitingSession(&tiles[z])) |waiting| { 1997 if (awaitingSession(&tiles[z])) |waiting| {
2274 exit_code = 0; 1998 exit_code = 0;
@@ -2285,20 +2009,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2285 // it is queued BEFORE the focus moves — so `Ctrl-\ n` cannot 2009 // it is queued BEFORE the focus moves — so `Ctrl-\ n` cannot
2286 // deliver the tail of a word to the tile it is jumping to. 2010 // deliver the tail of a word to the tile it is jumping to.
2287 if (cmd.forward.len > 0) { 2011 if (cmd.forward.len > 0) {
2288 // A press in another tile's rect moves the focus there. 2012 // A press in another tile's rect moves the focus there; a press in
2289 // The MouseFilter parses the terminal's SGR reports to find 2013 // the focused tile is not intercepted at all. `Event.at` is the
2290 // the press; the report bytes themselves go through to the 2014 // byte offset of each report, because keys before a click belong to
2291 // focused tile unchanged, so the Core that owns the drag 2015 // the OLD focus and keys after it to the new.
2292 // sees them. A click that lands in the focused tile is not
2293 // intercepted at all — it reaches the session that asked for
2294 // the mouse, exactly as a focused tile's did.
2295 //
2296 // `Event.at` is the byte offset in `forward` where each report
2297 // sits. A read holding keys and a click in the same burst must
2298 // deliver the keys that came BEFORE the click to the old focus
2299 // and the keys after it to the new — flushing everything to the
2300 // final focus would reroute a keystroke the user aimed at the
2301 // tile they were leaving.
2302 const report = mouse_filter.feed(cmd.forward, &mouse_out); 2016 const report = mouse_filter.feed(cmd.forward, &mouse_out);
2303 var seg_start: usize = 0; 2017 var seg_start: usize = 0;
2304 for (report.events) |ev| { 2018 for (report.events) |ev| {
@@ -2330,11 +2044,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2330 // that a keystroke can be for. 2044 // that a keystroke can be for.
2331 .ask_answer, .ask_decline => {}, 2045 .ask_answer, .ask_decline => {},
2332 .detach => { 2046 .detach => {
2333 // The slot goes back to the daemon before this process 2047 // The slot goes back to the daemon before this process does.
2334 // does — the pump writes the frame, because a Transport 2048 // The pump writes the frame; this waits briefly for the ack, so
2335 // has one owning thread, and this waits briefly for the 2049 // a `mux` typed straight after finds the session free.
2336 // ack so a `mux` typed straight afterwards does not find
2337 // the session full of its own corpse.
2338 tiles[z].detach_req.store(true, .release); 2050 tiles[z].detach_req.store(true, .release);
2339 ring(&tiles[z]); 2051 ring(&tiles[z]);
2340 awaitDetach(&tiles[z], &shared); 2052 awaitDetach(&tiles[z], &shared);