49eacdaf
fix: a picker birth onto a name the wall already has is refused to that pane
a73x 2026-09-03 05:20
Commit message
src/tui/wall_picker.zig
| Old | New | ||
|---|---|---|---|
| @@ -580,6 +580,24 @@ pub fn pickBirth(w: Wall, sel: usize) ?usize { | |||
| 580 | // `c` chord would have landed on, reached without a pump to ask. | 580 | // `c` chord would have landed on, reached without a pump to ask. |
| 581 | var name_buf: [proto.session_name_max]u8 = undefined; | 581 | var name_buf: [proto.session_name_max]u8 = undefined; |
| 582 | const name = client.nextFreeName(&name_buf, list); | 582 | const name = client.nextFreeName(&name_buf, list); |
| 583 | // The name is free on the DAEMON, which is not the same as free on this | ||
| 584 | // WALL. A daemon that restarted answers an empty list while the panes | ||
| 585 | // it used to serve stand there wearing `gone`, so the next free name is | ||
| 586 | // `0` and this would put a second `box#0` beside the first — two panes | ||
| 587 | // spelling one leaf, which `seedLayout` refuses as a repeat and which | ||
| 588 | // therefore costs the user the whole file at the next start. The pane | ||
| 589 | // that already spells it is the answer, and Enter there is the key that | ||
| 590 | // re-creates the session in its own rect. | ||
| 591 | if (paneOf(w, sel, name)) |at| { | ||
| 592 | wv.setFocus(w.liveTiles(), w.shared, at); | ||
| 593 | var buf: [96]u8 = undefined; | ||
| 594 | wv.setNotice(w.shared, std.fmt.bufPrint( | ||
| 595 | &buf, | ||
| 596 | "[{s} is already a pane - Enter there re-creates it]", | ||
| 597 | .{name}, | ||
| 598 | ) catch "[that session is already a pane]"); | ||
| 599 | return at; | ||
| 600 | } | ||
| 583 | const anchor = wall_layout.anchorTile(w.livePresent(), w.shared.sel); | 601 | const anchor = wall_layout.anchorTile(w.livePresent(), w.shared.sel); |
| 584 | const has_anchor = wv.presentCount(w.livePresent()) > 0; | 602 | const has_anchor = wv.presentCount(w.livePresent()) > 0; |
| 585 | // `-A` is inherited only within one host. A chord inherits it because | 603 | // `-A` is inherited only within one host. A chord inherits it because |
src/tui/wall_test_picker.zig
| Old | New | ||
|---|---|---|---|
| @@ -908,6 +908,51 @@ test "pickAdd: a listed session becomes a pane once; a second add zooms to it" { | |||
| 908 | try std.testing.expect(std.mem.indexOf(u8, wv.takeNotice(&shared, &buf), "no session") != null); | 908 | try std.testing.expect(std.mem.indexOf(u8, wv.takeNotice(&shared, &buf), "no session") != null); |
| 909 | } | 909 | } |
| 910 | 910 | ||
| 911 | test "pickBirth: the name a restarted daemon calls free is already a pane, and the birth is refused to it" { | ||
| 912 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | ||
| 913 | defer arena.deinit(); | ||
| 914 | const alloc = arena.allocator(); | ||
| 915 | var shared: Shared = undefined; | ||
| 916 | fixture.stoppedWall(alloc, &shared); | ||
| 917 | shared.size = .{ .cols = 120, .rows = 40 }; | ||
| 918 | // TWO hosts and the pane on the SECOND: a one-host fixture cannot see a | ||
| 919 | // birth that matched a pane on the wrong machine, and two daemons may | ||
| 920 | // each call their own next name `0`. | ||
| 921 | var hosts_table = [_]Host{ | ||
| 922 | fixture.testHost(&shared, "--sock /a", "/a"), | ||
| 923 | fixture.testHost(&shared, "--sock /b", "/b"), | ||
| 924 | }; | ||
| 925 | fixture.setList(&hosts_table[0], "0\n"); | ||
| 926 | fixture.setList(&hosts_table[1], "0\n"); | ||
| 927 | var tiles: [wv.max_tiles]Tile = undefined; | ||
| 928 | var present = [_]bool{false} ** wv.max_tiles; | ||
| 929 | var live: usize = 0; | ||
| 930 | defer fixture.endPumps(tiles[0..live]); | ||
| 931 | const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &hosts_table); | ||
| 932 | |||
| 933 | const pane = wall_picker.pickAdd(w, 1, 0).?; | ||
| 934 | try std.testing.expectEqualStrings("0", tiles[pane].r.session); | ||
| 935 | |||
| 936 | // The daemon restarts: it answers an empty list, the pane it used to | ||
| 937 | // serve stands there wearing `gone`, and `nextFreeName` off that list | ||
| 938 | // hands back `0` — the name the pane already spells. | ||
| 939 | fixture.setList(&hosts_table[1], ""); | ||
| 940 | var buf: [96]u8 = undefined; | ||
| 941 | _ = wv.takeNotice(&shared, &buf); | ||
| 942 | shared.sel = 0; | ||
| 943 | |||
| 944 | const at = wall_picker.pickBirth(w, 1); | ||
| 945 | // The pane the user meant, not a second tile onto one session: two | ||
| 946 | // panes spelling one leaf is a repeat, and `seedLayout` refuses a file | ||
| 947 | // with one WHOLE — the wall would be gone at the next start. | ||
| 948 | try std.testing.expectEqual(@as(?usize, pane), at); | ||
| 949 | try std.testing.expectEqual(@as(usize, 1), wv.presentCount(w.livePresent())); | ||
| 950 | try std.testing.expectEqual(pane, shared.sel); | ||
| 951 | const said = wv.takeNotice(&shared, &buf); | ||
| 952 | try std.testing.expect(std.mem.indexOf(u8, said, "already a pane") != null); | ||
| 953 | try std.testing.expect(std.mem.indexOf(u8, said, "0") != null); | ||
| 954 | } | ||
| 955 | |||
| 911 | test "paintPicker: the session level names the host and lists its sessions" { | 956 | test "paintPicker: the session level names the host and lists its sessions" { |
| 912 | const pipe = try std.posix.pipe2(.{ .NONBLOCK = true }); | 957 | const pipe = try std.posix.pipe2(.{ .NONBLOCK = true }); |
| 913 | defer std.posix.close(pipe[0]); | 958 | defer std.posix.close(pipe[0]); |