a73x

3af899d9

refactor: the interaction core explains the rule in two lines

a73x   2026-08-30 19:31

Commit message
refactor: the interaction core explains the rule in two lines

96 comment essays to 24. The doc blocks over Core, Claim, the mouse
filter and the terminal setup/teardown pair now state the contract and
the failure; the paragraphs re-deriving which commit found a race, what
an earlier sizing said, and what a probe once measured are gone.

4957 -> 4489 lines, 1693 -> 1225 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
@@ -9,7 +9,7 @@ engine.zig 17
9 flags.zig 2 9 flags.zig 2
10 handoff.zig 26 10 handoff.zig 26
11 hosts.zig 5 11 hosts.zig 5
12 interact.zig 96 12 interact.zig 24
13 keymap.zig 1 13 keymap.zig 1
14 layout.zig 8 14 layout.zig 8
15 main.zig 57 15 main.zig 57
src/tui/interact.zig
Old New
@@ -1,23 +1,15 @@
1 //! The session-interaction core: what happens between a user at a terminal 1 //! What happens between a user at a terminal and one attached session: the
2 //! and one attached session, with the dialling left out — the `Ctrl-\` 2 //! `Ctrl-\` chord layer, the mouse/wheel splitter and alternate scroll,
3 //! chord layer, the mouse/wheel splitter and alternate scroll, speculative 3 //! speculative echo, the side channels a session drives on the host terminal,
4 //! echo, the side channels a session drives on the host terminal (title, 4 //! and the terminal ownership those depend on.
5 //! clipboard, bell, modes), and the terminal ownership those depend on.
6 //! 5 //!
7 //! What does NOT live here is how a transport is BUILT or what a chord 6 //! Not here: how a transport is BUILT (client.zig) or what a chord MEANS —
8 //! MEANS. Targets, dialling, reconnect and the handoff are client.zig's; 7 //! `PrefixFilter` says which action was typed and each driver decides what it
9 //! `PrefixFilter` answers which action was typed and each driver decides 8 //! does. The transport is `anytype`, naming only `writeFrame`, so this loop
10 //! what that action does to its own world. 9 //! is transport-blind and testable with no dial behind it.
11 //! 10 //!
12 //! One driver: a wall tile's pump (`wallview.pumpTile`); every tile holds 11 //! Keeper of two CLAUDE.md invariants: prediction never enters the replica,
13 //! its own transport and replica, and nothing here dials. 12 //! and `replica.zig` is the one applier.
14 //!
15 //! The transport is `anytype` by choice, not by layering: naming only
16 //! `writeFrame(proto.MsgType, []const u8) !void` keeps this loop
17 //! transport-blind and testable with no dial behind it.
18 //!
19 //! Keeper of two CLAUDE.md invariants: prediction never enters the
20 //! replica, and `replica.zig` is the one applier.
21 13
22 const std = @import("std"); 14 const std = @import("std");
23 const Engine = @import("term").engine.Engine; 15 const Engine = @import("term").engine.Engine;
@@ -34,19 +26,13 @@ const select = @import("select.zig");
34 // watches for it with no session and no terminal in the picture. 26 // watches for it with no session and no terminal in the picture.
35 const detach_key = @import("client").keymap.detach_key; 27 const detach_key = @import("client").keymap.detach_key;
36 28
37 /// What `select` answers, in the shape `paint` asks for. 29 /// What `select` answers, in the shape `paint` asks for. The two are siblings
30 /// that may not import one another, so somebody above both joins them.
38 /// 31 ///
39 /// The two are siblings at layer 1 and may not import one another, so 32 /// It CONVERTS as well as adapts: `select` speaks absolute rows and a painter
40 /// somebody above both has to join them; this is the smaller half of that 33 /// speaks grid rows, so the history count of the frame being painted turns one
41 /// join, and the wall reuses it rather than keeping a second copy 34 /// into the other — read from the replica the paint is walking, or the
42 /// (`wallview`'s Core sink). 35 /// highlight lands on another frame's lines.
43 ///
44 /// It converts as well as adapts. `select` speaks ABSOLUTE rows —
45 /// counted from the oldest row the daemon retains — and a painter speaks
46 /// grid rows, so the history count of the frame being painted is what
47 /// turns one into the other. Reading it from the same replica the paint
48 /// is walking is the point: a highlight resolved against one frame's
49 /// history and painted onto another's is a highlight on the wrong lines.
50 pub const Highlight = struct { 36 pub const Highlight = struct {
51 drag: *const select.Drag, 37 drag: *const select.Drag,
52 /// Which tile the drag belongs to. Zero for a focused `Core`, which is 38 /// Which tile the drag belongs to. Zero for a focused `Core`, which is
@@ -68,24 +54,15 @@ pub const Highlight = struct {
68 } 54 }
69 }; 55 };
70 56
71 /// The attached client's keybinding layer: Ctrl-\ selects a command rather 57 /// The attached client's keybinding layer: `Ctrl-\` selects a command rather
72 /// than acting on its own. `d` or a second Ctrl-\ detach, `c` creates a new 58 /// than acting on its own. `d` or a second `Ctrl-\` detach, `c` creates a
73 /// session, `n` and `p` step to the next and previous one, `l` skips to the 59 /// session, `n`/`p` step, `l` skips to the last visited, `w` shows the wall;
74 /// last one visited, `w` shows the full wall; any other 60 /// any other key is dropped with the prefix.
75 /// key is dropped along with the prefix. Dropping is not a loss — a literal
76 /// 0x1c never reached the pty before this layer existed either.
77 ///
78 /// Only what is typed at an established session passes through here: the
79 /// keystrokes `attach` carried across the opening handshake go straight out
80 /// as input (see `carry`), because there was no session to command yet when
81 /// they were typed.
82 /// 61 ///
83 /// Public because the CLI wall's focused tile needs the same layer over the 62 /// Only what is typed at an ESTABLISHED session passes through: keystrokes
84 /// same keys (wallview.zig): a focused tile is a session on this terminal, 63 /// `attach` carried across the handshake go out as input, since there was no
85 /// and a second copy of this table would be a twin that drifts. What each 64 /// session to command yet. Public so the wall's focused tile shares the one
86 /// action MEANS is the caller's — in the wall `.detach` detaches and ends 65 /// table — what each action MEANS is the caller's, which byte spells it is not.
87 /// the run while `.next_session` moves the focus to the next session
88 /// (`wallview`'s run loop) — but which byte spells it is one table.
89 pub const PrefixFilter = struct { 66 pub const PrefixFilter = struct {
90 pub const Dir = enum { left, down, up, right }; 67 pub const Dir = enum { left, down, up, right };
91 68
@@ -170,11 +147,9 @@ pub const PrefixFilter = struct {
170 /// shell. The driver paints; this owns which key means what. 147 /// shell. The driver paints; this owns which key means what.
171 picking: bool = false, 148 picking: bool = false,
172 149
173 /// ssh is waiting on an answer. Over EVERYTHING, `picking` and its 150 /// ssh is waiting on an answer. Over EVERYTHING, `picking` included: a
174 /// editor included: a prompt is not a mode the user chose, it arrived, 151 /// prompt is not a mode the user chose, it arrived. Every byte is the
175 /// and it arrives while whatever was on screen is still on screen. Every 152 /// popup's — a `j` typed at a password box must not reach a shell.
176 /// byte is the popup's, which is `picking`'s rule for `picking`'s
177 /// reason — a `j` typed at a password box must not reach a shell.
178 asking: bool = false, 153 asking: bool = false,
179 /// What ssh said it was asking for. The filter carries it to the 154 /// What ssh said it was asking for. The filter carries it to the
180 /// painter and decides nothing: a secret is starred, a confirmation is 155 /// painter and decides nothing: a secret is starred, a confirmation is
@@ -227,27 +202,16 @@ pub const PrefixFilter = struct {
227 }; 202 };
228 } 203 }
229 204
230 /// Filters one raw stdin chunk in place — the layer only ever removes 205 /// Filters one raw stdin chunk in place — the layer only removes bytes, so
231 /// bytes, so the survivors compact leftwards over the same buffer. 206 /// survivors compact leftwards over the same buffer. An ACTION ends the
232 /// An action ends the chunk: whatever was typed behind it is dropped. 207 /// chunk and whatever was typed behind it is dropped: those bytes were
233 /// For `.detach` that is trivially right (the loop returns), and for a 208 /// typed at the OLD session, and forwarding them puts them in the wrong
234 /// switch it is the only honest answer — those bytes were typed at the 209 /// shell while returning them races a detach already on its way.
235 /// OLD session, so forwarding them to the new one would put them in the
236 /// wrong shell, and sending them back to the old one races the detach
237 /// that is already on its way.
238 ///
239 /// `.wall` re-cuts the wall and unzooms. It ends the chunk like every
240 /// chord above, so bytes typed behind it are dropped for the same
241 /// reason. `.add_tile` is the prompt's Enter: the line typed after
242 /// the picker's `a`, which the same rule ends the chunk on.
243 pub fn feed(self: *PrefixFilter, buf: []u8) Out { 210 pub fn feed(self: *PrefixFilter, buf: []u8) Out {
244 var kept: usize = 0; 211 var kept: usize = 0;
245 // The picker and its editor are MODES, so `s` and `a` do not end 212 // The picker and its editor are MODES, so `s` and `a` do not end the
246 // the read the way a one-shot chord does: the bytes behind them 213 // read: the bytes behind them were typed AT the mode that keystroke
247 // were typed AT the mode the same keystroke opened, and 214 // opened. Remembered, and reported only if nothing later said more.
248 // `\x1c s a --sock ...` in one read has to reach the line editor.
249 // The opens are remembered instead, and reported only if nothing
250 // later in the chunk had more to say.
251 var opened = false; 215 var opened = false;
252 var editing = false; 216 var editing = false;
253 for (buf, 0..) |b, i| { 217 for (buf, 0..) |b, i| {
@@ -260,22 +224,16 @@ pub const PrefixFilter = struct {
260 self.asking = false; 224 self.asking = false;
261 return .{ .forward = buf[0..kept], .action = .{ .ask_answer = self.ask_line[0..self.ask_len] } }; 225 return .{ .forward = buf[0..kept], .action = .{ .ask_answer = self.ask_line[0..self.ask_len] } };
262 }, 226 },
263 // The Esc that heads an arrow key takes its tail with 227 // The Esc that heads an arrow key takes its tail with it:
264 // it, the way the spelling editor's does: the chunk 228 // the chunk ends here, so `[A` never reaches a shell.
265 // ends here, so `[A` never reaches a shell. A prompt is
266 // not a popup you browse, so there is no key it has to
267 // stay open for.
268 0x1b, 0x03 => { 229 0x1b, 0x03 => {
269 self.asking = false; 230 self.asking = false;
270 return .{ .forward = buf[0..kept], .action = .ask_decline }; 231 return .{ .forward = buf[0..kept], .action = .ask_decline };
271 }, 232 },
272 0x7f, 0x08 => self.ask_len -|= 1, 233 0x7f, 0x08 => self.ask_len -|= 1,
273 // High bytes too, unlike the spelling editor's line: a 234 // High bytes too, unlike the spelling editor: a password is
274 // password is bytes and ssh takes any of them, so a 235 // bytes and ssh takes any of them. The star count is per
275 // UTF-8 one must be typeable. They are never an Esc 236 // byte — feedback, not an inventory.
276 // tail. The star count stays per byte, which is a
277 // character count only for ASCII — and a star count is
278 // feedback, not an inventory.
279 0x20...0x7e, 0x80...0xff => if (self.ask_len < askpass.answer_max) { 237 0x20...0x7e, 0x80...0xff => if (self.ask_len < askpass.answer_max) {
280 self.ask_line[self.ask_len] = b; 238 self.ask_line[self.ask_len] = b;
281 self.ask_len += 1; 239 self.ask_len += 1;
@@ -286,11 +244,9 @@ pub const PrefixFilter = struct {
286 } 244 }
287 if (self.prompting) { 245 if (self.prompting) {
288 switch (b) { 246 switch (b) {
289 // Submit and cancel both end the read: an Esc that is 247 // Submit and cancel both end the read: an Esc heading an
290 // the head of an arrow key or a mouse report must take 248 // arrow key must take its tail rather than hand `[A` to the
291 // its tail with it, not hand `[A` to the shell. Ctrl-C 249 // shell, and Ctrl-C is the reflex cancel.
292 // is the reflex cancel and ends it too, so a cancelled
293 // prompt is one behaviour whichever key reached for it.
294 '\r', '\n' => { 250 '\r', '\n' => {
295 self.prompting = false; 251 self.prompting = false;
296 if (self.line_len == 0) return .{ .forward = buf[0..kept], .action = .none }; 252 if (self.line_len == 0) return .{ .forward = buf[0..kept], .action = .none };
@@ -312,21 +268,16 @@ pub const PrefixFilter = struct {
312 if (self.picking) { 268 if (self.picking) {
313 switch (b) { 269 switch (b) {
314 0x1b => { 270 0x1b => {
315 // A terminal writes an arrow key as three bytes of 271 // A terminal writes an arrow key as three bytes of one
316 // one read, so the tail decides which key this Esc 272 // read, so the tail decides which key this Esc was.
317 // was. Nothing is held across reads: a bare Escape 273 // Nothing is held across reads: a bare Escape closes now.
318 // has to close the popup on the press, not on
319 // whatever the user types next.
320 const tail = buf[i + 1 ..]; 274 const tail = buf[i + 1 ..];
321 if (arrowMove(tail)) |d| 275 if (arrowMove(tail)) |d|
322 return .{ .forward = buf[0..kept], .action = .{ .pick_move = d } }; 276 return .{ .forward = buf[0..kept], .action = .{ .pick_move = d } };
323 // A CSI or SS3 head the popup has no key for is not 277 // A CSI or SS3 head the popup has no key for is not an
324 // an Escape: the focused session's mouse modes stay 278 // Escape: mouse modes stay armed under the box, so a
325 // armed under the box, so a wheel notch or a click 279 // wheel notch would shut a popup being read. The tail
326 // arrives as `\x1b[<..M` and would otherwise shut a 280 // leaves too — its digits read as row selections.
327 // popup the user is only reading. The tail leaves
328 // with it — a mouse report's digits, left in the
329 // chunk, read as row selections.
330 if (tail.len > 0 and (tail[0] == '[' or tail[0] == 'O')) 281 if (tail.len > 0 and (tail[0] == '[' or tail[0] == 'O'))
331 return .{ .forward = buf[0..kept], .action = .none }; 282 return .{ .forward = buf[0..kept], .action = .none };
332 self.picking = false; 283 self.picking = false;
@@ -432,41 +383,24 @@ const wheel_rows: u32 = 3;
432 /// Pulls SGR mouse reports out of the stdin stream and turns the wheel ones 383 /// Pulls SGR mouse reports out of the stdin stream and turns the wheel ones
433 /// into scrollback movement. 384 /// into scrollback movement.
434 /// 385 ///
435 /// TWO ROLES, and they are not alternatives. Every tile's `Core` owns a 386 /// TWO ROLES, not alternatives: every tile's `Core` owns one for a focused
436 /// filter for the bytes a focused session's terminal delivers; it runs only 387 /// session's bytes, bypassed entirely while an application in that session
437 /// while no application in that session has asked for the mouse, because 388 /// has asked for the mouse; and the wall owns one to hit-test a press for
438 /// when one has, the bytes are its own and the filter is bypassed entirely 389 /// focus, passing the report bytes on so the drag's Core still sees them.
439 /// (and reset, so a report split across that transition cannot be 390 /// Only the SGR form is recognised — the only form the client asks for.
440 /// half-eaten). The CLI wall owns one more, in its keyboard loop, to
441 /// hit-test a press for focus — the report bytes themselves pass through to
442 /// the focused tile, so the Core that owns the drag sees them.
443 ///
444 /// Only the SGR form (`ESC [ < b ; x ; y M|m`) is recognised, because it is
445 /// the only form the client ever asks its terminal for (`client_mouse_setup`).
446 /// 391 ///
447 /// What it deliberately does NOT do is hold a bare `ESC` or `ESC [` across 392 /// It never holds a bare `ESC` across a read boundary: that is the complete
448 /// a read boundary waiting to see whether a mouse report follows. That 393 /// parse, and it would leave a bare Escape typed in vim sitting here until
449 /// would be the complete parse, and it would cost the one thing a 394 /// the next keystroke. The hold starts at `ESC [ <`, three bytes no keyboard
450 /// multiplexer must never delay: a bare Escape typed in vim would sit here 395 /// produces, so only a split inside those three leaks through as input.
451 /// until the next keystroke. So the hold starts at `ESC [ <` — three bytes
452 /// no keyboard produces — and a report split inside those three bytes
453 /// leaks through as input. A terminal writes a report with one write, and
454 /// the pty delivers up to 16 KiB per read, so that split is a theoretical
455 /// one; a delayed Escape would be an every-session one.
456 pub const MouseFilter = struct { 396 pub const MouseFilter = struct {
457 /// Longest report worth holding: `ESC [ <` plus three parameters. A 397 /// Longest report worth holding: `ESC [ <` plus three parameters. Public
458 /// candidate that outgrows it was never a mouse report. 398 /// because a caller must size `feed`'s buffer at `in.len + max_held`.
459 ///
460 /// Public because a caller has to SIZE the buffer it passes `feed`, and
461 /// the size is `in.len + max_held`.
462 pub const max_held = 24; 399 pub const max_held = 24;
463 400
464 /// One report the filter understood, in this client's coordinates. 401 /// One report the filter understood, in this client's coordinates.
465 /// 402 /// `button` is the SGR word verbatim: what a click MEANS differs between
466 /// `button` is the SGR button word verbatim, modifier bits and all — 403 /// the wall's keyboard and a focused tile, so decoding is the driver's.
467 /// decoding it is the DRIVER's business, because what a click means
468 /// differs between the wall's keyboard and a focused tile and this filter must not
469 /// have to know which it is feeding.
470 pub const Event = struct { 404 pub const Event = struct {
471 pub const Kind = enum { press, motion, release }; 405 pub const Kind = enum { press, motion, release };
472 406
@@ -478,34 +412,19 @@ pub const MouseFilter = struct {
478 /// at the edge beats one at every use. 412 /// at the edge beats one at every use.
479 col: u16, 413 col: u16,
480 row: u16, 414 row: u16,
481 /// How many bytes of `Out.forward` had been emitted when this 415 /// How many bytes of `Out.forward` had been emitted when this report
482 /// report completed — its position among the keys, not its index 416 /// completed — its position among the KEYS, not its index among the
483 /// among the events. 417 /// events. `feed` returns two flat lists, so without this the
484 /// 418 /// interleaving is lost: one read holding `\r` and a click focuses the
485 /// A read carries keys and reports interleaved, and `feed` returns 419 /// tile the click selected rather than the one the user chose.
486 /// them as two flat lists; without this the interleaving is lost.
487 /// The case that costs is one read holding `\r` and a click: at the
488 /// wall, a press moves the focus, so draining all the events first focuses the tile
489 /// the click had just selected rather than the one the user chose.
490 /// Two orderings, one flattening, opposite right answers.
491 ///
492 /// Reports at the same position share an `at`; drivers drain in
493 /// order and the ties are already in the order they arrived.
494 at: usize, 420 at: usize,
495 }; 421 };
496 422
497 /// The most reports one `feed` can produce: the shortest complete SGR 423 /// The most reports one `feed` can produce: the shortest complete SGR
498 /// report is NINE bytes (`ESC [ < 0 ; 1 ; 1 M`), and a held candidate 424 /// report is NINE bytes, and a candidate held from the previous read can
499 /// carried in from the previous read can complete at most one more. 425 /// complete at most one more. Sized rather than coalesced so the filter is
500 /// Sized rather than coalesced so the filter is total — a drag that 426 /// total — a drag that outran a cap would lose its release. The nine is
501 /// outran a cap would lose the release and strand the drag state 427 /// counted: ten leaves the array 181 short of a full chunk.
502 /// machine mid-drag.
503 ///
504 /// The nine is counted, not eyeballed. It read ten here first, which
505 /// left the array 181 short of a full chunk and `feed` writing off the
506 /// end of it — reachable by nothing on this branch, because both
507 /// callers happen to read into `mailbox_max`, and a panic the moment
508 /// anyone fed it the stdin chunk this constant is named for.
509 const max_events = stdin_chunk / 9 + 1; 428 const max_events = stdin_chunk / 9 + 1;
510 429
511 pub const Out = struct { 430 pub const Out = struct {
@@ -526,17 +445,10 @@ pub const MouseFilter = struct {
526 self.len = 0; 445 self.len = 0;
527 } 446 }
528 447
529 /// Filter one raw stdin chunk into `out`, which must have room for 448 /// Filter one raw stdin chunk into `out`, which must hold
530 /// `in.len + max_held` — a candidate held from the previous read is 449 /// `in.len + max_held`: a candidate held from the previous read comes back
531 /// handed back ahead of this chunk's bytes when it turns out not to 450 /// ahead of this chunk when it turns out not to have been a report. Both
532 /// have been a report after all. 451 /// size preconditions are ASSERTED, not merely stated here.
533 ///
534 /// Both size preconditions are asserted rather than left in this
535 /// paragraph. They were prose only while this struct was private with
536 /// one caller; it is public with two now, and `max_events` is derived
537 /// from `stdin_chunk` while both callers happen to read into a buffer
538 /// a quarter that size — exactly the kind of accident that holds until
539 /// someone reads the doc and believes it.
540 pub fn feed(self: *MouseFilter, in: []const u8, out: []u8) Out { 452 pub fn feed(self: *MouseFilter, in: []const u8, out: []u8) Out {
541 std.debug.assert(in.len <= stdin_chunk); 453 std.debug.assert(in.len <= stdin_chunk);
542 std.debug.assert(out.len >= in.len + max_held); 454 std.debug.assert(out.len >= in.len + max_held);
@@ -551,11 +463,9 @@ pub const MouseFilter = struct {
551 wheel += wheelNotches(seq); 463 wheel += wheelNotches(seq);
552 if (decodeEvent(seq)) |ev| { 464 if (decodeEvent(seq)) |ev| {
553 self.events[evs] = ev; 465 self.events[evs] = ev;
554 // `kept` is already past the rewind that started 466 // `kept` is already past the rewind that started this
555 // this candidate — the three bytes of `ESC [ <` 467 // candidate, so this is a position in the FINAL forward
556 // came back out of `forward` when it opened — so 468 // and not a provisional one that later shrinks.
557 // this is the position in the FINAL forward, not a
558 // provisional one that later shrinks.
559 self.events[evs].at = kept; 469 self.events[evs].at = kept;
560 evs += 1; 470 evs += 1;
561 } 471 }
@@ -618,20 +528,16 @@ pub const MouseFilter = struct {
618 } 528 }
619 529
620 /// The wheel movement one complete SGR report means, or 0 for anything 530 /// The wheel movement one complete SGR report means, or 0 for anything
621 /// else — a click, a drag, a release, a horizontal wheel, a button this 531 /// else. Discarding those is the point: with no application asking for the
622 /// terminal invented. Discarding those is the point: with no 532 /// mouse, forwarding one types `[<0;40;12M` into the user's shell.
623 /// application asking for the mouse there is nobody to send them to,
624 /// and forwarding them would type `[<0;40;12M` into the user's shell.
625 fn wheelNotches(seq: []const u8) i32 { 533 fn wheelNotches(seq: []const u8) i32 {
626 // Wheel events are presses; a release cannot be one. 534 // Wheel events are presses; a release cannot be one.
627 if (seq[seq.len - 1] != 'M') return 0; 535 if (seq[seq.len - 1] != 'M') return 0;
628 var it = std.mem.splitScalar(u8, seq[3 .. seq.len - 1], ';'); 536 var it = std.mem.splitScalar(u8, seq[3 .. seq.len - 1], ';');
629 const button = std.fmt.parseInt(u16, it.first(), 10) catch return 0; 537 const button = std.fmt.parseInt(u16, it.first(), 10) catch return 0;
630 // Bit 6 marks the wheel buttons, bit 5 marks motion (a drag with 538 // Bit 6 marks the wheel buttons, bit 5 motion (a drag with the wheel
631 // the wheel held is not a scroll). The low two bits pick which of 539 // held is not a scroll); the low two bits pick vertical from
632 // the four: 0/1 are vertical, 2/3 horizontal and unhandled. The 540 // horizontal. Modifiers are ignored, so Ctrl+wheel scrolls.
633 // modifier bits (shift/meta/ctrl, 4/8/16) are ignored on purpose —
634 // Ctrl+wheel is a font-size change nobody here implements, so it scrolls.
635 if (button & 0x40 == 0 or button & 0x20 != 0) return 0; 541 if (button & 0x40 == 0 or button & 0x20 != 0) return 0;
636 return switch (button & 0x03) { 542 return switch (button & 0x03) {
637 0 => 1, 543 0 => 1,
@@ -666,95 +572,56 @@ pub fn winchRaised() bool {
666 } 572 }
667 573
668 /// This terminal's size, or null when there is no terminal to measure. 574 /// This terminal's size, or null when there is no terminal to measure.
669 /// 575 /// Public because the wall cuts its stripes before it has a Core, and a
670 /// Public because a driver that lays a screen out BEFORE it has a Core to 576 /// second copy would drift on exactly the 0x0 case below.
671 /// measure it needs the same answer: the CLI wall cuts its stripes from the
672 /// terminal at startup, and a second copy of this would be a twin that
673 /// drifts on exactly the 0x0 case below.
674 pub fn ttySize(fd: std.posix.fd_t) ?proto.Size { 577 pub fn ttySize(fd: std.posix.fd_t) ?proto.Size {
675 if (!std.posix.isatty(fd)) return null; 578 if (!std.posix.isatty(fd)) return null;
676 var ws: std.posix.winsize = undefined; 579 var ws: std.posix.winsize = undefined;
677 if (std.os.linux.ioctl(fd, std.os.linux.T.IOCGWINSZ, @intFromPtr(&ws)) != 0) return null; 580 if (std.os.linux.ioctl(fd, std.os.linux.T.IOCGWINSZ, @intFromPtr(&ws)) != 0) return null;
678 // A pty can report 0x0 (e.g. `script` with piped stdin); a zero-sized 581 // A pty can report 0x0 and a zero-sized grid is invalid for the engine,
679 // grid is invalid for the engine. Treat it as "unknown". 582 // so that is "unknown". The floor is the daemon's own, read rather than
680 // 583 // respelled: a size it refuses to move is as unusable as no size at all.
681 // The floor is the daemon's own (`proto.min_session_cols`), read rather
682 // than respelled: a terminal measured below it and quoted is a size the
683 // daemon refuses to move, so "too small to use" and "unknown" get the
684 // same answer here. A second spelling is a second chance to freeze a
685 // thin stripe when the floor moves.
686 if (ws.col < proto.min_session_cols or ws.row < proto.min_session_rows) return null; 584 if (ws.col < proto.min_session_cols or ws.row < proto.min_session_rows) return null;
687 return .{ .cols = ws.col, .rows = ws.row }; 585 return .{ .cols = ws.col, .rows = ws.row };
688 } 586 }
689 587
690 // ---- side channels ----------------------------------------------------- 588 // ---- side channels -----------------------------------------------------
691 //
692 // Everything below turns a typed semantic value into bytes for the host 589 // Everything below turns a typed semantic value into bytes for the host
693 // terminal. Untrusted wire validation belongs to client_core; these adapters 590 // terminal. Untrusted wire validation belongs to client_core; these adapters
694 // only perform the native platform operation selected by that shared core. 591 // only perform the operation that shared core selected.
695 592
696 /// Everything a driver does TO the host terminal to own a SCREEN, in the 593 /// Everything a driver does TO the host terminal to own a SCREEN, in order:
697 /// order it does it: push the title, enter the alternate screen, hide the 594 /// push the title, enter the alternate screen, hide the cursor, disable
698 /// cursor, disable autowrap. Named rather than inline because it is one 595 /// autowrap. One half of a pair — `terminal_teardown` undoes each, and one
699 /// half of a pair — `terminal_teardown` undoes each of these, and the pair 596 /// test pins them together so neither drifts alone. Written once for a
700 /// is pinned together in one test so neither half can drift alone. 597 /// driver's LIFETIME, not per session: the wall holds the screen while focus
701 /// 598 /// moves and a tile writes only `session_claim`. Autowrap goes off so an
702 /// Written once for a driver's whole LIFETIME rather than per session, 599 /// oversized row clips at the right edge instead of shifting the paint down.
703 /// because the driver is the wall (`wall_setup`): it holds the screen while
704 /// focus moves between its tiles, and a tile writes only
705 /// `session_claim`.
706 ///
707 /// Autowrap (`?7l`) goes off with the alternate screen and for its sake: an
708 /// oversized grid row must clip at the right edge rather than wrap and
709 /// shift the whole paint down a line.
710 const terminal_frame_setup = "\x1b[22;0t\x1b[?1049h\x1b[?25l\x1b[?7l"; 600 const terminal_frame_setup = "\x1b[22;0t\x1b[?1049h\x1b[?25l\x1b[?7l";
711 601
712 /// The half that is about holding a SESSION on somebody's terminal: the 602 /// The half about holding a SESSION on somebody's terminal: the mouse modes
713 /// mouse modes its wheel is read out of, and nothing else. A wall tile's 603 /// its wheel is read out of, and nothing else. A tile's focus claim writes
714 /// focus claim writes this first (`Core.claimTerminal`), then the session's own 604 /// this, then the session's own modes on top. One shared constant, so a mode
715 /// modes on top — the wall owns the screen for its whole life and a tile 605 /// added here is added to every claim and one dropped from `session_release`
716 /// may not touch it. 606 /// is dropped from every path that undoes one.
717 /// 607 ///
718 /// Spelled as a shared constant rather than repeated, so the pairing is 608 /// The enables are the CLIENT's own: with no mouse reporting on, a host
719 /// structural: this is the session half and `wall_setup` is the screen 609 /// terminal answers the wheel by synthesising arrow keys (DEC 1007), which
720 /// half, and `session_release` appears verbatim inside every teardown. 610 /// move the shell's history instead of the view.
721 /// A mouse mode added here is added to every claim there is, and one
722 /// dropped from the release is dropped from every path that undoes a claim.
723 ///
724 /// The mouse enables are the CLIENT's own, not a session's: with no mouse
725 /// reporting on, a host terminal answers the wheel by synthesising arrow
726 /// keys on the alternate screen (DEC 1007, "alternate scroll"), which land
727 /// in the session as input and move the shell's history instead of the
728 /// view. Asking for real wheel events is what makes the wheel scrollable at
729 /// all, and it turns 1007's synthesis off as a side effect.
730 const session_claim = client_mouse_setup; 611 const session_claim = client_mouse_setup;
731 612
732 /// The mouse modes the client asks its own terminal for when no application 613 /// The mouse modes the client asks its own terminal for when no application
733 /// in the session wants them: button presses (1000) and motion while a 614 /// in the session wants them: presses (1000) and motion with a button down
734 /// button is down (1002), both reported in SGR (1006). 615 /// (1002), in SGR (1006).
735 ///
736 /// 1002 is held for the whole run rather than armed for the length of a
737 /// drag, because there is no such length to arm it for. Motion reporting
738 /// has to be on BEFORE the press whose drag it reports, and at press time
739 /// nothing here knows whether a drag is coming — an escalation that can
740 /// only be decided after the fact is not an escalation.
741 /// 616 ///
742 /// What that costs is a report per motion event, read and, until something 617 /// 1002 is held for the whole run because there is no drag-length to arm it
743 /// wants one, dropped. That cost is why this set stopped at 1000, and it 618 /// for: motion reporting has to be on BEFORE the press whose drag it reports.
744 /// stopped being decisive when the wall itself became a mouse claimant 619 /// The cost is a report per motion event; nvim with `set mouse=a` pays the
745 /// (`client_mouse_setup`): the reports now have a consumer. The price is also 620 /// same. 1003 stays refused — motion with NO button down answers no question
746 /// one every editor already pays — nvim with `set mouse=a` sets exactly 621 /// this client asks.
747 /// `?1002h` and `?1006h` and holds them for the whole session (measured
748 /// 2026-08-21).
749 /// 622 ///
750 /// 1003 stays refused, and for the reason 1002 no longer is: it reports 623 /// The LIST is the fact, not the escape: `appendMouseModes` level-sets the
751 /// motion with NO button down, which is a report per cursor twitch over the 624 /// same modes, so one spelled here and not there is turned on then off.
752 /// window and answers no question this client asks.
753 ///
754 /// The list, not the escape, is the fact: `client_mouse_setup` writes it at
755 /// startup and `appendMouseModes` level-sets the same modes on every sample,
756 /// so a mode spelled in one place and not the other would be one the client
757 /// turns on and then immediately turns off.
758 const client_mouse_capture = [_]u16{ 1000, 1002, 1006 }; 625 const client_mouse_capture = [_]u16{ 1000, 1002, 1006 };
759 626
760 const client_mouse_setup = blk: { 627 const client_mouse_setup = blk: {
@@ -771,106 +638,43 @@ fn inClientCapture(comptime dec: u16) bool {
771 return false; 638 return false;
772 } 639 }
773 640
774 /// Everything the client must undo on its way out, in one literal. mux 641 /// Everything the client must undo on its way out, in one literal: leaving
775 /// turns these on; leaving any of them set hands the user a terminal that 642 /// any of these set hands the user a terminal that behaves oddly long after
776 /// behaves oddly long after mux exited, with nothing on screen to explain 643 /// mux exited. `?2004l` leads because a SESSION asked for it.
777 /// it. `?2004l` leads because it is the one a session asked for rather
778 /// than one the client needed for itself.
779 ///
780 /// Note what the `?2004l` assumes: it restores 2004 to the terminal's
781 /// power-on default rather than to whatever the outer program had, because
782 /// mux never asked the host what it had. That is correct rather than merely
783 /// tolerable, and by observation rather than by hope — a zsh running under
784 /// a pty writes `?2004h`, `?2004l`, `?2004h`, `?2004l`: it arms bracketed
785 /// paste when zle starts reading and DISARMS it before running each
786 /// command. readline does the same. So for the whole time mux runs as a
787 /// child of the shell that launched it, host 2004 is already off, and off
788 /// is exactly what we put back. A host that armed 2004 and then ran a child
789 /// without disarming would be restored wrongly — no shell in use here does.
790 /// 644 ///
791 /// The title pop (`23;0t`) is the one entry here that restores the user's 645 /// That `?2004l` restores 2004 to the power-on default rather than to what
792 /// OWN value rather than a power-on default — the terminal kept it on its 646 /// the outer program had, because mux never asked. Correct by observation:
793 /// stack, because mux cannot read a title back to restore it by hand. 647 /// zsh and readline both DISARM bracketed paste before running each command,
648 /// so host 2004 is already off for the whole time mux runs.
794 /// 649 ///
795 /// It sits SECOND TO LAST, and that placement is load-bearing even though 650 /// The title pop (`23;0t`) is the one entry restoring the user's OWN value,
796 /// the title stack and the alternate screen have nothing to do with each 651 /// off the terminal's stack, because mux cannot read a title back. It sits
797 /// other. `?1049l` must remain the final bytes a tty client writes: the 652 /// SECOND TO LAST: `?1049l` must remain the final bytes a tty client writes,
798 /// e2e doctored control for the pty capture (test/e2e.sh, tp1) asserts the 653 /// which the e2e's pty capture asserts by dropping exactly that tail.
799 /// capture's last 8 bytes ARE that exit, then drops them so its appended
800 /// row lands INSIDE the alt screen — `render` replays only up to the last
801 /// alt-screen exit, so an append after one changes nothing. Measured, not
802 /// guessed: the plain append rendered byte-identical, i.e. a control that
803 /// could never fire. A teardown that stops ending in `?1049l` now fails
804 /// that tail assertion instead of silently going back to a no-op.
805 ///
806 /// It is here because the question was ANSWERED, not assumed: the operator
807 /// ran the push/set/pop probe in a bare Alacritty window on 2026-08-15 and
808 /// the title returned (commit 217183c, and the design note it edits). A
809 /// terminal without the stack ignores both halves, which costs a title bar
810 /// left showing what the session set — the tmux behaviour, and the
811 /// fallback this would otherwise have shipped as.
812 const terminal_teardown = session_release ++ "\x1b[?7h\x1b[?25h\x1b[23;0t\x1b[?1049l"; 654 const terminal_teardown = session_release ++ "\x1b[?7h\x1b[?25h\x1b[23;0t\x1b[?1049l";
813 655
814 /// Undoes `session_claim`, and everything a SESSION can have asked this 656 /// Undoes `session_claim` and everything a SESSION asked this terminal for
815 /// terminal for while it held it: `?2004l` because the session armed 657 /// while it held it: bracketed paste, and the whole mouse table, since a
816 /// bracketed paste, the whole mouse table because the session's own modes 658 /// focused tile's application can have asked for modes the client never did.
817 /// were mirrored onto this terminal (`appendMouseModes`) and a focused
818 /// tile's application can have asked for modes the client never wanted.
819 ///
820 /// It leads `terminal_teardown` — the exit path — and it is inside
821 /// `wall_teardown`. Every route out of a terminal claim writes these
822 /// bytes; that is the pairing, and one test pins all four.
823 /// 659 ///
824 /// Public because the focus move does not write it from here. A wall's 660 /// Every route out of a terminal claim writes these bytes, and one test pins
825 /// focus change and a tile's pump are different threads, and the release 661 /// all four. Public because the focus move writes it from another thread: the
826 /// has to be ordered against the NEXT tile's claim rather than merely 662 /// release must be ORDERED against the next tile's claim, not merely happen.
827 /// happen — so the thread that moves the focus writes it, before the store
828 /// that lets
829 /// the next pump see the move (wallview's `setFocus`). Exported rather than
830 /// duplicated, for `wall_setup`/`wall_teardown`'s reason: a mouse mode
831 /// added to the claim must not need finding in four places.
832 pub const session_release = "\x1b[?2004l" ++ mouse_teardown; 663 pub const session_release = "\x1b[?2004l" ++ mouse_teardown;
833 664
834 /// What a driver that lends its screen to one session at a time writes on 665 /// What a driver that lends its screen to one session at a time writes on the
835 /// the way in and on the way out. The CLI wall is the driver: it holds the 666 /// way in and out. The wall is the driver: it holds the alternate screen for
836 /// alternate screen for its whole life while focus moves between its 667 /// its whole life, so the screen half is written once and never by a tile.
837 /// tiles, so the screen half is written once here and never by a
838 /// tile.
839 ///
840 /// The teardown is `terminal_teardown` itself, and that identity is the
841 /// design rather than a coincidence. The wall can be left while a tile is
842 /// still holds the terminal — `Ctrl-\ d` ends the run from any state —
843 /// so every mode a session set through that tile (bracketed paste, the
844 /// mouse modes it asked for) has to come off here as well as at the focus
845 /// handover. A wall exit and a client exit are the same
846 /// terminal, restored the same way; the only difference is that the wall
847 /// wrote the screen half once for N sessions.
848 /// 668 ///
849 /// The TITLE is the one thing that is NOT symmetric, deliberately. A 669 /// The teardown IS `terminal_teardown`, by design: `Ctrl-\ d` ends the run
850 /// focused tile sets it and a focus move leaves it standing: the wall pushed 670 /// from any state, so every mode a session set through a tile has to come off
851 /// one title for its whole life and pops it here, so a user who focuses 671 /// here as well as at the focus handover.
852 /// through four sessions sees four titles and gets their own back when the
853 /// wall exits. Restoring at each focus move would need a title to restore TO,
854 /// and mux cannot read one back — the tmux behaviour, and the right one.
855 /// 672 ///
856 /// That push (`22;0t`, "icon name and window title", matching the OSC 0 673 /// The TITLE is deliberately not symmetric. A focus move leaves the session's
857 /// `appendTermTitle` writes) is exactly one deep, and the guarantee was 674 /// title standing; the wall pushed one for its whole life and pops it here.
858 /// checked rather than assumed — an unmatched POP does not restore a 675 /// Restoring per focus move would need a title to restore TO, and mux cannot
859 /// title, it pops whatever the terminal had underneath, which is somebody 676 /// read one back. That push is exactly one deep — an unmatched POP restores
860 /// else's. This write and the pop inside `terminal_teardown` are the only 677 /// somebody else's title — and this write and that pop are the only two.
861 /// two in the tree, and the wall writes each once: one path in, and every
862 /// path out is the same teardown. What escapes is a signal that kills the
863 /// process outright — and that loses `?1049l` and `?25h` with it, leaving
864 /// the user on an alternate screen with no cursor, so a title stack one
865 /// deeper is not the part they will notice. Same exposure as every other
866 /// line of the teardown, not a new one.
867 ///
868 /// The wall no longer claims the mouse itself: the focused tile's
869 /// `claimTerminal` arms `session_claim`, so a click reaches the session
870 /// that asked for it and a click in another tile's rect moves the focus
871 /// there. `session_release` (written by the keyboard on a focus move)
872 /// clears the whole wire table, and the newly focused tile's claim puts
873 /// the modes back — ordered by the `paint_mu` lock, not by a re-arm here.
874 pub const wall_setup = terminal_frame_setup ++ "\x1b[H\x1b[2J"; 678 pub const wall_setup = terminal_frame_setup ++ "\x1b[H\x1b[2J";
875 pub const wall_teardown = terminal_teardown; 679 pub const wall_teardown = terminal_teardown;
876 680
@@ -969,11 +773,9 @@ fn appendTermTitle(
969 try out.append(alloc, 0x07); 773 try out.append(alloc, 0x07);
970 } 774 }
971 775
972 /// `owns_terminal` gates every channel: the claim arms the teardown, so 776 /// `owns_terminal` gates every channel: the claim arms the teardown, so a
973 /// a mode set under `.none` is one nothing undoes. 777 /// mode set under `.none` is one nothing undoes. `append` is declared, not
974 /// 778 /// `anytype`: allocation is a builder's only failure, so empty means refusal.
975 /// `append` is declared, not `anytype`: allocation is a builder's only
976 /// failure, so empty means refusal.
977 fn writeSideChannel( 779 fn writeSideChannel(
978 alloc: std.mem.Allocator, 780 alloc: std.mem.Allocator,
979 stdout_fd: std.posix.fd_t, 781 stdout_fd: std.posix.fd_t,
@@ -990,17 +792,12 @@ fn writeSideChannel(
990 } 792 }
991 793
992 // ---- prediction -------------------------------------------------------- 794 // ---- prediction --------------------------------------------------------
993 // 795 // Nothing below writes to the replica (see the invariants at the top), which
994 // Nothing below writes to the replica — see the invariants at the top of the 796 // is why the replica stays comparable to `mux d dump` at every instant.
995 // file. It is why the replica stays comparable to `mux d dump` at every
996 // instant.
997 797
998 /// What the replica shows at one cell — the `prev_ch` a prediction is 798 /// What the replica shows at one cell — the `prev_ch` a prediction is judged
999 /// judged against later. 799 /// against later. Read at the PREDICTED cursor, not the replica's own:
1000 /// 800 /// mid-burst the wrong one turns "not answered yet" into "contradicted".
1001 /// Read at the PREDICTED cursor, not the replica's own: mid-burst those
1002 /// are different cells, and the wrong one turns "not answered yet" into
1003 /// "contradicted" and flushes the queue.
1004 fn replicaCellChar(alloc: std.mem.Allocator, replica: *Engine, at: predict.CursorPos) u8 { 801 fn replicaCellChar(alloc: std.mem.Allocator, replica: *Engine, at: predict.CursorPos) u8 {
1005 const plain = replica.dumpPlain(alloc) catch return ' '; 802 const plain = replica.dumpPlain(alloc) catch return ' ';
1006 defer alloc.free(plain); 803 defer alloc.free(plain);
@@ -1082,12 +879,9 @@ fn offerKeystroke(
1082 out_fd: std.posix.fd_t, 879 out_fd: std.posix.fd_t,
1083 ) void { 880 ) void {
1084 if (chunk.len != 1) { 881 if (chunk.len != 1) {
1085 // An escape sequence, a multi-byte character, or a paste. None is 882 // An escape, a multi-byte character or a paste: none is one cell's
1086 // one cell's worth of change, and prediction speculates about none 883 // worth of change. Counted HERE, because a paste's lead byte is
1087 // of them. 884 // printable and `predictAt` would speculate on it.
1088 // The decision is made here, so the count is recorded here — a
1089 // paste's lead byte is printable, so handing it to predictAt would
1090 // predict the paste's first character instead of refusing it.
1091 overlay.recordSuppressed(); 885 overlay.recordSuppressed();
1092 return; 886 return;
1093 } 887 }
@@ -1131,12 +925,9 @@ pub const predict_stats_len = 192;
1131 /// already talks to, rather than taking a module edge for one struct. 925 /// already talks to, rather than taking a module edge for one struct.
1132 pub const PredictCounters = predict.Counters; 926 pub const PredictCounters = predict.Counters;
1133 927
1134 /// The `MUX_PREDICT_STATS` line, on the way out. 928 /// The `MUX_PREDICT_STATS` line, on the way out. A tile's Core lives on a
1135 /// 929 /// detached pump the process exit kills where it stands, so the driver that
1136 /// A wall tile's Core lives on a detached pump the process exit kills 930 /// owns the exit prints it — on the normal screen, not a discarded one.
1137 /// where it stands, so the driver that owns the exit prints this from
1138 /// counters the pump published — on the normal screen, not an alternate
1139 /// one about to be discarded.
1140 pub fn dumpPredictStats(c: predict.Counters) void { 931 pub fn dumpPredictStats(c: predict.Counters) void {
1141 const want = std.posix.getenv("MUX_PREDICT_STATS") orelse return; 932 const want = std.posix.getenv("MUX_PREDICT_STATS") orelse return;
1142 if (!std.mem.eql(u8, want, "1")) return; 933 if (!std.mem.eql(u8, want, "1")) return;
@@ -1146,13 +937,9 @@ pub fn dumpPredictStats(c: predict.Counters) void {
1146 } 937 }
1147 938
1148 /// Turn wheel notches into the arrow keys an alt-screen application reads, 939 /// Turn wheel notches into the arrow keys an alt-screen application reads,
1149 /// `wheel_rows` per notch so the wheel moves the same distance either way. 940 /// `wheel_rows` per notch. Sent as input, never predicted — a guess at a
1150 /// 941 /// full-screen application's cursor is about a layout mux cannot see — and
1151 /// Sent as input rather than predicted: a guess painted at the cursor of 942 /// batched, since one frame per arrow would put a hundred on the wire.
1152 /// a full-screen application is about a layout the client cannot see.
1153 ///
1154 /// Batched: a spin arrives as one burst and one frame per arrow would
1155 /// put a hundred frames on the wire.
1156 fn sendAltScroll(transport: anytype, wheel: i32, app_cursor: bool) !void { 943 fn sendAltScroll(transport: anytype, wheel: i32, app_cursor: bool) !void {
1157 const seq = altScrollSeq(wheel, app_cursor); 944 const seq = altScrollSeq(wheel, app_cursor);
1158 var buf: [alt_scroll_batch * 3]u8 = undefined; 945 var buf: [alt_scroll_batch * 3]u8 = undefined;
@@ -1205,32 +992,23 @@ const Pass = enum { carry_on, skip };
1205 /// What a `selection_reply` turned out to be worth — `Core.selectionCopy`'s 992 /// What a `selection_reply` turned out to be worth — `Core.selectionCopy`'s
1206 /// answer, and the whole of what a driver has to decide about. 993 /// answer, and the whole of what a driver has to decide about.
1207 pub const Copy = union(enum) { 994 pub const Copy = union(enum) {
1208 /// Nothing to do, and nothing to say. Somebody else's reply, a reply 995 /// Nothing to do, and nothing to say: somebody else's reply, one whose
1209 /// whose highlight is gone, a selection that came back empty, or a 996 /// highlight is gone, an empty selection, or a refusal the user cannot act
1210 /// refusal the user cannot act on: `.invalid` is this client sending 997 /// on. None is a sentence worth putting over their work.
1211 /// coordinates the daemon could not use, and `.unavailable` is a
1212 /// session with no screen to read — neither is a sentence worth
1213 /// putting over the user's work.
1214 none, 998 none,
1215 /// The selected text, borrowing the frame payload and valid only while 999 /// The selected text, borrowing the frame payload and valid only while
1216 /// it is. 1000 /// it is.
1217 text: []const u8, 1001 text: []const u8,
1218 /// `.ok`, and past what OSC 52 can carry. Said out loud rather than 1002 /// `.ok`, and past what OSC 52 can carry. Said out loud, because
1219 /// dropped: `client_core.validClipboard` refuses an oversized payload 1003 /// `appendHostEffect` refuses by writing NOTHING and a silent stop looks
1220 /// and `appendHostEffect` refuses by writing NOTHING, so a copy that 1004 /// exactly like a copy that worked. Never truncated: the user would find
1221 /// stopped here would look exactly like a copy that worked. Never 1005 /// out when they paste it.
1222 /// truncated either — half a selection is worse than a refused one,
1223 /// because the user finds out when they paste it.
1224 too_large, 1006 too_large,
1225 }; 1007 };
1226 1008
1227 /// What one frame turned out to be, once the Core has done its half of it. 1009 /// What one frame turned out to be, once the Core has done its half. The
1228 /// 1010 /// exhaustive switch over `proto.MsgType` lives in `Core.frame` and nowhere
1229 /// The exhaustive switch over `proto.MsgType` lives in `Core.frame` and 1011 /// else; this is the narrow set a driver still has to act on.
1230 /// nowhere else; this is the narrow set of answers a driver still has to
1231 /// act on. Two drivers wrote that switch between them before this existed
1232 /// and they had already drifted (a short snapshot was a `continue` in one
1233 /// and a broken frame loop in the other).
1234 pub const Routed = enum { 1012 pub const Routed = enum {
1235 /// Done here; the driver's pass carries on. 1013 /// Done here; the driver's pass carries on.
1236 handled, 1014 handled,
@@ -1241,34 +1019,25 @@ pub const Routed = enum {
1241 /// bookkeeping off the first state of an attach — the tile narrates 1019 /// bookkeeping off the first state of an attach — the tile narrates
1242 /// `[up]` on its label bar. 1020 /// `[up]` on its label bar.
1243 state, 1021 state,
1244 /// The replica took state and then refused what arrived, and can no 1022 /// The replica took state and then refused what arrived: it can no longer
1245 /// longer be trusted. The driver answers by re-attaching from scratch, 1023 /// be trusted. The driver re-attaches quoting no seq and no epoch, since
1246 /// quoting no seq and no epoch — the whole problem is that what we hold 1024 /// what it holds is exactly what is untrusted.
1247 /// is untrusted — at whatever size its rect claims
1248 /// (`wallview.sendAttach`).
1249 resync, 1025 resync,
1250 /// Not the Core's. `exit_status` and `taken_over` are the session's 1026 /// Not the Core's: what a lifecycle frame or a session list MEANS is
1251 /// LIFECYCLE and `sessions_reply` is the daemon's session list; what 1027 /// entirely the driver's — an exit ends a client and merely relabels a
1252 /// each MEANS is entirely the driver's — an exit ends a client and 1028 /// tile — so the Core names them and stops. Agent frames join them from
1253 /// merely relabels a tile — so the Core names them and stops. The agent 1029 /// the other end, since the driver holds their local socket.
1254 /// channel frames join them for the same reason from the other end: the
1255 /// driver holds the local socket they belong to.
1256 /// 1030 ///
1257 /// `selection_reply` is here for a narrower reason and is the one the 1031 /// `selection_reply` is the one the driver must call BACK about: the
1258 /// driver must call BACK about: `selectionCopy` correlates it, tests the 1032 /// terminal it copies to is the wall's, and the tile that asked is
1259 /// watermark and refuses an oversized one, but the terminal it copies 1033 /// usually not the focused one.
1260 /// to is the wall's and the tile that asked is usually not the focused
1261 /// one. Same shape as `sessions_reply` and `end_reply`, which the
1262 /// driver reads on the pump that asked.
1263 not_mine, 1034 not_mine,
1264 }; 1035 };
1265 1036
1266 /// Where a Core's paints are allowed to land. 1037 /// Where a Core's paints are allowed to land. A tile's terminal is shared
1267 /// 1038 /// with N stripes on N threads, so every paint asks first and the answer is
1268 /// A wall tile's terminal is shared with N stripes on N threads, so every 1039 /// held until it finishes: a focus move mid-paint would put one session's
1269 /// paint asks first and the answer is held until the paint is finished: a 1040 /// rows on another's screen. Side channels gate on `Core.claim` instead.
1270 /// focus move mid-paint would put one session's rows on another's
1271 /// screen. Side channels are gated on `Core.claim` instead.
1272 pub const Sink = struct { 1041 pub const Sink = struct {
1273 ctx: ?*anyopaque = null, 1042 ctx: ?*anyopaque = null,
1274 /// True when the Core may paint, with whatever lock makes that true 1043 /// True when the Core may paint, with whatever lock makes that true
@@ -1278,77 +1047,40 @@ pub const Sink = struct {
1278 end: ?*const fn (?*anyopaque) void = null, 1047 end: ?*const fn (?*anyopaque) void = null,
1279 }; 1048 };
1280 1049
1281 /// Whether a Core holds a claim on its terminal, and therefore whether 1050 /// Whether a Core holds a claim on its terminal, and so whether there is a
1282 /// there is a teardown to write. 1051 /// teardown to write. A Core never owns a SCREEN — the driver lends one to a
1283 /// 1052 /// session at a time — so a claim is only what a SESSION brings.
1284 /// A Core never owns a SCREEN. The driver that owns one lends it to a
1285 /// session at a time (`wall_setup`), so a claim is only what a SESSION
1286 /// brings — the mouse modes, and whatever the session then asks this
1287 /// terminal for.
1288 /// 1053 ///
1289 /// It is also the flag that arms the undo, which is the load-bearing part: 1054 /// It ARMS the undo, which is the load-bearing part: nothing may write a mode
1290 /// nothing may write a mode to a terminal this is `.none` on, because 1055 /// to a terminal this is `.none` on, because `.none` is the state in which
1291 /// `.none` is exactly the state in which nothing is arranged to unset it. 1056 /// nothing is arranged to unset it. A session that arms bracketed paste and
1292 /// `?2004l` is the case that bites. A session that arms bracketed paste 1057 /// then dies never sends the frame that would clear it, so this teardown is
1293 /// and then dies never sends the `term_modes` frame that would unset it, 1058 /// the only `?2004l` mux is certain to write.
1294 /// so the teardown this flag gates is the only `?2004l` mux is certain to
1295 /// write — and a `?2004h` that went out under `.none` is one nothing will
1296 /// ever undo, handing the user back a terminal that eats their pastes with
1297 /// nothing on screen to say why.
1298 ///
1299 /// It is one flag, not an ordering, and it was a race before it was a
1300 /// flag: the arm used to go out unconditionally and the claim simply
1301 /// always won on a tty. `writeSideChannel` takes the claim and refuses
1302 /// everything while it is false, so the set and its undo are gated on one
1303 /// flag rather than on who ran first — and the title is how the hole the
1304 /// race left open was found; see that function.
1305 pub const Claim = enum { none, session }; 1059 pub const Claim = enum { none, session };
1306 1060
1307 /// One session's interaction state, and everything done to a terminal on 1061 /// One session's interaction state and everything done to a terminal on its
1308 /// its behalf: the replica the daemon's frames are replayed into, the 1062 /// behalf: the replica frames are replayed into, the prediction overlay over
1309 /// prediction overlay drawn on top of it, the chord and mouse filters that 1063 /// it, the chord and mouse filters, and the terminal it happens on.
1310 /// split what the user types, and the terminal this all happens on.
1311 /// 1064 ///
1312 /// How you drive it. The driver owns the transport and the loop; the Core 1065 /// The driver owns the transport and the loop; the Core owns each event:
1313 /// owns what happens at each event:
1314 /// 1066 ///
1315 /// * `initSized` / `deinit` — the Core owns its Engine, its overlay and 1067 /// * `initSized` / `deinit` — the Core owns its Engine, its overlay and
1316 /// whatever terminal claim it still holds. `deinit` puts all three 1068 /// whatever terminal claim it still holds, and puts all three back.
1317 /// back; see there for the order.
1318 /// * `claimTerminal` / `releaseTerminal` — the terminal a tile BORROWS 1069 /// * `claimTerminal` / `releaseTerminal` — the terminal a tile BORROWS
1319 /// for as long as it is focused. Raw mode and the SIGWINCH handler 1070 /// while focused. Raw mode and SIGWINCH belong to the driver.
1320 /// belong to the driver that owns the screen (the wall arms both); a 1071 /// * per pass `idle`; per frame `frame(type, payload)`, which routes and
1321 /// Core only ever holds a session's claim. See `Claim`. 1072 /// returns only what is LEFT, the replica's apply included.
1322 /// * per pass: `idle`. 1073 /// * per read of stdin: the DRIVER's. It feeds its own `PrefixFilter` and
1323 /// * per frame: `frame(type, payload)` routes it and returns what is 1074 /// calls `forward` with the bytes that were not a chord.
1324 /// LEFT (see `Routed`) — the replica's apply included, because a driver
1325 /// that applied on its own would be a second exhaustive switch over the
1326 /// wire, which is what this replaced.
1327 /// * per read of stdin: the driver's, not the Core's. The wall's keyboard
1328 /// is on its own thread, so it feeds its own `PrefixFilter`, acts on
1329 /// the action, and calls `forward` with the bytes that were not a
1330 /// chord — everything below the chord layer (mouse, wheel, scrollback,
1331 /// prediction) is in `forward`.
1332 /// * around a reconnect: `dropScrollView` before, `reattached` after. 1075 /// * around a reconnect: `dropScrollView` before, `reattached` after.
1333 /// 1076 ///
1334 /// What it depends on: an Engine, a Replica, the prediction overlay, the 1077 /// It depends on an Engine, a Replica, the overlay, the painter and the
1335 /// painter, and the shared semantic decoder — never a transport type. Every 1078 /// shared decoder — never a transport type. Nothing here is a singleton: a
1336 /// method that writes takes the transport as `anytype` and calls exactly 1079 /// tile brings its own Core, so there is never a second applier for one tile.
1337 /// one thing on it, `writeFrame`.
1338 ///
1339 /// Nothing here is a singleton. A wall tile brings its own transport and
1340 /// its own Core, one per tile from birth — there is never a second replica
1341 /// or a second applier for one tile. `out_fd`/`size` say where a Core
1342 /// paints and `sink` says whether it may right now; a gone tile's says
1343 /// no, and the wall paints its dead-tile label instead.
1344 pub const Core = struct { 1080 pub const Core = struct {
1345 /// The tile index a `Core` selects under: it is the only 1081 /// The tile index a `Core` selects under: it is the only session on its
1346 /// session on its rect, so there is exactly one. Named rather than 1082 /// rect, so there is exactly one. Named because three sites must agree —
1347 /// written three times because the drag machine's `tile` is what 1083 /// `hitTest` stamps it, `highlight` passes it, `paintDragChange` reads it.
1348 /// confines a selection to one pane — `hitTest` stamps it, `highlight`
1349 /// hands it to the painter, and `paintDragChange` decides which rows
1350 /// changed with it. Three literals that must agree, and a highlight
1351 /// with holes in it if they ever stopped.
1352 const focus_tile: usize = 0; 1084 const focus_tile: usize = 0;
1353 1085
1354 alloc: std.mem.Allocator, 1086 alloc: std.mem.Allocator,
@@ -1378,11 +1110,9 @@ pub const Core = struct {
1378 /// one-tile wall owns every row; a tile among neighbours does not. Set 1110 /// one-tile wall owns every row; a tile among neighbours does not. Set
1379 /// by the driver that knows the layout, not inferred from `row_off`. 1111 /// by the driver that knows the layout, not inferred from `row_off`.
1380 owns_screen: bool = true, 1112 owns_screen: bool = true,
1381 /// The replay core (replica.zig). Public because the driver's own 1113 /// The replay core (replica.zig). Public because the driver reads it: a
1382 /// business reads it — a reconnect quotes `last_seq`/`session_epoch`, 1114 /// reconnect quotes `last_seq`/`session_epoch`, and `state_since_attach`
1383 /// and a refusal is told from a shell exiting by `state_since_attach`. 1115 /// tells a refusal from a shell exiting. The Core owns the Engine.
1384 /// The Core owns the Engine underneath it; the Replica only borrows it,
1385 /// so `deinit` here is the one owner.
1386 rep: Replica, 1116 rep: Replica,
1387 /// Speculative echo. Born `.never` and stays there until a daemon tells 1117 /// Speculative echo. Born `.never` and stays there until a daemon tells
1388 /// it otherwise, so an old daemon that has never heard of pty_mode gets 1118 /// it otherwise, so an old daemon that has never heard of pty_mode gets
@@ -1404,24 +1134,19 @@ pub const Core = struct {
1404 /// link carries one conversation, and `client_core` correlates against 1134 /// link carries one conversation, and `client_core` correlates against
1405 /// the pending one it was handed. 1135 /// the pending one it was handed.
1406 sel_id: u32 = 0, 1136 sel_id: u32 = 0,
1407 /// `history_rows` as of the request now in flight, and the whole of the 1137 /// `history_rows` as of the request in flight, and the whole staleness
1408 /// staleness test. Absolute rows count from the OLDEST RETAINED row, so 1138 /// test: an eviction between ask and answer renames the coordinate space
1409 /// an eviction between the ask and the answer renames the coordinate 1139 /// and yields text that is `.ok` and not what was highlighted. Only a
1410 /// space and yields text that is `.ok`, valid UTF-8, and not what was 1140 /// LOWER count is evidence — output raises it without moving row zero.
1411 /// highlighted. Ordinary output RAISES the count without moving row
1412 /// zero, so only a LOWER one is evidence (`protocol.SelectionReply`).
1413 sel_watermark: u32 = 0, 1141 sel_watermark: u32 = 0,
1414 /// The selection the request was taken from, compared against what is 1142 /// The selection the request was taken from, compared against what is
1415 /// still held when the answer comes back. A reply that outlived its own 1143 /// still held when the answer comes back. A reply that outlived its own
1416 /// highlight — a relayout, a forget, a focus move, a resync — is text for 1144 /// highlight — a relayout, a forget, a focus move, a resync — is text for
1417 /// rows nobody is looking at any more. 1145 /// rows nobody is looking at any more.
1418 sel_range: ?select.Range = null, 1146 sel_range: ?select.Range = null,
1419 /// Scroll mode: 0 = live; N = viewing the screenful whose bottom sits N 1147 /// Scroll mode: 0 = live; N = the screenful whose bottom sits N rows above
1420 /// rows above live. Rows rather than pages because the wheel moves by a 1148 /// live. Rows, not pages: the wheel moves a few lines and the keys move a
1421 /// few lines and the keys move by a screen — one unit that expresses 1149 /// screen. View state, not replay state, so it stays out of the Replica.
1422 /// both, and `fetch_scrollback` already addresses absolute rows.
1423 /// View state, not replay state, so it stays here rather than in the
1424 /// Replica (which holds grid/seq/epoch/history — see replica.zig).
1425 scroll_rows: u32 = 0, 1150 scroll_rows: u32 = 0,
1426 /// What this Core holds on the terminal, and therefore what it may 1151 /// What this Core holds on the terminal, and therefore what it may
1427 /// write there and what it must undo. See `Claim`. 1152 /// write there and what it must undo. See `Claim`.
@@ -1430,15 +1155,9 @@ pub const Core = struct {
1430 /// does. Set by a driver that shares its terminal; see `Sink`. 1155 /// does. Set by a driver that shares its terminal; see `Sink`.
1431 sink: Sink = .{}, 1156 sink: Sink = .{},
1432 /// Whether `deinit` is the right place to print the prediction stats. 1157 /// Whether `deinit` is the right place to print the prediction stats.
1433 /// 1158 /// False for a tile's Core, which lives on a detached pump: `deinit` is
1434 /// True for a Core whose driver ends by returning through it. False for 1159 /// not reliably reached, and when it is it prints onto a screen about to
1435 /// a wall tile's, which lives on a detached pump: the process exit 1160 /// be discarded and then a second time from the driver that owns the exit.
1436 /// kills that thread where it stands, so `deinit` is not reliably
1437 /// reached at all — and when it IS (a session that exited), it would
1438 /// print onto an alternate screen the wall is about to discard, and
1439 /// then print a second time from the driver that actually owns the
1440 /// exit. The wall publishes its counters instead; see
1441 /// `dumpPredictStats`.
1442 owns_stats: bool = true, 1161 owns_stats: bool = true,
1443 /// The first paint after a reconnect must be a full one, so the 1162 /// The first paint after a reconnect must be a full one, so the
1444 /// [reconnecting] banner goes away with everything else now stale. 1163 /// [reconnecting] banner goes away with everything else now stale.
@@ -1474,14 +1193,10 @@ pub const Core = struct {
1474 /// the driver prints on its way out land on a terminal already out of 1193 /// the driver prints on its way out land on a terminal already out of
1475 /// this session's modes. 1194 /// this session's modes.
1476 pub fn deinit(self: *Core) void { 1195 pub fn deinit(self: *Core) void {
1477 // Only undo what was actually claimed, and a claim is a SESSION's 1196 // Only undo what was claimed: the screen belongs to the driver, which
1478 // modes and nothing more: the screen belongs to the driver, which 1197 // is still using it. `.write` always — this is the path a pump takes
1479 // is still on it and still using it after this tile's pump has 1198 // when its SESSION ends while holding the terminal, so nobody else
1480 // gone. 1199 // wrote the release.
1481 // `.write`, always. This is the path a pump takes when its SESSION
1482 // ends while its tile holds the terminal — nobody moved the focus, so nobody else wrote
1483 // the release, and the wall goes on running with the terminal still
1484 // in that session's modes.
1485 self.releaseTerminal(.write); 1200 self.releaseTerminal(.write);
1486 if (self.owns_stats) dumpPredictStats(self.overlay.counters); 1201 if (self.owns_stats) dumpPredictStats(self.overlay.counters);
1487 self.overlay.deinit(); 1202 self.overlay.deinit();
@@ -1493,18 +1208,11 @@ pub const Core = struct {
1493 self.size = size; 1208 self.size = size;
1494 } 1209 }
1495 1210
1496 /// Take the terminal for this session ALONE — the wall's claim. 1211 /// Take the terminal for this session ALONE. Under the SINK for ORDER,
1497 /// 1212 /// not painting: the wall writes the previous holder's release under the
1498 /// Taken under the SINK for ORDER, not painting: the wall writes the 1213 /// same lock, and a claim outside it can land AFTER that release, leaving
1499 /// previous holder's release under the same lock, before the store that 1214 /// modes nothing undoes. The session's own modes go on top, because a
1500 /// makes the handover visible (wallview's `setFocus`). A claim taken 1215 /// claim's resize is answered by `resyncSnapshot`, which carries none.
1501 /// outside it can land AFTER that release, leaving modes nothing
1502 /// undoes. `false` is a claim the focus moved out from under.
1503 ///
1504 /// The session's own modes go on top because a claim's resize is
1505 /// answered by `resyncSnapshot`, which carries no modes: otherwise a
1506 /// tile whose application asked for the mouse would hold a terminal
1507 /// that never heard about it.
1508 pub fn claimTerminal(self: *Core) bool { 1216 pub fn claimTerminal(self: *Core) bool {
1509 if (!self.is_tty or self.claim != .none) return false; 1217 if (!self.is_tty or self.claim != .none) return false;
1510 if (!self.beginPaint()) return false; 1218 if (!self.beginPaint()) return false;
@@ -1526,42 +1234,25 @@ pub const Core = struct {
1526 return true; 1234 return true;
1527 } 1235 }
1528 1236
1529 /// Who writes the undo when a claim is given up. 1237 /// Who writes the undo when a claim is given up. `.already_written`
1530 /// 1238 /// exists because a shared terminal's handover must be ORDERED: the wall
1531 /// `.write` is the ordinary answer and the only one a Core can reach on 1239 /// writes the release on the thread that moves the focus. An argument
1532 /// its own. `.already_written` exists because a shared terminal's 1240 /// rather than a second method, so every call site has to say which —
1533 /// handover has to be ORDERED, not merely eventual: the wall's focus 1241 /// the wrong one is a terminal left reporting clicks.
1534 /// change writes the release itself, on the thread that moves the
1535 /// focus, before the store that lets the next tile's pump see the move.
1536 /// The outgoing pump then finds the bytes already gone and
1537 /// has only its own state left to drop. Spelled as an argument rather
1538 /// than a second method so every call site has to say which it is —
1539 /// silently taking the wrong one is a terminal left reporting clicks,
1540 /// or two releases racing a claim.
1541 pub const Undo = enum { write, already_written }; 1242 pub const Undo = enum { write, already_written };
1542 1243
1543 /// Give the terminal back: the release, and every other way out. 1244 /// Give the terminal back: the release, and every other way out. Nothing
1544 /// 1245 /// goes on the WIRE, but plenty comes off the terminal — a wall left
1545 /// Nothing goes on the WIRE — the release is client-local — but plenty 1246 /// reporting clicks into the user's shell is what this pairs against.
1546 /// comes off the terminal, because the session that held it set 1247 /// The scroll view goes with it either way, or the resuming stripe comes
1547 /// modes on it. A wall left still reporting clicks into the user's 1248 /// back showing nothing. Idempotent, and the teardown follows the claim.
1548 /// shell is the failure this pairs against.
1549 ///
1550 /// The scroll view goes with it whichever way the undo went: the
1551 /// stripe that resumes paints live state from the same replica, so a
1552 /// Core still suppressing paints would come back to a tile showing
1553 /// nothing.
1554 ///
1555 /// Idempotent, and it says which teardown by what was claimed.
1556 pub fn releaseTerminal(self: *Core, undo: Undo) void { 1249 pub fn releaseTerminal(self: *Core, undo: Undo) void {
1557 const held = self.claim; 1250 const held = self.claim;
1558 self.claim = .none; 1251 self.claim = .none;
1559 self.dropScrollView(); 1252 self.dropScrollView();
1560 // The highlight goes with the screen it was drawn on. A tile that 1253 // The highlight goes with the screen it was drawn on: kept, it would
1561 // lost focus paints nothing, so an inversion kept here would be 1254 // reappear over rows chosen in another session's lifetime, and a reply
1562 // invisible until the tile was focused again and then reappear over rows the 1255 // still in flight would copy that text.
1563 // user chose in another session's lifetime — and a reply still in
1564 // flight would copy text for them.
1565 self.drag.clear(); 1256 self.drag.clear();
1566 if (undo == .already_written) return; 1257 if (undo == .already_written) return;
1567 switch (held) { 1258 switch (held) {
@@ -1603,11 +1294,8 @@ pub const Core = struct {
1603 } 1294 }
1604 1295
1605 /// The whole screen from the replica — a local repaint at zero round 1296 /// The whole screen from the replica — a local repaint at zero round
1606 /// trips, the replica hot the whole time the tile has been painting its 1297 /// trips. The overlay goes back on top, because the rows just drawn have
1607 /// rect. 1298 /// overwritten predictions still outstanding.
1608 ///
1609 /// The overlay goes back on top: the rows just drawn have overwritten
1610 /// predictions still outstanding.
1611 pub fn repaint(self: *Core) !void { 1299 pub fn repaint(self: *Core) !void {
1612 if (!self.beginPaint()) return; 1300 if (!self.beginPaint()) return;
1613 defer self.endPaint(); 1301 defer self.endPaint();
@@ -1625,28 +1313,15 @@ pub const Core = struct {
1625 return .{ .x = c.x + vp.left, .y = c.y + vp.top }; 1313 return .{ .x = c.x + vp.left, .y = c.y + vp.top };
1626 } 1314 }
1627 1315
1628 /// Repaint the rows a drag report changed, and only those. 1316 /// Repaint the rows a drag report changed, and only those: the anchor
1629 /// 1317 /// does not move, so only the rows between the two ends can change. Every
1630 /// The anchor does not move, so a report that moved the active end 1318 /// row is still ASKED rather than reasoned about — a wrong answer is a
1631 /// changes the span on the rows between the two ends and on no others. 1319 /// highlight with a hole in it. Painting the whole screen instead cost
1632 /// Every row is still asked rather than reasoned about: the question is 1320 /// 4.8 KB a cell at 120x40. The overlay goes back on top, as in `repaint`.
1633 /// arithmetic, and a wrong answer here is a highlight with a hole in it.
1634 ///
1635 /// Painting the whole screen instead cost the screen's bytes for every
1636 /// cell the pointer crossed — 4.8 KB a cell at 120x40, so 288 KB to
1637 /// drag across sixty of them.
1638 ///
1639 /// The overlay goes back on top for the same reason `repaint` puts it
1640 /// there: a drag abandons no predictions, so any still outstanding have
1641 /// to survive the rows this redrew.
1642 fn paintDragChange(self: *Core, was: ?select.Range) !void { 1321 fn paintDragChange(self: *Core, was: ?select.Range) !void {
1643 // Scroll mode owns the screen: `renderScrollback` blits the 1322 // Scroll mode owns the screen: `renderScrollback` blits VT bytes with
1644 // daemon's VT bytes with no engine behind them, so a row painted 1323 // no engine behind them, so a row from the live replica lands on a
1645 // from the live replica lands on a page this Core cannot address. 1324 // page this Core cannot address. Leaving scroll mode repaints in full.
1646 // Reachable in three actions — select, wheel up, click — because
1647 // `hitTest` refuses while scrolled and a press with no hit CLEARS
1648 // the held range, which is a change like any other. Leaving scroll
1649 // mode repaints in full, so the highlight comes back either way.
1650 if (self.scroll_rows > 0) return; 1325 if (self.scroll_rows > 0) return;
1651 const now = self.drag.range(); 1326 const now = self.drag.range();
1652 if (std.meta.eql(was, now)) return; 1327 if (std.meta.eql(was, now)) return;
@@ -1705,11 +1380,9 @@ pub const Core = struct {
1705 } 1380 }
1706 } 1381 }
1707 1382
1708 /// The replica has taken a snapshot: tell the overlay, rebuild under 1383 /// The replica has taken a snapshot: tell the overlay, rebuild under it.
1709 /// it. 1384 /// A snapshot answers a resize and ends a reconnect; neither says a
1710 /// 1385 /// prediction was WRONG, only that we can no longer find out — so the
1711 /// A snapshot answers a resize and ends a reconnect. Neither says a
1712 /// prediction was wrong — it says we can no longer find out, so the
1713 /// queue goes and the counters do not move. 1386 /// queue goes and the counters do not move.
1714 fn snapshotTaken(self: *Core) !void { 1387 fn snapshotTaken(self: *Core) !void {
1715 self.overlay.setGrid(self.rep.grid.cols, self.rep.grid.rows); 1388 self.overlay.setGrid(self.rep.grid.cols, self.rep.grid.rows);
@@ -1740,22 +1413,12 @@ pub const Core = struct {
1740 if (self.scroll_rows == 0 and self.beginPaint()) { 1413 if (self.scroll_rows == 0 and self.beginPaint()) {
1741 defer self.endPaint(); 1414 defer self.endPaint();
1742 if (self.repaint_after_resync or verdict == .contradicted) { 1415 if (self.repaint_after_resync or verdict == .contradicted) {
1743 // First frame back after a reconnect. The daemon sent only 1416 // First frame back after a reconnect: the daemon sent only
1744 // what changed, which is correct — but the screen still 1417 // what changed, but the screen still carries the banner. A
1745 // carries the banner, so repaint the whole thing from the 1418 // contradiction takes the same route — the queue has just been
1746 // replica instead. 1419 // abandoned, and a full repaint is the rollback that is
1747 // 1420 // certainly right. Painted raw, not through `paintFull`: this
1748 // A contradiction (or an expiry) takes the same route: the 1421 // and the overlay below are ONE hold of a non-reentrant sink.
1749 // whole queue has just been abandoned, and repainting
1750 // everything from the replica is the simplest rollback that
1751 // is certainly right. It is affordable precisely because
1752 // reconcile v2 made contradictions rare — a burst outrunning
1753 // the round trip is no longer one.
1754 //
1755 // Painted raw rather than through `paintFull`: this arm and
1756 // the overlay below it are ONE hold of the sink (a focus move
1757 // between them would put the two halves of this frame
1758 // on two different screens), and the sink is not reentrant.
1759 const hl = self.highlight(); 1422 const hl = self.highlight();
1760 try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), null, self.owns_screen, self.out_fd); 1423 try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), null, self.owns_screen, self.out_fd);
1761 self.repaint_after_resync = false; 1424 self.repaint_after_resync = false;
@@ -1770,11 +1433,9 @@ pub const Core = struct {
1770 } 1433 }
1771 } 1434 }
1772 1435
1773 /// The session handed the terminal back and forth, or took it over. 1436 /// The session handed the terminal back and forth, or took it over. Mode
1774 /// 1437 /// churn is ordinary — readline does it around every command — so the
1775 /// Mode churn is ordinary — readline does it around every command — so 1438 /// repaint is spent only when the flush took something off the screen.
1776 /// the repaint is spent only when the flush actually took something off
1777 /// the screen.
1778 fn ptyModeChanged(self: *Core, payload: []const u8) !Pass { 1439 fn ptyModeChanged(self: *Core, payload: []const u8) !Pass {
1779 const flags = proto.decodePtyMode(payload) catch return .skip; 1440 const flags = proto.decodePtyMode(payload) catch return .skip;
1780 const had_pending = self.overlay.pendingCount() > 0; 1441 const had_pending = self.overlay.pendingCount() > 0;
@@ -1798,22 +1459,14 @@ pub const Core = struct {
1798 return .carry_on; 1459 return .carry_on;
1799 } 1460 }
1800 1461
1801 /// A term_event or term_modes frame: decoded by the shared core, then 1462 /// A `term_event` or `term_modes` frame: decoded by the shared core, then
1802 /// rendered onto the host terminal by this platform's adapters. 1463 /// rendered by this platform's adapters. The DECODE happens whatever the
1803 /// 1464 /// sink says and the WRITE does not — an unfocused tile tracks its
1804 /// The decode happens whatever the sink says and the WRITE does not, 1465 /// session's modes for the claim that will need them.
1805 /// which is the same split the claim already makes: a wall tile that is not focused
1806 /// tracks its session's modes for the focus claim that will need them (see
1807 /// `claimTerminal`) and puts nothing on a terminal it does not hold.
1808 /// 1466 ///
1809 /// Under the sink for a reason the claim gate does NOT cover, and the 1467 /// The claim answers ORDER, the sink answers ATOMICITY: a 64 KiB OSC 52
1810 /// two are worth keeping apart. The claim answers ORDER — may these 1468 /// payload leaves here as a write loop, and another thread's screen clear
1811 /// bytes exist on this terminal at all. The sink answers ATOMICITY — an 1469 /// spliced into it leaves the terminal hunting for a string terminator.
1812 /// OSC 52 clipboard payload runs to 64 KiB and leaves here as a write
1813 /// loop, so another thread's screen clear spliced into the middle of it
1814 /// leaves the terminal hunting for a string terminator and eating
1815 /// everything painted after. Short escapes were never the hazard; the
1816 /// one unbounded side channel is.
1817 fn semanticFrame(self: *Core, frame_type: proto.MsgType, payload: []const u8) !void { 1470 fn semanticFrame(self: *Core, frame_type: proto.MsgType, payload: []const u8) !void {
1818 const decoded = self.semantic.receive(frame_type, payload); 1471 const decoded = self.semantic.receive(frame_type, payload);
1819 if (!self.beginPaint()) return; 1472 if (!self.beginPaint()) return;
@@ -1821,12 +1474,9 @@ pub const Core = struct {
1821 switch (decoded) { 1474 switch (decoded) {
1822 .ignored => {}, 1475 .ignored => {},
1823 .state => |state| { 1476 .state => |state| {
1824 // Mode samples are deliberately not deduplicated. 1477 // Mode samples are deliberately not deduplicated: reasserting
1825 // sendResync repeats the current level on attach, reconnect 1478 // DECSET/DECRST 2004 is a harmless level-set that restores the
1826 // and forced re-attach; reasserting DECSET/DECRST 2004 is a 1479 // host after a new connection. Occurrences take the arm below.
1827 // harmless level-set and restores the host after a new
1828 // connection. Occurrence effects take the separate arm below
1829 // and are never covered by this repeat policy.
1830 try writeSideChannel( 1480 try writeSideChannel(
1831 self.alloc, 1481 self.alloc,
1832 self.out_fd, 1482 self.out_fd,
@@ -1846,21 +1496,15 @@ pub const Core = struct {
1846 ), 1496 ),
1847 // Nothing to write: a `.reply` answers something this client 1497 // Nothing to write: a `.reply` answers something this client
1848 // ASKED for, and the one there is goes out through 1498 // ASKED for, and the one there is goes out through
1849 // `selectionCopy`, which the driver calls with the selection 1499 // `selectionCopy`. Named, so a second kind of reply is an edit here.
1850 // still on screen. Nothing routes a reply here. Named rather
1851 // than swept so that a second kind of reply is an edit here.
1852 .reply => {}, 1500 .reply => {},
1853 } 1501 }
1854 } 1502 }
1855 1503
1856 /// The session's window title. 1504 /// The session's window title. Repeats are expected — every attach
1857 /// 1505 /// resends it — and harmless: setting a title to the value it already
1858 /// Repeats are expected here for the same reason as term_modes above — 1506 /// holds has no counter or stack behind it, and the daemon never sends
1859 /// every attach resends the title — and are harmless for a stronger 1507 /// an empty one, so a repeat cannot clear what the user is looking at.
1860 /// reason: setting a window title to the value it already holds is a
1861 /// no-op with no counter or stack behind it. Note the daemon never
1862 /// sends an empty one, so a repeat can never clear a title the user is
1863 /// looking at.
1864 fn titleFrame(self: *Core, payload: []const u8) !void { 1508 fn titleFrame(self: *Core, payload: []const u8) !void {
1865 // Under the sink, `semanticFrame`'s atomicity reason: a title is 1509 // Under the sink, `semanticFrame`'s atomicity reason: a title is
1866 // bounded at `term_title_max` but it is still an OSC with a 1510 // bounded at `term_title_max` but it is still an OSC with a
@@ -1877,23 +1521,13 @@ pub const Core = struct {
1877 ); 1521 );
1878 } 1522 }
1879 1523
1880 /// One frame from the daemon, routed. The exhaustive switch over the 1524 /// One frame from the daemon, routed. The exhaustive switch over the wire
1881 /// wire lives HERE and nowhere else. 1525 /// lives HERE and nowhere else — including the replica's apply, because a
1882 /// 1526 /// driver applying on its own is a second switch over the same enum.
1883 /// It applies to the replica too, which is the part worth defending: a
1884 /// driver that applied on its own would need to know which types the
1885 /// replica takes, which is a second switch over the same enum — and the
1886 /// two that existed before this had already drifted, one treating a
1887 /// short snapshot as a `continue` and the other as a broken frame loop.
1888 /// The Core owns the replica (`rep`), so the Core feeds it.
1889 ///
1890 /// What comes back is only what is LEFT (see `Routed`): the driver's
1891 /// own bookkeeping on first state, its re-attach spelling on a resync,
1892 /// and the frames that are nobody's but the driver's (`.not_mine`).
1893 /// 1527 ///
1894 /// Errors are the replica's, unchanged: a snapshot too short to read is 1528 /// What comes back is only what is LEFT (see `Routed`). Errors are the
1895 /// `.skip` because `readSnapshotPrefix` left everything untouched, and 1529 /// replica's, unchanged: a snapshot too short to read is `.skip`, since
1896 /// anything else out of a resize stays loud. 1530 /// `readSnapshotPrefix` left everything untouched.
1897 pub fn frame(self: *Core, frame_type: proto.MsgType, payload: []const u8) !Routed { 1531 pub fn frame(self: *Core, frame_type: proto.MsgType, payload: []const u8) !Routed {
1898 switch (frame_type) { 1532 switch (frame_type) {
1899 .snapshot => { 1533 .snapshot => {
@@ -1939,12 +1573,9 @@ pub const Core = struct {
1939 // an agent channel is never the Core's: these bytes go to a 1573 // an agent channel is never the Core's: these bytes go to a
1940 // socket, not the grid. 1574 // socket, not the grid.
1941 .agent_open, .agent_data, .agent_close => return .not_mine, 1575 .agent_open, .agent_data, .agent_close => return .not_mine,
1942 // Named rather than swept into the `else`, so that adding a 1576 // Named rather than swept into the `else`, so giving one a meaning
1943 // meaning for one of them is an edit here and not a new switch 1577 // is an edit here and not a new switch elsewhere. Each belongs to
1944 // somewhere else. Every one is either part of `mux a`'s 1578 // `mux a`'s conversation or travels the other way.
1945 // conversation with the daemon (its replies, and the
1946 // `cmd_state` pushes it subscribes to) or a frame that travels
1947 // the other way.
1948 .stats_reply, 1579 .stats_reply,
1949 .endpoint_reply, 1580 .endpoint_reply,
1950 .cmd_state, 1581 .cmd_state,
@@ -1980,23 +1611,12 @@ pub const Core = struct {
1980 /// the mouse split, alternate scroll, the scrollback view, and finally 1611 /// the mouse split, alternate scroll, the scrollback view, and finally
1981 /// the prediction and the input frame. 1612 /// the prediction and the input frame.
1982 pub fn forward(self: *Core, transport: anytype, typed: []const u8) !Step { 1613 pub fn forward(self: *Core, transport: anytype, typed: []const u8) !Step {
1983 // Who the wheel belongs to is the session's to say, and it says so 1614 // Who the wheel belongs to is the SESSION's to say: an application
1984 // in the modes it set: an application that asked for mouse 1615 // that asked for mouse reporting gets every mouse byte verbatim, and
1985 // reporting gets every mouse byte verbatim, and the filter is reset 1616 // the filter resets so a report straddling the handover is not
1986 // so a report straddling the handover is not half-eaten. This is 1617 // half-eaten. The claim gates it for a second reason — a client whose
1987 // the same rule as bracketed paste — the client mirrors what the 1618 // stdin is a PIPE never asked for mouse reports, so nothing it reads
1988 // session asked the terminal for — applied to the one device the 1619 // can be one, and `\x1b[<64;10;5M` in a heredoc is text to send.
1989 // client also has a use for.
1990 //
1991 // The claim gates it for a second reason, and it is the one that
1992 // bites hardest: it is the flag that says this Core took a terminal
1993 // over and wrote `client_mouse_setup` to it. A client whose stdin
1994 // is a PIPE never asked anyone for mouse reports, so nothing it
1995 // reads can be one — and filtering there is pure loss: the bytes
1996 // are whatever was piped in, and `\x1b[<64;10;5M` in a heredoc is
1997 // text a script meant to send. Measured:
1998 // `printf 'hello \x1b[<64;10;5M world\n' | mux` arrived at the pty
1999 // with the escape deleted.
2000 var keys = typed; 1620 var keys = typed;
2001 var wheel: i32 = 0; 1621 var wheel: i32 = 0;
2002 // A selection this read finished, asked for below rather than here 1622 // A selection this read finished, asked for below rather than here
@@ -2009,13 +1629,10 @@ pub const Core = struct {
2009 const was = self.drag.range(); 1629 const was = self.drag.range();
2010 if (self.claim == .none or self.semantic.terminal_modes.appMouse()) { 1630 if (self.claim == .none or self.semantic.terminal_modes.appMouse()) {
2011 self.mouse.reset(); 1631 self.mouse.reset();
2012 // The application asked for the mouse, so the application gets 1632 // The application asked for the mouse, so it gets the drag and
2013 // the drag and there is no mux selection here — the same rule 1633 // there is no mux selection; Shift+drag stays the terminal's own
2014 // the mode mirror already follows, and tmux's on 1634 // escape hatch. CLEARED, not ignored: a selection made before the
2015 // `mouse_any_flag`. Shift+drag stays the terminal's own escape 1635 // ask would sit inverted on a screen that is no longer its own.
2016 // hatch. Cleared rather than merely ignored: a selection made
2017 // before the application asked would otherwise sit inverted on
2018 // a screen that no longer belongs to it.
2019 self.drag.clear(); 1636 self.drag.clear();
2020 } else { 1637 } else {
2021 const m = self.mouse.feed(typed, &self.mouse_buf); 1638 const m = self.mouse.feed(typed, &self.mouse_buf);
@@ -2034,21 +1651,12 @@ pub const Core = struct {
2034 // ask goes out from where it stands — no relay, unlike the wall's. 1651 // ask goes out from where it stands — no relay, unlike the wall's.
2035 if (copy) |r| self.requestSelection(transport, r) catch return .lost; 1652 if (copy) |r| self.requestSelection(transport, r) catch return .lost;
2036 1653
2037 // Alternate scroll (tmux calls it that; DEC 1007 is the terminal's 1654 // Alternate scroll. The alt screen has no scrollback, so the rows this
2038 // own version). The alt screen has no scrollback — `historyRows` 1655 // notch would move do not exist — and a pager that did not ask for the
2039 // returns 0 there by contract, so the rows this notch would move do 1656 // mouse is still what the wheel is pointed at, so the notch becomes
2040 // not exist — and a pager that did not ask for the mouse is still 1657 // arrow keys it understands. Without this the notch is consumed and
2041 // the thing the wheel is pointed at. So the notch becomes the arrow 1658 // dropped. Only at the live view: a client already scrolled into
2042 // keys the pager does understand. 1659 // history owns its wheel.
2043 //
2044 // Without this the notch is CONSUMED and dropped: the filter has
2045 // already taken the bytes out, and the scroll arithmetic below
2046 // saturates at a history of zero. That was measured on `less`,
2047 // which is exactly the case this exists for.
2048 //
2049 // Only at the live view: a client scrolled into history before the
2050 // session took the alt screen still owns its wheel, and moving that
2051 // view is what a notch there means.
2052 if (wheel != 0 and self.scroll_rows == 0 and self.rep.eng.onAltScreen()) { 1660 if (wheel != 0 and self.scroll_rows == 0 and self.rep.eng.onAltScreen()) {
2053 sendAltScroll(transport, wheel, self.rep.eng.cursorKeys()) catch return .lost; 1661 sendAltScroll(transport, wheel, self.rep.eng.cursorKeys()) catch return .lost;
2054 wheel = 0; // spent on the session, not on our view 1662 wheel = 0; // spent on the session, not on our view
@@ -2074,11 +1682,10 @@ pub const Core = struct {
2074 @min(self.scroll_rows +| @as(u32, @intCast(by)), self.rep.history_rows) 1682 @min(self.scroll_rows +| @as(u32, @intCast(by)), self.rep.history_rows)
2075 else 1683 else
2076 self.scroll_rows -| @as(u32, @intCast(-by)); 1684 self.scroll_rows -| @as(u32, @intCast(-by));
2077 // Shift+PageDown returns to live even when it was already 1685 // Shift+PageDown returns to live even when already there: it is
2078 // there, and that is load-bearing: it is the only key that 1686 // the only key that clears an overlay left in scroll mode by a
2079 // clears an overlay left in scroll mode by a reconnect (see 1687 // reconnect. The wheel has no such arm — a notch at the live view
2080 // `dropScrollView`). The wheel does not get that arm — a notch 1688 // would repaint the whole screen for nothing.
2081 // at the live view would repaint the whole screen for nothing.
2082 if (next == 0 and (self.scroll_rows > 0 or key_dn)) { 1689 if (next == 0 and (self.scroll_rows > 0 or key_dn)) {
2083 self.scroll_rows = 0; 1690 self.scroll_rows = 0;
2084 self.overlay.setScrollMode(false); 1691 self.overlay.setScrollMode(false);
@@ -2096,26 +1703,18 @@ pub const Core = struct {
2096 // notch owes the pty nothing. 1703 // notch owes the pty nothing.
2097 if (keys.len == 0 or key_up or key_dn) return .ok; 1704 if (keys.len == 0 or key_up or key_dn) return .ok;
2098 if (self.scroll_rows > 0 and !was_live) { 1705 if (self.scroll_rows > 0 and !was_live) {
2099 // Any other key exits scroll mode (swallowed, not forwarded). 1706 // Any other key exits scroll mode, swallowed rather than
2100 // 1707 // forwarded. `was_live` keeps that honest when a notch and a
2101 // `was_live` is what keeps that rule honest when a notch and a 1708 // keystroke share one read: a key typed before the wheel moved the
2102 // keystroke share one read. The exit rule is about a key typed 1709 // view was typed at the SHELL, and swallowing it loses input.
2103 // AT a history view; a key typed at the live view milliseconds
2104 // before the wheel moved it was typed at the shell, and
2105 // swallowing it loses input to a view the user was not looking
2106 // at yet. Both hands are answered instead: the view moved, and
2107 // the keystroke goes where it was aimed.
2108 self.scroll_rows = 0; 1710 self.scroll_rows = 0;
2109 self.overlay.setScrollMode(false); 1711 self.overlay.setScrollMode(false);
2110 try self.paintFull(); 1712 try self.paintFull();
2111 } else { 1713 } else {
2112 // Speculate before sending, so the glyph is on screen while the 1714 // Speculate before sending, so the glyph is on screen while the
2113 // keystroke is still in flight. The bytes that go out are 1715 // keystroke is in flight. The bytes go out whatever the sink says
2114 // unchanged either way — and they go out whatever the sink 1716 // — a tile that lost focus still owes its session what was typed
2115 // says: a wall tile that lost focus still owes its session the bytes 1717 // at it — but an overlay glyph would be graffiti on the new holder.
2116 // that were typed at it while it held the terminal, it just may not
2117 // draw them. An overlay glyph it painted would be graffiti on
2118 // whichever session holds the terminal now.
2119 if (self.beginPaint()) { 1718 if (self.beginPaint()) {
2120 defer self.endPaint(); 1719 defer self.endPaint();
2121 offerKeystroke(self.alloc, &self.overlay, self.rep.eng, keys, self.viewport(), self.out_fd); 1720 offerKeystroke(self.alloc, &self.overlay, self.rep.eng, keys, self.viewport(), self.out_fd);
@@ -2127,15 +1726,10 @@ pub const Core = struct {
2127 return .ok; 1726 return .ok;
2128 } 1727 }
2129 1728
2130 /// Left button only. Middle is the terminal's own paste and right its 1729 /// Left button only: middle is the terminal's own paste and right its
2131 /// menu. 1730 /// menu. A plain click is a defined no-op — a tile is the only session on
2132 /// 1731 /// its rect. What comes back is the selection a release FINISHED, and one
2133 /// A plain click is a defined no-op: a tile is the only session 1732 /// read can hold several: the last is the answer, as latest-wins says.
2134 /// on its rect, so there is nothing for a click to select.
2135 ///
2136 /// What comes back is the selection a release FINISHED. One read can
2137 /// hold more than one release; the last is the answer, which is what
2138 /// `client_core.beginSelection`'s latest-wins would make of two.
2139 fn dragReports(self: *Core, events: []const MouseFilter.Event) ?select.Range { 1733 fn dragReports(self: *Core, events: []const MouseFilter.Event) ?select.Range {
2140 var done: ?select.Range = null; 1734 var done: ?select.Range = null;
2141 for (events) |ev| { 1735 for (events) |ev| {
@@ -2153,13 +1747,9 @@ pub const Core = struct {
2153 return done; 1747 return done;
2154 } 1748 }
2155 1749
2156 /// Ask the daemon for the text under a finished selection. 1750 /// Ask the daemon for the text under a finished selection. Called from
2157 /// 1751 /// `forward`, on the focused tile's pump — the thread that owns this
2158 /// Called from `forward`, which already runs on the focused tile's 1752 /// transport. The wall's KEYBOARD may not: it posts the range instead.
2159 /// pump — the thread that owns this transport. The WALL's
2160 /// keyboard may not: a `Transport` has exactly one owning thread, so it
2161 /// posts the range and its pump calls this instead
2162 /// (`wallview`'s `copySelection`).
2163 pub fn requestSelection(self: *Core, transport: anytype, r: select.Range) !void { 1753 pub fn requestSelection(self: *Core, transport: anytype, r: select.Range) !void {
2164 self.sel_id +%= 1; 1754 self.sel_id +%= 1;
2165 // Sampled HERE, from the replica the coordinates were resolved 1755 // Sampled HERE, from the replica the coordinates were resolved
@@ -2174,12 +1764,9 @@ pub const Core = struct {
2174 try transport.writeFrame(.selection_req, &req); 1764 try transport.writeFrame(.selection_req, &req);
2175 } 1765 }
2176 1766
2177 /// What a `selection_reply` is worth, given the selection that is still 1767 /// What a `selection_reply` is worth, given the selection still held. The
2178 /// held — `Core.drag`, read by the caller under its lock, which is why 1768 /// caller supplies the range because it reads `Core.drag` under its lock.
2179 /// it supplies the range rather than this reading one. 1769 /// Correlation is `client_core`'s: a reply answering no pending request is
2180 ///
2181 /// The correlation is `client_core`'s, unchanged: a reply that answers
2182 /// no pending request, or answers one this drag already replaced, is
2183 /// `.ignored` there and never reaches the tests below. 1770 /// `.ignored` there and never reaches the tests below.
2184 pub fn selectionCopy(self: *Core, payload: []const u8, held: ?select.Range) Copy { 1771 pub fn selectionCopy(self: *Core, payload: []const u8, held: ?select.Range) Copy {
2185 const reply = switch (self.semantic.receive(.selection_reply, payload)) { 1772 const reply = switch (self.semantic.receive(.selection_reply, payload)) {
@@ -2199,35 +1786,25 @@ pub const Core = struct {
2199 .invalid, .unavailable => return .none, 1786 .invalid, .unavailable => return .none,
2200 } 1787 }
2201 if (reply.text.len == 0) return .none; 1788 if (reply.text.len == 0) return .none;
2202 // The gap this feature could have shipped into: the daemon will 1789 // The daemon sends up to `selection_text_max` (1 MiB) and OSC 52 stops
2203 // send up to `selection_text_max` (1 MiB) and OSC 52 stops at 1790 // at `clipboard_base64_max` (64 KiB) of base64: everything between the
2204 // `clipboard_base64_max` (64 KiB) of BASE64, four bytes per three. 1791 // two round-trips `.ok` and copies nothing at all.
2205 // Everything between the two would round-trip `.ok` and copy
2206 // nothing at all.
2207 if (std.base64.standard.Encoder.calcSize(reply.text.len) > proto.clipboard_base64_max) 1792 if (std.base64.standard.Encoder.calcSize(reply.text.len) > proto.clipboard_base64_max)
2208 return .too_large; 1793 return .too_large;
2209 return .{ .text = reply.text }; 1794 return .{ .text = reply.text };
2210 } 1795 }
2211 1796
2212 /// Which line of this session a report landed on, or null for a report 1797 /// Which line of this session a report landed on, or null. The mapping is
2213 /// that names none. 1798 /// trivial, which is why selection lives in the Core: a terminal cell is a
2214 /// 1799 /// grid cell less the tile's origin, plus the history under it.
2215 /// The mapping is trivial and that is the whole reason selection lives
2216 /// in the Core: a terminal cell is a grid cell less the tile's own
2217 /// origin, and the only conversion left is the history the daemon is
2218 /// holding under it.
2219 fn hitTest(self: *Core, ev: MouseFilter.Event) ?select.Hit { 1800 fn hitTest(self: *Core, ev: MouseFilter.Event) ?select.Hit {
2220 // Scrolled back, nothing on this screen is addressable: 1801 // Scrolled back, nothing here is addressable: `renderScrollback` blits
2221 // `renderScrollback` blits VT bytes the daemon composed, with no 1802 // VT bytes with no engine behind them. Refused rather than
2222 // engine behind them, so there is no row here to name. Refused 1803 // half-answered, or the coordinates name the live view instead.
2223 // rather than half-answered — the alternative is a selection whose
2224 // coordinates belong to the live view the user is not looking at.
2225 if (self.scroll_rows > 0) return null; 1804 if (self.scroll_rows > 0) return null;
2226 // A wall tile paints at `row_off` and `col_off`, so a terminal cell 1805 // A tile paints at `row_off`/`col_off`, so a terminal cell is a grid
2227 // is a grid cell only after the tile's origin comes off — on BOTH 1806 // cell only after the origin comes off — on BOTH axes. Above or left
2228 // axes. Above the tile or left of it is a neighbour's cell, not a 1807 // of the tile is a neighbour's cell, not a cell of this grid.
2229 // cell of this grid. The plain client and a one-tile wall leave
2230 // both 0 and this is the identity it always was.
2231 if (ev.row < self.row_off or ev.col < self.col_off) return null; 1808 if (ev.row < self.row_off or ev.col < self.col_off) return null;
2232 const grow = ev.row - self.row_off; 1809 const grow = ev.row - self.row_off;
2233 const gcol = ev.col - self.col_off; 1810 const gcol = ev.col - self.col_off;
@@ -2239,12 +1816,9 @@ pub const Core = struct {
2239 return .{ 1816 return .{
2240 .tile = focus_tile, 1817 .tile = focus_tile,
2241 .row = self.rep.history_rows + grow, 1818 .row = self.rep.history_rows + grow,
2242 // Columns clamp instead of refusing, because the right edge is 1819 // Columns CLAMP rather than refuse: the right edge is where a hand
2243 // where a hand naturally overshoots. An out-of-range column is 1820 // overshoots, and a round trip spent being told `.invalid` is a
2244 // what makes the daemon answer `.invalid`, and a round trip 1821 // copy the user does not get. Clamped to what the tile SHOWS.
2245 // spent to be told so is a copy the user does not get. The
2246 // clamp is to what the tile SHOWS: a wider grid's remaining
2247 // columns are behind the rail, and no hand can reach them.
2248 .col = @min(gcol, @min(grid_cols, self.size.cols) -| 1), 1822 .col = @min(gcol, @min(grid_cols, self.size.cols) -| 1),
2249 }; 1823 };
2250 } 1824 }
@@ -2259,12 +1833,10 @@ pub const Core = struct {
2259 self.overlay.setScrollMode(false); 1833 self.overlay.setScrollMode(false);
2260 } 1834 }
2261 1835
2262 /// A new transport is up and an attach frame has gone out on it. 1836 /// A new transport is up and an attach frame has gone out on it. Only the
2263 /// 1837 /// driver knows, so the Replica's contract makes this clear the caller's.
2264 /// Only the driver knows a re-attach happened; the Replica's contract 1838 /// The overlay's contents were predicted against a connection that no
2265 /// says this clear is its caller's to do. Whatever the overlay held was 1839 /// longer exists — dropping them is no accusation, so counters hold.
2266 /// predicted against a connection that no longer exists — dropping it
2267 /// is not an accusation, so the counters stay where they are.
2268 pub fn reattached(self: *Core) void { 1840 pub fn reattached(self: *Core) void {
2269 // The absolute row space is counted from the oldest row the daemon 1841 // The absolute row space is counted from the oldest row the daemon
2270 // retains, and a resync renames it outright — so a highlight kept 1842 // retains, and a resync renames it outright — so a highlight kept
@@ -2635,12 +2207,10 @@ test "claimTerminal: a sink that refuses leaves the claim unheld, so the caller
2635 var core = try Core.initSized(alloc, -1, p[1], .{ .cols = 80, .rows = 24 }); 2207 var core = try Core.initSized(alloc, -1, p[1], .{ .cols = 80, .rows = 24 });
2636 defer core.deinit(); 2208 defer core.deinit();
2637 core.is_tty = true; 2209 core.is_tty = true;
2638 // The wall's host picker refuses every tile paint while its popup owns 2210 // The picker refuses every tile paint while its popup owns the screen, and
2639 // the screen, and a claim writes the session's modes THROUGH that same 2211 // a claim writes the session's modes THROUGH that sink. False and `.none`
2640 // sink. False and `.none` together are what tell the pump to re-arm: 2212 // together tell the pump to re-arm; read as "already held", the tile stays
2641 // a claim read as "already held" leaves that tile focused owning no 2213 // focused owning no terminal until the focus moves away and back.
2642 // terminal — no mouse modes, no side channels — until the focus moves
2643 // away and back.
2644 core.sink = .{ .begin = refuseSink }; 2214 core.sink = .{ .begin = refuseSink };
2645 try std.testing.expect(!core.claimTerminal()); 2215 try std.testing.expect(!core.claimTerminal());
2646 try std.testing.expectEqual(Claim.none, core.claim); 2216 try std.testing.expectEqual(Claim.none, core.claim);
@@ -2974,11 +2544,9 @@ test "interact: an event knows where it fell among the keys" {
2974 var f: MouseFilter = .{}; 2544 var f: MouseFilter = .{};
2975 var out: [64]u8 = undefined; 2545 var out: [64]u8 = undefined;
2976 2546
2977 // `forward` flattens the keys and drops the reports, so a driver 2547 // `forward` flattens the keys and drops the reports, so two flat lists
2978 // reading the two lists side by side cannot tell whether the click 2548 // cannot say whether the click came before or after the Enter — at the
2979 // came before or after the Enter — and at the wall that is the 2549 // wall, the difference between two tiles getting the focus.
2980 // difference between focusing the tile the user chose and focusing the
2981 // one their click had just moved the selection to.
2982 const key_first = f.feed("\r\x1b[<0;3;2M", &out); 2550 const key_first = f.feed("\r\x1b[<0;3;2M", &out);
2983 try std.testing.expectEqualStrings("\r", key_first.forward); 2551 try std.testing.expectEqualStrings("\r", key_first.forward);
2984 try std.testing.expectEqual(@as(usize, 1), key_first.events.len); 2552 try std.testing.expectEqual(@as(usize, 1), key_first.events.len);
@@ -2995,10 +2563,8 @@ test "interact: an event knows where it fell among the keys" {
2995 test "interact: a chunk packed with the shortest report fills, and does not overrun" { 2563 test "interact: a chunk packed with the shortest report fills, and does not overrun" {
2996 var f: MouseFilter = .{}; 2564 var f: MouseFilter = .{};
2997 // The cap this pins is the filter's totality claim, so the input is the 2565 // The cap this pins is the filter's totality claim, so the input is the
2998 // worst case the contract admits: a whole stdin chunk of the SHORTEST 2566 // worst case: a whole stdin chunk of the SHORTEST complete report there
2999 // complete report there is. `\x1b[<0;1;1M` is NINE bytes — counted, not 2567 // is. `\x1b[<0;1;1M` is NINE bytes — counted, because ten is 181 short.
3000 // eyeballed, because the first version of this sizing said ten and came
3001 // out 181 events short of a full chunk.
3002 const shortest = "\x1b[<0;1;1M"; 2568 const shortest = "\x1b[<0;1;1M";
3003 try std.testing.expectEqual(@as(usize, 9), shortest.len); 2569 try std.testing.expectEqual(@as(usize, 9), shortest.len);
3004 2570
@@ -3190,11 +2756,8 @@ test "prediction: prev_ch is read at the predicted cursor, not the replica's" {
3190 try std.testing.expectEqual(@as(u8, 'Y'), ov.pendingAt(1).prev_ch); 2756 try std.testing.expectEqual(@as(u8, 'Y'), ov.pendingAt(1).prev_ch);
3191 2757
3192 // A frame that changed neither cell: the daemon has not seen the 2758 // A frame that changed neither cell: the daemon has not seen the
3193 // keystrokes yet, so it has said nothing about them and both 2759 // keystrokes, so both predictions must survive it. Read `prev_ch` from the
3194 // predictions must survive it. Read prev_ch from the wrong cell and 2760 // wrong cell and the frame reads as a contradiction and flushes the burst.
3195 // this is where it shows — the second cell holds 'Y', which is neither
3196 // the prediction nor the 'X' a cursor-based read would have recorded,
3197 // so the frame reads as a contradiction and the burst is flushed.
3198 try std.testing.expectEqual( 2761 try std.testing.expectEqual(
3199 predict.Verdict.none, 2762 predict.Verdict.none,
3200 reconcileOverlay(alloc, &ov, replica, 2, 0), 2763 reconcileOverlay(alloc, &ov, replica, 2, 0),
@@ -3373,11 +2936,9 @@ test "prediction: a chunk that is not one printable byte is never speculated abo
3373 try std.testing.expectEqual(@as(u64, 0), ov.counters.made); 2936 try std.testing.expectEqual(@as(u64, 0), ov.counters.made);
3374 try std.testing.expectEqual(@as(u64, 3), ov.counters.suppressed); 2937 try std.testing.expectEqual(@as(u64, 3), ov.counters.suppressed);
3375 2938
3376 // A paste: several printable bytes in one read. This is the shape whose 2939 // A paste: several printable bytes in one read. Its lead byte sails
3377 // lead byte would sail through the printability check, so the length 2940 // through the printability check, so the length guard is the only thing
3378 // guard is the only thing refusing it — and it is refused, because a 2941 // refusing it — and a paste is not one cell's worth of change.
3379 // paste can carry newlines and bracketed-paste markers that are not one
3380 // cell's worth of change each.
3381 offerKeystroke(alloc, &ov, replica, "abc", full_vp, null_fd); 2942 offerKeystroke(alloc, &ov, replica, "abc", full_vp, null_fd);
3382 try std.testing.expectEqual(@as(usize, 0), ov.pendingCount()); 2943 try std.testing.expectEqual(@as(usize, 0), ov.pendingCount());
3383 try std.testing.expectEqual(@as(u64, 0), ov.counters.made); 2944 try std.testing.expectEqual(@as(u64, 0), ov.counters.made);
@@ -3405,11 +2966,9 @@ test "prediction: a repaint never reveals what was never shown" {
3405 try std.testing.expectEqual(@as(usize, 2), ov.pendingCount()); 2966 try std.testing.expectEqual(@as(usize, 2), ov.pendingCount());
3406 try std.testing.expectEqual(@as(u64, 0), ov.counters.displayed); 2967 try std.testing.expectEqual(@as(u64, 0), ov.counters.displayed);
3407 2968
3408 // Every authoritative paint is followed by re-laying the overlay on top, 2969 // Every authoritative paint re-lays the overlay on top, because a delta's
3409 // because a delta's row content wipes anything drawn over it. That 2970 // row content wipes anything drawn over it — a second chance to show a
3410 // repaint is a second, quieter chance to show a prediction that was 2971 // prediction, so it asks the keystroke path's question and gets its answer.
3411 // never displayed in the first place — so it asks the same question the
3412 // keystroke path did, and gets the same answer.
3413 paintOverlay(alloc, &ov, replica.cursorPos(), full_vp, p[1]); 2972 paintOverlay(alloc, &ov, replica.cursorPos(), full_vp, p[1]);
3414 std.posix.close(p[1]); 2973 std.posix.close(p[1]);
3415 2974
@@ -3582,11 +3141,9 @@ test "interact: an application that asked for the mouse gets exactly the modes i
3582 var out: std.ArrayList(u8) = .empty; 3141 var out: std.ArrayList(u8) = .empty;
3583 defer out.deinit(alloc); 3142 defer out.deinit(alloc);
3584 3143
3585 // vim's `set mouse=a`, which asks for exactly the set the client now 3144 // vim's `set mouse=a` asks for exactly the set the client holds for
3586 // holds for itself — so these bytes are identical to the no-application 3145 // itself, so these bytes match the no-application answer above. That
3587 // answer above. That identity is the measurement this feature rests on 3146 // identity is what the feature rests on, not a coincidence to route around.
3588 // rather than a coincidence to route around: an editor keeps 1000+1002
3589 // +1006 armed for a whole session and nobody calls that wasteful.
3590 try appendMouseModes(&out, alloc, .{ 3147 try appendMouseModes(&out, alloc, .{
3591 .bracketed_paste = false, 3148 .bracketed_paste = false,
3592 .mouse_normal = true, 3149 .mouse_normal = true,
@@ -3612,12 +3169,9 @@ test "interact: an application that asked for the mouse gets exactly the modes i
3612 }); 3169 });
3613 try std.testing.expect(std.mem.indexOf(u8, out.items, "\x1b[?1003h") != null); 3170 try std.testing.expect(std.mem.indexOf(u8, out.items, "\x1b[?1003h") != null);
3614 3171
3615 // An application on the legacy format gets the legacy format, and gets 3172 // An application on the legacy format gets it by SUBTRACTION: our own
3616 // it by SUBTRACTION: leaving our own 1006 on would spell every click in 3173 // 1006 would spell every click in a shape it cannot parse, and 1002 would
3617 // a shape it cannot parse, and leaving 1002 on would send it drags it 3174 // send drags it never asked for. The leg proving the override removes.
3618 // never asked for. Both are modes the client holds when nobody is
3619 // asking, so this is the leg that proves the override takes away as
3620 // well as adds.
3621 out.clearRetainingCapacity(); 3175 out.clearRetainingCapacity();
3622 try appendMouseModes(&out, alloc, .{ .bracketed_paste = false, .mouse_normal = true }); 3176 try appendMouseModes(&out, alloc, .{ .bracketed_paste = false, .mouse_normal = true });
3623 try std.testing.expect(std.mem.indexOf(u8, out.items, "\x1b[?1006l") != null); 3177 try std.testing.expect(std.mem.indexOf(u8, out.items, "\x1b[?1006l") != null);
@@ -3694,12 +3248,9 @@ test "interact: the exit teardown unsets every mode mux turned on, and pops the
3694 "\x1b[?7h\x1b[?25h\x1b[23;0t\x1b[?1049l", 3248 "\x1b[?7h\x1b[?25h\x1b[23;0t\x1b[?1049l",
3695 terminal_teardown, 3249 terminal_teardown,
3696 ); 3250 );
3697 // The pop is worthless — worse, it pops a stranger's title — without 3251 // Without its push, the pop is worthless and pops a stranger's title. The
3698 // the push that pairs with it, and the two live far apart: the push 3252 // two live far apart — one startup write against a teardown on every way
3699 // rides in on the wall's screen entry, one write at startup for a 3253 // out — so they are pinned together and deleting either one fails.
3700 // teardown written on every way out. Pinned here together so deleting
3701 // either one fails, rather than quietly leaving the terminal one push
3702 // deep forever or one pop too many.
3703 try std.testing.expectEqualStrings( 3254 try std.testing.expectEqualStrings(
3704 "\x1b[22;0t\x1b[?1049h\x1b[?25l\x1b[?7l" ++ 3255 "\x1b[22;0t\x1b[?1049h\x1b[?25l\x1b[?7l" ++
3705 "\x1b[H\x1b[2J", 3256 "\x1b[H\x1b[2J",
@@ -3718,14 +3269,10 @@ test "interact: the exit teardown unsets every mode mux turned on, and pops the
3718 } 3269 }
3719 3270
3720 test "interact: a borrowed terminal's claim is a session's, and every teardown undoes it" { 3271 test "interact: a borrowed terminal's claim is a session's, and every teardown undoes it" {
3721 // The claim a wall tile takes when it gains focus is the mouse modes and 3272 // A tile's focus claim is the mouse modes and nothing else — the SCREEN
3722 // nothing else — the SCREEN was taken once, by the wall — and the 3273 // was taken once, by the wall — and the release heads every teardown. This
3723 // release is the head of every teardown there is. 3274 // pairing turns over once per focus move where the screen's turns over
3724 // 3275 // once per process, so an exit-path-only half leaks on the first move.
3725 // The pairing this pins turns over many times per run — once per focus move,
3726 // where the screen's turns over once per process — so a half that only
3727 // works on the exit path is a wall that leaves a terminal reporting
3728 // clicks into a shell the moment focus moves on.
3729 try std.testing.expectEqualStrings("\x1b[?1000h\x1b[?1002h\x1b[?1006h", session_claim); 3276 try std.testing.expectEqualStrings("\x1b[?1000h\x1b[?1002h\x1b[?1006h", session_claim);
3730 try std.testing.expect(std.mem.startsWith(u8, terminal_teardown, session_release)); 3277 try std.testing.expect(std.mem.startsWith(u8, terminal_teardown, session_release));
3731 try std.testing.expect(std.mem.indexOf(u8, wall_teardown, session_release) != null); 3278 try std.testing.expect(std.mem.indexOf(u8, wall_teardown, session_release) != null);
@@ -3819,11 +3366,9 @@ test "interact: a promote takes the mouse, a demote gives it back, a demote twic
3819 // modes level-set on top of it. 3366 // modes level-set on top of it.
3820 const claimed = drainPipe(p[0], &buf); 3367 const claimed = drainPipe(p[0], &buf);
3821 try std.testing.expect(std.mem.startsWith(u8, claimed, session_claim)); 3368 try std.testing.expect(std.mem.startsWith(u8, claimed, session_claim));
3822 // A mode the claim never names, so its presence can only be the 3369 // A mode the claim never names, so its presence can only be the level-set
3823 // level-set — the half a claim needs because `resyncSnapshot` answers 3370 // — the half a claim needs because `resyncSnapshot` carries no
3824 // a resize with no `term_modes` at all. 1003 rather than 1002: 1002 is 3371 // `term_modes`. 1003 not 1002: the claim writes 1002 itself.
3825 // in the client's own capture set now, so the claim writes it and its
3826 // presence would no longer distinguish the two writes.
3827 try std.testing.expect(std.mem.indexOf(u8, claimed, "\x1b[?1003l") != null); 3372 try std.testing.expect(std.mem.indexOf(u8, claimed, "\x1b[?1003l") != null);
3828 3373
3829 // A claim onto the tile already focused re-asserts the grid, not the 3374 // A claim onto the tile already focused re-asserts the grid, not the
@@ -3967,11 +3512,9 @@ fn dragFixture(alloc: std.mem.Allocator, out_fd: std.posix.fd_t) !Core {
3967 // exactly the focused-tile case, and the reason a non-focused tile 3512 // exactly the focused-tile case, and the reason a non-focused tile
3968 // never reaches any of this. 3513 // never reaches any of this.
3969 _ = core.claimTerminal(); 3514 _ = core.claimTerminal();
3970 // Row 4 carries wide cells. Kept OFF the rows the column assertions 3515 // Row 4 carries wide cells, kept OFF the rows the column assertions use:
3971 // above use, rather than folded into them: a wide glyph shifts every 3516 // a wide glyph shifts every column right of it, so folding one into row 1
3972 // column to its right, so putting one in row 1 would have meant 3517 // means re-deriving fifteen hand-checked numbers.
3973 // re-deriving fifteen hand-checked column numbers — churn that hides
3974 // regressions instead of catching them.
3975 core.rep.eng.feed("row-zero\r\nrow-one\r\nrow-two\r\nrow-three\r\nw\u{6f22}\u{5b57}x"); 3518 core.rep.eng.feed("row-zero\r\nrow-one\r\nrow-two\r\nrow-three\r\nw\u{6f22}\u{5b57}x");
3976 return core; 3519 return core;
3977 } 3520 }
@@ -4272,11 +3815,9 @@ test "interact: the anchor is an absolute row, and the paint converts it back" {
4272 var buf: [8192]u8 = undefined; 3815 var buf: [8192]u8 = undefined;
4273 _ = drainPipe(p[0], &buf); 3816 _ = drainPipe(p[0], &buf);
4274 3817
4275 // A session with history behind it, which is every session that has 3818 // A session with history behind it, which is every used session. With
4276 // been used. The number is what makes the two directions of the 3819 // none retained a grid row and an absolute row are the same integer, and
4277 // conversion distinguishable at all: with none retained, a grid row 3820 // neither half of the conversion is under test.
4278 // and an absolute row are the same integer and neither half of the
4279 // arithmetic is under test.
4280 core.rep.history_rows = 500; 3821 core.rep.history_rows = 500;
4281 3822
4282 try mouse(&core, &tr, 0, 3, 2, 'M'); 3823 try mouse(&core, &tr, 0, 3, 2, 'M');
@@ -4420,11 +3961,9 @@ test "interact: a drag edge inside a wide cell repaints the row unshifted" {
4420 // (0-based 1, where 漢 starts), not at the column the pointer stopped in. 3961 // (0-based 1, where 漢 starts), not at the column the pointer stopped in.
4421 try std.testing.expect(std.mem.indexOf(u8, painted, comptime cha(2) ++ "\x1b[0m\x1b[7m") != null); 3962 try std.testing.expect(std.mem.indexOf(u8, painted, comptime cha(2) ++ "\x1b[0m\x1b[7m") != null);
4422 3963
4423 // The oracle, and the reason this test exists at all: replay what the 3964 // The oracle: replay what the client actually WROTE through a fresh engine
4424 // client actually WROTE through a fresh engine and read the row back. 3965 // and read the row back. Asserting escape bytes cannot see this class of
4425 // Asserting escape bytes cannot see this class of bug — a row that 3966 // bug — a row emitting 漢 twice still contains every expected substring.
4426 // emits 漢 twice still contains every substring the painter is supposed
4427 // to emit, and lands a column wider than the grid.
4428 var screen = try Engine.init(alloc, .{ .cols = 20, .rows = 6 }); 3967 var screen = try Engine.init(alloc, .{ .cols = 20, .rows = 6 });
4429 defer screen.deinit(); 3968 defer screen.deinit();
4430 screen.feed(painted); 3969 screen.feed(painted);
@@ -4468,12 +4007,9 @@ test "interact: a press left of the pane names no line, and the clamp is the pan
4468 var buf: [8192]u8 = undefined; 4007 var buf: [8192]u8 = undefined;
4469 _ = drainPipe(p[0], &buf); 4008 _ = drainPipe(p[0], &buf);
4470 4009
4471 // Screen column 1, which is a cell of the neighbour across the rail. 4010 // Screen column 1 is a cell of the neighbour across the rail: the column
4472 // The row axis refuses above the tile; the column axis refuses left of 4011 // axis refuses left of the tile as the row axis refuses above it. The row
4473 // it for the same reason — that cell is not a cell of this grid, and 4012 // here is inside the tile's band, so only the column can refuse.
4474 // naming it as one is how every drag in a right-hand pane came back
4475 // `col_off` columns adrift.
4476 // The row is inside the tile's band, so only the column can refuse it.
4477 _ = try core.forward(&tr, comptime std.fmt.comptimePrint("\x1b[<0;1;{d}M", .{drag_row_off + 2})); 4013 _ = try core.forward(&tr, comptime std.fmt.comptimePrint("\x1b[<0;1;{d}M", .{drag_row_off + 2}));
4478 try std.testing.expect(core.drag.range() == null); 4014 try std.testing.expect(core.drag.range() == null);
4479 4015
@@ -4706,11 +4242,9 @@ test "interact: a selection too big for OSC 52 is refused out loud, never trimme
4706 var buf: [8192]u8 = undefined; 4242 var buf: [8192]u8 = undefined;
4707 _ = drainPipe(p[0], &buf); 4243 _ = drainPipe(p[0], &buf);
4708 4244
4709 // Base64 is 4 bytes per 3, so the 64 KiB cap on the encoded form 4245 // Base64 is 4 bytes per 3, so `protocol.clipboard_base64_max` is reached
4710 // (`protocol.clipboard_base64_max`) is reached by three quarters of it 4246 // by three quarters of it in text — far under the 1 MiB the daemon will
4711 // in text — a long way under the 1 MiB the daemon will happily send 4247 // send, which is the gap this exists to fall into.
4712 // (`protocol.selection_text_max`), which is the whole reason this gap
4713 // exists to fall into.
4714 const fits = proto.clipboard_base64_max / 4 * 3; 4248 const fits = proto.clipboard_base64_max / 4 * 3;
4715 const rbuf = try alloc.alloc(u8, proto.selection_reply_prefix_len + fits + 1); 4249 const rbuf = try alloc.alloc(u8, proto.selection_reply_prefix_len + fits + 1);
4716 defer alloc.free(rbuf); 4250 defer alloc.free(rbuf);
@@ -4812,11 +4346,9 @@ test "interact: a frame the Core answers itself never reaches the driver" {
4812 // A page of history for a view that is already live would be painted 4346 // A page of history for a view that is already live would be painted
4813 // over a screen it no longer describes. 4347 // over a screen it no longer describes.
4814 try std.testing.expectEqual(Routed.skip, try core.frame(.scrollback_chunk, "\x00\x00\x00\x00\x01\x00")); 4348 try std.testing.expectEqual(Routed.skip, try core.frame(.scrollback_chunk, "\x00\x00\x00\x00\x01\x00"));
4815 // A delta the replica could not compose is a resync — and it is STATE 4349 // A delta the replica could not compose is a resync — and STATE to its
4816 // to its driver anyway, because `apply` marks the attach as landed 4350 // driver anyway, because `apply` marks the attach landed before it can
4817 // before it can refuse. Both drivers write their attach history off 4351 // refuse. A shell dying in the same read as its first delta must not lose it.
4818 // that, and a shell that dies in the same read as its first delta must
4819 // not lose it.
4820 try std.testing.expectEqual(Routed.resync, try core.frame(.delta, "")); 4352 try std.testing.expectEqual(Routed.resync, try core.frame(.delta, ""));
4821 try std.testing.expect(core.rep.state_since_attach); 4353 try std.testing.expect(core.rep.state_since_attach);
4822 } 4354 }