summaryrefslogtreecommitdiff
path: root/src/ninep/tree.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/tree.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/tree.zig')
-rw-r--r--src/ninep/tree.zig58
1 files changed, 52 insertions, 6 deletions
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);