7faf0467
feat: fullscreen is a relayout where the focus takes the terminal
a73x 2026-08-24 18:06
Commit message
src/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -247,7 +247,14 @@ const Shared = struct { | |||
| 247 | /// Relayout replaces it (freeing the old one); the keyboard thread | 247 | /// Relayout replaces it (freeing the old one); the keyboard thread |
| 248 | /// is the sole reader, under the same ownership as `tree`. | 248 | /// is the sole reader, under the same ownership as `tree`. |
| 249 | last_flat: ?layout.Flat = null, | 249 | last_flat: ?layout.Flat = null, |
| 250 | /// The non-fullscreen flatten, kept so `focus_dir` can answer from | ||
| 251 | /// the base layout while fullscreened — the fullscreen flat gives | ||
| 252 | /// every non-focused tile a 0×0 rect, so adjacency on it is null. | ||
| 253 | base_flat: ?layout.Flat = null, | ||
| 250 | flat_alloc: std.mem.Allocator = std.heap.page_allocator, | 254 | flat_alloc: std.mem.Allocator = std.heap.page_allocator, |
| 255 | /// `f` toggles it; relayout passes `sel` to flatten when set, so the | ||
| 256 | /// focused tile takes the whole terminal and every other gets 0×0. | ||
| 257 | fullscreen: bool = false, | ||
| 251 | /// Bumped when the terminal's contents are no longer anybody's paint — | 258 | /// Bumped when the terminal's contents are no longer anybody's paint — |
| 252 | /// a relayout re-cut the stripes, or the focus moved and the newly | 259 | /// a relayout re-cut the stripes, or the focus moved and the newly |
| 253 | /// focused tile owes a full repaint. Pumps compare it against what | 260 | /// focused tile owes a full repaint. Pumps compare it against what |
| @@ -1492,7 +1499,9 @@ fn pumpTile(t: *Tile) void { | |||
| 1492 | if (gen != painted_gen) { | 1499 | if (gen != painted_gen) { |
| 1493 | core.drag.clear(); | 1500 | core.drag.clear(); |
| 1494 | paintLabel(t, state); | 1501 | paintLabel(t, state); |
| 1495 | core.repaint() catch {}; | 1502 | // A 0-row rect (fullscreened out) paints nothing: the daemon |
| 1503 | // refused the 0×0 resize, so the session keeps its real grid. | ||
| 1504 | if (snap_view_rows > 0) core.repaint() catch {}; | ||
| 1496 | painted_gen = gen; | 1505 | painted_gen = gen; |
| 1497 | } | 1506 | } |
| 1498 | } | 1507 | } |
| @@ -1549,6 +1558,8 @@ fn focusAnswer( | |||
| 1549 | ) void { | 1558 | ) void { |
| 1550 | if (recut) relayout(alloc, tiles, present, shared, shared.sel); | 1559 | if (recut) relayout(alloc, tiles, present, shared, shared.sel); |
| 1551 | setFocus(tiles, shared, to); | 1560 | setFocus(tiles, shared, to); |
| 1561 | // Fullscreen re-flattens with the new focus so the full rect follows. | ||
| 1562 | if (shared.fullscreen) relayout(alloc, tiles, present, shared, shared.sel); | ||
| 1552 | } | 1563 | } |
| 1553 | 1564 | ||
| 1554 | /// The whole screen an empty wall gets: one line, at the top, saying the | 1565 | /// The whole screen an empty wall gets: one line, at the top, saying the |
| @@ -1606,17 +1617,22 @@ fn relayout( | |||
| 1606 | if (p) live += 1; | 1617 | if (p) live += 1; |
| 1607 | } | 1618 | } |
| 1608 | // A one-tile wall owns every row and draws no label bar; two or more | 1619 | // A one-tile wall owns every row and draws no label bar; two or more |
| 1609 | // tiles each lose their top row to one. Set before the tree is flattened | 1620 | // tiles each lose their top row to one. Fullscreen is the same: one |
| 1610 | // so `viewRows` is right from the first attach, and before the doorbells | 1621 | // visible pane, no bar — the plain client's byte stream. |
| 1611 | // so the pumps read it on the pass they answer. | 1622 | shared.label_rows = if (shared.fullscreen or live <= 1) 0 else 1; |
| 1612 | shared.label_rows = if (live > 1) 1 else 0; | ||
| 1613 | if (shared.is_tty) proto.writeAllFd(shared.out_fd, "\x1b[?25l\x1b[H\x1b[2J") catch {}; | 1623 | if (shared.is_tty) proto.writeAllFd(shared.out_fd, "\x1b[?25l\x1b[H\x1b[2J") catch {}; |
| 1614 | if (live == 0) { | 1624 | if (live == 0) { |
| 1615 | paintEmptyWallLocked(shared); | 1625 | paintEmptyWallLocked(shared); |
| 1616 | return; | 1626 | return; |
| 1617 | } | 1627 | } |
| 1618 | if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), null)) |flat| { | 1628 | // The base flat (null) is always computed so `focus_dir` can read |
| 1619 | // Replace the stored flat: free the old one first, then keep this. | 1629 | // adjacency from the real layout while fullscreened. |
| 1630 | if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), null)) |base| { | ||
| 1631 | if (shared.base_flat) |*old| old.deinit(shared.flat_alloc); | ||
| 1632 | shared.base_flat = base; | ||
| 1633 | } else |_| {} | ||
| 1634 | const fs_arg: ?u8 = if (shared.fullscreen) @intCast(sel) else null; | ||
| 1635 | if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), fs_arg)) |flat| { | ||
| 1620 | if (shared.last_flat) |*old| old.deinit(shared.flat_alloc); | 1636 | if (shared.last_flat) |*old| old.deinit(shared.flat_alloc); |
| 1621 | shared.last_flat = flat; | 1637 | shared.last_flat = flat; |
| 1622 | for (tiles, present) |*t, p| { | 1638 | for (tiles, present) |*t, p| { |
| @@ -1624,12 +1640,6 @@ fn relayout( | |||
| 1624 | if (flat.rectOf(@intCast(t.idx))) |r| { | 1640 | if (flat.rectOf(@intCast(t.idx))) |r| { |
| 1625 | t.rect = r; | 1641 | t.rect = r; |
| 1626 | } | 1642 | } |
| 1627 | // A rect that moved or reshaped owes the daemon its new size: | ||
| 1628 | // the pump is the transport's only writer, so the doorbell is | ||
| 1629 | // set here and the pump sends the `.resize` from its own pass. | ||
| 1630 | // `viewRows` reads `label_rows` and the rect together, both | ||
| 1631 | // set above and here — so the frame carries the rect this tile | ||
| 1632 | // now claims. | ||
| 1633 | t.resize_pending.store(true, .release); | 1643 | t.resize_pending.store(true, .release); |
| 1634 | } | 1644 | } |
| 1635 | paintRailsLocked(shared, flat); | 1645 | paintRailsLocked(shared, flat); |
| @@ -2585,11 +2595,15 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) ! | |||
| 2585 | ring(&tiles[z]); | 2595 | ring(&tiles[z]); |
| 2586 | }, | 2596 | }, |
| 2587 | .focus_dir => |d| { | 2597 | .focus_dir => |d| { |
| 2588 | if (shared.last_flat) |flat| { | 2598 | const flat = shared.base_flat orelse shared.last_flat; |
| 2589 | if (layout.neighbor(flat, @intCast(z), dirOf(d))) |nb| { | 2599 | if (flat) |f| { |
| 2600 | if (layout.neighbor(f, @intCast(z), dirOf(d))) |nb| { | ||
| 2590 | if (nb < live and present[nb] and nb != z) { | 2601 | if (nb < live and present[nb] and nb != z) { |
| 2591 | last_focus = z; | 2602 | last_focus = z; |
| 2592 | setFocus(tiles[0..live], &shared, nb); | 2603 | if (shared.fullscreen) |
| 2604 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, false, nb) | ||
| 2605 | else | ||
| 2606 | setFocus(tiles[0..live], &shared, nb); | ||
| 2593 | } | 2607 | } |
| 2594 | } | 2608 | } |
| 2595 | } | 2609 | } |
| @@ -2604,14 +2618,19 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) ! | |||
| 2604 | tiles[z].ask.store(@intFromEnum(client.SwitchIntent.new), .release); | 2618 | tiles[z].ask.store(@intFromEnum(client.SwitchIntent.new), .release); |
| 2605 | ring(&tiles[z]); | 2619 | ring(&tiles[z]); |
| 2606 | }, | 2620 | }, |
| 2607 | .fullscreen, | 2621 | .fullscreen => { |
| 2622 | shared.fullscreen = !shared.fullscreen; | ||
| 2623 | relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | ||
| 2624 | }, | ||
| 2608 | .resize, | 2625 | .resize, |
| 2609 | => {}, | 2626 | => {}, |
| 2610 | .focus => |idx| { | 2627 | .focus => |idx| { |
| 2611 | // `Ctrl-\ 1-9` focuses tile N (one-based). | ||
| 2612 | if (idx > 0 and idx <= live and present[idx - 1] and idx - 1 != z) { | 2628 | if (idx > 0 and idx <= live and present[idx - 1] and idx - 1 != z) { |
| 2613 | last_focus = z; | 2629 | last_focus = z; |
| 2614 | setFocus(tiles[0..live], &shared, idx - 1); | 2630 | if (shared.fullscreen) |
| 2631 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, false, idx - 1) | ||
| 2632 | else | ||
| 2633 | setFocus(tiles[0..live], &shared, idx - 1); | ||
| 2615 | } | 2634 | } |
| 2616 | }, | 2635 | }, |
| 2617 | .forget => { | 2636 | .forget => { |
| @@ -3065,6 +3084,7 @@ test "relayout sets resize_pending on every live tile" { | |||
| 3065 | shared.flat_alloc = alloc; | 3084 | shared.flat_alloc = alloc; |
| 3066 | defer shared.tree.deinit(); | 3085 | defer shared.tree.deinit(); |
| 3067 | defer if (shared.last_flat) |*f| f.deinit(alloc); | 3086 | defer if (shared.last_flat) |*f| f.deinit(alloc); |
| 3087 | defer if (shared.base_flat) |*f| f.deinit(alloc); | ||
| 3068 | try shared.tree.addFirst(0); | 3088 | try shared.tree.addFirst(0); |
| 3069 | try shared.tree.insert(0, 1); | 3089 | try shared.tree.insert(0, 1); |
| 3070 | const tiles = try alloc.alloc(Tile, 2); | 3090 | const tiles = try alloc.alloc(Tile, 2); |
| @@ -3091,9 +3111,117 @@ test "relayout sets resize_pending on every live tile" { | |||
| 3091 | try std.testing.expect(tiles[1].resize_pending.load(.acquire)); | 3111 | try std.testing.expect(tiles[1].resize_pending.load(.acquire)); |
| 3092 | } | 3112 | } |
| 3093 | 3113 | ||
| 3114 | test "fullscreen gives the focused tile the whole terminal and hides the rest" { | ||
| 3115 | const alloc = std.testing.allocator; | ||
| 3116 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | ||
| 3117 | shared.tree = layout.Tree.init(alloc); | ||
| 3118 | shared.flat_alloc = alloc; | ||
| 3119 | defer shared.tree.deinit(); | ||
| 3120 | defer if (shared.last_flat) |*f| f.deinit(alloc); | ||
| 3121 | defer if (shared.base_flat) |*f| f.deinit(alloc); | ||
| 3122 | try shared.tree.addFirst(0); | ||
| 3123 | try shared.tree.insert(0, 1); | ||
| 3124 | const tiles = try alloc.alloc(Tile, 2); | ||
| 3125 | defer alloc.free(tiles); | ||
| 3126 | const present = [_]bool{ true, true }; | ||
| 3127 | for (tiles, 0..) |*t, i| { | ||
| 3128 | t.* = Tile{ | ||
| 3129 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | ||
| 3130 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, | ||
| 3131 | .shared = &shared, | ||
| 3132 | .idx = i, | ||
| 3133 | .wake_r = -1, | ||
| 3134 | .wake_w = -1, | ||
| 3135 | }; | ||
| 3136 | } | ||
| 3137 | shared.fullscreen = true; | ||
| 3138 | relayout(alloc, tiles, &present, &shared, 0); | ||
| 3139 | // No label bar: the fullscreened pane is a one-tile wall. | ||
| 3140 | try std.testing.expectEqual(@as(u8, 0), shared.label_rows); | ||
| 3141 | // Tile 0 gets the whole terminal; tile 1 gets nothing. | ||
| 3142 | try std.testing.expectEqual(@as(u16, 24), tiles[0].rect.rows); | ||
| 3143 | try std.testing.expectEqual(@as(u16, 80), tiles[0].rect.cols); | ||
| 3144 | try std.testing.expectEqual(@as(u16, 0), tiles[1].rect.rows); | ||
| 3145 | try std.testing.expectEqual(@as(u16, 0), tiles[1].rect.cols); | ||
| 3146 | // The base flat is kept for neighbor. | ||
| 3147 | try std.testing.expect(shared.base_flat != null); | ||
| 3148 | } | ||
| 3149 | |||
| 3150 | test "toggle off fullscreen restores the real rects" { | ||
| 3151 | const alloc = std.testing.allocator; | ||
| 3152 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | ||
| 3153 | shared.tree = layout.Tree.init(alloc); | ||
| 3154 | shared.flat_alloc = alloc; | ||
| 3155 | defer shared.tree.deinit(); | ||
| 3156 | defer if (shared.last_flat) |*f| f.deinit(alloc); | ||
| 3157 | defer if (shared.base_flat) |*f| f.deinit(alloc); | ||
| 3158 | try shared.tree.addFirst(0); | ||
| 3159 | try shared.tree.insert(0, 1); | ||
| 3160 | const tiles = try alloc.alloc(Tile, 2); | ||
| 3161 | defer alloc.free(tiles); | ||
| 3162 | const present = [_]bool{ true, true }; | ||
| 3163 | for (tiles, 0..) |*t, i| { | ||
| 3164 | t.* = Tile{ | ||
| 3165 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | ||
| 3166 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, | ||
| 3167 | .shared = &shared, | ||
| 3168 | .idx = i, | ||
| 3169 | .wake_r = -1, | ||
| 3170 | .wake_w = -1, | ||
| 3171 | }; | ||
| 3172 | } | ||
| 3173 | shared.fullscreen = true; | ||
| 3174 | relayout(alloc, tiles, &present, &shared, 0); | ||
| 3175 | shared.fullscreen = false; | ||
| 3176 | relayout(alloc, tiles, &present, &shared, 0); | ||
| 3177 | // Two tiles again: label bar back, both have real rects (stacked: | ||
| 3178 | // full width, half height each). | ||
| 3179 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); | ||
| 3180 | try std.testing.expectEqual(@as(u16, 80), tiles[0].rect.cols); | ||
| 3181 | try std.testing.expectEqual(@as(u16, 80), tiles[1].rect.cols); | ||
| 3182 | try std.testing.expect(tiles[0].rect.rows < 24); | ||
| 3183 | try std.testing.expect(tiles[1].rect.rows > 0); | ||
| 3184 | } | ||
| 3185 | |||
| 3186 | test "focus_dir while fullscreened follows the focus" { | ||
| 3187 | const alloc = std.testing.allocator; | ||
| 3188 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | ||
| 3189 | shared.tree = layout.Tree.init(alloc); | ||
| 3190 | shared.flat_alloc = alloc; | ||
| 3191 | defer shared.tree.deinit(); | ||
| 3192 | defer if (shared.last_flat) |*f| f.deinit(alloc); | ||
| 3193 | defer if (shared.base_flat) |*f| f.deinit(alloc); | ||
| 3194 | try shared.tree.addFirst(0); | ||
| 3195 | try shared.tree.insert(0, 1); | ||
| 3196 | const tiles = try alloc.alloc(Tile, 2); | ||
| 3197 | defer alloc.free(tiles); | ||
| 3198 | const present = [_]bool{ true, true }; | ||
| 3199 | for (tiles, 0..) |*t, i| { | ||
| 3200 | t.* = Tile{ | ||
| 3201 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | ||
| 3202 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, | ||
| 3203 | .shared = &shared, | ||
| 3204 | .idx = i, | ||
| 3205 | .wake_r = -1, | ||
| 3206 | .wake_w = -1, | ||
| 3207 | }; | ||
| 3208 | } | ||
| 3209 | shared.fullscreen = true; | ||
| 3210 | relayout(alloc, tiles, &present, &shared, 0); | ||
| 3211 | // Move focus to tile 1 while fullscreened. | ||
| 3212 | focusAnswer(alloc, tiles, &present, &shared, false, 1); | ||
| 3213 | // The full rect followed: tile 1 now owns the terminal. | ||
| 3214 | try std.testing.expectEqual(@as(u16, 24), tiles[1].rect.rows); | ||
| 3215 | try std.testing.expectEqual(@as(u16, 80), tiles[1].rect.cols); | ||
| 3216 | try std.testing.expectEqual(@as(u16, 0), tiles[0].rect.rows); | ||
| 3217 | try std.testing.expectEqual(@as(usize, 1), shared.sel); | ||
| 3218 | } | ||
| 3219 | |||
| 3094 | test "a pump's answer that grew the wall re-cuts it; a mere focus move does not" { | 3220 | test "a pump's answer that grew the wall re-cuts it; a mere focus move does not" { |
| 3095 | const alloc = std.testing.allocator; | 3221 | const alloc = std.testing.allocator; |
| 3096 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | 3222 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; |
| 3223 | defer if (shared.last_flat) |*f| f.deinit(shared.flat_alloc); | ||
| 3224 | defer if (shared.base_flat) |*f| f.deinit(shared.flat_alloc); | ||
| 3097 | const tiles = try alloc.alloc(Tile, 2); | 3225 | const tiles = try alloc.alloc(Tile, 2); |
| 3098 | defer alloc.free(tiles); | 3226 | defer alloc.free(tiles); |
| 3099 | const present = try alloc.alloc(bool, 2); | 3227 | const present = try alloc.alloc(bool, 2); |
| @@ -3136,6 +3264,7 @@ test "a one-tile wall draws no label bar and paints row 1" { | |||
| 3136 | shared.flat_alloc = alloc; | 3264 | shared.flat_alloc = alloc; |
| 3137 | defer shared.tree.deinit(); | 3265 | defer shared.tree.deinit(); |
| 3138 | defer if (shared.last_flat) |*f| f.deinit(alloc); | 3266 | defer if (shared.last_flat) |*f| f.deinit(alloc); |
| 3267 | defer if (shared.base_flat) |*f| f.deinit(alloc); | ||
| 3139 | try shared.tree.addFirst(0); | 3268 | try shared.tree.addFirst(0); |
| 3140 | const tiles = try alloc.alloc(Tile, 1); | 3269 | const tiles = try alloc.alloc(Tile, 1); |
| 3141 | defer alloc.free(tiles); | 3270 | defer alloc.free(tiles); |
| @@ -3340,6 +3469,7 @@ test "a relayout drops every tile's highlight, because the stripes move under it | |||
| 3340 | shared.flat_alloc = alloc; | 3469 | shared.flat_alloc = alloc; |
| 3341 | defer shared.tree.deinit(); | 3470 | defer shared.tree.deinit(); |
| 3342 | defer if (shared.last_flat) |*f| f.deinit(alloc); | 3471 | defer if (shared.last_flat) |*f| f.deinit(alloc); |
| 3472 | defer if (shared.base_flat) |*f| f.deinit(alloc); | ||
| 3343 | try shared.tree.addFirst(0); | 3473 | try shared.tree.addFirst(0); |
| 3344 | try shared.tree.insert(0, 1); | 3474 | try shared.tree.insert(0, 1); |
| 3345 | const tiles = try alloc.alloc(Tile, 2); | 3475 | const tiles = try alloc.alloc(Tile, 2); |
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -370,7 +370,9 @@ ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh" | |||
| 370 | # sub-rect, so its daemon gets a socket of its own for the grid-size | 370 | # sub-rect, so its daemon gets a socket of its own for the grid-size |
| 371 | # reason every wall leg has. | 371 | # reason every wall leg has. |
| 372 | SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock" | 372 | SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock" |
| 373 | SOCK54="${TMPDIR:-/tmp}/muxd-e2e-fullscreen-$$.sock" | ||
| 373 | D54PID="" | 374 | D54PID="" |
| 375 | D55PID="" | ||
| 374 | 376 | ||
| 375 | # The wall as attach HISTORY (phase 2). A daemon AND a state home of its | 377 | # The wall as attach HISTORY (phase 2). A daemon AND a state home of its |
| 376 | # own, for the dynamic-wall leg's reason turned up one notch: what these | 378 | # own, for the dynamic-wall leg's reason turned up one notch: what these |
| @@ -1270,6 +1272,7 @@ cleanup() { | |||
| 1270 | [ -n "$D43PID" ] && kill "$D43PID" 2>/dev/null || true | 1272 | [ -n "$D43PID" ] && kill "$D43PID" 2>/dev/null || true |
| 1271 | [ -n "${D51PID:-}" ] && kill "$D51PID" 2>/dev/null || true | 1273 | [ -n "${D51PID:-}" ] && kill "$D51PID" 2>/dev/null || true |
| 1272 | [ -n "${D52PID:-}" ] && kill "$D52PID" 2>/dev/null || true | 1274 | [ -n "${D52PID:-}" ] && kill "$D52PID" 2>/dev/null || true |
| 1275 | [ -n "${D55PID:-}" ] && kill "$D55PID" 2>/dev/null || true | ||
| 1273 | # The ssh-agents the forwarding legs start. Not mux processes and so not | 1276 | # The ssh-agents the forwarding legs start. Not mux processes and so not |
| 1274 | # the leak sweep's business, but they are daemons this file forked: left | 1277 | # the leak sweep's business, but they are daemons this file forked: left |
| 1275 | # alive they outlive the suite holding a private key, which is the one | 1278 | # alive they outlive the suite holding a private key, which is the one |
| @@ -1314,6 +1317,7 @@ cleanup() { | |||
| 1314 | [ -S "$SOCK50" ] && "$MUXD" stop --sock "$SOCK50" 2>/dev/null || true | 1317 | [ -S "$SOCK50" ] && "$MUXD" stop --sock "$SOCK50" 2>/dev/null || true |
| 1315 | [ -S "$SOCK52" ] && "$MUXD" stop --sock "$SOCK52" 2>/dev/null || true | 1318 | [ -S "$SOCK52" ] && "$MUXD" stop --sock "$SOCK52" 2>/dev/null || true |
| 1316 | [ -S "$SOCK53" ] && "$MUXD" stop --sock "$SOCK53" 2>/dev/null || true | 1319 | [ -S "$SOCK53" ] && "$MUXD" stop --sock "$SOCK53" 2>/dev/null || true |
| 1320 | [ -S "$SOCK54" ] && "$MUXD" stop --sock "$SOCK54" 2>/dev/null || true | ||
| 1317 | 1321 | ||
| 1318 | # ---- the leak sweep (hygiene kit, 6a) ---- | 1322 | # ---- the leak sweep (hygiene kit, 6a) ---- |
| 1319 | # Here rather than at the bottom of the file, which `set -e` reaches only | 1323 | # Here rather than at the bottom of the file, which `set -e` reaches only |
| @@ -1328,7 +1332,8 @@ cleanup() { | |||
| 1328 | "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ | 1332 | "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ |
| 1329 | "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \ | 1333 | "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \ |
| 1330 | "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \ | 1334 | "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \ |
| 1331 | "$D38PID" "$D39PID" "$D40PID" "$D41PID" "$D42PID" "$D43PID" "$D54PID" | 1335 | "$D38PID" "$D39PID" "$D40PID" "$D41PID" "$D42PID" "$D43PID" "$D54PID" \ |
| 1336 | "$D55PID" | ||
| 1332 | _leak=0 | 1337 | _leak=0 |
| 1333 | leak_sweep "$_rc" || _leak=1 | 1338 | leak_sweep "$_rc" || _leak=1 |
| 1334 | 1339 | ||
| @@ -1397,7 +1402,10 @@ cleanup() { | |||
| 1397 | "$LESSDATA" "$OUT.pgrcap" "$OUT.pgrstop" "$SOCK35" \ | 1402 | "$LESSDATA" "$OUT.pgrcap" "$OUT.pgrstop" "$SOCK35" \ |
| 1398 | "$OUT.pipe" "$OUT.pipe.d" "$OUT.pipecap" "$OUT.pipestop" "$SOCK36" \ | 1403 | "$OUT.pipe" "$OUT.pipe.d" "$OUT.pipecap" "$OUT.pipestop" "$SOCK36" \ |
| 1399 | "$OUT.sb2.d" "$OUT.sb2a" "$OUT.sb2a.err" "$OUT.sb2b" "$OUT.sb2b.err" \ | 1404 | "$OUT.sb2.d" "$OUT.sb2a" "$OUT.sb2a.err" "$OUT.sb2b" "$OUT.sb2b.err" \ |
| 1400 | "$OUT.sb2cap" "$OUT.sb2cap.err" "$OUT.sb2pc" "$OUT.sb2stop" "$SOCK53" | 1405 | "$OUT.sb2cap" "$OUT.sb2cap.err" "$OUT.sb2pc" "$OUT.sb2stop" "$SOCK53" \ |
| 1406 | "$OUT.fs.d" "$OUT.fsa" "$OUT.fsa.err" "$OUT.fsb" "$OUT.fsb.err" \ | ||
| 1407 | "$OUT.fscap" "$OUT.fscap.err" "$OUT.fspc" "$OUT.fsfa" "$OUT.fsfb" \ | ||
| 1408 | "$OUT.fsstop" "$SOCK54" | ||
| 1401 | # ...and the non-tty capture that leg's session feeds. | 1409 | # ...and the non-tty capture that leg's session feeds. |
| 1402 | rm -f "$OUT.nogate" | 1410 | rm -f "$OUT.nogate" |
| 1403 | # ...and its other half: the paste capture and the file nvim wrote, which | 1411 | # ...and its other half: the paste capture and the file nvim wrote, which |
| @@ -7457,7 +7465,7 @@ DPID="" | |||
| 7457 | 7465 | ||
| 7458 | # The pins. Literals, not variables set from counting something else — | 7466 | # The pins. Literals, not variables set from counting something else — |
| 7459 | # "assert the literal, never the constant the code under test reads" | 7467 | # "assert the literal, never the constant the code under test reads" |
| 7460 | # (decisions.md, M10). 64 scenario checkpoints; 36 convergence points. | 7468 | # (decisions.md, M10). 65 scenario checkpoints; 37 convergence points. |
| 7461 | # (The first of these two numbers had drifted to 43 while the pin below | 7469 | # (The first of these two numbers had drifted to 43 while the pin below |
| 7462 | # said 63 — prose is not gated, which is why the pin is.) | 7470 | # said 63 — prose is not gated, which is why the pin is.) |
| 7463 | # Anyone adding a scenario updates these by hand, on purpose. | 7471 | # Anyone adding a scenario updates these by hand, on purpose. |
| @@ -7587,8 +7595,81 @@ DPID="" | |||
| 7587 | # it is the one leg that holds an ssh-agent under SIGSTOP, and a trap that | 7595 | # it is the one leg that holds an ssh-agent under SIGSTOP, and a trap that |
| 7588 | # has to CONT before it kills is cheaper to reason about with nothing | 7596 | # has to CONT before it kills is cheaper to reason about with nothing |
| 7589 | # after it. | 7597 | # after it. |
| 7590 | [ "$OK_COUNT" = "63" ] || { | 7598 | |
| 7591 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 63 —" | 7599 | # ---- fullscreen: f toggles, focus follows, restore ------------------- |
| 7600 | # | ||
| 7601 | # `f` is a layout op, not a mode: the focused tile takes the whole | ||
| 7602 | # terminal, every other gets 0x0. A 0x0 rect claims nothing, so hidden | ||
| 7603 | # panes stay attached and paint nothing. Focus moves while fullscreened | ||
| 7604 | # re-flatten, so Ctrl-\ 2 gives tile 2 the full rect. `f` again restores | ||
| 7605 | # the real layout. This leg proves all three with `muxa capture` — the | ||
| 7606 | # marker each session received names which tile owned the terminal. | ||
| 7607 | # | ||
| 7608 | # No convergence point: `muxa capture` reads the daemon's grid, not the | ||
| 7609 | # terminal's, and the daemon's grid is the side channel this asserts on. | ||
| 7610 | "$MUXD" run --sock "$SOCK54" --shell /bin/sh > "$OUT.fs.d" 2>&1 & | ||
| 7611 | D55PID=$! | ||
| 7612 | wait_sock "$SOCK54" "$OUT.fs.d" "fullscreen daemon never bound" | ||
| 7613 | |||
| 7614 | pipe_mux "$OUT.fsa" "$OUT.fsa.err" timeout 40 "$MUX" --sock "$SOCK54" --session a | ||
| 7615 | pipe_send 'printf "fs-%%s\\n" init-a\n' | ||
| 7616 | await_out "$OUT.fsa" "fs-init-a" "fullscreen: session a's marker never reached the client" | ||
| 7617 | pipe_detach | ||
| 7618 | wait_grid "$SOCK54" "fs-init-a" "fullscreen: session a's marker" a | ||
| 7619 | pipe_mux "$OUT.fsb" "$OUT.fsb.err" timeout 40 "$MUX" --sock "$SOCK54" --session b | ||
| 7620 | pipe_send 'printf "fs-%%s\\n" init-b\n' | ||
| 7621 | await_out "$OUT.fsb" "fs-init-b" "fullscreen: session b's marker never reached the client" | ||
| 7622 | pipe_detach | ||
| 7623 | wait_grid "$SOCK54" "fs-init-b" "fullscreen: session b's marker" b | ||
| 7624 | |||
| 7625 | set +e | ||
| 7626 | # wide: 80x24 trips the aspect rule (80 >= 48 → beside) | ||
| 7627 | timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.fscap" --err "$OUT.fscap.err" -- \ | ||
| 7628 | "$MUX" wall "--sock $SOCK54#a" "--sock $SOCK54#b" > "$OUT.fspc" 2>&1 <<'EOF' | ||
| 7629 | expect fs-init-b 20000 | ||
| 7630 | settle 700 20000 | ||
| 7631 | send \x1cf | ||
| 7632 | settle 700 20000 | ||
| 7633 | send printf 'fs-%s\n' one\n | ||
| 7634 | expect fs-one 10000 | ||
| 7635 | settle 400 15000 | ||
| 7636 | send \x1c2 | ||
| 7637 | settle 700 20000 | ||
| 7638 | send printf 'fs-%s\n' two\n | ||
| 7639 | expect fs-two 10000 | ||
| 7640 | settle 400 15000 | ||
| 7641 | send \x1cf | ||
| 7642 | settle 700 20000 | ||
| 7643 | send \x1cd | ||
| 7644 | waitexit 10000 | ||
| 7645 | EOF | ||
| 7646 | RC=$? | ||
| 7647 | set -e | ||
| 7648 | [ "$RC" -eq 0 ] || { | ||
| 7649 | echo "e2e FAIL: fullscreen: ptyclient leg exited $RC:" | ||
| 7650 | cat "$OUT.fspc" "$OUT.fscap.err"; exit 1; } | ||
| 7651 | # fs-one landed in session a (the tile that was fullscreened first); | ||
| 7652 | # fs-two landed in session b (the tile Ctrl-\ 2 moved the full rect to). | ||
| 7653 | timeout 20 "$MUXA" capture --sock "$SOCK54" --session a > "$OUT.fsfa" 2>&1 | ||
| 7654 | timeout 20 "$MUXA" capture --sock "$SOCK54" --session b > "$OUT.fsfb" 2>&1 | ||
| 7655 | grep -q "fs-one" "$OUT.fsfa" || { | ||
| 7656 | echo "e2e FAIL: fullscreen: session a never got fs-one:" | ||
| 7657 | cat "$OUT.fsfa"; exit 1; } | ||
| 7658 | grep -q "fs-one" "$OUT.fsfb" && { | ||
| 7659 | echo "e2e FAIL: fullscreen: fs-one reached session b, which was hidden:" | ||
| 7660 | cat "$OUT.fsfb"; exit 1; } | ||
| 7661 | grep -q "fs-two" "$OUT.fsfb" || { | ||
| 7662 | echo "e2e FAIL: fullscreen: Ctrl-\\ 2 did not move the full rect to session b:" | ||
| 7663 | cat "$OUT.fsfb"; exit 1; } | ||
| 7664 | grep -q "fs-two" "$OUT.fsfa" && { | ||
| 7665 | echo "e2e FAIL: fullscreen: fs-two reached session a, which was hidden after the focus move:" | ||
| 7666 | cat "$OUT.fsfa"; exit 1; } | ||
| 7667 | assert_stopped "$SOCK54" "$D55PID" "fullscreen" "$OUT.fsstop" | ||
| 7668 | D55PID="" | ||
| 7669 | ok "fullscreen gives the focused tile the terminal; focus follows; f restores" | ||
| 7670 | |||
| 7671 | [ "$OK_COUNT" = "64" ] || { | ||
| 7672 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 64 —" | ||
| 7592 | echo " a scenario was added (update the pin) or silently lost" | 7673 | echo " a scenario was added (update the pin) or silently lost" |
| 7593 | exit 1 | 7674 | exit 1 |
| 7594 | } | 7675 | } |
| @@ -7596,4 +7677,4 @@ DPID="" | |||
| 7596 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 37" | 7677 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 37" |
| 7597 | exit 1 | 7678 | exit 1 |
| 7598 | } | 7679 | } |
| 7599 | echo "e2e OK (62 scenarios, 37 convergence points)" | 7680 | echo "e2e OK (63 scenarios, 37 convergence points)" |