a73x

8bad3fee

refactor: comments say what the code is for, not how it got here

a73x   2026-08-30 19:12

Commit message
refactor: comments say what the code is for, not how it got here

wall_pump.zig and spawn.zig carried 47 comment essays between them —
race rationale re-derived line by line, and development history a reader
cannot act on. spawn.zig's header never said what spawn was.

Every block is now two lines naming what fails when it is wrong, and the
blocks ratchet drops to 0 for both files.

wall_pump.zig 1141 -> 851 lines, 529 -> 239 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
@@ -40,14 +40,14 @@ server_test_upgrade.zig 1
40 server.zig 108 40 server.zig 108
41 shellint.zig 7 41 shellint.zig 7
42 sockpath.zig 5 42 sockpath.zig 5
43 spawn.zig 2 43 spawn.zig 0
44 term.zig 0 44 term.zig 0
45 testtmp.zig 1 45 testtmp.zig 1
46 upgrade.zig 2 46 upgrade.zig 2
47 wall_host.zig 7 47 wall_host.zig 7
48 wall_layout.zig 2 48 wall_layout.zig 2
49 wall_picker.zig 6 49 wall_picker.zig 6
50 wall_pump.zig 45 50 wall_pump.zig 0
51 wall_test_harness.zig 1 51 wall_test_harness.zig 1
52 wall_test_host.zig 0 52 wall_test_host.zig 0
53 wall_test_layout.zig 2 53 wall_test_layout.zig 2
src/cli/spawn.zig
Old New
@@ -1,41 +1,26 @@
1 //! This image, as a path something can exec: `/proc/self/exe` read THROUGH 1 //! The file to exec when mux starts another copy of itself — a daemon.
2 //! to the file it names. Every production start hands execve this and never 2 //! `/proc/self/exe` read through to the real path, so a start runs THIS
3 //! a name resolved against PATH — that is the end of the ambient-PATH trap, 3 //! binary and never a `mux` that PATH happens to find. Under src/cli/
4 //! where an `execvp("muxd")` grades whatever release is installed and did 4 //! because asking the OS about the running process is not a client's question.
5 //! (an e2e leg whose daemon had died ran a v0.0.1-10 with no agent code in
6 //! it and passed).
7 //!
8 //! Under src/cli/ because it asks the OS about the process it is in, which
9 //! is a question a client module may not ask.
10 const std = @import("std"); 5 const std = @import("std");
11 6
12 /// The kernel's link to the running image. Only the fallback: a spawn is 7 /// The kernel's link to the running image. The fallback only: a start is
13 /// worth more than the name it will wear, so a readlink that fails still 8 /// worth more than the name it will wear.
14 /// starts a daemon.
15 pub const self_exe = "/proc/self/exe"; 9 pub const self_exe = "/proc/self/exe";
16 10
17 /// The file every production start execs: this image, resolved through 11 /// The file every production start execs: this image, resolved through
18 /// the /proc link to the path it names. 12 /// the /proc link to the path it names.
19 pub fn selfExe(buf: *[std.fs.max_path_bytes]u8) []const u8 { 13 pub fn selfExe(buf: *[std.fs.max_path_bytes]u8) []const u8 {
20 // Resolved, and that is the point. The kernel takes a process's `comm` 14 // Resolved, not the link: `comm` is the basename of the filename handed
21 // from the basename of the FILENAME handed to execve, so exec'ing the 15 // to execve, so exec'ing the link names every daemon `exe` in ps and pgrep.
22 // link itself leaves every daemon called `exe` — nothing `pgrep mux`,
23 // `killall mux`, `ps -o comm` or systemd's MainPID name can find —
24 // while only the args still read `mux d start`. The e2e that reads a
25 // spawned daemon's `/proc/PID/comm` is what says so.
26 return execOrLink(std.fs.selfExePath(buf) catch return self_exe); 16 return execOrLink(std.fs.selfExePath(buf) catch return self_exe);
27 } 17 }
28 18
29 /// The resolved path if it can still be exec'd, the /proc link if it cannot. 19 /// The resolved path if it can still be exec'd, the /proc link if it cannot.
30 /// Split from `selfExe` so the fallback is assertable without deleting a 20 /// Split out so the fallback is assertable without deleting a live binary.
31 /// binary out from under a running process.
32 fn execOrLink(resolved: []const u8) []const u8 { 21 fn execOrLink(resolved: []const u8) []const u8 {
33 // A readlink that SUCCEEDS can still name nothing: `make install` 22 // A readlink that SUCCEEDS can still name nothing: `make install` leaves
34 // replaces the file under a running wall and the kernel then answers 23 // `/…/mux (deleted)`, a description. The start outweighs the name.
35 // `/…/mux (deleted)`, which is a description and not a path. The link
36 // still opens the old inode, so the daemon starts wearing `exe` — the
37 // name is worth less than the start, and a refusal here would be an
38 // auto-start that fails for the length of one install.
39 std.posix.access(resolved, std.posix.X_OK) catch return self_exe; 24 std.posix.access(resolved, std.posix.X_OK) catch return self_exe;
40 return resolved; 25 return resolved;
41 } 26 }
@@ -45,17 +30,14 @@ fn execOrLink(resolved: []const u8) []const u8 {
45 test "selfExe: the exec'd name is a real file, not the /proc link" { 30 test "selfExe: the exec'd name is a real file, not the /proc link" {
46 var buf: [std.fs.max_path_bytes]u8 = undefined; 31 var buf: [std.fs.max_path_bytes]u8 = undefined;
47 const exe = selfExe(&buf); 32 const exe = selfExe(&buf);
48 // Handing execve the link itself is what named every spawned daemon 33 // comm is the basename of the filename exec'd, so this resolution IS the
49 // `exe`: comm is the basename of the filename exec'd, so the resolution 34 // name the daemon wears in ps, pgrep and killall.
50 // IS the name the daemon wears in ps, pgrep and killall.
51 try std.testing.expect(!std.mem.eql(u8, exe, self_exe)); 35 try std.testing.expect(!std.mem.eql(u8, exe, self_exe));
52 try std.posix.access(exe, std.posix.X_OK); 36 try std.posix.access(exe, std.posix.X_OK);
53 } 37 }
54 38
55 test "selfExe: a resolved path that is no longer a file falls back to the link" { 39 test "selfExe: a resolved path that is no longer a file falls back to the link" {
56 // What `make install` does to a running wall: the readlink SUCCEEDS and 40 // What `make install` does to a running wall. Spelled as the suffix the
57 // answers `/…/mux (deleted)`, so the only thing that can tell is asking
58 // whether the answer is still executable. Spelled as the suffix the
59 // kernel actually appends, because that is the string this must survive. 41 // kernel actually appends, because that is the string this must survive.
60 var buf: [std.fs.max_path_bytes]u8 = undefined; 42 var buf: [std.fs.max_path_bytes]u8 = undefined;
61 const live = try std.fs.selfExePath(&buf); 43 const live = try std.fs.selfExePath(&buf);
src/tui/wall_pump.zig
Old New
@@ -1,8 +1,7 @@
1 //! One tile's thread: the transport it owns in BOTH directions, its 1 //! One tile's thread: its transport in BOTH directions, its attach, claims,
2 //! attach, its claims and releases, its agent channels, its redials, and 2 //! releases, agent channels, redials, and the paint hooks the interaction
3 //! the paint hooks the interaction core calls back on. The keyboard hands 3 //! core calls back on. The keyboard hands it work through the tile's mailbox
4 //! this thread work through the tile's mailbox and doorbell and reads its 4 //! and doorbell. Nothing here touches another tile.
5 //! answers back the same way; nothing here touches another tile.
6 const std = @import("std"); 5 const std = @import("std");
7 const proto = @import("term").protocol; 6 const proto = @import("term").protocol;
8 const client = @import("client"); 7 const client = @import("client");
@@ -26,23 +25,17 @@ pub fn copySelection(
26 { 25 {
27 t.shared.paint_mu.lock(); 26 t.shared.paint_mu.lock();
28 defer t.shared.paint_mu.unlock(); 27 defer t.shared.paint_mu.unlock();
29 // The drag is this tile's own Core's now: a drag is per tile, and
30 // the pump that owns the link is the one whose Core holds it.
31 const held = core.drag.range(); 28 const held = core.drag.range();
32 answer = core.selectionCopy(payload, held); 29 answer = core.selectionCopy(payload, held);
33 switch (answer) { 30 switch (answer) {
34 // `is_tty` and not the tile's claim: a tile that has released 31 // `is_tty`, not the claim: a tile that released the terminal can
35 // the terminal can still be the one whose finished drag the 32 // still own the finished drag, and the copy is still the user's.
36 // answer reaches, and the copy is still the user's. A piped
37 // `mux` asks its terminal for no mouse modes, so it can have no
38 // drag to copy in the first place.
39 .text => |text| if (t.shared.is_tty) 33 .text => |text| if (t.shared.is_tty)
40 interact.writeSelectionCopy(alloc, t.shared.out_fd, text) catch {}, 34 interact.writeSelectionCopy(alloc, t.shared.out_fd, text) catch {},
41 .none, .too_large => {}, 35 .none, .too_large => {},
42 } 36 }
43 } 37 }
44 // Outside the hold: `tileBanner` takes the same lock, which is not 38 // Outside the hold: `tileBanner` takes the same non-reentrant lock.
45 // reentrant. The banner lands in this tile's own rect.
46 if (answer == .too_large) wv.tileBanner(t, "[selection too large to copy]"); 39 if (answer == .too_large) wv.tileBanner(t, "[selection too large to copy]");
47 } 40 }
48 41
@@ -52,22 +45,14 @@ pub fn tilePaintBegin(ctx: ?*anyopaque) bool {
52 const t: *Tile = @ptrCast(@alignCast(ctx.?)); 45 const t: *Tile = @ptrCast(@alignCast(ctx.?));
53 if (t.gone.load(.acquire)) return false; 46 if (t.gone.load(.acquire)) return false;
54 t.shared.paint_mu.lock(); 47 t.shared.paint_mu.lock();
55 // Read UNDER the lock, not before it: a pump that tested the flag and 48 // Under the lock, not before: a pump that loses the race paints over the
56 // then lost the race for `paint_mu` would paint its rect on top of the 49 // popup, and `picker_stamp` suppresses the repaint that would repair it.
57 // box the keyboard had just drawn — and `picker_stamp` suppresses the
58 // identical repaint that would have repaired it, so the damage sticks
59 // until a key changes the frame.
60 if (wv.popupOpen(t.shared)) { 50 if (wv.popupOpen(t.shared)) {
61 t.shared.paint_mu.unlock(); 51 t.shared.paint_mu.unlock();
62 return false; 52 return false;
63 } 53 }
64 // A pass a relayout has since superseded paints at a rect the screen 54 // A pass a relayout superseded paints at a rect the screen no longer
65 // no longer has. The relayout cleared the screen and moved this tile's 55 // has, across a neighbour's bar that nothing repaints after.
66 // neighbours' bars under the OLD offsets, and the pump re-takes a
67 // pass only at the top of its loop — so a paint from this pass would
68 // land a session's rows across a neighbour's bar, and nothing repaints
69 // that bar afterwards. The relayout already rang this pump; its next
70 // pass carries the new rect and the generation that repaints.
71 if (t.shared.repaint_gen.load(.acquire) != t.pass_gen) { 56 if (t.shared.repaint_gen.load(.acquire) != t.pass_gen) {
72 t.shared.paint_mu.unlock(); 57 t.shared.paint_mu.unlock();
73 return false; 58 return false;
@@ -78,13 +63,8 @@ pub fn tilePaintBegin(ctx: ?*anyopaque) bool {
78 pub fn tilePaintEnd(ctx: ?*anyopaque) void { 63 pub fn tilePaintEnd(ctx: ?*anyopaque) void {
79 const t: *Tile = @ptrCast(@alignCast(ctx.?)); 64 const t: *Tile = @ptrCast(@alignCast(ctx.?));
80 defer t.shared.paint_mu.unlock(); 65 defer t.shared.paint_mu.unlock();
81 // Before releasing the terminal: the cursor belongs to the FOCUSED tile. 66 // The cursor belongs to the FOCUSED tile: an unfocused paint's last act
82 // A focused paint records where its cursor landed; an unfocused paint's 67 // puts it back, hidden for the move so the show does not flash it there.
83 // last act is to put the cursor back there, so a redraw in tile 2 cannot
84 // steal the eye while the keys go to tile 1. The cursor is hidden for
85 // the move so the show after it does not flash it at the unfocused
86 // tile's final position before the CUP lands — then re-shown so the
87 // focused tile's cursor rests visible until its next paint.
88 if (t.idx == t.shared.sel) { 68 if (t.idx == t.shared.sel) {
89 if (t.core) |core| t.shared.cursor = core.screenCursor(); 69 if (t.core) |core| t.shared.cursor = core.screenCursor();
90 } else { 70 } else {
@@ -95,12 +75,9 @@ pub fn tilePaintEnd(ctx: ?*anyopaque) void {
95 } 75 }
96 76
97 /// The one place a wall tile puts an attach on the wire. A tile the user 77 /// The one place a wall tile puts an attach on the wire. A tile the user
98 /// asked for — the entry tile, a chord-born tile, a local line off the 78 /// asked for claims its rect, and that size is what lets the daemon create
99 /// saved wall — claims its rect on attach, and that size is what lets the 79 /// the session; a view tile attaches at 0x0 so it can only JOIN, then takes
100 /// daemon create the session. A view tile (wall argv, a remote saved 80 /// its rect with the resize doorbell one frame later.
101 /// line) attaches at 0x0 so it can only JOIN, then takes its rect with the
102 /// resize doorbell one frame later — every tile still claims its
103 /// rectangle, and a redial comes back claiming what the tile claimed.
104 pub fn sendAttach(t: *Tile, tr: *client.Transport, have_seq: u64, have_epoch: u64) !void { 81 pub fn sendAttach(t: *Tile, tr: *client.Transport, have_seq: u64, have_epoch: u64) !void {
105 // Snapshot under `paint_mu`: the keyboard may relayout (re-cut stripes, 82 // Snapshot under `paint_mu`: the keyboard may relayout (re-cut stripes,
106 // resize) concurrently with the pump's first attach. 83 // resize) concurrently with the pump's first attach.
@@ -123,18 +100,16 @@ pub fn sendAttach(t: *Tile, tr: *client.Transport, have_seq: u64, have_epoch: u6
123 have_epoch, 100 have_epoch,
124 proto.wireName(t.r.session), 101 proto.wireName(t.r.session),
125 )); 102 ));
126 // A view tile made no size claim, so it owes its rect now: the same 103 // A view tile made no size claim, so it owes its rect now, on the same
127 // doorbell path a relayout takes (adoptSize + .resize), run on this 104 // doorbell path a relayout takes.
128 // pump thread which is the transport's only writer.
129 if (!t.creates) { 105 if (!t.creates) {
130 t.shared.paint_mu.lock(); 106 t.shared.paint_mu.lock();
131 t.resize_pending = true; 107 t.resize_pending = true;
132 t.shared.paint_mu.unlock(); 108 t.shared.paint_mu.unlock();
133 wv.ring(t); 109 wv.ring(t);
134 } 110 }
135 // Re-armed on EVERY attach, not once per process: a redial is a fresh 111 // Re-armed on EVERY attach: a redial lands on a fresh daemon-side slot,
136 // attach onto a fresh daemon-side slot, which remembers no offer. Empty 112 // which remembers no offer.
137 // payload, and a daemon too old to know the frame skips it.
138 if (t.r.agent) try tr.writeFrame(.agent_offer, ""); 113 if (t.r.agent) try tr.writeFrame(.agent_offer, "");
139 } 114 }
140 115
@@ -150,29 +125,19 @@ pub const ClaimStep = enum {
150 125
151 /// May this tile take the terminal for the arm it is holding? 126 /// May this tile take the terminal for the arm it is holding?
152 fn claimAllowed(t: *Tile) bool { 127 fn claimAllowed(t: *Tile) bool {
153 // BEFORE the claim, never after. The keyboard arms `claim_pending` and 128 // BEFORE the claim, never after: an arm that outlives its focus and then
154 // the pump reads it a pass later, and the focus can move in between — 129 // succeeds puts two tiles' modes on one terminal, and nothing undoes it.
155 // a poller's tile arriving, the birth an Enter makes, all of it under
156 // the host picker's popup. An arm that outlives its focus and then
157 // SUCCEEDS puts two tiles' modes on one terminal, rests the cursor on
158 // the loser, and takes the focus notice with it; the outgoing tile's
159 // release was consumed a pass earlier, so nothing undoes any of it.
160 //
161 // Judging the claim's REFUSAL cannot cover this: the arm that matters
162 // is the one retried after the popup closed, which the sink admits.
163 t.shared.paint_mu.lock(); 130 t.shared.paint_mu.lock();
164 defer t.shared.paint_mu.unlock(); 131 defer t.shared.paint_mu.unlock();
165 return t.shared.sel == t.idx; 132 return t.shared.sel == t.idx;
166 } 133 }
167 134
168 /// The wall's ONLY door to `Core.claimTerminal`: the focus test, the size 135 /// The wall's ONLY door to `Core.claimTerminal`: focus test, size adopt,
169 /// adopt, and the claim, in that order and never apart. A caller that could 136 /// claim, in that order and never apart. Around it is a stale claim.
170 /// reach the claim around this is a caller that can mint a stale one.
171 pub fn claimFocus(t: *Tile, core: *interact.Core, rect: proto.Size) ClaimStep { 137 pub fn claimFocus(t: *Tile, core: *interact.Core, rect: proto.Size) ClaimStep {
172 if (!claimAllowed(t)) return .dropped; 138 if (!claimAllowed(t)) return .dropped;
173 // A tile born before somebody resized the terminal clips its paints to 139 // The tile's current RECT, never the whole terminal: a wall of two would
174 // a screen that is gone; the tile's current RECT, never the whole 140 // otherwise let this tile paint over its neighbour.
175 // terminal, which on a wall of two would let it paint over a neighbour.
176 if (core.size.cols != rect.cols or core.size.rows != rect.rows) 141 if (core.size.cols != rect.cols or core.size.rows != rect.rows)
177 core.adoptSize(rect); 142 core.adoptSize(rect);
178 return afterClaim(t, core.claimTerminal(), core.claim != .none, core.is_tty); 143 return afterClaim(t, core.claimTerminal(), core.claim != .none, core.is_tty);
@@ -180,23 +145,13 @@ pub fn claimFocus(t: *Tile, core: *interact.Core, rect: proto.Size) ClaimStep {
180 145
181 /// A refused focus claim, judged. 146 /// A refused focus claim, judged.
182 pub fn afterClaim(t: *Tile, claimed: bool, held: bool, is_tty: bool) ClaimStep { 147 pub fn afterClaim(t: *Tile, claimed: bool, held: bool, is_tty: bool) ClaimStep {
183 // `held` is `Core.claim != .none`. A claim answers false for three 148 // A claim answers false for three reasons and only the SINK's refusal is
184 // reasons and only ONE of them is worth another pass: not a tty (there 149 // worth another pass. Read off the Core: `picker_open` can clear between.
185 // is no terminal to hold), already held (re-arming would re-take the
186 // focus notice every pass, forever), and the SINK refused — which is
187 // the host picker, whose popup admits no paint and a claim writes the
188 // session's modes through one. Read off the Core, never off
189 // `picker_open` a second time: the keyboard can clear that flag between
190 // the two loads, and then the refusal is dropped exactly as before.
191 if (claimed or held or !is_tty) return .done; 150 if (claimed or held or !is_tty) return .done;
192 t.shared.paint_mu.lock(); 151 t.shared.paint_mu.lock();
193 defer t.shared.paint_mu.unlock(); 152 defer t.shared.paint_mu.unlock();
194 // ...and only while this tile is STILL the focus. `claimAllowed` asked 153 // ...and only while this tile is STILL the focus, or re-arming mints the
195 // the same question before the claim; the keyboard can answer it 154 // stale arm `claimAllowed` exists to stop.
196 // differently in between, and re-arming then would mint the very stale
197 // arm that check exists to stop. Left CLEAR rather than cleared — the
198 // caller's `swap` did that — because a focus that came back inside this
199 // window re-armed it legitimately.
200 if (t.shared.sel != t.idx) return .dropped; 155 if (t.shared.sel != t.idx) return .dropped;
201 t.claim_pending.store(true, .release); 156 t.claim_pending.store(true, .release);
202 return .rearmed; 157 return .rearmed;
@@ -212,7 +167,6 @@ fn publishStats(shared: *Shared, c: interact.PredictCounters) void {
212 167
213 /// Validated first: these bytes came out of a peer's `sessions_reply` and 168 /// Validated first: these bytes came out of a peer's `sessions_reply` and
214 /// `SessionName.of` memcpys with no bound of its own (`client.validPick`). 169 /// `SessionName.of` memcpys with no bound of its own (`client.validPick`).
215 /// A name it refuses is a name nobody is moved to.
216 fn postAnswer(t: *Tile, pick: []const u8) void { 170 fn postAnswer(t: *Tile, pick: []const u8) void {
217 const name = client.validPick(pick) orelse return; 171 const name = client.validPick(pick) orelse return;
218 { 172 {
@@ -231,9 +185,8 @@ fn endWith(t: *Tile, reason: EndReason, code: u8) void {
231 t.end.store(@intFromEnum(reason), .release); 185 t.end.store(@intFromEnum(reason), .release);
232 } 186 }
233 187
234 /// Whole-mailbox chunking, so `offerKeystroke` (one-byte chunks only) counts 188 /// Whole-mailbox chunking, so `offerKeystroke` counts keys arriving between
235 /// keystrokes that arrive between polls as suppressed: a wall predicts a 189 /// polls as suppressed. Splitting would speculate against a stale replica.
236 /// little less. Splitting would speculate against a stale replica.
237 pub fn takeKeys(t: *Tile, out: []u8) []u8 { 190 pub fn takeKeys(t: *Tile, out: []u8) []u8 {
238 t.in_mu.lock(); 191 t.in_mu.lock();
239 defer t.in_mu.unlock(); 192 defer t.in_mu.unlock();
@@ -250,10 +203,8 @@ fn drainWake(t: *const Tile) void {
250 203
251 /// Every wall dial's ssh sends its prompts to this client's popup. 204 /// Every wall dial's ssh sends its prompts to this client's popup.
252 pub fn askOn(target: client.Target, shared: *const Shared) client.Target { 205 pub fn askOn(target: client.Target, shared: *const Shared) client.Target {
253 // ONE place, so the two exclusions hold by construction rather than by 206 // ONE place, so the two exclusions hold by construction: the entry attach
254 // a call somebody has to remember: the entry attach opens on the main 207 // opens before any listener exists, and a poller spells `BatchMode`.
255 // thread before any listener exists, and a poller never comes through
256 // here at all — it spells `BatchMode` and asks nothing.
257 var out = target; 208 var out = target;
258 if (out != .hand) return out; 209 if (out != .hand) return out;
259 const l = shared.prompts orelse return out; 210 const l = shared.prompts orelse return out;
@@ -265,37 +216,26 @@ pub fn askOn(target: client.Target, shared: *const Shared) client.Target {
265 fn dial(alloc: std.mem.Allocator, t: *Tile, target_in: client.Target) ?client.Transport { 216 fn dial(alloc: std.mem.Allocator, t: *Tile, target_in: client.Target) ?client.Transport {
266 var target = askOn(target_in, t.shared); 217 var target = askOn(target_in, t.shared);
267 var backoff_ms: u64 = 0; 218 var backoff_ms: u64 = 0;
268 // `gone` as well as `running`: a tile forgotten while it is retrying a 219 // `gone` as well as `running`: a tile forgotten while retrying a dead
269 // dead host must stop retrying, not keep a thread and a backoff alive 220 // host must not keep a thread and a backoff alive.
270 // for a tile that is no longer on the wall.
271 while (t.shared.running.load(.acquire) and !t.gone.load(.acquire)) { 221 while (t.shared.running.load(.acquire) and !t.gone.load(.acquire)) {
272 // The reason is not kept — a tile that cannot dial says 222 // The reason is not kept — the picker row polls the same host and
273 // `connecting`, and the sentence ssh gave belongs to the picker 223 // carries it. The PID is: only it separates a refusal from a dead box.
274 // row, which polls the same host on its own interval — ten seconds
275 // for exactly this host, since a failed `.hand` poll answers
276 // `.pipe` and `pollDelayMs` stretches those. The PID is: it is the
277 // only thing that tells a dial the user refused from a box that is
278 // merely down.
279 var d: handoff.Dial = .{}; 224 var d: handoff.Dial = .{};
280 if (client.Transport.open(alloc, target, null, -1, &d)) |tr| return tr else |_| {} 225 if (client.Transport.open(alloc, target, null, -1, &d)) |tr| return tr else |_| {}
281 // A refused prompt is an answer, and retrying is arguing with it: 226 // A refused prompt is an answer, and retrying is arguing with it:
282 // without this the user who pressed Esc is asked again every two 227 // without this, Esc is answered with the same prompt every two seconds.
283 // seconds for as long as the wall is up. Another go is a birth
284 // from the picker, which is a thing they can choose to do.
285 if (t.shared.prompts) |l| { 228 if (t.shared.prompts) |l| {
286 if (l.declined(d.ssh_pid)) { 229 if (l.declined(d.ssh_pid)) {
287 // The bar says it, before the pump goes: an unfocused tile 230 // The bar says it before the pump goes: an unfocused tile that
288 // that ends without exiting narrates on its own bar and 231 // ends without exiting narrates itself (`wallview.endedTile`).
289 // asks the keyboard for nothing (`wallview.endedTile`).
290 wv.paintLabel(t, .declined); 232 wv.paintLabel(t, .declined);
291 endWith(t, .declined, 1); 233 endWith(t, .declined, 1);
292 return null; 234 return null;
293 } 235 }
294 } 236 }
295 // An ask buys ONE attempt. Every retry below is the wall's own 237 // An ask buys ONE attempt: the asking word per backoff would restart a
296 // idea: the asking word per backoff would restart a daemon for as 238 // daemon for as long as the tile lives.
297 // long as the tile lives, and a fallback line per backoff would
298 // scroll the alternate screen the tiles are painted on.
299 if (target == .hand) target.hand.asked = false; 239 if (target == .hand) target.hand.asked = false;
300 backoff_ms = client.nextBackoffMs(backoff_ms); 240 backoff_ms = client.nextBackoffMs(backoff_ms);
301 // Sliced sleep so quit is never behind a full backoff. 241 // Sliced sleep so quit is never behind a full backoff.
@@ -310,11 +250,8 @@ fn dial(alloc: std.mem.Allocator, t: *Tile, target_in: client.Target) ?client.Tr
310 } 250 }
311 251
312 /// One forwarded ssh-agent channel, this end of it: the id the daemon 252 /// One forwarded ssh-agent channel, this end of it: the id the daemon
313 /// allocated, and an fd to THIS machine's agent. 253 /// allocated, and an fd to THIS machine's agent. Thread-local by
314 /// 254 /// construction — the table lives in `pumpTile`'s frame, so nothing locks.
315 /// Thread-local by construction — the table lives in `pumpTile`'s frame and
316 /// no other thread can see it, which is why nothing here takes a lock and
317 /// why these helpers take the table as a slice rather than reaching for one.
318 pub const AgentLocal = struct { id: u32, fd: std.posix.fd_t }; 255 pub const AgentLocal = struct { id: u32, fd: std.posix.fd_t };
319 256
320 /// Fixed at `proto.agent_chans_max`, which is what the daemon opens anyway: 257 /// Fixed at `proto.agent_chans_max`, which is what the daemon opens anyway:
@@ -335,10 +272,8 @@ pub fn findLocal(locals: []?AgentLocal, id: u32) ?usize {
335 return null; 272 return null;
336 } 273 }
337 274
338 /// Hang one channel up from this end and say so, because the daemon is 275 /// Hang one channel up from this end and say so: the daemon is holding the
339 /// holding the far socket open waiting for bytes that are not coming. A 276 /// far socket open for bytes that are not coming.
340 /// failed write is the transport itself being gone, which the caller's next
341 /// pass turns into a redial.
342 pub fn closeLocal(locals: []?AgentLocal, slot: usize, transport: *client.Transport) void { 277 pub fn closeLocal(locals: []?AgentLocal, slot: usize, transport: *client.Transport) void {
343 const ch = locals[slot] orelse return; 278 const ch = locals[slot] orelse return;
344 locals[slot] = null; 279 locals[slot] = null;
@@ -353,13 +288,11 @@ pub fn openAgentChan(
353 offered: bool, 288 offered: bool,
354 sock: []const u8, 289 sock: []const u8,
355 ) bool { 290 ) bool {
356 // The OFFER is the consent, and it is per TILE: a wall where one tile 291 // The OFFER is the consent, and it is per TILE: one tile typed with `-A`
357 // was typed with `-A` must not hand another tile's host the keys, 292 // must not hand another tile's host the keys.
358 // whoever asks.
359 if (!offered) return false; 293 if (!offered) return false;
360 // A live id reused. Refusing keeps the channel already on that id 294 // A live id reused. Refusing keeps the channel already on that id, which
361 // intact, which is the half of the collision that has real bytes moving 295 // is the half of the collision with real bytes moving through it.
362 // through it.
363 if (findLocal(locals, id) != null) return false; 296 if (findLocal(locals, id) != null) return false;
364 const fd = client.connectAgent(sock) orelse return false; 297 const fd = client.connectAgent(sock) orelse return false;
365 if (storeLocal(locals, id, fd) == null) { 298 if (storeLocal(locals, id, fd) == null) {
@@ -381,10 +314,8 @@ pub fn deliverAgentData(
381 closeLocal(locals, s, transport); 314 closeLocal(locals, s, transport);
382 return true; 315 return true;
383 } 316 }
384 // Bytes onto the fd in order, never parsed and never reassembled: the 317 // Bytes onto the fd in order, never parsed: one agent message can arrive
385 // daemon reads the far end in `agent_data_max` bites, so one agent 318 // as several frames and several messages as one; the agent self-delimits.
386 // message can arrive as several frames and several messages as one. The
387 // agent protocol delimits itself over a stream, and this end is a pipe.
388 proto.writeAllFd(locals[s].?.fd, payload[proto.agent_id_len..]) catch 319 proto.writeAllFd(locals[s].?.fd, payload[proto.agent_id_len..]) catch
389 closeLocal(locals, s, transport); 320 closeLocal(locals, s, transport);
390 return true; 321 return true;
@@ -402,9 +333,6 @@ pub fn dropLocals(locals: []?AgentLocal) void {
402 /// The transport died, or the dial has to be redone: rebuild it on the CLI's 333 /// The transport died, or the dial has to be redone: rebuild it on the CLI's
403 /// backoff and re-attach quoting what this tile holds. False means the pump 334 /// backoff and re-attach quoting what this tile holds. False means the pump
404 /// is finished — the wall quit, or the tile was forgotten while retrying. 335 /// is finished — the wall quit, or the tile was forgotten while retrying.
405 ///
406 /// One function for what were four copies of five steps, which had begun to
407 /// differ: only some dropped a scroll view a resync was about to invalidate.
408 fn redial( 336 fn redial(
409 t: *Tile, 337 t: *Tile,
410 alloc: std.mem.Allocator, 338 alloc: std.mem.Allocator,
@@ -413,27 +341,17 @@ fn redial(
413 target: client.Target, 341 target: client.Target,
414 state: *State, 342 state: *State,
415 /// This tile's agent channels, which the dying connection owned. Dropped 343 /// This tile's agent channels, which the dying connection owned. Dropped
416 /// HERE, and here only, for the reason this function exists at all: 344 /// HERE and only here, so no call site can strand one.
417 /// every call site that had to remember would be one more chance to
418 /// strand a channel on a connection that cannot close it.
419 agents: []?AgentLocal, 345 agents: []?AgentLocal,
420 ) bool { 346 ) bool {
421 // A close that follows our own detach is the daemon saying goodbye back, 347 // A close after our own detach is the daemon saying goodbye, not a tear:
422 // not a tear to heal: the pump wrote the .detach frame and set 348 // redialing would re-attach the slot the user just released.
423 // `detach_ack` before the save's file I/O window let readFrame see the
424 // daemon's side. Redialing here would re-attach a slot the user just
425 // released.
426 if (t.detach_ack.load(.acquire)) return false; 349 if (t.detach_ack.load(.acquire)) return false;
427 // Before the cold-dial refusal below, which returns without reconnecting 350 // Before the cold-dial refusal below, which returns without reconnecting
428 // — a pump that ends still owes these fds. 351 // — a pump that ends still owes these fds.
429 dropLocals(agents); 352 dropLocals(agents);
430 // The plain client's rule, kept for the tile a `mux TARGET` is: a 353 // A transport that died before any state carried no session, so there is
431 // transport that died before a single frame of state carried no 354 // nothing to resume; wall tiles retry forever because a box reboots.
432 // session, so there is nothing to resume and retrying a bad host or a
433 // typo'd `--via` only makes an unkillable client. `session_epoch` is
434 // the right signal because it is set from the first snapshot and never
435 // reset. Wall tiles do the opposite deliberately — they retry forever,
436 // because a wall is a thing you leave up while a box reboots.
437 if (!t.retry_cold and core.rep.session_epoch == 0) { 355 if (!t.retry_cold and core.rep.session_epoch == 0) {
438 endWith(t, .lost, 1); 356 endWith(t, .lost, 1);
439 return false; 357 return false;
@@ -441,19 +359,15 @@ fn redial(
441 transport.close(); 359 transport.close();
442 state.* = .reconnecting; 360 state.* = .reconnecting;
443 wv.paintLabel(t, state.*); 361 wv.paintLabel(t, state.*);
444 // ...and the same news for a one-tile wall, which has no label 362 // The one-tile wall has no label bar to read this off. `banner` is gated
445 // bar on screen to read it off. The corner banner is the plain client's 363 // on the sink, so a stripe's redial writes nothing here.
446 // own, said before the dial rather than inside it for its reason: it is
447 // a PAINT on the session's screen, and the Core is what paints. `banner`
448 // is gated on the sink, so a stripe's re-dial writes nothing here.
449 core.banner("[reconnecting]"); 364 core.banner("[reconnecting]");
450 // The resync's own paint is what will arrive, so a history page held 365 // The resync's own paint is what will arrive, so a history page held
451 // here would be silently replaced a moment later. 366 // here would be silently replaced a moment later.
452 core.dropScrollView(); 367 core.dropScrollView();
453 transport.* = dial(alloc, t, target) orelse return false; 368 transport.* = dial(alloc, t, target) orelse return false;
454 // Clears `state_since_attach` (so the next exit_status is read as a 369 // Clears `state_since_attach` and drops speculation made against a
455 // refusal again) and drops speculation made against a connection that 370 // connection that no longer exists.
456 // no longer exists — the Core's own highlight with it.
457 core.reattached(); 371 core.reattached();
458 const have = core.rep.attachArgs(); 372 const have = core.rep.attachArgs();
459 sendAttach(t, transport, have.have_seq, have.have_epoch) catch return false; 373 sendAttach(t, transport, have.have_seq, have.have_epoch) catch return false;
@@ -472,21 +386,16 @@ const Pass = struct {
472 // A relayout re-cut this tile: the pump owes the daemon THIS pass's 386 // A relayout re-cut this tile: the pump owes the daemon THIS pass's
473 // content size. 387 // content size.
474 resize: bool, 388 resize: bool,
475 // The repaint generation this pass was taken under: the pump's repaint 389 // The repaint generation this pass was taken under: a later read would
476 // decision compares THIS, not a later read, so a relayout between the 390 // let a relayout be consumed by a paint at this pass's stale rect.
477 // pass and the decision is repainted by the next pass rather than
478 // consumed by a paint at this pass's stale rect.
479 gen: u64, 391 gen: u64,
480 }; 392 };
481 393
482 pub fn takePass(t: *Tile) Pass { 394 pub fn takePass(t: *Tile) Pass {
483 t.shared.paint_mu.lock(); 395 t.shared.paint_mu.lock();
484 defer t.shared.paint_mu.unlock(); 396 defer t.shared.paint_mu.unlock();
485 // The flag comes out of the SAME hold as the rect it describes. Read a 397 // The flag comes out of the SAME hold as the rect it describes: a
486 // pass apart from its doorbell and a relayout landing between the two 398 // relayout landing between two reads is swallowed, and nothing resends.
487 // is swallowed: the pump sends the rect it snapshotted first, clears
488 // the flag, and the daemon keeps a grid the tile has already stopped
489 // painting at — nothing re-sends, because the claim path does not.
490 const owed = t.resize_pending; 399 const owed = t.resize_pending;
491 t.resize_pending = false; 400 t.resize_pending = false;
492 const gen = t.shared.repaint_gen.load(.acquire); 401 const gen = t.shared.repaint_gen.load(.acquire);
@@ -504,33 +413,18 @@ pub fn takePass(t: *Tile) Pass {
504 }; 413 };
505 } 414 }
506 415
507 /// Absolute rows count from the oldest row the daemon keeps, and a resync 416 /// One tile's life: dial → attach → replay frames into its Core → repaint at
508 /// renames that space: a kept highlight inverts rows nobody selected. 417 /// its rect, on its own thread. On transport death it reconnects on the CLI's
509 /// One tile's life: dial → attach → replay frames into its Core → repaint 418 /// backoff. This thread is also the tile's only WRITER: every frame the
510 /// at its rect. Runs on its own thread (see module header). On transport 419 /// keyboard doorbells for goes out from here.
511 /// death: reconnect on the CLI's backoff schedule, quoting
512 /// have_seq/have_epoch, and the snapshot-vs-delta resolution does the rest.
513 /// Ends when `running` clears, the session exits, or the attach is refused.
514 ///
515 /// This thread is also the tile's WRITER: every frame the keyboard doorbells
516 /// for — a resize, a detach, a focus claim or release — goes out from here,
517 /// because a Transport has exactly one owning thread (module header).
518 pub fn pumpTile(t: *Tile) void { 420 pub fn pumpTile(t: *Tile) void {
519 // FIRST defer, so it runs LAST: every `return` below — a refused 421 // FIRST defer, so it runs LAST: every `return` below is this tile going
520 // attach, an exited session, a dial the quit interrupted, a Core that 422 // quiet, and the keyboard's test is `!alive` — so the bell follows the store.
521 // would not initialise — is this tile going quiet for good, and the
522 // keyboard needs to know which tiles it has to paint for. Declared
523 // before the allocator's own defer so nothing can end this thread
524 // without it running.
525 // The bell goes with the store and after it, `endWith`'s reason: the
526 // keyboard's test is `!alive`, so a ring that precedes the store is a
527 // wake-up that finds nothing.
528 defer { 423 defer {
529 t.alive.store(false, .release); 424 t.alive.store(false, .release);
530 wv.ringKeyboard(t.shared); 425 wv.ringKeyboard(t.shared);
531 // LAST, after the bell above has finished reading `t.shared`: this 426 // LAST, after the bell has finished reading `t.shared`: this hands the
532 // is what hands the slot to `birthTile`, and nothing may touch the 427 // slot to `birthTile`, and nothing may touch the tile after it.
533 // tile after it.
534 t.pump_done.store(true, .release); 428 t.pump_done.store(true, .release);
535 } 429 }
536 430
@@ -541,22 +435,12 @@ pub fn pumpTile(t: *Tile) void {
541 const alloc = gpa.allocator(); 435 const alloc = gpa.allocator();
542 436
543 // Whether this tile may narrate and may start a daemon travels IN its 437 // Whether this tile may narrate and may start a daemon travels IN its
544 // target, set once by whoever made the tile: a picker Enter is an ask, 438 // target. The pump's own copy: `t.r.target` belongs to the keyboard.
545 // a poll's list is not. This is the pump's own copy — `t.r.target` is
546 // read by the KEYBOARD thread under `paint_mu` for chord births and is
547 // never written from here — and it is spent below, once.
548 var target = t.r.target; 439 var target = t.r.target;
549 440
550 // ONE Core per tile, from birth. It owns this tile's replica, its 441 // ONE Core per tile, from birth: it owns this tile's replica, prediction
551 // prediction overlay and its drag for the tile's whole life: the tile 442 // overlay and drag for the tile's whole life. `in_fd` is the wall's stdin
552 // paints from that replica at its own offset, and what decides whether 443 // and this Core never reads it — it is passed for `is_tty`.
553 // a keystroke may be speculated at all is the pty's line discipline,
554 // which arrives in `.pty_mode` frames long before the tile is focused.
555 //
556 // `in_fd` is the wall's stdin and this Core never reads it — the
557 // keyboard thread does, on the far side of the mailbox. It is passed
558 // because it is the truth about whether there is a terminal here at all
559 // (`is_tty`), which the mouse split and the side channels are gated on.
560 var core = interact.Core.initSized( 444 var core = interact.Core.initSized(
561 alloc, 445 alloc,
562 std.posix.STDIN_FILENO, 446 std.posix.STDIN_FILENO,
@@ -564,18 +448,14 @@ pub fn pumpTile(t: *Tile) void {
564 t.shared.size, 448 t.shared.size,
565 ) catch return; 449 ) catch return;
566 defer core.deinit(); 450 defer core.deinit();
567 // Whether this tile has EVER held the terminal, which is the only 451 // Whether this tile has EVER held the terminal: a tile that never was
568 // question the publish below is gated on: a tile that was never focused 452 // focused must not overwrite the stats of the tile that was.
569 // has nothing to say about prediction and must not overwrite what the
570 // tile that was does.
571 var ever_focused = false; 453 var ever_focused = false;
572 // Focus, as this pump knows it. Not `core.claim`: a claim needs a 454 // Focus as this pump knows it, not `core.claim`: `mux` on a pipe has no
573 // terminal, and `mux` on a pipe has none, yet its one tile is focused 455 // terminal to claim, yet its one tile is focused.
574 // and its predictions still expire and still get counted.
575 var focused = false; 456 var focused = false;
576 // The last word on this tile's prediction, whichever way the pump ends 457 // The last word on this tile's prediction: an `exit_status` RETURNS, so
577 // — an exit_status arrives and RETURNS, so the per-pass publish inside 458 // the per-pass publish is always one pass stale by then.
578 // the loop is always one pass stale by then.
579 defer if (ever_focused) publishStats(t.shared, core.overlay.counters); 459 defer if (ever_focused) publishStats(t.shared, core.overlay.counters);
580 // Where this Core's paints land: this tile's rect. The sink admits a 460 // Where this Core's paints land: this tile's rect. The sink admits a
581 // paint whenever the tile has not been forgotten; see `tilePaintBegin`. 461 // paint whenever the tile has not been forgotten; see `tilePaintBegin`.
@@ -588,9 +468,8 @@ pub fn pumpTile(t: *Tile) void {
588 core.owns_stats = false; 468 core.owns_stats = false;
589 469
590 wv.paintLabel(t, .connecting); 470 wv.paintLabel(t, .connecting);
591 // The ENTRY tile arrives with its link already up — dialled on the main 471 // The ENTRY tile arrives with its link already up, dialled where the tty
592 // thread, where the tty was, so ssh could prompt. `adopt` is what moves 472 // was so ssh could prompt; `adopt` moves its QUIC out-queue onto us.
593 // its QUIC out-queue onto this thread's allocator; see there.
594 var transport = if (t.pre) |pre| blk: { 473 var transport = if (t.pre) |pre| blk: {
595 var tr = pre; 474 var tr = pre;
596 t.pre = null; 475 t.pre = null;
@@ -598,61 +477,40 @@ pub fn pumpTile(t: *Tile) void {
598 break :blk tr; 477 break :blk tr;
599 } else dial(alloc, t, target) orelse return; 478 } else dial(alloc, t, target) orelse return;
600 defer transport.close(); 479 defer transport.close();
601 // The ask is SPENT, on whichever of the two branches above got the 480 // The ask is SPENT on whichever branch got the link, so no reconnect can
602 // link: the entry tile's dial happened on the main thread, a picker 481 // start a daemon or print the fallback line onto the alternate screen.
603 // birth's just happened here. Every `redial` below is handed this
604 // copy, so a reconnect can neither start a daemon — `mux d stop` typed
605 // on that box would otherwise be undone by the next backoff, the
606 // poll's bug moved onto a tile — nor print the fallback line onto the
607 // alternate screen the tiles are painted on.
608 if (target == .hand) target.hand.asked = false; 482 if (target == .hand) target.hand.asked = false;
609 // The entry tile's attach carries its rect, so the session is sized to 483 // The entry tile's attach carries its rect, so no second resize follows:
610 // the terminal the tile claims and no second resize follows — 484 // re-asserting a size the daemon just heard costs a snapshot per `mux`.
611 // re-asserting a size the daemon just heard costs one more snapshot on
612 // every `mux`, which is exactly the round trip the convergence must not
613 // add. A view tile attaches at 0x0 (join-only) and `sendAttach` doorbells
614 // its rect behind the attach; that path is inside `sendAttach`.
615 sendAttach(t, &transport, 0, 0) catch { 485 sendAttach(t, &transport, 0, 0) catch {
616 endWith(t, .lost, 1); 486 endWith(t, .lost, 1);
617 return; 487 return;
618 }; 488 };
619 // One question at a time, with the deadline that makes a daemon too old 489 // One question at a time, with the deadline that makes a daemon too old
620 // to have heard it (`sessions_req` is 0x0c) say so instead of swallowing 490 // for `sessions_req` say so instead of swallowing every chord.
621 // every chord for the rest of the session. `client.PendingSwitch`
622 // verbatim — the client asked the same question and this is the same
623 // answer, moved to the thread that owns the link.
624 var pending: client.PendingSwitch = .{}; 491 var pending: client.PendingSwitch = .{};
625 // The ssh-agent channels this tile is serving, one open fd each to this 492 // The ssh-agent channels this tile serves, one open fd each. Table, fds
626 // machine's agent. Everything about them is thread-local: this table, 493 // and frames are all thread-local to this pump.
627 // the fds in it, and the frames that move them all live on this pump.
628 var agent_locals: [proto.agent_chans_max]?AgentLocal = @splat(null); 494 var agent_locals: [proto.agent_chans_max]?AgentLocal = @splat(null);
629 // Every `return` below is this tile going quiet with channels possibly 495 // Every `return` below is this tile going quiet with channels possibly
630 // still open, and the daemon's side of them dies with the transport the 496 // still open; the daemon's side dies with the transport.
631 // defer above closes.
632 defer dropLocals(&agent_locals); 497 defer dropLocals(&agent_locals);
633 498
634 var state: State = .connecting; 499 var state: State = .connecting;
635 // What this tile's paint is worth: while it matches the wall's 500 // What this tile's paint is worth: while it matches the wall's
636 // generation the terminal still holds what this thread drew. 501 // generation the terminal still holds what this thread drew.
637 var painted_gen = t.shared.repaint_gen.load(.acquire); 502 var painted_gen = t.shared.repaint_gen.load(.acquire);
638 // `gone` ends this thread exactly as `running` does — the defers close 503 // `gone` ends this thread exactly as `running` does: the defers free the
639 // the transport, which frees the daemon slot and NOTHING else. The 504 // daemon slot and nothing else. The session goes on running.
640 // session goes on running: "remove is detach".
641 outer: while (t.shared.running.load(.acquire) and !t.gone.load(.acquire)) { 505 outer: while (t.shared.running.load(.acquire) and !t.gone.load(.acquire)) {
642 // The link and the doorbell, then one fd per live agent channel — 506 // The link, the doorbell, then one fd per live agent channel — one
643 // joined into the pump's own poll rather than given a thread each, 507 // poll, because the transport has exactly one owning thread.
644 // because a Transport has exactly one owning thread and these bytes
645 // leave through it. `at` remembers which table slot each of those
646 // trailing fds came from, so a readable one can be traced back to
647 // its channel without a second search.
648 var fdbuf: [3 + agent_locals.len]std.posix.pollfd = undefined; 508 var fdbuf: [3 + agent_locals.len]std.posix.pollfd = undefined;
649 fdbuf[0] = .{ .fd = transport.pollFd(), .events = std.posix.POLL.IN, .revents = 0 }; 509 fdbuf[0] = .{ .fd = transport.pollFd(), .events = std.posix.POLL.IN, .revents = 0 };
650 fdbuf[1] = .{ .fd = t.wake_r, .events = std.posix.POLL.IN, .revents = 0 }; 510 fdbuf[1] = .{ .fd = t.wake_r, .events = std.posix.POLL.IN, .revents = 0 };
651 var nfds: usize = 2; 511 var nfds: usize = 2;
652 // A handoff that stayed on the ssh pipe has a third fd: ssh's 512 // Unread, ssh's stderr fills at 64k and ssh stops talking to the far
653 // stderr. Unread it fills at 64k and ssh stops talking to the far 513 // end — this tile going silent for a reason no frame can explain.
654 // end at all, which is this tile going silent for a reason no
655 // frame can explain.
656 const err_slot: ?usize = if (transport.errFd()) |efd| blk: { 514 const err_slot: ?usize = if (transport.errFd()) |efd| blk: {
657 fdbuf[nfds] = .{ .fd = efd, .events = std.posix.POLL.IN, .revents = 0 }; 515 fdbuf[nfds] = .{ .fd = efd, .events = std.posix.POLL.IN, .revents = 0 };
658 nfds += 1; 516 nfds += 1;
@@ -672,11 +530,8 @@ pub fn pumpTile(t: *Tile) void {
672 if (fdbuf[1].revents != 0) drainWake(t); 530 if (fdbuf[1].revents != 0) drainWake(t);
673 if (err_slot) |s| if (fdbuf[s].revents != 0) transport.drainErr(); 531 if (err_slot) |s| if (fdbuf[s].revents != 0) transport.drainErr();
674 532
675 // The paint offset for this pass: a relayout may have re-cut this 533 // The paint offset for this pass. Snapshot under `paint_mu`, which the
676 // tile's rect, and every paint the pass drives through the Core 534 // keyboard writes `rect` and `label_rows` under, and use it throughout.
677 // has to land in the rect the tile currently owns. Snapshot under
678 // `paint_mu` — the keyboard writes `rect` and `label_rows` under
679 // it — and use the snapshot for the whole pass.
680 const snap = takePass(t); 535 const snap = takePass(t);
681 core.row_off = snap.top + snap.label_rows; 536 core.row_off = snap.top + snap.label_rows;
682 core.col_off = snap.left; 537 core.col_off = snap.left;
@@ -686,57 +541,31 @@ pub fn pumpTile(t: *Tile) void {
686 const snap_view_rows: u16 = snap.rows -| snap.label_rows; 541 const snap_view_rows: u16 = snap.rows -| snap.label_rows;
687 const snap_view_cols: u16 = snap.cols; 542 const snap_view_cols: u16 = snap.cols;
688 543
689 // FOCUS CLAIM. The keyboard moved the focus onto this tile; the 544 // FOCUS CLAIM: the session's mouse modes and side channels go on here,
690 // session's mouse modes and side channels go on here, on the thread 545 // on the thread that owns the transport and the Core.
691 // that owns the transport and the Core. The resize the claim used
692 // to send is gone — the attach already carried the rect, and a
693 // relayout doorbells `resize_pending` for any later change.
694 if (t.claim_pending.swap(false, .acq_rel)) { 546 if (t.claim_pending.swap(false, .acq_rel)) {
695 // A refused claim is re-armed rather than lost: the host 547 // A refused claim is re-armed rather than lost, or the tile stays
696 // picker's popup admits no paint, and a claim writes the 548 // focused holding no terminal. The rest of the PASS still runs:
697 // session's modes through the paint sink, so a claim dropped 549 // `takePass` already cleared the resize this tile owes.
698 // under it leaves this pump focused holding no terminal — no
699 // mouse modes, no side channels, the notice below eaten — until
700 // the user moves the focus away and back. The picker's close
701 // rings this pump, so the retry is a keystroke away.
702 //
703 // The rest of the PASS still runs whatever comes back:
704 // `takePass` has already cleared `resize_pending`, so skipping
705 // out here would drop a relayout this tile owes the daemon.
706 const step = claimFocus(t, &core, .{ .cols = snap_view_cols, .rows = snap_view_rows }); 550 const step = claimFocus(t, &core, .{ .cols = snap_view_cols, .rows = snap_view_rows });
707 // A stale arm — the keyboard moved the focus between arming 551 // A stale arm takes nothing: no claim, no modes, no notice, and
708 // this and this pass — takes nothing: no claim, no modes, no 552 // these predictions are not the focus's to publish.
709 // notice, and these predictions are not the focus's to publish
710 // either. The tile that DOES hold the focus was armed by the
711 // same `setFocus` that took it from this one.
712 focused = step != .dropped; 553 focused = step != .dropped;
713 ever_focused = ever_focused or focused; 554 ever_focused = ever_focused or focused;
714 if (step == .done) { 555 if (step == .done) {
715 // A sentence the keyboard left for whoever owns the terminal 556 // A sentence the keyboard left for whoever owns the terminal
716 // next — a refused `Ctrl-\ c`, so far. Painted here because 557 // next. Taken now, shown once the grid below is up.
717 // a banner belongs to a Core and this is the Core that has
718 // just taken the screen; painted AFTER the repaint below
719 // would be wrong, so it is taken now and shown once the grid
720 // is up.
721 var notice_buf: [96]u8 = undefined; 558 var notice_buf: [96]u8 = undefined;
722 const notice = wv.takeNotice(t.shared, &notice_buf); 559 const notice = wv.takeNotice(t.shared, &notice_buf);
723 // The replica has been hot the whole time, so a claim paints 560 // The replica has been hot, so a claim paints from it NOW:
724 // from it NOW rather than waiting for the daemon's answering 561 // moving the focus costs a local repaint, never a wire frame.
725 // snapshot. That is the headline: moving the focus costs a 562 // Only when there IS one — a blank grid is a screen mux never drew.
726 // local repaint, never a wire frame.
727 // ...but only when there IS one. A tile focused before its
728 // first snapshot — the entry tile, on every `mux` — would
729 // otherwise paint a blank grid over the terminal before the
730 // session has said anything, which is a screen the plain
731 // client never drew and bytes a capture never held.
732 if (core.rep.session_epoch != 0) core.repaint() catch {}; 563 if (core.rep.session_epoch != 0) core.repaint() catch {};
733 if (notice.len > 0) core.banner(notice); 564 if (notice.len > 0) core.banner(notice);
734 } 565 }
735 } 566 }
736 // FOCUS RELEASE. The keyboard moved the focus off this tile and 567 // FOCUS RELEASE: the keyboard wrote the session's release itself under
737 // wrote the session's release itself, under `paint_mu`, before 568 // `paint_mu` before doorbelling, so this pump owes only its own state.
738 // doorbelling — so the handover is ordered and this pump owes only
739 // its own state. `.already_written` is that discipline.
740 if (t.release_pending.swap(false, .acq_rel)) { 569 if (t.release_pending.swap(false, .acq_rel)) {
741 focused = false; 570 focused = false;
742 core.releaseTerminal(.already_written); 571 core.releaseTerminal(.already_written);
@@ -745,14 +574,12 @@ pub fn pumpTile(t: *Tile) void {
745 core.overlay.flush(); 574 core.overlay.flush();
746 } 575 }
747 576
748 // RELAYOUT DOORBELL: this tile's rect changed. The pump is the 577 // RELAYOUT DOORBELL: the pump is the transport's only writer, so
749 // transport's only writer, so relayout sets the flag and the pump 578 // relayout sets the flag and the `.resize` goes out from here.
750 // sends the `.resize` from here.
751 if (snap.resize) { 579 if (snap.resize) {
752 core.overlay.setResizePending(true); 580 core.overlay.setResizePending(true);
753 // The Core clips every paint to its size; a resize the daemon 581 // The Core clips every paint to its size: a resize the daemon hears
754 // hears but the Core does not leaves the bottom of the new 582 // but the Core does not cuts the new grid's bottom off forever.
755 // grid cut off on screen forever.
756 core.adoptSize(.{ .cols = snap_view_cols, .rows = snap_view_rows }); 583 core.adoptSize(.{ .cols = snap_view_cols, .rows = snap_view_rows });
757 transport.writeFrame( 584 transport.writeFrame(
758 .resize, 585 .resize,
@@ -763,26 +590,20 @@ pub fn pumpTile(t: *Tile) void {
763 }; 590 };
764 } 591 }
765 592
766 // `Ctrl-\ d`: hand the daemon its slot back before the process 593 // `Ctrl-\ d`: hand the daemon its slot back before the process dies.
767 // dies, rather than leaving it for the socket's death to be 594 // Only this thread may write the frame, so the keyboard is waiting.
768 // noticed. Only this thread may write the frame, so the keyboard
769 // asked and is waiting for the ack.
770 if (t.detach_req.swap(false, .acq_rel)) { 595 if (t.detach_req.swap(false, .acq_rel)) {
771 transport.writeFrame(.detach, "") catch {}; 596 transport.writeFrame(.detach, "") catch {};
772 t.detach_ack.store(true, .release); 597 t.detach_ack.store(true, .release);
773 wv.ringKeyboard(t.shared); 598 wv.ringKeyboard(t.shared);
774 } 599 }
775 600
776 // A focus chord's question, put on the wire from the thread that 601 // A focus chord's question, from the thread that owns the link. The
777 // owns the link. The ANSWER is read out of the `.sessions_reply` 602 // ANSWER goes to the keyboard, the only thread that may move focus.
778 // arm below and handed to the keyboard, which is the only thread
779 // that may move the focus or grow the wall.
780 const asked: client.SwitchIntent = @enumFromInt(t.ask.swap(0, .acq_rel)); 603 const asked: client.SwitchIntent = @enumFromInt(t.ask.swap(0, .acq_rel));
781 if (asked != .none) { 604 if (asked != .none) {
782 // The deadline starts HERE, when the question goes on the wire, 605 // The deadline starts when the question goes on the wire: it
783 // not when the key was typed: it exists to catch a daemon too 606 // catches a daemon too old to have heard `sessions_req`.
784 // old to have heard `sessions_req`, and the time a chord spent
785 // in the mailbox is not that daemon's silence.
786 pending.arm(asked, std.time.milliTimestamp()); 607 pending.arm(asked, std.time.milliTimestamp());
787 var end_buf: [proto.end_req_max_len]u8 = undefined; 608 var end_buf: [proto.end_req_max_len]u8 = undefined;
788 const sent = switch (asked) { 609 const sent = switch (asked) {
@@ -802,14 +623,8 @@ pub fn pumpTile(t: *Tile) void {
802 }; 623 };
803 } 624 }
804 625
805 // A chord that was never answered. Said with the banner rather than 626 // Said with a banner, not stderr: the terminal is raw on the alternate
806 // stderr for the client's reason: the terminal is in raw mode on 627 // screen. Read before `expired` spends it, so the verb is named right.
807 // the alternate screen and a print there lands mid-grid. Reachable
808 // at all because the poll below is capped at 100ms.
809 //
810 // Read before `expired` spends it: the sentence names the verb the
811 // daemon did not know, and "no session list" over an `x` sends the
812 // user looking at the wrong end of the wire.
813 const waiting = pending.intent; 628 const waiting = pending.intent;
814 if (pending.expired(std.time.milliTimestamp())) 629 if (pending.expired(std.time.milliTimestamp()))
815 core.banner(switch (waiting) { 630 core.banner(switch (waiting) {
@@ -817,22 +632,9 @@ pub fn pumpTile(t: *Tile) void {
817 else => "[no session list: upgrade that daemon]", 632 else => "[no session list: upgrade that daemon]",
818 }); 633 });
819 634
820 // FRAMES BEFORE KEYS, and that order is load-bearing rather than 635 // FRAMES BEFORE KEYS: `Core.forward` splits a wheel notch by the mouse
821 // arbitrary. What the session has already said must be known before 636 // mode a `term_modes` frame carries, so keys judged first read it stale.
822 // what the user is saying is interpreted, because the interpreting 637 // QUIC frames can arrive with the socket never going readable.
823 // depends on it: `Core.forward` splits a wheel notch by whether the
824 // session's application asked for the mouse, and that fact arrives
825 // in a `term_modes` frame. A pass that holds both a readable frame
826 // and a mailbox of keystrokes and drains the mailbox first judges
827 // the keystrokes against a session it has not finished listening
828 // to — the plain client's loop read frames first, and an
829 // application holding the mouse lost its first wheel notch to this
830 // client's scrollback the moment the order was reversed. Found by
831 // the suite, not by reading.
832 //
833 // The `.quic` disjunct is the hub's lesson verbatim: QUIC frames
834 // can arrive from the stream layer with the socket never going
835 // readable.
836 if (fdbuf[0].revents != 0 or transport.link == .quic) frames: { 638 if (fdbuf[0].revents != 0 or transport.link == .quic) frames: {
837 while (true) { 639 while (true) {
838 const incoming = transport.readFrame(alloc) catch return; 640 const incoming = transport.readFrame(alloc) catch return;
@@ -845,20 +647,9 @@ pub fn pumpTile(t: *Tile) void {
845 }, 647 },
846 }; 648 };
847 defer frame.deinit(alloc); 649 defer frame.deinit(alloc);
848 // Everything the frame means to the replica and the screen, 650 // The Core paints at this tile's offset and writes side
849 // in the one place that switch lives. The Core paints at 651 // channels only while it holds the claim — except `.pty_mode`,
850 // this tile's offset through the sink, and writes side 652 // which gates speculation and must be true before any claim.
851 // channels only while it holds the claim — so a tile that
852 // is not focused still keeps its replica hot and its grid
853 // on its rect, but its title and mouse modes reach no
854 // terminal until the focus comes to it.
855 //
856 // The one exception is deliberate and is `.pty_mode`: the
857 // Core feeds the overlay's mode whatever the claim, because
858 // the pty's line discipline is the entire gate on
859 // speculation — a password prompt must never be predicted —
860 // and a claim has to start from the truth rather than
861 // from `.never` and a round trip.
862 const routed = core.frame(frame.type, frame.payload) catch break :frames; 653 const routed = core.frame(frame.type, frame.payload) catch break :frames;
863 switch (routed) { 654 switch (routed) {
864 .skip, .handled => {}, 655 .skip, .handled => {},
@@ -869,17 +660,12 @@ pub fn pumpTile(t: *Tile) void {
869 wv.paintLabel(t, state); 660 wv.paintLabel(t, state);
870 } 661 }
871 }, 662 },
872 // The replica is suspect, not the transport: re-attach 663 // The replica is suspect, not the transport: re-attach at
873 // quoting (0,0) explicitly — a quoted seq would invite 664 // (0,0), since a quoted seq invites an unfixable delta.
874 // the delta that cannot fix us.
875 .resync => { 665 .resync => {
876 core.rep.state_since_attach = false; 666 core.rep.state_since_attach = false;
877 // A resync renames the absolute row space, so a 667 // A resync renames the absolute row space, so a held
878 // held drag now names rows nobody selected — and 668 // drag would copy rows nobody selected.
879 // on an EPOCH change `sel_range` still matches it,
880 // so an in-flight reply would copy the new session's
881 // text. `redial` drops it for this reason; this path
882 // re-attaches without going through it.
883 core.drag.clear(); 669 core.drag.clear();
884 sendAttach(t, &transport, 0, 0) catch return; 670 sendAttach(t, &transport, 0, 0) catch return;
885 }, 671 },
@@ -898,28 +684,19 @@ pub fn pumpTile(t: *Tile) void {
898 return; 684 return;
899 }, 685 },
900 .taken_over => { 686 .taken_over => {
901 // Unsent by this daemon (wire-compat only), but 687 // Unsent by this daemon (wire-compat): somebody took
902 // the plain client had an answer for it and the 688 // the session, so this tile is finished, not redialing.
903 // converged one keeps it: somebody else took the
904 // session, so this tile is finished rather than
905 // reconnecting into a fight over the grid.
906 state = .exited; 689 state = .exited;
907 wv.paintLabel(t, state); 690 wv.paintLabel(t, state);
908 endWith(t, .taken, 0); 691 endWith(t, .taken, 0);
909 return; 692 return;
910 }, 693 },
911 .sessions_reply => { 694 .sessions_reply => {
912 // Gated on the intent this tile's own chord 695 // Gated on the intent this tile's chord armed, and
913 // armed: only a question we asked may move the 696 // SPENT here: one question, one answer.
914 // focus, so an unasked-for reply is ignored. The
915 // intent is SPENT here whichever name it picks
916 // — one question, one answer.
917 var name_buf: [proto.session_name_max]u8 = undefined; 697 var name_buf: [proto.session_name_max]u8 = undefined;
918 // A list is only ever asked for to NAME a new 698 // A list is only asked for to NAME a new session;
919 // session: `n`/`p` walk the wall's own tiles and 699 // `n`/`p` walk the wall's own tiles and ask nothing.
920 // ask nothing, and the end verbs are answered by
921 // `end_reply`. Anything else here is a reply to
922 // a question this tile did not put.
923 const pick: ?[]const u8 = switch (pending.take()) { 700 const pick: ?[]const u8 = switch (pending.take()) {
924 .new => client.nextFreeName(&name_buf, frame.payload), 701 .new => client.nextFreeName(&name_buf, frame.payload),
925 else => null, 702 else => null,
@@ -931,13 +708,7 @@ pub fn pumpTile(t: *Tile) void {
931 _ = pending.take(); 708 _ = pending.take();
932 wv.onEndReply(t, r, std.time.milliTimestamp()); 709 wv.onEndReply(t, r, std.time.milliTimestamp());
933 // An ACCEPTED end says nothing: the hangup's 710 // An ACCEPTED end says nothing: the hangup's
934 // `exit_status` is on its way and the `.exited` 711 // `exit_status` is next, very likely this same pass.
935 // path narrates it, so a banner here would be
936 // the client congratulating itself ahead of the
937 // daemon. Not a `break` either — that exit
938 // status is very likely the next frame in this
939 // same pass, and leaving it for the next poll
940 // is 100ms of a tile that is already gone.
941 if (!r.accepted) { 712 if (!r.accepted) {
942 var b: [96]u8 = undefined; 713 var b: [96]u8 = undefined;
943 core.banner(if (r.others == 0) 714 core.banner(if (r.others == 0)
@@ -952,13 +723,8 @@ pub fn pumpTile(t: *Tile) void {
952 .selection_reply => copySelection(t, alloc, &core, frame.payload), 723 .selection_reply => copySelection(t, alloc, &core, frame.payload),
953 .agent_open => { 724 .agent_open => {
954 const id = proto.decodeAgentId(frame.payload) catch break :frames; 725 const id = proto.decodeAgentId(frame.payload) catch break :frames;
955 // Every refusal is the same answer on the wire — 726 // Every refusal is the same answer on the wire: a
956 // a channel that closes without a byte, which the 727 // channel that closes unspoken, read as "no agent".
957 // far side's ssh reads as "no agent" and gives up
958 // on rather than hanging. Only one of the four
959 // reasons is ordinary — no agent on this machine;
960 // the rest are a daemon asking for something it
961 // should not.
962 const opened = openAgentChan( 728 const opened = openAgentChan(
963 &agent_locals, 729 &agent_locals,
964 id, 730 id,
@@ -978,9 +744,8 @@ pub fn pumpTile(t: *Tile) void {
978 )) break :frames, 744 )) break :frames,
979 .agent_close => { 745 .agent_close => {
980 const id = proto.decodeAgentId(frame.payload) catch break :frames; 746 const id = proto.decodeAgentId(frame.payload) catch break :frames;
981 // Silent, mirroring the daemon: it has already 747 // Silent, mirroring the daemon: it has retired this
982 // retired this id, so an `agent_close` back would 748 // id, so a close back would be an echo.
983 // be an echo it has to learn to ignore.
984 if (findLocal(&agent_locals, id)) |s| { 749 if (findLocal(&agent_locals, id)) |s| {
985 const ch = agent_locals[s].?; 750 const ch = agent_locals[s].?;
986 agent_locals[s] = null; 751 agent_locals[s] = null;
@@ -992,37 +757,10 @@ pub fn pumpTile(t: *Tile) void {
992 else => {}, 757 else => {},
993 }, 758 },
994 } 759 }
995 // Only the socket link guarantees one readable event is 760 // One event is one frame only on the socket link, and one PASS
996 // one frame; QUIC may have buffered more. 761 // is not one frame: a resync is a burst, and a wheel notch
997 // 762 // judged before its trailing `term_modes` is misread. A zero
998 // But "one event, one frame" is not "one pass, one frame". 763 // timeout drains what has ARRIVED and never blocks.
999 // A resync is a BURST — snapshot, pty mode, title, terminal
1000 // modes — and stopping after the first leaves the rest to be
1001 // read a pass at a time, with the user's keystrokes
1002 // interleaved between them. `Core.forward` decides what a
1003 // wheel notch MEANS from the session's terminal modes, so a
1004 // notch judged before the `term_modes` at the END of that
1005 // burst is stolen for this client's scrollback instead of
1006 // reaching the application that asked for the mouse. A
1007 // single-threaded client hid the hazard by being busy
1008 // painting the snapshot when the notch arrived; a keyboard
1009 // on its own thread notices it immediately, and the suite
1010 // caught it two runs out of three.
1011 //
1012 // So: drain what has already ARRIVED. A zero timeout waits
1013 // for nothing, which is what keeps this a drain and not a
1014 // second blocking read.
1015 //
1016 // It cannot starve the rest of the pass, and two measured
1017 // facts are why. The daemon coalesces its deltas, so a
1018 // loud session hands over a bounded burst and `poll(0)`
1019 // finds the socket empty within it rather than a stream
1020 // that refills as fast as it is read. And the keyboard is
1021 // a different thread with its own 400ms bound on the one
1022 // thing it ever waits for a pump to do (`awaitDetach`), so
1023 // even a pump that did stall here could not hold the
1024 // terminal hostage — which is the failure this would
1025 // otherwise have to be argued safe against.
1026 if (transport.link != .quic) { 764 if (transport.link != .quic) {
1027 var more = [_]std.posix.pollfd{ 765 var more = [_]std.posix.pollfd{
1028 .{ .fd = transport.pollFd(), .events = std.posix.POLL.IN, .revents = 0 }, 766 .{ .fd = transport.pollFd(), .events = std.posix.POLL.IN, .revents = 0 },
@@ -1033,34 +771,19 @@ pub fn pumpTile(t: *Tile) void {
1033 } 771 }
1034 } 772 }
1035 773
1036 // The other direction: what this machine's agent answered, back out 774 // What this machine's agent answered, back out as `agent_data`. AFTER
1037 // as `agent_data`. AFTER the frame drain, deliberately — the table 775 // the frame drain, so a channel just closed is already gone.
1038 // this walks is then the one the daemon's latest word left behind,
1039 // so a channel it has just closed is already gone rather than read
1040 // once more on its way out.
1041 for (agent_base..nfds) |i| { 776 for (agent_base..nfds) |i| {
1042 if (fdbuf[i].revents == 0) continue; 777 if (fdbuf[i].revents == 0) continue;
1043 const s = at[i - agent_base]; 778 const s = at[i - agent_base];
1044 const ch = agent_locals[s] orelse continue; 779 const ch = agent_locals[s] orelse continue;
1045 // The id is written into the head of the very buffer the read 780 // The id goes into the head of the read's own buffer, so a frame
1046 // fills, so a frame costs no second copy. The read is capped at 781 // costs no second copy. Capped at the wire's `agent_data_max`.
1047 // the wire's `agent_data_max` because that cap is what keeps one
1048 // busy channel from holding the link against the session's own
1049 // bytes — the same bound the daemon reads its end with.
1050 var buf: [proto.agent_id_len + proto.agent_data_max]u8 = undefined; 782 var buf: [proto.agent_id_len + proto.agent_data_max]u8 = undefined;
1051 buf[0..proto.agent_id_len].* = proto.encodeAgentId(ch.id); 783 buf[0..proto.agent_id_len].* = proto.encodeAgentId(ch.id);
1052 // DONTWAIT, and it is load-bearing rather than belt-and-braces: 784 // DONTWAIT: the poll ran before the frame drain, so this slot may
1053 // the poll above happened before the frame drain, so the daemon 785 // since have been refilled and be merely idle. EOF and a broken
1054 // may since have closed this slot's channel and an `agent_open` 786 // connection are one case — the far side needs to hear either.
1055 // in the same burst may have refilled the slot with a fresh
1056 // connection that has said nothing yet. A blocking read there
1057 // would stop the whole tile — its keystrokes, its paints — on a
1058 // socket that is merely idle. `agent_locals[s]` is re-read above,
1059 // so whatever this does return is attributed to the id that
1060 // actually owns the fd.
1061 //
1062 // EOF and a broken connection are one case: the agent is done
1063 // with this channel either way, and the far side needs to hear so.
1064 const n = std.posix.recv( 787 const n = std.posix.recv(
1065 ch.fd, 788 ch.fd,
1066 buf[proto.agent_id_len..], 789 buf[proto.agent_id_len..],
@@ -1080,10 +803,7 @@ pub fn pumpTile(t: *Tile) void {
1080 } 803 }
1081 804
1082 // Whatever the keyboard left, through everything a plain client's 805 // Whatever the keyboard left, through everything a plain client's
1083 // keystrokes go through: the mouse split, the wheel, alternate 806 // keystrokes go through. Every pump drains its own mailbox.
1084 // scroll, the scrollback view, the prediction and the input frame.
1085 // Only the focused tile's mailbox is ever written, but every pump
1086 // drains its own — an empty mailbox is the common, cheap answer.
1087 var keys_buf: [wv.mailbox_max]u8 = undefined; 807 var keys_buf: [wv.mailbox_max]u8 = undefined;
1088 const keys = takeKeys(t, &keys_buf); 808 const keys = takeKeys(t, &keys_buf);
1089 if (keys.len > 0) { 809 if (keys.len > 0) {
@@ -1094,30 +814,21 @@ pub fn pumpTile(t: *Tile) void {
1094 if (!redial(t, alloc, &core, &transport, target, &state, &agent_locals)) return; 814 if (!redial(t, alloc, &core, &transport, target, &state, &agent_locals)) return;
1095 continue :outer; 815 continue :outer;
1096 } 816 }
1097 // Input is moving again, so "input dropped" has stopped being 817 // Input is moving again, so "input dropped" has stopped being news.
1098 // news. Repainted rather than merely cleared, because the bar 818 // Repainted, because the bar still carries the old sentence.
1099 // is still carrying the old sentence until something draws
1100 // over it — and only when there is a bar, which `paintLabel`
1101 // already decides.
1102 if (t.in_dropped.swap(false, .acq_rel)) wv.paintLabel(t, state); 819 if (t.in_dropped.swap(false, .acq_rel)) wv.paintLabel(t, state);
1103 } 820 }
1104 821
1105 // A prediction the daemon never answered must not sit on the 822 // A prediction the daemon never answered must not sit on screen
1106 // screen forever, and only the clock can say so — no frame will. 823 // forever, and only the clock can say so. Focused tiles only.
1107 // Only while focused: an unfocused tile's rect shows no prediction
1108 // to retire.
1109 if (focused) { 824 if (focused) {
1110 core.idle() catch {}; 825 core.idle() catch {};
1111 publishStats(t.shared, core.overlay.counters); 826 publishStats(t.shared, core.overlay.counters);
1112 } 827 }
1113 828
1114 // A relayout re-cut the stripes (or cleared the screen for an empty 829 // A relayout re-cut the stripes and a quiet session sends nothing to
1115 // wall), and a quiet session sends nothing to trigger a repaint. 830 // trigger a repaint: the generation is what puts the SCREEN back. The
1116 // The replica is current — the SCREEN is not — so the generation is 831 // drag clears too — the rect its anchor was resolved against moved.
1117 // what puts it back. Checked on the poll timeout, so a tile comes
1118 // back within ~100ms of a relayout whether or not its session ever
1119 // speaks again. The drag clears here too: a relayout moved the rect
1120 // a held drag's anchor was resolved against.
1121 if (snap.gen != painted_gen) { 832 if (snap.gen != painted_gen) {
1122 core.drag.clear(); 833 core.drag.clear();
1123 wv.paintLabel(t, state); 834 wv.paintLabel(t, state);
@@ -1129,9 +840,8 @@ pub fn pumpTile(t: *Tile) void {
1129 } 840 }
1130 } 841 }
1131 842
1132 /// A refusal in THIS client's words. `parseEndReply` hands back the frame's 843 /// A refusal in THIS client's words: `parseEndReply` hands back the frame's
1133 /// tail unfiltered and `paintBanner` writes it verbatim, so a peer's 844 /// tail unfiltered, so a peer's escape would run outside the replica.
1134 /// `\x1b]0;..\x07` would run outside the replica. The reasons are constants.
1135 pub fn endRefusal(reason: []const u8) []const u8 { 845 pub fn endRefusal(reason: []const u8) []const u8 {
1136 if (std.mem.eql(u8, reason, proto.end_reason.no_session)) 846 if (std.mem.eql(u8, reason, proto.end_reason.no_session))
1137 return "[no such session on that daemon]"; 847 return "[no such session on that daemon]";