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/tree.zig | 58 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 6 deletions(-) (limited to 'src/ninep/tree.zig') diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 40543ecf..5499a337 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -89,6 +89,7 @@ pub fn needsQuiet(p: *Pardes, req: Req) bool { return switch (req.op) { .write, .setattr => true, .release => req.remove or holdsLines(p, req), + .read => holdsPage(p, req), else => false, }; } @@ -99,8 +100,9 @@ pub fn changesPane(p: *Pardes, req: Req) bool { return switch (req.op) { .write, .setattr => true, .open => req.node == @intFromEnum(TopFile.new), - .release => req.remove or holdsLines(p, req), - .lookup, .getattr, .read, .readdir => false, + .release => req.remove or holdsLines(p, req) or holdsPage(p, req), + .read => holdsPage(p, req), + .lookup, .getattr, .readdir => false, }; } @@ -138,6 +140,10 @@ pub const Open = struct { /// `/listeners`, a ctl opened to read) frozen at the open, as /// `/screen` is: reads in several chunks never splice two moments. frozen: []const u8, + /// A /pager open past its directory line: the +Pager's serial, the + /// text after that line held in `pending` until its read puts it + /// in (ctl.fillPager), or its close hands it to the editor's step. + pager: u32, } = .{ .snapshot = null }, /// What a write to a file of command lines left unfinished: a last line /// with no newline yet, or an Edit block still open. A write through a @@ -194,7 +200,7 @@ pub const Open = struct { gpa.free(run.line); }, .frozen => |bytes| gpa.free(bytes), - .event, .pty_data, .ctl, .lines, .writes => {}, + .event, .pty_data, .ctl, .lines, .writes, .pager => {}, } o.pending.deinit(gpa); if (o.replay) |r| gpa.free(r.bytes); @@ -899,7 +905,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { pn.fs.event_reader = pn.fs.event_reader or reader; }, .pty_data => pane_of.?.fs.pty_reader = true, - .run, .ctl, .lines, .writes => {}, + .run, .ctl, .lines, .writes, .pager => {}, } return .{ .tag = req.tag, .handle = @intCast(i + 1) }; } @@ -961,6 +967,12 @@ fn releaseHandle(p: *Pardes, req: Req) void { if (t == .col and t.col.file == .tag) cols.released(p, t.col.serial); if (t == .top and t.top == .tag) cols.released(p, null); const o = openOf(p, req) orelse return; + // A /pager open's text, not read for, goes in at the editor's next + // step (ctl.fillClosedPagers): this close need not wait for quiet. + if (o.what == .pager and o.pending.items.len > 0) { + p.fs.pages.append(p.gpa, .{ .serial = o.what.pager, .bytes = o.pending }) catch o.pending.deinit(p.gpa); + o.pending = .empty; + } // The last line an open wrote with no newline after it runs now. if (o.pending.items.len > 0) { const rest = p.scratch.allocator().dupe(u8, o.pending.items) catch ""; @@ -990,7 +1002,7 @@ fn releaseHandle(p: *Pardes, req: Req) void { .ctl => if (pn.fs.lock == req.handle) { pn.fs.lock = null; }, - .snapshot, .log, .lines, .writes, .frozen => {}, + .snapshot, .log, .lines, .writes, .frozen, .pager => {}, }; o.deinit(p.gpa); } @@ -1014,6 +1026,8 @@ fn setattr(p: *Pardes, req: Req, target: Target) Reply { } fn read(p: *Pardes, req: Req, target: Target) Reply { + // A /pager open's text goes in before its read answers the pane. + if (openOf(p, req)) |o| if (o.what == .pager) ctl.fillPager(p, o.what.pager, &o.pending) catch return Reply.fail(req.tag, E.NOMEM); // A listing reads what it was at the open. if (openOf(p, req)) |o| if (o.what == .frozen) { const bytes = o.what.frozen; @@ -1158,7 +1172,7 @@ fn linesOpen(target: Target, req: Req) bool { /// A release that will run a line its open held back. fn holdsLines(p: *Pardes, req: Req) bool { const o = openOf(p, req) orelse return false; - return o.pending.items.len > 0; + return o.pending.items.len > 0 and o.what != .pager; } /// A line held back longer than this is refused rather than kept growing. @@ -1173,7 +1187,39 @@ fn ctlFile(target: Target) bool { }; } +/// /pager on an open: its first line names the directory and runs as any +/// line does; what follows it, in that write and the open's later ones, is +/// a program's output, escapes and all, held whole and never run as lines. fn write(p: *Pardes, req: Req, target: Target) Reply { + const o = (if (target == .top and target.top == .pager) openOf(p, req) else null) orelse return writeLines(p, req, target); + if (o.what == .pager) { + if (o.pending.items.len + req.data.len > pager_cap) return failText(req.tag, E.NOSPC, "pager: no space: the text is over a file's limit"); + o.pending.appendSlice(p.gpa, req.data) catch return Reply.fail(req.tag, E.NOMEM); + return .{ .tag = req.tag, .written = @intCast(req.data.len) }; + } + const nl = std.mem.indexOfScalar(u8, req.data, '\n') orelse return writeLines(p, req, target); + var head = req; + head.data = req.data[0 .. nl + 1]; + const reply = writeLines(p, head, target); + if (reply.status != .ok or o.results.len == 0) return reply; + o.what = .{ .pager = o.results.list[0] }; + o.pending.clearRetainingCapacity(); + o.pending.appendSlice(p.gpa, req.data[nl + 1 ..]) catch return Reply.fail(req.tag, E.NOMEM); + return .{ .tag = req.tag, .written = @intCast(req.data.len) }; +} + +/// The most a /pager open holds: a file's limit, and `pardes -`'s note +/// that its stdin was cut there. +const pager_cap = @import("../memory.zig").limits.max_file_bytes + 4096; + +/// A /pager open holding text: its read puts it in (ctl.fillPager), its +/// close hands it to the editor's step. +pub fn holdsPage(p: *Pardes, req: Req) bool { + const o = openOf(p, req) orelse return false; + return o.what == .pager and o.pending.items.len > 0; +} + +fn writeLines(p: *Pardes, req: Req, target: Target) Reply { // A command line holds no control character but a tab: the whole write // is refused at once, not held to fail unseen at the close. if (resultsFile(target) or ctlFile(target)) for (req.data) |c| if ((c < ' ' and c != '\t' and c != '\n' and c != '\r') or c == 0x7f) return failText(req.tag, E.INVAL, ctl.e_control); -- cgit v1.3