a73x

e8a4d85c

feat: the wall's rects come from a tree that still cuts stripes

a73x   2026-08-24 18:06

Commit message
feat: the wall's rects come from a tree that still cuts stripes

wallview swaps layoutStripes for layout.Tree: every tile carries a
layout.Rect{top,left,rows,cols}, the tree lives beside the tiles under
paint_mu (keyboard-thread owned, same single-writer rule as sel), and
relayout flattens it to assign rects. The tree is built stacked-only
this task — the aspect rule that turns columns on arrives later — so
the wall still cuts horizontal stripes and the e2e counts do not move.

wallFloors computes the same label-bar arithmetic layoutStripes carried
(min_session_rows + 1 when n>1). The pump snapshot gains left and cols
beside top/rows; Core gets col_off = snap.left beside row_off; owns_screen
requires the full terminal so a one-tile wall still owns the screen.

Flat.deinit takes *const — freeing slices does not mutate the struct.

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

build.zig
Old New
@@ -284,7 +284,7 @@ const mod_table = [_]ModSpec{
284 // typed-at session; the overlay machinery itself is interact's, shared — 284 // typed-at session; the overlay machinery itself is interact's, shared —
285 // and phase 3 promotes the tile into that core rather than growing a 285 // and phase 3 promotes the tile into that core rather than growing a
286 // second copy of it. 286 // second copy of it.
287 .{ .name = "wallview", .path = "src/wallview.zig", .layer = 4, .link_libc = true, .imports = &.{ "protocol", "client", "interact", "wall", "handoff", "xdg", "sockpath", "proxy", "engine", "paint", "select" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, 287 .{ .name = "wallview", .path = "src/wallview.zig", .layer = 4, .link_libc = true, .imports = &.{ "protocol", "client", "interact", "wall", "handoff", "xdg", "sockpath", "proxy", "engine", "paint", "select", "layout" }, .test_imports = &.{"testtmp"}, .quic_tests = true },
288 // ---- layer 5 ---- 288 // ---- layer 5 ----
289 // wall owns the spelling grammar and the state file, so argv is parsed 289 // wall owns the spelling grammar and the state file, so argv is parsed
290 // by the SAME rules the page's POST /tiles and the restored file are — 290 // by the SAME rules the page's POST /tiles and the restored file are —
docscheck.budget
Old New
@@ -27,7 +27,7 @@ shellint.zig 0
27 sockpath.zig 0 27 sockpath.zig 0
28 spawn.zig 0 28 spawn.zig 0
29 testtmp.zig 0 29 testtmp.zig 0
30 wallview.zig 0 30 wallview.zig 277
31 wall.zig 0 31 wall.zig 0
32 wasm_core.zig 0 32 wasm_core.zig 0
33 webhub_main.zig 0 33 webhub_main.zig 0
src/layout.zig
Old New
@@ -31,7 +31,7 @@ pub const Flat = struct {
31 placed: []Placed, 31 placed: []Placed,
32 rails: []Rail, 32 rails: []Rail,
33 33
34 pub fn deinit(self: *Flat, alloc: std.mem.Allocator) void { 34 pub fn deinit(self: *const Flat, alloc: std.mem.Allocator) void {
35 alloc.free(self.placed); 35 alloc.free(self.placed);
36 alloc.free(self.rails); 36 alloc.free(self.rails);
37 } 37 }
src/select.zig
Old New
@@ -8,7 +8,7 @@
8 //! lets two drivers in different layers share one meaning: a zoomed 8 //! lets two drivers in different layers share one meaning: a zoomed
9 //! `interact.Core` at layer 2 and the wall's keyboard loop at layer 4. 9 //! `interact.Core` at layer 2 and the wall's keyboard loop at layer 4.
10 //! 10 //!
11 //! It must not know about `wallview.Stripe`, layer 4, which would invert 11 //! It must not know about `wallview.Tile`, layer 4, which would invert
12 //! the graph. Resolving a terminal row to a session line stays with 12 //! the graph. Resolving a terminal row to a session line stays with
13 //! whoever owns the layout. 13 //! whoever owns the layout.
14 //! 14 //!
src/wallview.zig
Old New
@@ -14,7 +14,7 @@
14 //! hands typed bytes over a mailbox and doorbell pipe. Threads, not one fair 14 //! hands typed bytes over a mailbox and doorbell pipe. Threads, not one fair
15 //! loop: readFrame blocks and userspace buffering is invisible to poll. 15 //! loop: readFrame blocks and userspace buffering is invisible to poll.
16 //! 16 //!
17 //! Horizontal stripes, not a column grid: the terminal does the clipping, 17 //! A stacked-only tree, not a column grid: the terminal does the clipping,
18 //! where a grid needs VT-safe truncation of dumpVtRow that nothing has. 18 //! where a grid needs VT-safe truncation of dumpVtRow that nothing has.
19 const std = @import("std"); 19 const std = @import("std");
20 const proto = @import("protocol"); 20 const proto = @import("protocol");
@@ -32,6 +32,7 @@ const select = @import("select");
32 // The chord table and the prediction hooks a focused tile shares with the 32 // The chord table and the prediction hooks a focused tile shares with the
33 // client: one interaction core, not a second copy (interact.zig). 33 // client: one interaction core, not a second copy (interact.zig).
34 const interact = @import("interact"); 34 const interact = @import("interact");
35 const layout = @import("layout");
35 const TmpDir = @import("testtmp").TmpDir; 36 const TmpDir = @import("testtmp").TmpDir;
36 37
37 pub const Resolved = struct { 38 pub const Resolved = struct {
@@ -94,37 +95,15 @@ pub fn resolveSpelling(
94 return .{ .target = target, .label = spelling, .session = p.session }; 95 return .{ .target = target, .label = spelling, .session = p.session };
95 } 96 }
96 97
97 /// One stripe's place on the terminal: `top` is 0-based; the label bar is 98 /// The daemon's row floor plus the label-bar arithmetic: a one-tile wall
98 /// terminal row top+1, content rows are top+2 .. top+rows (`rows` includes 99 /// draws no bar so its floor is `min_session_rows`; two or more tiles each
99 /// the label bar). 100 /// lose a row to a bar, so each stripe must hold that floor PLUS the bar
100 pub const Stripe = struct { top: u16, rows: u16 }; 101 /// or the daemon drops the resize and the tile freezes on a stale grid.
101 102 fn wallFloors(live: usize) layout.Floors {
102 /// Remainder to the top; too-thin stripes are refused. 103 return .{
103 pub fn layoutStripes( 104 .rows = proto.min_session_rows + @as(u16, @intFromBool(live > 1)),
104 alloc: std.mem.Allocator, 105 .cols = proto.min_session_cols,
105 n: usize, 106 };
106 tty_rows: u16,
107 ) error{ TooSmall, OutOfMemory }![]Stripe {
108 // The 65535 guards the cast, not the wall: `max_tiles` is 32, so no
109 // caller can reach it. `n` is `usize` because this is pure and gets
110 // called with counts, and a total function beats a debug-only trap.
111 // A one-tile wall draws no label bar, so its floor is the daemon's
112 // `min_session_rows`; two or more tiles each lose a row to a bar, so
113 // each stripe must hold that floor PLUS the bar or the daemon drops
114 // the resize and the tile freezes on a stale grid.
115 const floor: u16 = if (n > 1) proto.min_session_rows + 1 else proto.min_session_rows;
116 if (n == 0 or tty_rows / @as(u16, @intCast(@min(n, 65535))) < floor) return error.TooSmall;
117 const nn: u16 = @intCast(n);
118 const per = tty_rows / nn;
119 const rem = tty_rows % nn;
120 const stripes = try alloc.alloc(Stripe, n);
121 var top: u16 = 0;
122 for (stripes, 0..) |*s, i| {
123 const h = per + @intFromBool(i < rem);
124 s.* = .{ .top = top, .rows = h };
125 top += h;
126 }
127 return stripes;
128 } 107 }
129 108
130 const State = enum { 109 const State = enum {
@@ -155,7 +134,7 @@ const mailbox_max = 4096;
155 /// A hard bound rather than a growable array, because the pump threads hold 134 /// A hard bound rather than a growable array, because the pump threads hold
156 /// `*Tile` pointers for the wall's whole life: growing the array would move 135 /// `*Tile` pointers for the wall's whole life: growing the array would move
157 /// it under them. The number is generous beside what a terminal can show 136 /// it under them. The number is generous beside what a terminal can show
158 /// (`layoutStripes` refuses a stripe too thin for the daemon's row floor 137 /// (`wallFloors` refuses a rect too thin for the daemon's row floor
159 /// under a label bar, so an 80x24 terminal caps at eight tiles) and beside 138 /// under a label bar, so an 80x24 terminal caps at eight tiles) and beside
160 /// `max_clients = 8` per daemon. What it really 139 /// `max_clients = 8` per daemon. What it really
161 /// bounds is how much an attach history is allowed to cost when a saved 140 /// bounds is how much an attach history is allowed to cost when a saved
@@ -240,6 +219,10 @@ const Shared = struct {
240 /// one-tile wall owns every row and draws no bar. Set by `relayout`, so 219 /// one-tile wall owns every row and draws no bar. Set by `relayout`, so
241 /// `viewRows` and `core.row_off` agree with what is on the screen. 220 /// `viewRows` and `core.row_off` agree with what is on the screen.
242 label_rows: u16 = 1, 221 label_rows: u16 = 1,
222 /// The container tree that owns every tile's rect. The keyboard thread
223 /// mutates it under `paint_mu` — the same single-writer rule as `sel`
224 /// and `label_rows` — and relayout flattens it to read rects.
225 tree: layout.Tree = layout.Tree.init(std.heap.page_allocator),
243 /// Bumped when the terminal's contents are no longer anybody's paint — 226 /// Bumped when the terminal's contents are no longer anybody's paint —
244 /// a relayout re-cut the stripes, or the focus moved and the newly 227 /// a relayout re-cut the stripes, or the focus moved and the newly
245 /// focused tile owes a full repaint. Pumps compare it against what 228 /// focused tile owes a full repaint. Pumps compare it against what
@@ -248,15 +231,14 @@ const Shared = struct {
248 repaint_gen: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), 231 repaint_gen: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
249 }; 232 };
250 233
251 /// The stripe window a paint used, in the only terms a click can be 234 /// The rect a paint used, in the only terms a click can be resolved in.
252 /// resolved in.
253 /// 235 ///
254 /// Absolute rows are counted from the oldest row the DAEMON still retains 236 /// Absolute rows are counted from the oldest row the DAEMON still retains
255 /// (`protocol.SelectionReply`), so a grid row alone names nothing: it has 237 /// (`protocol.SelectionReply`), so a grid row alone names nothing: it has
256 /// to be read against the history count the same frame carried. 238 /// to be read against the history count the same frame carried.
257 const Tile = struct { 239 const Tile = struct {
258 r: Resolved, 240 r: Resolved,
259 stripe: Stripe, 241 rect: layout.Rect,
260 shared: *Shared, 242 shared: *Shared,
261 /// This tile's place in the wall: what the focus and the `1`-`9` 243 /// This tile's place in the wall: what the focus and the `1`-`9`
262 /// jump are indices into. 244 /// jump are indices into.
@@ -401,20 +383,24 @@ const Tile = struct {
401 release_pending: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), 383 release_pending: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
402 384
403 fn viewRows(t: *const Tile) u16 { 385 fn viewRows(t: *const Tile) u16 {
404 return t.stripe.rows -| t.shared.label_rows; 386 return t.rect.rows -| t.shared.label_rows;
387 }
388
389 fn viewCols(t: *const Tile) u16 {
390 return t.rect.cols;
405 } 391 }
406 }; 392 };
407 393
408 /// Which tile's rectangle a zero-based terminal row falls in, or null 394 /// Which tile's rectangle a zero-based terminal row falls in, or null
409 /// when none does. The keyboard routes a click to the tile it lands in so 395 /// when none does. The keyboard routes a click to the tile it lands in so
410 /// focus can follow it; the drag itself is the focused tile's Core's. 396 /// focus can follow it; the drag itself is the focused tile's Core's.
411 /// Under `paint_mu` because `Tile.stripe` is written under it. 397 /// Under `paint_mu` because `Tile.rect` is written under it.
412 fn rectHit(tiles: []Tile, present: []const bool, shared: *Shared, row: u16) ?usize { 398 fn rectHit(tiles: []Tile, present: []const bool, shared: *Shared, row: u16) ?usize {
413 shared.paint_mu.lock(); 399 shared.paint_mu.lock();
414 defer shared.paint_mu.unlock(); 400 defer shared.paint_mu.unlock();
415 for (tiles, present) |*t, p| { 401 for (tiles, present) |*t, p| {
416 if (!p) continue; 402 if (!p) continue;
417 if (row >= t.stripe.top and row < t.stripe.top + t.stripe.rows) return t.idx; 403 if (row >= t.rect.top and row < t.rect.top + t.rect.rows) return t.idx;
418 } 404 }
419 return null; 405 return null;
420 } 406 }
@@ -505,16 +491,16 @@ fn paintLabelLocked(t: *Tile) void {
505 else 491 else
506 t.state.word(); 492 t.state.word();
507 var text_buf: [256]u8 = undefined; 493 var text_buf: [256]u8 = undefined;
508 const shown = labelText(&text_buf, t.shared.size.cols, marker, t.r.label, status); 494 const shown = labelText(&text_buf, t.rect.cols, marker, t.r.label, status);
509 var out: [1024]u8 = undefined; 495 var out: [1024]u8 = undefined;
510 var fbs = std.io.fixedBufferStream(&out); 496 var fbs = std.io.fixedBufferStream(&out);
511 const w = fbs.writer(); 497 const w = fbs.writer();
512 w.print("\x1b[?2026h\x1b[{d};1H\x1b[7m{s}", .{ t.stripe.top + 1, shown }) catch return; 498 w.print("\x1b[?2026h\x1b[{d};{d}H\x1b[7m{s}", .{ t.rect.top + 1, t.rect.left + 1, shown }) catch return;
513 // Padded with spaces rather than \x1b[K: erase-to-EOL fills with the 499 // Padded with spaces rather than \x1b[K: erase-to-EOL fills with the
514 // background color, not the reverse-video attribute, on most 500 // background color, not the reverse-video attribute, on most
515 // terminals — the bar would end where the text does. 501 // terminals — the bar would end where the text does.
516 var i: usize = shown.len; 502 var i: usize = shown.len;
517 while (i < t.shared.size.cols) : (i += 1) w.writeByte(' ') catch break; 503 while (i < t.rect.cols) : (i += 1) w.writeByte(' ') catch break;
518 w.writeAll("\x1b[0m\x1b[?2026l") catch return; 504 w.writeAll("\x1b[0m\x1b[?2026l") catch return;
519 proto.writeAllFd(t.shared.out_fd, fbs.getWritten()) catch {}; 505 proto.writeAllFd(t.shared.out_fd, fbs.getWritten()) catch {};
520 } 506 }
@@ -602,7 +588,7 @@ fn sendAttach(t: *Tile, tr: *client.Transport, have_seq: u64, have_epoch: u64) !
602 t.shared.paint_mu.lock(); 588 t.shared.paint_mu.lock();
603 defer t.shared.paint_mu.unlock(); 589 defer t.shared.paint_mu.unlock();
604 break :blk .{ 590 break :blk .{
605 .cols = t.shared.size.cols, 591 .cols = t.viewCols(),
606 .view_rows = t.viewRows(), 592 .view_rows = t.viewRows(),
607 }; 593 };
608 }; 594 };
@@ -1060,23 +1046,30 @@ fn pumpTile(t: *Tile) void {
1060 if (fdbuf[1].revents != 0) drainWake(t); 1046 if (fdbuf[1].revents != 0) drainWake(t);
1061 1047
1062 // The paint offset for this pass: a relayout may have re-cut this 1048 // The paint offset for this pass: a relayout may have re-cut this
1063 // tile's stripe, and every paint the pass drives through the Core 1049 // tile's rect, and every paint the pass drives through the Core
1064 // has to land in the rect the tile currently owns. Snapshot under 1050 // has to land in the rect the tile currently owns. Snapshot under
1065 // `paint_mu` — the keyboard writes `stripe` and `label_rows` under 1051 // `paint_mu` — the keyboard writes `rect` and `label_rows` under
1066 // it — and use the snapshot for the whole pass. 1052 // it — and use the snapshot for the whole pass.
1067 const snap = blk: { 1053 const snap = blk: {
1068 t.shared.paint_mu.lock(); 1054 t.shared.paint_mu.lock();
1069 defer t.shared.paint_mu.unlock(); 1055 defer t.shared.paint_mu.unlock();
1070 break :blk .{ 1056 break :blk .{
1071 .top = t.stripe.top, 1057 .top = t.rect.top,
1072 .rows = t.stripe.rows, 1058 .left = t.rect.left,
1059 .rows = t.rect.rows,
1060 .cols = t.rect.cols,
1073 .label_rows = t.shared.label_rows, 1061 .label_rows = t.shared.label_rows,
1074 .cols = t.shared.size.cols, 1062 .term_cols = t.shared.size.cols,
1063 .term_rows = t.shared.size.rows,
1075 }; 1064 };
1076 }; 1065 };
1077 core.row_off = snap.top + snap.label_rows; 1066 core.row_off = snap.top + snap.label_rows;
1078 core.owns_screen = snap.top == 0 and snap.label_rows == 0; 1067 core.col_off = snap.left;
1068 core.owns_screen = snap.top == 0 and snap.left == 0 and
1069 snap.label_rows == 0 and snap.cols == snap.term_cols and
1070 snap.rows == snap.term_rows;
1079 const snap_view_rows: u16 = snap.rows -| snap.label_rows; 1071 const snap_view_rows: u16 = snap.rows -| snap.label_rows;
1072 const snap_view_cols: u16 = snap.cols;
1080 1073
1081 // FOCUS CLAIM. The keyboard moved the focus onto this tile; the 1074 // FOCUS CLAIM. The keyboard moved the focus onto this tile; the
1082 // session's mouse modes and side channels go on here, on the thread 1075 // session's mouse modes and side channels go on here, on the thread
@@ -1091,7 +1084,7 @@ fn pumpTile(t: *Tile) void {
1091 // RECT — never the whole terminal, which on a wall of two 1084 // RECT — never the whole terminal, which on a wall of two
1092 // would let this tile paint over its neighbour — so the first 1085 // would let this tile paint over its neighbour — so the first
1093 // paint is at the right grid. 1086 // paint is at the right grid.
1094 const rect: proto.Size = .{ .cols = snap.cols, .rows = snap_view_rows }; 1087 const rect: proto.Size = .{ .cols = snap_view_cols, .rows = snap_view_rows };
1095 if (core.size.cols != rect.cols or core.size.rows != rect.rows) 1088 if (core.size.cols != rect.cols or core.size.rows != rect.rows)
1096 core.adoptSize(rect); 1089 core.adoptSize(rect);
1097 _ = core.claimTerminal(); 1090 _ = core.claimTerminal();
@@ -1134,10 +1127,10 @@ fn pumpTile(t: *Tile) void {
1134 // The Core clips every paint to its size; a resize the daemon 1127 // The Core clips every paint to its size; a resize the daemon
1135 // hears but the Core does not leaves the bottom of the new 1128 // hears but the Core does not leaves the bottom of the new
1136 // grid cut off on screen forever. 1129 // grid cut off on screen forever.
1137 core.adoptSize(.{ .cols = snap.cols, .rows = snap_view_rows }); 1130 core.adoptSize(.{ .cols = snap_view_cols, .rows = snap_view_rows });
1138 transport.writeFrame( 1131 transport.writeFrame(
1139 .resize, 1132 .resize,
1140 &proto.encodeSize(snap.cols, snap_view_rows), 1133 &proto.encodeSize(snap_view_cols, snap_view_rows),
1141 ) catch { 1134 ) catch {
1142 if (!redial(t, alloc, &core, &transport, target, &state, &agent_locals)) return; 1135 if (!redial(t, alloc, &core, &transport, target, &state, &agent_locals)) return;
1143 continue :outer; 1136 continue :outer;
@@ -1568,7 +1561,7 @@ fn relayout(
1568 if (p) live += 1; 1561 if (p) live += 1;
1569 } 1562 }
1570 // A one-tile wall owns every row and draws no label bar; two or more 1563 // A one-tile wall owns every row and draws no label bar; two or more
1571 // tiles each lose their top row to one. Set before the stripes are cut 1564 // tiles each lose their top row to one. Set before the tree is flattened
1572 // so `viewRows` is right from the first attach, and before the doorbells 1565 // so `viewRows` is right from the first attach, and before the doorbells
1573 // so the pumps read it on the pass they answer. 1566 // so the pumps read it on the pass they answer.
1574 shared.label_rows = if (live > 1) 1 else 0; 1567 shared.label_rows = if (live > 1) 1 else 0;
@@ -1577,24 +1570,24 @@ fn relayout(
1577 paintEmptyWallLocked(shared); 1570 paintEmptyWallLocked(shared);
1578 return; 1571 return;
1579 } 1572 }
1580 if (layoutStripes(alloc, live, shared.size.rows)) |stripes| { 1573 if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), null)) |flat| {
1581 defer alloc.free(stripes); 1574 defer flat.deinit(alloc);
1582 var k: usize = 0;
1583 for (tiles, present) |*t, p| { 1575 for (tiles, present) |*t, p| {
1584 if (!p) continue; 1576 if (!p) continue;
1585 t.stripe = stripes[k]; 1577 if (flat.rectOf(@intCast(t.idx))) |r| {
1586 // A stripe that moved or reshaped owes the daemon its new size: 1578 t.rect = r;
1579 }
1580 // A rect that moved or reshaped owes the daemon its new size:
1587 // the pump is the transport's only writer, so the doorbell is 1581 // the pump is the transport's only writer, so the doorbell is
1588 // set here and the pump sends the `.resize` from its own pass. 1582 // set here and the pump sends the `.resize` from its own pass.
1589 // `viewRows` reads `label_rows` and the stripe together, both 1583 // `viewRows` reads `label_rows` and the rect together, both
1590 // set above and here — so the frame carries the rect this tile 1584 // set above and here — so the frame carries the rect this tile
1591 // now claims. 1585 // now claims.
1592 t.resize_pending.store(true, .release); 1586 t.resize_pending.store(true, .release);
1593 k += 1;
1594 } 1587 }
1595 } else |_| {} 1588 } else |_| {}
1596 1589
1597 // The generation bump is what puts the stripes back: every surviving 1590 // The generation bump is what puts the rects back: every surviving
1598 // pump repaints from its hot replica at its NEW rows, and the doorbell 1591 // pump repaints from its hot replica at its NEW rows, and the doorbell
1599 // makes that immediate rather than one poll timeout away. 1592 // makes that immediate rather than one poll timeout away.
1600 _ = shared.repaint_gen.fetchAdd(1, .release); 1593 _ = shared.repaint_gen.fetchAdd(1, .release);
@@ -1634,6 +1627,7 @@ fn forgetTile(
1634 present[sel] = false; 1627 present[sel] = false;
1635 t.gone.store(true, .release); 1628 t.gone.store(true, .release);
1636 ring(t); 1629 ring(t);
1630 shared.tree.remove(@intCast(sel));
1637 // Somewhere sane: the next tile down, wrapping — and the old index 1631 // Somewhere sane: the next tile down, wrapping — and the old index
1638 // when nothing is left, which the callers guard on `present`. 1632 // when nothing is left, which the callers guard on `present`.
1639 relayout(alloc, tiles, present, shared, stepPresent(present, sel, true) orelse sel); 1633 relayout(alloc, tiles, present, shared, stepPresent(present, sel, true) orelse sel);
@@ -1675,9 +1669,9 @@ fn sameTarget(a: client.Target, b: client.Target) bool {
1675 /// Asked BEFORE the add, because a failed re-layout keeps the old geometry: 1669 /// Asked BEFORE the add, because a failed re-layout keeps the old geometry:
1676 /// right for `x`, wrong here — the new tile would point at other rows. 1670 /// right for `x`, wrong here — the new tile would point at other rows.
1677 fn wallFits(rows: u16, n: usize) bool { 1671 fn wallFits(rows: u16, n: usize) bool {
1678 // Guards the cast below, as in `layoutStripes` — not the wall. 1672 // Guards the cast below, as in `wallFloors` — not the wall.
1679 if (n == 0 or n > 65535) return false; 1673 if (n == 0 or n > 65535) return false;
1680 // The same floor `layoutStripes` enforces: a label bar costs a row 1674 // The same floor `wallFloors` enforces: a label bar costs a row
1681 // once a second tile joins, so the content rows must still meet 1675 // once a second tile joins, so the content rows must still meet
1682 // `min_session_rows`. 1676 // `min_session_rows`.
1683 const floor: u16 = if (n > 1) proto.min_session_rows + 1 else proto.min_session_rows; 1677 const floor: u16 = if (n > 1) proto.min_session_rows + 1 else proto.min_session_rows;
@@ -1695,14 +1689,14 @@ fn presentCount(present: []const bool) usize {
1695 /// One tile, with the doorbell its pump polls. Split out of `run` because 1689 /// One tile, with the doorbell its pump polls. Split out of `run` because
1696 /// tiles are born in two places now — at startup, and whenever a focus chord 1690 /// tiles are born in two places now — at startup, and whenever a focus chord
1697 /// or the saved wall adds one. 1691 /// or the saved wall adds one.
1698 fn initTile(t: *Tile, r: Resolved, s: Stripe, shared: *Shared, idx: usize) !void { 1692 fn initTile(t: *Tile, r: Resolved, s: layout.Rect, shared: *Shared, idx: usize) !void {
1699 // The doorbell, before the pump that polls it exists. Non-blocking at 1693 // The doorbell, before the pump that polls it exists. Non-blocking at
1700 // both ends: the writer must never wedge the keyboard and the reader 1694 // both ends: the writer must never wedge the keyboard and the reader
1701 // must never wedge the pump. 1695 // must never wedge the pump.
1702 const wake = try std.posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); 1696 const wake = try std.posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true });
1703 t.* = .{ 1697 t.* = .{
1704 .r = r, 1698 .r = r,
1705 .stripe = s, 1699 .rect = s,
1706 .shared = shared, 1700 .shared = shared,
1707 .idx = idx, 1701 .idx = idx,
1708 .wake_r = wake[0], 1702 .wake_r = wake[0],
@@ -1777,16 +1771,27 @@ fn addSessionTile(
1777 // these slices for as long as it lives. 1771 // these slices for as long as it lives.
1778 const session = alloc.dupe(u8, want) catch return .full; 1772 const session = alloc.dupe(u8, want) catch return .full;
1779 const label = tileLabel(alloc, target, want) catch return .full; 1773 const label = tileLabel(alloc, target, want) catch return .full;
1780 // The real stripe, not a placeholder: a chord-born tile (creates = true) 1774 // The real rect, not a placeholder: a chord-born tile (creates = true)
1781 // puts its rect on the first attach frame, and the daemon refuses 1775 // puts its rect on the first attach frame, and the daemon refuses
1782 // creates under min_session_rows — a 2-row stripe under a label bar is 1776 // creates under min_session_rows — a 2-row rect under a label bar is
1783 // one content row, silently frozen. `label_rows` is set here so 1777 // one content row, silently frozen. `label_rows` is set here so
1784 // `viewRows` is right from the first attach; relayout re-cuts every 1778 // `viewRows` is right from the first attach; relayout re-flattens
1785 // stripe and sets it again, so the two agree. 1779 // every rect and sets it again, so the two agree.
1786 shared.label_rows = if (new_live > 1) 1 else 0; 1780 shared.label_rows = if (new_live > 1) 1 else 0;
1787 const stripes = layoutStripes(alloc, new_live, shared.size.rows) catch return .full;
1788 defer alloc.free(stripes);
1789 const at = live.*; 1781 const at = live.*;
1782 shared.tree.insert(@intCast(from), @intCast(at)) catch return .full;
1783 const flat = shared.tree.flatten(
1784 alloc,
1785 shared.size.rows,
1786 shared.size.cols,
1787 wallFloors(new_live),
1788 null,
1789 ) catch {
1790 shared.tree.remove(@intCast(at));
1791 return .full;
1792 };
1793 defer flat.deinit(alloc);
1794 const new_rect = flat.rectOf(@intCast(at)) orelse return .full;
1790 initTile( 1795 initTile(
1791 &tiles[at], 1796 &tiles[at],
1792 // The offer is inherited from the tile this one grew out of. Same 1797 // The offer is inherited from the tile this one grew out of. Same
@@ -1795,10 +1800,13 @@ fn addSessionTile(
1795 // the flag on, so not inheriting would silently end the forwarding 1800 // the flag on, so not inheriting would silently end the forwarding
1796 // at the first session switch. 1801 // at the first session switch.
1797 .{ .target = target, .label = label, .session = session, .agent = tiles[from].r.agent }, 1802 .{ .target = target, .label = label, .session = session, .agent = tiles[from].r.agent },
1798 stripes[new_live - 1], 1803 new_rect,
1799 shared, 1804 shared,
1800 at, 1805 at,
1801 ) catch return .full; 1806 ) catch {
1807 shared.tree.remove(@intCast(at));
1808 return .full;
1809 };
1802 tiles[at].record = true; 1810 tiles[at].record = true;
1803 tiles[at].creates = true; 1811 tiles[at].creates = true;
1804 tiles[at].born_from = from; 1812 tiles[at].born_from = from;
@@ -1999,16 +2007,34 @@ fn hydrate(
1999 const r = resolveSpelling(alloc, spelling, key, idle_ms) catch continue; 2007 const r = resolveSpelling(alloc, spelling, key, idle_ms) catch continue;
2000 if (showsSelf(r.target, r.session, env_sock, env_session)) continue; 2008 if (showsSelf(r.target, r.session, env_sock, env_session)) continue;
2001 const at = live.*; 2009 const at = live.*;
2002 // The real stripe, not a placeholder: a hydrated tile doorbells a 2010 // The real rect, not a placeholder: a hydrated tile doorbells a
2003 // resize on its first pass, and a 2-row stripe on a live session 2011 // resize on its first pass, and a 2-row rect on a live session
2004 // is destructive under latest-wins. `label_rows` is set per-add 2012 // is destructive under latest-wins. `label_rows` is set per-add
2005 // so `viewRows` is right from the first resize; relayout re-cuts 2013 // so `viewRows` is right from the first resize; relayout re-flattens
2006 // every stripe and sets it again. 2014 // every rect and sets it again.
2007 const new_live = presentCount(present[0..live.*]) + 1; 2015 const new_live = presentCount(present[0..live.*]) + 1;
2008 shared.label_rows = if (new_live > 1) 1 else 0; 2016 shared.label_rows = if (new_live > 1) 1 else 0;
2009 const stripes = layoutStripes(alloc, new_live, shared.size.rows) catch continue; 2017 const from_tile: u8 = if (at > 0) @intCast(at - 1) else 0;
2010 defer alloc.free(stripes); 2018 shared.tree.insert(from_tile, @intCast(at)) catch continue;
2011 initTile(&tiles[at], r, stripes[new_live - 1], shared, at) catch continue; 2019 const flat = shared.tree.flatten(
2020 alloc,
2021 shared.size.rows,
2022 shared.size.cols,
2023 wallFloors(new_live),
2024 null,
2025 ) catch {
2026 shared.tree.remove(@intCast(at));
2027 continue;
2028 };
2029 defer flat.deinit(alloc);
2030 const new_rect = flat.rectOf(@intCast(at)) orelse {
2031 shared.tree.remove(@intCast(at));
2032 continue;
2033 };
2034 initTile(&tiles[at], r, new_rect, shared, at) catch {
2035 shared.tree.remove(@intCast(at));
2036 continue;
2037 };
2012 present[at] = true; 2038 present[at] = true;
2013 live.* += 1; 2039 live.* += 1;
2014 spawnPump(&tiles[at]); 2040 spawnPump(&tiles[at]);
@@ -2134,17 +2160,31 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) !
2134 std.debug.print("mux: too many tiles ({d}, max {d})\n", .{ resolved.len, max_tiles }); 2160 std.debug.print("mux: too many tiles ({d}, max {d})\n", .{ resolved.len, max_tiles });
2135 return 2; 2161 return 2;
2136 } 2162 }
2137 // Stripes are cut for the tiles that exist NOW. A wall of one tile 2163 // Rects are cut for the tiles that exist NOW. A wall of one tile
2138 // has one, and the geometry is re-cut when the first Ctrl-\ w hydrates 2164 // has one, and the geometry is re-cut when the first Ctrl-\ w hydrates
2139 // the rest — `relayout` already does exactly that for `x`. 2165 // the rest — `relayout` already does exactly that for `x`.
2140 const stripes = layoutStripes(alloc, resolved.len, size.rows) catch |err| switch (err) { 2166 var shared = Shared{ .out_fd = stdout_fd, .size = size, .is_tty = is_tty };
2141 error.TooSmall => { 2167 {
2142 std.debug.print("mux: terminal too small for {d} tiles\n", .{resolved.len}); 2168 var i: usize = 0;
2143 return 2; 2169 while (i < resolved.len) : (i += 1) {
2144 }, 2170 if (i == 0) {
2145 else => |e| return e, 2171 shared.tree.addFirst(@intCast(i)) catch return 2;
2172 } else {
2173 shared.tree.insert(@intCast(i - 1), @intCast(i)) catch return 2;
2174 }
2175 }
2176 }
2177 const init_flat = shared.tree.flatten(
2178 alloc,
2179 size.rows,
2180 size.cols,
2181 wallFloors(resolved.len),
2182 null,
2183 ) catch {
2184 std.debug.print("mux: terminal too small for {d} tiles\n", .{resolved.len});
2185 return 2;
2146 }; 2186 };
2147 defer alloc.free(stripes); 2187 defer init_flat.deinit(alloc);
2148 2188
2149 // A daemon that dies mid-write must surface as a write error on that 2189 // A daemon that dies mid-write must surface as a write error on that
2150 // tile's thread, not a process-fatal SIGPIPE. 2190 // tile's thread, not a process-fatal SIGPIPE.
@@ -2197,7 +2237,6 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) !
2197 std.posix.tcsetattr(stdin_fd, .FLUSH, o) catch {}; 2237 std.posix.tcsetattr(stdin_fd, .FLUSH, o) catch {};
2198 }; 2238 };
2199 2239
2200 var shared = Shared{ .out_fd = stdout_fd, .size = size, .is_tty = is_tty };
2201 // The pumps' way of waking a keyboard asleep in read(2). Created before 2240 // The pumps' way of waking a keyboard asleep in read(2). Created before
2202 // any pump exists, because a pump that ends immediately (a refused 2241 // any pump exists, because a pump that ends immediately (a refused
2203 // attach) rings it before the keyboard has ever polled. 2242 // attach) rings it before the keyboard has ever polled.
@@ -2216,8 +2255,9 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) !
2216 var live: usize = 0; 2255 var live: usize = 0;
2217 const present = try alloc.alloc(bool, max_tiles); 2256 const present = try alloc.alloc(bool, max_tiles);
2218 @memset(present, false); 2257 @memset(present, false);
2219 for (resolved, stripes) |r, s| { 2258 for (resolved, 0..) |r, i| {
2220 try initTile(&tiles[live], r, s, &shared, live); 2259 const rect = init_flat.rectOf(@intCast(i)) orelse return 2;
2260 try initTile(&tiles[live], r, rect, &shared, live);
2221 present[live] = true; 2261 present[live] = true;
2222 live += 1; 2262 live += 1;
2223 } 2263 }
@@ -2350,6 +2390,7 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) !
2350 .vanish => |v| { 2390 .vanish => |v| {
2351 present[ended] = false; 2391 present[ended] = false;
2352 tiles[ended].gone.store(true, .release); 2392 tiles[ended].gone.store(true, .release);
2393 shared.tree.remove(@intCast(ended));
2353 if (v.msg) |msg| { 2394 if (v.msg) |msg| {
2354 // The notice is the banner on the surviving tile's 2395 // The notice is the banner on the surviving tile's
2355 // rect; stderr is deferred to after raw-mode restore 2396 // rect; stderr is deferred to after raw-mode restore
@@ -2533,41 +2574,52 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) !
2533 std.posix.exit(exit_code); 2574 std.posix.exit(exit_code);
2534 } 2575 }
2535 2576
2536 test "layoutStripes splits rows with the remainder at the top" { 2577 test "the tree's stacked cut splits rows with the remainder at the top" {
2537 const alloc = std.testing.allocator; 2578 const alloc = std.testing.allocator;
2538 const s = try layoutStripes(alloc, 3, 25); 2579 var tree = layout.Tree.init(alloc);
2539 defer alloc.free(s); 2580 defer tree.deinit();
2540 try std.testing.expectEqual(@as(usize, 3), s.len); 2581 try tree.addFirst(0);
2541 try std.testing.expectEqual(Stripe{ .top = 0, .rows = 9 }, s[0]); 2582 try tree.insert(0, 1);
2542 try std.testing.expectEqual(Stripe{ .top = 9, .rows = 8 }, s[1]); 2583 try tree.insert(1, 2);
2543 try std.testing.expectEqual(Stripe{ .top = 17, .rows = 8 }, s[2]); 2584 var f = try tree.flatten(alloc, 25, 80, wallFloors(3), null);
2544 } 2585 defer f.deinit(alloc);
2545 2586 try std.testing.expectEqual(layout.Rect{ .top = 0, .left = 0, .rows = 9, .cols = 80 }, f.rectOf(0).?);
2546 test "layoutStripes refuses a wall that cannot show a content row" { 2587 try std.testing.expectEqual(layout.Rect{ .top = 9, .left = 0, .rows = 8, .cols = 80 }, f.rectOf(1).?);
2588 try std.testing.expectEqual(layout.Rect{ .top = 17, .left = 0, .rows = 8, .cols = 80 }, f.rectOf(2).?);
2589 }
2590
2591 test "the tree refuses a wall that cannot show a content row" {
2547 const alloc = std.testing.allocator; 2592 const alloc = std.testing.allocator;
2593 var tree = layout.Tree.init(alloc);
2594 defer tree.deinit();
2548 // 12 tiles over 23 rows: 1 row each — a label with no content. 2595 // 12 tiles over 23 rows: 1 row each — a label with no content.
2549 try std.testing.expectError(error.TooSmall, layoutStripes(alloc, 12, 23)); 2596 try tree.addFirst(0);
2550 try std.testing.expectError(error.TooSmall, layoutStripes(alloc, 0, 24)); 2597 for (1..12) |i| try tree.insert(@intCast(i - 1), @intCast(i));
2598 try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 23, 80, wallFloors(12), null));
2551 } 2599 }
2552 2600
2553 test "layoutStripes refuses a multi-tile cut too thin for the daemon's row floor" { 2601 test "the tree refuses a multi-tile cut too thin for the daemon's row floor" {
2554 const alloc = std.testing.allocator; 2602 const alloc = std.testing.allocator;
2555 // Two tiles on five rows: each stripe is two rows, and under a label 2603 var tree = layout.Tree.init(alloc);
2604 defer tree.deinit();
2605 try tree.addFirst(0);
2606 try tree.insert(0, 1);
2607 // Two tiles on five rows: each rect is two rows, and under a label
2556 // bar that is one content row — below `min_session_rows`, so the 2608 // bar that is one content row — below `min_session_rows`, so the
2557 // daemon refuses the resize and the tile freezes on a stale grid. 2609 // daemon refuses the resize and the tile freezes on a stale grid.
2558 try std.testing.expectError(error.TooSmall, layoutStripes(alloc, 2, 5)); 2610 try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 5, 80, wallFloors(2), null));
2559 try std.testing.expectError(error.TooSmall, layoutStripes(alloc, 2, 4)); 2611 try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 4, 80, wallFloors(2), null));
2560 // Six rows is the first cut that fits: three a stripe, two of them 2612 // Six rows is the first cut that fits: three a rect, two of them
2561 // content under the bar — exactly the daemon's row floor. 2613 // content under the bar — exactly the daemon's row floor.
2562 const ok = try layoutStripes(alloc, 2, 6); 2614 var f = try tree.flatten(alloc, 6, 80, wallFloors(2), null);
2563 defer alloc.free(ok); 2615 defer f.deinit(alloc);
2564 try std.testing.expectEqual(@as(u16, 3), ok[0].rows); 2616 try std.testing.expectEqual(@as(u16, 3), f.rectOf(0).?.rows);
2565 try std.testing.expectEqual(@as(u16, 3), ok[1].rows); 2617 try std.testing.expectEqual(@as(u16, 3), f.rectOf(1).?.rows);
2566 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 6 }, .is_tty = false }; 2618 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 6 }, .is_tty = false };
2567 shared.label_rows = 1; // two tiles draw a bar 2619 shared.label_rows = 1; // two tiles draw a bar
2568 var t0 = Tile{ 2620 var t0 = Tile{
2569 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 2621 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
2570 .stripe = ok[0], 2622 .rect = f.rectOf(0).?,
2571 .shared = &shared, 2623 .shared = &shared,
2572 .idx = 0, 2624 .idx = 0,
2573 .wake_r = -1, 2625 .wake_r = -1,
@@ -2575,7 +2627,7 @@ test "layoutStripes refuses a multi-tile cut too thin for the daemon's row floor
2575 }; 2627 };
2576 var t1 = Tile{ 2628 var t1 = Tile{
2577 .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" }, 2629 .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" },
2578 .stripe = ok[1], 2630 .rect = f.rectOf(1).?,
2579 .shared = &shared, 2631 .shared = &shared,
2580 .idx = 1, 2632 .idx = 1,
2581 .wake_r = -1, 2633 .wake_r = -1,
@@ -2584,9 +2636,12 @@ test "layoutStripes refuses a multi-tile cut too thin for the daemon's row floor
2584 try std.testing.expectEqual(proto.min_session_rows, t0.viewRows()); 2636 try std.testing.expectEqual(proto.min_session_rows, t0.viewRows());
2585 try std.testing.expectEqual(proto.min_session_rows, t1.viewRows()); 2637 try std.testing.expectEqual(proto.min_session_rows, t1.viewRows());
2586 // A one-tile wall draws no bar, so its floor is the daemon's alone. 2638 // A one-tile wall draws no bar, so its floor is the daemon's alone.
2587 const one = try layoutStripes(alloc, 1, 2); 2639 var one = layout.Tree.init(alloc);
2588 defer alloc.free(one); 2640 defer one.deinit();
2589 try std.testing.expectEqual(@as(u16, 2), one[0].rows); 2641 try one.addFirst(0);
2642 var of = try one.flatten(alloc, 2, 80, wallFloors(1), null);
2643 defer of.deinit(alloc);
2644 try std.testing.expectEqual(@as(u16, 2), of.rectOf(0).?.rows);
2590 } 2645 }
2591 2646
2592 test "resolveSpelling: --sock with a session, label verbatim" { 2647 test "resolveSpelling: --sock with a session, label verbatim" {
@@ -2673,7 +2728,7 @@ fn endBench(
2673 for (tiles, 0..) |*t, i| { 2728 for (tiles, 0..) |*t, i| {
2674 t.* = .{ 2729 t.* = .{
2675 .r = .{ .target = .{ .sock = "/s" }, .label = "t", .session = "" }, 2730 .r = .{ .target = .{ .sock = "/s" }, .label = "t", .session = "" },
2676 .stripe = .{ .top = 0, .rows = 2 }, 2731 .rect = .{ .top = 0, .left = 0, .rows = 2, .cols = 80 },
2677 .shared = shared, 2732 .shared = shared,
2678 .idx = i, 2733 .idx = i,
2679 }; 2734 };
@@ -2687,7 +2742,7 @@ test "sendKeys: a chunk that does not fit is dropped whole, and says so" {
2687 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; 2742 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
2688 var t = Tile{ 2743 var t = Tile{
2689 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 2744 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
2690 .stripe = .{ .top = 0, .rows = 4 }, 2745 .rect = .{ .top = 0, .left = 0, .rows = 4, .cols = 80 },
2691 .shared = &shared, 2746 .shared = &shared,
2692 .idx = 0, 2747 .idx = 0,
2693 // -1 both ends: `ring` writes to the doorbell and ignores the 2748 // -1 both ends: `ring` writes to the doorbell and ignores the
@@ -2892,7 +2947,7 @@ test "viewRows: a one-tile wall keeps every row, a multi-tile wall loses the lab
2892 shared.label_rows = 0; 2947 shared.label_rows = 0;
2893 var t0 = Tile{ 2948 var t0 = Tile{
2894 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 2949 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
2895 .stripe = .{ .top = 0, .rows = 24 }, 2950 .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
2896 .shared = &shared, 2951 .shared = &shared,
2897 .idx = 0, 2952 .idx = 0,
2898 .wake_r = -1, 2953 .wake_r = -1,
@@ -2904,7 +2959,7 @@ test "viewRows: a one-tile wall keeps every row, a multi-tile wall loses the lab
2904 shared.label_rows = 1; 2959 shared.label_rows = 1;
2905 var t1 = Tile{ 2960 var t1 = Tile{
2906 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 2961 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
2907 .stripe = .{ .top = 0, .rows = 12 }, 2962 .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
2908 .shared = &shared, 2963 .shared = &shared,
2909 .idx = 0, 2964 .idx = 0,
2910 .wake_r = -1, 2965 .wake_r = -1,
@@ -2912,7 +2967,7 @@ test "viewRows: a one-tile wall keeps every row, a multi-tile wall loses the lab
2912 }; 2967 };
2913 var t2 = Tile{ 2968 var t2 = Tile{
2914 .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" }, 2969 .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" },
2915 .stripe = .{ .top = 12, .rows = 12 }, 2970 .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
2916 .shared = &shared, 2971 .shared = &shared,
2917 .idx = 1, 2972 .idx = 1,
2918 .wake_r = -1, 2973 .wake_r = -1,
@@ -2934,7 +2989,7 @@ test "relayout sets resize_pending on every live tile" {
2934 for (tiles, 0..) |*t, i| { 2989 for (tiles, 0..) |*t, i| {
2935 t.* = Tile{ 2990 t.* = Tile{
2936 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 2991 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
2937 .stripe = .{ .top = 0, .rows = 24 }, 2992 .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
2938 .shared = &shared, 2993 .shared = &shared,
2939 .idx = i, 2994 .idx = i,
2940 .wake_r = -1, 2995 .wake_r = -1,
@@ -2961,7 +3016,7 @@ test "a pump's answer that grew the wall re-cuts it; a mere focus move does not"
2961 for (tiles, 0..) |*t, i| { 3016 for (tiles, 0..) |*t, i| {
2962 t.* = Tile{ 3017 t.* = Tile{
2963 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 3018 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
2964 .stripe = .{ .top = 0, .rows = 24 }, 3019 .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
2965 .shared = &shared, 3020 .shared = &shared,
2966 .idx = i, 3021 .idx = i,
2967 .wake_r = -1, 3022 .wake_r = -1,
@@ -2997,7 +3052,7 @@ test "a one-tile wall draws no label bar and paints row 1" {
2997 present[0] = true; 3052 present[0] = true;
2998 tiles[0] = Tile{ 3053 tiles[0] = Tile{
2999 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 3054 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
3000 .stripe = .{ .top = 0, .rows = 24 }, 3055 .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
3001 .shared = &shared, 3056 .shared = &shared,
3002 .idx = 0, 3057 .idx = 0,
3003 .wake_r = -1, 3058 .wake_r = -1,
@@ -3029,7 +3084,7 @@ test "focus follows a click: rectHit picks the tile under a row" {
3029 var tiles: [2]Tile = undefined; 3084 var tiles: [2]Tile = undefined;
3030 tiles[0] = Tile{ 3085 tiles[0] = Tile{
3031 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 3086 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
3032 .stripe = .{ .top = 0, .rows = 12 }, 3087 .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
3033 .shared = &shared, 3088 .shared = &shared,
3034 .idx = 0, 3089 .idx = 0,
3035 .wake_r = -1, 3090 .wake_r = -1,
@@ -3037,7 +3092,7 @@ test "focus follows a click: rectHit picks the tile under a row" {
3037 }; 3092 };
3038 tiles[1] = Tile{ 3093 tiles[1] = Tile{
3039 .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" }, 3094 .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" },
3040 .stripe = .{ .top = 12, .rows = 12 }, 3095 .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
3041 .shared = &shared, 3096 .shared = &shared,
3042 .idx = 1, 3097 .idx = 1,
3043 .wake_r = -1, 3098 .wake_r = -1,
@@ -3087,7 +3142,7 @@ test "a copy too big for OSC 52 is said out loud rather than dropped" {
3087 var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; 3142 var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
3088 var t = Tile{ 3143 var t = Tile{
3089 .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, 3144 .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" },
3090 .stripe = .{ .top = 0, .rows = 24 }, 3145 .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
3091 .shared = &shared, 3146 .shared = &shared,
3092 .idx = 0, 3147 .idx = 0,
3093 .wake_r = -1, 3148 .wake_r = -1,
@@ -3139,7 +3194,7 @@ test "setFocus writes the outgoing tile's release before the store that lets the
3139 var tiles: [2]Tile = undefined; 3194 var tiles: [2]Tile = undefined;
3140 tiles[0] = Tile{ 3195 tiles[0] = Tile{
3141 .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, 3196 .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" },
3142 .stripe = .{ .top = 0, .rows = 12 }, 3197 .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
3143 .shared = &shared, 3198 .shared = &shared,
3144 .idx = 0, 3199 .idx = 0,
3145 .wake_r = -1, 3200 .wake_r = -1,
@@ -3147,7 +3202,7 @@ test "setFocus writes the outgoing tile's release before the store that lets the
3147 }; 3202 };
3148 tiles[1] = Tile{ 3203 tiles[1] = Tile{
3149 .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, 3204 .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" },
3150 .stripe = .{ .top = 12, .rows = 12 }, 3205 .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
3151 .shared = &shared, 3206 .shared = &shared,
3152 .idx = 1, 3207 .idx = 1,
3153 .wake_r = -1, 3208 .wake_r = -1,
@@ -3198,7 +3253,7 @@ test "a relayout drops every tile's highlight, because the stripes move under it
3198 for (tiles, 0..) |*t, i| { 3253 for (tiles, 0..) |*t, i| {
3199 t.* = Tile{ 3254 t.* = Tile{
3200 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 3255 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
3201 .stripe = .{ .top = 0, .rows = 24 }, 3256 .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
3202 .shared = &shared, 3257 .shared = &shared,
3203 .idx = i, 3258 .idx = i,
3204 .wake_r = -1, 3259 .wake_r = -1,
@@ -3222,7 +3277,7 @@ test "a reconnect drops the highlight over its own tile, and only its own" {
3222 var tiles: [2]Tile = undefined; 3277 var tiles: [2]Tile = undefined;
3223 tiles[0] = Tile{ 3278 tiles[0] = Tile{
3224 .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, 3279 .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" },
3225 .stripe = .{ .top = 0, .rows = 12 }, 3280 .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
3226 .shared = &shared, 3281 .shared = &shared,
3227 .idx = 0, 3282 .idx = 0,
3228 .wake_r = -1, 3283 .wake_r = -1,
@@ -3230,7 +3285,7 @@ test "a reconnect drops the highlight over its own tile, and only its own" {
3230 }; 3285 };
3231 tiles[1] = Tile{ 3286 tiles[1] = Tile{
3232 .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, 3287 .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" },
3233 .stripe = .{ .top = 12, .rows = 12 }, 3288 .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
3234 .shared = &shared, 3289 .shared = &shared,
3235 .idx = 1, 3290 .idx = 1,
3236 .wake_r = -1, 3291 .wake_r = -1,
@@ -3270,7 +3325,7 @@ test "a focus move drops what the wall's input filters are holding" {
3270 var tiles: [2]Tile = undefined; 3325 var tiles: [2]Tile = undefined;
3271 tiles[0] = Tile{ 3326 tiles[0] = Tile{
3272 .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, 3327 .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" },
3273 .stripe = .{ .top = 0, .rows = 12 }, 3328 .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
3274 .shared = &shared, 3329 .shared = &shared,
3275 .idx = 0, 3330 .idx = 0,
3276 .wake_r = -1, 3331 .wake_r = -1,
@@ -3278,7 +3333,7 @@ test "a focus move drops what the wall's input filters are holding" {
3278 }; 3333 };
3279 tiles[1] = Tile{ 3334 tiles[1] = Tile{
3280 .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, 3335 .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" },
3281 .stripe = .{ .top = 12, .rows = 12 }, 3336 .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
3282 .shared = &shared, 3337 .shared = &shared,
3283 .idx = 1, 3338 .idx = 1,
3284 .wake_r = -1, 3339 .wake_r = -1,
@@ -3469,7 +3524,7 @@ test "agent forwarding is per tile: no -A offers nothing and opens nothing" {
3469 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; 3524 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
3470 var t = Tile{ 3525 var t = Tile{
3471 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, 3526 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false },
3472 .stripe = .{ .top = 0, .rows = 4 }, 3527 .rect = .{ .top = 0, .left = 0, .rows = 4, .cols = 80 },
3473 .shared = &shared, 3528 .shared = &shared,
3474 .idx = 0, 3529 .idx = 0,
3475 .wake_r = -1, 3530 .wake_r = -1,
@@ -3514,7 +3569,7 @@ test "a view tile's attach makes no size claim; a tile the user asked for does"
3514 shared.label_rows = 0; 3569 shared.label_rows = 0;
3515 var t = Tile{ 3570 var t = Tile{
3516 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, 3571 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false },
3517 .stripe = .{ .top = 0, .rows = 24 }, 3572 .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
3518 .shared = &shared, 3573 .shared = &shared,
3519 .idx = 0, 3574 .idx = 0,
3520 .wake_r = -1, 3575 .wake_r = -1,
@@ -3565,7 +3620,7 @@ test "a newborn tile's first claim is its real stripe, not the placeholder" {
3565 // Real stripe for the second tile of a 2-tile wall on 24 rows. 3620 // Real stripe for the second tile of a 2-tile wall on 24 rows.
3566 var t = Tile{ 3621 var t = Tile{
3567 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, 3622 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false },
3568 .stripe = .{ .top = 12, .rows = 12 }, 3623 .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
3569 .shared = &shared, 3624 .shared = &shared,
3570 .idx = 1, 3625 .idx = 1,
3571 .wake_r = -1, 3626 .wake_r = -1,
@@ -3602,7 +3657,7 @@ test "the cursor sleeps in the focused tile, whoever painted last" {
3602 var tiles: [2]Tile = undefined; 3657 var tiles: [2]Tile = undefined;
3603 tiles[0] = Tile{ 3658 tiles[0] = Tile{
3604 .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, 3659 .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" },
3605 .stripe = .{ .top = 0, .rows = 12 }, 3660 .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
3606 .shared = &shared, 3661 .shared = &shared,
3607 .idx = 0, 3662 .idx = 0,
3608 .wake_r = -1, 3663 .wake_r = -1,
@@ -3610,7 +3665,7 @@ test "the cursor sleeps in the focused tile, whoever painted last" {
3610 }; 3665 };
3611 tiles[1] = Tile{ 3666 tiles[1] = Tile{
3612 .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, 3667 .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" },
3613 .stripe = .{ .top = 12, .rows = 12 }, 3668 .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
3614 .shared = &shared, 3669 .shared = &shared,
3615 .idx = 1, 3670 .idx = 1,
3616 .wake_r = -1, 3671 .wake_r = -1,