a73x

1fc81c25

feat: the poll grades panes and adds none; the layout is the only source of tiles

a73x   2026-09-03 05:20

Commit message
feat: the poll grades panes and adds none; the layout is the only source of tiles

src/tui/wall_host.zig
Old New
@@ -1,7 +1,9 @@
1 //! The wall's daemons: the hosts file's lines resolved to targets, the table 1 //! The wall's daemons: the hosts file's lines resolved to targets, the table
2 //! that holds them, and the poller that asks each for its live sessions once a 2 //! that holds them, and the poller that asks each for its live sessions once a
3 //! second. A host contributes TILES, never a tile of its own — `applyHostList` 3 //! second. A host contributes nothing but the GRADE of the panes the layout
4 //! is where a poll's answer becomes births and vanishings. 4 //! already gave it — `applyHostList` binds a pending pane whose session the
5 //! list names, marks `gone` one it does not, and vanishes a live pane whose
6 //! shell has ended. The layout is the only source of tiles.
5 const std = @import("std"); 7 const std = @import("std");
6 const proto = @import("term").protocol; 8 const proto = @import("term").protocol;
7 const client = @import("client"); 9 const client = @import("client");
@@ -20,9 +22,9 @@ pub const Resolved = struct {
20 label: []const u8, 22 label: []const u8,
21 session: []const u8, 23 session: []const u8,
22 /// Whether this tile's attaches offer this client's ssh-agent (`mux 24 /// Whether this tile's attaches offer this client's ssh-agent (`mux
23 /// -A`). Per tile rather than per wall: a tile the user never named — 25 /// -A`). Per tile rather than per wall: a tile whose host the user did
24 /// one a host's poll turned up — must not hand a stranger's host the 26 /// not offer the agent to must not hand that stranger the keys, even
25 /// keys, even while an `-A` tile is on the same wall. 27 /// while an `-A` tile is on the same wall.
26 agent: bool = false, 28 agent: bool = false,
27 }; 29 };
28 30
@@ -75,10 +77,10 @@ pub fn addHost(
75 idle_ms: u32, 77 idle_ms: u32,
76 path: ?[]const u8, 78 path: ?[]const u8,
77 ) AddHost { 79 ) AddHost {
78 // No tile is born here: the host's first poll is what turns its 80 // No tile is born here, and none comes later either: a host is a row,
79 // sessions into tiles, so the prompt cannot conjure a session that is 81 // and Enter on that row is what puts a pane on the wall. Twice is once
80 // not there. Twice is once — a second poller on the same daemon would 82 // — a second row for one daemon is a second poller grading the same
81 // tile every one of its sessions again. 83 // panes, and a picker naming one machine twice.
82 for (host_table[0..hosts_live.*], 0..) |*h, hi| { 84 for (host_table[0..hosts_live.*], 0..) |*h, hi| {
83 // A forgotten host is not on the wall, so its slot must not refuse 85 // A forgotten host is not on the wall, so its slot must not refuse
84 // the spelling back: `x` then `a` on the same host is one of the 86 // the spelling back: `x` then `a` on the same host is one of the
@@ -138,19 +140,17 @@ pub fn addHost(
138 return .{ .added = at }; 140 return .{ .added = at };
139 } 141 }
140 142
141 /// A diff's plan, bounded by the wall's own capacity so a daemon with more 143 /// A diff's plan, bounded by the wall's own capacity so an append can never
142 /// sessions than the wall can hold is COUNTED rather than overrunning. 144 /// overrun. Every plan here is a list of TILE indices, so the bound cannot
145 /// bite — a wall holds at most `wv.max_tiles` of them — and the guard is
146 /// what keeps that true if a caller ever hands it something else.
143 fn Fixed(comptime T: type) type { 147 fn Fixed(comptime T: type) type {
144 return struct { 148 return struct {
145 items: [wv.max_tiles]T = undefined, 149 items: [wv.max_tiles]T = undefined,
146 len: usize = 0, 150 len: usize = 0,
147 dropped: usize = 0,
148 151
149 fn append(self: *@This(), v: T) void { 152 fn append(self: *@This(), v: T) void {
150 if (self.len == self.items.len) { 153 if (self.len == self.items.len) return;
151 self.dropped += 1;
152 return;
153 }
154 self.items[self.len] = v; 154 self.items[self.len] = v;
155 self.len += 1; 155 self.len += 1;
156 } 156 }
@@ -161,7 +161,6 @@ fn Fixed(comptime T: type) type {
161 }; 161 };
162 } 162 }
163 163
164 pub const BirthNames = Fixed([]const u8);
165 pub const TileIdxs = Fixed(usize); 164 pub const TileIdxs = Fixed(usize);
166 165
167 /// Only a host's own sessions are its list's to keep or to drop. 166 /// Only a host's own sessions are its list's to keep or to drop.
@@ -170,9 +169,10 @@ pub fn ownedBy(t: *const Tile, host: usize) bool {
170 return h == host; 169 return h == host;
171 } 170 }
172 171
173 /// The whole of "a host's live sessions are its tiles", per HOST so that 172 /// The whole of "a host GRADES the panes the layout gave it", per HOST so
174 /// two daemons may share a session name. Pure: the rule is testable 173 /// that two daemons may share a session name. A name in the list that no
175 /// without a daemon. 174 /// pane spells is nobody's business: the layout is the only source of tiles.
175 /// Pure: the rule is testable without a daemon.
176 pub fn planHostDiff( 176 pub fn planHostDiff(
177 tiles: []Tile, 177 tiles: []Tile,
178 present: []const bool, 178 present: []const bool,
@@ -180,28 +180,27 @@ pub fn planHostDiff(
180 host: usize, 180 host: usize,
181 list: []const u8, 181 list: []const u8,
182 self_name: ?[]const u8, 182 self_name: ?[]const u8,
183 births: *BirthNames,
184 binds: *TileIdxs, 183 binds: *TileIdxs,
185 vanish: *TileIdxs, 184 vanish: *TileIdxs,
186 gones: *TileIdxs, 185 gones: *TileIdxs,
187 ) void { 186 ) void {
188 // `proto.sessionsIter` carries the trust policy: a peer's reply is bounded 187 // `proto.sessionsIter` carries the trust policy: a peer's reply is bounded
189 // only in total, so it yields only lines that are names — which is what 188 // only in total, so it yields only lines that are names — which is what
190 // makes a birth's name safe to hand to `encodeAttachNamed`. 189 // keeps a hostile line from matching a pane by accident.
191 var it = proto.sessionsIter(list); 190 var it = proto.sessionsIter(list);
192 while (it.next()) |name| { 191 while (it.next()) |name| {
192 // The shell mux is running inside cannot be a pane of the wall it is
193 // painting, so its name never wakes one — `seedLayout` refuses such
194 // a leaf, and this is the poll's own half of that refusal.
193 if (self_name) |self| if (std.mem.eql(u8, self, name)) continue; 195 if (self_name) |self| if (std.mem.eql(u8, self, name)) continue;
194 var found = false;
195 for (tiles[0..live], present[0..live], 0..) |*t, p, i| { 196 for (tiles[0..live], present[0..live], 0..) |*t, p, i| {
196 if (p and ownedBy(t, host) and std.mem.eql(u8, proto.resolveName(t.r.session), name)) { 197 if (p and ownedBy(t, host) and std.mem.eql(u8, proto.resolveName(t.r.session), name)) {
197 // The sidecar's guess, confirmed by the host's own list: 198 // The layout's guess, confirmed by the host's own list:
198 // wake the pending pane rather than birthing a twin. 199 // wake the pending pane where it already stands.
199 if (t.pending) binds.append(i); 200 if (t.pending) binds.append(i);
200 found = true;
201 break; 201 break;
202 } 202 }
203 } 203 }
204 if (!found) births.append(name);
205 } 204 }
206 for (tiles[0..live], present[0..live], 0..) |*t, p, i| { 205 for (tiles[0..live], present[0..live], 0..) |*t, p, i| {
207 if (!p or !ownedBy(t, host)) continue; 206 if (!p or !ownedBy(t, host)) continue;
@@ -286,8 +285,9 @@ pub fn pollHost(h: *Host) void {
286 h.poller_done.store(true, .release); 285 h.poller_done.store(true, .release);
287 } 286 }
288 287
289 /// A chord that births asks its host for a list NOW: a session made by 288 /// A chord that births asks its host for a list NOW: the tile is already on
290 /// `c` or `:` must not wait out the poll interval to become a tile. 289 /// the wall, and it is graded against a list that must name the session the
290 /// chord just made rather than the one from a second ago.
291 pub fn pokeHost(host_table: []Host, t: *const Tile) void { 291 pub fn pokeHost(host_table: []Host, t: *const Tile) void {
292 const hi = t.host orelse return; 292 const hi = t.host orelse return;
293 if (hi < host_table.len) host_table[hi].poll.poke.store(true, .release); 293 if (hi < host_table.len) host_table[hi].poll.poke.store(true, .release);
@@ -372,12 +372,11 @@ pub fn applyHostList(w: Wall, hi: usize) void {
372 // tiles, which reconnect on their own; vanishing them on a failed poll 372 // tiles, which reconnect on their own; vanishing them on a failed poll
373 // would tear a wall down over one dropped packet. 373 // would tear a wall down over one dropped packet.
374 if (reachable) { 374 if (reachable) {
375 var births = BirthNames{};
376 var binds = TileIdxs{}; 375 var binds = TileIdxs{};
377 var vanish = TileIdxs{}; 376 var vanish = TileIdxs{};
378 // Where the seeded panes this list disowns land. 377 // Where the seeded panes this list disowns land.
379 var gones = TileIdxs{}; 378 var gones = TileIdxs{};
380 planHostDiff(w.liveTiles(), w.livePresent(), w.live.*, hi, list, h.self_name, &births, &binds, &vanish, &gones); 379 planHostDiff(w.liveTiles(), w.livePresent(), w.live.*, hi, list, h.self_name, &binds, &vanish, &gones);
381 // Binds first, and they do not count as change: a pane whose rect 380 // Binds first, and they do not count as change: a pane whose rect
382 // was cut at seed time wakes in place, and re-cuts nothing. 381 // was cut at seed time wakes in place, and re-cuts nothing.
383 for (binds.items[0..binds.len]) |bi| wv.bindTile(w, bi); 382 for (binds.items[0..binds.len]) |bi| wv.bindTile(w, bi);
@@ -396,39 +395,10 @@ pub fn applyHostList(w: Wall, hi: usize) void {
396 wv.vanishTile(w.liveTiles(), w.livePresent(), w.shared, v, null); 395 wv.vanishTile(w.liveTiles(), w.livePresent(), w.shared, v, null);
397 changed = true; 396 changed = true;
398 } 397 }
399 var placed: usize = 0; 398 // A name the list carries that no pane spells ends here, unsaid: the
400 // The tile the NEXT birth sits beside: the focus first, then the one just 399 // user's layout says which sessions this wall shows, and a session
401 // born. `insert` puts a leaf immediately after its anchor, so anchoring 400 // somebody else started on the same daemon is not this wall's to add
402 // every birth at the focus lays {b, c} out as c, b. 401 // — nor is it something to apologise for in a notice.
403 var anchor = wall_layout.anchorTile(w.livePresent(), w.shared.sel);
404 // Stops at the FIRST refusal rather than retrying each name: the wall
405 // refuses for a reason that holds for the whole list, and this list comes
406 // back every second — a per-name retry is a flatten and an undo forever.
407 while (placed < births.len) : (placed += 1) {
408 const at = wv.birthTile(w, .{
409 // Joins, never creates: the daemon already has this session,
410 // and a sized attach on a live one would resize somebody.
411 // The name is this poll's reply buffer until the wall takes
412 // the tile — see `Birth.borrowed`.
413 .r = .{ .target = h.spec.target, .label = "", .session = births.get(placed) },
414 .from = anchor,
415 .place = .beside_focus,
416 .creates = false,
417 .born_from = null,
418 .host = hi,
419 .borrowed = true,
420 }) orelse break;
421 anchor = at;
422 wv.spawnPump(&w.tiles[at]);
423 changed = true;
424 }
425 const unplaced = births.dropped + (births.len - placed);
426 // Said out loud rather than dropped: a wall showing a PREFIX of a
427 // daemon's sessions is a wall lying about what it is.
428 if (unplaced > 0) {
429 var buf: [48]u8 = undefined;
430 wv.setNoticeIdle(w.shared, std.fmt.bufPrint(&buf, "[+{d} not shown]", .{unplaced}) catch "[not shown]");
431 }
432 applyDrift(w, hi, list); 402 applyDrift(w, hi, list);
433 } else dressSilent(w, hi); 403 } else dressSilent(w, hi);
434 if ((!had_focus or w.shared.sel >= w.live.* or !w.present[w.shared.sel]) and 404 if ((!had_focus or w.shared.sel >= w.live.* or !w.present[w.shared.sel]) and
src/tui/wall_test_host.zig
Old New
@@ -10,45 +10,73 @@ const wall_layout = @import("wall_layout.zig");
10 const wall_pump = @import("wall_pump.zig"); 10 const wall_pump = @import("wall_pump.zig");
11 const wv = @import("wallview.zig"); 11 const wv = @import("wallview.zig");
12 const AddHost = wall_host.AddHost; 12 const AddHost = wall_host.AddHost;
13 const BirthNames = wall_host.BirthNames;
14 const Host = wall_host.Host; 13 const Host = wall_host.Host;
15 const HostSpec = client.HostSpec; 14 const HostSpec = client.HostSpec;
16 const Shared = wv.Shared; 15 const Shared = wv.Shared;
17 const Tile = wv.Tile; 16 const Tile = wv.Tile;
18 const TileIdxs = wall_host.TileIdxs; 17 const TileIdxs = wall_host.TileIdxs;
19 18
20 test "planHostDiff: names the daemon has and the wall does not are births, tiles it dropped vanish, and another host's same name is untouched" { 19 /// Panes the layout seated and a poll already bound: live tiles, in the
20 /// tree, whose pump never ran — so `alive` is whatever the test states and
21 /// there is no thread to join. The wall's tiles come from the layout now,
22 /// so a poll test that wants tiles has to seat them itself.
23 fn seatBound(
24 tiles: []Tile,
25 present: []bool,
26 live: *usize,
27 shared: *Shared,
28 rs: []const wall_host.Resolved,
29 hostof: []const usize,
30 ) !void {
31 for (rs, hostof, 0..) |r, h, i| {
32 tiles[i] = .{
33 .r = r,
34 .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
35 .shared = shared,
36 .idx = i,
37 .wake_r = -1,
38 .wake_w = -1,
39 .host = h,
40 .alive = std.atomic.Value(bool).init(false),
41 };
42 present[i] = true;
43 if (i == 0) {
44 try shared.tree.addFirst(0);
45 } else {
46 try shared.tree.insert(@intCast(i - 1), @intCast(i));
47 }
48 }
49 live.* = rs.len;
50 }
51
52 test "planHostDiff: tiles the daemon dropped vanish, and another host's same name is untouched" {
21 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; 53 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
22 var tiles = fixture.diffFixture(&shared); 54 var tiles = fixture.diffFixture(&shared);
23 var present = [_]bool{ true, true, true }; 55 var present = [_]bool{ true, true, true };
24 56
25 var births = BirthNames{};
26 var binds = TileIdxs{}; 57 var binds = TileIdxs{};
27 var vanish = TileIdxs{}; 58 var vanish = TileIdxs{};
28 var gones = TileIdxs{}; 59 var gones = TileIdxs{};
29 // The birth is immediate; the vanish waits for a second list to agree 60 // The vanish waits for a second list to agree (see the grace test
30 // (see the grace test below), so this list is asked twice. 61 // below), so this list is asked twice. "c" is a session of host 0's
31 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &births, &binds, &vanish, &gones); 62 // that no pane spells, and it stays nothing to this wall.
32 try std.testing.expectEqual(@as(usize, 1), births.len); 63 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &binds, &vanish, &gones);
33 try std.testing.expectEqualStrings("c", births.get(0)); 64 try std.testing.expectEqual(@as(usize, 0), vanish.len);
34 births.len = 0; 65 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &binds, &vanish, &gones);
35 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &births, &binds, &vanish, &gones);
36 try std.testing.expectEqual(@as(usize, 1), vanish.len); 66 try std.testing.expectEqual(@as(usize, 1), vanish.len);
37 try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); 67 try std.testing.expectEqual(@as(usize, 1), vanish.get(0));
38 68
39 // Host 1's "a" survives host 0's list saying nothing about it. 69 // Host 1's "a" survives host 0's list saying nothing about it.
40 births.len = 0;
41 vanish.len = 0; 70 vanish.len = 0;
42 wall_host.planHostDiff(&tiles, &present, 3, 1, "a\n", null, &births, &binds, &vanish, &gones); 71 wall_host.planHostDiff(&tiles, &present, 3, 1, "a\n", null, &binds, &vanish, &gones);
43 try std.testing.expectEqual(@as(usize, 0), births.len + vanish.len); 72 try std.testing.expectEqual(@as(usize, 0), vanish.len);
44 73
45 // An empty list vanishes everything the host had, once every tile has 74 // An empty list vanishes everything the host had, once every tile has
46 // spent the grace. 75 // spent the grace.
47 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish, &gones); 76 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &binds, &vanish, &gones);
48 vanish.len = 0; 77 vanish.len = 0;
49 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish, &gones); 78 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &binds, &vanish, &gones);
50 try std.testing.expectEqual(@as(usize, 2), vanish.len); 79 try std.testing.expectEqual(@as(usize, 2), vanish.len);
51 try std.testing.expectEqual(@as(usize, 0), births.len);
52 } 80 }
53 81
54 test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it" { 82 test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it" {
@@ -56,7 +84,6 @@ test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it
56 var tiles = fixture.diffFixture(&shared); 84 var tiles = fixture.diffFixture(&shared);
57 var present = [_]bool{ true, true, true }; 85 var present = [_]bool{ true, true, true };
58 86
59 var births = BirthNames{};
60 var binds = TileIdxs{}; 87 var binds = TileIdxs{};
61 var vanish = TileIdxs{}; 88 var vanish = TileIdxs{};
62 var gones = TileIdxs{}; 89 var gones = TileIdxs{};
@@ -64,19 +91,19 @@ test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it
64 // the name while the pump has not yet read its `exit_status`. Vanishing 91 // the name while the pump has not yet read its `exit_status`. Vanishing
65 // on that list loses the exit code — on a wall of one, `endedTile` skips 92 // on that list loses the exit code — on a wall of one, `endedTile` skips
66 // a tile that is no longer present and mux never leaves. 93 // a tile that is no longer present and mux never leaves.
67 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); 94 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
68 try std.testing.expectEqual(@as(usize, 0), vanish.len); 95 try std.testing.expectEqual(@as(usize, 0), vanish.len);
69 try std.testing.expect(tiles[1].missed_once); 96 try std.testing.expect(tiles[1].missed_once);
70 97
71 // A list that names it again forgives it: a tile does not accumulate 98 // A list that names it again forgives it: a tile does not accumulate
72 // misses across the seconds it is legitimately live. 99 // misses across the seconds it is legitimately live.
73 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &births, &binds, &vanish, &gones); 100 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &binds, &vanish, &gones);
74 try std.testing.expectEqual(@as(usize, 0), vanish.len); 101 try std.testing.expectEqual(@as(usize, 0), vanish.len);
75 try std.testing.expect(!tiles[1].missed_once); 102 try std.testing.expect(!tiles[1].missed_once);
76 103
77 // Two consecutive lists without it, and it goes. 104 // Two consecutive lists without it, and it goes.
78 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); 105 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
79 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); 106 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
80 try std.testing.expectEqual(@as(usize, 1), vanish.len); 107 try std.testing.expectEqual(@as(usize, 1), vanish.len);
81 try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); 108 try std.testing.expectEqual(@as(usize, 1), vanish.get(0));
82 109
@@ -85,7 +112,7 @@ test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it
85 tiles[0].alive.store(false, .release); 112 tiles[0].alive.store(false, .release);
86 vanish.len = 0; 113 vanish.len = 0;
87 const only0 = [_]bool{ true, false, false }; 114 const only0 = [_]bool{ true, false, false };
88 wall_host.planHostDiff(&tiles, &only0, 3, 0, "", null, &births, &binds, &vanish, &gones); 115 wall_host.planHostDiff(&tiles, &only0, 3, 0, "", null, &binds, &vanish, &gones);
89 try std.testing.expectEqual(@as(usize, 1), vanish.len); 116 try std.testing.expectEqual(@as(usize, 1), vanish.len);
90 try std.testing.expectEqual(@as(usize, 0), vanish.get(0)); 117 try std.testing.expectEqual(@as(usize, 0), vanish.get(0));
91 } 118 }
@@ -101,13 +128,12 @@ test "planHostDiff: a pending pane the reachable list disowns is gone, not vanis
101 t.alive.store(false, .release); 128 t.alive.store(false, .release);
102 } 129 }
103 130
104 var births = BirthNames{};
105 var binds = TileIdxs{}; 131 var binds = TileIdxs{};
106 var vanish = TileIdxs{}; 132 var vanish = TileIdxs{};
107 var gones = TileIdxs{}; 133 var gones = TileIdxs{};
108 // The reachable host answers with NEITHER saved session: both panes are 134 // 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. 135 // 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); 136 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &binds, &vanish, &gones);
111 try std.testing.expectEqual(@as(usize, 0), vanish.len); 137 try std.testing.expectEqual(@as(usize, 0), vanish.len);
112 try std.testing.expectEqual(@as(usize, 2), gones.len); 138 try std.testing.expectEqual(@as(usize, 2), gones.len);
113 try std.testing.expectEqual(@as(usize, 0), gones.get(0)); 139 try std.testing.expectEqual(@as(usize, 0), gones.get(0));
@@ -118,16 +144,15 @@ test "planHostDiff: a pending pane the reachable list disowns is gone, not vanis
118 tiles[0].state = .gone; 144 tiles[0].state = .gone;
119 tiles[1].state = .gone; 145 tiles[1].state = .gone;
120 gones.len = 0; 146 gones.len = 0;
121 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish, &gones); 147 wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &binds, &vanish, &gones);
122 try std.testing.expectEqual(@as(usize, 0), gones.len); 148 try std.testing.expectEqual(@as(usize, 0), gones.len);
123 try std.testing.expectEqual(@as(usize, 0), vanish.len); 149 try std.testing.expectEqual(@as(usize, 0), vanish.len);
124 150
125 // The dressing is reversible: a list that names session "a" binds pane 0 151 // The dressing is reversible: a list that names session "a" binds pane 0
126 // (it stayed pending), and does not birth a twin. 152 // (it stayed pending), in the rect it never left.
127 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); 153 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
128 try std.testing.expectEqual(@as(usize, 1), binds.len); 154 try std.testing.expectEqual(@as(usize, 1), binds.len);
129 try std.testing.expectEqual(@as(usize, 0), binds.get(0)); 155 try std.testing.expectEqual(@as(usize, 0), binds.get(0));
130 try std.testing.expectEqual(@as(usize, 0), births.len);
131 } 156 }
132 157
133 test "planHostDiff: a vanished tile is not present, so the next list does not vanish it twice" { 158 test "planHostDiff: a vanished tile is not present, so the next list does not vanish it twice" {
@@ -135,54 +160,41 @@ test "planHostDiff: a vanished tile is not present, so the next list does not va
135 var tiles = fixture.diffFixture(&shared); 160 var tiles = fixture.diffFixture(&shared);
136 var present = [_]bool{ true, false, true }; 161 var present = [_]bool{ true, false, true };
137 162
138 var births = BirthNames{};
139 var binds = TileIdxs{}; 163 var binds = TileIdxs{};
140 var vanish = TileIdxs{}; 164 var vanish = TileIdxs{};
141 var gones = TileIdxs{}; 165 var gones = TileIdxs{};
142 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); 166 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
143 try std.testing.expectEqual(@as(usize, 0), vanish.len); 167 try std.testing.expectEqual(@as(usize, 0), vanish.len);
144 // ...and the name is not reborn either: the tile is gone, but the
145 // daemon no longer lists it, so there is nothing to bring back.
146 try std.testing.expectEqual(@as(usize, 0), births.len);
147 } 168 }
148 169
149 test "planHostDiff: the session this shell is inside is never born as a tile" { 170 test "planHostDiff: the session this shell is inside never wakes a pane" {
150 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; 171 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
151 var tiles = fixture.diffFixture(&shared); 172 var tiles = fixture.diffFixture(&shared);
152 var present = [_]bool{ true, true, true }; 173 var present = [_]bool{ true, true, true };
174 // Two pending panes, one of them spelling the session mux is running
175 // inside. `seedLayout` refuses such a leaf, so this is the poll's own
176 // half of the same refusal: a pane it woke would attach a replica to
177 // the grid it is painting into.
178 for (tiles[0..2]) |*t| {
179 t.pending = true;
180 t.state = .waiting;
181 t.alive.store(false, .release);
182 }
153 183
154 var births = BirthNames{};
155 var binds = TileIdxs{}; 184 var binds = TileIdxs{};
156 var vanish = TileIdxs{}; 185 var vanish = TileIdxs{};
157 var gones = TileIdxs{}; 186 var gones = TileIdxs{};
158 // The daemon has "a", "b" and "self"; "self" is the shell mux runs in, 187 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", "a", &binds, &vanish, &gones);
159 // so a tile of it would paint into the grid it is reading. 188 // Only "b" is woken; the pane named for this shell stays as it was,
160 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\nself\n", "self", &births, &binds, &vanish, &gones); 189 // and is not dressed gone either — the daemon does have that session.
161 try std.testing.expectEqual(@as(usize, 0), births.len); 190 try std.testing.expectEqual(@as(usize, 1), binds.len);
191 try std.testing.expectEqual(@as(usize, 1), binds.get(0));
192 try std.testing.expect(tiles[0].pending);
193 try std.testing.expectEqual(@as(usize, 0), gones.len);
162 try std.testing.expectEqual(@as(usize, 0), vanish.len); 194 try std.testing.expectEqual(@as(usize, 0), vanish.len);
163 } 195 }
164 196
165 test "planHostDiff: a name the wire grammar refuses never becomes a tile, however the peer spells it" { 197 test "applyReadyLists: only the host that reported is marked, and its sessions are not tiles" {
166 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
167 var tiles = fixture.diffFixture(&shared);
168 var present = [_]bool{ true, true, true };
169
170 var births = BirthNames{};
171 var binds = TileIdxs{};
172 var vanish = TileIdxs{};
173 var gones = TileIdxs{};
174 // A whole reply may be `sessions_reply_max`, so ONE "name" in it can be
175 // 1056 bytes; `encodeAttachNamed` memcpys the birth's name into a
176 // 32-byte tail behind nothing but an assert. Peer bytes are filtered
177 // where they become a tile, not asserted about at the wire.
178 const over_long = "x" ** (proto.session_name_max + 1);
179 const list = "a\n" ++ over_long ++ "\nhas space\n\x1b[2J\nc\n";
180 wall_host.planHostDiff(&tiles, &present, 3, 0, list, null, &births, &binds, &vanish, &gones);
181 try std.testing.expectEqual(@as(usize, 1), births.len);
182 try std.testing.expectEqualStrings("c", births.get(0));
183 }
184
185 test "applyReadyLists: a host added after the wall opened gets its sessions, and reaches past nothing" {
186 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 198 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
187 defer arena.deinit(); 199 defer arena.deinit();
188 const alloc = arena.allocator(); 200 const alloc = arena.allocator();
@@ -205,13 +217,15 @@ test "applyReadyLists: a host added after the wall opened gets its sessions, and
205 217
206 _ = wall_host.applyReadyLists(fixture.wallOf(alloc, &tiles, &present, &live, &shared, table[0..3])); 218 _ = wall_host.applyReadyLists(fixture.wallOf(alloc, &tiles, &present, &live, &shared, table[0..3]));
207 219
208 // The added host's session is a tile, and only the host that reported is 220 // Only the host that reported is marked: the two that opened the wall
209 // marked: the two that opened the wall are still owed a first list. 221 // are still owed a first list.
210 try std.testing.expect(table[2].applied); 222 try std.testing.expect(table[2].applied);
211 try std.testing.expect(!table[0].applied); 223 try std.testing.expect(!table[0].applied);
212 try std.testing.expect(!table[1].applied); 224 try std.testing.expect(!table[1].applied);
213 try std.testing.expectEqual(@as(usize, 1), live); 225 // ...and the session it reported is on nobody's wall: this one was
214 try std.testing.expectEqualStrings("late", tiles[0].r.session); 226 // authored with no panes, and a poll does not author one.
227 try std.testing.expectEqual(@as(usize, 0), live);
228 try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..]));
215 } 229 }
216 230
217 test "recordHost: a file it cannot write comes BACK — the wall may be on the alternate screen" { 231 test "recordHost: a file it cannot write comes BACK — the wall may be on the alternate screen" {
@@ -256,6 +270,11 @@ test "applyHostList: drift dresses every tile of its host, and repaints only on
256 }; 270 };
257 fixture.setList(&table[0], "a\n# mux 0.9"); 271 fixture.setList(&table[0], "a\n# mux 0.9");
258 fixture.setList(&table[1], "b\nc\n# mux 1.0 stale"); 272 fixture.setList(&table[1], "b\nc\n# mux 1.0 stale");
273 try seatBound(&tiles, &present, &live, &shared, &.{
274 .{ .target = .{ .sock = "/tmp/behind.sock" }, .label = "behind#a", .session = "a" },
275 .{ .target = .{ .sock = "/tmp/stale.sock" }, .label = "stale#b", .session = "b" },
276 .{ .target = .{ .sock = "/tmp/stale.sock" }, .label = "stale#c", .session = "c" },
277 }, &.{ 0, 1, 1 });
259 278
260 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); 279 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
261 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1); 280 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1);
@@ -283,7 +302,7 @@ test "applyHostList: drift dresses every tile of its host, and repaints only on
283 try std.testing.expect(shared.repaint_gen.load(.acquire) != gen); 302 try std.testing.expect(shared.repaint_gen.load(.acquire) != gen);
284 } 303 }
285 304
286 test "applyHostList: a host with no live session gets no tile — the wall shows sessions only" { 305 test "applyHostList: a host makes no tile — not when it is silent, not when it lists a session" {
287 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 306 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
288 defer arena.deinit(); 307 defer arena.deinit();
289 const alloc = arena.allocator(); 308 const alloc = arena.allocator();
@@ -311,13 +330,15 @@ test "applyHostList: a host with no live session gets no tile — the wall shows
311 try std.testing.expectEqual(@as(usize, 0), live); 330 try std.testing.expectEqual(@as(usize, 0), live);
312 try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..])); 331 try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..]));
313 332
314 // A session on the empty host, and the wall has exactly the one tile. 333 // The third way, and the one the layout changed: the host answers with
334 // a session, and this wall — which names no pane — still shows nothing.
335 // A session somebody else started is not this user's authored wall.
315 fixture.setList(&table[1], "a\n"); 336 fixture.setList(&table[1], "a\n");
316 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1); 337 for (0..2) |_|
338 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1);
317 339
318 try std.testing.expectEqual(@as(usize, 1), wv.presentCount(present[0..live])); 340 try std.testing.expectEqual(@as(usize, 0), live);
319 try std.testing.expectEqualStrings("a", tiles[0].r.session); 341 try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..]));
320 try std.testing.expectEqual(@as(?usize, 1), tiles[0].host);
321 } 342 }
322 343
323 test "applyHostList: a live tile survives one list that lost its session, and goes on the next" { 344 test "applyHostList: a live tile survives one list that lost its session, and goes on the next" {
@@ -329,18 +350,18 @@ test "applyHostList: a live tile survives one list that lost its session, and go
329 var tiles: [4]Tile = undefined; 350 var tiles: [4]Tile = undefined;
330 var present = [_]bool{false} ** 4; 351 var present = [_]bool{false} ** 4;
331 var live: usize = 0; 352 var live: usize = 0;
332 defer fixture.endPumps(tiles[0..live]); 353 // Two panes, because the tile that keeps its name is what shows the
333 // Two sessions, because the tile that keeps its name is what shows the 354 // grace is per tile and not a wall-wide pause. No pump ever runs here,
334 // grace is per tile and not a wall-wide pause. 355 // so `endPumps` has nothing to wait for and the liveness the rule turns
356 // on is stated rather than raced for.
335 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; 357 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};
336 fixture.setList(&table[0], "a\nb\n"); 358 try seatBound(&tiles, &present, &live, &shared, &.{
337 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); 359 .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box#a", .session = "a" },
338 try std.testing.expectEqual(@as(usize, 2), live); 360 .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box#b", .session = "b" },
339 361 }, &.{ 0, 0 });
340 // The wall's pumps are stopped in this fixture, so the liveness the rule
341 // turns on is stated rather than raced for.
342 tiles[0].alive.store(true, .release); 362 tiles[0].alive.store(true, .release);
343 tiles[1].alive.store(true, .release); 363 tiles[1].alive.store(true, .release);
364
344 fixture.setList(&table[0], "a\n"); 365 fixture.setList(&table[0], "a\n");
345 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); 366 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
346 // Still there: this list may have overtaken an `exit_status` the pump 367 // Still there: this list may have overtaken an `exit_status` the pump
@@ -353,139 +374,32 @@ test "applyHostList: a live tile survives one list that lost its session, and go
353 try std.testing.expect(present[0]); 374 try std.testing.expect(present[0]);
354 } 375 }
355 376
356 test "applyHostList: two sessions on an empty wall are two tiles, the first focused" { 377 test "applyHostList: a busy daemon leaves an empty wall empty — a list is not a source of tiles" {
357 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
358 defer arena.deinit();
359 const alloc = arena.allocator();
360 var shared: Shared = undefined;
361 fixture.stoppedWall(alloc, &shared);
362 var tiles: [4]Tile = undefined;
363 var present = [_]bool{false} ** 4;
364 var live: usize = 0;
365 defer fixture.endPumps(tiles[0..live]);
366 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};
367 fixture.setList(&table[0], "a\nb\n");
368
369 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
370
371 // The first birth has no leaf to sit beside — an empty tree takes it as
372 // its root, and the second inserts against it.
373 try std.testing.expectEqual(@as(usize, 2), live);
374 try std.testing.expectEqual(@as(usize, 2), shared.tree.count());
375 try std.testing.expectEqualStrings("a", tiles[0].r.session);
376 try std.testing.expectEqualStrings("b", tiles[1].r.session);
377 try std.testing.expectEqual(@as(usize, 0), shared.sel);
378 try std.testing.expect(tiles[0].claim_pending.load(.acquire));
379 }
380
381 test "applyHostList: one list's tiles are laid out in the order the daemon reported them" {
382 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 378 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
383 defer arena.deinit(); 379 defer arena.deinit();
384 const alloc = arena.allocator(); 380 const alloc = arena.allocator();
385 var shared: Shared = undefined; 381 var shared: Shared = undefined;
386 fixture.stoppedWall(alloc, &shared); 382 fixture.stoppedWall(alloc, &shared);
387 defer if (shared.last_flat) |*f| f.deinit(alloc);
388 var tiles: [4]Tile = undefined; 383 var tiles: [4]Tile = undefined;
389 var present = [_]bool{false} ** 4; 384 var present = [_]bool{false} ** 4;
390 var live: usize = 0; 385 var live: usize = 0;
391 defer fixture.endPumps(tiles[0..live]); 386 defer fixture.endPumps(tiles[0..live]);
392 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; 387 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};
393 // THREE, because two cannot fail: the first birth roots the tree and the 388 // THREE sessions, none of them a pane: a wall the user authored empty
394 // second lands after it whatever it anchored on. The third is the one 389 // stays empty however busy the box is, and the picker is the door onto
395 // that has a choice, and anchoring it back at the focus puts it BETWEEN 390 // it. Twice, because a tile born once is still a tile.
396 // its two siblings.
397 fixture.setList(&table[0], "a\nb\nc\n"); 391 fixture.setList(&table[0], "a\nb\nc\n");
398 392
399 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); 393 for (0..2) |_|
400 394 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
401 try std.testing.expectEqual(@as(usize, 3), live);
402 // Down the screen in the daemon's own order. The tile ARRAY is in that
403 // order by construction; what this reads is the flatten, which is where
404 // an anchor that did not move shows up.
405 const flat = shared.last_flat orelse return error.NoFlat;
406 const top_a = (flat.rectOf(0) orelse return error.NoRect).top;
407 const top_b = (flat.rectOf(1) orelse return error.NoRect).top;
408 const top_c = (flat.rectOf(2) orelse return error.NoRect).top;
409 try std.testing.expectEqualStrings("a", tiles[0].r.session);
410 try std.testing.expectEqualStrings("b", tiles[1].r.session);
411 try std.testing.expectEqualStrings("c", tiles[2].r.session);
412 try std.testing.expect(top_a < top_b);
413 try std.testing.expect(top_b < top_c);
414 }
415
416 test "applyHostList: sessions past the wall's capacity are counted in the notice, not dropped in silence" {
417 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
418 defer arena.deinit();
419 const alloc = arena.allocator();
420 var shared: Shared = undefined;
421 fixture.stoppedWall(alloc, &shared);
422 var tiles: [wv.max_tiles]Tile = undefined;
423 var present = [_]bool{true} ** wv.max_tiles;
424 var live: usize = wv.max_tiles;
425 var names: [wv.max_tiles][2]u8 = undefined;
426 var text: std.ArrayList(u8) = .empty;
427 for (&tiles, 0..) |*t, i| {
428 const name = std.fmt.bufPrint(&names[i], "{d}", .{i}) catch unreachable;
429 t.* = .{
430 .r = .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box", .session = name },
431 .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
432 .shared = &shared,
433 .idx = i,
434 .wake_r = -1,
435 .wake_w = -1,
436 .host = 0,
437 .alive = std.atomic.Value(bool).init(false),
438 };
439 try text.appendSlice(alloc, name);
440 try text.append(alloc, '\n');
441 }
442 // Two more than the wall can hold.
443 try text.appendSlice(alloc, "x\ny\n");
444 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};
445 fixture.setList(&table[0], text.items);
446
447 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
448
449 try std.testing.expectEqual(@as(usize, wv.max_tiles), live);
450 var buf: [96]u8 = undefined;
451 try std.testing.expectEqualStrings("[+2 not shown]", wv.takeNotice(&shared, &buf));
452 }
453
454 test "applyHostList: a wall too thin for a second pane takes what fits and retains nothing for the rest" {
455 // The TESTING allocator, not an arena: what this pins is that a birth
456 // the wall refuses leaves no allocation behind, and only leak detection
457 // can say so. The tile that DID fit owns its copies, so the test frees
458 // exactly those two.
459 const alloc = std.testing.allocator;
460 var shared: Shared = undefined;
461 fixture.stoppedWall(alloc, &shared);
462 // Four rows on a tty: one pane fits (`min_session_rows` plus its label
463 // bar), two never can — each would owe a bar on top of that floor.
464 shared.is_tty = true;
465 shared.size = .{ .cols = 80, .rows = 4 };
466 defer shared.tree.deinit();
467 defer if (shared.last_flat) |*f| f.deinit(alloc);
468 defer if (shared.base_flat) |*f| f.deinit(alloc);
469 var tiles: [4]Tile = undefined;
470 var present = [_]bool{false} ** 4;
471 var live: usize = 0;
472 // Freed only after `endPumps` (defers run last-declared-first): a
473 // pump's first act is to paint its `connecting` bar, and on a tty
474 // every wall has bars, so the label must outlive the thread.
475 defer for (tiles[0..live]) |*t| {
476 alloc.free(t.r.session);
477 alloc.free(t.r.label);
478 };
479 defer fixture.endPumps(tiles[0..live]);
480 var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};
481 fixture.setList(&table[0], "a\nb\n");
482
483 wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
484 395
485 try std.testing.expectEqual(@as(usize, 1), live); 396 try std.testing.expectEqual(@as(usize, 0), live);
486 try std.testing.expectEqualStrings("a", tiles[0].r.session); 397 try std.testing.expectEqual(@as(usize, 0), shared.tree.count());
398 try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..]));
399 // Not even a word about them: a wall that shows the panes it was given
400 // is not a wall hiding the rest of the daemon.
487 var buf: [96]u8 = undefined; 401 var buf: [96]u8 = undefined;
488 try std.testing.expectEqualStrings("[+1 not shown]", wv.takeNotice(&shared, &buf)); 402 try std.testing.expectEqualStrings("", wv.takeNotice(&shared, &buf));
489 } 403 }
490 404
491 test "applyHostList: when a host's last session goes the wall empties, and nothing stands in for the host" { 405 test "applyHostList: when a host's last session goes the wall empties, and nothing stands in for the host" {
@@ -531,19 +445,18 @@ test "planHostDiff: a creating tile whose attach has not landed yet is not vanis
531 tiles[1].creates = true; 445 tiles[1].creates = true;
532 var present = [_]bool{ true, true, true }; 446 var present = [_]bool{ true, true, true };
533 447
534 var births = BirthNames{};
535 var binds = TileIdxs{}; 448 var binds = TileIdxs{};
536 var vanish = TileIdxs{}; 449 var vanish = TileIdxs{};
537 var gones = TileIdxs{}; 450 var gones = TileIdxs{};
538 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); 451 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
539 try std.testing.expectEqual(@as(usize, 0), vanish.len); 452 try std.testing.expectEqual(@as(usize, 0), vanish.len);
540 453
541 // ...and once its session exists, the same list does drop it — after 454 // ...and once its session exists, the same list does drop it — after
542 // the one list of grace a live pump gets: a session the daemon HAD and 455 // the one list of grace a live pump gets: a session the daemon HAD and
543 // no longer lists is gone. 456 // no longer lists is gone.
544 tiles[1].ever_up.store(true, .release); 457 tiles[1].ever_up.store(true, .release);
545 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); 458 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
546 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); 459 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
547 try std.testing.expectEqual(@as(usize, 1), vanish.len); 460 try std.testing.expectEqual(@as(usize, 1), vanish.len);
548 try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); 461 try std.testing.expectEqual(@as(usize, 1), vanish.get(0));
549 } 462 }
@@ -756,7 +669,7 @@ fn seedBench(alloc: std.mem.Allocator, tiles: []Tile, present: []bool, live: *us
756 live.* = 3; 669 live.* = 3;
757 } 670 }
758 671
759 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" { 672 test "planHostDiff: a pending pane the list names is a bind, and only its own host's list may wake it" {
760 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 673 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
761 defer arena.deinit(); 674 defer arena.deinit();
762 const alloc = arena.allocator(); 675 const alloc = arena.allocator();
@@ -769,25 +682,23 @@ test "planHostDiff: a pending pane the list names is a bind, not a birth - and t
769 try seedBench(alloc, &tiles, &present, &live, &shared); 682 try seedBench(alloc, &tiles, &present, &live, &shared);
770 defer fixture.endPumps(tiles[0..live]); 683 defer fixture.endPumps(tiles[0..live]);
771 684
772 var births = BirthNames{};
773 var binds = TileIdxs{}; 685 var binds = TileIdxs{};
774 var vanish = TileIdxs{}; 686 var vanish = TileIdxs{};
775 var gones = TileIdxs{}; 687 var gones = TileIdxs{};
776 // Host 0's whole list is pending panes: two binds, no birth, no twin. 688 // Host 0's whole list is pending panes: two binds, and no pane of
777 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &births, &binds, &vanish, &gones); 689 // host 1's is touched by a name host 0 spelled.
778 try std.testing.expectEqual(@as(usize, 0), births.len); 690 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &binds, &vanish, &gones);
779 try std.testing.expectEqual(@as(usize, 0), vanish.len); 691 try std.testing.expectEqual(@as(usize, 0), vanish.len);
780 try std.testing.expectEqual(@as(usize, 2), binds.len); 692 try std.testing.expectEqual(@as(usize, 2), binds.len);
781 try std.testing.expectEqual(@as(usize, 0), binds.get(0)); 693 try std.testing.expectEqual(@as(usize, 0), binds.get(0));
782 try std.testing.expectEqual(@as(usize, 1), binds.get(1)); 694 try std.testing.expectEqual(@as(usize, 1), binds.get(1));
783 // Host 1's "a" is its own pane, not host 0's; its "x" is a stranger 695 // Host 1's "a" is its own pane, not host 0's; its "x" is a session no
784 // the sidecar never named, so it is still a birth. 696 // pane spells, and the wall has nothing to say about it.
785 binds.len = 0; 697 binds.len = 0;
786 wall_host.planHostDiff(&tiles, &present, 3, 1, "a\nx\n", null, &births, &binds, &vanish, &gones); 698 wall_host.planHostDiff(&tiles, &present, 3, 1, "a\nx\n", null, &binds, &vanish, &gones);
787 try std.testing.expectEqual(@as(usize, 1), binds.len); 699 try std.testing.expectEqual(@as(usize, 1), binds.len);
788 try std.testing.expectEqual(@as(usize, 2), binds.get(0)); 700 try std.testing.expectEqual(@as(usize, 2), binds.get(0));
789 try std.testing.expectEqual(@as(usize, 1), births.len); 701 try std.testing.expectEqual(@as(usize, 3), live);
790 try std.testing.expectEqualStrings("x", births.get(0));
791 } 702 }
792 703
793 test "applyHostList: binding a saved pane moves no rect and re-cuts nothing" { 704 test "applyHostList: binding a saved pane moves no rect and re-cuts nothing" {
@@ -895,7 +806,7 @@ test "bindTile: the pane under the saved focus claims the terminal when it binds
895 try std.testing.expect(!tiles[0].claim_pending.load(.acquire)); 806 try std.testing.expect(!tiles[0].claim_pending.load(.acquire));
896 } 807 }
897 808
898 test "applyHostList: the list that disowns a seeded pane still births the newcomer beside it - one re-cut" { 809 test "applyHostList: one list binds a pane, dresses the pane it disowns gone, and adds nothing for the name it never saw" {
899 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 810 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
900 defer arena.deinit(); 811 defer arena.deinit();
901 const alloc = arena.allocator(); 812 const alloc = arena.allocator();
@@ -912,23 +823,30 @@ test "applyHostList: the list that disowns a seeded pane still births the newcom
912 fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"), 823 fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"),
913 }; 824 };
914 const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table); 825 const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table);
826 // The seed's own cut, then quiet: what this list must not disturb.
915 wall_layout.relayout(w, 0); 827 wall_layout.relayout(w, 0);
916 const gen = shared.repaint_gen.load(.acquire); 828 const gen = shared.repaint_gen.load(.acquire);
829 var rects: [3]@TypeOf(tiles[0].rect) = undefined;
830 for (tiles[0..3], 0..) |t, i| rects[i] = t.rect;
917 831
918 // One list: "a" binds, "b" is disowned, "c" is new. One relayout serves 832 // One list, all three answers in it: "a" binds, "b" is disowned, and
919 // the whole list however many of the three it moved. 833 // "c" is a session of this daemon's that no pane spells.
920 fixture.setList(&table[0], "a\nc\n"); 834 fixture.setList(&table[0], "a\nc\n");
921 wall_host.applyHostList(w, 0); 835 wall_host.applyHostList(w, 0);
922 836
923 try std.testing.expectEqual(gen + 1, shared.repaint_gen.load(.acquire));
924 try std.testing.expect(!tiles[0].pending); 837 try std.testing.expect(!tiles[0].pending);
925 // The disowned pane keeps its digit and its rect: the host said no, and 838 // The disowned pane keeps its digit and its rect: the host said no, and
926 // a pane the user saved is not the daemon's to collapse. 839 // a pane the user authored is not the daemon's to collapse.
927 try std.testing.expect(present[1] and tiles[1].pending); 840 try std.testing.expect(present[1] and tiles[1].pending);
928 // So the newcomer takes the next FREE digit rather than a freed one. 841 try std.testing.expectEqual(wv.State.gone, tiles[1].state);
929 try std.testing.expectEqual(@as(usize, 4), live); 842 // "c" is nobody's business: the wall the user wrote down is three panes
930 try std.testing.expect(present[3]); 843 // wide before this list and three after it.
931 try std.testing.expectEqualStrings("c", tiles[3].r.session); 844 try std.testing.expectEqual(@as(usize, 3), live);
845 try std.testing.expectEqual(@as(usize, 3), wv.presentCount(present[0..live]));
846 // Nothing was re-cut: a bind wakes a pane in place and a gone pane is
847 // dressed where it stands, so no rect moved and no repaint was asked for.
848 try std.testing.expectEqual(gen, shared.repaint_gen.load(.acquire));
849 for (tiles[0..3], 0..) |t, i| try std.testing.expectEqual(rects[i], t.rect);
932 } 850 }
933 851
934 test "sendKeys: the mailbox of a pending pane holds what was typed for the pump the bind will start" { 852 test "sendKeys: the mailbox of a pending pane holds what was typed for the pump the bind will start" {
@@ -1154,3 +1072,19 @@ test "endKey: x on a gone pane drops the tile locally" {
1154 // coincidence, so it is asserted rather than assumed. 1072 // coincidence, so it is asserted rather than assumed.
1155 try std.testing.expectEqual(wv.EndKey.drop, wv.endKey(&tiles[0], 0)); 1073 try std.testing.expectEqual(wv.EndKey.drop, wv.endKey(&tiles[0], 0));
1156 } 1074 }
1075
1076 test "planHostDiff: a session the daemon has and the wall does not is nobody's business: no birth, no tile" {
1077 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
1078 var tiles = fixture.diffFixture(&shared);
1079 var present = [_]bool{ true, true, true };
1080 var binds = TileIdxs{};
1081 var vanish = TileIdxs{};
1082 var gones = TileIdxs{};
1083 // Host 0 answers with its two panes' sessions and three the wall never
1084 // asked for. Twice, so the grace has been spent and a vanish would show.
1085 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\nx\ny\nz\n", null, &binds, &vanish, &gones);
1086 wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\nx\ny\nz\n", null, &binds, &vanish, &gones);
1087 try std.testing.expectEqual(@as(usize, 0), vanish.len);
1088 try std.testing.expectEqual(@as(usize, 0), gones.len);
1089 try std.testing.expectEqual(@as(usize, 3), wv.presentCount(&present));
1090 }