2cad1d1c
feat: a host's list wakes the pane the sidecar promised it
a73x 2026-08-30 16:18
Commit message
src/tui/wall_host.zig
| Old | New | ||
|---|---|---|---|
| @@ -187,6 +187,7 @@ pub fn planHostDiff( | |||
| 187 | list: []const u8, | 187 | list: []const u8, |
| 188 | self_name: ?[]const u8, | 188 | self_name: ?[]const u8, |
| 189 | births: *BirthNames, | 189 | births: *BirthNames, |
| 190 | binds: *TileIdxs, | ||
| 190 | vanish: *TileIdxs, | 191 | vanish: *TileIdxs, |
| 191 | ) void { | 192 | ) void { |
| 192 | var it = std.mem.splitScalar(u8, list, '\n'); | 193 | var it = std.mem.splitScalar(u8, list, '\n'); |
| @@ -199,8 +200,11 @@ pub fn planHostDiff( | |||
| 199 | if (!proto.validSessionName(name)) continue; | 200 | if (!proto.validSessionName(name)) continue; |
| 200 | if (self_name) |self| if (std.mem.eql(u8, self, name)) continue; | 201 | if (self_name) |self| if (std.mem.eql(u8, self, name)) continue; |
| 201 | var found = false; | 202 | var found = false; |
| 202 | for (tiles[0..live], present[0..live]) |*t, p| { | 203 | for (tiles[0..live], present[0..live], 0..) |*t, p, i| { |
| 203 | if (p and ownedBy(t, host) and std.mem.eql(u8, proto.resolveName(t.r.session), name)) { | 204 | if (p and ownedBy(t, host) and std.mem.eql(u8, proto.resolveName(t.r.session), name)) { |
| 205 | // The sidecar's guess, confirmed by the host's own list: | ||
| 206 | // wake the pending pane rather than birthing a twin. | ||
| 207 | if (t.pending) binds.append(i); | ||
| 204 | found = true; | 208 | found = true; |
| 205 | break; | 209 | break; |
| 206 | } | 210 | } |
| @@ -328,8 +332,12 @@ pub fn applyHostList(w: Wall, hi: usize) void { | |||
| 328 | // would tear a wall down over one dropped packet. | 332 | // would tear a wall down over one dropped packet. |
| 329 | if (reachable) { | 333 | if (reachable) { |
| 330 | var births = BirthNames{}; | 334 | var births = BirthNames{}; |
| 335 | var binds = TileIdxs{}; | ||
| 331 | var vanish = TileIdxs{}; | 336 | var vanish = TileIdxs{}; |
| 332 | planHostDiff(w.liveTiles(), w.livePresent(), w.live.*, hi, list, h.self_name, &births, &vanish); | 337 | planHostDiff(w.liveTiles(), w.livePresent(), w.live.*, hi, list, h.self_name, &births, &binds, &vanish); |
| 338 | // Binds first, and they do not count as change: a pane whose rect | ||
| 339 | // was cut at seed time wakes in place, and re-cuts nothing. | ||
| 340 | for (binds.items[0..binds.len]) |bi| wv.bindTile(w, bi); | ||
| 333 | for (vanish.items[0..vanish.len]) |v| { | 341 | for (vanish.items[0..vanish.len]) |v| { |
| 334 | wv.vanishTile(w.liveTiles(), w.livePresent(), w.shared, v, null); | 342 | wv.vanishTile(w.liveTiles(), w.livePresent(), w.shared, v, null); |
| 335 | changed = true; | 343 | changed = true; |
src/tui/wall_test_host.zig
| Old | New | ||
|---|---|---|---|
| @@ -6,6 +6,8 @@ const hosts = @import("client").hosts; | |||
| 6 | const TmpDir = @import("testtmp").TmpDir; | 6 | const TmpDir = @import("testtmp").TmpDir; |
| 7 | const fixture = @import("wall_test_harness.zig"); | 7 | const fixture = @import("wall_test_harness.zig"); |
| 8 | const wall_host = @import("wall_host.zig"); | 8 | const wall_host = @import("wall_host.zig"); |
| 9 | const wall_layout = @import("wall_layout.zig"); | ||
| 10 | const wall_pump = @import("wall_pump.zig"); | ||
| 9 | const wv = @import("wallview.zig"); | 11 | const wv = @import("wallview.zig"); |
| 10 | const AddHost = wall_host.AddHost; | 12 | const AddHost = wall_host.AddHost; |
| 11 | const BirthNames = wall_host.BirthNames; | 13 | const BirthNames = wall_host.BirthNames; |
| @@ -21,28 +23,29 @@ test "planHostDiff: names the daemon has and the wall does not are births, tiles | |||
| 21 | var present = [_]bool{ true, true, true }; | 23 | var present = [_]bool{ true, true, true }; |
| 22 | 24 | ||
| 23 | var births = BirthNames{}; | 25 | var births = BirthNames{}; |
| 26 | var binds = TileIdxs{}; | ||
| 24 | var vanish = TileIdxs{}; | 27 | var vanish = TileIdxs{}; |
| 25 | // The birth is immediate; the vanish waits for a second list to agree | 28 | // The birth is immediate; the vanish waits for a second list to agree |
| 26 | // (see the grace test below), so this list is asked twice. | 29 | // (see the grace test below), so this list is asked twice. |
| 27 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &births, &vanish); | 30 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &births, &binds, &vanish); |
| 28 | try std.testing.expectEqual(@as(usize, 1), births.len); | 31 | try std.testing.expectEqual(@as(usize, 1), births.len); |
| 29 | try std.testing.expectEqualStrings("c", births.get(0)); | 32 | try std.testing.expectEqualStrings("c", births.get(0)); |
| 30 | births.len = 0; | 33 | births.len = 0; |
| 31 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &births, &vanish); | 34 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &births, &binds, &vanish); |
| 32 | try std.testing.expectEqual(@as(usize, 1), vanish.len); | 35 | try std.testing.expectEqual(@as(usize, 1), vanish.len); |
| 33 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); | 36 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); |
| 34 | 37 | ||
| 35 | // Host 1's "a" survives host 0's list saying nothing about it. | 38 | // Host 1's "a" survives host 0's list saying nothing about it. |
| 36 | births.len = 0; | 39 | births.len = 0; |
| 37 | vanish.len = 0; | 40 | vanish.len = 0; |
| 38 | wall_host.planHostDiff(&tiles, &present, 3, 1, "a\n", null, &births, &vanish); | 41 | wall_host.planHostDiff(&tiles, &present, 3, 1, "a\n", null, &births, &binds, &vanish); |
| 39 | try std.testing.expectEqual(@as(usize, 0), births.len + vanish.len); | 42 | try std.testing.expectEqual(@as(usize, 0), births.len + vanish.len); |
| 40 | 43 | ||
| 41 | // An empty list vanishes everything the host had, once every tile has | 44 | // An empty list vanishes everything the host had, once every tile has |
| 42 | // spent the grace. | 45 | // spent the grace. |
| 43 | wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &vanish); | 46 | wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish); |
| 44 | vanish.len = 0; | 47 | vanish.len = 0; |
| 45 | wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &vanish); | 48 | wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish); |
| 46 | try std.testing.expectEqual(@as(usize, 2), vanish.len); | 49 | try std.testing.expectEqual(@as(usize, 2), vanish.len); |
| 47 | try std.testing.expectEqual(@as(usize, 0), births.len); | 50 | try std.testing.expectEqual(@as(usize, 0), births.len); |
| 48 | } | 51 | } |
| @@ -53,24 +56,25 @@ test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it | |||
| 53 | var present = [_]bool{ true, true, true }; | 56 | var present = [_]bool{ true, true, true }; |
| 54 | 57 | ||
| 55 | var births = BirthNames{}; | 58 | var births = BirthNames{}; |
| 59 | var binds = TileIdxs{}; | ||
| 56 | var vanish = TileIdxs{}; | 60 | var vanish = TileIdxs{}; |
| 57 | // A poll can overtake a session's exit: the daemon has already dropped | 61 | // A poll can overtake a session's exit: the daemon has already dropped |
| 58 | // the name while the pump has not yet read its `exit_status`. Vanishing | 62 | // the name while the pump has not yet read its `exit_status`. Vanishing |
| 59 | // on that list loses the exit code — on a wall of one, `endedTile` skips | 63 | // on that list loses the exit code — on a wall of one, `endedTile` skips |
| 60 | // a tile that is no longer present and mux never leaves. | 64 | // a tile that is no longer present and mux never leaves. |
| 61 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &vanish); | 65 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); |
| 62 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 66 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 63 | try std.testing.expect(tiles[1].missed_once); | 67 | try std.testing.expect(tiles[1].missed_once); |
| 64 | 68 | ||
| 65 | // A list that names it again forgives it: a tile does not accumulate | 69 | // A list that names it again forgives it: a tile does not accumulate |
| 66 | // misses across the seconds it is legitimately live. | 70 | // misses across the seconds it is legitimately live. |
| 67 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &births, &vanish); | 71 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &births, &binds, &vanish); |
| 68 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 72 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 69 | try std.testing.expect(!tiles[1].missed_once); | 73 | try std.testing.expect(!tiles[1].missed_once); |
| 70 | 74 | ||
| 71 | // Two consecutive lists without it, and it goes. | 75 | // Two consecutive lists without it, and it goes. |
| 72 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &vanish); | 76 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); |
| 73 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &vanish); | 77 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); |
| 74 | try std.testing.expectEqual(@as(usize, 1), vanish.len); | 78 | try std.testing.expectEqual(@as(usize, 1), vanish.len); |
| 75 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); | 79 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); |
| 76 | 80 | ||
| @@ -79,7 +83,7 @@ test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it | |||
| 79 | tiles[0].alive.store(false, .release); | 83 | tiles[0].alive.store(false, .release); |
| 80 | vanish.len = 0; | 84 | vanish.len = 0; |
| 81 | const only0 = [_]bool{ true, false, false }; | 85 | const only0 = [_]bool{ true, false, false }; |
| 82 | wall_host.planHostDiff(&tiles, &only0, 3, 0, "", null, &births, &vanish); | 86 | wall_host.planHostDiff(&tiles, &only0, 3, 0, "", null, &births, &binds, &vanish); |
| 83 | try std.testing.expectEqual(@as(usize, 1), vanish.len); | 87 | try std.testing.expectEqual(@as(usize, 1), vanish.len); |
| 84 | try std.testing.expectEqual(@as(usize, 0), vanish.get(0)); | 88 | try std.testing.expectEqual(@as(usize, 0), vanish.get(0)); |
| 85 | } | 89 | } |
| @@ -90,8 +94,9 @@ test "planHostDiff: a vanished tile is not present, so the next list does not va | |||
| 90 | var present = [_]bool{ true, false, true }; | 94 | var present = [_]bool{ true, false, true }; |
| 91 | 95 | ||
| 92 | var births = BirthNames{}; | 96 | var births = BirthNames{}; |
| 97 | var binds = TileIdxs{}; | ||
| 93 | var vanish = TileIdxs{}; | 98 | var vanish = TileIdxs{}; |
| 94 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &vanish); | 99 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); |
| 95 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 100 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 96 | // ...and the name is not reborn either: the tile is gone, but the | 101 | // ...and the name is not reborn either: the tile is gone, but the |
| 97 | // daemon no longer lists it, so there is nothing to bring back. | 102 | // daemon no longer lists it, so there is nothing to bring back. |
| @@ -104,10 +109,11 @@ test "planHostDiff: the session this shell is inside is never born as a tile" { | |||
| 104 | var present = [_]bool{ true, true, true }; | 109 | var present = [_]bool{ true, true, true }; |
| 105 | 110 | ||
| 106 | var births = BirthNames{}; | 111 | var births = BirthNames{}; |
| 112 | var binds = TileIdxs{}; | ||
| 107 | var vanish = TileIdxs{}; | 113 | var vanish = TileIdxs{}; |
| 108 | // The daemon has "a", "b" and "self"; "self" is the shell mux runs in, | 114 | // The daemon has "a", "b" and "self"; "self" is the shell mux runs in, |
| 109 | // so a tile of it would paint into the grid it is reading. | 115 | // so a tile of it would paint into the grid it is reading. |
| 110 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\nself\n", "self", &births, &vanish); | 116 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\nself\n", "self", &births, &binds, &vanish); |
| 111 | try std.testing.expectEqual(@as(usize, 0), births.len); | 117 | try std.testing.expectEqual(@as(usize, 0), births.len); |
| 112 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 118 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 113 | } | 119 | } |
| @@ -118,6 +124,7 @@ test "planHostDiff: a name the wire grammar refuses never becomes a tile, howeve | |||
| 118 | var present = [_]bool{ true, true, true }; | 124 | var present = [_]bool{ true, true, true }; |
| 119 | 125 | ||
| 120 | var births = BirthNames{}; | 126 | var births = BirthNames{}; |
| 127 | var binds = TileIdxs{}; | ||
| 121 | var vanish = TileIdxs{}; | 128 | var vanish = TileIdxs{}; |
| 122 | // A whole reply may be `sessions_text_max`, so ONE "name" in it can be | 129 | // A whole reply may be `sessions_text_max`, so ONE "name" in it can be |
| 123 | // 1056 bytes; `encodeAttachNamed` memcpys the birth's name into a | 130 | // 1056 bytes; `encodeAttachNamed` memcpys the birth's name into a |
| @@ -125,7 +132,7 @@ test "planHostDiff: a name the wire grammar refuses never becomes a tile, howeve | |||
| 125 | // where they become a tile, not asserted about at the wire. | 132 | // where they become a tile, not asserted about at the wire. |
| 126 | const over_long = "x" ** (proto.session_name_max + 1); | 133 | const over_long = "x" ** (proto.session_name_max + 1); |
| 127 | const list = "a\n" ++ over_long ++ "\nhas space\n\x1b[2J\nc\n"; | 134 | const list = "a\n" ++ over_long ++ "\nhas space\n\x1b[2J\nc\n"; |
| 128 | wall_host.planHostDiff(&tiles, &present, 3, 0, list, null, &births, &vanish); | 135 | wall_host.planHostDiff(&tiles, &present, 3, 0, list, null, &births, &binds, &vanish); |
| 129 | try std.testing.expectEqual(@as(usize, 1), births.len); | 136 | try std.testing.expectEqual(@as(usize, 1), births.len); |
| 130 | try std.testing.expectEqualStrings("c", births.get(0)); | 137 | try std.testing.expectEqualStrings("c", births.get(0)); |
| 131 | } | 138 | } |
| @@ -429,16 +436,17 @@ test "planHostDiff: a creating tile whose attach has not landed yet is not vanis | |||
| 429 | var present = [_]bool{ true, true, true }; | 436 | var present = [_]bool{ true, true, true }; |
| 430 | 437 | ||
| 431 | var births = BirthNames{}; | 438 | var births = BirthNames{}; |
| 439 | var binds = TileIdxs{}; | ||
| 432 | var vanish = TileIdxs{}; | 440 | var vanish = TileIdxs{}; |
| 433 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &vanish); | 441 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); |
| 434 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 442 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 435 | 443 | ||
| 436 | // ...and once its session exists, the same list does drop it — after | 444 | // ...and once its session exists, the same list does drop it — after |
| 437 | // the one list of grace a live pump gets: a session the daemon HAD and | 445 | // the one list of grace a live pump gets: a session the daemon HAD and |
| 438 | // no longer lists is gone. | 446 | // no longer lists is gone. |
| 439 | tiles[1].ever_up.store(true, .release); | 447 | tiles[1].ever_up.store(true, .release); |
| 440 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &vanish); | 448 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); |
| 441 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &vanish); | 449 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); |
| 442 | try std.testing.expectEqual(@as(usize, 1), vanish.len); | 450 | try std.testing.expectEqual(@as(usize, 1), vanish.len); |
| 443 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); | 451 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); |
| 444 | } | 452 | } |
| @@ -624,3 +632,203 @@ fn hasWord(argv: []const []const u8, want: []const u8) bool { | |||
| 624 | for (argv) |w| if (std.mem.eql(u8, w, want)) return true; | 632 | for (argv) |w| if (std.mem.eql(u8, w, want)) return true; |
| 625 | return false; | 633 | return false; |
| 626 | } | 634 | } |
| 635 | |||
| 636 | /// Three pending panes off one sidecar: "a" and "b" on host 0 and an | ||
| 637 | /// aliasing "a" on host 1, so a bind keyed on name alone is caught. | ||
| 638 | /// Dupes the names: every wall tile owns its copies, and the birth that | ||
| 639 | /// reuses a collapsed pane's digit frees them with the wall's allocator. | ||
| 640 | fn seedBench(alloc: std.mem.Allocator, tiles: []Tile, present: []bool, live: *usize, shared: *Shared) !void { | ||
| 641 | try shared.tree.addFirst(0); | ||
| 642 | try shared.tree.insert(0, 1); | ||
| 643 | try shared.tree.insert(1, 2); | ||
| 644 | const rs = [_]wall_host.Resolved{ | ||
| 645 | .{ .target = .{ .sock = "/tmp/h0.sock" }, .label = "--sock /tmp/h0.sock#a", .session = "a" }, | ||
| 646 | .{ .target = .{ .sock = "/tmp/h0.sock" }, .label = "--sock /tmp/h0.sock#b", .session = "b" }, | ||
| 647 | .{ .target = .{ .sock = "/tmp/h1.sock" }, .label = "--sock /tmp/h1.sock#a", .session = "a" }, | ||
| 648 | }; | ||
| 649 | const hostof = [_]usize{ 0, 0, 1 }; | ||
| 650 | for (rs, hostof, 0..) |r, h, i| { | ||
| 651 | const owned: wall_host.Resolved = .{ | ||
| 652 | .target = r.target, | ||
| 653 | .label = try alloc.dupe(u8, r.label), | ||
| 654 | .session = try alloc.dupe(u8, r.session), | ||
| 655 | }; | ||
| 656 | try wv.seedTile(&tiles[i], owned, .{ .top = 0, .left = 0, .rows = 0, .cols = 0 }, shared, i, h); | ||
| 657 | present[i] = true; | ||
| 658 | } | ||
| 659 | live.* = 3; | ||
| 660 | } | ||
| 661 | |||
| 662 | test "planHostDiff: a pending pane the list names is a bind, not a birth - and the same name on another host is still a birth" { | ||
| 663 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | ||
| 664 | defer arena.deinit(); | ||
| 665 | const alloc = arena.allocator(); | ||
| 666 | var shared: Shared = undefined; | ||
| 667 | fixture.stoppedWall(alloc, &shared); | ||
| 668 | defer shared.tree.deinit(); | ||
| 669 | var tiles: [4]Tile = undefined; | ||
| 670 | var present = [_]bool{false} ** 4; | ||
| 671 | var live: usize = 0; | ||
| 672 | try seedBench(alloc, &tiles, &present, &live, &shared); | ||
| 673 | defer fixture.endPumps(tiles[0..live]); | ||
| 674 | |||
| 675 | var births = BirthNames{}; | ||
| 676 | var binds = TileIdxs{}; | ||
| 677 | var vanish = TileIdxs{}; | ||
| 678 | // Host 0's whole list is pending panes: two binds, no birth, no twin. | ||
| 679 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &births, &binds, &vanish); | ||
| 680 | try std.testing.expectEqual(@as(usize, 0), births.len); | ||
| 681 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | ||
| 682 | try std.testing.expectEqual(@as(usize, 2), binds.len); | ||
| 683 | try std.testing.expectEqual(@as(usize, 0), binds.get(0)); | ||
| 684 | try std.testing.expectEqual(@as(usize, 1), binds.get(1)); | ||
| 685 | // Host 1's "a" is its own pane, not host 0's; its "x" is a stranger | ||
| 686 | // the sidecar never named, so it is still a birth. | ||
| 687 | binds.len = 0; | ||
| 688 | wall_host.planHostDiff(&tiles, &present, 3, 1, "a\nx\n", null, &births, &binds, &vanish); | ||
| 689 | try std.testing.expectEqual(@as(usize, 1), binds.len); | ||
| 690 | try std.testing.expectEqual(@as(usize, 2), binds.get(0)); | ||
| 691 | try std.testing.expectEqual(@as(usize, 1), births.len); | ||
| 692 | try std.testing.expectEqualStrings("x", births.get(0)); | ||
| 693 | } | ||
| 694 | |||
| 695 | test "planHostDiff: a pending pane the list does not name goes on the first list - it has no exit code to lose" { | ||
| 696 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | ||
| 697 | defer arena.deinit(); | ||
| 698 | const alloc = arena.allocator(); | ||
| 699 | var shared: Shared = undefined; | ||
| 700 | fixture.stoppedWall(alloc, &shared); | ||
| 701 | defer shared.tree.deinit(); | ||
| 702 | var tiles: [4]Tile = undefined; | ||
| 703 | var present = [_]bool{false} ** 4; | ||
| 704 | var live: usize = 0; | ||
| 705 | try seedBench(alloc, &tiles, &present, &live, &shared); | ||
| 706 | defer fixture.endPumps(tiles[0..live]); | ||
| 707 | |||
| 708 | var births = BirthNames{}; | ||
| 709 | var binds = TileIdxs{}; | ||
| 710 | var vanish = TileIdxs{}; | ||
| 711 | // "b" is not on the host's own list: no grace, unlike a live pump's | ||
| 712 | // tile - the contrast the grace test above pins. | ||
| 713 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish); | ||
| 714 | try std.testing.expectEqual(@as(usize, 1), binds.len); | ||
| 715 | try std.testing.expectEqual(@as(usize, 1), vanish.len); | ||
| 716 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); | ||
| 717 | } | ||
| 718 | |||
| 719 | test "applyHostList: binding a saved pane moves no rect and re-cuts nothing" { | ||
| 720 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | ||
| 721 | defer arena.deinit(); | ||
| 722 | const alloc = arena.allocator(); | ||
| 723 | var shared: Shared = undefined; | ||
| 724 | fixture.stoppedWall(alloc, &shared); | ||
| 725 | defer shared.tree.deinit(); | ||
| 726 | var tiles: [4]Tile = undefined; | ||
| 727 | var present = [_]bool{false} ** 4; | ||
| 728 | var live: usize = 0; | ||
| 729 | try seedBench(alloc, &tiles, &present, &live, &shared); | ||
| 730 | defer fixture.endPumps(tiles[0..live]); | ||
| 731 | var table = [_]Host{ | ||
| 732 | fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"), | ||
| 733 | fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"), | ||
| 734 | }; | ||
| 735 | const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table); | ||
| 736 | // The seed's own cut, then quiet: what the list must not disturb. | ||
| 737 | wall_layout.relayout(w, 0); | ||
| 738 | for (tiles[0..3]) |*t| t.resize_pending = false; | ||
| 739 | const gen = shared.repaint_gen.load(.acquire); | ||
| 740 | var rects: [3]@TypeOf(tiles[0].rect) = undefined; | ||
| 741 | for (tiles[0..3], 0..) |t, i| rects[i] = t.rect; | ||
| 742 | |||
| 743 | fixture.setList(&table[0], "a\nb\n"); | ||
| 744 | wall_host.applyHostList(w, 0); | ||
| 745 | |||
| 746 | try std.testing.expectEqual(gen, shared.repaint_gen.load(.acquire)); | ||
| 747 | for (tiles[0..3], 0..) |t, i| { | ||
| 748 | try std.testing.expectEqual(rects[i], t.rect); | ||
| 749 | try std.testing.expect(!t.resize_pending); | ||
| 750 | } | ||
| 751 | // Bound panes joined - the daemon already has these sessions, and a | ||
| 752 | // sized attach on a live one would resize somebody. | ||
| 753 | try std.testing.expect(!tiles[0].pending and !tiles[1].pending); | ||
| 754 | try std.testing.expect(!tiles[0].creates and !tiles[1].creates); | ||
| 755 | // Host 1's aliasing "a" is still waiting for ITS host. | ||
| 756 | try std.testing.expect(tiles[2].pending); | ||
| 757 | } | ||
| 758 | |||
| 759 | test "bindTile: the pane under the saved focus claims the terminal when it binds, its neighbour does not" { | ||
| 760 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | ||
| 761 | defer arena.deinit(); | ||
| 762 | const alloc = arena.allocator(); | ||
| 763 | var shared: Shared = undefined; | ||
| 764 | fixture.stoppedWall(alloc, &shared); | ||
| 765 | defer shared.tree.deinit(); | ||
| 766 | var tiles: [4]Tile = undefined; | ||
| 767 | var present = [_]bool{false} ** 4; | ||
| 768 | var live: usize = 0; | ||
| 769 | try seedBench(alloc, &tiles, &present, &live, &shared); | ||
| 770 | defer fixture.endPumps(tiles[0..live]); | ||
| 771 | var table = [_]Host{ | ||
| 772 | fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"), | ||
| 773 | fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"), | ||
| 774 | }; | ||
| 775 | const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table); | ||
| 776 | // The saved focus record put the user on pane 1 before any host spoke. | ||
| 777 | shared.sel = 1; | ||
| 778 | fixture.setList(&table[0], "a\nb\n"); | ||
| 779 | wall_host.applyHostList(w, 0); | ||
| 780 | try std.testing.expect(tiles[1].claim_pending.load(.acquire)); | ||
| 781 | try std.testing.expect(!tiles[0].claim_pending.load(.acquire)); | ||
| 782 | } | ||
| 783 | |||
| 784 | test "applyHostList: the list that collapses a gone pane is the list that births the newcomer - one re-cut" { | ||
| 785 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | ||
| 786 | defer arena.deinit(); | ||
| 787 | const alloc = arena.allocator(); | ||
| 788 | var shared: Shared = undefined; | ||
| 789 | fixture.stoppedWall(alloc, &shared); | ||
| 790 | defer shared.tree.deinit(); | ||
| 791 | var tiles: [8]Tile = undefined; | ||
| 792 | var present = [_]bool{false} ** 8; | ||
| 793 | var live: usize = 0; | ||
| 794 | try seedBench(alloc, &tiles, &present, &live, &shared); | ||
| 795 | defer fixture.endPumps(tiles[0..live]); | ||
| 796 | var table = [_]Host{ | ||
| 797 | fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"), | ||
| 798 | fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"), | ||
| 799 | }; | ||
| 800 | const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table); | ||
| 801 | wall_layout.relayout(w, 0); | ||
| 802 | const gen = shared.repaint_gen.load(.acquire); | ||
| 803 | |||
| 804 | // One list: "a" binds, "b" is gone, "c" is new. One relayout serves | ||
| 805 | // the collapse and the birth both. | ||
| 806 | fixture.setList(&table[0], "a\nc\n"); | ||
| 807 | wall_host.applyHostList(w, 0); | ||
| 808 | |||
| 809 | try std.testing.expectEqual(gen + 1, shared.repaint_gen.load(.acquire)); | ||
| 810 | try std.testing.expect(!tiles[0].pending); | ||
| 811 | // The newcomer takes the collapsed pane's freed digit - reuse, not | ||
| 812 | // renumbering, same as any departed tile's. | ||
| 813 | try std.testing.expect(present[1] and !tiles[1].pending); | ||
| 814 | try std.testing.expectEqual(@as(usize, 3), live); | ||
| 815 | try std.testing.expectEqualStrings("c", tiles[1].r.session); | ||
| 816 | } | ||
| 817 | |||
| 818 | test "sendKeys: the mailbox of a pending pane holds what was typed for the pump the bind will start" { | ||
| 819 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | ||
| 820 | defer arena.deinit(); | ||
| 821 | const alloc = arena.allocator(); | ||
| 822 | var shared: Shared = undefined; | ||
| 823 | fixture.stoppedWall(alloc, &shared); | ||
| 824 | defer shared.tree.deinit(); | ||
| 825 | var tiles: [4]Tile = undefined; | ||
| 826 | var present = [_]bool{false} ** 4; | ||
| 827 | var live: usize = 0; | ||
| 828 | try seedBench(alloc, &tiles, &present, &live, &shared); | ||
| 829 | defer fixture.endPumps(tiles[0..live]); | ||
| 830 | |||
| 831 | wv.sendKeys(&tiles[1], "typed-early"); | ||
| 832 | var buf: [64]u8 = undefined; | ||
| 833 | try std.testing.expectEqualStrings("typed-early", wall_pump.takeKeys(&tiles[1], &buf)); | ||
| 834 | } | ||
src/tui/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -1001,6 +1001,19 @@ fn initTile(t: *Tile, r: Resolved, s: layout.Rect, shared: *Shared, idx: usize, | |||
| 1001 | }; | 1001 | }; |
| 1002 | } | 1002 | } |
| 1003 | 1003 | ||
| 1004 | /// The one maker of a pending tile: a slot whose rect the seed already | ||
| 1005 | /// cut, wearing the sidecar's names, that nothing will dial. `pump_done` | ||
| 1006 | /// is true because there is no thread to wait for; `bindTile` undoes it. | ||
| 1007 | pub fn seedTile(t: *Tile, r: Resolved, s: layout.Rect, shared: *Shared, idx: usize, host: usize) !void { | ||
| 1008 | try initTile(t, r, s, shared, idx, .fresh); | ||
| 1009 | t.pending = true; | ||
| 1010 | t.host = host; | ||
| 1011 | t.state = .waiting; | ||
| 1012 | t.creates = false; | ||
| 1013 | t.alive.store(false, .release); | ||
| 1014 | t.pump_done.store(true, .release); | ||
| 1015 | } | ||
| 1016 | |||
| 1004 | pub fn spawnPump(t: *Tile) void { | 1017 | pub fn spawnPump(t: *Tile) void { |
| 1005 | // The one arming point is also the one refusal: a remembered shape | 1018 | // The one arming point is also the one refusal: a remembered shape |
| 1006 | // never dials, so the sidecar cannot re-create a session. | 1019 | // never dials, so the sidecar cannot re-create a session. |
| @@ -1079,6 +1092,26 @@ const Birth = struct { | |||
| 1079 | borrowed: bool = false, | 1092 | borrowed: bool = false, |
| 1080 | }; | 1093 | }; |
| 1081 | 1094 | ||
| 1095 | /// A pending pane the host's own list confirmed: wake it in place. The | ||
| 1096 | /// rect was cut at seed time, so binding moves nothing and re-cuts | ||
| 1097 | /// nothing — which is the whole point of seeding. | ||
| 1098 | pub fn bindTile(w: Wall, i: usize) void { | ||
| 1099 | const t = &w.tiles[i]; | ||
| 1100 | w.shared.paint_mu.lock(); | ||
| 1101 | t.pending = false; | ||
| 1102 | t.state = .connecting; | ||
| 1103 | w.shared.paint_mu.unlock(); | ||
| 1104 | t.end_seen = false; | ||
| 1105 | // The saved focus may be sitting on this pane; the claim is what makes | ||
| 1106 | // it reach the terminal, since no `setFocus` moves onto it. | ||
| 1107 | if (i == w.shared.sel) t.claim_pending.store(true, .release); | ||
| 1108 | // False must land before the thread exists: the pump only ever stores | ||
| 1109 | // true, and `freeSlot` reads it as "no thread still holds this slot". | ||
| 1110 | t.pump_done.store(false, .release); | ||
| 1111 | t.alive.store(true, .release); | ||
| 1112 | spawnPump(t); | ||
| 1113 | } | ||
| 1114 | |||
| 1082 | /// Every road onto a running wall — chord, fold, prompt — one body. | 1115 | /// Every road onto a running wall — chord, fold, prompt — one body. |
| 1083 | pub fn birthTile(w: Wall, b: Birth) ?usize { | 1116 | pub fn birthTile(w: Wall, b: Birth) ?usize { |
| 1084 | return birthTileOrRefuse(w, b) catch null; | 1117 | return birthTileOrRefuse(w, b) catch null; |