a73x

44e36df7

refactor: the pane tree and its sidecar are wall_layout.zig

a73x   2026-08-28 20:51

Commit message
refactor: the pane tree and its sidecar are wall_layout.zig

relayout is the single flatten point and the sidecar is the tree it
writes down; both are about rects, not about tiles. The flagged-prose
budget moves with the prose — same 1596 bytes, new file.

docscheck.budget
Old New
@@ -30,7 +30,7 @@ sockpath.zig 0
30 spawn.zig 0 30 spawn.zig 0
31 testtmp.zig 0 31 testtmp.zig 0
32 upgrade.zig 269 32 upgrade.zig 269
33 wallview.zig 1596 33 wallview.zig 0
34 wall.zig 0 34 wall.zig 0
35 wasm_core.zig 0 35 wasm_core.zig 0
36 webhub_main.zig 0 36 webhub_main.zig 0
@@ -51,3 +51,4 @@ 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
src/tui/wall_host.zig
Old New
@@ -11,6 +11,7 @@ const hosts = @import("hosts");
11 const handoff = @import("handoff"); 11 const handoff = @import("handoff");
12 const xdg = @import("xdg"); 12 const xdg = @import("xdg");
13 const sockpath = @import("sockpath"); 13 const sockpath = @import("sockpath");
14 const wall_layout = @import("wall_layout.zig");
14 const wv = @import("wallview.zig"); 15 const wv = @import("wallview.zig");
15 const Shared = wv.Shared; 16 const Shared = wv.Shared;
16 const Tile = wv.Tile; 17 const Tile = wv.Tile;
@@ -472,7 +473,7 @@ pub fn applyHostList(
472 // anchor, so anchoring every birth at the focus would lay a list of 473 // anchor, so anchoring every birth at the focus would lay a list of
473 // {b, c} out as c, b — a wall reading back-to-front against the 474 // {b, c} out as c, b — a wall reading back-to-front against the
474 // order its daemon reported, and against the digits the chords use. 475 // order its daemon reported, and against the digits the chords use.
475 var anchor = wv.anchorTile(present[0..live.*], shared.sel); 476 var anchor = wall_layout.anchorTile(present[0..live.*], shared.sel);
476 // Stops at the FIRST refusal rather than retrying each name: the 477 // Stops at the FIRST refusal rather than retrying each name: the
477 // wall refuses for a reason that holds for the whole list (no slot, 478 // wall refuses for a reason that holds for the whole list (no slot,
478 // no room to cut), and this list comes back every second — a 479 // no room to cut), and this list comes back every second — a
@@ -506,8 +507,8 @@ pub fn applyHostList(
506 } 507 }
507 if ((!had_focus or shared.sel >= live.* or !present[shared.sel]) and 508 if ((!had_focus or shared.sel >= live.* or !present[shared.sel]) and
508 wv.presentCount(present[0..live.*]) > 0) 509 wv.presentCount(present[0..live.*]) > 0)
509 wv.setFocus(tiles[0..live.*], shared, wv.firstPresent(present[0..live.*]) orelse 0); 510 wv.setFocus(tiles[0..live.*], shared, wall_layout.firstPresent(present[0..live.*]) orelse 0);
510 if (changed) wv.relayout(alloc, tiles[0..live.*], present[0..live.*], shared, shared.sel); 511 if (changed) wall_layout.relayout(alloc, tiles[0..live.*], present[0..live.*], shared, shared.sel);
511 } 512 }
512 513
513 /// The host grammar's own spelling of a target, for a wall entered by 514 /// The host grammar's own spelling of a target, for a wall entered by
src/tui/wall_layout.zig
Old New
@@ -0,0 +1,427 @@
1 //! The pane tree's operations and the layout sidecar. `relayout` is the
2 //! single flatten point that turns the tree into tile rects and paints the
3 //! rails; the sidecar saves that tree on the last detach and heals it back
4 //! per leaf against the hosts' live lists.
5 const std = @import("std");
6 const proto = @import("protocol");
7 const wall = @import("wall");
8 const interact = @import("interact");
9 const layout = @import("layout");
10 const wall_host = @import("wall_host.zig");
11 const wv = @import("wallview.zig");
12 const Resolved = wall_host.Resolved;
13 const Shared = wv.Shared;
14 const Tile = wv.Tile;
15
16 /// 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 /// lose a row to a bar, so each stripe must hold that floor PLUS the bar
19 /// or the daemon drops the resize and the tile freezes on a stale grid.
20 pub fn wallFloors(live: usize) layout.Floors {
21 return .{
22 .rows = proto.min_session_rows + @as(u16, @intFromBool(live > 1)),
23 .cols = proto.min_session_cols,
24 };
25 }
26
27 /// The root container's orientation for N tiles at a given terminal size:
28 /// `.beside` when the terminal is wide enough that columns are the natural
29 /// cut, `.stacked` otherwise. The 2x corrects for cell shape — a terminal
30 /// twice as wide as it is tall has roughly square panes side-by-side.
31 pub fn rootOrient(size: proto.Size) layout.Orient {
32 return if (size.cols >= 2 * size.rows) .beside else .stacked;
33 }
34
35 /// `interact.Dir` and `layout.Dir` are the same enum tags in different
36 /// modules; this is the one place they meet, so the switch stays explicit.
37 pub fn dirOf(d: interact.PrefixFilter.Dir) layout.Dir {
38 return switch (d) {
39 .left => .left,
40 .down => .down,
41 .up => .up,
42 .right => .right,
43 };
44 }
45
46 /// layout.resize always GAINS focus cells; shrink = grow a neighbor at
47 /// focus's expense. Fullscreen refuses — a hidden layout resizing
48 /// invisibly is surprise, not power.
49 pub fn doResize(
50 alloc: std.mem.Allocator,
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);
59 const grow = switch (ld) {
60 .right, .down => true,
61 .left, .up => false,
62 };
63 const flat = shared.base_flat orelse shared.last_flat orelse return false;
64 const focus_tile: u8 = @intCast(sel);
65 var moved = false;
66 if (grow) {
67 // Focus gains from the sibling toward `ld`; if none there (edge
68 // pane), try the opposite side — gaining from either sibling
69 // widens or tallens the focus.
70 if (shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), focus_tile, ld, 1)) {
71 moved = true;
72 } else {
73 const opp = switch (ld) {
74 .right => layout.Dir.left,
75 .down => layout.Dir.up,
76 .left => layout.Dir.right,
77 .up => layout.Dir.down,
78 };
79 moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), focus_tile, opp, 1);
80 }
81 } else {
82 // Shrink: a neighbor on the same axis gains a cell from focus.
83 // Try the side the key points at first, then the opposite — a
84 // pane pressed against one wall can still shrink toward the other.
85 const opp = switch (ld) {
86 .left => layout.Dir.right,
87 .up => layout.Dir.down,
88 .right => layout.Dir.left,
89 .down => layout.Dir.up,
90 };
91 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);
93 }
94 if (!moved) {
95 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);
97 }
98 }
99 }
100 if (moved) relayout(alloc, tiles, present, shared, sel);
101 return moved;
102 }
103
104 /// Paint the vertical rails between `.beside` siblings. One column of
105 /// `\x1b[7m \x1b[0m` per rail — a reverse-video bar in the label-bar's
106 /// style, so a rail reads as structure and not as session output. The wall
107 /// owns rails; tiles never touch them (paint.zig's span-bounded clears).
108 fn paintRailsLocked(shared: *Shared, flat: layout.Flat) void {
109 if (!shared.is_tty) return;
110 if (flat.rails.len == 0) return;
111 var buf: [128]u8 = undefined;
112 for (flat.rails) |rail| {
113 var row: u16 = rail.top;
114 while (row < rail.top + rail.rows) : (row += 1) {
115 const out = std.fmt.bufPrint(&buf, "\x1b[{d};{d}H\x1b[7m \x1b[0m", .{ row + 1, rail.col + 1 }) catch continue;
116 proto.writeAllFd(shared.out_fd, out) catch {};
117 }
118 }
119 }
120
121 /// One `paint_mu` hold: no window where a pump paints rows that just
122 /// changed owner.
123 pub fn relayout(
124 alloc: std.mem.Allocator,
125 tiles: []Tile,
126 present: []const bool,
127 shared: *Shared,
128 sel: usize,
129 ) void {
130 shared.paint_mu.lock();
131 defer shared.paint_mu.unlock();
132 shared.sel = sel;
133
134 var live: usize = 0;
135 for (present) |p| {
136 if (p) live += 1;
137 }
138 // 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
140 // visible pane, no bar — the plain client's byte stream.
141 shared.label_rows = if (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 {};
143 // The screen the popup was on has just been cleared, so the next paint
144 // owes it however unchanged its rows are.
145 if (shared.picker_open.load(.acquire)) shared.picker_stamp = 0;
146 if (live == 0) {
147 wv.paintEmptyWallLocked(shared);
148 return;
149 }
150 // The base flat (null) is always computed so `focus_dir` can read
151 // adjacency from the real layout while fullscreened.
152 if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), null)) |base| {
153 if (shared.base_flat) |*old| old.deinit(shared.flat_alloc);
154 shared.base_flat = base;
155 } else |_| {}
156 const fs_arg: ?u8 = if (shared.fullscreen) @intCast(sel) else null;
157 var cut = shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), fs_arg);
158 if (cut) |_| {} else |e| {
159 // A split, an insert, a resize key: those are OPERATIONS, the user
160 // asked, and refusing leaves the screen exactly as it was. A
161 // SIGWINCH is neither — the terminal has ALREADY shrunk, so
162 // refusing leaves every rect pointing past the bottom of a screen
163 // that was just cleared. Degrade instead, to the view the wall
164 // already has for a terminal that holds one pane: the focused tile
165 // whole, the rest at 0x0, which paints nothing and claims no size.
166 // The tree is untouched, so growing back re-cuts every pane.
167 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| {
169 cut = only;
170 // One visible pane draws no bar. Left at 1, `viewRows`
171 // would owe the daemon a row the tile does not have.
172 shared.label_rows = 0;
173 } else |_| {}
174 }
175 }
176 if (cut) |flat| {
177 if (shared.last_flat) |*old| old.deinit(shared.flat_alloc);
178 shared.last_flat = flat;
179 for (tiles, present) |*t, p| {
180 if (!p) continue;
181 if (flat.rectOf(@intCast(t.idx))) |r| {
182 t.rect = r;
183 }
184 t.resize_pending = true;
185 }
186 paintRailsLocked(shared, flat);
187 } else |_| {}
188
189 // 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
191 // makes that immediate rather than one poll timeout away.
192 _ = shared.repaint_gen.fetchAdd(1, .release);
193 for (tiles, present) |*t, p| {
194 if (p) wv.ring(t);
195 }
196 // ...except the tiles with no pump left to hear it: their bars are the
197 // keyboard's, the same rule `setFocus` paints a focus move by. No
198 // `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
200 // to say the target refused.
201 wv.paintDeadBarsLocked(tiles);
202 }
203
204 /// Rebuild the wall's tree from a saved sidecar, healing per leaf: each
205 /// saved leaf id takes the first wall tile whose label matches its
206 /// spelling, unmatched saved leaves collapse out, and unmatched wall tiles
207 /// insert beside the first matched one. The saved root orientation wins
208 /// over the aspect heuristic — `setRootOrient` is never called here.
209 ///
210 /// Pure of file I/O: bytes come in, the caller decides whether to feed
211 /// them. False on any malformation, zero matches, or allocation failure —
212 /// the caller builds today's default tree and the sidecar is ignored.
213 pub fn restoreLayout(
214 alloc: std.mem.Allocator,
215 resolved: []const Resolved,
216 shared: *Shared,
217 bytes: []const u8,
218 focus_out: *?u8,
219 ) bool {
220 var parsed = layout.parse(alloc, bytes) orelse return false;
221 // Every early-false path deinits parsed; the success path moves the
222 // tree out and deinits only the spellings.
223
224 const n_saved = parsed.spellings.items.len;
225 const map = alloc.alloc(?u8, n_saved) catch {
226 parsed.deinit(alloc);
227 return false;
228 };
229 defer alloc.free(map);
230
231 var taken = alloc.alloc(bool, resolved.len) catch {
232 parsed.deinit(alloc);
233 return false;
234 };
235 defer alloc.free(taken);
236 @memset(taken, false);
237
238 var any_match = false;
239 for (0..n_saved) |i| {
240 const spelling = parsed.spellings.items[i];
241 map[i] = null;
242 for (resolved, 0..) |r, j| {
243 if (!taken[j] and std.mem.eql(u8, r.label, spelling)) {
244 taken[j] = true;
245 map[i] = @intCast(j);
246 any_match = true;
247 break;
248 }
249 }
250 }
251
252 // Zero matches: the sidecar describes a different wall entirely.
253 // Deinit parsed and return false so the caller builds the default tree.
254 if (!any_match) {
255 parsed.deinit(alloc);
256 return false;
257 }
258
259 // Map the saved focus through the spelling match before remapLeaves
260 // consumes map. A null here means the focused leaf did not survive
261 // (no wall tile matched its spelling) — the caller falls back to 0.
262 if (parsed.focus) |k| {
263 if (k < map.len) focus_out.* = map[k];
264 }
265
266 parsed.tree.remapLeaves(map);
267
268 // Unmatched wall indices, ascending, insert beside the first matched
269 // tile — the same beside-focus placement the initial build uses.
270 const first_tile: u8 = if (map.len > 0) blk: {
271 var k: usize = 0;
272 while (k < map.len) : (k += 1) {
273 if (map[k] != null) break :blk map[k].?;
274 }
275 break :blk 0;
276 } else 0;
277
278 var next_tile: u8 = first_tile;
279 for (taken, 0..) |is_taken, j| {
280 if (!is_taken) {
281 const new_id: u8 = @intCast(j);
282 parsed.tree.insert(next_tile, new_id) catch {
283 parsed.deinit(alloc);
284 return false;
285 };
286 next_tile = new_id;
287 }
288 }
289
290 // Move the parsed tree into shared, deinit the old one first.
291 shared.tree.deinit();
292 shared.tree = parsed.tree;
293 // The spellings are no longer needed — the tree holds only ids now.
294 for (parsed.spellings.items) |s| alloc.free(s);
295 parsed.spellings.deinit(alloc);
296 return true;
297 }
298
299 /// `serialize` indexes spellings by leaf ID (tile index), so the array is
300 /// tile-indexed: holes get "" and are never serialized (the tree dropped
301 /// them). A failed write prints one stderr line and returns.
302 pub fn saveLayoutTo(
303 alloc: std.mem.Allocator,
304 path: []const u8,
305 tiles: []Tile,
306 present: []const bool,
307 shared: *Shared,
308 ) void {
309 const spellings = alloc.alloc([]const u8, tiles.len) catch return;
310 defer alloc.free(spellings);
311 for (tiles, present, 0..) |*t, p, i| {
312 spellings[i] = if (p) t.r.label else "";
313 }
314 var buf = std.ArrayListUnmanaged(u8){};
315 defer buf.deinit(alloc);
316 // shared.sel is the focused tile index; serialize maps it to the
317 // encounter index of its leaf in the depth-first walk.
318 const focus: ?u8 = if (shared.sel < tiles.len and present[shared.sel]) @intCast(shared.sel) else null;
319 shared.tree.serialize(spellings, focus, buf.writer(alloc)) catch |err| {
320 std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)});
321 return;
322 };
323 wall.saveLayout(path, buf.items) catch |err| {
324 std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)});
325 };
326 }
327
328 /// Resolves the sidecar path from env and delegates to `saveLayoutTo`.
329 pub fn saveSidecar(
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
336 // 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.
338 if (!shared.is_tty) return;
339 const path = wall.layoutPath(alloc) catch |err| {
340 std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)});
341 return;
342 };
343 defer alloc.free(path);
344 saveLayoutTo(alloc, path, tiles, present, shared);
345 }
346
347 /// The saved layout over the tiles the hosts turned out to have; null
348 /// when nothing matched.
349 pub fn restoreSidecar(
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
358 // `focusAnswer(recut = true)`, whose `relayout` flags `resize_pending`
359 // on EVERY present tile whether or not that tile's rect moved; each
360 // pump then sends `.resize`, and `Server.onResize` answers every one of
361 // them with `resyncSnapshot` — which has no "nothing changed" guard.
362 // Measured on a scripted `mux quic://...` whose state home already held
363 // 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
365 // sending a resize (see the pump's focus-claim block).
366 if (!shared.is_tty) return null;
367 const path = wall.layoutPath(alloc) catch return null;
368 defer alloc.free(path);
369 const bytes = wall.loadLayout(alloc, path) orelse return null;
370 defer alloc.free(bytes);
371 return restoreLayoutFrom(alloc, tiles, present, live, shared, bytes, focus_out);
372 }
373
374 /// Pure of file I/O like `restoreLayout`: the chained id translations —
375 /// saved→dense, then dense→real — are what a test must reach.
376 pub fn restoreLayoutFrom(
377 alloc: std.mem.Allocator,
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;
387 const dense = alloc.alloc(Resolved, n) catch return null;
388 defer alloc.free(dense);
389 const remap = alloc.alloc(?u8, n) catch return null;
390 defer alloc.free(remap);
391 var di: usize = 0;
392 for (tiles[0..live], present[0..live], 0..) |*t, p, ti| {
393 if (p) {
394 dense[di] = t.r;
395 remap[di] = @intCast(ti);
396 di += 1;
397 }
398 }
399 var dense_focus: ?u8 = null;
400 if (restoreLayout(alloc, dense, shared, bytes, &dense_focus)) {
401 // The tree's leaf ids are dense indices into `dense`; remap them
402 // to the real tile indices relayout reads, and the saved focus
403 // with them.
404 shared.tree.remapLeaves(remap);
405 if (dense_focus) |d| {
406 if (d < remap.len) {
407 if (remap[d]) |real| focus_out.* = real;
408 }
409 }
410 return {};
411 }
412 return null;
413 }
414
415 /// The leaf a birth sits beside: `birthTile` inserts against a LEAF, and
416 /// the focus can be a hole.
417 pub fn anchorTile(present: []const bool, sel: usize) usize {
418 if (sel < present.len and present[sel]) return sel;
419 return firstPresent(present) orelse 0;
420 }
421
422 pub fn firstPresent(present: []const bool) ?usize {
423 for (present, 0..) |p, i| {
424 if (p) return i;
425 }
426 return null;
427 }
src/tui/wall_picker.zig
Old New
@@ -10,6 +10,7 @@ const client = @import("client");
10 const hosts = @import("hosts"); 10 const hosts = @import("hosts");
11 const interact = @import("interact"); 11 const interact = @import("interact");
12 const wall_host = @import("wall_host.zig"); 12 const wall_host = @import("wall_host.zig");
13 const wall_layout = @import("wall_layout.zig");
13 const wv = @import("wallview.zig"); 14 const wv = @import("wallview.zig");
14 const Host = wall_host.Host; 15 const Host = wall_host.Host;
15 const Shared = wv.Shared; 16 const Shared = wv.Shared;
@@ -219,7 +220,7 @@ pub fn pickBirth(
219 alloc.free(session); 220 alloc.free(session);
220 return null; 221 return null;
221 }; 222 };
222 const anchor = wv.anchorTile(present[0..live.*], shared.sel); 223 const anchor = wall_layout.anchorTile(present[0..live.*], shared.sel);
223 const has_anchor = wv.presentCount(present[0..live.*]) > 0; 224 const has_anchor = wv.presentCount(present[0..live.*]) > 0;
224 // `-A` is inherited only within one host. A chord inherits it because 225 // `-A` is inherited only within one host. A chord inherits it because
225 // the new session is on the machine the offer was already made to; the 226 // the new session is on the machine the offer was already made to; the
src/tui/wallview.zig
Old New
@@ -37,6 +37,7 @@ const interact = @import("interact");
37 const layout = @import("layout"); 37 const layout = @import("layout");
38 const TmpDir = @import("testtmp").TmpDir; 38 const TmpDir = @import("testtmp").TmpDir;
39 const wall_host = @import("wall_host.zig"); 39 const wall_host = @import("wall_host.zig");
40 const wall_layout = @import("wall_layout.zig");
40 const wall_picker = @import("wall_picker.zig"); 41 const wall_picker = @import("wall_picker.zig");
41 const wall_pump = @import("wall_pump.zig"); 42 const wall_pump = @import("wall_pump.zig");
42 const AddHost = wall_host.AddHost; 43 const AddHost = wall_host.AddHost;
@@ -64,94 +65,6 @@ fn headless(out_fd: std.posix.fd_t) bool {
64 return interact.ttySize(out_fd) == null; 65 return interact.ttySize(out_fd) == null;
65 } 66 }
66 67
67 /// The daemon's row floor plus the label-bar arithmetic: a one-tile wall
68 /// draws no bar so its floor is `min_session_rows`; two or more tiles each
69 /// lose a row to a bar, so each stripe must hold that floor PLUS the bar
70 /// or the daemon drops the resize and the tile freezes on a stale grid.
71 fn wallFloors(live: usize) layout.Floors {
72 return .{
73 .rows = proto.min_session_rows + @as(u16, @intFromBool(live > 1)),
74 .cols = proto.min_session_cols,
75 };
76 }
77
78 /// The root container's orientation for N tiles at a given terminal size:
79 /// `.beside` when the terminal is wide enough that columns are the natural
80 /// cut, `.stacked` otherwise. The 2x corrects for cell shape — a terminal
81 /// twice as wide as it is tall has roughly square panes side-by-side.
82 fn rootOrient(size: proto.Size) layout.Orient {
83 return if (size.cols >= 2 * size.rows) .beside else .stacked;
84 }
85
86 /// `interact.Dir` and `layout.Dir` are the same enum tags in different
87 /// modules; this is the one place they meet, so the switch stays explicit.
88 fn dirOf(d: interact.PrefixFilter.Dir) layout.Dir {
89 return switch (d) {
90 .left => .left,
91 .down => .down,
92 .up => .up,
93 .right => .right,
94 };
95 }
96
97 /// layout.resize always GAINS focus cells; shrink = grow a neighbor at
98 /// focus's expense. Fullscreen refuses — a hidden layout resizing
99 /// invisibly is surprise, not power.
100 fn doResize(
101 alloc: std.mem.Allocator,
102 tiles: []Tile,
103 present: []const bool,
104 shared: *Shared,
105 sel: usize,
106 d: interact.PrefixFilter.Dir,
107 ) bool {
108 if (shared.fullscreen) return false;
109 const ld = dirOf(d);
110 const grow = switch (ld) {
111 .right, .down => true,
112 .left, .up => false,
113 };
114 const flat = shared.base_flat orelse shared.last_flat orelse return false;
115 const focus_tile: u8 = @intCast(sel);
116 var moved = false;
117 if (grow) {
118 // Focus gains from the sibling toward `ld`; if none there (edge
119 // pane), try the opposite side — gaining from either sibling
120 // widens or tallens the focus.
121 if (shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), focus_tile, ld, 1)) {
122 moved = true;
123 } else {
124 const opp = switch (ld) {
125 .right => layout.Dir.left,
126 .down => layout.Dir.up,
127 .left => layout.Dir.right,
128 .up => layout.Dir.down,
129 };
130 moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), focus_tile, opp, 1);
131 }
132 } else {
133 // Shrink: a neighbor on the same axis gains a cell from focus.
134 // Try the side the key points at first, then the opposite — a
135 // pane pressed against one wall can still shrink toward the other.
136 const opp = switch (ld) {
137 .left => layout.Dir.right,
138 .up => layout.Dir.down,
139 .right => layout.Dir.left,
140 .down => layout.Dir.up,
141 };
142 if (layout.neighbor(flat, focus_tile, ld)) |nb| {
143 moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), nb, opp, 1);
144 }
145 if (!moved) {
146 if (layout.neighbor(flat, focus_tile, opp)) |nb| {
147 moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), nb, ld, 1);
148 }
149 }
150 }
151 if (moved) relayout(alloc, tiles, present, shared, sel);
152 return moved;
153 }
154
155 pub const State = enum { 68 pub const State = enum {
156 connecting, 69 connecting,
157 up, 70 up,
@@ -666,7 +579,7 @@ fn paintLabelLocked(t: *Tile) void {
666 // `gone` rather than `present` because a focus move has no `present` slice 579 // `gone` rather than `present` because a focus move has no `present` slice
667 // to consult; `vanishTile` stores the two together, and a vanished tile 580 // to consult; `vanishTile` stores the two together, and a vanished tile
668 // must not paint — its rect belongs to a neighbour now. 581 // must not paint — its rect belongs to a neighbour now.
669 fn paintDeadBarsLocked(tiles: []Tile) void { 582 pub fn paintDeadBarsLocked(tiles: []Tile) void {
670 for (tiles) |*t| { 583 for (tiles) |*t| {
671 if (!t.alive.load(.acquire) and !t.gone.load(.acquire)) paintLabelLocked(t); 584 if (!t.alive.load(.acquire) and !t.gone.load(.acquire)) paintLabelLocked(t);
672 } 585 }
@@ -840,10 +753,10 @@ fn focusAnswer(
840 recut: bool, 753 recut: bool,
841 to: usize, 754 to: usize,
842 ) void { 755 ) void {
843 if (recut) relayout(alloc, tiles, present, shared, shared.sel); 756 if (recut) wall_layout.relayout(alloc, tiles, present, shared, shared.sel);
844 setFocus(tiles, shared, to); 757 setFocus(tiles, shared, to);
845 // Fullscreen re-flattens with the new focus so the full rect follows. 758 // Fullscreen re-flattens with the new focus so the full rect follows.
846 if (shared.fullscreen) relayout(alloc, tiles, present, shared, shared.sel); 759 if (shared.fullscreen) wall_layout.relayout(alloc, tiles, present, shared, shared.sel);
847 } 760 }
848 761
849 /// TAKEN, not read: an empty wall has no pump to paint a notice as a 762 /// TAKEN, not read: an empty wall has no pump to paint a notice as a
@@ -859,7 +772,7 @@ fn emptyWallHint(shared: *Shared) []const u8 {
859 /// wall is empty and the way out. A blank terminal with no cursor reads as 772 /// wall is empty and the way out. A blank terminal with no cursor reads as
860 /// hung, and the last `x` is precisely when the user needs to be told that 773 /// hung, and the last `x` is precisely when the user needs to be told that
861 /// what they see is the answer and not a crash. 774 /// what they see is the answer and not a crash.
862 fn paintEmptyWallLocked(shared: *Shared) void { 775 pub fn paintEmptyWallLocked(shared: *Shared) void {
863 if (!shared.is_tty) return; 776 if (!shared.is_tty) return;
864 var text_buf: [256]u8 = undefined; 777 var text_buf: [256]u8 = undefined;
865 const shown = labelText( 778 const shown = labelText(
@@ -875,106 +788,6 @@ fn paintEmptyWallLocked(shared: *Shared) void {
875 proto.writeAllFd(shared.out_fd, fbs.getWritten()) catch {}; 788 proto.writeAllFd(shared.out_fd, fbs.getWritten()) catch {};
876 } 789 }
877 790
878 /// Paint the vertical rails between `.beside` siblings. One column of
879 /// `\x1b[7m \x1b[0m` per rail — a reverse-video bar in the label-bar's
880 /// style, so a rail reads as structure and not as session output. The wall
881 /// owns rails; tiles never touch them (paint.zig's span-bounded clears).
882 fn paintRailsLocked(shared: *Shared, flat: layout.Flat) void {
883 if (!shared.is_tty) return;
884 if (flat.rails.len == 0) return;
885 var buf: [128]u8 = undefined;
886 for (flat.rails) |rail| {
887 var row: u16 = rail.top;
888 while (row < rail.top + rail.rows) : (row += 1) {
889 const out = std.fmt.bufPrint(&buf, "\x1b[{d};{d}H\x1b[7m \x1b[0m", .{ row + 1, rail.col + 1 }) catch continue;
890 proto.writeAllFd(shared.out_fd, out) catch {};
891 }
892 }
893 }
894
895 /// One `paint_mu` hold: no window where a pump paints rows that just
896 /// changed owner.
897 pub fn relayout(
898 alloc: std.mem.Allocator,
899 tiles: []Tile,
900 present: []const bool,
901 shared: *Shared,
902 sel: usize,
903 ) void {
904 shared.paint_mu.lock();
905 defer shared.paint_mu.unlock();
906 shared.sel = sel;
907
908 var live: usize = 0;
909 for (present) |p| {
910 if (p) live += 1;
911 }
912 // A one-tile wall owns every row and draws no label bar; two or more
913 // tiles each lose their top row to one. Fullscreen is the same: one
914 // visible pane, no bar — the plain client's byte stream.
915 shared.label_rows = if (shared.fullscreen or live <= 1) 0 else 1;
916 if (shared.is_tty) proto.writeAllFd(shared.out_fd, "\x1b[?25l\x1b[H\x1b[2J") catch {};
917 // The screen the popup was on has just been cleared, so the next paint
918 // owes it however unchanged its rows are.
919 if (shared.picker_open.load(.acquire)) shared.picker_stamp = 0;
920 if (live == 0) {
921 paintEmptyWallLocked(shared);
922 return;
923 }
924 // The base flat (null) is always computed so `focus_dir` can read
925 // adjacency from the real layout while fullscreened.
926 if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), null)) |base| {
927 if (shared.base_flat) |*old| old.deinit(shared.flat_alloc);
928 shared.base_flat = base;
929 } else |_| {}
930 const fs_arg: ?u8 = if (shared.fullscreen) @intCast(sel) else null;
931 var cut = shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), fs_arg);
932 if (cut) |_| {} else |e| {
933 // A split, an insert, a resize key: those are OPERATIONS, the user
934 // asked, and refusing leaves the screen exactly as it was. A
935 // SIGWINCH is neither — the terminal has ALREADY shrunk, so
936 // refusing leaves every rect pointing past the bottom of a screen
937 // that was just cleared. Degrade instead, to the view the wall
938 // already has for a terminal that holds one pane: the focused tile
939 // whole, the rest at 0x0, which paints nothing and claims no size.
940 // The tree is untouched, so growing back re-cuts every pane.
941 if (e == error.TooSmall and fs_arg == null) {
942 if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), @intCast(sel))) |only| {
943 cut = only;
944 // One visible pane draws no bar. Left at 1, `viewRows`
945 // would owe the daemon a row the tile does not have.
946 shared.label_rows = 0;
947 } else |_| {}
948 }
949 }
950 if (cut) |flat| {
951 if (shared.last_flat) |*old| old.deinit(shared.flat_alloc);
952 shared.last_flat = flat;
953 for (tiles, present) |*t, p| {
954 if (!p) continue;
955 if (flat.rectOf(@intCast(t.idx))) |r| {
956 t.rect = r;
957 }
958 t.resize_pending = true;
959 }
960 paintRailsLocked(shared, flat);
961 } else |_| {}
962
963 // The generation bump is what puts the rects back: every surviving
964 // pump repaints from its hot replica at its NEW rows, and the doorbell
965 // makes that immediate rather than one poll timeout away.
966 _ = shared.repaint_gen.fetchAdd(1, .release);
967 for (tiles, present) |*t, p| {
968 if (p) ring(t);
969 }
970 // ...except the tiles with no pump left to hear it: their bars are the
971 // keyboard's, the same rule `setFocus` paints a focus move by. No
972 // `label_rows` guard here, unlike there: the screen was just cleared,
973 // so on a one-tile wall of a dead tile that bar is the only thing left
974 // to say the target refused.
975 paintDeadBarsLocked(tiles);
976 }
977
978 /// A tile leaves the wall: off the `present` roll, off the tree, and its 791 /// A tile leaves the wall: off the `present` roll, off the tree, and its
979 /// pump told to return. The ONE owner — the chord and the poll's diff both 792 /// pump told to return. The ONE owner — the chord and the poll's diff both
980 /// come here, which is why the focus hand-off can only be written once. 793 /// come here, which is why the focus hand-off can only be written once.
@@ -1015,149 +828,6 @@ pub fn showsSelf(
1015 return std.mem.eql(u8, es, sock) and std.mem.eql(u8, en, proto.resolveName(name)); 828 return std.mem.eql(u8, es, sock) and std.mem.eql(u8, en, proto.resolveName(name));
1016 } 829 }
1017 830
1018 /// Rebuild the wall's tree from a saved sidecar, healing per leaf: each
1019 /// saved leaf id takes the first wall tile whose label matches its
1020 /// spelling, unmatched saved leaves collapse out, and unmatched wall tiles
1021 /// insert beside the first matched one. The saved root orientation wins
1022 /// over the aspect heuristic — `setRootOrient` is never called here.
1023 ///
1024 /// Pure of file I/O: bytes come in, the caller decides whether to feed
1025 /// them. False on any malformation, zero matches, or allocation failure —
1026 /// the caller builds today's default tree and the sidecar is ignored.
1027 fn restoreLayout(
1028 alloc: std.mem.Allocator,
1029 resolved: []const Resolved,
1030 shared: *Shared,
1031 bytes: []const u8,
1032 focus_out: *?u8,
1033 ) bool {
1034 var parsed = layout.parse(alloc, bytes) orelse return false;
1035 // Every early-false path deinits parsed; the success path moves the
1036 // tree out and deinits only the spellings.
1037
1038 const n_saved = parsed.spellings.items.len;
1039 const map = alloc.alloc(?u8, n_saved) catch {
1040 parsed.deinit(alloc);
1041 return false;
1042 };
1043 defer alloc.free(map);
1044
1045 var taken = alloc.alloc(bool, resolved.len) catch {
1046 parsed.deinit(alloc);
1047 return false;
1048 };
1049 defer alloc.free(taken);
1050 @memset(taken, false);
1051
1052 var any_match = false;
1053 for (0..n_saved) |i| {
1054 const spelling = parsed.spellings.items[i];
1055 map[i] = null;
1056 for (resolved, 0..) |r, j| {
1057 if (!taken[j] and std.mem.eql(u8, r.label, spelling)) {
1058 taken[j] = true;
1059 map[i] = @intCast(j);
1060 any_match = true;
1061 break;
1062 }
1063 }
1064 }
1065
1066 // Zero matches: the sidecar describes a different wall entirely.
1067 // Deinit parsed and return false so the caller builds the default tree.
1068 if (!any_match) {
1069 parsed.deinit(alloc);
1070 return false;
1071 }
1072
1073 // Map the saved focus through the spelling match before remapLeaves
1074 // consumes map. A null here means the focused leaf did not survive
1075 // (no wall tile matched its spelling) — the caller falls back to 0.
1076 if (parsed.focus) |k| {
1077 if (k < map.len) focus_out.* = map[k];
1078 }
1079
1080 parsed.tree.remapLeaves(map);
1081
1082 // Unmatched wall indices, ascending, insert beside the first matched
1083 // tile — the same beside-focus placement the initial build uses.
1084 const first_tile: u8 = if (map.len > 0) blk: {
1085 var k: usize = 0;
1086 while (k < map.len) : (k += 1) {
1087 if (map[k] != null) break :blk map[k].?;
1088 }
1089 break :blk 0;
1090 } else 0;
1091
1092 var next_tile: u8 = first_tile;
1093 for (taken, 0..) |is_taken, j| {
1094 if (!is_taken) {
1095 const new_id: u8 = @intCast(j);
1096 parsed.tree.insert(next_tile, new_id) catch {
1097 parsed.deinit(alloc);
1098 return false;
1099 };
1100 next_tile = new_id;
1101 }
1102 }
1103
1104 // Move the parsed tree into shared, deinit the old one first.
1105 shared.tree.deinit();
1106 shared.tree = parsed.tree;
1107 // The spellings are no longer needed — the tree holds only ids now.
1108 for (parsed.spellings.items) |s| alloc.free(s);
1109 parsed.spellings.deinit(alloc);
1110 return true;
1111 }
1112
1113 /// `serialize` indexes spellings by leaf ID (tile index), so the array is
1114 /// tile-indexed: holes get "" and are never serialized (the tree dropped
1115 /// them). A failed write prints one stderr line and returns.
1116 fn saveLayoutTo(
1117 alloc: std.mem.Allocator,
1118 path: []const u8,
1119 tiles: []Tile,
1120 present: []const bool,
1121 shared: *Shared,
1122 ) void {
1123 const spellings = alloc.alloc([]const u8, tiles.len) catch return;
1124 defer alloc.free(spellings);
1125 for (tiles, present, 0..) |*t, p, i| {
1126 spellings[i] = if (p) t.r.label else "";
1127 }
1128 var buf = std.ArrayListUnmanaged(u8){};
1129 defer buf.deinit(alloc);
1130 // shared.sel is the focused tile index; serialize maps it to the
1131 // encounter index of its leaf in the depth-first walk.
1132 const focus: ?u8 = if (shared.sel < tiles.len and present[shared.sel]) @intCast(shared.sel) else null;
1133 shared.tree.serialize(spellings, focus, buf.writer(alloc)) catch |err| {
1134 std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)});
1135 return;
1136 };
1137 wall.saveLayout(path, buf.items) catch |err| {
1138 std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)});
1139 };
1140 }
1141
1142 /// Resolves the sidecar path from env and delegates to `saveLayoutTo`.
1143 fn saveSidecar(
1144 alloc: std.mem.Allocator,
1145 tiles: []Tile,
1146 present: []const bool,
1147 shared: *Shared,
1148 ) void {
1149 // A pipe has no stripes, so it has no layout worth remembering — and
1150 // a tree it saved would be a tree the next TERMINAL restores over the
1151 // aspect rule. See `restoreSidecar` for the cost of the other half.
1152 if (!shared.is_tty) return;
1153 const path = wall.layoutPath(alloc) catch |err| {
1154 std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)});
1155 return;
1156 };
1157 defer alloc.free(path);
1158 saveLayoutTo(alloc, path, tiles, present, shared);
1159 }
1160
1161 /// Two tiles on the same daemon, as SPELLED. Identity dedup would need an 831 /// Two tiles on the same daemon, as SPELLED. Identity dedup would need an
1162 /// endpoint handshake the spec deliberately refuses, so the same session 832 /// endpoint handshake the spec deliberately refuses, so the same session
1163 /// reached as `HOST#S` and as `quic://…#S` is two tiles — and the ring 833 /// reached as `HOST#S` and as `quic://…#S` is two tiles — and the ring
@@ -1331,7 +1001,7 @@ pub fn birthTile(
1331 alloc, 1001 alloc,
1332 shared.size.rows, 1002 shared.size.rows,
1333 shared.size.cols, 1003 shared.size.cols,
1334 wallFloors(new_live), 1004 wall_layout.wallFloors(new_live),
1335 null, 1005 null,
1336 ) catch { 1006 ) catch {
1337 shared.tree.remove(@intCast(at)); 1007 shared.tree.remove(@intCast(at));
@@ -1693,88 +1363,6 @@ fn awaitDetach(t: *Tile, shared: *Shared) void {
1693 } 1363 }
1694 } 1364 }
1695 1365
1696 /// The saved layout over the tiles the hosts turned out to have; null
1697 /// when nothing matched.
1698 fn restoreSidecar(
1699 alloc: std.mem.Allocator,
1700 tiles: []Tile,
1701 present: []bool,
1702 live: usize,
1703 shared: *Shared,
1704 focus_out: *?usize,
1705 ) ?void {
1706 // Not on a pipe, and the cost is why. A restored tree is applied by
1707 // `focusAnswer(recut = true)`, whose `relayout` flags `resize_pending`
1708 // on EVERY present tile whether or not that tile's rect moved; each
1709 // pump then sends `.resize`, and `Server.onResize` answers every one of
1710 // them with `resyncSnapshot` — which has no "nothing changed" guard.
1711 // Measured on a scripted `mux quic://...` whose state home already held
1712 // a sidecar: a second full snapshot on the wire for a wall of one tile
1713 // that was already the right shape. The CLAIM is innocent; it stopped
1714 // sending a resize (see the pump's focus-claim block).
1715 if (!shared.is_tty) return null;
1716 const path = wall.layoutPath(alloc) catch return null;
1717 defer alloc.free(path);
1718 const bytes = wall.loadLayout(alloc, path) orelse return null;
1719 defer alloc.free(bytes);
1720 return restoreLayoutFrom(alloc, tiles, present, live, shared, bytes, focus_out);
1721 }
1722
1723 /// Pure of file I/O like `restoreLayout`: the chained id translations —
1724 /// saved→dense, then dense→real — are what a test must reach.
1725 fn restoreLayoutFrom(
1726 alloc: std.mem.Allocator,
1727 tiles: []Tile,
1728 present: []bool,
1729 live: usize,
1730 shared: *Shared,
1731 bytes: []const u8,
1732 focus_out: *?usize,
1733 ) ?void {
1734 const n = presentCount(present[0..live]);
1735 if (n == 0) return null;
1736 const dense = alloc.alloc(Resolved, n) catch return null;
1737 defer alloc.free(dense);
1738 const remap = alloc.alloc(?u8, n) catch return null;
1739 defer alloc.free(remap);
1740 var di: usize = 0;
1741 for (tiles[0..live], present[0..live], 0..) |*t, p, ti| {
1742 if (p) {
1743 dense[di] = t.r;
1744 remap[di] = @intCast(ti);
1745 di += 1;
1746 }
1747 }
1748 var dense_focus: ?u8 = null;
1749 if (restoreLayout(alloc, dense, shared, bytes, &dense_focus)) {
1750 // The tree's leaf ids are dense indices into `dense`; remap them
1751 // to the real tile indices relayout reads, and the saved focus
1752 // with them.
1753 shared.tree.remapLeaves(remap);
1754 if (dense_focus) |d| {
1755 if (d < remap.len) {
1756 if (remap[d]) |real| focus_out.* = real;
1757 }
1758 }
1759 return {};
1760 }
1761 return null;
1762 }
1763
1764 /// The leaf a birth sits beside: `birthTile` inserts against a LEAF, and
1765 /// the focus can be a hole.
1766 pub fn anchorTile(present: []const bool, sel: usize) usize {
1767 if (sel < present.len and present[sel]) return sel;
1768 return firstPresent(present) orelse 0;
1769 }
1770
1771 pub fn firstPresent(present: []const bool) ?usize {
1772 for (present, 0..) |p, i| {
1773 if (p) return i;
1774 }
1775 return null;
1776 }
1777
1778 /// The session on this host that mux itself is running inside, if any. 1366 /// The session on this host that mux itself is running inside, if any.
1779 pub fn selfSession(target: client.Target, env_sock: ?[]const u8, env_session: ?[]const u8) ?[]const u8 { 1367 pub fn selfSession(target: client.Target, env_sock: ?[]const u8, env_session: ?[]const u8) ?[]const u8 {
1780 const es = env_session orelse return null; 1368 const es = env_session orelse return null;
@@ -1935,7 +1523,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
1935 alloc, 1523 alloc,
1936 size.rows, 1524 size.rows,
1937 size.cols, 1525 size.cols,
1938 wallFloors(@intFromBool(has_entry)), 1526 wall_layout.wallFloors(@intFromBool(has_entry)),
1939 null, 1527 null,
1940 ) catch { 1528 ) catch {
1941 std.debug.print("mux: terminal too small\n", .{}); 1529 std.debug.print("mux: terminal too small\n", .{});
@@ -2055,7 +1643,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2055 // A wall with no tile yet paints its one line rather than nothing: a 1643 // A wall with no tile yet paints its one line rather than nothing: a
2056 // blank terminal with no cursor reads as hung, and the hosts are up to 1644 // blank terminal with no cursor reads as hung, and the hosts are up to
2057 // a poll away from having anything to show. 1645 // a poll away from having anything to show.
2058 if (live == 0) relayout(alloc, tiles[0..live], present[0..live], &shared, 0); 1646 if (live == 0) wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, 0);
2059 1647
2060 // One poller per host, all of them at once and none of them on this 1648 // One poller per host, all of them at once and none of them on this
2061 // thread: the user asked for one session and must not be held on 1649 // thread: the user asked for one session and must not be held on
@@ -2166,7 +1754,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2166 shared.paint_mu.lock(); 1754 shared.paint_mu.lock();
2167 shared.size = measured2; 1755 shared.size = measured2;
2168 shared.paint_mu.unlock(); 1756 shared.paint_mu.unlock();
2169 relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 1757 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel);
2170 } 1758 }
2171 } 1759 }
2172 1760
@@ -2238,10 +1826,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2238 setNotice(&shared, "[focus on a dead tile - no live neighbour]"); 1826 setNotice(&shared, "[focus on a dead tile - no live neighbour]");
2239 } 1827 }
2240 vanishTile(tiles[0..live], present[0..live], &shared, ended, v.back); 1828 vanishTile(tiles[0..live], present[0..live], &shared, ended, v.back);
2241 relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 1829 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel);
2242 }, 1830 },
2243 .finish => |how| { 1831 .finish => |how| {
2244 saveSidecar(alloc, tiles[0..live], present[0..live], &shared); 1832 wall_layout.saveSidecar(alloc, tiles[0..live], present[0..live], &shared);
2245 exit_code = how.code; 1833 exit_code = how.code;
2246 exit_msg = how.msg; 1834 exit_msg = how.msg;
2247 break :keys; 1835 break :keys;
@@ -2263,7 +1851,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2263 if (all_reported or std.time.milliTimestamp() >= restore_due) { 1851 if (all_reported or std.time.milliTimestamp() >= restore_due) {
2264 restore_tried = true; 1852 restore_tried = true;
2265 var saved_focus: ?usize = null; 1853 var saved_focus: ?usize = null;
2266 if (live > 0 and restoreSidecar(alloc, tiles, present, live, &shared, &saved_focus) != null) { 1854 if (live > 0 and wall_layout.restoreSidecar(alloc, tiles, present, live, &shared, &saved_focus) != null) {
2267 restore_ok = true; 1855 restore_ok = true;
2268 // The entry tile is the one the user is already typing 1856 // The entry tile is the one the user is already typing
2269 // into; a saved focus record must not move them off it. 1857 // into; a saved focus record must not move them off it.
@@ -2275,9 +1863,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2275 if (restore_tried and !restore_ok and !oriented and presentCount(present[0..live]) > 1) { 1863 if (restore_tried and !restore_ok and !oriented and presentCount(present[0..live]) > 1) {
2276 oriented = true; 1864 oriented = true;
2277 shared.paint_mu.lock(); 1865 shared.paint_mu.lock();
2278 shared.tree.setRootOrient(rootOrient(shared.size)); 1866 shared.tree.setRootOrient(wall_layout.rootOrient(shared.size));
2279 shared.paint_mu.unlock(); 1867 shared.paint_mu.unlock();
2280 relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 1868 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel);
2281 } 1869 }
2282 // An empty wall IS the picker: there is nothing else on the screen 1870 // An empty wall IS the picker: there is nothing else on the screen
2283 // to act from, and the last `x` is exactly when the user needs the 1871 // to act from, and the last `x` is exactly when the user needs the
@@ -2305,7 +1893,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2305 picker_shown = false; 1893 picker_shown = false;
2306 shared.picker_open.store(false, .release); 1894 shared.picker_open.store(false, .release);
2307 shared.picker_stamp = 0; 1895 shared.picker_stamp = 0;
2308 relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 1896 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel);
2309 }, 1897 },
2310 .leave => {}, 1898 .leave => {},
2311 } 1899 }
@@ -2420,7 +2008,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2420 if (birth_at) |at| 2008 if (birth_at) |at|
2421 focusAnswer(alloc, tiles[0..live], present[0..live], &shared, true, at) 2009 focusAnswer(alloc, tiles[0..live], present[0..live], &shared, true, at)
2422 else 2010 else
2423 relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 2011 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel);
2424 // The focused pump may be holding a claim the popup refused; 2012 // The focused pump may be holding a claim the popup refused;
2425 // it re-arms and retries on its next pass, and this is what 2013 // it re-arms and retries on its next pass, and this is what
2426 // makes that pass happen now rather than within a poll. 2014 // makes that pass happen now rather than within a poll.
@@ -2448,7 +2036,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2448 }, 2036 },
2449 .new_session, .split_right, .split_below => { 2037 .new_session, .split_right, .split_below => {
2450 setNotice(&shared, "[no session to birth beside - Ctrl-\\ s picks a host]"); 2038 setNotice(&shared, "[no session to birth beside - Ctrl-\\ s picks a host]");
2451 relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 2039 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel);
2452 }, 2040 },
2453 else => {}, 2041 else => {},
2454 } 2042 }
@@ -2525,7 +2113,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2525 tiles[z].detach_req.store(true, .release); 2113 tiles[z].detach_req.store(true, .release);
2526 ring(&tiles[z]); 2114 ring(&tiles[z]);
2527 awaitDetach(&tiles[z], &shared); 2115 awaitDetach(&tiles[z], &shared);
2528 saveSidecar(alloc, tiles[0..live], present[0..live], &shared); 2116 wall_layout.saveSidecar(alloc, tiles[0..live], present[0..live], &shared);
2529 exit_code = 0; 2117 exit_code = 0;
2530 exit_msg = "mux: detached (session still running; run mux to reattach)"; 2118 exit_msg = "mux: detached (session still running; run mux to reattach)";
2531 break :keys; 2119 break :keys;
@@ -2534,7 +2122,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2534 // Nothing left to fold in: every tile the wall will ever 2122 // Nothing left to fold in: every tile the wall will ever
2535 // have is already here or on its way from a host's list. 2123 // have is already here or on its way from a host's list.
2536 // `Ctrl-\ w` is a re-cut of the stripes, and an unzoom. 2124 // `Ctrl-\ w` is a re-cut of the stripes, and an unzoom.
2537 relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 2125 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel);
2538 }, 2126 },
2539 .new_session, .split_right, .split_below => { 2127 .new_session, .split_right, .split_below => {
2540 // Ask the focused tile's daemon for a new session. The 2128 // Ask the focused tile's daemon for a new session. The
@@ -2565,7 +2153,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2565 .focus_dir => |d| { 2153 .focus_dir => |d| {
2566 const flat = shared.base_flat orelse shared.last_flat; 2154 const flat = shared.base_flat orelse shared.last_flat;
2567 if (flat) |f| { 2155 if (flat) |f| {
2568 if (layout.neighbor(f, @intCast(z), dirOf(d))) |nb| { 2156 if (layout.neighbor(f, @intCast(z), wall_layout.dirOf(d))) |nb| {
2569 if (nb < live and present[nb] and nb != z) { 2157 if (nb < live and present[nb] and nb != z) {
2570 if (shared.fullscreen) 2158 if (shared.fullscreen)
2571 focusAnswer(alloc, tiles[0..live], present[0..live], &shared, false, nb) 2159 focusAnswer(alloc, tiles[0..live], present[0..live], &shared, false, nb)
@@ -2577,10 +2165,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2577 }, 2165 },
2578 .fullscreen => { 2166 .fullscreen => {
2579 shared.fullscreen = !shared.fullscreen; 2167 shared.fullscreen = !shared.fullscreen;
2580 relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 2168 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel);
2581 }, 2169 },
2582 .resize => |d| { 2170 .resize => |d| {
2583 _ = doResize(alloc, tiles[0..live], present[0..live], &shared, z, d); 2171 _ = wall_layout.doResize(alloc, tiles[0..live], present[0..live], &shared, z, d);
2584 }, 2172 },
2585 .focus => |idx| { 2173 .focus => |idx| {
2586 if (idx > 0 and idx <= live and present[idx - 1] and idx - 1 != z) { 2174 if (idx > 0 and idx <= live and present[idx - 1] and idx - 1 != z) {
@@ -2614,7 +2202,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2614 break :keys; 2202 break :keys;
2615 } 2203 }
2616 setNotice(&shared, "[nothing attached there yet - tile closed]"); 2204 setNotice(&shared, "[nothing attached there yet - tile closed]");
2617 relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); 2205 wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel);
2618 }, 2206 },
2619 .none => {}, 2207 .none => {},
2620 } 2208 }
@@ -2663,7 +2251,7 @@ test "the tree's stacked cut splits rows with the remainder at the top" {
2663 try tree.addFirst(0); 2251 try tree.addFirst(0);
2664 try tree.insert(0, 1); 2252 try tree.insert(0, 1);
2665 try tree.insert(1, 2); 2253 try tree.insert(1, 2);
2666 var f = try tree.flatten(alloc, 25, 80, wallFloors(3), null); 2254 var f = try tree.flatten(alloc, 25, 80, wall_layout.wallFloors(3), null);
2667 defer f.deinit(alloc); 2255 defer f.deinit(alloc);
2668 try std.testing.expectEqual(layout.Rect{ .top = 0, .left = 0, .rows = 9, .cols = 80 }, f.rectOf(0).?); 2256 try std.testing.expectEqual(layout.Rect{ .top = 0, .left = 0, .rows = 9, .cols = 80 }, f.rectOf(0).?);
2669 try std.testing.expectEqual(layout.Rect{ .top = 9, .left = 0, .rows = 8, .cols = 80 }, f.rectOf(1).?); 2257 try std.testing.expectEqual(layout.Rect{ .top = 9, .left = 0, .rows = 8, .cols = 80 }, f.rectOf(1).?);
@@ -2677,7 +2265,7 @@ test "the tree refuses a wall that cannot show a content row" {
2677 // 12 tiles over 23 rows: 1 row each — a label with no content. 2265 // 12 tiles over 23 rows: 1 row each — a label with no content.
2678 try tree.addFirst(0); 2266 try tree.addFirst(0);
2679 for (1..12) |i| try tree.insert(@intCast(i - 1), @intCast(i)); 2267 for (1..12) |i| try tree.insert(@intCast(i - 1), @intCast(i));
2680 try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 23, 80, wallFloors(12), null)); 2268 try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 23, 80, wall_layout.wallFloors(12), null));
2681 } 2269 }
2682 2270
2683 test "the tree refuses a multi-tile cut too thin for the daemon's row floor" { 2271 test "the tree refuses a multi-tile cut too thin for the daemon's row floor" {
@@ -2689,11 +2277,11 @@ test "the tree refuses a multi-tile cut too thin for the daemon's row floor" {
2689 // Two tiles on five rows: each rect is two rows, and under a label 2277 // Two tiles on five rows: each rect is two rows, and under a label
2690 // bar that is one content row — below `min_session_rows`, so the 2278 // bar that is one content row — below `min_session_rows`, so the
2691 // daemon refuses the resize and the tile freezes on a stale grid. 2279 // daemon refuses the resize and the tile freezes on a stale grid.
2692 try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 5, 80, wallFloors(2), null)); 2280 try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 5, 80, wall_layout.wallFloors(2), null));
2693 try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 4, 80, wallFloors(2), null)); 2281 try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 4, 80, wall_layout.wallFloors(2), null));
2694 // Six rows is the first cut that fits: three a rect, two of them 2282 // Six rows is the first cut that fits: three a rect, two of them
2695 // content under the bar — exactly the daemon's row floor. 2283 // content under the bar — exactly the daemon's row floor.
2696 var f = try tree.flatten(alloc, 6, 80, wallFloors(2), null); 2284 var f = try tree.flatten(alloc, 6, 80, wall_layout.wallFloors(2), null);
2697 defer f.deinit(alloc); 2285 defer f.deinit(alloc);
2698 try std.testing.expectEqual(@as(u16, 3), f.rectOf(0).?.rows); 2286 try std.testing.expectEqual(@as(u16, 3), f.rectOf(0).?.rows);
2699 try std.testing.expectEqual(@as(u16, 3), f.rectOf(1).?.rows); 2287 try std.testing.expectEqual(@as(u16, 3), f.rectOf(1).?.rows);
@@ -2721,7 +2309,7 @@ test "the tree refuses a multi-tile cut too thin for the daemon's row floor" {
2721 var one = layout.Tree.init(alloc); 2309 var one = layout.Tree.init(alloc);
2722 defer one.deinit(); 2310 defer one.deinit();
2723 try one.addFirst(0); 2311 try one.addFirst(0);
2724 var of = try one.flatten(alloc, 2, 80, wallFloors(1), null); 2312 var of = try one.flatten(alloc, 2, 80, wall_layout.wallFloors(1), null);
2725 defer of.deinit(alloc); 2313 defer of.deinit(alloc);
2726 try std.testing.expectEqual(@as(u16, 2), of.rectOf(0).?.rows); 2314 try std.testing.expectEqual(@as(u16, 2), of.rectOf(0).?.rows);
2727 } 2315 }
@@ -4272,7 +3860,7 @@ test "birthTile: a beside wall admits more panes than rows/3" {
4272 try std.testing.expectEqual(@as(usize, n), shared.tree.count()); 3860 try std.testing.expectEqual(@as(usize, n), shared.tree.count());
4273 // Admitted AND habitable: every pane keeps the full height and clears 3861 // Admitted AND habitable: every pane keeps the full height and clears
4274 // the column floor, which is what makes the refusal wrong. 3862 // the column floor, which is what makes the refusal wrong.
4275 const flat = try shared.tree.flatten(alloc, 24, 200, wallFloors(n), null); 3863 const flat = try shared.tree.flatten(alloc, 24, 200, wall_layout.wallFloors(n), null);
4276 defer flat.deinit(alloc); 3864 defer flat.deinit(alloc);
4277 try std.testing.expectEqual(@as(usize, n), flat.placed.len); 3865 try std.testing.expectEqual(@as(usize, n), flat.placed.len);
4278 for (flat.placed) |p| { 3866 for (flat.placed) |p| {
@@ -4928,7 +4516,7 @@ test "relayout sets resize_pending on every live tile" {
4928 .wake_w = -1, 4516 .wake_w = -1,
4929 }; 4517 };
4930 } 4518 }
4931 relayout(alloc, tiles, present, &shared, 0); 4519 wall_layout.relayout(alloc, tiles, present, &shared, 0);
4932 // Two tiles: a label bar appears. 4520 // Two tiles: a label bar appears.
4933 try std.testing.expectEqual(@as(u8, 1), shared.label_rows); 4521 try std.testing.expectEqual(@as(u8, 1), shared.label_rows);
4934 // Every tile is doorbelled: its pump sends the new rect. 4522 // Every tile is doorbelled: its pump sends the new rect.
@@ -4997,7 +4585,7 @@ test "a relayout landing mid-pass is still sent" {
4997 shared.paint_mu.lock(); 4585 shared.paint_mu.lock();
4998 shared.size = .{ .cols = 80, .rows = heights[i % heights.len] }; 4586 shared.size = .{ .cols = 80, .rows = heights[i % heights.len] };
4999 shared.paint_mu.unlock(); 4587 shared.paint_mu.unlock();
5000 relayout(alloc, tiles, &present, &shared, 0); 4588 wall_layout.relayout(alloc, tiles, &present, &shared, 0);
5001 4589
5002 // Quiesce: the pump owes nothing, and the pass that took the last 4590 // Quiesce: the pump owes nothing, and the pass that took the last
5003 // doorbell has finished. Only then is "what the daemon holds" a 4591 // doorbell has finished. Only then is "what the daemon holds" a
@@ -5106,7 +4694,7 @@ test "a terminal too small for the cut falls back to the focused pane, and grows
5106 shared.paint_mu.lock(); 4694 shared.paint_mu.lock();
5107 shared.size = .{ .cols = 80, .rows = rows }; 4695 shared.size = .{ .cols = 80, .rows = rows };
5108 shared.paint_mu.unlock(); 4696 shared.paint_mu.unlock();
5109 relayout(alloc, tiles, present, &shared, 0); 4697 wall_layout.relayout(alloc, tiles, present, &shared, 0);
5110 // What the pumps do with the generation bump: repaint their bars. 4698 // What the pumps do with the generation bump: repaint their bars.
5111 for (tiles, present) |*t, p| if (p) paintLabel(t, .up); 4699 for (tiles, present) |*t, p| if (p) paintLabel(t, .up);
5112 screen.drain(); 4700 screen.drain();
@@ -5170,7 +4758,7 @@ test "fullscreen gives the focused tile the whole terminal and hides the rest" {
5170 }; 4758 };
5171 } 4759 }
5172 shared.fullscreen = true; 4760 shared.fullscreen = true;
5173 relayout(alloc, tiles, &present, &shared, 0); 4761 wall_layout.relayout(alloc, tiles, &present, &shared, 0);
5174 // No label bar: the fullscreened pane is a one-tile wall. 4762 // No label bar: the fullscreened pane is a one-tile wall.
5175 try std.testing.expectEqual(@as(u8, 0), shared.label_rows); 4763 try std.testing.expectEqual(@as(u8, 0), shared.label_rows);
5176 // Tile 0 gets the whole terminal; tile 1 gets nothing. 4764 // Tile 0 gets the whole terminal; tile 1 gets nothing.
@@ -5206,9 +4794,9 @@ test "toggle off fullscreen restores the real rects" {
5206 }; 4794 };
5207 } 4795 }
5208 shared.fullscreen = true; 4796 shared.fullscreen = true;
5209 relayout(alloc, tiles, &present, &shared, 0); 4797 wall_layout.relayout(alloc, tiles, &present, &shared, 0);
5210 shared.fullscreen = false; 4798 shared.fullscreen = false;
5211 relayout(alloc, tiles, &present, &shared, 0); 4799 wall_layout.relayout(alloc, tiles, &present, &shared, 0);
5212 // Two tiles again: label bar back, both have real rects (stacked: 4800 // Two tiles again: label bar back, both have real rects (stacked:
5213 // full width, half height each). 4801 // full width, half height each).
5214 try std.testing.expectEqual(@as(u8, 1), shared.label_rows); 4802 try std.testing.expectEqual(@as(u8, 1), shared.label_rows);
@@ -5242,7 +4830,7 @@ test "focus_dir while fullscreened follows the focus" {
5242 }; 4830 };
5243 } 4831 }
5244 shared.fullscreen = true; 4832 shared.fullscreen = true;
5245 relayout(alloc, tiles, &present, &shared, 0); 4833 wall_layout.relayout(alloc, tiles, &present, &shared, 0);
5246 // Move focus to tile 1 while fullscreened. 4834 // Move focus to tile 1 while fullscreened.
5247 focusAnswer(alloc, tiles, &present, &shared, false, 1); 4835 focusAnswer(alloc, tiles, &present, &shared, false, 1);
5248 // The full rect followed: tile 1 now owns the terminal. 4836 // The full rect followed: tile 1 now owns the terminal.
@@ -5279,13 +4867,13 @@ test "resize: l grows the focused pane, h shrinks it" {
5279 .wake_w = -1, 4867 .wake_w = -1,
5280 }; 4868 };
5281 } 4869 }
5282 relayout(alloc, tiles, &present, &shared, 0); 4870 wall_layout.relayout(alloc, tiles, &present, &shared, 0);
5283 const before = tiles[0].rect.cols; 4871 const before = tiles[0].rect.cols;
5284 // l (grow width): focus 0 gains from its right sibling. 4872 // l (grow width): focus 0 gains from its right sibling.
5285 try std.testing.expect(doResize(alloc, tiles, &present, &shared, 0, .right)); 4873 try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .right));
5286 try std.testing.expect(tiles[0].rect.cols > before); 4874 try std.testing.expect(tiles[0].rect.cols > before);
5287 // h (shrink width): the right neighbor gains a cell back from focus. 4875 // h (shrink width): the right neighbor gains a cell back from focus.
5288 try std.testing.expect(doResize(alloc, tiles, &present, &shared, 0, .left)); 4876 try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .left));
5289 try std.testing.expectEqual(before, tiles[0].rect.cols); 4877 try std.testing.expectEqual(before, tiles[0].rect.cols);
5290 } 4878 }
5291 4879
@@ -5312,11 +4900,11 @@ test "resize: j grows height, k shrinks height" {
5312 .wake_w = -1, 4900 .wake_w = -1,
5313 }; 4901 };
5314 } 4902 }
5315 relayout(alloc, tiles, &present, &shared, 0); 4903 wall_layout.relayout(alloc, tiles, &present, &shared, 0);
5316 const before = tiles[0].rect.rows; 4904 const before = tiles[0].rect.rows;
5317 try std.testing.expect(doResize(alloc, tiles, &present, &shared, 0, .down)); 4905 try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .down));
5318 try std.testing.expect(tiles[0].rect.rows > before); 4906 try std.testing.expect(tiles[0].rect.rows > before);
5319 try std.testing.expect(doResize(alloc, tiles, &present, &shared, 0, .up)); 4907 try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .up));
5320 try std.testing.expectEqual(before, tiles[0].rect.rows); 4908 try std.testing.expectEqual(before, tiles[0].rect.rows);
5321 } 4909 }
5322 4910
@@ -5344,8 +4932,8 @@ test "resize refuses while fullscreened" {
5344 }; 4932 };
5345 } 4933 }
5346 shared.fullscreen = true; 4934 shared.fullscreen = true;
5347 relayout(alloc, tiles, &present, &shared, 0); 4935 wall_layout.relayout(alloc, tiles, &present, &shared, 0);
5348 try std.testing.expect(!doResize(alloc, tiles, &present, &shared, 0, .right)); 4936 try std.testing.expect(!wall_layout.doResize(alloc, tiles, &present, &shared, 0, .right));
5349 } 4937 }
5350 4938
5351 fn noticeBench(shared: *Shared, tiles: []Tile) void { 4939 fn noticeBench(shared: *Shared, tiles: []Tile) void {
@@ -5455,7 +5043,7 @@ test "a one-tile wall draws no label bar and paints row 1" {
5455 .wake_r = -1, 5043 .wake_r = -1,
5456 .wake_w = -1, 5044 .wake_w = -1,
5457 }; 5045 };
5458 relayout(alloc, tiles, present, &shared, 0); 5046 wall_layout.relayout(alloc, tiles, present, &shared, 0);
5459 // One tile: no label bar, every row is content. 5047 // One tile: no label bar, every row is content.
5460 try std.testing.expectEqual(@as(u8, 0), shared.label_rows); 5048 try std.testing.expectEqual(@as(u8, 0), shared.label_rows);
5461 try std.testing.expectEqual(@as(u16, 24), tiles[0].viewRows()); 5049 try std.testing.expectEqual(@as(u16, 24), tiles[0].viewRows());
@@ -5674,7 +5262,7 @@ test "a relayout drops every tile's highlight, because the stripes move under it
5674 }; 5262 };
5675 } 5263 }
5676 const before = shared.repaint_gen.load(.acquire); 5264 const before = shared.repaint_gen.load(.acquire);
5677 relayout(alloc, tiles, present, &shared, 0); 5265 wall_layout.relayout(alloc, tiles, present, &shared, 0);
5678 // One bump, every tile's pump clears its drag on it: the stripes moved, 5266 // One bump, every tile's pump clears its drag on it: the stripes moved,
5679 // so a held drag's anchor names a rect that is somewhere else now. 5267 // so a held drag's anchor names a rect that is somewhere else now.
5680 try std.testing.expect(shared.repaint_gen.load(.acquire) > before); 5268 try std.testing.expect(shared.repaint_gen.load(.acquire) > before);
@@ -6249,7 +5837,7 @@ test "saveLayoutTo writes the sidecar for the present tiles, spellings verbatim"
6249 }; 5837 };
6250 } 5838 }
6251 const present = [_]bool{ true, true }; 5839 const present = [_]bool{ true, true };
6252 saveLayoutTo(alloc, path, tiles, &present, &shared); 5840 wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared);
6253 const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; 5841 const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult;
6254 defer alloc.free(got); 5842 defer alloc.free(got);
6255 try std.testing.expect(std.mem.startsWith(u8, got, "mux-layout 1\nbeside 0\n")); 5843 try std.testing.expect(std.mem.startsWith(u8, got, "mux-layout 1\nbeside 0\n"));
@@ -6299,7 +5887,7 @@ test "saveLayoutTo handles a vanished middle tile without panicking" {
6299 }; 5887 };
6300 } 5888 }
6301 const present = [_]bool{ true, false, true }; 5889 const present = [_]bool{ true, false, true };
6302 saveLayoutTo(alloc, path, tiles, &present, &shared); 5890 wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared);
6303 const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; 5891 const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult;
6304 defer alloc.free(got); 5892 defer alloc.free(got);
6305 // Tile 2's label is verbatim; the hole (tile 1) is never serialized. 5893 // Tile 2's label is verbatim; the hole (tile 1) is never serialized.
@@ -6322,7 +5910,7 @@ test "restore: a saved beside pair comes back verbatim on a tall terminal" {
6322 }; 5910 };
6323 const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n leaf 39 --sock /tmp/x#b\n"; 5911 const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n leaf 39 --sock /tmp/x#b\n";
6324 var focus_out: ?u8 = null; 5912 var focus_out: ?u8 = null;
6325 try std.testing.expect(restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); 5913 try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out));
6326 const tiles = try alloc.alloc(Tile, 2); 5914 const tiles = try alloc.alloc(Tile, 2);
6327 defer alloc.free(tiles); 5915 defer alloc.free(tiles);
6328 const present = [_]bool{ true, true }; 5916 const present = [_]bool{ true, true };
@@ -6336,7 +5924,7 @@ test "restore: a saved beside pair comes back verbatim on a tall terminal" {
6336 .wake_w = -1, 5924 .wake_w = -1,
6337 }; 5925 };
6338 } 5926 }
6339 relayout(alloc, tiles, &present, &shared, 0); 5927 wall_layout.relayout(alloc, tiles, &present, &shared, 0);
6340 try std.testing.expectEqual(@as(u16, 0), tiles[1].rect.top); 5928 try std.testing.expectEqual(@as(u16, 0), tiles[1].rect.top);
6341 try std.testing.expect(tiles[1].rect.left > 0); 5929 try std.testing.expect(tiles[1].rect.left > 0);
6342 } 5930 }
@@ -6361,9 +5949,9 @@ test "restore: heals — unknown wall line inserted, lost leaf dropped" {
6361 const bytes = "mux-layout 1\nbeside 0\n leaf 26 --sock /tmp/x#a\n" ++ 5949 const bytes = "mux-layout 1\nbeside 0\n leaf 26 --sock /tmp/x#a\n" ++
6362 " leaf 26 --sock /tmp/x#gone\n leaf 26 --sock /tmp/x#c\n"; 5950 " leaf 26 --sock /tmp/x#gone\n leaf 26 --sock /tmp/x#c\n";
6363 var focus_out: ?u8 = null; 5951 var focus_out: ?u8 = null;
6364 try std.testing.expect(restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); 5952 try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out));
6365 try std.testing.expectEqual(@as(usize, 3), shared.tree.count()); 5953 try std.testing.expectEqual(@as(usize, 3), shared.tree.count());
6366 const flat = try shared.tree.flatten(alloc, 24, 80, wallFloors(3), null); 5954 const flat = try shared.tree.flatten(alloc, 24, 80, wall_layout.wallFloors(3), null);
6367 defer flat.deinit(alloc); 5955 defer flat.deinit(alloc);
6368 const a = flat.rectOf(0) orelse return error.TestUnexpectedResult; 5956 const a = flat.rectOf(0) orelse return error.TestUnexpectedResult;
6369 const c = flat.rectOf(1) orelse return error.TestUnexpectedResult; 5957 const c = flat.rectOf(1) orelse return error.TestUnexpectedResult;
@@ -6389,9 +5977,9 @@ test "restore: zero matches or garbage degrade to false" {
6389 .{ .target = .{ .sock = "/tmp/x" }, .label = "--sock /tmp/x#a", .session = "a" }, 5977 .{ .target = .{ .sock = "/tmp/x" }, .label = "--sock /tmp/x#a", .session = "a" },
6390 }; 5978 };
6391 var focus_out: ?u8 = null; 5979 var focus_out: ?u8 = null;
6392 try std.testing.expect(!restoreLayout(alloc, &resolved, &shared, "not a sidecar", &focus_out)); 5980 try std.testing.expect(!wall_layout.restoreLayout(alloc, &resolved, &shared, "not a sidecar", &focus_out));
6393 const nomatch = "mux-layout 1\nleaf 0 --sock /nowhere#z\n"; 5981 const nomatch = "mux-layout 1\nleaf 0 --sock /nowhere#z\n";
6394 try std.testing.expect(!restoreLayout(alloc, &resolved, &shared, nomatch, &focus_out)); 5982 try std.testing.expect(!wall_layout.restoreLayout(alloc, &resolved, &shared, nomatch, &focus_out));
6395 } 5983 }
6396 5984
6397 test "restore: duplicate spellings pair positionally" { 5985 test "restore: duplicate spellings pair positionally" {
@@ -6412,12 +6000,12 @@ test "restore: duplicate spellings pair positionally" {
6412 // near-equal rects and hides in the leaf count. 6000 // near-equal rects and hides in the leaf count.
6413 const bytes = "mux-layout 1\nbeside 0\n leaf 59 --sock /tmp/x#dup\n leaf 20 --sock /tmp/x#dup\n"; 6001 const bytes = "mux-layout 1\nbeside 0\n leaf 59 --sock /tmp/x#dup\n leaf 20 --sock /tmp/x#dup\n";
6414 var focus_out: ?u8 = null; 6002 var focus_out: ?u8 = null;
6415 try std.testing.expect(restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); 6003 try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out));
6416 try std.testing.expectEqual(@as(usize, 2), shared.tree.count()); 6004 try std.testing.expectEqual(@as(usize, 2), shared.tree.count());
6417 // Pairing is the whole claim and it IS observable: saved leaf 0 owns 6005 // Pairing is the whole claim and it IS observable: saved leaf 0 owns
6418 // the left, wide slot, so it must be wall tile 0. Paired the other 6006 // the left, wide slot, so it must be wall tile 0. Paired the other
6419 // way round the count is still 2 and the tiles have swapped screens. 6007 // way round the count is still 2 and the tiles have swapped screens.
6420 const flat = try shared.tree.flatten(alloc, 24, 80, wallFloors(2), null); 6008 const flat = try shared.tree.flatten(alloc, 24, 80, wall_layout.wallFloors(2), null);
6421 defer flat.deinit(alloc); 6009 defer flat.deinit(alloc);
6422 const t0 = flat.rectOf(0) orelse return error.TestUnexpectedResult; 6010 const t0 = flat.rectOf(0) orelse return error.TestUnexpectedResult;
6423 const t1 = flat.rectOf(1) orelse return error.TestUnexpectedResult; 6011 const t1 = flat.rectOf(1) orelse return error.TestUnexpectedResult;
@@ -6460,9 +6048,9 @@ test "restoreLayoutFrom: dense sidecar indices land on the real tile indices" {
6460 const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n" ++ 6048 const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n" ++
6461 " leaf 20 --sock /tmp/x#c\n leaf 18 --sock /tmp/x#d\n"; 6049 " leaf 20 --sock /tmp/x#c\n leaf 18 --sock /tmp/x#d\n";
6462 var restored_focus: ?usize = null; 6050 var restored_focus: ?usize = null;
6463 restoreLayoutFrom(alloc, &tiles, &present, 4, &shared, bytes, &restored_focus) orelse 6051 wall_layout.restoreLayoutFrom(alloc, &tiles, &present, 4, &shared, bytes, &restored_focus) orelse
6464 return error.TestUnexpectedResult; 6052 return error.TestUnexpectedResult;
6465 const flat = try shared.tree.flatten(alloc, 24, 80, wallFloors(3), null); 6053 const flat = try shared.tree.flatten(alloc, 24, 80, wall_layout.wallFloors(3), null);
6466 defer flat.deinit(alloc); 6054 defer flat.deinit(alloc);
6467 try std.testing.expectEqual(@as(?layout.Rect, null), flat.rectOf(1)); 6055 try std.testing.expectEqual(@as(?layout.Rect, null), flat.rectOf(1));
6468 const a = flat.rectOf(0) orelse return error.TestUnexpectedResult; 6056 const a = flat.rectOf(0) orelse return error.TestUnexpectedResult;
@@ -6494,7 +6082,7 @@ test "restore: focus_out maps the saved focus through the spelling match" {
6494 }; 6082 };
6495 const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n leaf 39 --sock /tmp/x#b\nfocus 1\n"; 6083 const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n leaf 39 --sock /tmp/x#b\nfocus 1\n";
6496 var focus_out: ?u8 = null; 6084 var focus_out: ?u8 = null;
6497 try std.testing.expect(restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); 6085 try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out));
6498 try std.testing.expectEqual(@as(?u8, 1), focus_out); 6086 try std.testing.expectEqual(@as(?u8, 1), focus_out);
6499 } 6087 }
6500 6088
@@ -6512,7 +6100,7 @@ test "restore: focus_out maps the saved focus through the spelling match" {
6512 }; 6100 };
6513 const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n leaf 39 --sock /tmp/x#gone\nfocus 1\n"; 6101 const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n leaf 39 --sock /tmp/x#gone\nfocus 1\n";
6514 var focus_out: ?u8 = null; 6102 var focus_out: ?u8 = null;
6515 try std.testing.expect(restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); 6103 try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out));
6516 try std.testing.expectEqual(@as(?u8, null), focus_out); 6104 try std.testing.expectEqual(@as(?u8, null), focus_out);
6517 } 6105 }
6518 } 6106 }