From edad6d960dc0fc1165cbb3b801e0538045b2247f Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 1 Oct 2026 10:09:54 -0300 Subject: 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 --- src/ninep/ctl.zig | 103 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 10 deletions(-) (limited to 'src/ninep/ctl.zig') 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(); -- cgit v1.3