a73x

6a936579

feat: columns on — aspect hydration, rails, focus_dir, split chords

a73x   2026-08-24 18:06

Commit message
feat: columns on — aspect hydration, rails, focus_dir, split chords

The root container's orientation is `.beside` when `cols >= 2*rows`,
`.stacked` otherwise (2x corrects for cell shape). Hydration calls
`setRootOrient` after building the tree from N tiles; `run()` does the
same for its initial tree.

Rails: relayout paints a 1-column reverse-video separator between
`.beside` siblings, gated on `is_tty`. The wall owns rails; tiles never
touch them.

`focus_dir` reads `shared.last_flat` (stored by relayout) and calls
`layout.neighbor` to find the adjacent tile, then moves focus via the
existing `setFocus` path. Null neighbor stays silently.

Split chords (`.split_right`, `.split_below`) birth a new session tile
like `.new_session` but with `splitRight`/`splitBelow` at the focused
leaf. A `pending_place` field on Tile tells `addSessionTile` which tree
op to use.

e2e: multi-tile wall legs that tripped the aspect rule (cols >= 2*rows)
are pinned TALL with a comment naming the rule. Legs that grep for full
bar labels use 70x36 (stacked: 70 < 72, and 70 cols fits the label).

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

docscheck.budget
Old New
@@ -8,7 +8,7 @@ engine.zig 0
8 handoff.zig 0 8 handoff.zig 0
9 interact.zig 0 9 interact.zig 0
10 keymap.zig 0 10 keymap.zig 0
11 layout.zig 528 11 layout.zig 761
12 main.zig 0 12 main.zig 0
13 muxa.zig 0 13 muxa.zig 0
14 mux_main.zig 0 14 mux_main.zig 0
@@ -27,7 +27,7 @@ shellint.zig 0
27 sockpath.zig 0 27 sockpath.zig 0
28 spawn.zig 0 28 spawn.zig 0
29 testtmp.zig 0 29 testtmp.zig 0
30 wallview.zig 277 30 wallview.zig 694
31 wall.zig 0 31 wall.zig 0
32 wasm_core.zig 0 32 wasm_core.zig 0
33 webhub_main.zig 0 33 webhub_main.zig 0
src/layout.zig
Old New
@@ -249,6 +249,17 @@ pub const Tree = struct {
249 try self.split(focus, tile, .stacked); 249 try self.split(focus, tile, .stacked);
250 } 250 }
251 251
252 /// Set the root container's orientation. `insert` wraps a bare root in
253 /// `.stacked` by default (matching the stripe era); hydration picks
254 /// `.beside` when the terminal is wide enough, and calls this to flip
255 /// the root the first `insert` created.
256 pub fn setRootOrient(self: *Tree, o: Orient) void {
257 if (self.root) |r| switch (r.*) {
258 .container => r.container.orient = o,
259 .leaf => {},
260 };
261 }
262
252 /// Delete the leaf; a container left with one child dissolves — the child 263 /// Delete the leaf; a container left with one child dissolves — the child
253 /// takes its place in the grandparent (or becomes root). 264 /// takes its place in the grandparent (or becomes root).
254 pub fn remove(self: *Tree, tile: u8) void { 265 pub fn remove(self: *Tree, tile: u8) void {
@@ -816,3 +827,23 @@ test "resize refuses before a nested container's slot goes sub-minimum" {
816 const r1 = f.rectOf(1).?; 827 const r1 = f.rectOf(1).?;
817 try std.testing.expect(r1.cols >= 2); 828 try std.testing.expect(r1.cols >= 2);
818 } 829 }
830
831 test "setRootOrient flips the root container's axis" {
832 // insert wraps a bare root in .stacked; hydration calls setRootOrient
833 // to pick .beside when the terminal is wide. Three tiles left→right
834 // is the proof the root's axis moved.
835 var t = Tree.init(std.testing.allocator);
836 defer t.deinit();
837 try t.addFirst(0);
838 try t.insert(0, 1);
839 try t.insert(1, 2);
840 t.setRootOrient(.beside);
841 var f = try t.flatten(std.testing.allocator, 24, 82, .{ .rows = 2, .cols = 2 }, null);
842 defer f.deinit(std.testing.allocator);
843 // Three beside panes over 82 cols: 2 rails → 80 cols, 80/3 = 26r2,
844 // so 27/27/26 left→right. The left-to-right ordering is what a
845 // .beside root means, and what a .stacked root would not give.
846 try std.testing.expect(f.rectOf(0).?.left < f.rectOf(1).?.left);
847 try std.testing.expect(f.rectOf(1).?.left < f.rectOf(2).?.left);
848 try std.testing.expectEqual(@as(usize, 2), f.rails.len);
849 }
src/wallview.zig
Old New
@@ -106,6 +106,25 @@ fn wallFloors(live: usize) layout.Floors {
106 }; 106 };
107 } 107 }
108 108
109 /// The root container's orientation for N tiles at a given terminal size:
110 /// `.beside` when the terminal is wide enough that columns are the natural
111 /// cut, `.stacked` otherwise. The 2x corrects for cell shape — a terminal
112 /// twice as wide as it is tall has roughly square panes side-by-side.
113 fn rootOrient(size: proto.Size) layout.Orient {
114 return if (size.cols >= 2 * size.rows) .beside else .stacked;
115 }
116
117 /// `interact.Dir` and `layout.Dir` are the same enum tags in different
118 /// modules; this is the one place they meet, so the switch stays explicit.
119 fn dirOf(d: interact.PrefixFilter.Dir) layout.Dir {
120 return switch (d) {
121 .left => .left,
122 .down => .down,
123 .up => .up,
124 .right => .right,
125 };
126 }
127
109 const State = enum { 128 const State = enum {
110 connecting, 129 connecting,
111 up, 130 up,
@@ -223,6 +242,12 @@ const Shared = struct {
223 /// mutates it under `paint_mu` — the same single-writer rule as `sel` 242 /// mutates it under `paint_mu` — the same single-writer rule as `sel`
224 /// and `label_rows` — and relayout flattens it to read rects. 243 /// and `label_rows` — and relayout flattens it to read rects.
225 tree: layout.Tree = layout.Tree.init(std.heap.page_allocator), 244 tree: layout.Tree = layout.Tree.init(std.heap.page_allocator),
245 /// The last flatten result, kept so `focus_dir` can read adjacency
246 /// without re-flattening under the keyboard's `paint_mu` hold.
247 /// Relayout replaces it (freeing the old one); the keyboard thread
248 /// is the sole reader, under the same ownership as `tree`.
249 last_flat: ?layout.Flat = null,
250 flat_alloc: std.mem.Allocator = std.heap.page_allocator,
226 /// Bumped when the terminal's contents are no longer anybody's paint — 251 /// Bumped when the terminal's contents are no longer anybody's paint —
227 /// a relayout re-cut the stripes, or the focus moved and the newly 252 /// a relayout re-cut the stripes, or the focus moved and the newly
228 /// focused tile owes a full repaint. Pumps compare it against what 253 /// focused tile owes a full repaint. Pumps compare it against what
@@ -312,6 +337,9 @@ const Tile = struct {
312 /// tile must attach at 0x0 — the entry tile and a chord-born tile may 337 /// tile must attach at 0x0 — the entry tile and a chord-born tile may
313 /// carry the rect. 338 /// carry the rect.
314 creates: bool = false, 339 creates: bool = false,
340 /// How a chord-born tile enters the tree when its session name arrives.
341 /// Set by the keyboard with `.ask`, read by the answer handler.
342 pending_place: Place = .beside_focus,
315 /// One wall-file complaint per tile, whatever went wrong and however 343 /// One wall-file complaint per tile, whatever went wrong and however
316 /// often. `client.warnWall`'s latch, one per tile because a tile is the 344 /// often. `client.warnWall`'s latch, one per tile because a tile is the
317 /// unit that has an attach to record. 345 /// unit that has an attach to record.
@@ -1543,6 +1571,23 @@ fn paintEmptyWallLocked(shared: *Shared) void {
1543 proto.writeAllFd(shared.out_fd, fbs.getWritten()) catch {}; 1571 proto.writeAllFd(shared.out_fd, fbs.getWritten()) catch {};
1544 } 1572 }
1545 1573
1574 /// Paint the vertical rails between `.beside` siblings. One column of
1575 /// `\x1b[7m \x1b[0m` per rail — a reverse-video bar in the label-bar's
1576 /// style, so a rail reads as structure and not as session output. The wall
1577 /// owns rails; tiles never touch them (paint.zig's span-bounded clears).
1578 fn paintRailsLocked(shared: *Shared, flat: layout.Flat) void {
1579 if (!shared.is_tty) return;
1580 if (flat.rails.len == 0) return;
1581 var buf: [128]u8 = undefined;
1582 for (flat.rails) |rail| {
1583 var row: u16 = rail.top;
1584 while (row < rail.top + rail.rows) : (row += 1) {
1585 const out = std.fmt.bufPrint(&buf, "\x1b[{d};{d}H\x1b[7m \x1b[0m", .{ row + 1, rail.col + 1 }) catch continue;
1586 proto.writeAllFd(shared.out_fd, out) catch {};
1587 }
1588 }
1589 }
1590
1546 /// One `paint_mu` hold: no window where a pump paints rows that just 1591 /// One `paint_mu` hold: no window where a pump paints rows that just
1547 /// changed owner. 1592 /// changed owner.
1548 fn relayout( 1593 fn relayout(
@@ -1571,7 +1616,9 @@ fn relayout(
1571 return; 1616 return;
1572 } 1617 }
1573 if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), null)) |flat| { 1618 if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), null)) |flat| {
1574 defer flat.deinit(alloc); 1619 // Replace the stored flat: free the old one first, then keep this.
1620 if (shared.last_flat) |*old| old.deinit(shared.flat_alloc);
1621 shared.last_flat = flat;
1575 for (tiles, present) |*t, p| { 1622 for (tiles, present) |*t, p| {
1576 if (!p) continue; 1623 if (!p) continue;
1577 if (flat.rectOf(@intCast(t.idx))) |r| { 1624 if (flat.rectOf(@intCast(t.idx))) |r| {
@@ -1585,6 +1632,7 @@ fn relayout(
1585 // now claims. 1632 // now claims.
1586 t.resize_pending.store(true, .release); 1633 t.resize_pending.store(true, .release);
1587 } 1634 }
1635 paintRailsLocked(shared, flat);
1588 } else |_| {} 1636 } else |_| {}
1589 1637
1590 // The generation bump is what puts the rects back: every surviving 1638 // The generation bump is what puts the rects back: every surviving
@@ -1744,6 +1792,10 @@ const FocusTo = union(enum) {
1744 stay, 1792 stay,
1745 }; 1793 };
1746 1794
1795 /// How a chord-born tile enters the tree: `beside_focus` is the `c` chord's
1796 /// sibling-after-focus (insert), `right_of` / `below` are the split chords.
1797 const Place = enum { beside_focus, right_of, below };
1798
1747 /// A sibling is focused if it has a tile and GETS one if not — otherwise a 1799 /// A sibling is focused if it has a tile and GETS one if not — otherwise a
1748 /// tile labelled S would paint T. 1800 /// tile labelled S would paint T.
1749 fn addSessionTile( 1801 fn addSessionTile(
@@ -1754,6 +1806,7 @@ fn addSessionTile(
1754 shared: *Shared, 1806 shared: *Shared,
1755 from: usize, 1807 from: usize,
1756 name: []const u8, 1808 name: []const u8,
1809 place: Place,
1757 ) FocusTo { 1810 ) FocusTo {
1758 const target = tiles[from].r.target; 1811 const target = tiles[from].r.target;
1759 const want = proto.resolveName(name); 1812 const want = proto.resolveName(name);
@@ -1779,7 +1832,11 @@ fn addSessionTile(
1779 // every rect and sets it again, so the two agree. 1832 // every rect and sets it again, so the two agree.
1780 shared.label_rows = if (new_live > 1) 1 else 0; 1833 shared.label_rows = if (new_live > 1) 1 else 0;
1781 const at = live.*; 1834 const at = live.*;
1782 shared.tree.insert(@intCast(from), @intCast(at)) catch return .full; 1835 switch (place) {
1836 .beside_focus => shared.tree.insert(@intCast(from), @intCast(at)) catch return .full,
1837 .right_of => shared.tree.splitRight(@intCast(from), @intCast(at)) catch return .full,
1838 .below => shared.tree.splitBelow(@intCast(from), @intCast(at)) catch return .full,
1839 }
1783 const flat = shared.tree.flatten( 1840 const flat = shared.tree.flatten(
1784 alloc, 1841 alloc,
1785 shared.size.rows, 1842 shared.size.rows,
@@ -2040,6 +2097,7 @@ fn hydrate(
2040 spawnPump(&tiles[at]); 2097 spawnPump(&tiles[at]);
2041 added += 1; 2098 added += 1;
2042 } 2099 }
2100 if (added > 0) shared.tree.setRootOrient(rootOrient(shared.size));
2043 return added; 2101 return added;
2044 } 2102 }
2045 2103
@@ -2164,6 +2222,8 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) !
2164 // has one, and the geometry is re-cut when the first Ctrl-\ w hydrates 2222 // has one, and the geometry is re-cut when the first Ctrl-\ w hydrates
2165 // the rest — `relayout` already does exactly that for `x`. 2223 // the rest — `relayout` already does exactly that for `x`.
2166 var shared = Shared{ .out_fd = stdout_fd, .size = size, .is_tty = is_tty }; 2224 var shared = Shared{ .out_fd = stdout_fd, .size = size, .is_tty = is_tty };
2225 shared.tree = layout.Tree.init(alloc);
2226 shared.flat_alloc = alloc;
2167 { 2227 {
2168 var i: usize = 0; 2228 var i: usize = 0;
2169 while (i < resolved.len) : (i += 1) { 2229 while (i < resolved.len) : (i += 1) {
@@ -2173,6 +2233,8 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) !
2173 shared.tree.insert(@intCast(i - 1), @intCast(i)) catch return 2; 2233 shared.tree.insert(@intCast(i - 1), @intCast(i)) catch return 2;
2174 } 2234 }
2175 } 2235 }
2236 if (resolved.len > 1)
2237 shared.tree.setRootOrient(rootOrient(size));
2176 } 2238 }
2177 const init_flat = shared.tree.flatten( 2239 const init_flat = shared.tree.flatten(
2178 alloc, 2240 alloc,
@@ -2184,7 +2246,7 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) !
2184 std.debug.print("mux: terminal too small for {d} tiles\n", .{resolved.len}); 2246 std.debug.print("mux: terminal too small for {d} tiles\n", .{resolved.len});
2185 return 2; 2247 return 2;
2186 }; 2248 };
2187 defer init_flat.deinit(alloc); 2249 shared.last_flat = init_flat;
2188 2250
2189 // A daemon that dies mid-write must surface as a write error on that 2251 // A daemon that dies mid-write must surface as a write error on that
2190 // tile's thread, not a process-fatal SIGPIPE. 2252 // tile's thread, not a process-fatal SIGPIPE.
@@ -2366,6 +2428,7 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) !
2366 &shared, 2428 &shared,
2367 z, 2429 z,
2368 name.slice(), 2430 name.slice(),
2431 tiles[z].pending_place,
2369 )) { 2432 )) {
2370 .moved => |to| { 2433 .moved => |to| {
2371 last_focus = z; 2434 last_focus = z;
@@ -2509,6 +2572,7 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) !
2509 // answer arrives on the pump's transport and is posted 2572 // answer arrives on the pump's transport and is posted
2510 // back to the keyboard (`postAnswer`), which moves the 2573 // back to the keyboard (`postAnswer`), which moves the
2511 // focus or grows the wall. 2574 // focus or grows the wall.
2575 tiles[z].pending_place = .beside_focus;
2512 tiles[z].ask.store(@intFromEnum(client.SwitchIntent.new), .release); 2576 tiles[z].ask.store(@intFromEnum(client.SwitchIntent.new), .release);
2513 ring(&tiles[z]); 2577 ring(&tiles[z]);
2514 }, 2578 },
@@ -2520,9 +2584,26 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) !
2520 tiles[z].ask.store(@intFromEnum(client.SwitchIntent.prev), .release); 2584 tiles[z].ask.store(@intFromEnum(client.SwitchIntent.prev), .release);
2521 ring(&tiles[z]); 2585 ring(&tiles[z]);
2522 }, 2586 },
2523 .focus_dir, 2587 .focus_dir => |d| {
2524 .split_right, 2588 if (shared.last_flat) |flat| {
2525 .split_below, 2589 if (layout.neighbor(flat, @intCast(z), dirOf(d))) |nb| {
2590 if (nb < live and present[nb] and nb != z) {
2591 last_focus = z;
2592 setFocus(tiles[0..live], &shared, nb);
2593 }
2594 }
2595 }
2596 },
2597 .split_right => {
2598 tiles[z].pending_place = .right_of;
2599 tiles[z].ask.store(@intFromEnum(client.SwitchIntent.new), .release);
2600 ring(&tiles[z]);
2601 },
2602 .split_below => {
2603 tiles[z].pending_place = .below;
2604 tiles[z].ask.store(@intFromEnum(client.SwitchIntent.new), .release);
2605 ring(&tiles[z]);
2606 },
2526 .fullscreen, 2607 .fullscreen,
2527 .resize, 2608 .resize,
2528 => {}, 2609 => {},
@@ -2980,6 +3061,12 @@ test "viewRows: a one-tile wall keeps every row, a multi-tile wall loses the lab
2980 test "relayout sets resize_pending on every live tile" { 3061 test "relayout sets resize_pending on every live tile" {
2981 const alloc = std.testing.allocator; 3062 const alloc = std.testing.allocator;
2982 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; 3063 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
3064 shared.tree = layout.Tree.init(alloc);
3065 shared.flat_alloc = alloc;
3066 defer shared.tree.deinit();
3067 defer if (shared.last_flat) |*f| f.deinit(alloc);
3068 try shared.tree.addFirst(0);
3069 try shared.tree.insert(0, 1);
2983 const tiles = try alloc.alloc(Tile, 2); 3070 const tiles = try alloc.alloc(Tile, 2);
2984 defer alloc.free(tiles); 3071 defer alloc.free(tiles);
2985 const present = try alloc.alloc(bool, 2); 3072 const present = try alloc.alloc(bool, 2);
@@ -3045,6 +3132,11 @@ test "a pump's answer that grew the wall re-cuts it; a mere focus move does not"
3045 test "a one-tile wall draws no label bar and paints row 1" { 3132 test "a one-tile wall draws no label bar and paints row 1" {
3046 const alloc = std.testing.allocator; 3133 const alloc = std.testing.allocator;
3047 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; 3134 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
3135 shared.tree = layout.Tree.init(alloc);
3136 shared.flat_alloc = alloc;
3137 defer shared.tree.deinit();
3138 defer if (shared.last_flat) |*f| f.deinit(alloc);
3139 try shared.tree.addFirst(0);
3048 const tiles = try alloc.alloc(Tile, 1); 3140 const tiles = try alloc.alloc(Tile, 1);
3049 defer alloc.free(tiles); 3141 defer alloc.free(tiles);
3050 const present = try alloc.alloc(bool, 1); 3142 const present = try alloc.alloc(bool, 1);
@@ -3244,6 +3336,12 @@ test "a relayout drops every tile's highlight, because the stripes move under it
3244 // The generation is the whole mechanism, so it is what this pins. 3336 // The generation is the whole mechanism, so it is what this pins.
3245 const alloc = std.testing.allocator; 3337 const alloc = std.testing.allocator;
3246 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; 3338 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
3339 shared.tree = layout.Tree.init(alloc);
3340 shared.flat_alloc = alloc;
3341 defer shared.tree.deinit();
3342 defer if (shared.last_flat) |*f| f.deinit(alloc);
3343 try shared.tree.addFirst(0);
3344 try shared.tree.insert(0, 1);
3247 const tiles = try alloc.alloc(Tile, 2); 3345 const tiles = try alloc.alloc(Tile, 2);
3248 defer alloc.free(tiles); 3346 defer alloc.free(tiles);
3249 const present = try alloc.alloc(bool, 2); 3347 const present = try alloc.alloc(bool, 2);
test/e2e.sh
Old New
@@ -4970,7 +4970,8 @@ wait_grid "$SOCK26" "cwb-pin" "CLI wall: session b's marker" b
4970 --sock "$SOCK26" --session b > "$OUT.cwinj" 2>&1 ) & 4970 --sock "$SOCK26" --session b > "$OUT.cwinj" 2>&1 ) &
4971 CWINJPID=$! 4971 CWINJPID=$!
4972 set +e 4972 set +e
4973 timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.cwcap" --err "$OUT.cwcap.err" -- \ 4973 # tall: two tiles at 100x30 trip the aspect rule (100 >= 60)
4974 timeout 40 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.cwcap" --err "$OUT.cwcap.err" -- \
4974 "$MUX" wall --sock "$SOCK26#a" "--sock $SOCK26#b" > "$OUT.cwpc" 2>&1 <<'EOF' 4975 "$MUX" wall --sock "$SOCK26#a" "--sock $SOCK26#b" > "$OUT.cwpc" 2>&1 <<'EOF'
4975 expect cwlive-pin 20000 4976 expect cwlive-pin 20000
4976 send \x1cd 4977 send \x1cd
@@ -4992,7 +4993,7 @@ grep -q "cwb-pin" "$OUT.cwcap" || {
4992 grep -q "cwlive-pin" "$OUT.cwcap" || { 4993 grep -q "cwlive-pin" "$OUT.cwcap" || {
4993 echo "e2e FAIL: CLI wall: the live delta never painted"; exit 1; } 4994 echo "e2e FAIL: CLI wall: the live delta never painted"; exit 1; }
4994 # Every tile claims its rect: a and b were created 80-wide by pipe, and 4995 # Every tile claims its rect: a and b were created 80-wide by pipe, and
4995 # this 100-wide wall resized both on attach. There is no 0x0 passivity to 4996 # this 40-wide wall resized both on attach. There is no 0x0 passivity to
4996 # assert — the wall writes the sessions it shows. 4997 # assert — the wall writes the sessions it shows.
4997 assert_stopped "$SOCK26" "$D23PID" "CLI wall" "$OUT.cwstop" 4998 assert_stopped "$SOCK26" "$D23PID" "CLI wall" "$OUT.cwstop"
4998 D23PID="" 4999 D23PID=""
@@ -5091,7 +5092,8 @@ D25PID=$!
5091 wait_sock "$SOCK28" "$OUT.nsw.d" "new-session daemon never bound" 5092 wait_sock "$SOCK28" "$OUT.nsw.d" "new-session daemon never bound"
5092 5093
5093 set +e 5094 set +e
5094 timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.nsw" --err "$OUT.nsw.err" \ 5095 # tall: Ctrl-\ c adds a second tile at 80x24 (80 >= 48)
5096 timeout 40 "$PTYCLIENT" --cols 40 --rows 24 --out "$OUT.nsw" --err "$OUT.nsw.err" \
5095 -- "$MUX" --sock "$SOCK28" > "$OUT.nsw.log" 2>&1 <<'EOF' 5097 -- "$MUX" --sock "$SOCK28" > "$OUT.nsw.log" 2>&1 <<'EOF'
5096 expect \x1b[?1049h 15000 5098 expect \x1b[?1049h 15000
5097 settle 400 15000 5099 settle 400 15000
@@ -5216,7 +5218,8 @@ D26PID=$!
5216 wait_sock "$SOCK29" "$OUT.ring.d" "session-ring daemon never bound" 5218 wait_sock "$SOCK29" "$OUT.ring.d" "session-ring daemon never bound"
5217 5219
5218 set +e 5220 set +e
5219 timeout 60 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.ring" --err "$OUT.ring.err" \ 5221 # tall: Ctrl-\ c adds tiles at 80x24 (80 >= 48)
5222 timeout 60 "$PTYCLIENT" --cols 40 --rows 24 --out "$OUT.ring" --err "$OUT.ring.err" \
5220 -- "$MUX" --sock "$SOCK29" > "$OUT.ring.log" 2>&1 <<'EOF' 5223 -- "$MUX" --sock "$SOCK29" > "$OUT.ring.log" 2>&1 <<'EOF'
5221 expect \x1b[?1049h 15000 5224 expect \x1b[?1049h 15000
5222 settle 400 15000 5225 settle 400 15000
@@ -5654,7 +5657,8 @@ wait_grid "$SOCK37" "zsb-pin" "focus skip: session b's marker" b
5654 ZSATT_BEFORE=$(attaches_now "$SOCK37") 5657 ZSATT_BEFORE=$(attaches_now "$SOCK37")
5655 watch_clients "$SOCK37" "$OUT.zswatch" 5658 watch_clients "$SOCK37" "$OUT.zswatch"
5656 set +e 5659 set +e
5657 timeout 90 "$PTYCLIENT" --cols 92 --rows 30 --out "$OUT.zscap" --err "$OUT.zscap.err" -- \ 5660 # tall: the aspect rule would cut columns at 92x30 (92 >= 60)
5661 timeout 90 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.zscap" --err "$OUT.zscap.err" -- \
5658 "$MUX" wall "--sock $SOCK37#a" "--sock $SOCK37#b" > "$OUT.zspc" 2>&1 <<'EOF' 5662 "$MUX" wall "--sock $SOCK37#a" "--sock $SOCK37#b" > "$OUT.zspc" 2>&1 <<'EOF'
5659 expect zsb-pin 20000 5663 expect zsb-pin 20000
5660 settle 700 20000 5664 settle 700 20000
@@ -5713,13 +5717,12 @@ grep -q "zs-two" "$OUT.zsfa" && {
5713 assert_attach_delta "$ZSATT_BEFORE" "$ZSATT_AFTER" 2 "focus skip" 5717 assert_attach_delta "$ZSATT_BEFORE" "$ZSATT_AFTER" 2 "focus skip"
5714 assert_never_two_clients "$OUT.zswatch" a "focus skip" 5718 assert_never_two_clients "$OUT.zswatch" a "focus skip"
5715 assert_never_two_clients "$OUT.zswatch" b "focus skip" 5719 assert_never_two_clients "$OUT.zswatch" b "focus skip"
5716 # Both tiles claimed their rect: 92 cols each (this leg's own width) and 14 5720 # Both tiles claimed their rect: 40 cols each (this leg's own width) and 14
5717 # rows each (30 terminal rows cut into two stripes of 15, minus one label 5721 # rows each (30 terminal rows cut into two stripes of 15, minus one label
5718 # bar each — layoutStripes' arithmetic, held to the size the wall attached 5722 # bar each). A focus move resizes nothing.
5719 # at). A focus move resizes nothing.
5720 for _s in a b; do 5723 for _s in a b; do
5721 timeout 20 "$MUXA" status --sock "$SOCK37" --session "$_s" > "$OUT.zsst$_s" 2>&1 5724 timeout 20 "$MUXA" status --sock "$SOCK37" --session "$_s" > "$OUT.zsst$_s" 2>&1
5722 grep -q '"cols":92' "$OUT.zsst$_s" || { 5725 grep -q '"cols":40' "$OUT.zsst$_s" || {
5723 echo "e2e FAIL: focus skip: session $_s is not at the width its tile claimed:" 5726 echo "e2e FAIL: focus skip: session $_s is not at the width its tile claimed:"
5724 cat "$OUT.zsst$_s"; exit 1; } 5727 cat "$OUT.zsst$_s"; exit 1; }
5725 grep -q '"rows":14' "$OUT.zsst$_s" || { 5728 grep -q '"rows":14' "$OUT.zsst$_s" || {
@@ -5903,7 +5906,9 @@ pipe_detach
5903 wait_grid "$SOCK39" "zdlive-pin" "dead tile: the live session's marker" a 5906 wait_grid "$SOCK39" "zdlive-pin" "dead tile: the live session's marker" a
5904 5907
5905 set +e 5908 set +e
5906 timeout 90 "$PTYCLIENT" --cols 90 --rows 24 --out "$OUT.zdcap" --err "$OUT.zdcap.err" -- \ 5909 # tall: two tiles at 90x24 trip the aspect rule (90 >= 48); 70x36 stays
5910 # stacked (70 < 72) and fits the bar label the assertion greps for
5911 timeout 90 "$PTYCLIENT" --cols 70 --rows 36 --out "$OUT.zdcap" --err "$OUT.zdcap.err" -- \
5907 "$MUX" wall "--sock $SOCK39#a" "--sock $SOCK39#ghost" > "$OUT.zdpc" 2>&1 <<'EOF' 5912 "$MUX" wall "--sock $SOCK39#a" "--sock $SOCK39#ghost" > "$OUT.zdpc" 2>&1 <<'EOF'
5908 expect zdlive-pin 20000 5913 expect zdlive-pin 20000
5909 settle 800 20000 5914 settle 800 20000
@@ -6124,7 +6129,8 @@ _whx_n=$(wc -l < "$WHXSTATE/mux/wall")
6124 # single expect plus `settle` gives; the tile it forgets is the FOCUSED 6129 # single expect plus `settle` gives; the tile it forgets is the FOCUSED
6125 # one, tile 0, whose line the file is then asserted NOT to hold. 6130 # one, tile 0, whose line the file is then asserted NOT to hold.
6126 set +e 6131 set +e
6127 XDG_STATE_HOME="$WHXSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \ 6132 # tall: two tiles at 100x30 trip the aspect rule (100 >= 60)
6133 XDG_STATE_HOME="$WHXSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \
6128 --out "$OUT.whxcap" --err "$OUT.whxcap.err" -- \ 6134 --out "$OUT.whxcap" --err "$OUT.whxcap.err" -- \
6129 "$MUX" wall > "$OUT.whxpc" 2>&1 <<'EOF' 6135 "$MUX" wall > "$OUT.whxpc" 2>&1 <<'EOF'
6130 expect whxb-pin 20000 6136 expect whxb-pin 20000
@@ -6158,17 +6164,17 @@ _whx_bars=$(grep -o "whxb-pin" "$OUT.whxcap" | wc -l)
6158 cat "$OUT.whxpc"; exit 1; } 6164 cat "$OUT.whxpc"; exit 1; }
6159 # ...and the SESSION is untouched: it still answers, and it still holds 6165 # ...and the SESSION is untouched: it still answers, and it still holds
6160 # what it held. "Remove is detach" — the tile went, the session did not. 6166 # what it held. "Remove is detach" — the tile went, the session did not.
6161 # xa was 80-wide from its pipe attach; this 100-wide wall resized it on 6167 # xa was 80-wide from its pipe attach; this 40-wide wall resized it on
6162 # hydrate, and the detach left that size behind. 6168 # hydrate, and the detach left that size behind.
6163 XDG_STATE_HOME="$WHSTATE" "$MUXA" status --sock "$SOCK40" --session xa > "$OUT.whxst" 2>&1 6169 XDG_STATE_HOME="$WHSTATE" "$MUXA" status --sock "$SOCK40" --session xa > "$OUT.whxst" 2>&1
6164 grep -q '"cols":100' "$OUT.whxst" || { 6170 grep -q '"cols":40' "$OUT.whxst" || {
6165 echo "e2e FAIL: x forgets: session xa stopped answering — \\x1cx killed it:" 6171 echo "e2e FAIL: x forgets: session xa stopped answering — \\x1cx killed it:"
6166 cat "$OUT.whxst"; exit 1; } 6172 cat "$OUT.whxst"; exit 1; }
6167 wait_grid "$SOCK40" "whxa-pin" "x forgets: xa's grid outlived its tile" xa 6173 wait_grid "$SOCK40" "whxa-pin" "x forgets: xa's grid outlived its tile" xa
6168 6174
6169 # The last tile: an empty wall SAYS so rather than going blank. 6175 # The last tile: an empty wall SAYS so rather than going blank.
6170 set +e 6176 set +e
6171 XDG_STATE_HOME="$WHXSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \ 6177 XDG_STATE_HOME="$WHXSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \
6172 --out "$OUT.whxcap2" --err "$OUT.whxcap2.err" -- \ 6178 --out "$OUT.whxcap2" --err "$OUT.whxcap2.err" -- \
6173 "$MUX" wall > "$OUT.whxpc2" 2>&1 <<'EOF' 6179 "$MUX" wall > "$OUT.whxpc2" 2>&1 <<'EOF'
6174 expect whxb-pin 20000 6180 expect whxb-pin 20000
@@ -6269,7 +6275,9 @@ CVATT_BEFORE=$(attaches_now "$SOCK45")
6269 --sock "$SOCK45" --session 0 > "$OUT.cvinj" 2>&1 ) & 6275 --sock "$SOCK45" --session 0 > "$OUT.cvinj" 2>&1 ) &
6270 CVINJPID=$! 6276 CVINJPID=$!
6271 set +e 6277 set +e
6272 XDG_STATE_HOME="$CVSTATE" timeout 90 "$PTYCLIENT" --cols 100 --rows 30 \ 6278 # tall: Ctrl-\ w folds in a saved tile, making two at 100x30 (100 >= 60);
6279 # 70x36 stays stacked (70 < 72) and fits the bar label the assertion greps
6280 XDG_STATE_HOME="$CVSTATE" timeout 90 "$PTYCLIENT" --cols 70 --rows 36 \
6273 --out "$OUT.cvcap" --err "$OUT.cvcap.err" \ 6281 --out "$OUT.cvcap" --err "$OUT.cvcap.err" \
6274 -- "$MUX" --sock "$SOCK45" > "$OUT.cvpc" 2>&1 <<'EOF' 6282 -- "$MUX" --sock "$SOCK45" > "$OUT.cvpc" 2>&1 <<'EOF'
6275 expect \x1b[?1049h 15000 6283 expect \x1b[?1049h 15000
@@ -6360,7 +6368,8 @@ grep -qx -- "--sock $SOCK46#two" "$RGSTATE/mux/wall" 2>/dev/null && {
6360 6368
6361 RGATT_BEFORE=$(attaches_now "$SOCK46") 6369 RGATT_BEFORE=$(attaches_now "$SOCK46")
6362 set +e 6370 set +e
6363 XDG_STATE_HOME="$RGSTATE" timeout 90 "$PTYCLIENT" --cols 100 --rows 30 \ 6371 # tall: Ctrl-\ n grows the wall to two tiles at 100x30 (100 >= 60)
6372 XDG_STATE_HOME="$RGSTATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 \
6364 --out "$OUT.rgcap" --err "$OUT.rgcap.err" \ 6373 --out "$OUT.rgcap" --err "$OUT.rgcap.err" \
6365 -- "$MUX" --sock "$SOCK46" > "$OUT.rgpc" 2>&1 <<'EOF' 6374 -- "$MUX" --sock "$SOCK46" > "$OUT.rgpc" 2>&1 <<'EOF'
6366 expect \x1b[?1049h 15000 6375 expect \x1b[?1049h 15000
@@ -6453,7 +6462,8 @@ pipe_send 'exit 7\n'
6453 pipe_waitexit "exit semantics: the only tile's shell exited 7, and mux" 7 6462 pipe_waitexit "exit semantics: the only tile's shell exited 7, and mux" 7
6454 6463
6455 set +e 6464 set +e
6456 XDG_STATE_HOME="$XESTATE" timeout 90 "$PTYCLIENT" --cols 100 --rows 30 \ 6465 # tall: Ctrl-\ c creates a second tile at 100x30 (100 >= 60)
6466 XDG_STATE_HOME="$XESTATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 \
6457 --out "$OUT.xecap" --err "$OUT.xecap.err" \ 6467 --out "$OUT.xecap" --err "$OUT.xecap.err" \
6458 -- "$MUX" --sock "$SOCK47" > "$OUT.xepc" 2>&1 <<'EOF' 6468 -- "$MUX" --sock "$SOCK47" > "$OUT.xepc" 2>&1 <<'EOF'
6459 expect \x1b[?1049h 15000 6469 expect \x1b[?1049h 15000
@@ -6977,7 +6987,8 @@ wait_grid "$SOCK50" "wmb-pin" "wall cluster: session b's marker" b
6977 # other way. A negative that cannot fail is not a test, so each carries the 6987 # other way. A negative that cannot fail is not a test, so each carries the
6978 # marker that MUST land in the focused session. 6988 # marker that MUST land in the focused session.
6979 set +e 6989 set +e
6980 timeout 60 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.wmcap3" --err "$OUT.wmcap3.err" -- \ 6990 # tall: two tiles at 100x30 trip the aspect rule (100 >= 60)
6991 timeout 60 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.wmcap3" --err "$OUT.wmcap3.err" -- \
6981 "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc3" 2>&1 <<'EOF' 6992 "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc3" 2>&1 <<'EOF'
6982 expect wmb-pin 20000 6993 expect wmb-pin 20000
6983 settle 700 20000 6994 settle 700 20000
@@ -7006,7 +7017,8 @@ grep -q "wm-three" "$OUT.wmfa" && {
7006 # The other direction: focus b explicitly, then click a's content and watch 7017 # The other direction: focus b explicitly, then click a's content and watch
7007 # the focus come back to a. 7018 # the focus come back to a.
7008 set +e 7019 set +e
7009 timeout 60 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.wmcap4" --err "$OUT.wmcap4.err" -- \ 7020 # tall: two tiles at 100x30 trip the aspect rule (100 >= 60)
7021 timeout 60 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.wmcap4" --err "$OUT.wmcap4.err" -- \
7010 "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc4" 2>&1 <<'EOF' 7022 "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc4" 2>&1 <<'EOF'
7011 expect wmb-pin 20000 7023 expect wmb-pin 20000
7012 settle 700 20000 7024 settle 700 20000
@@ -7324,7 +7336,8 @@ pipe_detach
7324 wait_grid "$SOCK52" "mc-pin" "cursor: session c's marker" c 7336 wait_grid "$SOCK52" "mc-pin" "cursor: session c's marker" c
7325 7337
7326 set +e 7338 set +e
7327 timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.cucap" --err "$OUT.cucap.err" -- \ 7339 # tall: three tiles at 100x30 trip the aspect rule (100 >= 60)
7340 timeout 40 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.cucap" --err "$OUT.cucap.err" -- \
7328 "$MUX" wall --sock "$SOCK52#a" "--sock $SOCK52#b" "--sock $SOCK52#c" > "$OUT.cupc" 2>&1 <<'EOF' 7341 "$MUX" wall --sock "$SOCK52#a" "--sock $SOCK52#b" "--sock $SOCK52#c" > "$OUT.cupc" 2>&1 <<'EOF'
7329 expect mc-pin 20000 7342 expect mc-pin 20000
7330 settle 500 15000 7343 settle 500 15000
@@ -7397,7 +7410,8 @@ pipe_detach
7397 wait_grid "$SOCK53" "sb2nbr" "scrollback-rect: session b's pin" b 7410 wait_grid "$SOCK53" "sb2nbr" "scrollback-rect: session b's pin" b
7398 7411
7399 set +e 7412 set +e
7400 timeout 90 "$PTYCLIENT" --cols 92 --rows 30 --out "$OUT.sb2cap" --err "$OUT.sb2cap.err" -- \ 7413 # tall: two tiles at 92x30 trip the aspect rule (92 >= 60)
7414 timeout 90 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.sb2cap" --err "$OUT.sb2cap.err" -- \
7401 "$MUX" wall "--sock $SOCK53#a" "--sock $SOCK53#b" > "$OUT.sb2pc" 2>&1 <<'EOF' 7415 "$MUX" wall "--sock $SOCK53#a" "--sock $SOCK53#b" > "$OUT.sb2pc" 2>&1 <<'EOF'
7402 expect sb2nbr 20000 7416 expect sb2nbr 20000
7403 settle 700 20000 7417 settle 700 20000