summaryrefslogtreecommitdiff
path: root/src/ninep/ctl.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 10:09:54 -0300
committerGabriel Schneider <[email protected]>2026-10-01 10:36:28 -0300
commitedad6d960dc0fc1165cbb3b801e0538045b2247f (patch)
tree0bedfbb9cd371e5d77cb697438c5198c688653c5 /src/ninep/ctl.zig
parent4db57bf5bd788a292fcf2cfbea3b6d8b281510f0 (diff)
downloadpardes-edad6d960dc0fc1165cbb3b801e0538045b2247f.tar.gz
pardes-edad6d960dc0fc1165cbb3b801e0538045b2247f.zip
A +Pager keeps its program's colours: the session parses what /pager is written after its directory with ghostty-vt, SGR becomes spans drawn over the plain text, every other escape is dropped, and PagerColor off pages it plain
Diffstat (limited to 'src/ninep/ctl.zig')
-rw-r--r--src/ninep/ctl.zig103
1 files changed, 93 insertions, 10 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index cd8bc6c9..d4751191 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -650,7 +650,8 @@ pub fn readRoot(p: *Pardes, req: Req) Reply {
/// /pager, `pardes -`'s: the directory written (empty, or not absolute: the
/// session's) gets its one +Pager, made or emptied; the serial is what a
-/// read of this same open answers, never another client's.
+/// read of this same open answers, never another client's. What the open
+/// writes after that line is the text, held by tree.zig for `fillPager`.
pub fn writePager(p: *Pardes, req: Req) Reply {
// A refused write leaves its open no answer, not the last one's.
p.fs.results_len = 0;
@@ -690,6 +691,35 @@ pub fn writePager(p: *Pardes, req: Req) Reply {
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
+/// The text a /pager open wrote after its directory (`held`), a program's
+/// output for a terminal, put in the +Pager `serial` as `pardes -` once
+/// wrote it to the body: through ghostty-vt (pager.zig), its colours kept
+/// as the pane's spans unless PagerColor is off, one undo step, and left
+/// clean. Reads of the pane are its plain text. The held bytes go either way.
+pub fn fillPager(p: *Pardes, serial: u32, held: *std.ArrayList(u8)) error{OutOfMemory}!void {
+ if (held.items.len == 0) return;
+ defer held.clearAndFree(p.gpa);
+ const id = p.paneBySerial(serial) orelse return;
+ const pane = p.panes[id].?;
+ const f = if (pane.file) |*file| file else return;
+ const page = try pardes.pager.parse(p.gpa, held.items, p.settings.pager_color);
+ held.clearAndFree(p.gpa);
+ if (page.text.len == 0 and f.content.len == 0) return page.deinit(p.gpa);
+ if (!pane.fs.nomark) panes.File.pushUndo(p, pane);
+ panes.File.setContent(p, f, page.text);
+ f.pager_spans = page.spans;
+ f.saved_revision = f.revision;
+}
+
+/// The editor's step: the text of /pager opens closed unread goes in.
+pub fn fillClosedPagers(p: *Pardes) void {
+ for (p.fs.pages.items) |*page| {
+ fillPager(p, page.serial, &page.bytes) catch {};
+ page.bytes.deinit(p.gpa);
+ }
+ p.fs.pages.clearRetainingCapacity();
+}
+
/// One session builtin a line, at whichever pane has the keyboard as each
/// runs, as the root's look and exec click there; all are checked first.
pub fn writeRoot(p: *Pardes, req: Req) Reply {
@@ -4088,21 +4118,26 @@ test "pager answers its +Pager on the open that asked, whatever another client e
// `pardes -` once read, and wrote its text into.
try testing.expect(!std.mem.eql(u8, want, rd(p, exec, 0, 64).bytes));
try testing.expectEqualStrings(want, call(p, .{ .tag = 5, .op = .read, .node = pager, .handle = asked, .size = 64 }).bytes);
- // Refused, the same open answers nothing, not its last answer.
- try testing.expectEqual(E.INVAL, call(p, .{ .tag = 6, .op = .write, .node = pager, .handle = asked, .data = "relative\n" }).errno());
- try testing.expectEqualStrings("", call(p, .{ .tag = 7, .op = .read, .node = pager, .handle = asked, .size = 64 }).bytes);
+ _ = call(p, .{ .tag = 6, .op = .release, .node = pager, .handle = asked, .opened = true });
+ // Refused, an open answers nothing, not the session's last answer.
+ const refused = call(p, .{ .tag = 7, .op = .open, .node = pager, .omode = 2 }).reply.handle;
+ try testing.expectEqual(E.INVAL, call(p, .{ .tag = 8, .op = .write, .node = pager, .handle = refused, .data = "relative\n" }).errno());
+ try testing.expectEqualStrings("", call(p, .{ .tag = 9, .op = .read, .node = pager, .handle = refused, .size = 64 }).bytes);
+ _ = call(p, .{ .tag = 10, .op = .release, .node = pager, .handle = refused, .opened = true });
// Resolved: `/tmp/../tmp/.` is /tmp's +Pager, the one made above.
- const again = call(p, .{ .tag = 8, .op = .open, .node = pager, .omode = 2 }).reply.handle;
- try testing.expectEqual(Status.ok, call(p, .{ .tag = 8, .op = .write, .node = pager, .handle = again, .data = "/tmp/../tmp/.\n" }).reply.status);
- try testing.expectEqualStrings(want, call(p, .{ .tag = 9, .op = .read, .node = pager, .handle = again, .size = 64 }).bytes);
- _ = call(p, .{ .tag = 9, .op = .release, .node = pager, .handle = again });
+ const resolved = call(p, .{ .tag = 11, .op = .open, .node = pager, .omode = 2 }).reply.handle;
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 12, .op = .write, .node = pager, .handle = resolved, .data = "/tmp/../tmp/.\n" }).reply.status);
+ try testing.expectEqualStrings(want, call(p, .{ .tag = 13, .op = .read, .node = pager, .handle = resolved, .size = 64 }).bytes);
+ _ = call(p, .{ .tag = 14, .op = .release, .node = pager, .handle = resolved, .opened = true });
// One that may not be written is refused, permission denied.
try testing.expectEqual(E.PERM, wr(p, pager, "/proc/1\n").errno());
// One directory a write, absolute, and there.
try testing.expectEqual(E.INVAL, wr(p, pager, "/a\n/b\n").errno());
- // A second line on an open that asked already, as a mount sends two.
+ // One directory an open: one refused is not followed by another, as a
+ // mount sends two lines in two writes. (After one taken, what follows
+ // is the paged text: the test below.)
const twice = call(p, .{ .tag = 20, .op = .open, .node = pager, .omode = 2 }).reply.handle;
- try testing.expectEqual(Status.ok, call(p, .{ .tag = 21, .op = .write, .node = pager, .handle = twice, .data = "/tmp\n" }).reply.status);
+ try testing.expectEqual(E.INVAL, call(p, .{ .tag = 21, .op = .write, .node = pager, .handle = twice, .data = "relative\n" }).errno());
try testing.expectEqual(E.INVAL, call(p, .{ .tag = 22, .op = .write, .node = pager, .handle = twice, .data = "/tmp\n" }).errno());
_ = call(p, .{ .tag = 23, .op = .release, .node = pager, .handle = twice });
const relative = wr(p, pager, "relative\n");
@@ -4112,6 +4147,54 @@ test "pager answers its +Pager on the open that asked, whatever another client e
try testing.expectEqual(E.NOTDIR, wr(p, pager, "/etc/passwd\n").errno());
}
+test "what a /pager open writes after its directory is the text: plain in the body, its colours the pane's, clean, in by the read" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const pager = @intFromEnum(tree.TopFile.pager);
+ const asked = call(p, .{ .tag = 1, .op = .open, .node = pager, .omode = 2 }).reply.handle;
+ // The directory and the text's start in one write, escapes and all, a
+ // second line no directory; the rest in writes after it.
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .write, .node = pager, .handle = asked, .data = "/tmp\n\x1b[31mred\x1b[m /a\n" }).reply.status);
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 3, .op = .write, .node = pager, .handle = asked, .data = "\x1b]0;title\x07N\x08Next\r\n" }).reply.status);
+ const id = for (p.panes, 0..) |slot, i| {
+ const pn = slot orelse continue;
+ if (pn.file) |f| if (std.mem.eql(u8, f.path, "/tmp/+Pager")) break i;
+ } else return error.NoPager;
+ const pn = p.panes[id].?;
+ try testing.expectEqualStrings("", pn.file.?.content);
+ // The read that answers the pane finds the text in.
+ var line: [16]u8 = undefined;
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&line, "{d}\n", .{pn.serial}), call(p, .{ .tag = 4, .op = .read, .node = pager, .handle = asked, .size = 64 }).bytes);
+ _ = call(p, .{ .tag = 5, .op = .release, .node = pager, .handle = asked, .opened = true });
+ const f = &pn.file.?;
+ try testing.expectEqualStrings("red /a\nNext\n", f.content);
+ try testing.expectEqualStrings("red /a\nNext\n", rd(p, Node.of(pn.serial, .body), 0, 64).bytes);
+ try testing.expectEqual(f.revision, f.saved_revision);
+ try testing.expectEqual(@as(usize, 2), f.pager_spans.len);
+ try testing.expectEqualStrings("red", f.content[f.pager_spans[0].start..f.pager_spans[0].end]);
+ try testing.expectEqual(pardes.Color{ .index = 1 }, f.pager_spans[0].style.fg);
+ try testing.expect(f.pager_spans[1].style.bold);
+ // One undo step back to the empty pane.
+ try testing.expect(f.history.undo_len > 0);
+ try testing.expectEqualStrings("", f.history.undo[f.history.undo_len - 1].content);
+ // An edit drops the colours (pager.zig's ponytail).
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 6, .op = .write, .node = Node.of(pn.serial, .body), .data = "more\n" }).reply.status);
+ try testing.expectEqual(@as(usize, 0), f.pager_spans.len);
+
+ // PagerColor off, and an open closed with no read: the same text, plain,
+ // in at the editor's next step (a close through a mount never waits).
+ p.settings.pager_color = false;
+ const shut = call(p, .{ .tag = 7, .op = .open, .node = pager, .omode = 1 }).reply.handle;
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 8, .op = .write, .node = pager, .handle = shut, .data = "/tmp\n" }).reply.status);
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 9, .op = .write, .node = pager, .handle = shut, .data = "\x1b[1;32mgreen\x1b[m\n" }).reply.status);
+ _ = call(p, .{ .tag = 10, .op = .release, .node = pager, .handle = shut, .opened = true });
+ try testing.expectEqualStrings("", f.content);
+ fillClosedPagers(p);
+ try testing.expectEqualStrings("green\n", f.content);
+ try testing.expectEqual(@as(usize, 0), f.pager_spans.len);
+ try testing.expectEqual(f.revision, f.saved_revision);
+}
+
test "a command pane an exec open was answered is leased to it while it stays open: another client's command gets a pane of its own" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();