a73x

6cdc66e5

feat: a disowned seeded pane plans as gone, not vanish

a73x   2026-09-01 09:56

Commit message
feat: a disowned seeded pane plans as gone, not vanish

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

src/tui/wall_host.zig
Old New
@@ -183,6 +183,7 @@ pub fn planHostDiff(
183 births: *BirthNames, 183 births: *BirthNames,
184 binds: *TileIdxs, 184 binds: *TileIdxs,
185 vanish: *TileIdxs, 185 vanish: *TileIdxs,
186 gones: *TileIdxs,
186 ) void { 187 ) void {
187 // `proto.sessionsIter` carries the trust policy: a peer's reply is bounded 188 // `proto.sessionsIter` carries the trust policy: a peer's reply is bounded
188 // only in total, so it yields only lines that are names — which is what 189 // only in total, so it yields only lines that are names — which is what
@@ -215,6 +216,15 @@ pub fn planHostDiff(
215 t.missed_once = false; 216 t.missed_once = false;
216 continue; 217 continue;
217 } 218 }
219 // A seeded pane the host itself disowns is GONE, never vanished:
220 // its rect is the user's authored layout, and collapsing it here is
221 // the one-second re-cut the seed exists to prevent. Listed only on
222 // the transition — the poll returns every second, and re-dressing
223 // a pane per poll would repaint its bar for nothing.
224 if (t.pending) {
225 if (t.state != .gone) gones.append(i);
226 continue;
227 }
218 // A LIVE pump gets one list's grace: the daemon drains `exit_status` 228 // A LIVE pump gets one list's grace: the daemon drains `exit_status`
219 // before clearing the slot, but the poll takes milliseconds the pump can 229 // before clearing the slot, but the poll takes milliseconds the pump can
220 // be descheduled for — and vanishing the tile first loses the shell's 230 // be descheduled for — and vanishing the tile first loses the shell's
@@ -330,7 +340,11 @@ pub fn applyHostList(w: Wall, hi: usize) void {
330 var births = BirthNames{}; 340 var births = BirthNames{};
331 var binds = TileIdxs{}; 341 var binds = TileIdxs{};
332 var vanish = TileIdxs{}; 342 var vanish = TileIdxs{};
333 planHostDiff(w.liveTiles(), w.livePresent(), w.live.*, hi, list, h.self_name, &births, &binds, &vanish); 343 // Where the seeded panes this list disowns land. Nothing reads them
344 // yet: taking them off the vanish path is what makes such a pane
345 // stand, and dressing its bar is a separate change.
346 var gones = TileIdxs{};
347 planHostDiff(w.liveTiles(), w.livePresent(), w.live.*, hi, list, h.self_name, &births, &binds, &vanish, &gones);
334 // Binds first, and they do not count as change: a pane whose rect 348 // Binds first, and they do not count as change: a pane whose rect
335 // was cut at seed time wakes in place, and re-cuts nothing. 349 // was cut at seed time wakes in place, and re-cuts nothing.
336 for (binds.items[0..binds.len]) |bi| wv.bindTile(w, bi); 350 for (binds.items[0..binds.len]) |bi| wv.bindTile(w, bi);
src/tui/wall_test_host.zig
Old New
@@ -25,27 +25,28 @@ test "planHostDiff: names the daemon has and the wall does not are births, tiles
25 var births = BirthNames{}; 25 var births = BirthNames{};
26 var binds = TileIdxs{}; 26 var binds = TileIdxs{};
27 var vanish = TileIdxs{}; 27 var vanish = TileIdxs{};
28 var gones = TileIdxs{};
28 // The birth is immediate; the vanish waits for a second list to agree 29 // The birth is immediate; the vanish waits for a second list to agree
29 // (see the grace test below), so this list is asked twice. 30 // (see the grace test below), so this list is asked twice.
30 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &births, &binds, &vanish); 31 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &births, &binds, &vanish, &gones);
31 try std.testing.expectEqual(@as(usize, 1), births.len); 32 try std.testing.expectEqual(@as(usize, 1), births.len);
32 try std.testing.expectEqualStrings("c", births.get(0)); 33 try std.testing.expectEqualStrings("c", births.get(0));
33 births.len = 0; 34 births.len = 0;
34 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &births, &binds, &vanish); 35 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &births, &binds, &vanish, &gones);
35 try std.testing.expectEqual(@as(usize, 1), vanish.len); 36 try std.testing.expectEqual(@as(usize, 1), vanish.len);
36 try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); 37 try std.testing.expectEqual(@as(usize, 1), vanish.get(0));
37 38
38 // Host 1's "a" survives host 0's list saying nothing about it. 39 // Host 1's "a" survives host 0's list saying nothing about it.
39 births.len = 0; 40 births.len = 0;
40 vanish.len = 0; 41 vanish.len = 0;
41 wall_host.planHostDiff(&tiles, &present, 3, 1, "a\n", null, &births, &binds, &vanish); 42 wall_host.planHostDiff(&tiles, &present, 3, 1, "a\n", null, &births, &binds, &vanish, &gones);
42 try std.testing.expectEqual(@as(usize, 0), births.len + vanish.len); 43 try std.testing.expectEqual(@as(usize, 0), births.len + vanish.len);
43 44
44 // An empty list vanishes everything the host had, once every tile has 45 // An empty list vanishes everything the host had, once every tile has
45 // spent the grace. 46 // spent the grace.
46 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish); 47 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish, &gones);
47 vanish.len = 0; 48 vanish.len = 0;
48 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish); 49 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish, &gones);
49 try std.testing.expectEqual(@as(usize, 2), vanish.len); 50 try std.testing.expectEqual(@as(usize, 2), vanish.len);
50 try std.testing.expectEqual(@as(usize, 0), births.len); 51 try std.testing.expectEqual(@as(usize, 0), births.len);
51 } 52 }
@@ -58,23 +59,24 @@ test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it
58 var births = BirthNames{}; 59 var births = BirthNames{};
59 var binds = TileIdxs{}; 60 var binds = TileIdxs{};
60 var vanish = TileIdxs{}; 61 var vanish = TileIdxs{};
62 var gones = TileIdxs{};
61 // A poll can overtake a session's exit: the daemon has already dropped 63 // A poll can overtake a session's exit: the daemon has already dropped
62 // the name while the pump has not yet read its `exit_status`. Vanishing 64 // the name while the pump has not yet read its `exit_status`. Vanishing
63 // on that list loses the exit code — on a wall of one, `endedTile` skips 65 // on that list loses the exit code — on a wall of one, `endedTile` skips
64 // a tile that is no longer present and mux never leaves. 66 // a tile that is no longer present and mux never leaves.
65 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); 67 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones);
66 try std.testing.expectEqual(@as(usize, 0), vanish.len); 68 try std.testing.expectEqual(@as(usize, 0), vanish.len);
67 try std.testing.expect(tiles[1].missed_once); 69 try std.testing.expect(tiles[1].missed_once);
68 70
69 // A list that names it again forgives it: a tile does not accumulate 71 // A list that names it again forgives it: a tile does not accumulate
70 // misses across the seconds it is legitimately live. 72 // misses across the seconds it is legitimately live.
71 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &births, &binds, &vanish); 73 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &births, &binds, &vanish, &gones);
72 try std.testing.expectEqual(@as(usize, 0), vanish.len); 74 try std.testing.expectEqual(@as(usize, 0), vanish.len);
73 try std.testing.expect(!tiles[1].missed_once); 75 try std.testing.expect(!tiles[1].missed_once);
74 76
75 // Two consecutive lists without it, and it goes. 77 // Two consecutive lists without it, and it goes.
76 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); 78 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones);
77 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); 79 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones);
78 try std.testing.expectEqual(@as(usize, 1), vanish.len); 80 try std.testing.expectEqual(@as(usize, 1), vanish.len);
79 try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); 81 try std.testing.expectEqual(@as(usize, 1), vanish.get(0));
80 82
@@ -83,11 +85,51 @@ test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it
83 tiles[0].alive.store(false, .release); 85 tiles[0].alive.store(false, .release);
84 vanish.len = 0; 86 vanish.len = 0;
85 const only0 = [_]bool{ true, false, false }; 87 const only0 = [_]bool{ true, false, false };
86 wall_host.planHostDiff(&tiles, &only0, 3, 0, "", null, &births, &binds, &vanish); 88 wall_host.planHostDiff(&tiles, &only0, 3, 0, "", null, &births, &binds, &vanish, &gones);
87 try std.testing.expectEqual(@as(usize, 1), vanish.len); 89 try std.testing.expectEqual(@as(usize, 1), vanish.len);
88 try std.testing.expectEqual(@as(usize, 0), vanish.get(0)); 90 try std.testing.expectEqual(@as(usize, 0), vanish.get(0));
89 } 91 }
90 92
93 test "planHostDiff: a pending pane the reachable list disowns is gone, not vanished — and a later list still binds it" {
94 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
95 var tiles = fixture.diffFixture(&shared);
96 var present = [_]bool{ true, true, true };
97 // Panes 0 and 1 are sidecar restores: no pump ever ran, nothing dialed.
98 for (tiles[0..2]) |*t| {
99 t.pending = true;
100 t.state = .waiting;
101 t.alive.store(false, .release);
102 }
103
104 var births = BirthNames{};
105 var binds = TileIdxs{};
106 var vanish = TileIdxs{};
107 var gones = TileIdxs{};
108 // The reachable host answers with NEITHER saved session: both panes are
109 // gone-fodder, neither is vanish-fodder, and host 1's tile is untouched.
110 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish, &gones);
111 try std.testing.expectEqual(@as(usize, 0), vanish.len);
112 try std.testing.expectEqual(@as(usize, 2), gones.len);
113 try std.testing.expectEqual(@as(usize, 0), gones.get(0));
114 try std.testing.expectEqual(@as(usize, 1), gones.get(1));
115
116 // Once dressed, a pane is not re-listed every second: the poll comes
117 // back every 1s and a repaint per poll would flicker the bar.
118 tiles[0].state = .gone;
119 tiles[1].state = .gone;
120 gones.len = 0;
121 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish, &gones);
122 try std.testing.expectEqual(@as(usize, 0), gones.len);
123 try std.testing.expectEqual(@as(usize, 0), vanish.len);
124
125 // The dressing is reversible: a list that names session "a" binds pane 0
126 // (it stayed pending), and does not birth a twin.
127 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones);
128 try std.testing.expectEqual(@as(usize, 1), binds.len);
129 try std.testing.expectEqual(@as(usize, 0), binds.get(0));
130 try std.testing.expectEqual(@as(usize, 0), births.len);
131 }
132
91 test "planHostDiff: a vanished tile is not present, so the next list does not vanish it twice" { 133 test "planHostDiff: a vanished tile is not present, so the next list does not vanish it twice" {
92 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; 134 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
93 var tiles = fixture.diffFixture(&shared); 135 var tiles = fixture.diffFixture(&shared);
@@ -96,7 +138,8 @@ test "planHostDiff: a vanished tile is not present, so the next list does not va
96 var births = BirthNames{}; 138 var births = BirthNames{};
97 var binds = TileIdxs{}; 139 var binds = TileIdxs{};
98 var vanish = TileIdxs{}; 140 var vanish = TileIdxs{};
99 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); 141 var gones = TileIdxs{};
142 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones);
100 try std.testing.expectEqual(@as(usize, 0), vanish.len); 143 try std.testing.expectEqual(@as(usize, 0), vanish.len);
101 // ...and the name is not reborn either: the tile is gone, but the 144 // ...and the name is not reborn either: the tile is gone, but the
102 // daemon no longer lists it, so there is nothing to bring back. 145 // daemon no longer lists it, so there is nothing to bring back.
@@ -111,9 +154,10 @@ test "planHostDiff: the session this shell is inside is never born as a tile" {
111 var births = BirthNames{}; 154 var births = BirthNames{};
112 var binds = TileIdxs{}; 155 var binds = TileIdxs{};
113 var vanish = TileIdxs{}; 156 var vanish = TileIdxs{};
157 var gones = TileIdxs{};
114 // The daemon has "a", "b" and "self"; "self" is the shell mux runs in, 158 // The daemon has "a", "b" and "self"; "self" is the shell mux runs in,
115 // so a tile of it would paint into the grid it is reading. 159 // so a tile of it would paint into the grid it is reading.
116 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\nself\n", "self", &births, &binds, &vanish); 160 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\nself\n", "self", &births, &binds, &vanish, &gones);
117 try std.testing.expectEqual(@as(usize, 0), births.len); 161 try std.testing.expectEqual(@as(usize, 0), births.len);
118 try std.testing.expectEqual(@as(usize, 0), vanish.len); 162 try std.testing.expectEqual(@as(usize, 0), vanish.len);
119 } 163 }
@@ -126,13 +170,14 @@ test "planHostDiff: a name the wire grammar refuses never becomes a tile, howeve
126 var births = BirthNames{}; 170 var births = BirthNames{};
127 var binds = TileIdxs{}; 171 var binds = TileIdxs{};
128 var vanish = TileIdxs{}; 172 var vanish = TileIdxs{};
173 var gones = TileIdxs{};
129 // A whole reply may be `sessions_text_max`, so ONE "name" in it can be 174 // A whole reply may be `sessions_text_max`, so ONE "name" in it can be
130 // 1056 bytes; `encodeAttachNamed` memcpys the birth's name into a 175 // 1056 bytes; `encodeAttachNamed` memcpys the birth's name into a
131 // 32-byte tail behind nothing but an assert. Peer bytes are filtered 176 // 32-byte tail behind nothing but an assert. Peer bytes are filtered
132 // where they become a tile, not asserted about at the wire. 177 // where they become a tile, not asserted about at the wire.
133 const over_long = "x" ** (proto.session_name_max + 1); 178 const over_long = "x" ** (proto.session_name_max + 1);
134 const list = "a\n" ++ over_long ++ "\nhas space\n\x1b[2J\nc\n"; 179 const list = "a\n" ++ over_long ++ "\nhas space\n\x1b[2J\nc\n";
135 wall_host.planHostDiff(&tiles, &present, 3, 0, list, null, &births, &binds, &vanish); 180 wall_host.planHostDiff(&tiles, &present, 3, 0, list, null, &births, &binds, &vanish, &gones);
136 try std.testing.expectEqual(@as(usize, 1), births.len); 181 try std.testing.expectEqual(@as(usize, 1), births.len);
137 try std.testing.expectEqualStrings("c", births.get(0)); 182 try std.testing.expectEqualStrings("c", births.get(0));
138 } 183 }
@@ -442,15 +487,16 @@ test "planHostDiff: a creating tile whose attach has not landed yet is not vanis
442 var births = BirthNames{}; 487 var births = BirthNames{};
443 var binds = TileIdxs{}; 488 var binds = TileIdxs{};
444 var vanish = TileIdxs{}; 489 var vanish = TileIdxs{};
445 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); 490 var gones = TileIdxs{};
491 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones);
446 try std.testing.expectEqual(@as(usize, 0), vanish.len); 492 try std.testing.expectEqual(@as(usize, 0), vanish.len);
447 493
448 // ...and once its session exists, the same list does drop it — after 494 // ...and once its session exists, the same list does drop it — after
449 // the one list of grace a live pump gets: a session the daemon HAD and 495 // the one list of grace a live pump gets: a session the daemon HAD and
450 // no longer lists is gone. 496 // no longer lists is gone.
451 tiles[1].ever_up.store(true, .release); 497 tiles[1].ever_up.store(true, .release);
452 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); 498 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones);
453 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); 499 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones);
454 try std.testing.expectEqual(@as(usize, 1), vanish.len); 500 try std.testing.expectEqual(@as(usize, 1), vanish.len);
455 try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); 501 try std.testing.expectEqual(@as(usize, 1), vanish.get(0));
456 } 502 }
@@ -639,8 +685,8 @@ fn hasWord(argv: []const []const u8, want: []const u8) bool {
639 685
640 /// Three pending panes off one sidecar: "a" and "b" on host 0 and an 686 /// Three pending panes off one sidecar: "a" and "b" on host 0 and an
641 /// aliasing "a" on host 1, so a bind keyed on name alone is caught. 687 /// aliasing "a" on host 1, so a bind keyed on name alone is caught.
642 /// Dupes the names: every wall tile owns its copies, and the birth that 688 /// Dupes the names: every wall tile owns its copies, because a birth that
643 /// reuses a collapsed pane's digit frees them with the wall's allocator. 689 /// takes a freed digit frees the departed tile's with the wall's allocator.
644 fn seedBench(alloc: std.mem.Allocator, tiles: []Tile, present: []bool, live: *usize, shared: *Shared) !void { 690 fn seedBench(alloc: std.mem.Allocator, tiles: []Tile, present: []bool, live: *usize, shared: *Shared) !void {
645 try shared.tree.addFirst(0); 691 try shared.tree.addFirst(0);
646 try shared.tree.insert(0, 1); 692 try shared.tree.insert(0, 1);
@@ -679,8 +725,9 @@ test "planHostDiff: a pending pane the list names is a bind, not a birth - and t
679 var births = BirthNames{}; 725 var births = BirthNames{};
680 var binds = TileIdxs{}; 726 var binds = TileIdxs{};
681 var vanish = TileIdxs{}; 727 var vanish = TileIdxs{};
728 var gones = TileIdxs{};
682 // Host 0's whole list is pending panes: two binds, no birth, no twin. 729 // Host 0's whole list is pending panes: two binds, no birth, no twin.
683 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &births, &binds, &vanish); 730 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &births, &binds, &vanish, &gones);
684 try std.testing.expectEqual(@as(usize, 0), births.len); 731 try std.testing.expectEqual(@as(usize, 0), births.len);
685 try std.testing.expectEqual(@as(usize, 0), vanish.len); 732 try std.testing.expectEqual(@as(usize, 0), vanish.len);
686 try std.testing.expectEqual(@as(usize, 2), binds.len); 733 try std.testing.expectEqual(@as(usize, 2), binds.len);
@@ -689,37 +736,13 @@ test "planHostDiff: a pending pane the list names is a bind, not a birth - and t
689 // Host 1's "a" is its own pane, not host 0's; its "x" is a stranger 736 // Host 1's "a" is its own pane, not host 0's; its "x" is a stranger
690 // the sidecar never named, so it is still a birth. 737 // the sidecar never named, so it is still a birth.
691 binds.len = 0; 738 binds.len = 0;
692 wall_host.planHostDiff(&tiles, &present, 3, 1, "a\nx\n", null, &births, &binds, &vanish); 739 wall_host.planHostDiff(&tiles, &present, 3, 1, "a\nx\n", null, &births, &binds, &vanish, &gones);
693 try std.testing.expectEqual(@as(usize, 1), binds.len); 740 try std.testing.expectEqual(@as(usize, 1), binds.len);
694 try std.testing.expectEqual(@as(usize, 2), binds.get(0)); 741 try std.testing.expectEqual(@as(usize, 2), binds.get(0));
695 try std.testing.expectEqual(@as(usize, 1), births.len); 742 try std.testing.expectEqual(@as(usize, 1), births.len);
696 try std.testing.expectEqualStrings("x", births.get(0)); 743 try std.testing.expectEqualStrings("x", births.get(0));
697 } 744 }
698 745
699 test "planHostDiff: a pending pane the list does not name goes on the first list - it has no exit code to lose" {
700 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
701 defer arena.deinit();
702 const alloc = arena.allocator();
703 var shared: Shared = undefined;
704 fixture.stoppedWall(alloc, &shared);
705 defer shared.tree.deinit();
706 var tiles: [4]Tile = undefined;
707 var present = [_]bool{false} ** 4;
708 var live: usize = 0;
709 try seedBench(alloc, &tiles, &present, &live, &shared);
710 defer fixture.endPumps(tiles[0..live]);
711
712 var births = BirthNames{};
713 var binds = TileIdxs{};
714 var vanish = TileIdxs{};
715 // "b" is not on the host's own list: no grace, unlike a live pump's
716 // tile - the contrast the grace test above pins.
717 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish);
718 try std.testing.expectEqual(@as(usize, 1), binds.len);
719 try std.testing.expectEqual(@as(usize, 1), vanish.len);
720 try std.testing.expectEqual(@as(usize, 1), vanish.get(0));
721 }
722
723 test "applyHostList: binding a saved pane moves no rect and re-cuts nothing" { 746 test "applyHostList: binding a saved pane moves no rect and re-cuts nothing" {
724 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 747 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
725 defer arena.deinit(); 748 defer arena.deinit();
@@ -825,7 +848,7 @@ test "bindTile: the pane under the saved focus claims the terminal when it binds
825 try std.testing.expect(!tiles[0].claim_pending.load(.acquire)); 848 try std.testing.expect(!tiles[0].claim_pending.load(.acquire));
826 } 849 }
827 850
828 test "applyHostList: the list that collapses a gone pane is the list that births the newcomer - one re-cut" { 851 test "applyHostList: the list that disowns a seeded pane still births the newcomer beside it - one re-cut" {
829 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 852 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
830 defer arena.deinit(); 853 defer arena.deinit();
831 const alloc = arena.allocator(); 854 const alloc = arena.allocator();
@@ -845,18 +868,20 @@ test "applyHostList: the list that collapses a gone pane is the list that births
845 wall_layout.relayout(w, 0); 868 wall_layout.relayout(w, 0);
846 const gen = shared.repaint_gen.load(.acquire); 869 const gen = shared.repaint_gen.load(.acquire);
847 870
848 // One list: "a" binds, "b" is gone, "c" is new. One relayout serves 871 // One list: "a" binds, "b" is disowned, "c" is new. One relayout serves
849 // the collapse and the birth both. 872 // the whole list however many of the three it moved.
850 fixture.setList(&table[0], "a\nc\n"); 873 fixture.setList(&table[0], "a\nc\n");
851 wall_host.applyHostList(w, 0); 874 wall_host.applyHostList(w, 0);
852 875
853 try std.testing.expectEqual(gen + 1, shared.repaint_gen.load(.acquire)); 876 try std.testing.expectEqual(gen + 1, shared.repaint_gen.load(.acquire));
854 try std.testing.expect(!tiles[0].pending); 877 try std.testing.expect(!tiles[0].pending);
855 // The newcomer takes the collapsed pane's freed digit - reuse, not 878 // The disowned pane keeps its digit and its rect: the host said no, and
856 // renumbering, same as any departed tile's. 879 // a pane the user saved is not the daemon's to collapse.
857 try std.testing.expect(present[1] and !tiles[1].pending); 880 try std.testing.expect(present[1] and tiles[1].pending);
858 try std.testing.expectEqual(@as(usize, 3), live); 881 // So the newcomer takes the next FREE digit rather than a freed one.
859 try std.testing.expectEqualStrings("c", tiles[1].r.session); 882 try std.testing.expectEqual(@as(usize, 4), live);
883 try std.testing.expect(present[3]);
884 try std.testing.expectEqualStrings("c", tiles[3].r.session);
860 } 885 }
861 886
862 test "sendKeys: the mailbox of a pending pane holds what was typed for the pump the bind will start" { 887 test "sendKeys: the mailbox of a pending pane holds what was typed for the pump the bind will start" {
src/tui/wallview.zig
Old New
@@ -47,6 +47,13 @@ pub const State = enum {
47 /// A pending pane whose host's poll failed: the picker row's word, 47 /// A pending pane whose host's poll failed: the picker row's word,
48 /// worn on the pane, so the wall itself says which setup is dark. 48 /// worn on the pane, so the wall itself says which setup is dark.
49 @"unreachable", 49 @"unreachable",
50 /// A pending pane whose REACHABLE host answered without its session:
51 /// the host said no, not nothing. The pane stands — its rect is the
52 /// user's saved layout, not the daemon's state — until Enter re-creates
53 /// the session in place or `x` dismisses it. Reversible like
54 /// `unreachable`: the tile stays pending, so a list that names the
55 /// session later binds it.
56 gone,
50 connecting, 57 connecting,
51 up, 58 up,
52 reconnecting, 59 reconnecting,
@@ -61,6 +68,7 @@ pub const State = enum {
61 return switch (s) { 68 return switch (s) {
62 .waiting => "waiting", 69 .waiting => "waiting",
63 .@"unreachable" => "unreachable", 70 .@"unreachable" => "unreachable",
71 .gone => "gone",
64 .connecting => "connecting", 72 .connecting => "connecting",
65 .up => "up", 73 .up => "up",
66 .reconnecting => "reconnecting", 74 .reconnecting => "reconnecting",