285ceedf
feat: the seed — the sidecar's cut before any host has answered
a73x 2026-08-30 16:31
Commit message
src/tui/wall_layout.zig
| Old | New | ||
|---|---|---|---|
| @@ -9,6 +9,7 @@ const interact = @import("interact.zig"); | |||
| 9 | const layout = @import("client").layout; | 9 | const layout = @import("client").layout; |
| 10 | const wall_host = @import("wall_host.zig"); | 10 | const wall_host = @import("wall_host.zig"); |
| 11 | const wv = @import("wallview.zig"); | 11 | const wv = @import("wallview.zig"); |
| 12 | const Host = wall_host.Host; | ||
| 12 | const Resolved = wall_host.Resolved; | 13 | const Resolved = wall_host.Resolved; |
| 13 | const Shared = wv.Shared; | 14 | const Shared = wv.Shared; |
| 14 | const Tile = wv.Tile; | 15 | const Tile = wv.Tile; |
| @@ -333,6 +334,195 @@ pub fn saveSidecar(w: Wall) void { | |||
| 333 | saveLayoutTo(w.alloc, path, w.liveTiles(), w.livePresent(), w.shared); | 334 | saveLayoutTo(w.alloc, path, w.liveTiles(), w.livePresent(), w.shared); |
| 334 | } | 335 | } |
| 335 | 336 | ||
| 337 | pub const SeedPane = struct { host: usize, session: []u8, label: []u8 }; | ||
| 338 | |||
| 339 | const SeedKeep = struct { saved: usize, host: usize }; | ||
| 340 | |||
| 341 | /// What a seeded wall is made of, index = tile index. A null pane is the | ||
| 342 | /// ENTRY tile's own slot: real from birth, never pending. | ||
| 343 | pub const SeedPlan = struct { | ||
| 344 | panes: []?SeedPane, | ||
| 345 | focus: ?usize, | ||
| 346 | dropped: usize, | ||
| 347 | |||
| 348 | pub fn deinit(self: *SeedPlan, alloc: std.mem.Allocator) void { | ||
| 349 | for (self.panes) |mp| { | ||
| 350 | if (mp) |pane| { | ||
| 351 | alloc.free(pane.session); | ||
| 352 | alloc.free(pane.label); | ||
| 353 | } | ||
| 354 | } | ||
| 355 | alloc.free(self.panes); | ||
| 356 | } | ||
| 357 | }; | ||
| 358 | |||
| 359 | /// The sidecar's cut before any host answers; every failure degrades | ||
| 360 | /// silently, leaf by leaf or file whole, to the default cut. | ||
| 361 | pub fn seedLayout( | ||
| 362 | alloc: std.mem.Allocator, | ||
| 363 | table: []const Host, | ||
| 364 | shared: *Shared, | ||
| 365 | bytes: []const u8, | ||
| 366 | entry_spelling: ?[]const u8, | ||
| 367 | ) ?SeedPlan { | ||
| 368 | // Pass 1: read the spellings once and give each leaf its fate. | ||
| 369 | var probe = layout.parse(alloc, bytes) orelse return null; | ||
| 370 | var keeps = std.ArrayListUnmanaged(SeedKeep){}; | ||
| 371 | defer keeps.deinit(alloc); | ||
| 372 | var entry_at: ?usize = null; | ||
| 373 | var dropped: usize = 0; | ||
| 374 | for (probe.spellings.items, 0..) |sp, i| { | ||
| 375 | if (entry_spelling) |es| { | ||
| 376 | if (entry_at == null and std.mem.eql(u8, sp, es)) { | ||
| 377 | entry_at = i; | ||
| 378 | continue; | ||
| 379 | } | ||
| 380 | } | ||
| 381 | const cut = std.mem.lastIndexOfScalar(u8, sp, '#') orelse { | ||
| 382 | dropped += 1; | ||
| 383 | continue; | ||
| 384 | }; | ||
| 385 | const sess = sp[cut + 1 ..]; | ||
| 386 | if (!proto.validSessionName(sess)) { | ||
| 387 | dropped += 1; | ||
| 388 | continue; | ||
| 389 | } | ||
| 390 | // The host part must be ON the wall, byte for byte: the file is a | ||
| 391 | // match key, never an address, so nothing here can be resurrected. | ||
| 392 | const hi = for (table, 0..) |*h, j| { | ||
| 393 | if (std.mem.eql(u8, h.spec.spelling, sp[0..cut])) break j; | ||
| 394 | } else { | ||
| 395 | dropped += 1; | ||
| 396 | continue; | ||
| 397 | }; | ||
| 398 | // The shell's own session would wait forever: never birthed | ||
| 399 | // (`showsSelf`) and named by every list, so never collapsed. | ||
| 400 | if (table[hi].self_name) |self| { | ||
| 401 | if (std.mem.eql(u8, self, sess)) { | ||
| 402 | dropped += 1; | ||
| 403 | continue; | ||
| 404 | } | ||
| 405 | } | ||
| 406 | // A host's list names a session once; a second pane on the same | ||
| 407 | // (host, session) could never bind. | ||
| 408 | const dup = blk: { | ||
| 409 | if (entry_spelling) |es| { | ||
| 410 | if (std.mem.eql(u8, sp, es)) break :blk true; | ||
| 411 | } | ||
| 412 | for (keeps.items) |k| { | ||
| 413 | if (std.mem.eql(u8, probe.spellings.items[k.saved], sp)) break :blk true; | ||
| 414 | } | ||
| 415 | break :blk false; | ||
| 416 | }; | ||
| 417 | if (dup) { | ||
| 418 | dropped += 1; | ||
| 419 | continue; | ||
| 420 | } | ||
| 421 | keeps.append(alloc, .{ .saved = i, .host = hi }) catch { | ||
| 422 | probe.deinit(alloc); | ||
| 423 | return null; | ||
| 424 | }; | ||
| 425 | } | ||
| 426 | const base: usize = if (entry_spelling != null) 1 else 0; | ||
| 427 | while (base + keeps.items.len > wv.max_tiles) { | ||
| 428 | _ = keeps.pop(); | ||
| 429 | dropped += 1; | ||
| 430 | } | ||
| 431 | probe.deinit(alloc); | ||
| 432 | if (keeps.items.len == 0) return null; | ||
| 433 | // A tree cut on a big screen must not refuse the boot on a laptop: | ||
| 434 | // drop the last pane and retry until the terminal can hold what is | ||
| 435 | // left, or nothing is left and today's default cut runs. | ||
| 436 | var n = keeps.items.len; | ||
| 437 | while (n >= 1) : (n -= 1) { | ||
| 438 | if (seedAttempt(alloc, shared, bytes, entry_at, keeps.items[0..n], base)) |plan| { | ||
| 439 | var out = plan; | ||
| 440 | out.dropped = dropped + (keeps.items.len - n); | ||
| 441 | return out; | ||
| 442 | } | ||
| 443 | } | ||
| 444 | return null; | ||
| 445 | } | ||
| 446 | |||
| 447 | /// One sizing attempt over the first `keeps` panes; the tree is | ||
| 448 | /// installed only if the terminal can cut it. | ||
| 449 | fn seedAttempt( | ||
| 450 | alloc: std.mem.Allocator, | ||
| 451 | shared: *Shared, | ||
| 452 | bytes: []const u8, | ||
| 453 | entry_at: ?usize, | ||
| 454 | keeps: []const SeedKeep, | ||
| 455 | base: usize, | ||
| 456 | ) ?SeedPlan { | ||
| 457 | var parsed = layout.parse(alloc, bytes) orelse return null; | ||
| 458 | const map = alloc.alloc(?u8, parsed.spellings.items.len) catch { | ||
| 459 | parsed.deinit(alloc); | ||
| 460 | return null; | ||
| 461 | }; | ||
| 462 | defer alloc.free(map); | ||
| 463 | @memset(map, null); | ||
| 464 | if (entry_at) |e| map[e] = 0; | ||
| 465 | for (keeps, 0..) |k, i| map[k.saved] = @intCast(base + i); | ||
| 466 | var focus: ?usize = null; | ||
| 467 | if (parsed.focus) |k| { | ||
| 468 | if (k < map.len) { | ||
| 469 | if (map[k]) |f| focus = f; | ||
| 470 | } | ||
| 471 | } | ||
| 472 | const total = base + keeps.len; | ||
| 473 | var plan: SeedPlan = .{ | ||
| 474 | .panes = alloc.alloc(?SeedPane, total) catch { | ||
| 475 | parsed.deinit(alloc); | ||
| 476 | return null; | ||
| 477 | }, | ||
| 478 | .focus = focus, | ||
| 479 | .dropped = 0, | ||
| 480 | }; | ||
| 481 | @memset(plan.panes, null); | ||
| 482 | // Duped: a wall tile owns its two copies, and the birth that reuses a | ||
| 483 | // collapsed pane's digit frees them with the wall's allocator. | ||
| 484 | const ok = blk: { | ||
| 485 | for (keeps, 0..) |k, i| { | ||
| 486 | const sp = parsed.spellings.items[k.saved]; | ||
| 487 | const cut = std.mem.lastIndexOfScalar(u8, sp, '#').?; | ||
| 488 | const sess = alloc.dupe(u8, sp[cut + 1 ..]) catch break :blk false; | ||
| 489 | const label = alloc.dupe(u8, sp) catch { | ||
| 490 | alloc.free(sess); | ||
| 491 | break :blk false; | ||
| 492 | }; | ||
| 493 | plan.panes[base + i] = .{ .host = k.host, .session = sess, .label = label }; | ||
| 494 | } | ||
| 495 | break :blk true; | ||
| 496 | }; | ||
| 497 | if (!ok) { | ||
| 498 | plan.deinit(alloc); | ||
| 499 | parsed.deinit(alloc); | ||
| 500 | return null; | ||
| 501 | } | ||
| 502 | parsed.tree.remapLeaves(map); | ||
| 503 | if (base == 1 and entry_at == null) { | ||
| 504 | const anchor: u8 = if (focus) |f| @intCast(f) else @intCast(base); | ||
| 505 | parsed.tree.insert(anchor, 0) catch { | ||
| 506 | plan.deinit(alloc); | ||
| 507 | parsed.deinit(alloc); | ||
| 508 | return null; | ||
| 509 | }; | ||
| 510 | } | ||
| 511 | if (parsed.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(total), null)) |flat| { | ||
| 512 | var f = flat; | ||
| 513 | f.deinit(alloc); | ||
| 514 | } else |_| { | ||
| 515 | plan.deinit(alloc); | ||
| 516 | parsed.deinit(alloc); | ||
| 517 | return null; | ||
| 518 | } | ||
| 519 | shared.tree.deinit(); | ||
| 520 | shared.tree = parsed.tree; | ||
| 521 | for (parsed.spellings.items) |sp| alloc.free(sp); | ||
| 522 | parsed.spellings.deinit(alloc); | ||
| 523 | return plan; | ||
| 524 | } | ||
| 525 | |||
| 336 | /// The saved layout over the tiles the hosts turned out to have; null | 526 | /// The saved layout over the tiles the hosts turned out to have; null |
| 337 | /// when nothing matched. | 527 | /// when nothing matched. |
| 338 | pub fn restoreSidecar(w: Wall, focus_out: *?usize) ?void { | 528 | pub fn restoreSidecar(w: Wall, focus_out: *?usize) ?void { |
src/tui/wall_test_layout.zig
| Old | New | ||
|---|---|---|---|
| @@ -937,3 +937,237 @@ test "layoutPathFrom: the sidecar sits beside the hosts file" { | |||
| 937 | defer alloc.free(q); | 937 | defer alloc.free(q); |
| 938 | try std.testing.expectEqualStrings("/home/u/.local/state/mux/layout", q); | 938 | try std.testing.expectEqualStrings("/home/u/.local/state/mux/layout", q); |
| 939 | } | 939 | } |
| 940 | |||
| 941 | // ---- the seed: the sidecar's cut before any host has answered ---- | ||
| 942 | |||
| 943 | /// Two hosts and a three-leaf sidecar — beside(a, stacked(b, c)) with | ||
| 944 | /// UNEQUAL weights — so orientation, weight and host aliasing are all | ||
| 945 | /// dimensions every seed test exercises by default. | ||
| 946 | const seed_bytes = "mux-layout 1\nbeside 0\n leaf 70 --sock /tmp/h0.sock#a\n stacked 29\n leaf 20 --sock /tmp/h0.sock#b\n leaf 8 --sock /tmp/h1.sock#a\nfocus 1\n"; | ||
| 947 | |||
| 948 | fn seedShared(alloc: std.mem.Allocator, shared: *Shared) void { | ||
| 949 | shared.* = Shared{ .out_fd = -1, .size = .{ .cols = 100, .rows = 30 }, .is_tty = false }; | ||
| 950 | shared.tree = layout.Tree.init(alloc); | ||
| 951 | shared.flat_alloc = alloc; | ||
| 952 | } | ||
| 953 | |||
| 954 | test "seed: the saved cut is the tree before any host has answered - orientation and weights both" { | ||
| 955 | const alloc = std.testing.allocator; | ||
| 956 | var shared: Shared = undefined; | ||
| 957 | seedShared(alloc, &shared); | ||
| 958 | defer shared.tree.deinit(); | ||
| 959 | var table = [_]wall_host.Host{ | ||
| 960 | fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"), | ||
| 961 | fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"), | ||
| 962 | }; | ||
| 963 | var plan = wall_layout.seedLayout(alloc, &table, &shared, seed_bytes, null) orelse | ||
| 964 | return error.SeedRefusedAValidSidecar; | ||
| 965 | defer plan.deinit(alloc); | ||
| 966 | try std.testing.expectEqual(@as(usize, 3), plan.panes.len); | ||
| 967 | try std.testing.expectEqual(@as(usize, 0), plan.dropped); | ||
| 968 | try std.testing.expectEqual(@as(usize, 0), plan.panes[0].?.host); | ||
| 969 | try std.testing.expectEqualStrings("a", plan.panes[0].?.session); | ||
| 970 | try std.testing.expectEqual(@as(usize, 1), plan.panes[2].?.host); | ||
| 971 | var f = try shared.tree.flatten(alloc, 30, 100, wall_layout.wallFloors(3), null); | ||
| 972 | defer f.deinit(alloc); | ||
| 973 | // The saved orientation: pane 0 beside the stack of 1 over 2. The old | ||
| 974 | // heal lost both to the default cut; the rects are the pin. | ||
| 975 | const r0 = f.rectOf(0).?; | ||
| 976 | const r1 = f.rectOf(1).?; | ||
| 977 | const r2 = f.rectOf(2).?; | ||
| 978 | try std.testing.expect(r1.left > 0 and r1.top == 0); | ||
| 979 | try std.testing.expect(r2.left == r1.left and r2.top > 0); | ||
| 980 | // The saved weights: 70/29 is not an even cut. | ||
| 981 | try std.testing.expect(r0.cols > 2 * r1.cols); | ||
| 982 | } | ||
| 983 | |||
| 984 | test "seed: a leaf whose host is not on the wall is dropped and the rest keep their cut" { | ||
| 985 | const alloc = std.testing.allocator; | ||
| 986 | var shared: Shared = undefined; | ||
| 987 | seedShared(alloc, &shared); | ||
| 988 | defer shared.tree.deinit(); | ||
| 989 | var table = [_]wall_host.Host{ | ||
| 990 | fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"), | ||
| 991 | }; | ||
| 992 | var plan = wall_layout.seedLayout(alloc, &table, &shared, seed_bytes, null) orelse | ||
| 993 | return error.SeedRefusedAValidSidecar; | ||
| 994 | defer plan.deinit(alloc); | ||
| 995 | try std.testing.expectEqual(@as(usize, 2), plan.panes.len); | ||
| 996 | try std.testing.expectEqual(@as(usize, 1), plan.dropped); | ||
| 997 | try std.testing.expectEqualStrings("--sock /tmp/h0.sock#a", plan.panes[0].?.label); | ||
| 998 | try std.testing.expectEqualStrings("--sock /tmp/h0.sock#b", plan.panes[1].?.label); | ||
| 999 | try std.testing.expectEqual(@as(usize, 2), shared.tree.count()); | ||
| 1000 | } | ||
| 1001 | |||
| 1002 | test "seed: a leaf naming no session, or a name the wire grammar refuses, is dropped" { | ||
| 1003 | const alloc = std.testing.allocator; | ||
| 1004 | var shared: Shared = undefined; | ||
| 1005 | seedShared(alloc, &shared); | ||
| 1006 | defer shared.tree.deinit(); | ||
| 1007 | var table = [_]wall_host.Host{ | ||
| 1008 | fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"), | ||
| 1009 | }; | ||
| 1010 | const bytes = "mux-layout 1\nstacked 0\n leaf 10 --sock /tmp/h0.sock#a\n leaf 10 no-hash-at-all\n leaf 10 --sock /tmp/h0.sock#ha sh\n"; | ||
| 1011 | var plan = wall_layout.seedLayout(alloc, &table, &shared, bytes, null) orelse | ||
| 1012 | return error.SeedRefusedAValidSidecar; | ||
| 1013 | defer plan.deinit(alloc); | ||
| 1014 | try std.testing.expectEqual(@as(usize, 1), plan.panes.len); | ||
| 1015 | try std.testing.expectEqual(@as(usize, 2), plan.dropped); | ||
| 1016 | } | ||
| 1017 | |||
| 1018 | test "seed: two leaves naming one host's one session become one pane" { | ||
| 1019 | const alloc = std.testing.allocator; | ||
| 1020 | var shared: Shared = undefined; | ||
| 1021 | seedShared(alloc, &shared); | ||
| 1022 | defer shared.tree.deinit(); | ||
| 1023 | var table = [_]wall_host.Host{ | ||
| 1024 | fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"), | ||
| 1025 | }; | ||
| 1026 | const bytes = "mux-layout 1\nstacked 0\n leaf 10 --sock /tmp/h0.sock#a\n leaf 10 --sock /tmp/h0.sock#a\n"; | ||
| 1027 | var plan = wall_layout.seedLayout(alloc, &table, &shared, bytes, null) orelse | ||
| 1028 | return error.SeedRefusedAValidSidecar; | ||
| 1029 | defer plan.deinit(alloc); | ||
| 1030 | try std.testing.expectEqual(@as(usize, 1), plan.panes.len); | ||
| 1031 | try std.testing.expectEqual(@as(usize, 1), plan.dropped); | ||
| 1032 | } | ||
| 1033 | |||
| 1034 | test "seed: the session this shell is standing in is never seeded" { | ||
| 1035 | const alloc = std.testing.allocator; | ||
| 1036 | var shared: Shared = undefined; | ||
| 1037 | seedShared(alloc, &shared); | ||
| 1038 | defer shared.tree.deinit(); | ||
| 1039 | var table = [_]wall_host.Host{ | ||
| 1040 | fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"), | ||
| 1041 | fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"), | ||
| 1042 | }; | ||
| 1043 | table[0].self_name = "a"; | ||
| 1044 | var plan = wall_layout.seedLayout(alloc, &table, &shared, seed_bytes, null) orelse | ||
| 1045 | return error.SeedRefusedAValidSidecar; | ||
| 1046 | defer plan.deinit(alloc); | ||
| 1047 | try std.testing.expectEqual(@as(usize, 2), plan.panes.len); | ||
| 1048 | try std.testing.expectEqual(@as(usize, 1), plan.dropped); | ||
| 1049 | // Host 1's aliasing "a" is another host's session and stays. | ||
| 1050 | try std.testing.expectEqualStrings("--sock /tmp/h0.sock#b", plan.panes[0].?.label); | ||
| 1051 | try std.testing.expectEqualStrings("--sock /tmp/h1.sock#a", plan.panes[1].?.label); | ||
| 1052 | } | ||
| 1053 | |||
| 1054 | test "seed: garbage, an empty file and a wall with no matching host all leave the tree exactly as it was" { | ||
| 1055 | const alloc = std.testing.allocator; | ||
| 1056 | var shared: Shared = undefined; | ||
| 1057 | seedShared(alloc, &shared); | ||
| 1058 | defer shared.tree.deinit(); | ||
| 1059 | try shared.tree.addFirst(7); | ||
| 1060 | var table = [_]wall_host.Host{ | ||
| 1061 | fixture.testHost(&shared, "--sock /tmp/other.sock", "/tmp/other.sock"), | ||
| 1062 | }; | ||
| 1063 | const cases = [_][]const u8{ "not a layout at all", "", seed_bytes }; | ||
| 1064 | for (cases) |bytes| { | ||
| 1065 | try std.testing.expectEqual( | ||
| 1066 | @as(?wall_layout.SeedPlan, null), | ||
| 1067 | wall_layout.seedLayout(alloc, &table, &shared, bytes, null), | ||
| 1068 | ); | ||
| 1069 | try std.testing.expectEqual(@as(usize, 1), shared.tree.count()); | ||
| 1070 | } | ||
| 1071 | } | ||
| 1072 | |||
| 1073 | test "seed: more leaves than the terminal can cut seeds what fits and counts the rest" { | ||
| 1074 | const alloc = std.testing.allocator; | ||
| 1075 | var shared: Shared = undefined; | ||
| 1076 | seedShared(alloc, &shared); | ||
| 1077 | // Nine stacked panes at 3 rows each need 27; give them 13 rows, which | ||
| 1078 | // holds four. The boot must not refuse - relayout's TooSmall degrade | ||
| 1079 | // never runs on the first flatten, so the seed carries its own. | ||
| 1080 | shared.size = .{ .cols = 100, .rows = 13 }; | ||
| 1081 | defer shared.tree.deinit(); | ||
| 1082 | var table = [_]wall_host.Host{ | ||
| 1083 | fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"), | ||
| 1084 | }; | ||
| 1085 | var buf = std.ArrayListUnmanaged(u8){}; | ||
| 1086 | defer buf.deinit(alloc); | ||
| 1087 | try buf.appendSlice(alloc, "mux-layout 1\nstacked 0\n"); | ||
| 1088 | for (0..9) |i| try buf.writer(alloc).print(" leaf 10 --sock /tmp/h0.sock#s{d}\n", .{i}); | ||
| 1089 | var plan = wall_layout.seedLayout(alloc, &table, &shared, buf.items, null) orelse | ||
| 1090 | return error.SeedRefusedAFittableSidecar; | ||
| 1091 | defer plan.deinit(alloc); | ||
| 1092 | try std.testing.expectEqual(@as(usize, 4), plan.panes.len); | ||
| 1093 | try std.testing.expectEqual(@as(usize, 5), plan.dropped); | ||
| 1094 | try std.testing.expectEqual(@as(usize, 4), shared.tree.count()); | ||
| 1095 | } | ||
| 1096 | |||
| 1097 | test "seed: more leaves than max_tiles seeds max_tiles" { | ||
| 1098 | const alloc = std.testing.allocator; | ||
| 1099 | var shared: Shared = undefined; | ||
| 1100 | seedShared(alloc, &shared); | ||
| 1101 | // Tall enough that every kept pane fits: the cap under test is the | ||
| 1102 | // wall's, not the terminal's. | ||
| 1103 | shared.size = .{ .cols = 100, .rows = 500 }; | ||
| 1104 | defer shared.tree.deinit(); | ||
| 1105 | var table = [_]wall_host.Host{ | ||
| 1106 | fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"), | ||
| 1107 | }; | ||
| 1108 | var buf = std.ArrayListUnmanaged(u8){}; | ||
| 1109 | defer buf.deinit(alloc); | ||
| 1110 | try buf.appendSlice(alloc, "mux-layout 1\nstacked 0\n"); | ||
| 1111 | for (0..40) |i| try buf.writer(alloc).print(" leaf 10 --sock /tmp/h0.sock#s{d}\n", .{i}); | ||
| 1112 | var plan = wall_layout.seedLayout(alloc, &table, &shared, buf.items, null) orelse | ||
| 1113 | return error.SeedRefusedAFittableSidecar; | ||
| 1114 | defer plan.deinit(alloc); | ||
| 1115 | try std.testing.expectEqual(wv.max_tiles, plan.panes.len); | ||
| 1116 | try std.testing.expectEqual(@as(usize, 8), plan.dropped); | ||
| 1117 | } | ||
| 1118 | |||
| 1119 | test "seed: the saved focus comes back as the pane that carries it, and is null when that leaf was dropped" { | ||
| 1120 | const alloc = std.testing.allocator; | ||
| 1121 | var shared: Shared = undefined; | ||
| 1122 | seedShared(alloc, &shared); | ||
| 1123 | defer shared.tree.deinit(); | ||
| 1124 | var table = [_]wall_host.Host{ | ||
| 1125 | fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"), | ||
| 1126 | fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"), | ||
| 1127 | }; | ||
| 1128 | var plan = wall_layout.seedLayout(alloc, &table, &shared, seed_bytes, null) orelse | ||
| 1129 | return error.SeedRefusedAValidSidecar; | ||
| 1130 | try std.testing.expectEqual(@as(?usize, 1), plan.focus); | ||
| 1131 | plan.deinit(alloc); | ||
| 1132 | // The focused leaf (saved index 1, host 0's "b") dropped with its host | ||
| 1133 | // gone from the wall: nothing carries the focus record. | ||
| 1134 | shared.tree.deinit(); | ||
| 1135 | shared.tree = layout.Tree.init(alloc); | ||
| 1136 | var one = [_]wall_host.Host{ | ||
| 1137 | fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"), | ||
| 1138 | }; | ||
| 1139 | var plan2 = wall_layout.seedLayout(alloc, &one, &shared, seed_bytes, null) orelse | ||
| 1140 | return error.SeedRefusedAValidSidecar; | ||
| 1141 | defer plan2.deinit(alloc); | ||
| 1142 | try std.testing.expectEqual(@as(?usize, null), plan2.focus); | ||
| 1143 | } | ||
| 1144 | |||
| 1145 | test "seed: the entry tile's own pane is not pending; a sidecar that does not know it inserts it" { | ||
| 1146 | const alloc = std.testing.allocator; | ||
| 1147 | var shared: Shared = undefined; | ||
| 1148 | seedShared(alloc, &shared); | ||
| 1149 | defer shared.tree.deinit(); | ||
| 1150 | var table = [_]wall_host.Host{ | ||
| 1151 | fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"), | ||
| 1152 | fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"), | ||
| 1153 | }; | ||
| 1154 | // Matched: the entry stands where its saved pane was, id 0, not | ||
| 1155 | // pending, and no second pane claims its (host, session). | ||
| 1156 | var plan = wall_layout.seedLayout(alloc, &table, &shared, seed_bytes, "--sock /tmp/h0.sock#a") orelse | ||
| 1157 | return error.SeedRefusedAValidSidecar; | ||
| 1158 | try std.testing.expectEqual(@as(usize, 3), plan.panes.len); | ||
| 1159 | try std.testing.expectEqual(@as(?wall_layout.SeedPane, null), plan.panes[0]); | ||
| 1160 | try std.testing.expect(plan.panes[1] != null and plan.panes[2] != null); | ||
| 1161 | try std.testing.expectEqual(@as(usize, 3), shared.tree.count()); | ||
| 1162 | plan.deinit(alloc); | ||
| 1163 | // Unmatched: the sidecar predates this entry; its pane is inserted | ||
| 1164 | // beside the saved focus, and every saved leaf is still a pane. | ||
| 1165 | shared.tree.deinit(); | ||
| 1166 | shared.tree = layout.Tree.init(alloc); | ||
| 1167 | var plan2 = wall_layout.seedLayout(alloc, &table, &shared, seed_bytes, "--sock /tmp/h9.sock#z") orelse | ||
| 1168 | return error.SeedRefusedAValidSidecar; | ||
| 1169 | defer plan2.deinit(alloc); | ||
| 1170 | try std.testing.expectEqual(@as(usize, 4), plan2.panes.len); | ||
| 1171 | try std.testing.expectEqual(@as(?wall_layout.SeedPane, null), plan2.panes[0]); | ||
| 1172 | try std.testing.expectEqual(@as(usize, 4), shared.tree.count()); | ||
| 1173 | } | ||