diff options
Diffstat (limited to 'src/ninep')
| -rw-r--r-- | src/ninep/ctl.zig | 103 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 58 |
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); |
