summaryrefslogtreecommitdiff
path: root/test/panes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-21 20:07:43 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit9070942b29bd10dddcdecdb0e88ba0fb40608467 (patch)
treefa1fc5984c7847c52bf4e78d277586edce2d8325 /test/panes.zig
parent0122e94fb37422085325f2dc78ca015051ce5f91 (diff)
downloadpardes-9070942b29bd10dddcdecdb0e88ba0fb40608467.tar.gz
pardes-9070942b29bd10dddcdecdb0e88ba0fb40608467.zip
Plan 9 idiom for the control filesystem, and the regressions a624a56 left
The 9P tree stops being a command language wearing a filesystem. /new created a pane as a side effect of a *read*; it is now Tcreate in /pane, with Tremove to close, which cloud9's engine has always supported and the editor never declared: tree.zig now says `features = .{ .create = true, .remove = true }`. Eleven pane ctl verbs become files that can be read as well as written -- dot, limit, dirty, mark, scroll, look, exec -- leaving ctl with `get`, the one verb no file would say better. Root /ctl splits into a read-only /status and the /look and /exec files whose write IS the click. stat carries real sizes where it used to answer 0, and qid versions track a pane's revision, so a client can poll for change without re-reading the body. Commit a624a56 moved raw-tty keys to an early-return branch that knew only Ctrl-B and bare Escape, and in the same edit deleted the paste branch below it. That cost Shift-Escape (the unconditional way out of tty mode) and both paste chords: Ctrl-V and Ctrl-Shift-V reached the child as keystrokes, so an agent CLI running in a pane took Ctrl-V for its image-paste binding and answered "No image found in clipboard". Both are restored, with tests. Nested detection was not subtly broken but deleted: 60367d8 removed nested.zig's process-ancestry walk and left "am I inside pardes" derived from PARDES_FORWARD_LOOK, which read "0" both for --nested and for "the listener did not come up". PARDES_PID now answers that question on its own, checked with kill(pid, 0); PARDES_9P and PARDES_PANE answer how to reach it; the flag is gone. The posted-9P registry also self-heals now -- a session that aborts cannot unlink its own socket, so posting sweeps entries whose target refuses a connection, symlinks only and on a definite ECONNREFUSED only. Elsewhere: tty scrolling is sticky-bottom, following new output only from the last row, with typing and entering raw mode snapping back to live; the boot layouts are a Boot enum instead of a chain of ifs, and the bare tty startup (Boot.tty, which main.zig names) opens an empty text pane under the shell while tests keep Boot.tty_shell; builtins announce themselves on the message row under a Verbose setting that is on by default; Config prints each setting the way you would type it back, so WindowOpacity 70 rather than "WindowOpacity: 70%"; LocationsConfig opens its window only when called bare; every tagline puts the word that closes the thing last, and a column now outlives its panes -- closing the last one leaves an empty pane, and only Delcol, newly on the column tagline, takes the column away. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'test/panes.zig')
-rw-r--r--test/panes.zig76
1 files changed, 71 insertions, 5 deletions
diff --git a/test/panes.zig b/test/panes.zig
index d3c29856..e4e677e4 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -731,6 +731,72 @@ const TtySelectionTests = struct {
}
}
}
+
+ /// Every byte the pane's program would have read off its pty since the
+ /// last call, joined.
+ fn childInput(p: *Pardes, buf: []u8) []const u8 {
+ var n: usize = 0;
+ while (p.nextEffect()) |effect| switch (effect) {
+ .write => |w| {
+ const bytes = w.bytes.slice();
+ if (n + bytes.len > buf.len) break;
+ @memcpy(buf[n..][0..bytes.len], bytes);
+ n += bytes.len;
+ },
+ else => {},
+ };
+ return buf[0..n];
+ }
+
+ // Raw tty mode forwards nearly every key to the child; the two paste
+ // chords are the exception, and forwarding them instead is what broke
+ // pasting into a terminal pane. An application that gets Ctrl-V rather
+ // than the text goes and reads the desktop clipboard on its own account,
+ // and a coding agent reading it is looking for an image, not for words.
+ test "raw tty paste chords type at the program instead of reaching it as keys" {
+ if (comptime !panes.Terminal.enabled) return error.SkipZigTest;
+ for ([_]bool{ false, true }) |bracketed| {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 12 });
+ defer p.deinit();
+ const pane = p.panes[0].?;
+ try std.testing.expectEqual(Pane.Mode.tty, pane.mode);
+ if (bracketed) p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?2004h" } });
+ try std.testing.expectEqual(bracketed, panes.Terminal.bracketedPaste(pane));
+ p.yank = try p.gpa.dupe(u8, "one\ntwo"); // what a `y` anywhere left behind
+ var buf: [256]u8 = undefined;
+ _ = childInput(p, &buf);
+
+ // Ctrl-V types the register straight at the program, asking the
+ // desktop for nothing. Under DECSET 2004 the program's own fences
+ // carry it, which is what stops readline RUNNING a multi-line paste.
+ p.update(.{ .key = .{ .cp = 'v', .ctrl = true } });
+ try std.testing.expectEqualStrings(
+ if (bracketed) "\x1b[200~one\ntwo\x1b[201~" else "one\rtwo",
+ childInput(p, &buf),
+ );
+ try std.testing.expect(p.clip_pending == null);
+
+ // Ctrl-Shift-V is the other store: it ASKS, types nothing yet, and
+ // the answer lands at the program rather than in an edit buffer. A
+ // host that folds the Shift into the letter says the same thing.
+ for ([_]Key{
+ .{ .cp = 'v', .ctrl = true, .shift = true },
+ .{ .cp = 'V', .ctrl = true },
+ }) |chord| {
+ p.update(.{ .key = chord });
+ try std.testing.expect(p.clip_pending != null);
+ try std.testing.expectEqualStrings("", childInput(p, &buf));
+ p.update(.{ .paste = "from-desktop" });
+ try std.testing.expect(p.clip_pending == null);
+ try std.testing.expectEqualStrings(
+ if (bracketed) "\x1b[200~from-desktop\x1b[201~" else "from-desktop",
+ childInput(p, &buf),
+ );
+ }
+ // ...and neither chord put the clipboard in the register on its way.
+ try std.testing.expectEqualStrings("one\ntwo", p.yank orelse "");
+ }
+ }
};
const ClickCursorTests = struct {
@@ -1616,7 +1682,7 @@ test "generated output refuses every pipe prompt before seeding the tag" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
const content = try p.gpa.dupe(u8, "generated\n");
- const pane = try panes.Output.open(p, 1, "/", .{ .cmd = .Help }, "", content);
+ const pane = try panes.Output.open(p, p.freeSlot().?, "/", .{ .cmd = .Help }, "", content);
p.active = 1;
for ([_]Key{ .{ .cp = '|' }, .{ .cp = '|', .alt = true }, .{ .cp = '!' }, .{ .cp = '!', .alt = true } }) |key| {
p.update(.{ .key = key });
@@ -1825,14 +1891,14 @@ test "file history construction failures preserve existing panes" {
defer if (incoming) |bytes| p.gpa.free(bytes);
failing.fail_index = failing.alloc_index + failure;
const result = switch (case) {
- .open => panes.File.open(p, 1, path, 0),
+ .open => panes.File.open(p, p.freeSlot().?, path, 0),
.restore => panes.File.restore(p, 1, .{
.kind = .file,
.tag = "",
.body = "",
.file = .{ .path = "/restored.zig", .content = "restored\n" },
}),
- .output => panes.Output.open(p, 1, "/", .{ .cmd = .Help }, "", incoming.?),
+ .output => panes.Output.open(p, p.freeSlot().?, "/", .{ .cmd = .Help }, "", incoming.?),
.tutor => p.openTutorView(1),
.replace => p.setTestFile("replacement\n"),
};
@@ -1900,14 +1966,14 @@ test "file history owns bounded snapshot storage without per-edit bookkeeping al
try std.testing.expectEqual(@as(usize, 0), history.redo_len);
}
-test "file history allocation precedes a filesystem read with side effects" {
+test "file history allocation precedes reading the served tree" {
var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
const p = try Pardes.init(failing.allocator(), .{ .tty_only = true, .cols = 40, .rows = 12 });
defer p.deinit();
const original = p.panes[0].?;
const serial = p.next_serial;
failing.fail_index = failing.alloc_index + 2;
- try std.testing.expectError(error.OutOfMemory, panes.File.open(p, 1, "/n/self/new", 0));
+ try std.testing.expectError(error.OutOfMemory, panes.File.open(p, p.freeSlot().?, "/n/self/index", 0));
try std.testing.expect(failing.has_induced_failure);
try std.testing.expectEqual(original, p.panes[0].?);
try std.testing.expectEqual(serial, p.next_serial);