summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep')
-rw-r--r--src/ninep/ctl.zig103
-rw-r--r--src/ninep/tree.zig58
2 files changed, 145 insertions, 16 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();
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);