src/tui/wall_test_wall.zig
Ref: Size: 60.7 KiB History
//! The wall itself: tiles, focus, births, endings and label bars.
const std = @import("std");
const proto = @import("term").protocol;
const client = @import("client");
const interact = @import("interact.zig");
const fixture = @import("wall_test_harness.zig");
const wall_host = @import("wall_host.zig");
const wall_layout = @import("wall_layout.zig");
const wall_pump = @import("wall_pump.zig");
const wv = @import("wallview.zig");
const EndAction = wv.EndAction;
const EndReason = wv.EndReason;
const Resolved = wall_host.Resolved;
const Shared = wv.Shared;
const State = wv.State;
const Tile = wv.Tile;
test "birthTile: a chord-born tile creates and offers no agent" {
const alloc = std.testing.allocator;
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
defer shared.tree.deinit();
try shared.tree.addFirst(0);
var tiles: [2]Tile = undefined;
tiles[0] = .{
.r = .{ .target = .{ .sock = "/tmp/a" }, .label = "--sock /tmp/a", .session = "" },
.rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
.shared = &shared,
.idx = 0,
.wake_r = -1,
.wake_w = -1,
};
var present = [_]bool{ true, false };
var live: usize = 1;
const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" };
const at = wv.birthTile(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &.{}), .{
.r = r,
.from = 0,
.place = .beside_focus,
.creates = true,
.born_from = 0,
}) orelse return error.TestUnexpectedResult;
defer std.posix.close(tiles[at].wake_r);
defer std.posix.close(tiles[at].wake_w);
try std.testing.expectEqual(@as(usize, 1), at);
try std.testing.expect(tiles[at].creates);
try std.testing.expect(!tiles[at].r.agent);
try std.testing.expectEqual(@as(?usize, 0), tiles[at].born_from);
try std.testing.expect(present[1]);
try std.testing.expectEqual(@as(usize, 2), live);
// A tty wall draws a bar — derived from `is_tty`, so a birth cannot
// forget it and viewRows is right from the first attach.
try std.testing.expectEqual(@as(u16, 1), shared.labelRows());
}
test "the empty wall advertises the key the picker really answers to" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
const hint = wv.emptyWallHint(&shared);
// The sentence names a chord; this is what stops it outliving the
// binding. `Ctrl-\ s` in the text, `\x1c s` through the filter.
try std.testing.expect(std.mem.indexOf(u8, hint, "Ctrl-\\ s") != null);
var f: interact.PrefixFilter = .{};
var keys = "\x1cs".*;
try std.testing.expectEqual(interact.PrefixFilter.Action.pick_open, f.feed(&keys).action);
}
test "the empty-wall notice names the picker key" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
// The refusal a birth chord earns when the focused tile has no pump
// left. A SENTENCE, not code: a mechanical rename over `live` rewrote
// this line and three like it, and every gate stayed green because
// nothing read a notice back.
wv.setNotice(&shared, wv.no_live_here);
var buf: [96]u8 = undefined;
try std.testing.expectEqualStrings(
"[no live session here - Ctrl-\\ s picks a host]",
wv.takeNotice(&shared, &buf),
);
// And the way out it names is a chord the filter really answers to.
var f: interact.PrefixFilter = .{};
var keys = "\x1cs".*;
try std.testing.expectEqual(interact.PrefixFilter.Action.pick_open, f.feed(&keys).action);
}
test "emptyWallHint: a chord an empty wall refuses is said there, and said once" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
const way_out = "Ctrl-\\ s to pick a host - Ctrl-\\ d to leave";
try std.testing.expectEqualStrings(way_out, wv.emptyWallHint(&shared));
wv.setNotice(&shared, "[no session to birth beside - Ctrl-\\ s picks a host]");
shared.paint_mu.lock();
defer shared.paint_mu.unlock();
// An empty wall has no pump, and a notice is painted by the pump that
// claims the terminal — so this line is the only place it can be said.
try std.testing.expectEqualStrings(
"[no session to birth beside - Ctrl-\\ s picks a host]",
wv.emptyWallHint(&shared),
);
// Once: every relayout repaints the empty line, and a sentence that
// outlived the keystroke that earned it would never leave the screen.
try std.testing.expectEqualStrings(way_out, wv.emptyWallHint(&shared));
}
test "awaitDetach: a pump that has already returned is not waited on" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var shared: Shared = undefined;
fixture.stoppedWall(arena.allocator(), &shared);
var t: Tile = .{
.r = .{ .target = .{ .sock = "/tmp/a" }, .label = "--sock /tmp/a", .session = "0" },
.rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
.shared = &shared,
.idx = 0,
.wake_r = -1,
.wake_w = -1,
.alive = std.atomic.Value(bool).init(false),
};
// The pump is the only thread that sets `detach_ack`, and this one has
// returned: the wait can only ever run out its 400ms bound.
const began = std.time.milliTimestamp();
wv.awaitDetach(&t, &shared);
try std.testing.expect(std.time.milliTimestamp() - began < 100);
}
test "setNoticeIdle: a standing condition waits — a refusal the user just earned is not overwritten" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
var buf: [96]u8 = undefined;
// The poll re-derives `[+N not shown]` every second per host, and the
// slot is one: the earned sentence has to survive to a paint.
wv.setNotice(&shared, "[bad host: FlagLikeTarget]");
wv.setNoticeIdle(&shared, "[+2 not shown]");
try std.testing.expectEqualStrings("[bad host: FlagLikeTarget]", wv.takeNotice(&shared, &buf));
// Read, and the standing condition takes the slot it was waiting for.
wv.setNoticeIdle(&shared, "[+2 not shown]");
try std.testing.expectEqualStrings("[+2 not shown]", wv.takeNotice(&shared, &buf));
}
test "X on a tile whose daemon refused arms a second press, and the window closes after 3 s" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
defer shared.tree.deinit();
var t: Tile = .{
.r = .{ .target = .{ .sock = "/tmp/a" }, .label = "--sock /tmp/a", .session = "work" },
.rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
.shared = &shared,
.idx = 0,
.wake_r = -1,
.wake_w = -1,
};
// A first `X` is never a force: the daemon is the one that knows who
// else is attached, so the client asks before it insists.
try std.testing.expectEqual(client.SwitchIntent.end, wv.intentForEnd(&t, 1000));
wv.onEndReply(&t, .{ .accepted = false, .others = 2, .reason = "others attached" }, 1000);
try std.testing.expectEqual(@as(i64, 1000 + wv.end_arm_ms), t.end_armed_until.load(.acquire));
try std.testing.expectEqual(client.SwitchIntent.end_force, wv.intentForEnd(&t, 2500));
// The window SHUTS. Without this an `X` typed minutes later, on a
// session that has picked up watchers since, kills them without asking.
try std.testing.expectEqual(client.SwitchIntent.end, wv.intentForEnd(&t, 4001));
// An accepted end disarms rather than leaving the window standing: the
// reply for the tile that is going must not force the next one.
t.end_armed_until.store(9000, .release);
wv.onEndReply(&t, .{ .accepted = true, .others = 0, .reason = "" }, 5000);
try std.testing.expectEqual(@as(i64, 0), t.end_armed_until.load(.acquire));
}
test "n/p walk the wall's tiles: a hole is stepped over, and a wall of one has nowhere to go" {
// Three slots with the middle one vanished, because the walk `n` used
// to do was one daemon's session ring and the wall is every host's
// sessions — a fixture of two adjacent tiles cannot see the hole.
const three = [_]bool{ true, false, true };
try std.testing.expectEqual(@as(?usize, 2), wv.walkTiles(&three, 0, true));
try std.testing.expectEqual(@as(?usize, 0), wv.walkTiles(&three, 2, true));
try std.testing.expectEqual(@as(?usize, 2), wv.walkTiles(&three, 0, false));
try std.testing.expectEqual(@as(?usize, 0), wv.walkTiles(&three, 2, false));
// One tile is its own neighbour, which is not a move: a focus that
// "moves" to itself releases the terminal and re-claims it for nothing.
const one = [_]bool{ false, true, false };
try std.testing.expectEqual(@as(?usize, null), wv.walkTiles(&one, 1, true));
try std.testing.expectEqual(@as(?usize, null), wv.walkTiles(&one, 1, false));
const none = [_]bool{ false, false };
try std.testing.expectEqual(@as(?usize, null), wv.walkTiles(&none, 0, true));
}
test "birthTile: no room is null and the tree is left as it was" {
const alloc = std.testing.allocator;
// Four rows cannot hold two stacked tiles under a bar: `wallFloors`
// asks min_session_rows + 1 of each on a tty. The leaf count pins the
// undo — a birth that refused and kept its insert would leave the tree
// one leaf wider than the wall.
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 4 }, .is_tty = true };
defer shared.tree.deinit();
try shared.tree.addFirst(0);
var tiles: [2]Tile = undefined;
tiles[0] = .{
.r = .{ .target = .{ .sock = "/tmp/a" }, .label = "--sock /tmp/a", .session = "" },
.rect = .{ .top = 0, .left = 0, .rows = 4, .cols = 80 },
.shared = &shared,
.idx = 0,
.wake_r = -1,
.wake_w = -1,
};
var present = [_]bool{ true, false };
var live: usize = 1;
const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" };
try std.testing.expectEqual(@as(?usize, null), wv.birthTile(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &.{}), .{
.r = r,
.from = 0,
.place = .beside_focus,
.creates = true,
.born_from = 0,
}));
try std.testing.expectEqual(@as(usize, 1), live);
try std.testing.expect(!present[1]);
try std.testing.expectEqual(@as(usize, 1), shared.tree.count());
}
test "birthTile: a beside wall admits more panes than rows/3" {
const alloc = std.testing.allocator;
// Nine panes side by side on 24x200: every one keeps all 24 rows and
// 8 rails plus 9 columns of 21 fit inside 200. A rows/n admission test
// refuses this at the 9th (24/9 = 2), capping ANY terminal at rows/3
// panes however wide it is. The tree owns the answer.
const n = 9;
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 200, .rows = 24 }, .is_tty = false };
defer shared.tree.deinit();
try shared.tree.addFirst(0);
var tiles: [n]Tile = undefined;
var present = [_]bool{false} ** n;
var live: usize = 0;
var i: u8 = 0;
while (i < n - 1) : (i += 1) {
if (i > 0) {
try shared.tree.insert(i - 1, i);
// `insert` wraps a bare root leaf in `.stacked`; a wide
// terminal's wall is cut the other way (`rootOrient`).
if (i == 1) shared.tree.setRootOrient(.beside);
}
tiles[i] = .{
.r = .{ .target = .{ .sock = "/tmp/a" }, .label = "--sock /tmp/a", .session = "" },
.rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 21 },
.shared = &shared,
.idx = i,
.wake_r = -1,
.wake_w = -1,
};
present[i] = true;
live += 1;
}
const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" };
const at = wv.birthTile(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &.{}), .{
.r = r,
.from = n - 2,
.place = .beside_focus,
.creates = true,
.born_from = n - 2,
// Borrowed, so the tile gets copies of its own: these are literals,
// and `Birth.borrowed = false` promises the slot's next reuse two
// slices it may free.
.borrowed = true,
}) orelse return error.TestUnexpectedResult;
defer std.posix.close(tiles[at].wake_r);
defer std.posix.close(tiles[at].wake_w);
defer alloc.free(tiles[at].r.session);
defer alloc.free(tiles[at].r.label);
try std.testing.expectEqual(@as(usize, n), shared.tree.count());
// Admitted AND habitable: every pane keeps the full height and clears
// the column floor, which is what makes the refusal wrong.
const flat = try shared.tree.flatten(alloc, 24, 200, wall_layout.wallFloors(true), null);
defer flat.deinit(alloc);
try std.testing.expectEqual(@as(usize, n), flat.placed.len);
for (flat.placed) |p| {
try std.testing.expectEqual(@as(u16, 24), p.rect.rows);
try std.testing.expect(p.rect.cols >= proto.min_session_cols);
}
}
test "birthTile: a vanished digit is taken back, and not before its pump returned" {
const alloc = std.testing.allocator;
// Five slots on a wall wide enough for all of them: the claim is which
// digit a birth lands on, so no refusal may decide it instead.
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 200, .rows = 24 }, .is_tty = false };
defer shared.tree.deinit();
try shared.tree.addFirst(0);
var tiles: [5]Tile = undefined;
var present = [_]bool{ true, false, false, false, false };
var live: usize = 1;
const target: client.Target = .{ .sock = "/tmp/a" };
tiles[0] = .{
.r = .{ .target = target, .label = "--sock /tmp/a#0", .session = "0" },
.rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 200 },
.shared = &shared,
.idx = 0,
.wake_r = -1,
.wake_w = -1,
};
// Every other tile is born the way every live birth road spells it —
// `Birth.borrowed`, so the tile takes its own copies — and those are the
// copies a reuse has to free.
defer for (tiles[1..], present[1..]) |*t, p| {
if (!p) continue;
alloc.free(t.r.session);
alloc.free(t.r.label);
std.posix.close(t.wake_r);
std.posix.close(t.wake_w);
};
const born = struct {
fn at(a: std.mem.Allocator, ts: []Tile, ps: []bool, lv: *usize, sh: *Shared, tg: client.Target, name: []const u8, host: ?usize) ?usize {
return wv.birthTile(fixture.wallOf(a, ts, ps, lv, sh, &.{}), .{
.r = .{ .target = tg, .label = "", .session = name },
.from = 0,
.place = .beside_focus,
.creates = true,
.born_from = 0,
.host = host,
.borrowed = true,
});
}
}.at;
try std.testing.expectEqual(@as(?usize, 1), born(alloc, &tiles, &present, &live, &shared, target, "1", 3));
try std.testing.expectEqual(@as(?usize, 2), born(alloc, &tiles, &present, &live, &shared, target, "2", 3));
// The middle tile leaves, carrying a full set of dirt: every one of
// these is a field whose stale value would be a lie about the NEXT
// tile in the slot — a session that was never up, a list that never
// missed it, an `X` nobody pressed, a host that does not own it.
tiles[1].ever_up.store(true, .release);
tiles[1].missed_once = true;
tiles[1].end_armed_until.store(9000, .release);
const doorbell = tiles[1].wake_r;
wv.vanishTile(&tiles, &present, &shared, 1, null);
// Its pump has not returned, so the digit is not free yet: the birth
// appends rather than hand a live thread's `*Tile` to a new tile.
try std.testing.expectEqual(@as(?usize, 3), born(alloc, &tiles, &present, &live, &shared, target, "3", 3));
tiles[1].pump_done.store(true, .release);
try std.testing.expectEqual(@as(?usize, 1), born(alloc, &tiles, &present, &live, &shared, target, "1", null));
// Reuse, not growth: the wall is four tiles wide, not five.
try std.testing.expectEqual(@as(usize, 4), live);
try std.testing.expectEqual(@as(usize, 1), tiles[1].idx);
try std.testing.expect(!tiles[1].ever_up.load(.acquire));
try std.testing.expect(!tiles[1].missed_once);
try std.testing.expectEqual(@as(i64, 0), tiles[1].end_armed_until.load(.acquire));
try std.testing.expectEqual(@as(?usize, null), tiles[1].host);
try std.testing.expect(!tiles[1].pump_done.load(.acquire));
// The doorbell is the slot's, not the tile's: a fresh pipe per reuse
// would leak the fd pair the old one is never closed on.
try std.testing.expectEqual(doorbell, tiles[1].wake_r);
}
test "birthTile: the digit's next tile frees the copies an unborrowed birth handed over" {
const alloc = std.testing.allocator;
// `Birth.borrowed = false` hands the tile two slices outright, and the
// birth that takes its digit back is what frees them. The testing
// allocator is the whole oracle: a slice it never handed out aborts the
// free, and one nobody frees is reported leaked — which is what a
// caller passing a literal and a reuse that kept the old copies each
// look like.
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 200, .rows = 24 }, .is_tty = false };
defer shared.tree.deinit();
try shared.tree.addFirst(0);
var tiles: [3]Tile = undefined;
var present = [_]bool{ true, false, false };
var live: usize = 1;
const target: client.Target = .{ .sock = "/tmp/a" };
tiles[0] = .{
.r = .{ .target = target, .label = "--sock /tmp/a#0", .session = "0" },
.rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 200 },
.shared = &shared,
.idx = 0,
.wake_r = -1,
.wake_w = -1,
};
// The chord's road, verbatim: the caller allocates, `birthTile` takes
// the slices as they are.
const handed = struct {
fn birth(
a: std.mem.Allocator,
ts: []Tile,
ps: []bool,
lv: *usize,
sh: *Shared,
tg: client.Target,
name: []const u8,
) ?usize {
const session = a.dupe(u8, name) catch return null;
const label = wv.tileLabel(a, tg, session) catch {
a.free(session);
return null;
};
return wv.birthTile(fixture.wallOf(a, ts, ps, lv, sh, &.{}), .{
.r = .{ .target = tg, .label = label, .session = session },
.from = 0,
.place = .beside_focus,
.creates = true,
.born_from = 0,
});
}
}.birth;
const first = handed(alloc, &tiles, &present, &live, &shared, target, "1") orelse
return error.TestUnexpectedResult;
try std.testing.expectEqual(@as(usize, 1), first);
defer if (present[1]) {
alloc.free(tiles[1].r.session);
alloc.free(tiles[1].r.label);
std.posix.close(tiles[1].wake_r);
std.posix.close(tiles[1].wake_w);
};
wv.vanishTile(&tiles, &present, &shared, 1, null);
tiles[1].pump_done.store(true, .release);
// The same digit, so the free under test is one this birth really did.
try std.testing.expectEqual(
@as(?usize, 1),
handed(alloc, &tiles, &present, &live, &shared, target, "2"),
);
}
test "labelText: the state word survives truncation at every width" {
var buf: [256]u8 = undefined;
// A label longer than any bar, so truncation is what is under test and
// not the label's own length.
const long = "x" ** 400;
// The markers are the ones a bar actually carries: three bytes with the
// chord digit, four for tiles 10 and up. Every one of them comes out of
// the label's cut, so the marker width is part of what this pins — a
// two-byte marker exercises no bar that ships.
// Narrow: the label is cut, the bar fits the terminal, the state stays.
const narrow = wv.labelText(&buf, 30, "1> ", long, "up");
if (!std.mem.endsWith(u8, narrow, " [up]")) return error.OneDigitNarrowBarLostTheStateWord;
if (narrow.len > 30) return error.OneDigitNarrowBarOverranTheTerminal;
// The same width with the widest marker: the two extra bytes come off
// the label, never off the state.
const narrow2 = wv.labelText(&buf, 30, "12 ", long, "up");
if (!std.mem.endsWith(u8, narrow2, " [up]")) return error.TwoDigitNarrowBarLostTheStateWord;
if (narrow2.len > 30) return error.TwoDigitNarrowBarOverranTheTerminal;
// Wide: `cols` alone would have asked for more than `buf` holds, and the
// failed bufPrint used to hand back the buffer's undefined bytes — with
// the state word among what was lost.
const wide = wv.labelText(&buf, 300, "12> ", long, "reconnecting");
if (!std.mem.endsWith(u8, wide, " [reconnecting]")) return error.TwoDigitWideBarLostTheStateWord;
if (wide.len > buf.len) return error.TwoDigitWideBarOverranItsBuffer;
// Wider than `buf` with the longest status word a bar can carry: the
// bound that matters is `buf`'s, and it is not `cols`'.
const widest = wv.labelText(&buf, 400, "12 ", long, State.reconnecting.word());
if (!std.mem.endsWith(u8, widest, " [reconnecting]")) return error.TwoDigitWidestBarLostTheStateWord;
if (widest.len > buf.len) return error.TwoDigitWidestBarOverranItsBuffer;
}
test "showsSelf: neither the wall nor the CLI attach opens the session the shell is standing in" {
const sock = "/run/user/1000/muxd.sock";
const target: client.Target = .{ .sock = sock };
// The case: `mux --session 1` typed in session 0's shell. Session 0 is
// on the host's own list, and tiling it would paint that session's
// stripe into the grid it is reading.
try std.testing.expect(wv.showsSelf(target, "0", sock, "0"));
// The empty name is the wire's spelling of the default, and the daemon
// plants the resolved one.
try std.testing.expect(wv.showsSelf(target, "", sock, "0"));
try std.testing.expect(wv.showsSelf(target, "work", sock, "work"));
// Every other session of the same daemon is exactly what the wall is
// for — it must not drop those.
try std.testing.expect(!wv.showsSelf(target, "1", sock, "0"));
try std.testing.expect(!wv.showsSelf(target, "", sock, "1"));
// A different daemon's socket is a different session whatever it is
// called.
try std.testing.expect(!wv.showsSelf(target, "0", "/run/user/1000/other.sock", "0"));
// Not a unix socket: a quic:// or --via target cannot be the daemon
// whose shell planted these.
try std.testing.expect(!wv.showsSelf(.{ .via = "ssh host mux d proxy" }, "0", sock, "0"));
// Unset, and the emptied spelling of unset that the self-attach
// refusal names as the way to override it.
try std.testing.expect(!wv.showsSelf(target, "0", null, "0"));
try std.testing.expect(!wv.showsSelf(target, "0", sock, null));
try std.testing.expect(!wv.showsSelf(target, "0", "", "0"));
try std.testing.expect(!wv.showsSelf(target, "0", sock, ""));
}
test "sendKeys: a chunk that does not fit is dropped whole, and says so" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
var t = Tile{
.r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
.rect = .{ .top = 0, .left = 0, .rows = 4, .cols = 80 },
.shared = &shared,
.idx = 0,
// -1 both ends: `ring` writes to the doorbell and ignores the
// failure, which is exactly what it promises to do.
.wake_r = -1,
.wake_w = -1,
};
const head = "abc";
wv.sendKeys(&t, head);
try std.testing.expectEqual(@as(usize, 3), t.in_len);
try std.testing.expect(!t.in_dropped.load(.acquire));
// One byte more than the room left. The old code copied what fit.
const too_big = [_]u8{'z'} ** (wv.mailbox_max - 2);
wv.sendKeys(&t, &too_big);
try std.testing.expectEqual(@as(usize, 3), t.in_len);
try std.testing.expect(t.in_dropped.load(.acquire));
// ...and the mailbox still holds exactly what was typed before it, so
// the next chunk cannot splice onto a half-delivered one.
wv.sendKeys(&t, "def");
var out: [wv.mailbox_max]u8 = undefined;
try std.testing.expectEqualStrings("abcdef", wall_pump.takeKeys(&t, &out));
// Exactly filling it is not overflow — the bound is `>`, not `>=`.
const exact = [_]u8{'q'} ** wv.mailbox_max;
t.in_dropped.store(false, .release);
wv.sendKeys(&t, &exact);
try std.testing.expectEqual(@as(usize, wv.mailbox_max), t.in_len);
try std.testing.expect(!t.in_dropped.load(.acquire));
}
test "driftWord: says only what differs — version, staleness, both, or nothing" {
var buf: [wv.drift_max]u8 = undefined;
const same: proto.SessionsMeta = .{ .version = "1.0", .stale = false };
const old: proto.SessionsMeta = .{ .version = "0.9", .stale = false };
const same_stale: proto.SessionsMeta = .{ .version = "1.0", .stale = true };
const old_stale: proto.SessionsMeta = .{ .version = "0.9", .stale = true };
// The common case is silence: a daemon of our own version on its own
// living image, and a daemon too old to say anything at all.
try std.testing.expectEqualStrings("", wv.driftWord(&buf, "1.0", same));
try std.testing.expectEqualStrings("", wv.driftWord(&buf, "1.0", null));
try std.testing.expectEqualStrings("daemon 0.9", wv.driftWord(&buf, "1.0", old));
try std.testing.expectEqualStrings("daemon stale", wv.driftWord(&buf, "1.0", same_stale));
try std.testing.expectEqualStrings("daemon 0.9 stale", wv.driftWord(&buf, "1.0", old_stale));
// A client that does not know its own version cannot judge anyone
// else's, but a replaced image is the daemon's own report and stands.
try std.testing.expectEqualStrings("", wv.driftWord(&buf, "", old));
try std.testing.expectEqualStrings("daemon stale", wv.driftWord(&buf, "", old_stale));
}
test "labelText: a dropped chunk is narrated beside the state that caused it" {
// The bar's own format, asserted through the same truncation path the
// states go through, because "reconnecting, input dropped" is now the
// longest thing a bar can say and `buf`'s bound is what it tests.
var buf: [256]u8 = undefined;
const long = "x" ** 400;
const said = wv.labelText(&buf, 300, "> ", long, "reconnecting, input dropped");
try std.testing.expect(std.mem.endsWith(u8, said, " [reconnecting, input dropped]"));
try std.testing.expect(said.len <= buf.len);
}
test "noticeText: the banner says what the exit line says, whichever sentence it is" {
var buf: [128]u8 = undefined;
// Both sentences a vanish can carry. A notice that names a cause the
// stderr line contradicts is worse than either alone.
try std.testing.expectEqualStrings(
"[cannot create a new session (daemon full?)]",
wv.noticeText(&buf, "mux: cannot create a new session (daemon full?)"),
);
try std.testing.expectEqualStrings(
"[cannot create a new session (daemon unreachable)]",
wv.noticeText(&buf, "mux: cannot create a new session (daemon unreachable)"),
);
// A sentence that never wore the prefix keeps all of its words.
try std.testing.expectEqualStrings("[detached]", wv.noticeText(&buf, "detached"));
}
test "endAction: on a terminal the last tile's exit leaves an empty wall, not an exit" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
var tiles: [3]Tile = undefined;
const one = [_]bool{ true, false, false };
// `x` ends a session, never the program: the wall stays up with the
// picker over it, and the machines are still there to birth on. There
// is no tile left to hand the focus to, which is what `back` says.
fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
try std.testing.expectEqual(
EndAction{ .vanish = .{ .back = null, .msg = null } },
wv.endAction(&tiles, &one, 3, 0, true, true),
);
// The piped half is unchanged and load-bearing: a wall of one with no
// keyboard is what a script runs, and it exits with the shell's code.
fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
try std.testing.expectEqual(
EndAction{ .finish = .{ .code = 7, .msg = null } },
wv.endAction(&tiles, &one, 3, 0, true, false),
);
}
test "endAction: a birth the picker made into a standing wall is refused ON it, not out of mux" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
var tiles: [3]Tile = undefined;
const one = [_]bool{ true, false, false };
// The headline path: the wall's last session ends, the picker opens
// itself, Enter lands on a daemon that is full. There was no anchor to
// hand the focus back to, so `born_from` is null — and the ENTRY tile
// carries null for the opposite reason, which is why the flag exists.
fixture.endBench(&tiles, &shared, 0, .refused, 0, null);
tiles[0].keeps_wall = true;
try std.testing.expectEqual(
EndAction{ .vanish = .{ .back = null, .msg = "mux: cannot create a new session (daemon full?)" } },
wv.endAction(&tiles, &one, 3, 0, true, true),
);
// A box that never answered: the picker advertises `unreachable` rows
// as a place to press Enter, so this is a designed path, not a fault.
fixture.endBench(&tiles, &shared, 0, .lost, 0, null);
tiles[0].keeps_wall = true;
try std.testing.expectEqual(
EndAction{ .vanish = .{ .back = null, .msg = "mux: cannot create a new session (daemon unreachable)" } },
wv.endAction(&tiles, &one, 3, 0, true, true),
);
// Nothing refused anything here, and the wall still stands.
fixture.endBench(&tiles, &shared, 0, .no_thread, 0, null);
tiles[0].keeps_wall = true;
try std.testing.expectEqual(
EndAction{ .vanish = .{ .back = null, .msg = "mux: could not start a thread for this session" } },
wv.endAction(&tiles, &one, 3, 0, true, true),
);
// The entry tile of `mux TARGET` IS this mux, so its refusal is the
// program's: the sentence and the code. Byte for byte the inputs of the
// first block above — same reason, same terminal, same lone tile, same
// null `born_from` — and `keeps_wall` is what separates the two answers,
// which is the whole argument for keeping it. (The entry tile is also
// the one tile with `retry_cold` false, so a reader hunting for another
// distinguishing field will find that one. It is not the field to read:
// it answers whether a cold-lost link is worth redialling, and
// `endAction` must not change meaning when that rule does.)
fixture.endBench(&tiles, &shared, 0, .refused, 0, null);
try std.testing.expectEqual(
EndAction{ .finish = .{ .code = 1, .msg = "mux: attach refused or no state received (session full?)" } },
wv.endAction(&tiles, &one, 3, 0, true, true),
);
// ...and a piped `mux` has no wall to leave standing, flag or no flag.
fixture.endBench(&tiles, &shared, 0, .refused, 0, null);
tiles[0].keeps_wall = true;
try std.testing.expectEqual(
EndAction{ .finish = .{ .code = 1, .msg = "mux: attach refused or no state received (session full?)" } },
wv.endAction(&tiles, &one, 3, 0, true, false),
);
}
test "endAction: the only tile's ending is mux's, and so is any ending nobody can leave" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
var tiles: [3]Tile = undefined;
const one = [_]bool{ true, false, false };
const two = [_]bool{ true, true, false };
// One tile, shell exited: the code is mux's, which is what keeps `mux`
// something you can put in a script.
fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
try std.testing.expectEqual(
EndAction{ .finish = .{ .code = 7, .msg = null } },
wv.endAction(&tiles, &one, 3, 0, true, false),
);
// Two tiles: the exit vanishes — a dead stripe for a cleanly exited
// shell is noise — and the wall re-cuts onto the survivor.
fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
try std.testing.expectEqual(
EndAction{ .vanish = .{ .back = 1, .msg = null } },
wv.endAction(&tiles, &two, 3, 0, true, false),
);
// ...but not once stdin has closed. A piped `mux` with no keyboard
// waits in poll forever with the code in hand — reproduced at RC=124
// before this argument existed, which is the whole reason the rule is
// stated in one pure function and not at the read that happens to
// notice EOF.
fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
try std.testing.expectEqual(
EndAction{ .finish = .{ .code = 7, .msg = null } },
wv.endAction(&tiles, &two, 3, 0, false, false),
);
// A tile the user already forgot is not company: `present`, never the
// wall's length, is what "somewhere to go" counts.
const one_left = [_]bool{ true, false, false };
fixture.endBench(&tiles, &shared, 0, .exited, 3, null);
try std.testing.expectEqual(
EndAction{ .finish = .{ .code = 3, .msg = null } },
wv.endAction(&tiles, &one_left, 3, 0, true, false),
);
}
test "endAction: a refusal a chord earned goes back where the chord was typed" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
var tiles: [3]Tile = undefined;
const two = [_]bool{ true, true, false };
// The plain client's `.refused` recovery: the daemon would not create
// the session, and the chord-born tile leaves with no trace — the
// sentence is the record a script reads, the survivor the screen.
fixture.endBench(&tiles, &shared, 1, .refused, 1, 0);
try std.testing.expectEqual(
EndAction{ .vanish = .{ .back = 0, .msg = "mux: cannot create a new session (daemon full?)" } },
wv.endAction(&tiles, &two, 3, 1, true, false),
);
// It outranks the closed stdin, because vanishing lands on a live
// SESSION rather than on nothing: whatever ends that one ends the run.
fixture.endBench(&tiles, &shared, 1, .refused, 1, 0);
try std.testing.expectEqual(
EndAction{ .vanish = .{ .back = 0, .msg = "mux: cannot create a new session (daemon full?)" } },
wv.endAction(&tiles, &two, 3, 1, false, false),
);
// A daemon that never answered is not a daemon that answered and said
// no. `x` on the last LOCAL session ends the local daemon, so a wall
// whose own machine has gone is exactly how this arrives — and "full?"
// would send the user hunting for sessions that are not there.
fixture.endBench(&tiles, &shared, 1, .lost, 1, 0);
try std.testing.expectEqual(
EndAction{ .vanish = .{ .back = 0, .msg = "mux: cannot create a new session (daemon unreachable)" } },
wv.endAction(&tiles, &two, 3, 1, true, false),
);
// ...but a chord-born tile that HAD its session and lost the link later
// is not a failed creation at all: it keeps its rect and narrates, like
// any other lost tile.
fixture.endBench(&tiles, &shared, 1, .lost, 1, 0);
tiles[1].ever_up.store(true, .release);
try std.testing.expectEqual(
EndAction{ .refocus = 0 },
wv.endAction(&tiles, &two, 3, 1, true, false),
);
// A refusal with nowhere to fall back to is an ending like any other,
// and says the thing the plain client said.
fixture.endBench(&tiles, &shared, 0, .refused, 1, null);
const one = [_]bool{ true, false, false };
const out = wv.endAction(&tiles, &one, 3, 0, true, false);
try std.testing.expectEqual(@as(u8, 1), out.finish.code);
try std.testing.expectEqualStrings(
"mux: attach refused or no state received (session full?)",
out.finish.msg.?,
);
// ...and so is one whose fall-back tile the user forgot meanwhile.
fixture.endBench(&tiles, &shared, 1, .refused, 1, 0);
const gone_back = [_]bool{ false, true, false };
try std.testing.expectEqual(
@as(u8, 1),
wv.endAction(&tiles, &gone_back, 3, 1, true, false).finish.code,
);
}
test "endAction: an exited session's tile vanishes; a lost one narrates" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
var tiles: [3]Tile = undefined;
const two = [_]bool{ true, true, false };
// A clean exit is noise once it is over: the tile leaves, the wall
// re-cuts, and nothing is said.
fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
try std.testing.expectEqual(
EndAction{ .vanish = .{ .back = 1, .msg = null } },
wv.endAction(&tiles, &two, 3, 0, true, false),
);
// A lost link can heal: the tile keeps its rect and its bar, and the
// focus steps to the next present tile rather than the tile leaving.
fixture.endBench(&tiles, &shared, 0, .lost, 1, null);
try std.testing.expectEqual(
EndAction{ .refocus = 1 },
wv.endAction(&tiles, &two, 3, 0, true, false),
);
// The last present tile exiting is still mux's own ending, with the
// shell's code — `mux` in a script is the thing that ran the shell.
fixture.endBench(&tiles, &shared, 0, .exited, 3, null);
const one = [_]bool{ true, false, false };
try std.testing.expectEqual(
EndAction{ .finish = .{ .code = 3, .msg = null } },
wv.endAction(&tiles, &one, 3, 0, true, false),
);
}
test "endAction: a vanishing tile hands focus to a live neighbour, not a corpse" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
var tiles: [3]Tile = undefined;
const two = [_]bool{ true, true, false };
// Tile 0 (focused) has just exited — its pump is dead. Tile 1 is
// present but dead (pump ended, narrating a lost link). With no live
// neighbour, the back-target falls back to the dead tile (the wall
// still shows it), but with a live third tile, focus lands there.
fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
tiles[0].alive.store(false, .release);
tiles[1].alive.store(false, .release);
tiles[1].end.store(@intFromEnum(EndReason.lost), .release);
tiles[1].end_seen = true;
// Two tiles, both dead: fallback to the present-only step.
const out_dead = wv.endAction(&tiles, &two, 3, 0, true, false);
try std.testing.expect(out_dead == .vanish);
try std.testing.expectEqual(@as(usize, 1), out_dead.vanish.back);
// Three tiles: 0 dead ending, 1 dead narrating, 2 alive. Focus must
// land on tile 2, the live neighbour — never the dead tile 1.
const three = [_]bool{ true, true, true };
fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
tiles[0].alive.store(false, .release);
tiles[1].alive.store(false, .release);
tiles[1].end.store(@intFromEnum(EndReason.lost), .release);
tiles[1].end_seen = true;
const out_live = wv.endAction(&tiles, &three, 3, 0, true, false);
try std.testing.expect(out_live == .vanish);
try std.testing.expectEqual(@as(usize, 2), out_live.vanish.back);
}
test "closeNotice: a sentence the popup left behind is given a tile to land on" {
const alloc = std.testing.allocator;
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
const tiles = try alloc.alloc(Tile, 2);
defer alloc.free(tiles);
fixture.noticeBench(&shared, tiles);
const present = [_]bool{ true, true };
// Focus on tile 1, and the forgotten host owned NEITHER tile: the focus
// does not move on its own here, which is the whole of the finding.
shared.sel = 1;
wv.setNotice(&shared, "[hosts file not updated: read-only file system]");
wv.closeNotice(tiles, &present, &shared);
// Armed, not merely rung: a pump paints the notice on a CLAIM, and a
// doorbell alone gives it nothing to claim for.
if (!tiles[1].claim_pending.load(.acquire)) return error.NoticeHasNoClaimToRideOn;
// Still there for the pump to take on that claim, whole: a keyboard
// that consumed it here would paint nothing and lose the sentence.
var buf: [96]u8 = undefined;
try std.testing.expectEqualStrings(
"[hosts file not updated: read-only file system]",
wv.takeNotice(&shared, &buf),
);
// The control: with nothing to say, the close is the bare retry it was,
// and no tile is handed a claim it did not earn.
fixture.noticeBench(&shared, tiles);
wv.closeNotice(tiles, &present, &shared);
try std.testing.expect(!tiles[1].claim_pending.load(.acquire));
}
test "bare keys reach the focused tile; the prefix does not" {
var f: interact.PrefixFilter = .{};
// Bare keys are forwarded to the focused tile.
var keys: [3]u8 = .{ 'a', 'b', 'c' };
const a = f.feed(&keys);
try std.testing.expectEqualStrings("abc", a.forward);
try std.testing.expectEqual(interact.PrefixFilter.Action.none, a.action);
// A chord consumes its key and produces no forward bytes.
var chord: [2]u8 = .{ 0x1c, 'n' };
const b = f.feed(&chord);
try std.testing.expectEqual(@as(usize, 0), b.forward.len);
try std.testing.expectEqual(interact.PrefixFilter.Action.next_session, b.action);
}
test "focus follows a click: rectHit picks the tile under a row and column" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
var tiles: [2]Tile = undefined;
tiles[0] = Tile{
.r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
.rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
.shared = &shared,
.idx = 0,
.wake_r = -1,
.wake_w = -1,
};
tiles[1] = Tile{
.r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" },
.rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
.shared = &shared,
.idx = 1,
.wake_r = -1,
.wake_w = -1,
};
const present = [_]bool{ true, true };
// A click in row 3 hits tile 0.
try std.testing.expectEqual(@as(?usize, 0), wv.rectHit(&tiles, &present, &shared, 3, 0));
// A click in row 15 hits tile 1.
try std.testing.expectEqual(@as(?usize, 1), wv.rectHit(&tiles, &present, &shared, 15, 0));
// A beside pair: same rows, different columns. A click in the left
// half hits tile 0, the right half hits tile 1, and a rail column
// between them hits neither.
tiles[0].rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 39 };
tiles[1].rect = .{ .top = 0, .left = 41, .rows = 24, .cols = 39 };
try std.testing.expectEqual(@as(?usize, 0), wv.rectHit(&tiles, &present, &shared, 5, 10));
try std.testing.expectEqual(@as(?usize, 1), wv.rectHit(&tiles, &present, &shared, 5, 50));
try std.testing.expectEqual(@as(?usize, null), wv.rectHit(&tiles, &present, &shared, 5, 40));
}
test "setFocus writes the outgoing tile's release before the store that lets the next pump claim" {
// The keyboard thread's half of the terminal handover, and the order is
// load-bearing: two pumps are two threads, and A's release and B's claim
// race if the release is not on the wire first. The outgoing pump writes
// nothing (already_written), so if this stops emitting, nothing does —
// and the wall reports clicks into whatever shell the user lands in.
// is_tty is true because this leg is about what a focus move WRITES, and
// the gate that suppresses those writes on a pipe is exactly what it
// must not be testing.
const p = try std.posix.pipe2(.{ .NONBLOCK = true });
defer std.posix.close(p[0]);
defer std.posix.close(p[1]);
var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
var tiles: [2]Tile = undefined;
tiles[0] = Tile{
.r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" },
.rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
.shared = &shared,
.idx = 0,
.wake_r = -1,
.wake_w = -1,
};
tiles[1] = Tile{
.r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" },
.rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
.shared = &shared,
.idx = 1,
.wake_r = -1,
// The claim doorbell lands on the SAME pipe as the release: the
// write order on one fd is the only order a test can read, and the
// release must come first.
.wake_w = p[1],
};
shared.sel = 0;
var buf: [512]u8 = undefined;
// A move to the SAME tile has no outgoing session to take off, so it
// writes no release — a release here would unset modes the user's own
// terminal may have arrived with.
wv.setFocus(&tiles, &shared, 0);
try std.testing.expect(std.mem.indexOf(u8, fixture.readAvail(p[0], &buf), interact.session_release) == null);
// A -> B: the release is written under paint_mu, before sel is stored
// and before B is doorbelled to claim. On the one pipe both share that
// shows up as the release preceding the claim's wake byte.
wv.setFocus(&tiles, &shared, 1);
const out = fixture.readAvail(p[0], &buf);
const rel = std.mem.indexOf(u8, out, interact.session_release) orelse
return error.NoReleaseOnFocusMove;
const bell = std.mem.indexOfScalar(u8, out, 0) orelse
return error.NoClaimDoorbell;
try std.testing.expect(rel < bell);
// The outgoing pump is told to release, the incoming one to claim.
try std.testing.expect(tiles[0].release_pending.load(.acquire));
try std.testing.expect(tiles[1].claim_pending.load(.acquire));
try std.testing.expectEqual(@as(usize, 1), shared.sel);
}
test "setFocus re-arms the wall's own mouse capture itself — a parked pump cannot" {
// Clicking an [unreachable] tile is a designed act (birthing on one is
// too), and that tile's pump is parked in `dial`'s backoff loop: the
// focus claim that used to re-enable mouse reporting never runs there.
// The release alone then left the terminal deaf to every later click —
// including the one that would have moved focus back off the dead tile
// (found live 2026-09-02: one click on a dark laptop's pane killed the
// mouse for the whole wall until a keyboard chord landed on a live
// tile). The re-arm is the KEYBOARD's own write, after the release and
// before the doorbell, so the wall hears clicks with no pump running.
const p = try std.posix.pipe2(.{ .NONBLOCK = true });
defer std.posix.close(p[0]);
defer std.posix.close(p[1]);
var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
var tiles: [2]Tile = undefined;
tiles[0] = Tile{
.r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" },
.rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
.shared = &shared,
.idx = 0,
.wake_r = -1,
.wake_w = -1,
};
tiles[1] = Tile{
.r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" },
.rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
.shared = &shared,
.idx = 1,
.wake_r = -1,
.wake_w = p[1],
};
shared.sel = 0;
var buf: [512]u8 = undefined;
wv.setFocus(&tiles, &shared, 1);
const out = fixture.readAvail(p[0], &buf);
const rel = std.mem.indexOf(u8, out, interact.session_release) orelse
return error.NoReleaseOnFocusMove;
const arm = std.mem.indexOf(u8, out, interact.wall_mouse_capture) orelse
return error.NoWallRearm;
const bell = std.mem.indexOfScalar(u8, out, 0) orelse
return error.NoClaimDoorbell;
try std.testing.expect(rel < arm);
try std.testing.expect(arm < bell);
}
test "a focus move drops what the wall's input filters are holding" {
// The input-filter state that mattered — the drag — moved off the
// wall's keyboard loop and onto each tile's Core. setFocus drops it the
// same way relayout does: a repaint_gen bump every pump clears its
// core.drag on. The wall-level prefix is deliberately KEPT, because a
// chord is a wall command now and not a tile's; what stopped existing on
// a focus move is the screen the drag was drawn on, not a half-typed
// chord about it.
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
var tiles: [2]Tile = undefined;
tiles[0] = Tile{
.r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" },
.rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
.shared = &shared,
.idx = 0,
.wake_r = -1,
.wake_w = -1,
};
tiles[1] = Tile{
.r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" },
.rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
.shared = &shared,
.idx = 1,
.wake_r = -1,
.wake_w = -1,
};
shared.sel = 0;
const before = shared.repaint_gen.load(.acquire);
wv.setFocus(&tiles, &shared, 1);
try std.testing.expect(shared.repaint_gen.load(.acquire) > before);
}
test "setFocus: the marker moves between two tiles with no pump to move it" {
// A refused attach and an exited session both leave a tile
// whose pump has ended: repaint_gen and the doorbell reach nobody, so
// the bar keeps whatever marker it was born with. Two of them and
// Ctrl-\ 1 / Ctrl-\ 2 change nothing on screen; one of them beside a
// live tile shows two `>`. The keyboard is the only thread left that
// can paint these, which is what this pins.
const p = try std.posix.pipe2(.{ .NONBLOCK = true });
defer std.posix.close(p[0]);
defer std.posix.close(p[1]);
var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
var tiles: [3]Tile = undefined;
tiles[0] = Tile{
.r = .{ .target = .{ .sock = "/s" }, .label = "one", .session = "a" },
.rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
.shared = &shared,
.idx = 0,
.wake_r = -1,
.wake_w = -1,
.state = .refused,
.alive = std.atomic.Value(bool).init(false),
};
tiles[1] = Tile{
.r = .{ .target = .{ .sock = "/s" }, .label = "two", .session = "b" },
.rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
.shared = &shared,
.idx = 1,
.wake_r = -1,
.wake_w = -1,
.state = .refused,
.alive = std.atomic.Value(bool).init(false),
};
// Forgotten, not merely dead: `x` clears `present` and sets `removed`
// together, and a bar painted into a rect the layout no longer owns
// writes over a neighbour.
tiles[2] = Tile{
.r = .{ .target = .{ .sock = "/s" }, .label = "ghost", .session = "c" },
.rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
.shared = &shared,
.idx = 2,
.wake_r = -1,
.wake_w = -1,
.state = .exited,
.alive = std.atomic.Value(bool).init(false),
.removed = std.atomic.Value(bool).init(true),
};
shared.sel = 0;
wv.setFocus(&tiles, &shared, 1);
var buf: [2048]u8 = undefined;
const out = fixture.readAvail(p[0], &buf);
// The bar is one contiguous write (`paintLabelLocked`), so the cursor
// move and the marker are adjacent bytes: matching them together is
// what says the marker landed in THIS tile's bar and not a neighbour's.
if (std.mem.indexOf(u8, out, "\x1b[13;1H\x1b[7m 2> two") == null)
return error.DeadTileNeverGainsTheMarker;
if (std.mem.indexOf(u8, out, "\x1b[1;1H\x1b[7m 1 one") == null)
return error.DeadTileNeverDropsTheMarker;
if (std.mem.indexOf(u8, out, "ghost") != null)
return error.ForgottenTilePaintedItsBar;
}
test "the bar's digit is the tile's chord: idx + 1, and a two-digit tile still says where it is" {
// Ctrl-\ 1-9 focuses by POSITION (tiles[idx - 1]) while the bar named the
// session (#0, #2...): two series that stop lining up the moment a tile is
// forgotten or a session dies. The digit on the bar is the chord's, so the
// two cannot diverge.
const p = try std.posix.pipe2(.{ .NONBLOCK = true });
defer std.posix.close(p[0]);
defer std.posix.close(p[1]);
var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
shared.sel = 0;
var tiles: [3]Tile = undefined;
const labels = [_][]const u8{ "one", "two", "twelve" };
// idx 11 with no tiles 3..10: the digit is the tile's own idx, never a
// count of what is on screen. Past 9 the chord cannot reach it, but the
// number still says where it sits — the chord's reach is the chord's
// limit, not the bar's.
const idxs = [_]usize{ 0, 1, 11 };
for (&tiles, labels, idxs, 0..) |*t, label, idx, row| {
t.* = Tile{
.r = .{ .target = .{ .sock = "/s" }, .label = label, .session = "a" },
.rect = .{ .top = @intCast(row * 8), .left = 0, .rows = 8, .cols = 80 },
.shared = &shared,
.idx = idx,
.wake_r = -1,
.wake_w = -1,
.state = .up,
.alive = std.atomic.Value(bool).init(false),
};
wv.paintLabelLocked(t);
}
var buf: [2048]u8 = undefined;
const out = fixture.readAvail(p[0], &buf);
// Cursor move and marker together, as in the setFocus pin: that is what
// says the digit landed in THIS tile's bar and not a neighbour's.
if (std.mem.indexOf(u8, out, "\x1b[1;1H\x1b[7m 1> one") == null)
return error.FocusedBarLostItsChordDigit;
if (std.mem.indexOf(u8, out, "\x1b[9;1H\x1b[7m 2 two") == null)
return error.UnfocusedBarLostItsChordDigit;
if (std.mem.indexOf(u8, out, "\x1b[17;1H\x1b[7m 12 twelve") == null)
return error.TwoDigitTileLostItsNumber;
}
test "a vanished tile is stepped over" {
// A tile's end and a poll that no longer lists its session both refocus
// through stepPresent, and a click's rectHit and the Ctrl-\ l and 1-9
// chords check the same present array a vanish clears. Tiles are never
// compacted — pumps hold pointers into the array — so a vanished tile is
// a hole focus steps over, not a gap it fills. `removed` and present clear
// together in `vanishTile`, so stepping on present is stepping past
// `removed`.
const mid = [_]bool{ true, false, true };
try std.testing.expectEqual(@as(?usize, 2), wv.stepPresent(&mid, 0, true));
try std.testing.expectEqual(@as(?usize, 0), wv.stepPresent(&mid, 2, true));
try std.testing.expectEqual(@as(?usize, 0), wv.stepPresent(&mid, 2, false));
try std.testing.expectEqual(@as(?usize, 2), wv.stepPresent(&mid, 0, false));
// Standing on the hole (the moment after x, before the selection has
// moved) still steps to a real tile.
try std.testing.expectEqual(@as(?usize, 2), wv.stepPresent(&mid, 1, true));
try std.testing.expectEqual(@as(?usize, 0), wv.stepPresent(&mid, 1, false));
// The last tile gone: nothing is a selection any more, and every
// motion says so rather than picking a tile that is gone.
const none = [_]bool{ false, false };
try std.testing.expectEqual(@as(?usize, null), wv.stepPresent(&none, 0, true));
try std.testing.expectEqual(@as(?usize, null), wv.stepPresent(&none, 0, false));
// One survivor is its own answer, exactly as a one-tile wall is.
const one = [_]bool{ false, true, false };
try std.testing.expectEqual(@as(?usize, 1), wv.stepPresent(&one, 1, true));
try std.testing.expectEqual(@as(?usize, 1), wv.stepPresent(&one, 0, false));
}
test "tileBanner: a prompt wider than its pane shows the tail inside the pane" {
// A tile that painted a whole long spelling would run across the rail
// into its neighbour, so the banner is bounded by the tile's rect and
// not by the terminal. The tail is what is kept: the cursor end of the
// line the user is still typing.
const pipe = try std.posix.pipe();
defer std.posix.close(pipe[0]);
var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
var t = Tile{
.r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
.rect = .{ .top = 2, .left = 5, .rows = 5, .cols = 10 },
.shared = &shared,
.idx = 0,
.wake_r = -1,
.wake_w = -1,
};
const text = ": --sock /tmp/very/long/paths_";
try std.testing.expectEqual(@as(usize, 30), text.len);
wv.tileBanner(&t, text);
std.posix.close(pipe[1]);
var out: [256]u8 = undefined;
const n = try std.posix.read(pipe[0], &out);
const got = out[0..n];
// The tile's own corner: rect.top + labelRows + 1, rect.left + 1.
try std.testing.expect(std.mem.indexOf(u8, got, "\x1b[4;6H") != null);
const from = std.mem.indexOf(u8, got, "\x1b[7m").? + 4;
const to = std.mem.indexOf(u8, got, "\x1b[0m").?;
try std.testing.expectEqualStrings(text[text.len - 10 ..], got[from..to]);
}
test "endedTile: a pending pane is not an ended one - mux does not exit on a pane no pump ran for" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
var tiles = fixture.diffFixture(&shared);
const present = [_]bool{ true, true, true };
// Plural on purpose: two pending panes, and the focus sits on one of
// them - the resumed wall's exact first pass.
for (&tiles) |*t| t.alive.store(false, .release);
tiles[0].pending = true;
tiles[1].pending = true;
tiles[2].end.store(@intFromEnum(EndReason.exited), .release);
shared.sel = 0;
// The real exit among the pending panes is still found; neither
// pending pane is.
try std.testing.expectEqual(@as(?usize, 2), wv.endedTile(&tiles, &present, &shared));
try std.testing.expectEqual(@as(?usize, null), wv.endedTile(&tiles, &present, &shared));
try std.testing.expect(!tiles[0].end_seen and !tiles[1].end_seen);
// The other arm proves the guard is load-bearing: the same wall with
// `pending` off reads the focused pane as ended with reason `.none`.
tiles[0].pending = false;
try std.testing.expectEqual(@as(?usize, 0), wv.endedTile(&tiles, &present, &shared));
}
test "spawnPump: a remembered shape never dials - a pending tile gets no thread" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
var tiles = fixture.diffFixture(&shared);
shared.running.store(false, .release);
tiles[1].pending = true;
tiles[1].alive.store(false, .release);
tiles[1].pump_done.store(true, .release);
tiles[1].state = .waiting;
wv.spawnPump(&tiles[1]);
// A pump's first acts are `t.core = &core` and a `.connecting` label;
// a refused spawn leaves the seed's own settings alone. Bounded wait,
// so a pump that DID spawn has time to betray itself.
std.Thread.sleep(20 * std.time.ns_per_ms);
try std.testing.expect(!tiles[1].alive.load(.acquire));
try std.testing.expect(tiles[1].pump_done.load(.acquire));
try std.testing.expectEqual(State.waiting, tiles[1].state);
try std.testing.expectEqual(@as(?*interact.Core, null), tiles[1].core);
}
test "State.waiting has a word of its own, and truncation drops the label before it" {
var buf: [256]u8 = undefined;
const long = "x" ** 400;
const bar = wv.labelText(&buf, 30, "2> ", long, State.waiting.word());
if (!std.mem.endsWith(u8, bar, " [waiting]")) return error.WaitingBarLostTheStateWord;
if (bar.len > 30) return error.WaitingBarOverranTheTerminal;
}
test "removePane: the pane leaves the wall, its pump is told to detach, and nothing is asked to end" {
var shared = Shared{ .out_fd = -1, .size = .{ .cols = 120, .rows = 40 }, .is_tty = false };
defer shared.tree.deinit();
// The flats `relayout` cuts are the test allocator's, so the wall must
// free them with it: the default is the page allocator, and a leak here
// fails the whole file rather than this test.
shared.flat_alloc = std.testing.allocator;
defer if (shared.last_flat) |*f| f.deinit(std.testing.allocator);
defer if (shared.base_flat) |*f| f.deinit(std.testing.allocator);
var tiles = [_]Tile{ fixture.claimBench(&shared, 0), fixture.claimBench(&shared, 1) };
var present = [_]bool{ true, true };
// No pump is spawned here, so nothing will ever clear the liveness a
// tile is born with and `endPumps` would wait on it forever. `removePane`
// reads none of it: the detach is stored and the bell rung whatever the
// pump is doing, which is why this fixture can state it rather than race.
for (&tiles) |*t| t.alive.store(false, .release);
try shared.tree.addFirst(0);
try shared.tree.splitRight(0, 1);
const w = fixture.wallAll(std.testing.allocator, &tiles, &present, &shared);
shared.sel = 1;
wv.removePane(w, 1);
try std.testing.expect(!present[1]);
try std.testing.expect(present[0]);
try std.testing.expect(tiles[1].detach_req.load(.acquire));
// The daemon was NOT asked to end anything: the ask mailbox is empty.
try std.testing.expectEqual(@as(u8, 0), tiles[1].ask.load(.acquire));
try std.testing.expectEqual(@as(usize, 0), shared.sel);
var buf: [128]u8 = undefined;
try std.testing.expectEqualStrings("[pane removed - the session is still on its daemon]", wv.takeNotice(&shared, &buf));
fixture.endPumps(&tiles);
}