6cdc66e5
feat: a disowned seeded pane plans as gone, not vanish
a73x 2026-09-01 09:56
Commit message
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", |