summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/Output.zig43
-rw-r--r--src/main.zig88
-rw-r--r--src/ninep/ctl.zig12
-rw-r--r--test/fs.py25
4 files changed, 157 insertions, 11 deletions
diff --git a/src/Output.zig b/src/Output.zig
index 1160c455..e6448e36 100644
--- a/src/Output.zig
+++ b/src/Output.zig
@@ -666,6 +666,49 @@ pub fn openUnsaved(p: *Pardes, id: usize, content: []u8) void {
p.active = id;
}
+/// `pardes -`'s pane: the one `<dir>/+Pager`, emptied for the next paged
+/// text when it is there (a directory's paged commands refill it, as its
+/// command pane is reused), else made, named so before it is announced,
+/// and placed wherever there is room without taking the keyboard. `dir`
+/// empty (or not absolute): the session's directory. Its id; NoPaneRoom
+/// or NoPaneSlots when it has no room at all.
+pub fn openPager(p: *Pardes, from: usize, dir_arg: []const u8) !usize {
+ const dir = if (dir_arg.len > 0 and dir_arg[0] == '/') dir_arg else p.sessionDir();
+ const path = try std.fs.path.join(p.gpa, &.{ dir, "+Pager" });
+ for (p.panes, 0..) |slot, i| {
+ const pane = slot orelse continue;
+ const f = if (pane.file) |*file| file else continue;
+ if (!std.mem.eql(u8, f.path, path)) continue;
+ p.gpa.free(path);
+ File.setContent(p, f, try p.gpa.alloc(u8, 0));
+ resetBody(p, pane);
+ f.saved_revision = f.revision;
+ return i;
+ }
+ const free = p.freeSlot() orelse {
+ p.gpa.free(path);
+ return error.NoPaneSlots;
+ };
+ const empty = p.gpa.alloc(u8, 0) catch |err| {
+ p.gpa.free(path);
+ return err;
+ };
+ const pane = open(p, free, dir, .{ .cmd = .New }, "", empty) catch |err| {
+ p.gpa.free(path);
+ return err;
+ };
+ // The pane owns the name from here.
+ p.gpa.free(pane.file.?.path);
+ pane.file.?.path = path;
+ const keyboard = p.active;
+ exec.placeNew(p, from, null, free, .fs);
+ if (p.unplaced.isSet(free)) return error.NoPaneRoom;
+ // Shown, not asked for: the keyboard stays where it was, as a command
+ // pane's does.
+ if (p.panes[keyboard] != null) p.active = keyboard;
+ return free;
+}
+
/// Pane `gone` closed: its row goes from +Unsaved, which lists only panes
/// still there to lose.
pub fn dropUnsaved(p: *Pardes, gone: *const Pane) void {
diff --git a/src/main.zig b/src/main.zig
index 8818645e..6627bece 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -100,6 +100,58 @@ const help_text =
\\
;
+/// `pardes -` inside a session: `text` into the directory's one +Pager
+/// (root ctl `pager <dir>`, refilled when it is there), streamed into its
+/// body a bounded chunk at a time as one undo step, and left clean. A text
+/// the session would not take is never lost: it goes to stderr, with why,
+/// and the exit is 1.
+fn pageInto(io: std.Io, gpa: std.mem.Allocator, dial: []const u8, text: []const u8) void {
+ const Fail = struct {
+ fn with(o: std.Io, t: []const u8, what: []const u8, err: anyerror) noreturn {
+ var words_buf: [128]u8 = undefined;
+ const why = if (err == error.Remote) ninep_io.Client.lastSaid() else forwardWords(err, &words_buf);
+ var buf: [512]u8 = undefined;
+ const head = std.fmt.bufPrint(&buf, "pardes: -: {s}: {s}; the text follows\n", .{ what, why }) catch "pardes: -: refused; the text follows\n";
+ std.Io.File.stderr().writeStreamingAll(o, head) catch {};
+ std.Io.File.stderr().writeStreamingAll(o, t) catch {};
+ std.process.exit(1);
+ }
+ };
+ // The pane's directory: this one, unless its name could not be one line
+ // of a ctl (a newline, a control byte): then the session's.
+ var cwd_buf: [4096]u8 = undefined;
+ const cwd: []const u8 = if (std.c.getcwd(&cwd_buf, cwd_buf.len)) |c| std.mem.span(@as([*:0]u8, @ptrCast(c))) else "";
+ const plain = for (cwd) |c| {
+ if (c < ' ' or c == 0x7f) break false;
+ } else true;
+ var line_buf: [4200]u8 = undefined;
+ const ctl_line = std.fmt.bufPrint(&line_buf, "pager {s}\n", .{if (plain) cwd else ""}) catch "pager\n";
+ ninep_io.Client.write(gpa, dial, "/ctl", ctl_line) catch |err| Fail.with(io, text, "no +Pager pane", err);
+ const answer = ninep_io.Client.read(gpa, dial, "/exec", "/exec") catch |err| Fail.with(io, text, "no +Pager pane", err);
+ const serial = std.fmt.parseInt(u32, std.mem.trim(u8, answer, " \n"), 10) catch |err| Fail.with(io, text, "no +Pager pane", err);
+ var path_buf: [64]u8 = undefined;
+ const mark = std.fmt.bufPrint(&path_buf, "/pane/{d}/mark", .{serial}) catch unreachable;
+ // One undo step for the whole text, however many writes carry it.
+ ninep_io.Client.write(gpa, dial, mark, "0\n") catch |err| Fail.with(io, text, "the +Pager pane went", err);
+ var body_buf: [64]u8 = undefined;
+ const body = std.fmt.bufPrint(&body_buf, "/pane/{d}/body", .{serial}) catch unreachable;
+ // A chunk ends on a line's end (never inside a character), each its own
+ // write with its own deadline, so a large text arrives whole.
+ const chunk_max = 8 * 1024 * 1024;
+ var at: usize = 0;
+ while (at < text.len) {
+ var end = @min(text.len, at + chunk_max);
+ if (end < text.len) if (std.mem.lastIndexOfScalar(u8, text[at..end], '\n')) |nl| {
+ end = at + nl + 1;
+ };
+ ninep_io.Client.write(gpa, dial, body, text[at..end]) catch |err| Fail.with(io, text[at..], "the +Pager pane would not take it", err);
+ at = end;
+ }
+ ninep_io.Client.write(gpa, dial, mark, "1\n") catch {};
+ var ctl_buf: [64]u8 = undefined;
+ ninep_io.Client.write(gpa, dial, std.fmt.bufPrint(&ctl_buf, "/pane/{d}/ctl", .{serial}) catch unreachable, "clean\n") catch {};
+}
+
/// Stdin to its end, its terminal escapes stripped (a pager's input is
/// coloured for a terminal; a text pane shows text), at most a file's
/// limit, a longer one cut there and said so at its end.
@@ -167,6 +219,23 @@ pub fn stripEscapes(gpa: std.mem.Allocator, bytes: []const u8) ![]u8 {
i += 1;
continue;
}
+ // A carriage return: CRLF is a newline; alone, the line starts
+ // over, as on a terminal, so a progress line keeps its last state.
+ if (c == '\r') {
+ if (i + 1 < bytes.len and bytes[i + 1] == '\n') {
+ i += 1;
+ continue;
+ }
+ const line_start = if (std.mem.lastIndexOfScalar(u8, out.items, '\n')) |nl| nl + 1 else 0;
+ out.shrinkRetainingCapacity(line_start);
+ i += 1;
+ continue;
+ }
+ // Bytes a terminal acts on and never shows: NUL, BEL, SO, SI.
+ if (c == 0 or c == 0x07 or c == 0x0e or c == 0x0f) {
+ i += 1;
+ continue;
+ }
out.appendAssumeCapacity(c);
i += 1;
}
@@ -181,6 +250,9 @@ test "stripEscapes takes out colour, other escapes and overstrike, and keeps the
.{ "N\x08NA\x08AM\x08ME\x08E _\x08u", "NAME u" },
.{ "plain\ttext\n", "plain\ttext\n" },
.{ "\x1b(Bdone", "done" },
+ .{ "a\r\nb\r\n", "a\nb\n" },
+ .{ "10%\r50%\r100%\ndone\n", "100%\ndone\n" },
+ .{ "x\x00y\x07z\x0e\x0f\n", "xyz\n" },
};
for (cases) |c| {
const got = try stripEscapes(gpa, c[0]);
@@ -537,17 +609,9 @@ fn nativeMain(init: std.process.Init) !void {
// and the body), and back at once: the PAGER a pane's shell gets.
if (positional) |word| if (std.mem.eql(u8, word, "-")) {
const text = readStdinText(arena) catch |err| Refuse.with(init.io, "stdin", err);
- const made = ninep_io.Client.read(arena, parent.dial, "/pane/new", "/pane/new") catch |err| Refuse.with(init.io, "-", err);
- const serial = std.fmt.parseInt(u32, std.mem.trim(u8, made, " \n"), 10) catch |err| Refuse.with(init.io, "-", err);
- var cwd_buf: [4096]u8 = undefined;
- const cwd = if (std.c.getcwd(&cwd_buf, cwd_buf.len)) |c| std.mem.span(@as([*:0]u8, @ptrCast(c))) else "/";
- var path_buf: [64]u8 = undefined;
- var line_buf: [4200]u8 = undefined;
- const name_line = std.fmt.bufPrint(&line_buf, "{s}/+Pager\n", .{std.mem.trimEnd(u8, cwd, "/")}) catch "/+Pager\n";
- ninep_io.Client.write(arena, parent.dial, try std.fmt.bufPrint(&path_buf, "/pane/{d}/name", .{serial}), name_line) catch |err| Refuse.with(init.io, "-", err);
- if (text.len > 0) ninep_io.Client.write(arena, parent.dial, try std.fmt.bufPrint(&path_buf, "/pane/{d}/body", .{serial}), text) catch |err| Refuse.with(init.io, "-", err);
- // Shown, not edited: nothing to ask about at Exit.
- ninep_io.Client.write(arena, parent.dial, try std.fmt.bufPrint(&path_buf, "/pane/{d}/ctl", .{serial}), "clean\n") catch {};
+ // Nothing paged: no pane, and done.
+ if (text.len == 0) return;
+ pageInto(init.io, arena, parent.dial, text);
return;
};
const typed_word = positional orelse {
@@ -644,6 +708,8 @@ fn nativeMain(init: std.process.Init) !void {
// as `vim -` does; the terminal is /dev/tty, never stdin.
if (positional) |a| if (std.mem.eql(u8, a, "-")) {
opts.stdin_text = readStdinText(arena) catch |err| badArgs(init.io, "stdin: {s}", .{@errorName(err)});
+ // Nothing paged: nothing to show.
+ if (opts.stdin_text.?.len == 0) return;
positional = null;
};
if (positional) |a| {
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index fd3418a1..82001f32 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -642,6 +642,18 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply {
var it = Messages.init(p, req.data);
while (it.next()) |line| {
if (line.len == 0 or exec_line.isComment(line)) continue;
+ // `pardes -`'s: the directory's +Pager made or emptied, its
+ // serial what /exec reads back.
+ if (std.mem.startsWith(u8, line, "pager ") or std.mem.eql(u8, line, "pager")) {
+ if (!apply) continue;
+ const id = pardes.panes.Output.openPager(p, p.active, std.mem.trim(u8, line["pager".len..], " \t")) catch |err|
+ return tree.failText(req.tag, if (err == error.NoPaneRoom or err == error.NoPaneSlots) E.NOSPC else E.IO, switch (err) {
+ error.NoPaneRoom, error.NoPaneSlots => "pager: no space for a +Pager pane",
+ else => "pager: could not make a +Pager pane",
+ });
+ noteResult(p, p.panes[id].?.serial);
+ continue;
+ }
if (std.mem.startsWith(u8, line, "size ") or std.mem.eql(u8, line, "size")) {
const size = sizeOf(line) orelse return refuse(p, req, e_size, line);
// A frontend's window is the size; only a session none is
diff --git a/test/fs.py b/test/fs.py
index fce315fc..b2f52c3e 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -1456,7 +1456,32 @@ def test(binary, quic=False):
assert client.read(f'/pane/{made_now}/body') == b'bold text\n'
row = [r for r in client.read('/index').splitlines() if r.split()[0] == str(made_now).encode()][0]
assert row.split()[2] == b'0', row # clean: nothing to ask at Exit
+ # Its `new` record names it as it is: no +New and a rename.
+ log_now = client.read('/log').decode()
+ assert f'\nnew {made_now} {root}/+Pager\n' in '\n' + log_now, log_now[-400:]
+ assert f'\nrename {made_now} ' not in '\n' + log_now
+ # The directory's one +Pager: the next paged text refills it.
+ fed = subprocess.run([binary, '-'], cwd=root, env=env, input=b'again\r\nprogress 10%\rprogress 100%\n',
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10)
+ assert fed.returncode == 0, fed.stderr
+ assert serials() - before == {made_now}, (serials(), before)
+ assert client.read(f'/pane/{made_now}/body') == b'again\nprogress 100%\n'
client.remove(f'/pane/{made_now}')
+ # From a directory whose name could not be a ctl line: the
+ # session's directory's +Pager, the text whole, no stray +New.
+ odd = root / 'two\nlines'
+ odd.mkdir()
+ fed = subprocess.run([binary, '-'], cwd=odd, env=env, input=b'odd dir text\n',
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10)
+ assert fed.returncode == 0, fed.stderr
+ odd_pane, = serials() - before
+ assert client.read(f'/pane/{odd_pane}/name') == f'{root}/+Pager\n'.encode()
+ assert client.read(f'/pane/{odd_pane}/body') == b'odd dir text\n'
+ client.remove(f'/pane/{odd_pane}')
+ # Nothing on stdin: no pane, and done.
+ fed = subprocess.run([binary, '-'], cwd=root, env=env, input=b'',
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10)
+ assert fed.returncode == 0 and serials() == before, (fed, serials())
# A new name in a directory there but not writable: refused now,
# not a pane that only fails at Save.
shut = root / 'shut-dir'