diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-21 20:07:43 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 9070942b29bd10dddcdecdb0e88ba0fb40608467 (patch) | |
| tree | fa1fc5984c7847c52bf4e78d277586edce2d8325 /test/panes.zig | |
| parent | 0122e94fb37422085325f2dc78ca015051ce5f91 (diff) | |
| download | pardes-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.zig | 76 |
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); |
