a73x

131681f2

refactor: the wall is one value, not six parameters

a73x   2026-08-29 10:01

Commit message
refactor: the wall is one value, not six parameters

(alloc, tiles, present, live, shared, host_table) was the parameter list
of fourteen functions across wallview, wall_host, wall_picker and
wall_layout, and every multi-line call site spelled it again. `Wall` is
that value: the fixed tile array, which slots are on the wall, the
high-water mark of slots ever used, the shared state, and the hosts being
polled — with `liveTiles`/`livePresent` naming the prefix walk that the
six loose parameters could not distinguish from a walk of the whole array.
`hosts` is re-sliced at the one place `hosts_live` grows.

Keyboard-thread only: a pump still gets a `*Tile` and a `*Shared`, so
`paint_mu`'s ownership of the layout tree is untouched, and every rect
still comes from relayout.

Tests keep their arrays and build the same Wall through wall_test_harness
(wallOf / wallLive / wallAll). wall_layout.zig's budget drops 1596 -> 1432
with the signatures it documents. Pinned by the whole wall_test_* suite
and e2e 07_wallcli 5, 12_panes 10, 09_hosts 10.

docscheck.budget
Old New
@@ -51,7 +51,7 @@ server_sessions.zig 0
51 wall_host.zig 0 51 wall_host.zig 0
52 wall_picker.zig 0 52 wall_picker.zig 0
53 wall_pump.zig 0 53 wall_pump.zig 0
54 wall_layout.zig 1596 54 wall_layout.zig 1432
55 wall_test_harness.zig 0 55 wall_test_harness.zig 0
56 wall_test_host.zig 0 56 wall_test_host.zig 0
57 wall_test_picker.zig 0 57 wall_test_picker.zig 0
src/tui/wall_host.zig
Old New
@@ -15,6 +15,7 @@ const wall_layout = @import("wall_layout.zig");
15 const wv = @import("wallview.zig"); 15 const wv = @import("wallview.zig");
16 const Shared = wv.Shared; 16 const Shared = wv.Shared;
17 const Tile = wv.Tile; 17 const Tile = wv.Tile;
18 const Wall = wv.Wall;
18 19
19 pub const Resolved = struct { 20 pub const Resolved = struct {
20 target: client.Target, 21 target: client.Target,
@@ -418,23 +419,16 @@ pub fn pokeHost(host_table: []Host, t: *const Tile) void {
418 } 419 }
419 420
420 /// Every host with news, applied to the wall. True when any reported. 421 /// Every host with news, applied to the wall. True when any reported.
421 pub fn applyReadyLists( 422 pub fn applyReadyLists(w: Wall) bool {
422 alloc: std.mem.Allocator,
423 tiles: []Tile,
424 present: []bool,
425 live: *usize,
426 shared: *Shared,
427 host_table: []Host,
428 ) bool {
429 var news = false; 423 var news = false;
430 for (host_table, 0..) |*h, hi| { 424 for (w.hosts, 0..) |*h, hi| {
431 if (!h.list_ready.swap(false, .acq_rel)) continue; 425 if (!h.list_ready.swap(false, .acq_rel)) continue;
432 news = true; 426 news = true;
433 // A poll already in flight when the host was forgotten still lands. 427 // A poll already in flight when the host was forgotten still lands.
434 // Applying it would re-birth the tiles the forget just took off the 428 // Applying it would re-birth the tiles the forget just took off the
435 // wall, one poll later. 429 // wall, one poll later.
436 if (h.forgotten.load(.acquire)) continue; 430 if (h.forgotten.load(.acquire)) continue;
437 applyHostList(alloc, tiles, present, live, shared, host_table, hi); 431 applyHostList(w, hi);
438 h.applied = true; 432 h.applied = true;
439 } 433 }
440 return news; 434 return news;
@@ -442,16 +436,8 @@ pub fn applyReadyLists(
442 436
443 /// One host's list, applied to the wall. The keyboard thread only: it is 437 /// One host's list, applied to the wall. The keyboard thread only: it is
444 /// the single writer of the tile array and the layout tree. 438 /// the single writer of the tile array and the layout tree.
445 pub fn applyHostList( 439 pub fn applyHostList(w: Wall, hi: usize) void {
446 alloc: std.mem.Allocator, 440 const h = &w.hosts[hi];
447 tiles: []Tile,
448 present: []bool,
449 live: *usize,
450 shared: *Shared,
451 host_table: []Host,
452 hi: usize,
453 ) void {
454 const h = &host_table[hi];
455 const reachable = h.reachable.load(.acquire); 441 const reachable = h.reachable.load(.acquire);
456 var list_buf: [proto.sessions_text_max]u8 = undefined; 442 var list_buf: [proto.sessions_text_max]u8 = undefined;
457 var list: []const u8 = ""; 443 var list: []const u8 = "";
@@ -460,7 +446,7 @@ pub fn applyHostList(
460 // wall has none, and a vanish can take the one there was — either way 446 // wall has none, and a vanish can take the one there was — either way
461 // the wall owes the tile it ends up with a `setFocus`, which is the only 447 // the wall owes the tile it ends up with a `setFocus`, which is the only
462 // thing that arms a claim. 448 // thing that arms a claim.
463 const had_focus = shared.sel < live.* and present[shared.sel]; 449 const had_focus = w.shared.sel < w.live.* and w.present[w.shared.sel];
464 var changed = false; 450 var changed = false;
465 // Only a list DRIVES the diff. A host that has gone quiet keeps its 451 // Only a list DRIVES the diff. A host that has gone quiet keeps its
466 // tiles, which reconnect on their own; vanishing them on a failed poll 452 // tiles, which reconnect on their own; vanishing them on a failed poll
@@ -468,9 +454,9 @@ pub fn applyHostList(
468 if (reachable) { 454 if (reachable) {
469 var births = BirthNames{}; 455 var births = BirthNames{};
470 var vanish = TileIdxs{}; 456 var vanish = TileIdxs{};
471 planHostDiff(tiles[0..live.*], present[0..live.*], live.*, hi, list, h.self_name, &births, &vanish); 457 planHostDiff(w.liveTiles(), w.livePresent(), w.live.*, hi, list, h.self_name, &births, &vanish);
472 for (vanish.items[0..vanish.len]) |v| { 458 for (vanish.items[0..vanish.len]) |v| {
473 wv.vanishTile(tiles[0..live.*], present[0..live.*], shared, v, null); 459 wv.vanishTile(w.liveTiles(), w.livePresent(), w.shared, v, null);
474 changed = true; 460 changed = true;
475 } 461 }
476 var placed: usize = 0; 462 var placed: usize = 0;
@@ -479,14 +465,14 @@ pub fn applyHostList(
479 // anchor, so anchoring every birth at the focus would lay a list of 465 // anchor, so anchoring every birth at the focus would lay a list of
480 // {b, c} out as c, b — a wall reading back-to-front against the 466 // {b, c} out as c, b — a wall reading back-to-front against the
481 // order its daemon reported, and against the digits the chords use. 467 // order its daemon reported, and against the digits the chords use.
482 var anchor = wall_layout.anchorTile(present[0..live.*], shared.sel); 468 var anchor = wall_layout.anchorTile(w.livePresent(), w.shared.sel);
483 // Stops at the FIRST refusal rather than retrying each name: the 469 // Stops at the FIRST refusal rather than retrying each name: the
484 // wall refuses for a reason that holds for the whole list (no slot, 470 // wall refuses for a reason that holds for the whole list (no slot,
485 // no room to cut), and this list comes back every second — a 471 // no room to cut), and this list comes back every second — a
486 // per-name retry is an insert, a flatten and an undo per name per 472 // per-name retry is an insert, a flatten and an undo per name per
487 // poll, forever. 473 // poll, forever.
488 while (placed < births.len) : (placed += 1) { 474 while (placed < births.len) : (placed += 1) {
489 const at = wv.birthTile(alloc, tiles, present, live, shared, .{ 475 const at = wv.birthTile(w, .{
490 // Joins, never creates: the daemon already has this session, 476 // Joins, never creates: the daemon already has this session,
491 // and a sized attach on a live one would resize somebody. 477 // and a sized attach on a live one would resize somebody.
492 // The name is this poll's reply buffer until the wall takes 478 // The name is this poll's reply buffer until the wall takes
@@ -500,7 +486,7 @@ pub fn applyHostList(
500 .borrowed = true, 486 .borrowed = true,
501 }) orelse break; 487 }) orelse break;
502 anchor = at; 488 anchor = at;
503 wv.spawnPump(&tiles[at]); 489 wv.spawnPump(&w.tiles[at]);
504 changed = true; 490 changed = true;
505 } 491 }
506 const unplaced = births.dropped + (births.len - placed); 492 const unplaced = births.dropped + (births.len - placed);
@@ -508,13 +494,13 @@ pub fn applyHostList(
508 // daemon's sessions is a wall lying about what it is. 494 // daemon's sessions is a wall lying about what it is.
509 if (unplaced > 0) { 495 if (unplaced > 0) {
510 var buf: [48]u8 = undefined; 496 var buf: [48]u8 = undefined;
511 wv.setNoticeIdle(shared, std.fmt.bufPrint(&buf, "[+{d} not shown]", .{unplaced}) catch "[not shown]"); 497 wv.setNoticeIdle(w.shared, std.fmt.bufPrint(&buf, "[+{d} not shown]", .{unplaced}) catch "[not shown]");
512 } 498 }
513 } 499 }
514 if ((!had_focus or shared.sel >= live.* or !present[shared.sel]) and 500 if ((!had_focus or w.shared.sel >= w.live.* or !w.present[w.shared.sel]) and
515 wv.presentCount(present[0..live.*]) > 0) 501 wv.presentCount(w.livePresent()) > 0)
516 wv.setFocus(tiles[0..live.*], shared, wall_layout.firstPresent(present[0..live.*]) orelse 0); 502 wv.setFocus(w.liveTiles(), w.shared, wall_layout.firstPresent(w.livePresent()) orelse 0);
517 if (changed) wall_layout.relayout(alloc, tiles[0..live.*], present[0..live.*], shared, shared.sel); 503 if (changed) wall_layout.relayout(w, w.shared.sel);
518 } 504 }
519 505
520 /// The host grammar's own spelling of a target, for a wall entered by 506 /// The host grammar's own spelling of a target, for a wall entered by
src/tui/wall_layout.zig
Old New
@@ -12,6 +12,7 @@ const wv = @import("wallview.zig");
12 const Resolved = wall_host.Resolved; 12 const Resolved = wall_host.Resolved;
13 const Shared = wv.Shared; 13 const Shared = wv.Shared;
14 const Tile = wv.Tile; 14 const Tile = wv.Tile;
15 const Wall = wv.Wall;
15 16
16 /// The daemon's row floor plus the label-bar arithmetic: a one-tile wall 17 /// The daemon's row floor plus the label-bar arithmetic: a one-tile wall
17 /// draws no bar so its floor is `min_session_rows`; two or more tiles each 18 /// draws no bar so its floor is `min_session_rows`; two or more tiles each
@@ -46,28 +47,21 @@ pub fn dirOf(d: interact.PrefixFilter.Dir) layout.Dir {
46 /// layout.resize always GAINS focus cells; shrink = grow a neighbor at 47 /// layout.resize always GAINS focus cells; shrink = grow a neighbor at
47 /// focus's expense. Fullscreen refuses — a hidden layout resizing 48 /// focus's expense. Fullscreen refuses — a hidden layout resizing
48 /// invisibly is surprise, not power. 49 /// invisibly is surprise, not power.
49 pub fn doResize( 50 pub fn doResize(w: Wall, sel: usize, d: interact.PrefixFilter.Dir) bool {
50 alloc: std.mem.Allocator, 51 if (w.shared.fullscreen) return false;
51 tiles: []Tile,
52 present: []const bool,
53 shared: *Shared,
54 sel: usize,
55 d: interact.PrefixFilter.Dir,
56 ) bool {
57 if (shared.fullscreen) return false;
58 const ld = dirOf(d); 52 const ld = dirOf(d);
59 const grow = switch (ld) { 53 const grow = switch (ld) {
60 .right, .down => true, 54 .right, .down => true,
61 .left, .up => false, 55 .left, .up => false,
62 }; 56 };
63 const flat = shared.base_flat orelse shared.last_flat orelse return false; 57 const flat = w.shared.base_flat orelse w.shared.last_flat orelse return false;
64 const focus_tile: u8 = @intCast(sel); 58 const focus_tile: u8 = @intCast(sel);
65 var moved = false; 59 var moved = false;
66 if (grow) { 60 if (grow) {
67 // Focus gains from the sibling toward `ld`; if none there (edge 61 // Focus gains from the sibling toward `ld`; if none there (edge
68 // pane), try the opposite side — gaining from either sibling 62 // pane), try the opposite side — gaining from either sibling
69 // widens or tallens the focus. 63 // widens or tallens the focus.
70 if (shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), focus_tile, ld, 1)) { 64 if (w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.liveTiles().len), focus_tile, ld, 1)) {
71 moved = true; 65 moved = true;
72 } else { 66 } else {
73 const opp = switch (ld) { 67 const opp = switch (ld) {
@@ -76,7 +70,7 @@ pub fn doResize(
76 .left => layout.Dir.right, 70 .left => layout.Dir.right,
77 .up => layout.Dir.down, 71 .up => layout.Dir.down,
78 }; 72 };
79 moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), focus_tile, opp, 1); 73 moved = w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.liveTiles().len), focus_tile, opp, 1);
80 } 74 }
81 } else { 75 } else {
82 // Shrink: a neighbor on the same axis gains a cell from focus. 76 // Shrink: a neighbor on the same axis gains a cell from focus.
@@ -89,15 +83,15 @@ pub fn doResize(
89 .down => layout.Dir.up, 83 .down => layout.Dir.up,
90 }; 84 };
91 if (layout.neighbor(flat, focus_tile, ld)) |nb| { 85 if (layout.neighbor(flat, focus_tile, ld)) |nb| {
92 moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), nb, opp, 1); 86 moved = w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.liveTiles().len), nb, opp, 1);
93 } 87 }
94 if (!moved) { 88 if (!moved) {
95 if (layout.neighbor(flat, focus_tile, opp)) |nb| { 89 if (layout.neighbor(flat, focus_tile, opp)) |nb| {
96 moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), nb, ld, 1); 90 moved = w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.liveTiles().len), nb, ld, 1);
97 } 91 }
98 } 92 }
99 } 93 }
100 if (moved) relayout(alloc, tiles, present, shared, sel); 94 if (moved) relayout(w, sel);
101 return moved; 95 return moved;
102 } 96 }
103 97
@@ -120,41 +114,35 @@ fn paintRailsLocked(shared: *Shared, flat: layout.Flat) void {
120 114
121 /// One `paint_mu` hold: no window where a pump paints rows that just 115 /// One `paint_mu` hold: no window where a pump paints rows that just
122 /// changed owner. 116 /// changed owner.
123 pub fn relayout( 117 pub fn relayout(w: Wall, sel: usize) void {
124 alloc: std.mem.Allocator, 118 w.shared.paint_mu.lock();
125 tiles: []Tile, 119 defer w.shared.paint_mu.unlock();
126 present: []const bool, 120 w.shared.sel = sel;
127 shared: *Shared,
128 sel: usize,
129 ) void {
130 shared.paint_mu.lock();
131 defer shared.paint_mu.unlock();
132 shared.sel = sel;
133 121
134 var live: usize = 0; 122 var live: usize = 0;
135 for (present) |p| { 123 for (w.livePresent()) |p| {
136 if (p) live += 1; 124 if (p) live += 1;
137 } 125 }
138 // A one-tile wall owns every row and draws no label bar; two or more 126 // A one-tile wall owns every row and draws no label bar; two or more
139 // tiles each lose their top row to one. Fullscreen is the same: one 127 // tiles each lose their top row to one. Fullscreen is the same: one
140 // visible pane, no bar — the plain client's byte stream. 128 // visible pane, no bar — the plain client's byte stream.
141 shared.label_rows = if (shared.fullscreen or live <= 1) 0 else 1; 129 w.shared.label_rows = if (w.shared.fullscreen or live <= 1) 0 else 1;
142 if (shared.is_tty) proto.writeAllFd(shared.out_fd, "\x1b[?25l\x1b[H\x1b[2J") catch {}; 130 if (w.shared.is_tty) proto.writeAllFd(w.shared.out_fd, "\x1b[?25l\x1b[H\x1b[2J") catch {};
143 // The screen the popup was on has just been cleared, so the next paint 131 // The screen the popup was on has just been cleared, so the next paint
144 // owes it however unchanged its rows are. 132 // owes it however unchanged its rows are.
145 if (shared.picker_open.load(.acquire)) shared.picker_stamp = 0; 133 if (w.shared.picker_open.load(.acquire)) w.shared.picker_stamp = 0;
146 if (live == 0) { 134 if (live == 0) {
147 wv.paintEmptyWallLocked(shared); 135 wv.paintEmptyWallLocked(w.shared);
148 return; 136 return;
149 } 137 }
150 // The base flat (null) is always computed so `focus_dir` can read 138 // The base flat (null) is always computed so `focus_dir` can read
151 // adjacency from the real layout while fullscreened. 139 // adjacency from the real layout while fullscreened.
152 if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), null)) |base| { 140 if (w.shared.tree.flatten(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(live), null)) |base| {
153 if (shared.base_flat) |*old| old.deinit(shared.flat_alloc); 141 if (w.shared.base_flat) |*old| old.deinit(w.shared.flat_alloc);
154 shared.base_flat = base; 142 w.shared.base_flat = base;
155 } else |_| {} 143 } else |_| {}
156 const fs_arg: ?u8 = if (shared.fullscreen) @intCast(sel) else null; 144 const fs_arg: ?u8 = if (w.shared.fullscreen) @intCast(sel) else null;
157 var cut = shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), fs_arg); 145 var cut = w.shared.tree.flatten(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(live), fs_arg);
158 if (cut) |_| {} else |e| { 146 if (cut) |_| {} else |e| {
159 // A split, an insert, a resize key: those are OPERATIONS, the user 147 // A split, an insert, a resize key: those are OPERATIONS, the user
160 // asked, and refusing leaves the screen exactly as it was. A 148 // asked, and refusing leaves the screen exactly as it was. A
@@ -165,32 +153,32 @@ pub fn relayout(
165 // whole, the rest at 0x0, which paints nothing and claims no size. 153 // whole, the rest at 0x0, which paints nothing and claims no size.
166 // The tree is untouched, so growing back re-cuts every pane. 154 // The tree is untouched, so growing back re-cuts every pane.
167 if (e == error.TooSmall and fs_arg == null) { 155 if (e == error.TooSmall and fs_arg == null) {
168 if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), @intCast(sel))) |only| { 156 if (w.shared.tree.flatten(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(live), @intCast(sel))) |only| {
169 cut = only; 157 cut = only;
170 // One visible pane draws no bar. Left at 1, `viewRows` 158 // One visible pane draws no bar. Left at 1, `viewRows`
171 // would owe the daemon a row the tile does not have. 159 // would owe the daemon a row the tile does not have.
172 shared.label_rows = 0; 160 w.shared.label_rows = 0;
173 } else |_| {} 161 } else |_| {}
174 } 162 }
175 } 163 }
176 if (cut) |flat| { 164 if (cut) |flat| {
177 if (shared.last_flat) |*old| old.deinit(shared.flat_alloc); 165 if (w.shared.last_flat) |*old| old.deinit(w.shared.flat_alloc);
178 shared.last_flat = flat; 166 w.shared.last_flat = flat;
179 for (tiles, present) |*t, p| { 167 for (w.liveTiles(), w.livePresent()) |*t, p| {
180 if (!p) continue; 168 if (!p) continue;
181 if (flat.rectOf(@intCast(t.idx))) |r| { 169 if (flat.rectOf(@intCast(t.idx))) |r| {
182 t.rect = r; 170 t.rect = r;
183 } 171 }
184 t.resize_pending = true; 172 t.resize_pending = true;
185 } 173 }
186 paintRailsLocked(shared, flat); 174 paintRailsLocked(w.shared, flat);
187 } else |_| {} 175 } else |_| {}
188 176
189 // The generation bump is what puts the rects back: every surviving 177 // The generation bump is what puts the rects back: every surviving
190 // pump repaints from its hot replica at its NEW rows, and the doorbell 178 // pump repaints from its hot replica at its NEW rows, and the doorbell
191 // makes that immediate rather than one poll timeout away. 179 // makes that immediate rather than one poll timeout away.
192 _ = shared.repaint_gen.fetchAdd(1, .release); 180 _ = w.shared.repaint_gen.fetchAdd(1, .release);
193 for (tiles, present) |*t, p| { 181 for (w.liveTiles(), w.livePresent()) |*t, p| {
194 if (p) wv.ring(t); 182 if (p) wv.ring(t);
195 } 183 }
196 // ...except the tiles with no pump left to hear it: their bars are the 184 // ...except the tiles with no pump left to hear it: their bars are the
@@ -198,7 +186,7 @@ pub fn relayout(
198 // `label_rows` guard here, unlike there: the screen was just cleared, 186 // `label_rows` guard here, unlike there: the screen was just cleared,
199 // so on a one-tile wall of a dead tile that bar is the only thing left 187 // so on a one-tile wall of a dead tile that bar is the only thing left
200 // to say the target refused. 188 // to say the target refused.
201 wv.paintDeadBarsLocked(tiles); 189 wv.paintDeadBarsLocked(w.liveTiles());
202 } 190 }
203 191
204 /// Rebuild the wall's tree from a saved sidecar, healing per leaf: each 192 /// Rebuild the wall's tree from a saved sidecar, healing per leaf: each
@@ -326,34 +314,22 @@ pub fn saveLayoutTo(
326 } 314 }
327 315
328 /// Resolves the sidecar path from env and delegates to `saveLayoutTo`. 316 /// Resolves the sidecar path from env and delegates to `saveLayoutTo`.
329 pub fn saveSidecar( 317 pub fn saveSidecar(w: Wall) void {
330 alloc: std.mem.Allocator,
331 tiles: []Tile,
332 present: []const bool,
333 shared: *Shared,
334 ) void {
335 // A pipe has no stripes, so it has no layout worth remembering — and 318 // A pipe has no stripes, so it has no layout worth remembering — and
336 // a tree it saved would be a tree the next TERMINAL restores over the 319 // a tree it saved would be a tree the next TERMINAL restores over the
337 // aspect rule. See `restoreSidecar` for the cost of the other half. 320 // aspect rule. See `restoreSidecar` for the cost of the other half.
338 if (!shared.is_tty) return; 321 if (!w.shared.is_tty) return;
339 const path = wall.layoutPath(alloc) catch |err| { 322 const path = wall.layoutPath(w.alloc) catch |err| {
340 std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)}); 323 std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)});
341 return; 324 return;
342 }; 325 };
343 defer alloc.free(path); 326 defer w.alloc.free(path);
344 saveLayoutTo(alloc, path, tiles, present, shared); 327 saveLayoutTo(w.alloc, path, w.liveTiles(), w.livePresent(), w.shared);
345 } 328 }
346 329
347 /// The saved layout over the tiles the hosts turned out to have; null 330 /// The saved layout over the tiles the hosts turned out to have; null
348 /// when nothing matched. 331 /// when nothing matched.
349 pub fn restoreSidecar( 332 pub fn restoreSidecar(w: Wall, focus_out: *?usize) ?void {
350 alloc: std.mem.Allocator,
351 tiles: []Tile,
352 present: []bool,
353 live: usize,
354 shared: *Shared,
355 focus_out: *?usize,
356 ) ?void {
357 // Not on a pipe, and the cost is why. A restored tree is applied by 333 // Not on a pipe, and the cost is why. A restored tree is applied by
358 // `focusAnswer(recut = true)`, whose `relayout` flags `resize_pending` 334 // `focusAnswer(recut = true)`, whose `relayout` flags `resize_pending`
359 // on EVERY present tile whether or not that tile's rect moved; each 335 // on EVERY present tile whether or not that tile's rect moved; each
@@ -363,33 +339,25 @@ pub fn restoreSidecar(
363 // a sidecar: a second full snapshot on the wire for a wall of one tile 339 // a sidecar: a second full snapshot on the wire for a wall of one tile
364 // that was already the right shape. The CLAIM is innocent; it stopped 340 // that was already the right shape. The CLAIM is innocent; it stopped
365 // sending a resize (see the pump's focus-claim block). 341 // sending a resize (see the pump's focus-claim block).
366 if (!shared.is_tty) return null; 342 if (!w.shared.is_tty) return null;
367 const path = wall.layoutPath(alloc) catch return null; 343 const path = wall.layoutPath(w.alloc) catch return null;
368 defer alloc.free(path); 344 defer w.alloc.free(path);
369 const bytes = wall.loadLayout(alloc, path) orelse return null; 345 const bytes = wall.loadLayout(w.alloc, path) orelse return null;
370 defer alloc.free(bytes); 346 defer w.alloc.free(bytes);
371 return restoreLayoutFrom(alloc, tiles, present, live, shared, bytes, focus_out); 347 return restoreLayoutFrom(w, bytes, focus_out);
372 } 348 }
373 349
374 /// Pure of file I/O like `restoreLayout`: the chained id translations — 350 /// Pure of file I/O like `restoreLayout`: the chained id translations —
375 /// saved→dense, then dense→real — are what a test must reach. 351 /// saved→dense, then dense→real — are what a test must reach.
376 pub fn restoreLayoutFrom( 352 pub fn restoreLayoutFrom(w: Wall, bytes: []const u8, focus_out: *?usize) ?void {
377 alloc: std.mem.Allocator, 353 const n = wv.presentCount(w.livePresent());
378 tiles: []Tile,
379 present: []bool,
380 live: usize,
381 shared: *Shared,
382 bytes: []const u8,
383 focus_out: *?usize,
384 ) ?void {
385 const n = wv.presentCount(present[0..live]);
386 if (n == 0) return null; 354 if (n == 0) return null;
387 const dense = alloc.alloc(Resolved, n) catch return null; 355 const dense = w.alloc.alloc(Resolved, n) catch return null;
388 defer alloc.free(dense); 356 defer w.alloc.free(dense);
389 const remap = alloc.alloc(?u8, n) catch return null; 357 const remap = w.alloc.alloc(?u8, n) catch return null;
390 defer alloc.free(remap); 358 defer w.alloc.free(remap);
391 var di: usize = 0; 359 var di: usize = 0;
392 for (tiles[0..live], present[0..live], 0..) |*t, p, ti| { 360 for (w.liveTiles(), w.livePresent(), 0..) |*t, p, ti| {
393 if (p) { 361 if (p) {
394 dense[di] = t.r; 362 dense[di] = t.r;
395 remap[di] = @intCast(ti); 363 remap[di] = @intCast(ti);
@@ -397,11 +365,11 @@ pub fn restoreLayoutFrom(
397 } 365 }
398 } 366 }
399 var dense_focus: ?u8 = null; 367 var dense_focus: ?u8 = null;
400 if (restoreLayout(alloc, dense, shared, bytes, &dense_focus)) { 368 if (restoreLayout(w.alloc, dense, w.shared, bytes, &dense_focus)) {
401 // The tree's leaf ids are dense indices into `dense`; remap them 369 // The tree's leaf ids are dense indices into `dense`; remap them
402 // to the real tile indices relayout reads, and the saved focus 370 // to the real tile indices relayout reads, and the saved focus
403 // with them. 371 // with them.
404 shared.tree.remapLeaves(remap); 372 w.shared.tree.remapLeaves(remap);
405 if (dense_focus) |d| { 373 if (dense_focus) |d| {
406 if (d < remap.len) { 374 if (d < remap.len) {
407 if (remap[d]) |real| focus_out.* = real; 375 if (remap[d]) |real| focus_out.* = real;
src/tui/wall_picker.zig
Old New
@@ -15,6 +15,7 @@ const wv = @import("wallview.zig");
15 const Host = wall_host.Host; 15 const Host = wall_host.Host;
16 const Shared = wv.Shared; 16 const Shared = wv.Shared;
17 const Tile = wv.Tile; 17 const Tile = wv.Tile;
18 const Wall = wv.Wall;
18 19
19 /// The widest row the picker draws. A spelling past it is cut, never 20 /// The widest row the picker draws. A spelling past it is cut, never
20 /// wrapped: a host list that reflows is one a digit cannot address. 21 /// wrapped: a host list that reflows is one a digit cannot address.
@@ -168,15 +169,7 @@ pub fn isPickAction(a: interact.PrefixFilter.Action) bool {
168 169
169 /// Enter or `c`: a new session on the SELECTED host, tile or no tile. 170 /// Enter or `c`: a new session on the SELECTED host, tile or no tile.
170 /// Null when nothing was made. 171 /// Null when nothing was made.
171 pub fn pickBirth( 172 pub fn pickBirth(w: Wall, sel: usize) ?usize {
172 alloc: std.mem.Allocator,
173 tiles: []Tile,
174 present: []bool,
175 live: *usize,
176 shared: *Shared,
177 host_table: []Host,
178 sel: usize,
179 ) ?usize {
180 // The tile creates on attach exactly as a chord-born one does: no side 173 // The tile creates on attach exactly as a chord-born one does: no side
181 // connection, and no second road onto the wall to keep in step. 174 // connection, and no second road onto the wall to keep in step.
182 // A forgotten host is off the rows and out of the file, and its poller 175 // A forgotten host is off the rows and out of the file, and its poller
@@ -184,14 +177,14 @@ pub fn pickBirth(
184 // would sit on the wall naming a machine the user has just removed. 177 // would sit on the wall naming a machine the user has just removed.
185 // `pickerNearest` cannot save this — with every row gone it returns the 178 // `pickerNearest` cannot save this — with every row gone it returns the
186 // selection unchanged. 179 // selection unchanged.
187 if (sel >= host_table.len or host_table[sel].forgotten.load(.acquire)) { 180 if (sel >= w.hosts.len or w.hosts[sel].forgotten.load(.acquire)) {
188 // The one key the footer advertises, on a wall with nothing to 181 // The one key the footer advertises, on a wall with nothing to
189 // birth on: an Enter that closes the popup and does nothing reads 182 // birth on: an Enter that closes the popup and does nothing reads
190 // as a broken key rather than as an empty hosts file. 183 // as a broken key rather than as an empty hosts file.
191 wv.setNotice(shared, "[no hosts to start a session on - a adds one]"); 184 wv.setNotice(w.shared, "[no hosts to start a session on - a adds one]");
192 return null; 185 return null;
193 } 186 }
194 const h = &host_table[sel]; 187 const h = &w.hosts[sel];
195 // Enter IS the ask, and this copy is where that is written down: the 188 // Enter IS the ask, and this copy is where that is written down: the
196 // row it lands on is often the one the poller calls unreachable, and 189 // row it lands on is often the one the poller calls unreachable, and
197 // starting that machine's daemon is what choosing it means. The SPEC 190 // starting that machine's daemon is what choosing it means. The SPEC
@@ -212,15 +205,15 @@ pub fn pickBirth(
212 // `c` chord would have landed on, reached without a pump to ask. 205 // `c` chord would have landed on, reached without a pump to ask.
213 var name_buf: [proto.session_name_max]u8 = undefined; 206 var name_buf: [proto.session_name_max]u8 = undefined;
214 const name = client.nextFreeName(&name_buf, list); 207 const name = client.nextFreeName(&name_buf, list);
215 const anchor = wall_layout.anchorTile(present[0..live.*], shared.sel); 208 const anchor = wall_layout.anchorTile(w.livePresent(), w.shared.sel);
216 const has_anchor = wv.presentCount(present[0..live.*]) > 0; 209 const has_anchor = wv.presentCount(w.livePresent()) > 0;
217 // `-A` is inherited only within one host. A chord inherits it because 210 // `-A` is inherited only within one host. A chord inherits it because
218 // the new session is on the machine the offer was already made to; the 211 // the new session is on the machine the offer was already made to; the
219 // picker can cross to a host the user never offered an agent, and a 212 // picker can cross to a host the user never offered an agent, and a
220 // popup must not be the thing that hands a stranger the keys. 213 // popup must not be the thing that hands a stranger the keys.
221 const agent = has_anchor and tiles[anchor].host != null and 214 const agent = has_anchor and w.tiles[anchor].host != null and
222 tiles[anchor].host.? == sel and tiles[anchor].r.agent; 215 w.tiles[anchor].host.? == sel and w.tiles[anchor].r.agent;
223 const at = wv.birthTile(alloc, tiles, present, live, shared, .{ 216 const at = wv.birthTile(w, .{
224 // `name` is this stack's buffer until the wall takes the tile — 217 // `name` is this stack's buffer until the wall takes the tile —
225 // see `Birth.borrowed`. 218 // see `Birth.borrowed`.
226 .r = .{ .target = target, .label = "", .session = name, .agent = agent }, 219 .r = .{ .target = target, .label = "", .session = name, .agent = agent },
@@ -234,10 +227,10 @@ pub fn pickBirth(
234 .host = sel, 227 .host = sel,
235 .borrowed = true, 228 .borrowed = true,
236 }) orelse { 229 }) orelse {
237 wv.setNotice(shared, "[no room on the wall for another session]"); 230 wv.setNotice(w.shared, "[no room on the wall for another session]");
238 return null; 231 return null;
239 }; 232 };
240 wv.spawnPump(&tiles[at]); 233 wv.spawnPump(&w.tiles[at]);
241 // The new session must not wait out a poll to be confirmed by the list 234 // The new session must not wait out a poll to be confirmed by the list
242 // that will also stop the diff from vanishing it. 235 // that will also stop the diff from vanishing it.
243 h.poke.store(true, .release); 236 h.poke.store(true, .release);
@@ -245,20 +238,11 @@ pub fn pickBirth(
245 } 238 }
246 239
247 /// `x`: the host leaves the file, its poller stops and its tiles go. 240 /// `x`: the host leaves the file, its poller stops and its tiles go.
248 pub fn pickForget( 241 pub fn pickForget(w: Wall, sel: usize, path: ?[]const u8) void {
249 alloc: std.mem.Allocator,
250 tiles: []Tile,
251 present: []bool,
252 live: usize,
253 shared: *Shared,
254 host_table: []Host,
255 sel: usize,
256 path: ?[]const u8,
257 ) void {
258 // The SESSIONS keep running: forgetting a daemon is `mux hosts rm` 242 // The SESSIONS keep running: forgetting a daemon is `mux hosts rm`
259 // typed from inside, and that ends nothing. 243 // typed from inside, and that ends nothing.
260 if (sel >= host_table.len) return; 244 if (sel >= w.hosts.len) return;
261 const h = &host_table[sel]; 245 const h = &w.hosts[sel];
262 if (h.forgotten.load(.acquire)) return; 246 if (h.forgotten.load(.acquire)) return;
263 // The file first, for `addHost`'s reason: the wall the user is looking 247 // The file first, for `addHost`'s reason: the wall the user is looking
264 // at and the wall they get back next time are the same wall. 248 // at and the wall they get back next time are the same wall.
@@ -270,15 +254,15 @@ pub fn pickForget(
270 var gone_from_file = true; 254 var gone_from_file = true;
271 var why: ?anyerror = null; 255 var why: ?anyerror = null;
272 if (path) |p| { 256 if (path) |p| {
273 gone_from_file = hosts.forget(alloc, p, h.spec.spelling) catch |e| blk: { 257 gone_from_file = hosts.forget(w.alloc, p, h.spec.spelling) catch |e| blk: {
274 why = e; 258 why = e;
275 break :blk false; 259 break :blk false;
276 }; 260 };
277 } 261 }
278 h.forgotten.store(true, .release); 262 h.forgotten.store(true, .release);
279 for (0..live) |i| { 263 for (0..w.live.*) |i| {
280 if (present[i] and wall_host.ownedBy(&tiles[i], sel)) 264 if (w.present[i] and wall_host.ownedBy(&w.tiles[i], sel))
281 wv.vanishTile(tiles[0..live], present[0..live], shared, i, null); 265 wv.vanishTile(w.liveTiles(), w.livePresent(), w.shared, i, null);
282 } 266 }
283 var buf: [96]u8 = undefined; 267 var buf: [96]u8 = undefined;
284 const said = if (why) |e| 268 const said = if (why) |e|
@@ -287,7 +271,7 @@ pub fn pickForget(
287 std.fmt.bufPrint(&buf, "[{s} was not on the wall]", .{h.spec.spelling}) catch "[that host was not on the wall]" 271 std.fmt.bufPrint(&buf, "[{s} was not on the wall]", .{h.spec.spelling}) catch "[that host was not on the wall]"
288 else 272 else
289 std.fmt.bufPrint(&buf, "[forgot {s}]", .{h.spec.spelling}) catch "[forgot the host]"; 273 std.fmt.bufPrint(&buf, "[forgot {s}]", .{h.spec.spelling}) catch "[forgot the host]";
290 wv.setNotice(shared, said); 274 wv.setNotice(w.shared, said);
291 } 275 }
292 276
293 /// The popup. Painted by the KEYBOARD thread, which is the only one that 277 /// The popup. Painted by the KEYBOARD thread, which is the only one that
src/tui/wall_test_harness.zig
Old New
@@ -14,6 +14,37 @@ const Host = wall_host.Host;
14 const Shared = wv.Shared; 14 const Shared = wv.Shared;
15 const Tile = wv.Tile; 15 const Tile = wv.Tile;
16 16
17 /// The `Wall` the product passes around, from a test's own arrays.
18 pub fn wallOf(
19 alloc: std.mem.Allocator,
20 tiles: []Tile,
21 present: []bool,
22 live: *usize,
23 shared: *Shared,
24 hosts: []Host,
25 ) wv.Wall {
26 return .{ .alloc = alloc, .tiles = tiles, .present = present, .live = live, .shared = shared, .hosts = hosts };
27 }
28
29 /// The cell `wallLive` points at. File-scope because a layout test builds
30 /// its wall per call and Zig runs the tests in one process, one at a time;
31 /// nothing here births, so nothing writes it but the call being made. The
32 /// nearer trap is intra-test: two `wallLive` calls with different `live`
33 /// share this cell, and the first wall then reads the second's count.
34 var standing_live: usize = 0;
35
36 /// A wall of `live` slots over arrays a test declared, with no hosts: what
37 /// the layout and focus calls take, none of which grows the wall.
38 pub fn wallLive(alloc: std.mem.Allocator, tiles: []Tile, present: []bool, live: usize, shared: *Shared) wv.Wall {
39 standing_live = live;
40 return wallOf(alloc, tiles, present, &standing_live, shared, &.{});
41 }
42
43 /// `wallLive` where every slot the test declared is on the wall.
44 pub fn wallAll(alloc: std.mem.Allocator, tiles: []Tile, present: []bool, shared: *Shared) wv.Wall {
45 return wallLive(alloc, tiles, present, tiles.len, shared);
46 }
47
17 /// Three tiles: two sessions on host 0 and a same-named one on host 1, so a 48 /// Three tiles: two sessions on host 0 and a same-named one on host 1, so a
18 /// diff that aliased by NAME alone would be caught. 49 /// diff that aliased by NAME alone would be caught.
19 pub fn diffFixture(shared: *Shared) [3]Tile { 50 pub fn diffFixture(shared: *Shared) [3]Tile {
src/tui/wall_test_host.zig
Old New
@@ -151,7 +151,7 @@ test "applyReadyLists: a host added after the wall opened gets its sessions, and
151 fixture.setList(&table[2], "late\n"); 151 fixture.setList(&table[2], "late\n");
152 table[2].list_ready.store(true, .release); 152 table[2].list_ready.store(true, .release);
153 153
154 _ = wall_host.applyReadyLists(alloc, &tiles, &present, &live, &shared, table[0..3]); 154 _ = wall_host.applyReadyLists(fixture.wallOf(alloc, &tiles, &present, &live, &shared, table[0..3]));
155 155
156 // The added host's session is a tile, and only the host that reported is 156 // The added host's session is a tile, and only the host that reported is
157 // marked: the two that opened the wall are still owed a first list. 157 // marked: the two that opened the wall are still owed a first list.
@@ -205,8 +205,8 @@ test "applyHostList: a host with no live session gets no tile — the wall shows
205 205
206 // Twice, because a placeholder that is born once is still a placeholder. 206 // Twice, because a placeholder that is born once is still a placeholder.
207 for (0..2) |_| { 207 for (0..2) |_| {
208 wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); 208 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
209 wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 1); 209 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1);
210 } 210 }
211 211
212 try std.testing.expectEqual(@as(usize, 0), live); 212 try std.testing.expectEqual(@as(usize, 0), live);
@@ -214,7 +214,7 @@ test "applyHostList: a host with no live session gets no tile — the wall shows
214 214
215 // A session on the empty host, and the wall has exactly the one tile. 215 // A session on the empty host, and the wall has exactly the one tile.
216 fixture.setList(&table[1], "a\n"); 216 fixture.setList(&table[1], "a\n");
217 wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 1); 217 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1);
218 218
219 try std.testing.expectEqual(@as(usize, 1), wv.presentCount(present[0..live])); 219 try std.testing.expectEqual(@as(usize, 1), wv.presentCount(present[0..live]));
220 try std.testing.expectEqualStrings("a", tiles[0].r.session); 220 try std.testing.expectEqualStrings("a", tiles[0].r.session);
@@ -235,7 +235,7 @@ test "applyHostList: a live tile survives one list that lost its session, and go
235 // grace is per tile and not a wall-wide pause. 235 // grace is per tile and not a wall-wide pause.
236 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; 236 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};
237 fixture.setList(&table[0], "a\nb\n"); 237 fixture.setList(&table[0], "a\nb\n");
238 wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); 238 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
239 try std.testing.expectEqual(@as(usize, 2), live); 239 try std.testing.expectEqual(@as(usize, 2), live);
240 240
241 // The wall's pumps are stopped in this fixture, so the liveness the rule 241 // The wall's pumps are stopped in this fixture, so the liveness the rule
@@ -243,13 +243,13 @@ test "applyHostList: a live tile survives one list that lost its session, and go
243 tiles[0].alive.store(true, .release); 243 tiles[0].alive.store(true, .release);
244 tiles[1].alive.store(true, .release); 244 tiles[1].alive.store(true, .release);
245 fixture.setList(&table[0], "a\n"); 245 fixture.setList(&table[0], "a\n");
246 wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); 246 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
247 // Still there: this list may have overtaken an `exit_status` the pump 247 // Still there: this list may have overtaken an `exit_status` the pump
248 // has not read yet, and that code is the run's own on a wall of one. 248 // has not read yet, and that code is the run's own on a wall of one.
249 try std.testing.expect(present[1]); 249 try std.testing.expect(present[1]);
250 try std.testing.expect(present[0]); 250 try std.testing.expect(present[0]);
251 251
252 wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); 252 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
253 try std.testing.expect(!present[1]); 253 try std.testing.expect(!present[1]);
254 try std.testing.expect(present[0]); 254 try std.testing.expect(present[0]);
255 } 255 }
@@ -267,7 +267,7 @@ test "applyHostList: two sessions on an empty wall are two tiles, the first focu
267 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; 267 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};
268 fixture.setList(&table[0], "a\nb\n"); 268 fixture.setList(&table[0], "a\nb\n");
269 269
270 wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); 270 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
271 271
272 // The first birth has no leaf to sit beside — an empty tree takes it as 272 // The first birth has no leaf to sit beside — an empty tree takes it as
273 // its root, and the second inserts against it. 273 // its root, and the second inserts against it.
@@ -297,7 +297,7 @@ test "applyHostList: one list's tiles are laid out in the order the daemon repor
297 // its two siblings. 297 // its two siblings.
298 fixture.setList(&table[0], "a\nb\nc\n"); 298 fixture.setList(&table[0], "a\nb\nc\n");
299 299
300 wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); 300 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
301 301
302 try std.testing.expectEqual(@as(usize, 3), live); 302 try std.testing.expectEqual(@as(usize, 3), live);
303 // Down the screen in the daemon's own order. The tile ARRAY is in that 303 // Down the screen in the daemon's own order. The tile ARRAY is in that
@@ -345,7 +345,7 @@ test "applyHostList: sessions past the wall's capacity are counted in the notice
345 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; 345 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};
346 fixture.setList(&table[0], text.items); 346 fixture.setList(&table[0], text.items);
347 347
348 wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); 348 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
349 349
350 try std.testing.expectEqual(@as(usize, wv.max_tiles), live); 350 try std.testing.expectEqual(@as(usize, wv.max_tiles), live);
351 var buf: [96]u8 = undefined; 351 var buf: [96]u8 = undefined;
@@ -377,7 +377,7 @@ test "applyHostList: a wall too thin for a second pane takes what fits and retai
377 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; 377 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};
378 fixture.setList(&table[0], "a\nb\n"); 378 fixture.setList(&table[0], "a\nb\n");
379 379
380 wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); 380 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
381 381
382 try std.testing.expectEqual(@as(usize, 1), live); 382 try std.testing.expectEqual(@as(usize, 1), live);
383 try std.testing.expectEqualStrings("a", tiles[0].r.session); 383 try std.testing.expectEqualStrings("a", tiles[0].r.session);
@@ -411,7 +411,7 @@ test "applyHostList: when a host's last session goes the wall empties, and nothi
411 // The daemon has nothing left: a restart, or another client ended it. 411 // The daemon has nothing left: a restart, or another client ended it.
412 fixture.setList(&table[0], ""); 412 fixture.setList(&table[0], "");
413 413
414 wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); 414 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
415 415
416 // The host is up with nothing on it, which the wall says by showing 416 // The host is up with nothing on it, which the wall says by showing
417 // nothing: no placeholder tile takes the gone session's place. 417 // nothing: no placeholder tile takes the gone session's place.
src/tui/wall_test_layout.zig
Old New
@@ -146,7 +146,7 @@ test "relayout sets resize_pending on every live tile" {
146 .wake_w = -1, 146 .wake_w = -1,
147 }; 147 };
148 } 148 }
149 wall_layout.relayout(alloc, tiles, present, &shared, 0); 149 wall_layout.relayout(fixture.wallAll(alloc, tiles, present, &shared), 0);
150 // Two tiles: a label bar appears. 150 // Two tiles: a label bar appears.
151 try std.testing.expectEqual(@as(u8, 1), shared.label_rows); 151 try std.testing.expectEqual(@as(u8, 1), shared.label_rows);
152 // Every tile is doorbelled: its pump sends the new rect. 152 // Every tile is doorbelled: its pump sends the new rect.
@@ -166,7 +166,7 @@ test "a relayout landing mid-pass is still sent" {
166 try shared.tree.insert(0, 1); 166 try shared.tree.insert(0, 1);
167 const tiles = try alloc.alloc(Tile, 2); 167 const tiles = try alloc.alloc(Tile, 2);
168 defer alloc.free(tiles); 168 defer alloc.free(tiles);
169 const present = [_]bool{ true, true }; 169 var present = [_]bool{ true, true };
170 for (tiles, 0..) |*t, i| { 170 for (tiles, 0..) |*t, i| {
171 t.* = Tile{ 171 t.* = Tile{
172 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 172 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
@@ -194,7 +194,7 @@ test "a relayout landing mid-pass is still sent" {
194 shared.paint_mu.lock(); 194 shared.paint_mu.lock();
195 shared.size = .{ .cols = 80, .rows = heights[i % heights.len] }; 195 shared.size = .{ .cols = 80, .rows = heights[i % heights.len] };
196 shared.paint_mu.unlock(); 196 shared.paint_mu.unlock();
197 wall_layout.relayout(alloc, tiles, &present, &shared, 0); 197 wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0);
198 198
199 // Quiesce: the pump owes nothing, and the pass that took the last 199 // Quiesce: the pump owes nothing, and the pass that took the last
200 // doorbell has finished. Only then is "what the daemon holds" a 200 // doorbell has finished. Only then is "what the daemon holds" a
@@ -263,7 +263,7 @@ test "a terminal too small for the cut falls back to the focused pane, and grows
263 shared.paint_mu.lock(); 263 shared.paint_mu.lock();
264 shared.size = .{ .cols = 80, .rows = rows }; 264 shared.size = .{ .cols = 80, .rows = rows };
265 shared.paint_mu.unlock(); 265 shared.paint_mu.unlock();
266 wall_layout.relayout(alloc, tiles, present, &shared, 0); 266 wall_layout.relayout(fixture.wallAll(alloc, tiles, present, &shared), 0);
267 // What the pumps do with the generation bump: repaint their bars. 267 // What the pumps do with the generation bump: repaint their bars.
268 for (tiles, present) |*t, p| if (p) wv.paintLabel(t, .up); 268 for (tiles, present) |*t, p| if (p) wv.paintLabel(t, .up);
269 screen.drain(); 269 screen.drain();
@@ -315,7 +315,7 @@ test "fullscreen gives the focused tile the whole terminal and hides the rest" {
315 try shared.tree.insert(0, 1); 315 try shared.tree.insert(0, 1);
316 const tiles = try alloc.alloc(Tile, 2); 316 const tiles = try alloc.alloc(Tile, 2);
317 defer alloc.free(tiles); 317 defer alloc.free(tiles);
318 const present = [_]bool{ true, true }; 318 var present = [_]bool{ true, true };
319 for (tiles, 0..) |*t, i| { 319 for (tiles, 0..) |*t, i| {
320 t.* = Tile{ 320 t.* = Tile{
321 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 321 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
@@ -327,7 +327,7 @@ test "fullscreen gives the focused tile the whole terminal and hides the rest" {
327 }; 327 };
328 } 328 }
329 shared.fullscreen = true; 329 shared.fullscreen = true;
330 wall_layout.relayout(alloc, tiles, &present, &shared, 0); 330 wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0);
331 // No label bar: the fullscreened pane is a one-tile wall. 331 // No label bar: the fullscreened pane is a one-tile wall.
332 try std.testing.expectEqual(@as(u8, 0), shared.label_rows); 332 try std.testing.expectEqual(@as(u8, 0), shared.label_rows);
333 // Tile 0 gets the whole terminal; tile 1 gets nothing. 333 // Tile 0 gets the whole terminal; tile 1 gets nothing.
@@ -351,7 +351,7 @@ test "toggle off fullscreen restores the real rects" {
351 try shared.tree.insert(0, 1); 351 try shared.tree.insert(0, 1);
352 const tiles = try alloc.alloc(Tile, 2); 352 const tiles = try alloc.alloc(Tile, 2);
353 defer alloc.free(tiles); 353 defer alloc.free(tiles);
354 const present = [_]bool{ true, true }; 354 var present = [_]bool{ true, true };
355 for (tiles, 0..) |*t, i| { 355 for (tiles, 0..) |*t, i| {
356 t.* = Tile{ 356 t.* = Tile{
357 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 357 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
@@ -363,9 +363,9 @@ test "toggle off fullscreen restores the real rects" {
363 }; 363 };
364 } 364 }
365 shared.fullscreen = true; 365 shared.fullscreen = true;
366 wall_layout.relayout(alloc, tiles, &present, &shared, 0); 366 wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0);
367 shared.fullscreen = false; 367 shared.fullscreen = false;
368 wall_layout.relayout(alloc, tiles, &present, &shared, 0); 368 wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0);
369 // Two tiles again: label bar back, both have real rects (stacked: 369 // Two tiles again: label bar back, both have real rects (stacked:
370 // full width, half height each). 370 // full width, half height each).
371 try std.testing.expectEqual(@as(u8, 1), shared.label_rows); 371 try std.testing.expectEqual(@as(u8, 1), shared.label_rows);
@@ -387,7 +387,7 @@ test "focus_dir while fullscreened follows the focus" {
387 try shared.tree.insert(0, 1); 387 try shared.tree.insert(0, 1);
388 const tiles = try alloc.alloc(Tile, 2); 388 const tiles = try alloc.alloc(Tile, 2);
389 defer alloc.free(tiles); 389 defer alloc.free(tiles);
390 const present = [_]bool{ true, true }; 390 var present = [_]bool{ true, true };
391 for (tiles, 0..) |*t, i| { 391 for (tiles, 0..) |*t, i| {
392 t.* = Tile{ 392 t.* = Tile{
393 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 393 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
@@ -399,9 +399,9 @@ test "focus_dir while fullscreened follows the focus" {
399 }; 399 };
400 } 400 }
401 shared.fullscreen = true; 401 shared.fullscreen = true;
402 wall_layout.relayout(alloc, tiles, &present, &shared, 0); 402 wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0);
403 // Move focus to tile 1 while fullscreened. 403 // Move focus to tile 1 while fullscreened.
404 wv.focusAnswer(alloc, tiles, &present, &shared, false, 1); 404 wv.focusAnswer(fixture.wallAll(alloc, tiles, &present, &shared), false, 1);
405 // The full rect followed: tile 1 now owns the terminal. 405 // The full rect followed: tile 1 now owns the terminal.
406 try std.testing.expectEqual(@as(u16, 24), tiles[1].rect.rows); 406 try std.testing.expectEqual(@as(u16, 24), tiles[1].rect.rows);
407 try std.testing.expectEqual(@as(u16, 80), tiles[1].rect.cols); 407 try std.testing.expectEqual(@as(u16, 80), tiles[1].rect.cols);
@@ -425,7 +425,7 @@ test "resize: l grows the focused pane, h shrinks it" {
425 try shared.tree.splitRight(0, 1); 425 try shared.tree.splitRight(0, 1);
426 const tiles = try alloc.alloc(Tile, 2); 426 const tiles = try alloc.alloc(Tile, 2);
427 defer alloc.free(tiles); 427 defer alloc.free(tiles);
428 const present = [_]bool{ true, true }; 428 var present = [_]bool{ true, true };
429 for (tiles, 0..) |*t, i| { 429 for (tiles, 0..) |*t, i| {
430 t.* = Tile{ 430 t.* = Tile{
431 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 431 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
@@ -436,13 +436,13 @@ test "resize: l grows the focused pane, h shrinks it" {
436 .wake_w = -1, 436 .wake_w = -1,
437 }; 437 };
438 } 438 }
439 wall_layout.relayout(alloc, tiles, &present, &shared, 0); 439 wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0);
440 const before = tiles[0].rect.cols; 440 const before = tiles[0].rect.cols;
441 // l (grow width): focus 0 gains from its right sibling. 441 // l (grow width): focus 0 gains from its right sibling.
442 try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .right)); 442 try std.testing.expect(wall_layout.doResize(fixture.wallAll(alloc, tiles, &present, &shared), 0, .right));
443 try std.testing.expect(tiles[0].rect.cols > before); 443 try std.testing.expect(tiles[0].rect.cols > before);
444 // h (shrink width): the right neighbor gains a cell back from focus. 444 // h (shrink width): the right neighbor gains a cell back from focus.
445 try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .left)); 445 try std.testing.expect(wall_layout.doResize(fixture.wallAll(alloc, tiles, &present, &shared), 0, .left));
446 try std.testing.expectEqual(before, tiles[0].rect.cols); 446 try std.testing.expectEqual(before, tiles[0].rect.cols);
447 } 447 }
448 448
@@ -458,7 +458,7 @@ test "resize: j grows height, k shrinks height" {
458 try shared.tree.splitBelow(0, 1); 458 try shared.tree.splitBelow(0, 1);
459 const tiles = try alloc.alloc(Tile, 2); 459 const tiles = try alloc.alloc(Tile, 2);
460 defer alloc.free(tiles); 460 defer alloc.free(tiles);
461 const present = [_]bool{ true, true }; 461 var present = [_]bool{ true, true };
462 for (tiles, 0..) |*t, i| { 462 for (tiles, 0..) |*t, i| {
463 t.* = Tile{ 463 t.* = Tile{
464 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 464 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
@@ -469,11 +469,11 @@ test "resize: j grows height, k shrinks height" {
469 .wake_w = -1, 469 .wake_w = -1,
470 }; 470 };
471 } 471 }
472 wall_layout.relayout(alloc, tiles, &present, &shared, 0); 472 wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0);
473 const before = tiles[0].rect.rows; 473 const before = tiles[0].rect.rows;
474 try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .down)); 474 try std.testing.expect(wall_layout.doResize(fixture.wallAll(alloc, tiles, &present, &shared), 0, .down));
475 try std.testing.expect(tiles[0].rect.rows > before); 475 try std.testing.expect(tiles[0].rect.rows > before);
476 try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .up)); 476 try std.testing.expect(wall_layout.doResize(fixture.wallAll(alloc, tiles, &present, &shared), 0, .up));
477 try std.testing.expectEqual(before, tiles[0].rect.rows); 477 try std.testing.expectEqual(before, tiles[0].rect.rows);
478 } 478 }
479 479
@@ -489,7 +489,7 @@ test "resize refuses while fullscreened" {
489 try shared.tree.splitRight(0, 1); 489 try shared.tree.splitRight(0, 1);
490 const tiles = try alloc.alloc(Tile, 2); 490 const tiles = try alloc.alloc(Tile, 2);
491 defer alloc.free(tiles); 491 defer alloc.free(tiles);
492 const present = [_]bool{ true, true }; 492 var present = [_]bool{ true, true };
493 for (tiles, 0..) |*t, i| { 493 for (tiles, 0..) |*t, i| {
494 t.* = Tile{ 494 t.* = Tile{
495 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, 495 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
@@ -501,8 +501,8 @@ test "resize refuses while fullscreened" {
501 }; 501 };
502 } 502 }
503 shared.fullscreen = true; 503 shared.fullscreen = true;
504 wall_layout.relayout(alloc, tiles, &present, &shared, 0); 504 wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0);
505 try std.testing.expect(!wall_layout.doResize(alloc, tiles, &present, &shared, 0, .right)); 505 try std.testing.expect(!wall_layout.doResize(fixture.wallAll(alloc, tiles, &present, &shared), 0, .right));
506 } 506 }
507 507
508 test "a pump's answer that grew the wall re-cuts it; a mere focus move does not" { 508 test "a pump's answer that grew the wall re-cuts it; a mere focus move does not" {
@@ -529,7 +529,7 @@ test "a pump's answer that grew the wall re-cuts it; a mere focus move does not"
529 // Growth: without the re-cut the new tile sits on placeholder geometry 529 // Growth: without the re-cut the new tile sits on placeholder geometry
530 // and the old tile still owns the whole terminal — the screen keeps 530 // and the old tile still owns the whole terminal — the screen keeps
531 // showing tile 0 while the keyboard types into tile 1's rows. 531 // showing tile 0 while the keyboard types into tile 1's rows.
532 wv.focusAnswer(alloc, tiles, present, &shared, true, 1); 532 wv.focusAnswer(fixture.wallAll(alloc, tiles, present, &shared), true, 1);
533 try std.testing.expectEqual(@as(u8, 1), shared.label_rows); 533 try std.testing.expectEqual(@as(u8, 1), shared.label_rows);
534 try std.testing.expect(tiles[0].resize_pending); 534 try std.testing.expect(tiles[0].resize_pending);
535 try std.testing.expect(tiles[1].resize_pending); 535 try std.testing.expect(tiles[1].resize_pending);
@@ -538,7 +538,7 @@ test "a pump's answer that grew the wall re-cuts it; a mere focus move does not"
538 tiles[0].resize_pending = false; 538 tiles[0].resize_pending = false;
539 tiles[1].resize_pending = false; 539 tiles[1].resize_pending = false;
540 // No growth: a full clear here would flash the wall for a focus move. 540 // No growth: a full clear here would flash the wall for a focus move.
541 wv.focusAnswer(alloc, tiles, present, &shared, false, 0); 541 wv.focusAnswer(fixture.wallAll(alloc, tiles, present, &shared), false, 0);
542 try std.testing.expect(!tiles[0].resize_pending); 542 try std.testing.expect(!tiles[0].resize_pending);
543 try std.testing.expect(!tiles[1].resize_pending); 543 try std.testing.expect(!tiles[1].resize_pending);
544 try std.testing.expectEqual(@as(usize, 0), shared.sel); 544 try std.testing.expectEqual(@as(usize, 0), shared.sel);
@@ -567,7 +567,7 @@ test "a one-tile wall draws no label bar and paints row 1" {
567 .wake_r = -1, 567 .wake_r = -1,
568 .wake_w = -1, 568 .wake_w = -1,
569 }; 569 };
570 wall_layout.relayout(alloc, tiles, present, &shared, 0); 570 wall_layout.relayout(fixture.wallAll(alloc, tiles, present, &shared), 0);
571 // One tile: no label bar, every row is content. 571 // One tile: no label bar, every row is content.
572 try std.testing.expectEqual(@as(u8, 0), shared.label_rows); 572 try std.testing.expectEqual(@as(u8, 0), shared.label_rows);
573 try std.testing.expectEqual(@as(u16, 24), tiles[0].viewRows()); 573 try std.testing.expectEqual(@as(u16, 24), tiles[0].viewRows());
@@ -605,7 +605,7 @@ test "a relayout drops every tile's highlight, because the stripes move under it
605 }; 605 };
606 } 606 }
607 const before = shared.repaint_gen.load(.acquire); 607 const before = shared.repaint_gen.load(.acquire);
608 wall_layout.relayout(alloc, tiles, present, &shared, 0); 608 wall_layout.relayout(fixture.wallAll(alloc, tiles, present, &shared), 0);
609 // One bump, every tile's pump clears its drag on it: the stripes moved, 609 // One bump, every tile's pump clears its drag on it: the stripes moved,
610 // so a held drag's anchor names a rect that is somewhere else now. 610 // so a held drag's anchor names a rect that is somewhere else now.
611 try std.testing.expect(shared.repaint_gen.load(.acquire) > before); 611 try std.testing.expect(shared.repaint_gen.load(.acquire) > before);
@@ -646,7 +646,7 @@ test "saveLayoutTo writes the sidecar for the present tiles, spellings verbatim"
646 .wake_w = -1, 646 .wake_w = -1,
647 }; 647 };
648 } 648 }
649 const present = [_]bool{ true, true }; 649 var present = [_]bool{ true, true };
650 wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared); 650 wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared);
651 const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; 651 const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult;
652 defer alloc.free(got); 652 defer alloc.free(got);
@@ -696,7 +696,7 @@ test "saveLayoutTo handles a vanished middle tile without panicking" {
696 .wake_w = -1, 696 .wake_w = -1,
697 }; 697 };
698 } 698 }
699 const present = [_]bool{ true, false, true }; 699 var present = [_]bool{ true, false, true };
700 wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared); 700 wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared);
701 const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; 701 const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult;
702 defer alloc.free(got); 702 defer alloc.free(got);
@@ -723,7 +723,7 @@ test "restore: a saved beside pair comes back verbatim on a tall terminal" {
723 try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); 723 try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out));
724 const tiles = try alloc.alloc(Tile, 2); 724 const tiles = try alloc.alloc(Tile, 2);
725 defer alloc.free(tiles); 725 defer alloc.free(tiles);
726 const present = [_]bool{ true, true }; 726 var present = [_]bool{ true, true };
727 for (tiles, 0..) |*t, i| { 727 for (tiles, 0..) |*t, i| {
728 t.* = Tile{ 728 t.* = Tile{
729 .r = resolved[i], 729 .r = resolved[i],
@@ -734,7 +734,7 @@ test "restore: a saved beside pair comes back verbatim on a tall terminal" {
734 .wake_w = -1, 734 .wake_w = -1,
735 }; 735 };
736 } 736 }
737 wall_layout.relayout(alloc, tiles, &present, &shared, 0); 737 wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0);
738 try std.testing.expectEqual(@as(u16, 0), tiles[1].rect.top); 738 try std.testing.expectEqual(@as(u16, 0), tiles[1].rect.top);
739 try std.testing.expect(tiles[1].rect.left > 0); 739 try std.testing.expect(tiles[1].rect.left > 0);
740 } 740 }
@@ -858,7 +858,7 @@ test "restoreLayoutFrom: dense sidecar indices land on the real tile indices" {
858 const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n" ++ 858 const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n" ++
859 " leaf 20 --sock /tmp/x#c\n leaf 18 --sock /tmp/x#d\n"; 859 " leaf 20 --sock /tmp/x#c\n leaf 18 --sock /tmp/x#d\n";
860 var restored_focus: ?usize = null; 860 var restored_focus: ?usize = null;
861 wall_layout.restoreLayoutFrom(alloc, &tiles, &present, 4, &shared, bytes, &restored_focus) orelse 861 wall_layout.restoreLayoutFrom(fixture.wallLive(alloc, &tiles, &present, 4, &shared), bytes, &restored_focus) orelse
862 return error.TestUnexpectedResult; 862 return error.TestUnexpectedResult;
863 const flat = try shared.tree.flatten(alloc, 24, 80, wall_layout.wallFloors(3), null); 863 const flat = try shared.tree.flatten(alloc, 24, 80, wall_layout.wallFloors(3), null);
864 defer flat.deinit(alloc); 864 defer flat.deinit(alloc);
src/tui/wall_test_picker.zig
Old New
@@ -400,7 +400,7 @@ test "pickBirth: Enter on an emptied popup births nothing, not a session on a ho
400 h.forgotten.store(true, .release); 400 h.forgotten.store(true, .release);
401 } 401 }
402 402
403 const at = wall_picker.pickBirth(alloc, &tiles, &present, &live, &shared, &table, 1); 403 const at = wall_picker.pickBirth(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1);
404 404
405 if (at != null) return error.BornOnAForgottenHost; 405 if (at != null) return error.BornOnAForgottenHost;
406 if (live != 0) return error.AForgottenHostTookASlot; 406 if (live != 0) return error.AForgottenHostTookASlot;
@@ -438,7 +438,7 @@ test "pickBirth: Enter is an ask — the tile it births may start a daemon, the
438 h.applied = true; 438 h.applied = true;
439 } 439 }
440 440
441 const at = wall_picker.pickBirth(alloc, &tiles, &present, &live, &shared, &table, 1) orelse 441 const at = wall_picker.pickBirth(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1) orelse
442 return error.EnterBornNothing; 442 return error.EnterBornNothing;
443 443
444 // The row Enter lands on is often exactly the one the poller calls 444 // The row Enter lands on is often exactly the one the poller calls
src/tui/wall_test_wall.zig
Old New
@@ -33,7 +33,7 @@ test "birthTile: a chord-born tile creates and offers no agent" {
33 var present = [_]bool{ true, false }; 33 var present = [_]bool{ true, false };
34 var live: usize = 1; 34 var live: usize = 1;
35 const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" }; 35 const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" };
36 const at = wv.birthTile(alloc, &tiles, &present, &live, &shared, .{ 36 const at = wv.birthTile(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &.{}), .{
37 .r = r, 37 .r = r,
38 .from = 0, 38 .from = 0,
39 .place = .beside_focus, 39 .place = .beside_focus,
@@ -64,6 +64,24 @@ test "the empty wall advertises the key the picker really answers to" {
64 try std.testing.expectEqual(interact.PrefixFilter.Action.pick_open, f.feed(&keys).action); 64 try std.testing.expectEqual(interact.PrefixFilter.Action.pick_open, f.feed(&keys).action);
65 } 65 }
66 66
67 test "the empty-wall notice names the picker key" {
68 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
69 // The refusal a birth chord earns when the focused tile has no pump
70 // left. A SENTENCE, not code: a mechanical rename over `live` rewrote
71 // this line and three like it, and every gate stayed green because
72 // nothing read a notice back.
73 wv.setNotice(&shared, wv.no_live_here);
74 var buf: [96]u8 = undefined;
75 try std.testing.expectEqualStrings(
76 "[no live session here - Ctrl-\\ s picks a host]",
77 wv.takeNotice(&shared, &buf),
78 );
79 // And the way out it names is a chord the filter really answers to.
80 var f: interact.PrefixFilter = .{};
81 var keys = "\x1cs".*;
82 try std.testing.expectEqual(interact.PrefixFilter.Action.pick_open, f.feed(&keys).action);
83 }
84
67 test "emptyWallHint: a chord an empty wall refuses is said there, and said once" { 85 test "emptyWallHint: a chord an empty wall refuses is said there, and said once" {
68 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; 86 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
69 const way_out = "Ctrl-\\ s to pick a host - Ctrl-\\ d to leave"; 87 const way_out = "Ctrl-\\ s to pick a host - Ctrl-\\ d to leave";
@@ -224,7 +242,7 @@ test "birthTile: no room is null and the tree is left as it was" {
224 var present = [_]bool{ true, false }; 242 var present = [_]bool{ true, false };
225 var live: usize = 1; 243 var live: usize = 1;
226 const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" }; 244 const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" };
227 try std.testing.expectEqual(@as(?usize, null), wv.birthTile(alloc, &tiles, &present, &live, &shared, .{ 245 try std.testing.expectEqual(@as(?usize, null), wv.birthTile(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &.{}), .{
228 .r = r, 246 .r = r,
229 .from = 0, 247 .from = 0,
230 .place = .beside_focus, 248 .place = .beside_focus,
@@ -269,7 +287,7 @@ test "birthTile: a beside wall admits more panes than rows/3" {
269 live += 1; 287 live += 1;
270 } 288 }
271 const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" }; 289 const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" };
272 const at = wv.birthTile(alloc, &tiles, &present, &live, &shared, .{ 290 const at = wv.birthTile(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &.{}), .{
273 .r = r, 291 .r = r,
274 .from = n - 2, 292 .from = n - 2,
275 .place = .beside_focus, 293 .place = .beside_focus,
@@ -326,7 +344,7 @@ test "birthTile: a vanished digit is taken back, and not before its pump returne
326 }; 344 };
327 const born = struct { 345 const born = struct {
328 fn at(a: std.mem.Allocator, ts: []Tile, ps: []bool, lv: *usize, sh: *Shared, tg: client.Target, name: []const u8, host: ?usize) ?usize { 346 fn at(a: std.mem.Allocator, ts: []Tile, ps: []bool, lv: *usize, sh: *Shared, tg: client.Target, name: []const u8, host: ?usize) ?usize {
329 return wv.birthTile(a, ts, ps, lv, sh, .{ 347 return wv.birthTile(fixture.wallOf(a, ts, ps, lv, sh, &.{}), .{
330 .r = .{ .target = tg, .label = "", .session = name }, 348 .r = .{ .target = tg, .label = "", .session = name },
331 .from = 0, 349 .from = 0,
332 .place = .beside_focus, 350 .place = .beside_focus,
@@ -410,7 +428,7 @@ test "birthTile: the digit's next tile frees the copies an unborrowed birth hand
410 a.free(session); 428 a.free(session);
411 return null; 429 return null;
412 }; 430 };
413 return wv.birthTile(a, ts, ps, lv, sh, .{ 431 return wv.birthTile(fixture.wallOf(a, ts, ps, lv, sh, &.{}), .{
414 .r = .{ .target = tg, .label = label, .session = session }, 432 .r = .{ .target = tg, .label = label, .session = session },
415 .from = 0, 433 .from = 0,
416 .place = .beside_focus, 434 .place = .beside_focus,
src/tui/wallview.zig
Old New
@@ -391,6 +391,32 @@ pub const Tile = struct {
391 } 391 }
392 }; 392 };
393 393
394 /// The keyboard thread's whole wall, as one value. `liveTiles`/
395 /// `livePresent` name the prefix walk; a walk of the whole array is a walk
396 /// into uninitialised slots.
397 ///
398 /// Keyboard-thread only, so `paint_mu`'s ownership of the tree is
399 /// unchanged: a pump is handed a `*Tile` and a `*Shared` and never this.
400 pub const Wall = struct {
401 alloc: std.mem.Allocator,
402 tiles: []Tile,
403 present: []bool,
404 /// The high-water mark of slots ever used, not the tile count: a reused
405 /// digit is already inside it, and it never falls.
406 live: *usize,
407 shared: *Shared,
408 hosts: []Host = &.{},
409
410 /// The slots ever used; the array past it is uninitialised.
411 pub fn liveTiles(self: Wall) []Tile {
412 return self.tiles[0..self.live.*];
413 }
414
415 pub fn livePresent(self: Wall) []bool {
416 return self.present[0..self.live.*];
417 }
418 };
419
394 /// Which tile's rectangle a zero-based terminal (row, col) falls in, or 420 /// Which tile's rectangle a zero-based terminal (row, col) falls in, or
395 /// null when none does. A beside layout gives every tile the same rows, so 421 /// null when none does. A beside layout gives every tile the same rows, so
396 /// the column is what separates them; a click on a rail column hits no 422 /// the column is what separates them; a click on a rail column hits no
@@ -743,20 +769,15 @@ pub fn setFocus(tiles: []Tile, shared: *Shared, next: usize) void {
743 769
744 /// Re-cut before the move: `setFocus` releases the outgoing session 770 /// Re-cut before the move: `setFocus` releases the outgoing session
745 /// and needs the true previous focus. 771 /// and needs the true previous focus.
746 pub fn focusAnswer( 772 pub fn focusAnswer(w: Wall, recut: bool, to: usize) void {
747 alloc: std.mem.Allocator, 773 if (recut) wall_layout.relayout(w, w.shared.sel);
748 tiles: []Tile, 774 setFocus(w.liveTiles(), w.shared, to);
749 present: []const bool,
750 shared: *Shared,
751 recut: bool,
752 to: usize,
753 ) void {
754 if (recut) wall_layout.relayout(alloc, tiles, present, shared, shared.sel);
755 setFocus(tiles, shared, to);
756 // Fullscreen re-flattens with the new focus so the full rect follows. 775 // Fullscreen re-flattens with the new focus so the full rect follows.
757 if (shared.fullscreen) wall_layout.relayout(alloc, tiles, present, shared, shared.sel); 776 if (w.shared.fullscreen) wall_layout.relayout(w, w.shared.sel);
758 } 777 }
759 778
779 pub const no_live_here = "[no live session here - Ctrl-\\ s picks a host]";
780
760 /// TAKEN, not read: an empty wall has no pump to paint a notice as a 781 /// TAKEN, not read: an empty wall has no pump to paint a notice as a
761 /// banner, and every relayout would repaint a sentence left on this line. 782 /// banner, and every relayout would repaint a sentence left on this line.
762 pub fn emptyWallHint(shared: *Shared) []const u8 { 783 pub fn emptyWallHint(shared: *Shared) []const u8 {
@@ -974,58 +995,43 @@ const Birth = struct {
974 }; 995 };
975 996
976 /// Every road onto a running wall — chord, fold, prompt — one body. 997 /// Every road onto a running wall — chord, fold, prompt — one body.
977 /// Null: nothing was added. 998 pub fn birthTile(w: Wall, b: Birth) ?usize {
978 pub fn birthTile( 999 return birthTileOrRefuse(w, b) catch null;
979 alloc: std.mem.Allocator,
980 tiles: []Tile,
981 present: []bool,
982 live: *usize,
983 shared: *Shared,
984 b: Birth,
985 ) ?usize {
986 return birthTileOrRefuse(alloc, tiles, present, live, shared, b) catch null;
987 } 1000 }
988 1001
989 fn birthTileOrRefuse( 1002 fn birthTileOrRefuse(w: Wall, b: Birth) !usize {
990 alloc: std.mem.Allocator,
991 tiles: []Tile,
992 present: []bool,
993 live: *usize,
994 shared: *Shared,
995 b: Birth,
996 ) !usize {
997 // The lowest digit a departed tile left behind, before a new one: 1003 // The lowest digit a departed tile left behind, before a new one:
998 // create 1 2 3, end 2, create — and the wall says 2, not 4. Reuse, not 1004 // create 1 2 3, end 2, create — and the wall says 2, not 4. Reuse, not
999 // renumbering: the tiles that stayed keep the digit their user learned. 1005 // renumbering: the tiles that stayed keep the digit their user learned.
1000 const reuse = freeSlot(tiles[0..live.*], present[0..live.*]); 1006 const reuse = freeSlot(w.liveTiles(), w.livePresent());
1001 if (reuse == null and live.* >= max_tiles) return error.WallFull; 1007 if (reuse == null and w.live.* >= max_tiles) return error.WallFull;
1002 const new_live = presentCount(present[0..live.*]) + 1; 1008 const new_live = presentCount(w.livePresent()) + 1;
1003 // "Does it fit" has ONE owner, and it is the tree: insert, flatten, 1009 // "Does it fit" has ONE owner, and it is the tree: insert, flatten,
1004 // and undo the insert when flatten refuses. Row arithmetic here 1010 // and undo the insert when flatten refuses. Row arithmetic here
1005 // capped every terminal at rows/3 panes however wide, because it 1011 // capped every terminal at rows/3 panes however wide, because it
1006 // cannot see that a `.beside` cut spends columns. 1012 // cannot see that a `.beside` cut spends columns.
1007 const at = reuse orelse live.*; 1013 const at = reuse orelse w.live.*;
1008 switch (b.place) { 1014 switch (b.place) {
1009 .beside_focus => if (shared.tree.root == null) 1015 .beside_focus => if (w.shared.tree.root == null)
1010 // A wall whose tiles all arrive from a host's list starts with 1016 // A wall whose tiles all arrive from a host's list starts with
1011 // no tree at all; `insert` has no leaf to sit beside. 1017 // no tree at all; `insert` has no leaf to sit beside.
1012 try shared.tree.addFirst(@intCast(at)) 1018 try w.shared.tree.addFirst(@intCast(at))
1013 else 1019 else
1014 try shared.tree.insert(@intCast(b.from), @intCast(at)), 1020 try w.shared.tree.insert(@intCast(b.from), @intCast(at)),
1015 .right_of => try shared.tree.splitRight(@intCast(b.from), @intCast(at)), 1021 .right_of => try w.shared.tree.splitRight(@intCast(b.from), @intCast(at)),
1016 .below => try shared.tree.splitBelow(@intCast(b.from), @intCast(at)), 1022 .below => try w.shared.tree.splitBelow(@intCast(b.from), @intCast(at)),
1017 } 1023 }
1018 // Past the insert, so every refusal below puts the tree back exactly 1024 // Past the insert, so every refusal below puts the tree back exactly
1019 // as the caller found it. 1025 // as the caller found it.
1020 errdefer shared.tree.remove(@intCast(at)); 1026 errdefer w.shared.tree.remove(@intCast(at));
1021 const flat = try shared.tree.flatten( 1027 const flat = try w.shared.tree.flatten(
1022 alloc, 1028 w.alloc,
1023 shared.size.rows, 1029 w.shared.size.rows,
1024 shared.size.cols, 1030 w.shared.size.cols,
1025 wall_layout.wallFloors(new_live), 1031 wall_layout.wallFloors(new_live),
1026 null, 1032 null,
1027 ); 1033 );
1028 defer flat.deinit(alloc); 1034 defer flat.deinit(w.alloc);
1029 const new_rect = flat.rectOf(@intCast(at)) orelse return error.NoRect; 1035 const new_rect = flat.rectOf(@intCast(at)) orelse return error.NoRect;
1030 // The real rect, not a placeholder: a creating tile puts its rect on 1036 // The real rect, not a placeholder: a creating tile puts its rect on
1031 // the first attach frame and the daemon refuses creates under 1037 // the first attach frame and the daemon refuses creates under
@@ -1033,75 +1039,66 @@ fn birthTileOrRefuse(
1033 // pass, and a 2-row rect on a live session is destructive under 1039 // pass, and a 2-row rect on a live session is destructive under
1034 // latest-wins. `label_rows` is set here so `viewRows` is right from 1040 // latest-wins. `label_rows` is set here so `viewRows` is right from
1035 // the first attach; relayout re-flattens every rect and sets it again. 1041 // the first attach; relayout re-flattens every rect and sets it again.
1036 shared.label_rows = if (new_live > 1) 1 else 0; 1042 w.shared.label_rows = if (new_live > 1) 1 else 0;
1037 // Past every refusal: the tile is the wall's now, so the copies a pump 1043 // Past every refusal: the tile is the wall's now, so the copies a pump
1038 // will hold for its whole life are worth making. Made BEFORE the slot 1044 // will hold for its whole life are worth making. Made BEFORE the slot
1039 // is overwritten, so the last thing that can fail here still fails 1045 // is overwritten, so the last thing that can fail here still fails
1040 // against a slot that is exactly as the caller found it. 1046 // against a slot that is exactly as the caller found it.
1041 var r = b.r; 1047 var r = b.r;
1042 if (b.borrowed) { 1048 if (b.borrowed) {
1043 const session = try alloc.dupe(u8, b.r.session); 1049 const session = try w.alloc.dupe(u8, b.r.session);
1044 errdefer alloc.free(session); 1050 errdefer w.alloc.free(session);
1045 r.session = session; 1051 r.session = session;
1046 r.label = try tileLabel(alloc, b.r.target, session); 1052 r.label = try tileLabel(w.alloc, b.r.target, session);
1047 } 1053 }
1048 // Only the copies THIS call made: a caller that owns them frees its own. 1054 // Only the copies THIS call made: a caller that owns them frees its own.
1049 errdefer if (b.borrowed) { 1055 errdefer if (b.borrowed) {
1050 alloc.free(r.session); 1056 w.alloc.free(r.session);
1051 alloc.free(r.label); 1057 w.alloc.free(r.label);
1052 }; 1058 };
1053 // The departed tile's copies go with its digit. Every tile owns these 1059 // The departed tile's copies go with its digit. Every tile owns these
1054 // two — `run` dupes even the entry tile's session for this — so a 1060 // two — `run` dupes even the entry tile's session for this — so a
1055 // reused slot that kept them would leak one label and one name per 1061 // reused slot that kept them would leak one label and one name per
1056 // birth for the wall's whole life. 1062 // birth for the wall's whole life.
1057 if (reuse != null) { 1063 if (reuse != null) {
1058 alloc.free(tiles[at].r.session); 1064 w.alloc.free(w.tiles[at].r.session);
1059 alloc.free(tiles[at].r.label); 1065 w.alloc.free(w.tiles[at].r.label);
1060 } 1066 }
1061 // Only a `.fresh` doorbell can fail, and it fails before the slot is 1067 // Only a `.fresh` doorbell can fail, and it fails before the slot is
1062 // written, so the errdefers above are the whole unwind. 1068 // written, so the errdefers above are the whole unwind.
1063 try initTile(&tiles[at], r, new_rect, shared, at, if (reuse == null) .fresh else .kept); 1069 try initTile(&w.tiles[at], r, new_rect, w.shared, at, if (reuse == null) .fresh else .kept);
1064 // The caller's row of the birth table, and the whole of what separates 1070 // The caller's row of the birth table, and the whole of what separates
1065 // the roads: a chord row inherits its target and agent and CREATES, a 1071 // the roads: a chord row inherits its target and agent and CREATES, a
1066 // poll row joins a session the daemon already has. The pump spawn stays 1072 // poll row joins a session the daemon already has. The pump spawn stays
1067 // the caller's. 1073 // the caller's.
1068 tiles[at].creates = b.creates; 1074 w.tiles[at].creates = b.creates;
1069 tiles[at].born_from = b.born_from; 1075 w.tiles[at].born_from = b.born_from;
1070 tiles[at].keeps_wall = b.keeps_wall; 1076 w.tiles[at].keeps_wall = b.keeps_wall;
1071 tiles[at].host = b.host; 1077 w.tiles[at].host = b.host;
1072 present[at] = true; 1078 w.present[at] = true;
1073 // `live` is the high-water mark of slots ever used, not the tile count: 1079 // `live` is the high-water mark of slots ever used, not the tile count:
1074 // a reused digit is already inside it, and growing here would walk the 1080 // a reused digit is already inside it, and growing here would walk the
1075 // keyboard's `tiles[0..live]` off the end of the array. 1081 // keyboard's `liveTiles` walk off the end of the array.
1076 if (reuse == null) live.* += 1; 1082 if (reuse == null) w.live.* += 1;
1077 return at; 1083 return at;
1078 } 1084 }
1079 1085
1080 /// A sibling is focused if it has a tile and GETS one if not — otherwise a 1086 /// A sibling is focused if it has a tile and GETS one if not — otherwise a
1081 /// tile labelled S would paint T. 1087 /// tile labelled S would paint T.
1082 fn addSessionTile( 1088 fn addSessionTile(w: Wall, from: usize, name: []const u8, place: Place) FocusTo {
1083 alloc: std.mem.Allocator,
1084 tiles: []Tile,
1085 present: []bool,
1086 live: *usize,
1087 shared: *Shared,
1088 from: usize,
1089 name: []const u8,
1090 place: Place,
1091 ) FocusTo {
1092 // The parent's ask does not descend. A chord is a session on a daemon 1089 // The parent's ask does not descend. A chord is a session on a daemon
1093 // this wall is already attached to, so there is nothing here to start; 1090 // this wall is already attached to, so there is nothing here to start;
1094 // inheriting the bit would hand every descendant of a picker-born tile 1091 // inheriting the bit would hand every descendant of a picker-born tile
1095 // a permission nobody asked for, for the rest of its life. 1092 // a permission nobody asked for, for the rest of its life.
1096 var target = tiles[from].r.target; 1093 var target = w.tiles[from].r.target;
1097 if (target == .hand) target.hand.asked = false; 1094 if (target == .hand) target.hand.asked = false;
1098 const want = proto.resolveName(name); 1095 const want = proto.resolveName(name);
1099 for (tiles[0..live.*], present[0..live.*], 0..) |*t, p, i| { 1096 for (w.liveTiles(), w.livePresent(), 0..) |*t, p, i| {
1100 if (!p) continue; 1097 if (!p) continue;
1101 if (!sameTarget(t.r.target, target)) continue; 1098 if (!sameTarget(t.r.target, target)) continue;
1102 if (std.mem.eql(u8, proto.resolveName(t.r.session), want)) return .{ .moved = i }; 1099 if (std.mem.eql(u8, proto.resolveName(t.r.session), want)) return .{ .moved = i };
1103 } 1100 }
1104 const at = birthTile(alloc, tiles, present, live, shared, .{ 1101 const at = birthTile(w, .{
1105 // The offer is inherited from the tile this one grew out of. Same 1102 // The offer is inherited from the tile this one grew out of. Same
1106 // target, so `-A` exposes nothing the user has not already exposed 1103 // target, so `-A` exposes nothing the user has not already exposed
1107 // to that host — and a chord-made tile has no command line to spell 1104 // to that host — and a chord-made tile has no command line to spell
@@ -1109,15 +1106,15 @@ fn addSessionTile(
1109 // at the first session switch. 1106 // at the first session switch.
1110 // `want` borrows the caller's name: the wall makes its own copies 1107 // `want` borrows the caller's name: the wall makes its own copies
1111 // only if it keeps the tile — see `Birth.borrowed`. 1108 // only if it keeps the tile — see `Birth.borrowed`.
1112 .r = .{ .target = target, .label = "", .session = want, .agent = tiles[from].r.agent }, 1109 .r = .{ .target = target, .label = "", .session = want, .agent = w.tiles[from].r.agent },
1113 .from = from, 1110 .from = from,
1114 .place = place, 1111 .place = place,
1115 .creates = true, 1112 .creates = true,
1116 .born_from = from, 1113 .born_from = from,
1117 .host = tiles[from].host, 1114 .host = w.tiles[from].host,
1118 .borrowed = true, 1115 .borrowed = true,
1119 }) orelse return .full; 1116 }) orelse return .full;
1120 spawnPump(&tiles[at]); 1117 spawnPump(&w.tiles[at]);
1121 return .{ .moved = at }; 1118 return .{ .moved = at };
1122 } 1119 }
1123 1120
@@ -1592,6 +1589,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1592 var live: usize = 0; 1589 var live: usize = 0;
1593 const present = try alloc.alloc(bool, max_tiles); 1590 const present = try alloc.alloc(bool, max_tiles);
1594 @memset(present, false); 1591 @memset(present, false);
1592 // One value for the six the wall used to pass around. `hosts` is
1593 // re-sliced wherever `hosts_live` grows — the picker's `a` is the only
1594 // thing that does.
1595 var w: Wall = .{ .alloc = alloc, .tiles = tiles, .present = present, .live = &live, .shared = &shared };
1595 const env_sock = std.posix.getenv(proto.sock_env); 1596 const env_sock = std.posix.getenv(proto.sock_env);
1596 const env_session = std.posix.getenv(proto.session_env); 1597 const env_session = std.posix.getenv(proto.session_env);
1597 if (has_entry) { 1598 if (has_entry) {
@@ -1624,7 +1625,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1624 @memcpy(tiles[0].in[0..n], entry.carry[0..n]); 1625 @memcpy(tiles[0].in[0..n], entry.carry[0..n]);
1625 tiles[0].in_len = n; 1626 tiles[0].in_len = n;
1626 present[0] = true; 1627 present[0] = true;
1627 live = 1; 1628 w.live.* = 1;
1628 // Its pump claims the terminal on its first pass. Set before 1629 // Its pump claims the terminal on its first pass. Set before
1629 // `spawnPump` so the claim is the first thing the pump does after 1630 // `spawnPump` so the claim is the first thing the pump does after
1630 // the dial, and the attach already carries the terminal's size. 1631 // the dial, and the attach already carries the terminal's size.
@@ -1633,12 +1634,12 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1633 // A one-tile wall owns every row and draws no label bar; two or more 1634 // A one-tile wall owns every row and draws no label bar; two or more
1634 // tiles each lose their top row to one. Set before the pumps start so 1635 // tiles each lose their top row to one. Set before the pumps start so
1635 // `viewRows` is right on the first attach. 1636 // `viewRows` is right on the first attach.
1636 shared.label_rows = if (live > 1) 1 else 0; 1637 shared.label_rows = if (w.live.* > 1) 1 else 0;
1637 for (tiles[0..live]) |*t| spawnPump(t); 1638 for (tiles[0..live]) |*t| spawnPump(t);
1638 // A wall with no tile yet paints its one line rather than nothing: a 1639 // A wall with no tile yet paints its one line rather than nothing: a
1639 // blank terminal with no cursor reads as hung, and the hosts are up to 1640 // blank terminal with no cursor reads as hung, and the hosts are up to
1640 // a poll away from having anything to show. 1641 // a poll away from having anything to show.
1641 if (live == 0) wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, 0); 1642 if (w.live.* == 0) wall_layout.relayout(w, 0);
1642 1643
1643 // One poller per host, all of them at once and none of them on this 1644 // One poller per host, all of them at once and none of them on this
1644 // thread: the user asked for one session and must not be held on 1645 // thread: the user asked for one session and must not be held on
@@ -1649,6 +1650,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1649 // when the picker's `a` adds a host to it. 1650 // when the picker's `a` adds a host to it.
1650 const host_table = try alloc.alloc(Host, max_tiles); 1651 const host_table = try alloc.alloc(Host, max_tiles);
1651 var hosts_live: usize = @min(host_specs.len, max_tiles); 1652 var hosts_live: usize = @min(host_specs.len, max_tiles);
1653 w.hosts = host_table[0..hosts_live];
1652 var over_buf: [64]u8 = undefined; 1654 var over_buf: [64]u8 = undefined;
1653 if (wall_host.hostsOverCapacity(&over_buf, host_specs.len)) |said| setNotice(&shared, said); 1655 if (wall_host.hostsOverCapacity(&over_buf, host_specs.len)) |said| setNotice(&shared, said);
1654 for (host_table[0..hosts_live], host_specs[0..hosts_live]) |*h, spec| { 1656 for (host_table[0..hosts_live], host_specs[0..hosts_live]) |*h, spec| {
@@ -1749,7 +1751,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1749 shared.paint_mu.lock(); 1751 shared.paint_mu.lock();
1750 shared.size = measured2; 1752 shared.size = measured2;
1751 shared.paint_mu.unlock(); 1753 shared.paint_mu.unlock();
1752 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 1754 wall_layout.relayout(w, shared.sel);
1753 } 1755 }
1754 } 1756 }
1755 1757
@@ -1760,7 +1762,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1760 // about to read, and acting on the end of a tile nobody is 1762 // about to read, and acting on the end of a tile nobody is
1761 // looking at any more is the wrong order to notice things in. 1763 // looking at any more is the wrong order to notice things in.
1762 const z = shared.sel; 1764 const z = shared.sel;
1763 if (z < live and tiles[z].ans_ready.swap(false, .acq_rel)) { 1765 if (z < w.live.* and tiles[z].ans_ready.swap(false, .acq_rel)) {
1764 var name: client.SessionName = undefined; 1766 var name: client.SessionName = undefined;
1765 { 1767 {
1766 tiles[z].ans_mu.lock(); 1768 tiles[z].ans_mu.lock();
@@ -1772,30 +1774,21 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1772 // it was, and a wall that read growth off it would put the 1774 // it was, and a wall that read growth off it would put the
1773 // new tile on a screen nothing re-cut. 1775 // new tile on a screen nothing re-cut.
1774 const before = presentCount(present[0..live]); 1776 const before = presentCount(present[0..live]);
1775 switch (addSessionTile( 1777 switch (addSessionTile(w, z, name.slice(), tiles[z].pending_place)) {
1776 alloc,
1777 tiles,
1778 present,
1779 &live,
1780 &shared,
1781 z,
1782 name.slice(),
1783 tiles[z].pending_place,
1784 )) {
1785 .moved => |to| { 1778 .moved => |to| {
1786 focusAnswer(alloc, tiles[0..live], present[0..live], &shared, presentCount(present[0..live]) > before, to); 1779 focusAnswer(w, presentCount(present[0..live]) > before, to);
1787 }, 1780 },
1788 .full => setNotice(&shared, "[no room on the wall for another tile]"), 1781 .full => setNotice(&shared, "[no room on the wall for another tile]"),
1789 .stay => {}, 1782 .stay => {},
1790 } 1783 }
1791 wall_host.pokeHost(host_table[0..hosts_live], &tiles[z]); 1784 wall_host.pokeHost(w.hosts, &tiles[z]);
1792 } 1785 }
1793 } 1786 }
1794 // Ends are read every pass, not only on the bell: two pumps dying 1787 // Ends are read every pass, not only on the bell: two pumps dying
1795 // together share one ring, and `drainBell` clears it, so a residual 1788 // together share one ring, and `drainBell` clears it, so a residual
1796 // end would otherwise wait for a ring that may not come. 1789 // end would otherwise wait for a ring that may not come.
1797 if (endedTile(tiles[0..live], present[0..live], &shared)) |ended| { 1790 if (endedTile(tiles[0..live], present[0..live], &shared)) |ended| {
1798 switch (endAction(tiles, present, live, ended, stdin_open, shared.is_tty)) { 1791 switch (endAction(tiles, present, w.live.*, ended, stdin_open, shared.is_tty)) {
1799 .refocus => |to| { 1792 .refocus => |to| {
1800 if (!tiles[to].alive.load(.acquire)) 1793 if (!tiles[to].alive.load(.acquire))
1801 setNotice(&shared, "[focus on a dead tile - no live neighbour]"); 1794 setNotice(&shared, "[focus on a dead tile - no live neighbour]");
@@ -1821,10 +1814,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1821 setNotice(&shared, "[focus on a dead tile - no live neighbour]"); 1814 setNotice(&shared, "[focus on a dead tile - no live neighbour]");
1822 } 1815 }
1823 vanishTile(tiles[0..live], present[0..live], &shared, ended, v.back); 1816 vanishTile(tiles[0..live], present[0..live], &shared, ended, v.back);
1824 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 1817 wall_layout.relayout(w, shared.sel);
1825 }, 1818 },
1826 .finish => |how| { 1819 .finish => |how| {
1827 wall_layout.saveSidecar(alloc, tiles[0..live], present[0..live], &shared); 1820 wall_layout.saveSidecar(w);
1828 exit_code = how.code; 1821 exit_code = how.code;
1829 exit_msg = how.msg; 1822 exit_msg = how.msg;
1830 break :keys; 1823 break :keys;
@@ -1834,7 +1827,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1834 // Ends first, lists second: a session that exited is its pump's 1827 // Ends first, lists second: a session that exited is its pump's
1835 // news and arrives at once, while a list is up to a poll behind. 1828 // news and arrives at once, while a list is up to a poll behind.
1836 // Reading the list first would vanish the tile the exit code is on. 1829 // Reading the list first would vanish the tile the exit code is on.
1837 const host_news = wall_host.applyReadyLists(alloc, tiles, present, &live, &shared, host_table[0..hosts_live]); 1830 const host_news = wall_host.applyReadyLists(w);
1838 if (!restore_tried) { 1831 if (!restore_tried) {
1839 var all_reported = true; 1832 var all_reported = true;
1840 for (host_table[0..opening_hosts]) |*h| { 1833 for (host_table[0..opening_hosts]) |*h| {
@@ -1846,12 +1839,12 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1846 if (all_reported or std.time.milliTimestamp() >= restore_due) { 1839 if (all_reported or std.time.milliTimestamp() >= restore_due) {
1847 restore_tried = true; 1840 restore_tried = true;
1848 var saved_focus: ?usize = null; 1841 var saved_focus: ?usize = null;
1849 if (live > 0 and wall_layout.restoreSidecar(alloc, tiles, present, live, &shared, &saved_focus) != null) { 1842 if (w.live.* > 0 and wall_layout.restoreSidecar(w, &saved_focus) != null) {
1850 restore_ok = true; 1843 restore_ok = true;
1851 // The entry tile is the one the user is already typing 1844 // The entry tile is the one the user is already typing
1852 // into; a saved focus record must not move them off it. 1845 // into; a saved focus record must not move them off it.
1853 const to = if (has_entry) shared.sel else (saved_focus orelse shared.sel); 1846 const to = if (has_entry) shared.sel else (saved_focus orelse shared.sel);
1854 focusAnswer(alloc, tiles[0..live], present[0..live], &shared, true, to); 1847 focusAnswer(w, true, to);
1855 } 1848 }
1856 } 1849 }
1857 } 1850 }
@@ -1860,7 +1853,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1860 shared.paint_mu.lock(); 1853 shared.paint_mu.lock();
1861 shared.tree.setRootOrient(wall_layout.rootOrient(shared.size)); 1854 shared.tree.setRootOrient(wall_layout.rootOrient(shared.size));
1862 shared.paint_mu.unlock(); 1855 shared.paint_mu.unlock();
1863 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 1856 wall_layout.relayout(w, shared.sel);
1864 } 1857 }
1865 // An empty wall IS the picker: there is nothing else on the screen 1858 // An empty wall IS the picker: there is nothing else on the screen
1866 // to act from, and the last `x` is exactly when the user needs the 1859 // to act from, and the last `x` is exactly when the user needs the
@@ -1881,14 +1874,14 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1881 picker_opened = true; 1874 picker_opened = true;
1882 picker_shown = true; 1875 picker_shown = true;
1883 input.prefix.picking = true; 1876 input.prefix.picking = true;
1884 picker_sel = wall_picker.pickerNearest(host_table[0..hosts_live], picker_sel); 1877 picker_sel = wall_picker.pickerNearest(w.hosts, picker_sel);
1885 }, 1878 },
1886 .close => { 1879 .close => {
1887 input.prefix.picking = false; 1880 input.prefix.picking = false;
1888 picker_shown = false; 1881 picker_shown = false;
1889 shared.picker_open.store(false, .release); 1882 shared.picker_open.store(false, .release);
1890 shared.picker_stamp = 0; 1883 shared.picker_stamp = 0;
1891 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 1884 wall_layout.relayout(w, shared.sel);
1892 }, 1885 },
1893 .leave => {}, 1886 .leave => {},
1894 } 1887 }
@@ -1905,7 +1898,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1905 var foot_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined; 1898 var foot_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined;
1906 const repair = wall_picker.pickerRepaint(&foot_buf, &input.prefix, picker_opened or host_news or 1899 const repair = wall_picker.pickerRepaint(&foot_buf, &input.prefix, picker_opened or host_news or
1907 winch or fds[1].revents != 0 or shared.picker_stamp == 0); 1900 winch or fds[1].revents != 0 or shared.picker_stamp == 0);
1908 if (repair.due) wall_picker.paintPicker(&shared, host_table[0..hosts_live], picker_sel, repair.line); 1901 if (repair.due) wall_picker.paintPicker(&shared, w.hosts, picker_sel, repair.line);
1909 if (fds[0].revents == 0) continue; 1902 if (fds[0].revents == 0) continue;
1910 1903
1911 const n = std.posix.read(stdin_fd, &b) catch break; 1904 const n = std.posix.read(stdin_fd, &b) catch break;
@@ -1924,42 +1917,25 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1924 // no tile at all, which is why it is answered ahead of both. 1917 // no tile at all, which is why it is answered ahead of both.
1925 if (input.prefix.picking or wall_picker.isPickAction(cmd.action)) { 1918 if (input.prefix.picking or wall_picker.isPickAction(cmd.action)) {
1926 // Typed AT the session, ahead of the chord, in the same read. 1919 // Typed AT the session, ahead of the chord, in the same read.
1927 if (cmd.forward.len > 0 and z < live and present[z]) sendKeys(&tiles[z], cmd.forward); 1920 if (cmd.forward.len > 0 and z < w.live.* and present[z]) sendKeys(&tiles[z], cmd.forward);
1928 var birth_at: ?usize = null; 1921 var birth_at: ?usize = null;
1929 // Opening on a focused tile pre-selects that tile's host: the 1922 // Opening on a focused tile pre-selects that tile's host: the
1930 // machine the user is already on is the one they mean. Judged 1923 // machine the user is already on is the one they mean. Judged
1931 // on the popup's own state, not on `.pick_open`, because one 1924 // on the popup's own state, not on `.pick_open`, because one
1932 // read can open the picker and its editor together. 1925 // read can open the picker and its editor together.
1933 if (!picker_shown) { 1926 if (!picker_shown) {
1934 if (z < live and present[z]) { 1927 if (z < w.live.* and present[z]) {
1935 if (tiles[z].host) |hi| picker_sel = hi; 1928 if (tiles[z].host) |hi| picker_sel = hi;
1936 } 1929 }
1937 picker_sel = wall_picker.pickerNearest(host_table[0..hosts_live], picker_sel); 1930 picker_sel = wall_picker.pickerNearest(w.hosts, picker_sel);
1938 } 1931 }
1939 switch (cmd.action) { 1932 switch (cmd.action) {
1940 .pick_move => |d| picker_sel = wall_picker.pickerStep(host_table[0..hosts_live], picker_sel, d), 1933 .pick_move => |d| picker_sel = wall_picker.pickerStep(w.hosts, picker_sel, d),
1941 .pick_select => |row| { 1934 .pick_select => |row| {
1942 if (wall_picker.pickerAt(host_table[0..hosts_live], row - 1)) |hi| picker_sel = hi; 1935 if (wall_picker.pickerAt(w.hosts, row - 1)) |hi| picker_sel = hi;
1943 }, 1936 },
1944 .pick_birth => birth_at = wall_picker.pickBirth( 1937 .pick_birth => birth_at = wall_picker.pickBirth(w, picker_sel),
1945 alloc, 1938 .pick_forget => wall_picker.pickForget(w, picker_sel, hosts_path),
1946 tiles,
1947 present,
1948 &live,
1949 &shared,
1950 host_table[0..hosts_live],
1951 picker_sel,
1952 ),
1953 .pick_forget => wall_picker.pickForget(
1954 alloc,
1955 tiles,
1956 present,
1957 live,
1958 &shared,
1959 host_table[0..hosts_live],
1960 picker_sel,
1961 hosts_path,
1962 ),
1963 .add_tile => |spelling| { 1939 .add_tile => |spelling| {
1964 switch (wall_host.addHost( 1940 switch (wall_host.addHost(
1965 alloc, 1941 alloc,
@@ -1972,6 +1948,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1972 hosts_path, 1948 hosts_path,
1973 )) { 1949 )) {
1974 .added => |hi| { 1950 .added => |hi| {
1951 // The only thing that grows `hosts_live`, so the
1952 // only place the wall's own slice has to follow.
1953 w.hosts = host_table[0..hosts_live];
1975 const th = std.Thread.spawn(.{}, wall_host.pollHost, .{&host_table[hi]}) catch null; 1954 const th = std.Thread.spawn(.{}, wall_host.pollHost, .{&host_table[hi]}) catch null;
1976 if (th) |handle| handle.detach(); 1955 if (th) |handle| handle.detach();
1977 // The row the user just made is the row they meant. 1956 // The row the user just made is the row they meant.
@@ -1989,7 +1968,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1989 if (input.prefix.picking) { 1968 if (input.prefix.picking) {
1990 var line_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined; 1969 var line_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined;
1991 const keyed = wall_picker.pickerRepaint(&line_buf, &input.prefix, true); 1970 const keyed = wall_picker.pickerRepaint(&line_buf, &input.prefix, true);
1992 wall_picker.paintPicker(&shared, host_table[0..hosts_live], picker_sel, keyed.line); 1971 wall_picker.paintPicker(&shared, w.hosts, picker_sel, keyed.line);
1993 } else { 1972 } else {
1994 // The close gives the terminal back: `relayout` clears it 1973 // The close gives the terminal back: `relayout` clears it
1995 // and bumps `repaint_gen`, which is the only thing that 1974 // and bumps `repaint_gen`, which is the only thing that
@@ -2001,9 +1980,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2001 picker_shown = false; 1980 picker_shown = false;
2002 picker_auto.taken(); 1981 picker_auto.taken();
2003 if (birth_at) |at| 1982 if (birth_at) |at|
2004 focusAnswer(alloc, tiles[0..live], present[0..live], &shared, true, at) 1983 focusAnswer(w, true, at)
2005 else 1984 else
2006 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 1985 wall_layout.relayout(w, shared.sel);
2007 // The focused pump may be holding a claim the popup refused; 1986 // The focused pump may be holding a claim the popup refused;
2008 // it re-arms and retries on its next pass, and this is what 1987 // it re-arms and retries on its next pass, and this is what
2009 // makes that pass happen now rather than within a poll. 1988 // makes that pass happen now rather than within a poll.
@@ -2031,7 +2010,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2031 }, 2010 },
2032 .new_session, .split_right, .split_below => { 2011 .new_session, .split_right, .split_below => {
2033 setNotice(&shared, "[no session to birth beside - Ctrl-\\ s picks a host]"); 2012 setNotice(&shared, "[no session to birth beside - Ctrl-\\ s picks a host]");
2034 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 2013 wall_layout.relayout(w, shared.sel);
2035 }, 2014 },
2036 else => {}, 2015 else => {},
2037 } 2016 }
@@ -2108,7 +2087,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2108 tiles[z].detach_req.store(true, .release); 2087 tiles[z].detach_req.store(true, .release);
2109 ring(&tiles[z]); 2088 ring(&tiles[z]);
2110 awaitDetach(&tiles[z], &shared); 2089 awaitDetach(&tiles[z], &shared);
2111 wall_layout.saveSidecar(alloc, tiles[0..live], present[0..live], &shared); 2090 wall_layout.saveSidecar(w);
2112 exit_code = 0; 2091 exit_code = 0;
2113 exit_msg = "mux: detached (session still running; run mux to reattach)"; 2092 exit_msg = "mux: detached (session still running; run mux to reattach)";
2114 break :keys; 2093 break :keys;
@@ -2117,7 +2096,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2117 // Nothing left to fold in: every tile the wall will ever 2096 // Nothing left to fold in: every tile the wall will ever
2118 // have is already here or on its way from a host's list. 2097 // have is already here or on its way from a host's list.
2119 // `Ctrl-\ w` is a re-cut of the stripes, and an unzoom. 2098 // `Ctrl-\ w` is a re-cut of the stripes, and an unzoom.
2120 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 2099 wall_layout.relayout(w, shared.sel);
2121 }, 2100 },
2122 .new_session, .split_right, .split_below => { 2101 .new_session, .split_right, .split_below => {
2123 // Ask the focused tile's daemon for a new session. The 2102 // Ask the focused tile's daemon for a new session. The
@@ -2131,9 +2110,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2131 }; 2110 };
2132 tiles[z].ask.store(@intFromEnum(client.SwitchIntent.new), .release); 2111 tiles[z].ask.store(@intFromEnum(client.SwitchIntent.new), .release);
2133 if (ringLive(&tiles[z])) { 2112 if (ringLive(&tiles[z])) {
2134 wall_host.pokeHost(host_table[0..hosts_live], &tiles[z]); 2113 wall_host.pokeHost(w.hosts, &tiles[z]);
2135 } else { 2114 } else {
2136 setNotice(&shared, "[no live session here - Ctrl-\\ s picks a host]"); 2115 setNotice(&shared, no_live_here);
2137 showRefusal(tiles[0..live], &shared, z); 2116 showRefusal(tiles[0..live], &shared, z);
2138 } 2117 }
2139 }, 2118 },
@@ -2142,16 +2121,16 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2142 // every session every host has, so asking one daemon for a 2121 // every session every host has, so asking one daemon for a
2143 // ring would walk its slice and skip the rest of the wall. 2122 // ring would walk its slice and skip the rest of the wall.
2144 if (walkTiles(present[0..live], z, cmd.action == .next_session)) |to| { 2123 if (walkTiles(present[0..live], z, cmd.action == .next_session)) |to| {
2145 focusAnswer(alloc, tiles[0..live], present[0..live], &shared, false, to); 2124 focusAnswer(w, false, to);
2146 } 2125 }
2147 }, 2126 },
2148 .focus_dir => |d| { 2127 .focus_dir => |d| {
2149 const flat = shared.base_flat orelse shared.last_flat; 2128 const flat = shared.base_flat orelse shared.last_flat;
2150 if (flat) |f| { 2129 if (flat) |f| {
2151 if (layout.neighbor(f, @intCast(z), wall_layout.dirOf(d))) |nb| { 2130 if (layout.neighbor(f, @intCast(z), wall_layout.dirOf(d))) |nb| {
2152 if (nb < live and present[nb] and nb != z) { 2131 if (nb < w.live.* and present[nb] and nb != z) {
2153 if (shared.fullscreen) 2132 if (shared.fullscreen)
2154 focusAnswer(alloc, tiles[0..live], present[0..live], &shared, false, nb) 2133 focusAnswer(w, false, nb)
2155 else 2134 else
2156 setFocus(tiles[0..live], &shared, nb); 2135 setFocus(tiles[0..live], &shared, nb);
2157 } 2136 }
@@ -2160,20 +2139,20 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2160 }, 2139 },
2161 .fullscreen => { 2140 .fullscreen => {
2162 shared.fullscreen = !shared.fullscreen; 2141 shared.fullscreen = !shared.fullscreen;
2163 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 2142 wall_layout.relayout(w, shared.sel);
2164 }, 2143 },
2165 .resize => |d| { 2144 .resize => |d| {
2166 _ = wall_layout.doResize(alloc, tiles[0..live], present[0..live], &shared, z, d); 2145 _ = wall_layout.doResize(w, z, d);
2167 }, 2146 },
2168 .focus => |idx| { 2147 .focus => |idx| {
2169 if (idx > 0 and idx <= live and present[idx - 1] and idx - 1 != z) { 2148 if (idx > 0 and idx <= w.live.* and present[idx - 1] and idx - 1 != z) {
2170 if (shared.fullscreen) 2149 if (shared.fullscreen)
2171 focusAnswer(alloc, tiles[0..live], present[0..live], &shared, false, idx - 1) 2150 focusAnswer(w, false, idx - 1)
2172 else 2151 else
2173 setFocus(tiles[0..live], &shared, idx - 1); 2152 setFocus(tiles[0..live], &shared, idx - 1);
2174 } 2153 }
2175 }, 2154 },
2176 .end_session => if (z < live and present[z]) { 2155 .end_session => if (z < w.live.* and present[z]) {
2177 // `Ctrl-\ x` ends the SESSION on its daemon. The tile leaves 2156 // `Ctrl-\ x` ends the SESSION on its daemon. The tile leaves
2178 // when the daemon's list no longer has it, not when the key 2157 // when the daemon's list no longer has it, not when the key
2179 // is pressed. 2158 // is pressed.
@@ -2197,7 +2176,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2197 break :keys; 2176 break :keys;
2198 } 2177 }
2199 setNotice(&shared, "[nothing attached there yet - tile closed]"); 2178 setNotice(&shared, "[nothing attached there yet - tile closed]");
2200 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 2179 wall_layout.relayout(w, shared.sel);
2201 }, 2180 },
2202 .none => {}, 2181 .none => {},
2203 } 2182 }