summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep')
-rw-r--r--src/ninep/addr.zig16
-rw-r--r--src/ninep/ctl.zig317
-rw-r--r--src/ninep/events.zig15
-rw-r--r--src/ninep/pane.zig241
-rw-r--r--src/ninep/pty.zig9
-rw-r--r--src/ninep/screen.zig25
-rw-r--r--src/ninep/testing.zig29
-rw-r--r--src/ninep/tree.zig207
8 files changed, 544 insertions, 315 deletions
diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig
index f7642b9e..566e2364 100644
--- a/src/ninep/addr.zig
+++ b/src/ninep/addr.zig
@@ -255,17 +255,25 @@ test "the address language, form by form" {
try testing.expectEqual(@as(u32, 6), p.fs.panes[0].addr.q0); // the `o` in "two"
try testing.expectEqual(@as(u32, 7), p.fs.panes[0].addr.q1);
+ // limit is its own file: copying addr onto it bounds the search, and
+ // truncating it lifts the bound again.
+ const limit = Node.of(serial, .limit);
_ = th.wr(p, addr, "1");
- _ = th.wr(p, Node.of(serial, .ctl), "limit=addr\n");
+ try testing.expectEqualStrings("", th.rd(p, limit, 0, 64).bytes);
+ var copied: [64]u8 = undefined;
+ const pair = th.rd(p, addr, 0, 64).bytes;
+ @memcpy(copied[0..pair.len], pair);
+ _ = th.wr(p, limit, copied[0..pair.len]);
+ try testing.expectEqual(@as(u32, 4), p.fs.panes[0].limit.?.q1);
+ try testing.expectEqualStrings(" 0 4 ", th.rd(p, limit, 0, 64).bytes);
_ = th.wr(p, addr, "#0");
try testing.expectEqual(E.INVAL, th.wr(p, addr, "/three/").errno());
- _ = th.wr(p, Node.of(serial, .ctl), "clean\n");
- _ = th.call(p, .{ .tag = 6, .op = .open, .node = Node.of(serial, .ctl) });
+ _ = th.call(p, .{ .tag = 6, .op = .setattr, .node = limit, .truncate = true });
try testing.expect(p.fs.panes[0].limit == null);
_ = th.wr(p, addr, "#0");
try testing.expectEqual(Status.ok, th.wr(p, addr, "/three/").reply.status);
- for ([_][]const u8{ "zzz", "#", "//", "/nomatch/", "1 2", "99", "/a\\" }) |bad| {
+ for ([_][]const u8{ "zzz", "#", "//", "/nomatch/", "1 2 3", "99", "/a\\" }) |bad| {
_ = th.wr(p, addr, "#0");
try testing.expectEqual(E.INVAL, th.wr(p, addr, bad).errno());
}
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index d61ebf75..c213ece5 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -1,6 +1,9 @@
-//! The two ctl files. `look TEXT` is a right click and `exec TEXT` a middle
-//! click in the editor's own command language; a pane's ctl adds the addr
-//! verbs. Reading /ctl answers the serials the last command made or touched.
+//! The command files and the status they answer. Writing a line to `look` is
+//! a right click on it and writing one to `exec` a middle click, at the
+//! active pane from the root and at that pane from /pane/<n>/; reading either
+//! answers the serials the last command made or touched. /status reports the
+//! editor, and a pane's ctl its acme status line and the one verb, `get`,
+//! that no file of its own would say any better.
const std = @import("std");
const pardes = @import("../pardes.zig");
const panes = @import("../panes.zig");
@@ -16,36 +19,6 @@ const Req = tree.Req;
const Reply = tree.Reply;
const E = tree.E;
-const Verb = enum {
- @"addr=dot",
- clean,
- cleartag,
- dirty,
- @"dot=addr",
- get,
- @"limit=addr",
- mark,
- nomark,
- noscroll,
- scroll,
- show,
-};
-
-fn verbIs(line: []const u8, word: []const u8) bool {
- if (!std.mem.startsWith(u8, line, word)) return false;
- return line.len == word.len or line[word.len] == ' ' or line[word.len] == '\t';
-}
-
-/// The text after `look` or `exec`, or null when the line is not that verb.
-/// Control characters other than tab are refused.
-fn textAfter(line: []const u8, verb: []const u8) ?[]const u8 {
- if (!verbIs(line, verb)) return null;
- const text = std.mem.trim(u8, line[verb.len..], " \t");
- if (text.len == 0) return "";
- for (text) |c| if (c < ' ' and c != '\t') return "";
- return text;
-}
-
// ---- which panes a command made or touched ----
const Serials = struct {
@@ -96,37 +69,49 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void {
noteResult(p, if (exec) context else activeSerial(p) orelse return);
}
-fn beginResults(p: *Pardes) void {
- p.fs.results_len = 0;
- p.fs.results_valid = true;
-}
-
-// ---- /ctl ----
+// ---- look and exec ----
-pub fn writeRoot(p: *Pardes, req: Req) Reply {
+/// One command per line, checked whole before any line runs: a line is the
+/// clicked text itself, so it may not be empty or carry control characters.
+/// A null `serial` clicks at whichever pane is active as each line runs,
+/// which is what the root's own look and exec mean; a serial stays put.
+pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool) Reply {
for ([2]bool{ false, true }) |apply| {
- if (apply) beginResults(p);
+ if (apply) p.fs.results_len = 0;
var it = std.mem.splitScalar(u8, req.data, '\n');
while (it.next()) |raw| {
- const line = std.mem.trim(u8, raw, " \t\r");
- if (line.len == 0) continue;
- const exec = verbIs(line, "exec");
- const text = textAfter(line, if (exec) "exec" else "look") orelse return Reply.fail(req.tag, E.INVAL);
- if (text.len == 0) return Reply.fail(req.tag, E.INVAL);
+ const text = std.mem.trim(u8, raw, " \t\r");
+ if (text.len == 0) continue;
+ for (text) |c| if (c < ' ' and c != '\t') return Reply.fail(req.tag, E.INVAL);
if (!apply) continue;
- if (p.panes[p.active] == null) return Reply.fail(req.tag, E.NOENT);
- run(p, p.active, exec, text);
+ const id = if (serial) |s| p.paneBySerial(s) orelse break else p.active;
+ if (p.panes[id] == null) return Reply.fail(req.tag, E.NOENT);
+ run(p, id, exec, text);
}
}
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
-pub fn rootText(p: *Pardes, buf: []u8) []const u8 {
- var w = std.Io.Writer.fixed(buf);
- if (p.fs.results_valid) {
- for (p.fs.results[0..p.fs.results_len]) |serial| w.print("{d}\n", .{serial}) catch break;
- return w.buffered();
+pub fn resultsLen(p: *Pardes) u64 {
+ var n: u64 = 0;
+ for (p.fs.results[0..p.fs.results_len]) |serial| {
+ var digits: [16]u8 = undefined;
+ n += (std.fmt.bufPrint(&digits, "{d}\n", .{serial}) catch unreachable).len;
}
+ return n;
+}
+
+pub fn readResults(p: *Pardes, req: Req) Reply {
+ const out = p.fs.stage(p.gpa);
+ for (p.fs.results[0..p.fs.results_len]) |serial|
+ out.print(p.gpa, "{d}\n", .{serial}) catch return Reply.fail(req.tag, E.NOMEM);
+ return tree.stagedReply(p, req);
+}
+
+// ---- /status ----
+
+fn statusText(p: *Pardes, buf: []u8) []const u8 {
+ var w = std.Io.Writer.fixed(buf);
var count: usize = 0;
for (p.panes) |slot| count += @intFromBool(slot != null);
const pid: u32 = if (comptime pardes.hosted) @intCast(std.c.getpid()) else 0;
@@ -134,13 +119,17 @@ pub fn rootText(p: *Pardes, buf: []u8) []const u8 {
return w.buffered();
}
-pub const root_text_cap = @max(MAX_PANES * 11, 64 + pardes.version.len);
+pub const status_text_cap = 64 + pardes.version.len;
+
+pub fn statusLen(p: *Pardes) u64 {
+ var buf: [status_text_cap]u8 = undefined;
+ return statusText(p, &buf).len;
+}
-pub fn readRoot(p: *Pardes, req: Req) Reply {
- var buf: [root_text_cap]u8 = undefined;
- const text = rootText(p, &buf);
+pub fn readStatus(p: *Pardes, req: Req) Reply {
+ var buf: [status_text_cap]u8 = undefined;
const out = p.fs.stage(p.gpa);
- out.appendSlice(p.gpa, text) catch return Reply.fail(req.tag, E.NOMEM);
+ out.appendSlice(p.gpa, statusText(p, &buf)) catch return Reply.fail(req.tag, E.NOMEM);
return tree.stagedReply(p, req);
}
@@ -190,74 +179,35 @@ pub fn readPane(p: *Pardes, req: Req, pane: *Pane) Reply {
return tree.stagedReply(p, req);
}
-pub fn writePane(p: *Pardes, req: Req, serial: u32) Reply {
- for ([2]bool{ false, true }) |apply| {
- if (apply) beginResults(p);
- var it = std.mem.splitScalar(u8, req.data, '\n');
- while (it.next()) |raw| {
- const line = std.mem.trim(u8, raw, " \t\r");
- if (line.len == 0) continue;
- const live = p.paneBySerial(serial) orelse if (apply) break else return Reply.fail(req.tag, E.NOENT);
- const errno = paneVerb(p, live, line, apply);
- if (errno != 0) return Reply.fail(req.tag, errno);
- }
+/// `get` is the one thing here that no file of the pane's own would say: it
+/// reloads the buffer from the name it carries, wherever that name resolves.
+/// Repeating it in one write would only reload the same bytes, so it runs once.
+pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply {
+ var asked = false;
+ var it = std.mem.splitScalar(u8, req.data, '\n');
+ while (it.next()) |raw| {
+ const line = std.mem.trim(u8, raw, " \t\r");
+ if (line.len == 0) continue;
+ if (!std.mem.eql(u8, line, "get")) return Reply.fail(req.tag, E.INVAL);
+ asked = true;
+ }
+ if (asked) {
+ const errno = get(p, pane);
+ if (errno != 0) return Reply.fail(req.tag, errno);
}
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
-fn paneVerb(p: *Pardes, id: usize, line: []const u8, apply: bool) u16 {
- const pane = p.panes[id] orelse return E.INVAL;
- const pf = &p.fs.panes[id];
-
- for ([_]bool{ false, true }) |exec| if (textAfter(line, if (exec) "exec" else "look")) |text| {
- if (text.len == 0) return E.INVAL;
- if (apply) run(p, id, exec, text);
- return 0;
+fn get(p: *Pardes, pane: *Pane) u16 {
+ const f = pane_files.fileOf(pane) orelse return 0;
+ if (!panes.Output.fileTraits(f.output).saves) return 0;
+ const bytes = fs.read(p, f.path) catch |err| return switch (err) {
+ error.FileNotFound => E.NOENT,
+ else => E.IO,
};
- const v = std.meta.stringToEnum(Verb, line) orelse return E.INVAL;
- if (!apply) return 0;
-
- switch (v) {
- .@"addr=dot" => pf.addr = pane_files.dotOf(pane),
- .@"dot=addr" => {
- pane_files.clampAddr(pf, pane_files.bodyOf(pane).len);
- pane_files.setDot(pane, pf.addr);
- },
- .@"limit=addr" => {
- pane_files.clampAddr(pf, pane_files.bodyOf(pane).len);
- pf.limit = pf.addr;
- },
- .clean => if (pane_files.fileOf(pane)) |f| {
- f.saved_revision = f.revision;
- },
- .dirty => if (pane_files.fileOf(pane)) |f| {
- f.saved_revision = f.revision -% 1;
- },
- .cleartag => {
- pane.tag_tail_len = 0;
- pane.tag_init = true;
- },
- .get => if (pane_files.fileOf(pane)) |f| {
- if (panes.Output.fileTraits(f.output).saves) {
- if (fs.read(p, f.path)) |bytes| {
- panes.File.pushUndo(p, pane);
- panes.File.setContent(p, f, bytes);
- f.saved_revision = f.revision;
- } else |err| return switch (err) {
- error.FileNotFound => E.NOENT,
- else => E.IO,
- };
- }
- },
- .mark => {
- pf.nomark = false;
- panes.File.pushUndo(p, pane);
- },
- .nomark => pf.nomark = true,
- .noscroll => pf.noscroll = true,
- .scroll => pf.noscroll = false,
- .show => pane_files.showOffset(pane, pane_files.dotOf(pane).q0),
- }
+ panes.File.pushUndo(p, pane);
+ panes.File.setContent(p, f, bytes);
+ f.saved_revision = f.revision;
return 0;
}
@@ -272,7 +222,9 @@ const withFile = th.withFile;
const serialOf = th.serialOf;
const Node = tree.Node;
const Status = tree.Status;
-const root_ctl = @intFromEnum(tree.TopFile.ctl);
+const root_status = @intFromEnum(tree.TopFile.status);
+const root_look = @intFromEnum(tree.TopFile.look);
+const root_exec = @intFromEnum(tree.TopFile.exec);
test "pane ctl read is index's five fields plus width in cells, font and tab width" {
const gpa = testing.allocator;
@@ -299,37 +251,18 @@ test "pane ctl read is index's five fields plus width in cells, font and tab wid
try testing.expectEqualStrings("'it''s'", w.buffered());
}
-test "every pane ctl verb, and every refusal" {
+test "the pane ctl takes get, and nothing that a file of its own now answers" {
const gpa = testing.allocator;
const p = try withFile(gpa, "one\ntwo\n");
defer p.deinit();
- const serial = serialOf(p);
- const ctl_node = Node.of(serial, .ctl);
- const pane = p.panes[0].?;
- const pf = &p.fs.panes[0];
-
- try testing.expectEqual(Status.ok, wr(p, ctl_node, "nomark\nnoscroll\ndirty\n").reply.status);
- try testing.expect(pf.nomark and pf.noscroll and pane_files.dirtyOf(pane));
- try testing.expectEqual(Status.ok, wr(p, ctl_node, "mark\nscroll\nclean\n").reply.status);
- try testing.expect(!pf.nomark and !pf.noscroll and !pane_files.dirtyOf(pane));
-
- _ = wr(p, ctl_node, "cleartag\n");
- try testing.expectEqual(@as(usize, 0), pane.tag_tail_len);
-
- _ = wr(p, Node.of(serial, .addr), "2");
- _ = wr(p, ctl_node, "limit=addr\n");
- try testing.expectEqual(@as(u32, 4), pf.limit.?.q0);
- _ = wr(p, ctl_node, "dot=addr\nshow\n");
- try testing.expectEqual(@as(i32, 1), pane.cur_row);
-
+ const ctl_node = Node.of(serialOf(p), .ctl);
for ([_][]const u8{
- "menu", "nomenu", "dump echo hi", "dumpdir /tmp", "font Go Mono", "lock", "unlock", "bogus", "DEL",
- "name x.txt", "put", "del", "delete", "Look x", "Exec Save",
+ "menu", "nomenu", "dump echo hi", "font Go Mono", "lock", "bogus", "DEL",
+ "name x.txt", "put", "del", "delete", "Look x", "Exec Save", "clean",
+ "dirty", "cleartag", "dot=addr", "addr=dot", "show", "mark", "nomark",
+ "scroll", "limit=addr", "get x", "look /tmp", "exec Del",
}) |bad| try testing.expectEqual(E.INVAL, wr(p, ctl_node, bad).errno());
-
- try testing.expect(!pane_files.dirtyOf(pane));
- try testing.expectEqual(E.INVAL, wr(p, ctl_node, "dirty\nbogus\n").errno());
- try testing.expect(!pane_files.dirtyOf(pane));
+ try testing.expect(p.paneBySerial(serialOf(p)) != null);
}
test "exec runs a builtin at the pane and records the pane it acted on" {
@@ -338,61 +271,69 @@ test "exec runs a builtin at the pane and records the pane it acted on" {
defer p.deinit();
const serial = serialOf(p);
const pane = p.panes[0].?;
- try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "exec Msg from ctl\n").reply.status);
- try testing.expectEqualStrings("from ctl", pane.msg[0..pane.msg_len]);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .exec), "Msg from exec\n").reply.status);
+ try testing.expectEqualStrings("from exec", pane.msg[0..pane.msg_len]);
var expected: [16]u8 = undefined;
- try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{serial}), rd(p, root_ctl, 0, 4096).bytes);
+ const own = try std.fmt.bufPrint(&expected, "{d}\n", .{serial});
+ try testing.expectEqualStrings(own, rd(p, root_exec, 0, 4096).bytes);
+ // Either command file reads back what the last one did.
+ try testing.expectEqualStrings(own, rd(p, Node.of(serial, .look), 0, 4096).bytes);
+ try testing.expectEqual(@as(u64, own.len), call(p, .{ .tag = 1, .op = .getattr, .node = root_exec }).reply.attr.size);
- try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "exec New\n").reply.status);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .exec), "New\n").reply.status);
const made = p.panes[p.active].?;
try testing.expect(made.serial != serial);
- try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{made.serial}), rd(p, root_ctl, 0, 4096).bytes);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{made.serial}), rd(p, root_exec, 0, 4096).bytes);
- try testing.expectEqual(Status.ok, wr(p, Node.of(made.serial, .ctl), "exec Del\n").reply.status);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(made.serial, .exec), "Del\n").reply.status);
try testing.expect(p.paneBySerial(made.serial) == null);
- try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{made.serial}), rd(p, root_ctl, 0, 4096).bytes);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{made.serial}), rd(p, root_exec, 0, 4096).bytes);
}
-test "the root ctl reads editor status until a command runs and validates whole batches" {
+test "status reads the editor, and look and exec validate whole batches" {
const gpa = testing.allocator;
const p = try withFile(gpa, "Msg root\n");
defer p.deinit();
- const status = rd(p, root_ctl, 0, 4096);
+ const status = rd(p, root_status, 0, 4096);
try testing.expectEqual(Status.ok, status.reply.status);
var lines = std.mem.tokenizeScalar(u8, status.bytes, '\n');
try testing.expect(std.mem.startsWith(u8, lines.next().?, "pid "));
var expected: [64]u8 = undefined;
try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "version {s}", .{pardes.version}), lines.next().?);
- try testing.expectEqualStrings("panes 1", lines.next().?);
+ var live: usize = 0;
+ for (p.panes) |slot| live += @intFromBool(slot != null);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "panes {d}", .{live}), lines.next().?);
try testing.expect(lines.next() == null);
- const stat = call(p, .{ .tag = 1, .op = .getattr, .node = root_ctl });
+ const stat = call(p, .{ .tag = 1, .op = .getattr, .node = root_status });
try testing.expectEqual(@as(u64, status.bytes.len), stat.reply.attr.size);
- try testing.expectEqual(@as(u16, 0o666), stat.reply.attr.mode);
+ try testing.expectEqual(@as(u16, 0o444), stat.reply.attr.mode);
+ try testing.expectEqual(E.PERM, wr(p, root_status, "anything\n").errno());
+ try testing.expectEqualStrings("", rd(p, root_exec, 0, 4096).bytes);
const serial = p.next_serial;
- for ([_][]const u8{ "look", "exec", "look \t", "Look /virtual/index", "exec Msg a\x00b", "exec Msg x\nbogus\n", "name x", "put" }) |invalid| {
- try testing.expectEqual(E.INVAL, wr(p, root_ctl, invalid).errno());
+ for ([_][]const u8{ "Msg a\x00b", "Msg x\nMsg \x01y\n" }) |invalid| {
+ try testing.expectEqual(E.INVAL, wr(p, root_exec, invalid).errno());
try testing.expectEqual(serial, p.next_serial);
}
- try testing.expectEqualStrings(status.bytes, rd(p, root_ctl, 0, 4096).bytes);
+ // An empty write is a no-op, not a command.
+ try testing.expectEqual(Status.ok, wr(p, root_exec, "\n \n").reply.status);
+ try testing.expectEqualStrings("", rd(p, root_exec, 0, 4096).bytes);
+ try testing.expectEqualStrings(status.bytes, rd(p, root_status, 0, 4096).bytes);
const pane = p.panes[0].?;
- try testing.expectEqual(Status.ok, wr(p, root_ctl, "exec Msg hello there\n").reply.status);
+ try testing.expectEqual(Status.ok, wr(p, root_exec, "Msg hello there\n").reply.status);
try testing.expectEqualStrings("hello there", pane.msg[0..pane.msg_len]);
- try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{pane.serial}), rd(p, root_ctl, 0, 4096).bytes);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{pane.serial}), rd(p, root_exec, 0, 4096).bytes);
- try testing.expectEqual(Status.ok, wr(p, root_ctl, "look /virtual/index\nexec New\n").reply.status);
- var made: [MAX_PANES]u32 = undefined;
- var n: usize = 0;
- for (p.panes) |slot| if (slot) |q| if (q.serial != pane.serial) {
- made[n] = q.serial;
- n += 1;
- };
- try testing.expectEqual(@as(usize, 2), n);
- const listed = rd(p, root_ctl, 0, 4096);
- try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n{d}\n", .{ @min(made[0], made[1]), @max(made[0], made[1]) }), listed.bytes);
- const opened = p.panes[p.paneBySerial(@min(made[0], made[1])).?].?;
+ try testing.expectEqual(Status.ok, wr(p, root_look, "/virtual/index\n").reply.status);
+ const opened = p.panes[p.active].?;
try testing.expectEqualStrings("/virtual/index", opened.file.?.path);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{opened.serial}), rd(p, root_look, 0, 4096).bytes);
+
+ try testing.expectEqual(Status.ok, wr(p, root_exec, "New\n").reply.status);
+ const made = p.panes[p.active].?;
+ try testing.expect(made.serial != opened.serial);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{made.serial}), rd(p, root_exec, 0, 4096).bytes);
}
test "look opens spaced paths and locations without editing the source pane" {
@@ -406,12 +347,12 @@ test "look opens spaced paths and locations without editing the source pane" {
const p = try withFile(gpa, "source stays intact\n");
defer p.deinit();
const source = p.panes[0].?;
- const ctl_node = Node.of(source.serial, .ctl);
+ const look_node = Node.of(source.serial, .look);
const revision = source.file.?.revision;
const undo_len = source.file.?.history.undo_len;
source.cur_col = 3;
- var command: [4200]u8 = undefined;
- try testing.expectEqual(Status.ok, wr(p, ctl_node, try std.fmt.bufPrint(&command, "look {s}:2:3\n", .{path})).reply.status);
+ var clicked: [4200]u8 = undefined;
+ try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&clicked, "{s}:2:3\n", .{path})).reply.status);
const opened = p.panes[p.active].?;
try testing.expect(opened != source);
try testing.expectEqualStrings(path, opened.file.?.path);
@@ -423,14 +364,14 @@ test "look opens spaced paths and locations without editing the source pane" {
try testing.expectEqual(undo_len, source.file.?.history.undo_len);
try testing.expectEqual(@as(i32, 3), source.cur_col);
var expected: [16]u8 = undefined;
- try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{opened.serial}), rd(p, root_ctl, 0, 4096).bytes);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{opened.serial}), rd(p, root_look, 0, 4096).bytes);
p.active = 0;
- try testing.expectEqual(Status.ok, wr(p, root_ctl, try std.fmt.bufPrint(&command, "look {s}:3:2\n", .{path})).reply.status);
+ try testing.expectEqual(Status.ok, wr(p, root_look, try std.fmt.bufPrint(&clicked, "{s}:3:2\n", .{path})).reply.status);
try testing.expect(p.panes[p.active].? == opened);
try testing.expectEqual(@as(i32, 2), opened.cur_row);
try testing.expectEqual(@as(i32, 1), opened.cur_col);
- try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{opened.serial}), rd(p, root_ctl, 0, 4096).bytes);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{opened.serial}), rd(p, root_look, 0, 4096).bytes);
}
test "look validates the whole batch before opening virtual files" {
@@ -438,17 +379,17 @@ test "look validates the whole batch before opening virtual files" {
const p = try withFile(gpa, "source\n");
defer p.deinit();
const source = p.panes[0].?;
- const ctl_node = Node.of(source.serial, .ctl);
+ const look_node = Node.of(source.serial, .look);
const serial = p.next_serial;
for ([_][]const u8{
- "look", "look \t", "look /virtual/index\x00ignored", "look /virtual/index\nbogus\n",
+ "/virtual/index\x00ignored", "/virtual/index\n/virtual/\x01index\n",
}) |invalid| {
- try testing.expectEqual(E.INVAL, wr(p, ctl_node, invalid).errno());
+ try testing.expectEqual(E.INVAL, wr(p, look_node, invalid).errno());
try testing.expectEqual(serial, p.next_serial);
try testing.expectEqual(@as(usize, 0), p.active);
try testing.expectEqualStrings("source\n", source.file.?.content);
}
- try testing.expectEqual(Status.ok, wr(p, ctl_node, "look /n/self/index\n").reply.status);
+ try testing.expectEqual(Status.ok, wr(p, look_node, "/n/self/index\n").reply.status);
const opened = p.panes[p.active].?;
try testing.expectEqualStrings("/virtual/index", opened.file.?.path);
try testing.expect(std.mem.indexOf(u8, opened.file.?.content, "/test.txt") != null);
@@ -480,7 +421,7 @@ test "get reloads the pane from disk and reports a missing file without losing d
_ = wr(p, Node.of(serial, .name), try std.fmt.bufPrint(&name, "{s}/missing.txt\n", .{directory}));
_ = wr(p, Node.of(serial, .body), "unsaved\n");
- try testing.expectEqual(E.NOENT, wr(p, ctl_node, "get\nexec Del\n").errno());
+ try testing.expectEqual(E.NOENT, wr(p, ctl_node, "get\n").errno());
try testing.expect(p.paneBySerial(serial) != null);
try testing.expectEqualStrings("from disk\nunsaved\n", pane.file.?.content);
try testing.expect(pane_files.dirtyOf(pane));
@@ -494,7 +435,7 @@ test "exec Save writes the named file through the host" {
const pane = p.panes[0].?;
try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .name), "/tmp/renamed.txt\n").reply.status);
try testing.expect(pane_files.dirtyOf(pane));
- const saved = wr(p, Node.of(serial, .ctl), "exec Save\n");
+ const saved = wr(p, Node.of(serial, .exec), "Save\n");
try testing.expectEqual(Status.ok, saved.reply.status);
try testing.expect(saved.saved);
p.perform(.{ .save_file = .{ .pane = 0 } });
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index fb49e41b..bd53bcd9 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -89,6 +89,13 @@ pub const Queue = struct {
}
};
+/// What the next read would answer, which is the size a stat reports: a
+/// client can see there is something waiting without parking on it.
+pub fn pending(q: *const Queue) u64 {
+ const record = q.peek() orelse return 0;
+ return record.len;
+}
+
/// One record per read; `.again` parks the read until a record arrives.
pub fn readQueue(p: *Pardes, req: Req, q: *Queue) Reply {
const record = q.peek() orelse return .{ .tag = req.tag, .status = .again };
@@ -461,7 +468,7 @@ test "a pane deleted while its event file is open leaves no suppression behind"
const b = call(p, .{ .tag = 19, .op = .open, .node = event });
try testing.expectEqual(@as(u16, 2), p.fs.listeners);
- _ = wr(p, Node.of(serial, .ctl), "exec Del\n");
+ _ = wr(p, Node.of(serial, .exec), "Del\n");
try testing.expect(p.paneBySerial(serial) == null);
try testing.expectEqual(@as(u16, 0), p.fs.listeners);
@@ -471,7 +478,7 @@ test "a pane deleted while its event file is open leaves no suppression behind"
try testing.expectEqual(E.NOENT, rd(p, event, 0, 64).errno());
try testing.expectEqual(E.NOENT, rd(p, Node.of(serial, .body), 0, 64).errno());
- try testing.expectEqual(E.NOENT, wr(p, Node.of(serial, .ctl), "clean\n").errno());
+ try testing.expectEqual(E.NOENT, wr(p, Node.of(serial, .dirty), "0\n").errno());
try testing.expectEqual(E.NOENT, call(p, .{ .tag = 22, .op = .open, .node = event }).errno());
}
@@ -535,7 +542,7 @@ test "the log parks until a pane is created, renamed, saved or deleted" {
try std.fmt.bufPrint(&expected, "rename {d} /tmp/logged.txt\n", .{serial}),
rd(p, log, 0, 4096).bytes,
);
- const saving = wr(p, Node.of(serial, .ctl), "exec Save\n");
+ const saving = wr(p, Node.of(serial, .exec), "Save\n");
try testing.expectEqual(Status.ok, saving.reply.status);
try testing.expect(saving.saved);
p.perform(.{ .save_file = .{ .pane = @intCast(id) } });
@@ -543,7 +550,7 @@ test "the log parks until a pane is created, renamed, saved or deleted" {
try std.fmt.bufPrint(&expected, "save {d} /tmp/logged.txt\n", .{serial}),
rd(p, log, 0, 4096).bytes,
);
- try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "exec Del\n").reply.status);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .exec), "Del\n").reply.status);
try testing.expectEqualStrings(
try std.fmt.bufPrint(&expected, "del {d} /tmp/logged.txt\n", .{serial}),
rd(p, log, 0, 4096).bytes,
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index b37ae645..42c0c48e 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -1,5 +1,6 @@
-//! The files under /pane/<serial>/: name, body, tag, ctl, addr, data, xdata,
-//! sel, errors and event. Terminal panes add pty/ (pty.zig).
+//! The files under /pane/<serial>/: name, body, tag, ctl, the three range
+//! files addr, dot and limit, data, xdata, sel, the three flag files dirty,
+//! mark and scroll, errors, event, look and exec. Terminals add pty/ (pty.zig).
const std = @import("std");
const pardes = @import("../pardes.zig");
const panes = @import("../panes.zig");
@@ -187,10 +188,15 @@ pub fn spliceBody(p: *Pardes, id: usize, pane: *Pane, q0: usize, q1: usize, byte
return take;
}
-// ---- sizes reported by stat ----
+// ---- what stat reports ----
+
+/// A range file's text: acme's addr format, two right-aligned offsets, so
+/// that copying one range file onto another is the whole of `dot=addr`.
+pub const range_len: u64 = 24;
pub fn fileSize(p: *Pardes, id: usize, f: PaneFile) u64 {
const pane = p.panes[id] orelse return 0;
+ const pf = &p.fs.panes[id];
return switch (f) {
.body, .data, .xdata => bodyOf(pane).len,
.tag => tagOf(p, pane).len,
@@ -203,7 +209,24 @@ pub fn fileSize(p: *Pardes, id: usize, f: PaneFile) u64 {
const d = dotOf(pane);
break :sel @max(d.q0, d.q1) - d.q0;
},
- .dir, .addr, .errors, .event, .pty, .pty_ctl, .pty_status, .pty_data => 0,
+ .addr, .dot => range_len,
+ .limit => if (pf.limit == null) 0 else range_len,
+ .dirty, .mark, .scroll => 2,
+ .look, .exec => ctl.resultsLen(p),
+ .event => events.pending(&pf.events),
+ .pty_status => pty.status_len,
+ .pty_data => events.pending(&pf.pty_out),
+ .dir, .errors, .pty, .pty_ctl => 0,
+ };
+}
+
+/// The qid version, which a client polls to watch a file for change. Only
+/// the pane's text can promise one: its revision counts every edit, and
+/// nothing else here changes in step with it.
+pub fn versionOf(pane: *const Pane, f: PaneFile) u32 {
+ return switch (f) {
+ .body, .data, .xdata => if (pane.file) |*file| file.revision else 0,
+ else => 0,
};
}
@@ -246,13 +269,17 @@ pub fn read(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply
.ctl => ctl.readPane(p, req, pane),
.addr => addr: {
clampAddr(pf, bodyOf(pane).len);
- const out = p.fs.stage(p.gpa);
- out.print(p.gpa, "{d:>11} {d:>11} ", .{ pf.addr.q0, pf.addr.q1 }) catch {};
- break :addr tree.stagedReply(p, req);
+ break :addr readRange(p, req, pf.addr);
},
+ .dot => readRange(p, req, dotOf(pane)),
+ .limit => readRange(p, req, pf.limit),
.data => readData(req, id, pane, pf, false),
.xdata => readData(req, id, pane, pf, true),
.sel => readSel(req, id, pane),
+ .dirty => readFlag(p, req, dirtyOf(pane)),
+ .mark => readFlag(p, req, !pf.nomark),
+ .scroll => readFlag(p, req, !pf.noscroll),
+ .look, .exec => ctl.readResults(p, req),
.event => events.readQueue(p, req, &pf.events),
.pty_status => pty.readStatus(p, req, id, pane),
.pty_data => pty.readData(p, req, pf),
@@ -260,6 +287,20 @@ pub fn read(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply
};
}
+/// An unset limit reads empty, which is how a client tells it from one that
+/// happens to bound nothing.
+fn readRange(p: *Pardes, req: Req, r: ?State.Range) Reply {
+ const out = p.fs.stage(p.gpa);
+ if (r) |range| out.print(p.gpa, "{d:>11} {d:>11} ", .{ range.q0, range.q1 }) catch {};
+ return tree.stagedReply(p, req);
+}
+
+fn readFlag(p: *Pardes, req: Req, on: bool) Reply {
+ const out = p.fs.stage(p.gpa);
+ out.appendSlice(p.gpa, if (on) "1\n" else "0\n") catch {};
+ return tree.stagedReply(p, req);
+}
+
fn readBody(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
if (pane.file != null) {
const text = bodyOf(pane);
@@ -322,10 +363,12 @@ pub fn write(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply
.name => writeName(p, req, id, pane),
.body => writeBody(p, req, id, pane),
.tag => writeTag(p, req, pane),
- .ctl => ctl.writePane(p, req, pane.serial),
- .addr => writeAddr(p, req, id, pane),
+ .ctl => ctl.writePane(p, req, pane),
+ .addr, .dot, .limit => writeRange(p, req, id, pane, file),
.data, .xdata => writeData(p, req, id, pane),
.sel => writeSel(p, req, id, pane),
+ .dirty, .mark, .scroll => writeFlag(p, req, id, pane, file),
+ .look, .exec => ctl.command(p, req, pane.serial, file == .exec),
.event => events.writeEvent(p, req, id),
.errors => if (appendErrors(p, id, req.data)) |took|
.{ .tag = req.tag, .written = @intCast(took) }
@@ -389,15 +432,68 @@ fn writeSel(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
return .{ .tag = req.tag, .written = @intCast(take) };
}
-fn writeAddr(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
+/// The pair of offsets a range file reads back, so that copying `addr` onto
+/// `dot` or `limit` does what acme spelled `dot=addr` and `limit=addr`.
+fn pairOf(text: []const u8) ?State.Range {
+ var it = std.mem.tokenizeAny(u8, text, " \t");
+ const q0 = std.fmt.parseInt(u32, it.next() orelse return null, 10) catch return null;
+ const q1 = std.fmt.parseInt(u32, it.next() orelse return null, 10) catch return null;
+ if (it.next() != null) return null;
+ return .{ .q0 = q0, .q1 = @max(q0, q1) };
+}
+
+/// A range file takes an address expression, or that pair of offsets.
+fn rangeOf(pf: *State, text: []const u8, data: []const u8) ?State.Range {
+ const expr = std.mem.trimEnd(u8, data, "\n");
+ if (pairOf(expr)) |r| {
+ const n = clip(text.len);
+ return .{ .q0 = @min(r.q0, n), .q1 = @min(r.q1, n) };
+ }
+ var a: addressing.Addr = .{ .text = text, .lim = pf.limit, .expr = expr };
+ const r = a.address(pf.addr) orelse return null;
+ return if (a.i < expr.len) null else r;
+}
+
+fn writeRange(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply {
const pf = &p.fs.panes[id];
const text = bodyOf(pane);
clampAddr(pf, text.len);
- const expr = std.mem.trimEnd(u8, req.data, "\n");
- var a: addressing.Addr = .{ .text = text, .lim = pf.limit, .expr = expr };
- const r = a.address(pf.addr) orelse return Reply.fail(req.tag, E.INVAL);
- if (a.i < expr.len) return Reply.fail(req.tag, E.INVAL);
- pf.addr = r;
+ const r = rangeOf(pf, text, req.data) orelse return Reply.fail(req.tag, E.INVAL);
+ switch (file) {
+ .addr => pf.addr = r,
+ .limit => pf.limit = r,
+ // Setting dot scrolls to it, which is the whole of acme's `show`.
+ .dot => {
+ if (fileOf(pane) == null) return Reply.fail(req.tag, E.INVAL);
+ setDot(pane, r);
+ },
+ else => unreachable,
+ }
+ return .{ .tag = req.tag, .written = @intCast(req.data.len) };
+}
+
+fn flagOf(data: []const u8) ?bool {
+ const word = std.mem.trim(u8, data, " \t\r\n");
+ if (std.mem.eql(u8, word, "1")) return true;
+ if (std.mem.eql(u8, word, "0")) return false;
+ return null;
+}
+
+fn writeFlag(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply {
+ const on = flagOf(req.data) orelse return Reply.fail(req.tag, E.INVAL);
+ const pf = &p.fs.panes[id];
+ switch (file) {
+ .dirty => {
+ const f = fileOf(pane) orelse return Reply.fail(req.tag, E.INVAL);
+ f.saved_revision = if (on) f.revision -% 1 else f.revision;
+ },
+ .mark => {
+ pf.nomark = !on;
+ if (on) panes.File.pushUndo(p, pane);
+ },
+ .scroll => pf.noscroll = !on,
+ else => unreachable,
+ }
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
@@ -475,14 +571,24 @@ pub fn appendErrors(p: *Pardes, id: usize, text: []const u8) ?usize {
return text.len;
}
-/// Truncation empties a body; every other setattr just answers fresh attributes.
+/// Truncation empties whatever the file holds: the body, the tag's editable
+/// tail (acme's `cleartag`), a range. Every other setattr just answers fresh
+/// attributes.
pub fn truncate(p: *Pardes, id: usize, pane: *Pane, file: PaneFile) tree.Status {
+ const pf = &p.fs.panes[id];
switch (file) {
.body, .data, .xdata => if (fileOf(pane) != null) {
_ = spliceBody(p, id, pane, 0, bodyOf(pane).len, "") orelse return .err;
- p.fs.panes[id].addr = .{};
+ pf.addr = .{};
setDot(pane, .{});
},
+ .tag => {
+ pane.tag_tail_len = 0;
+ pane.tag_init = true;
+ },
+ .addr => pf.addr = .{},
+ .limit => pf.limit = null,
+ .dot => if (fileOf(pane) != null) setDot(pane, .{}),
else => {},
}
return .ok;
@@ -509,15 +615,19 @@ test "index lists serial kind dirty flag and name and stat reports its length" {
const a = rd(p, index, 0, 4096);
try testing.expectEqual(Status.ok, a.reply.status);
var want: [64]u8 = undefined;
- try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d} text 0 /test.txt\n", .{pane.serial}), a.bytes);
+ const clean_row = try std.fmt.bufPrint(&want, "{d} text 0 /test.txt\n", .{pane.serial});
+ try testing.expect(std.mem.indexOf(u8, a.bytes, clean_row) != null);
+ const at = std.mem.indexOf(u8, a.bytes, clean_row).?;
const stat = call(p, .{ .tag = 1, .op = .getattr, .node = index });
try testing.expectEqual(@as(u64, a.bytes.len), stat.reply.attr.size);
try testing.expectEqual(@as(u16, 0o444), stat.reply.attr.mode);
pane.file.?.saved_revision = pane.file.?.revision -% 1;
- try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d} text 1 /test.txt\n", .{pane.serial}), rd(p, index, 0, 4096).bytes);
- const mid = rd(p, index, 2, 4);
- try testing.expectEqualStrings("text", mid.bytes);
+ var dirty_want: [64]u8 = undefined;
+ const dirty_row = try std.fmt.bufPrint(&dirty_want, "{d} text 1 /test.txt\n", .{pane.serial});
+ try testing.expect(std.mem.indexOf(u8, rd(p, index, 0, 4096).bytes, dirty_row) != null);
+ const kind = at + std.mem.indexOfScalar(u8, clean_row, ' ').? + 1;
+ try testing.expectEqualStrings("text", rd(p, index, kind, 4).bytes);
const serial = try th.newPane(p);
const both = rd(p, index, 0, 4096);
@@ -533,10 +643,11 @@ test "a terminal is listed as term with its directory as name" {
const pane = p.panes[0].?;
p.setCwd(0, "/work/dir");
var want: [64]u8 = undefined;
- try testing.expectEqualStrings(
- try std.fmt.bufPrint(&want, "{d} term 0 /work/dir\n", .{pane.serial}),
+ try testing.expect(std.mem.indexOf(
+ u8,
rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes,
- );
+ try std.fmt.bufPrint(&want, "{d} term 0 /work/dir\n", .{pane.serial}),
+ ) != null);
try testing.expectEqualStrings("/work/dir\n", rd(p, Node.of(pane.serial, .name), 0, 4096).bytes);
try testing.expectEqual(E.PERM, wr(p, Node.of(pane.serial, .name), "/elsewhere\n").errno());
}
@@ -566,6 +677,13 @@ test "body reads at any offset and writes append" {
try testing.expectEqual(@as(u64, "one\ntwo\nthree\na".len), stat.reply.attr.size);
try testing.expectEqual(@as(u16, 0o666), stat.reply.attr.mode);
try testing.expect(stat.reply.attr.mtime >= p.fs.started);
+
+ // The qid version is the pane's revision, so a stat sees an edit land.
+ try testing.expectEqual(p.panes[0].?.file.?.revision, stat.reply.attr.version);
+ _ = wr(p, body, "more\n");
+ const later = call(p, .{ .tag = 7, .op = .getattr, .node = body });
+ try testing.expect(later.reply.attr.version != stat.reply.attr.version);
+ try testing.expectEqual(@as(u32, 0), call(p, .{ .tag = 8, .op = .getattr, .node = Node.of(serial, .tag) }).reply.attr.version);
}
test "a body write to a terminal pane types at its shell" {
@@ -653,18 +771,29 @@ test "sel reads the selection and writing sel replaces it" {
const p = try withFile(gpa, "one\ntwo\n");
defer p.deinit();
const serial = serialOf(p);
- const ctl_node = Node.of(serial, .ctl);
+ const addr_node = Node.of(serial, .addr);
+ const dot = Node.of(serial, .dot);
const sel = Node.of(serial, .sel);
- _ = wr(p, Node.of(serial, .addr), "#0,#3");
- try testing.expectEqual(Status.ok, wr(p, ctl_node, "dot=addr\n").reply.status);
+ // Copying addr onto dot is the whole of acme's `dot=addr`, and back again
+ // of `addr=dot`; each range file reads exactly what the other takes.
+ _ = wr(p, addr_node, "#0,#3");
+ try testing.expectEqual(Status.ok, wr(p, dot, " 0 3 ").reply.status);
+ try testing.expectEqualStrings(" 0 3 ", rd(p, dot, 0, 100).bytes);
+ try testing.expectEqual(range_len, call(p, .{ .tag = 1, .op = .getattr, .node = dot }).reply.attr.size);
try testing.expectEqualStrings("one", rd(p, sel, 0, 100).bytes);
try testing.expectEqual(@as(u64, 3), call(p, .{ .tag = 1, .op = .getattr, .node = sel }).reply.attr.size);
- _ = wr(p, ctl_node, "addr=dot\n");
+ _ = wr(p, addr_node, "#0");
+ _ = wr(p, addr_node, " 0 3 ");
try testing.expectEqual(@as(u32, 0), p.fs.panes[0].addr.q0);
try testing.expectEqual(@as(u32, 3), p.fs.panes[0].addr.q1);
+ // A truncation empties dot; an address expression addresses it.
+ _ = call(p, .{ .tag = 9, .op = .setattr, .node = dot, .truncate = true });
+ try testing.expectEqualStrings("", rd(p, sel, 0, 100).bytes);
+ try testing.expectEqual(Status.ok, wr(p, dot, "#0,#3").reply.status);
+
try testing.expectEqual(Status.ok, wr(p, sel, "ONE").reply.status);
try testing.expectEqualStrings("ONE\ntwo\n", p.panes[0].?.file.?.content);
_ = wr(p, sel, "!");
@@ -672,6 +801,62 @@ test "sel reads the selection and writing sel replaces it" {
try testing.expectEqual(@as(u64, 0), call(p, .{ .tag = 2, .op = .getattr, .node = sel }).reply.attr.size);
}
+test "the flag files read and write the state acme kept behind ctl verbs" {
+ const gpa = testing.allocator;
+ const p = try withFile(gpa, "one\ntwo\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const pane = p.panes[0].?;
+ const pf = &p.fs.panes[0];
+ const dirty = Node.of(serial, .dirty);
+ const mark = Node.of(serial, .mark);
+ const scroll = Node.of(serial, .scroll);
+
+ for ([_]u64{ dirty, mark, scroll }) |node| {
+ try testing.expectEqual(@as(u64, 2), call(p, .{ .tag = 1, .op = .getattr, .node = node }).reply.attr.size);
+ for ([_][]const u8{ "", "yes", "2", "0 1", "true" }) |bad|
+ try testing.expectEqual(E.INVAL, wr(p, node, bad).errno());
+ }
+
+ try testing.expectEqualStrings("0\n", rd(p, dirty, 0, 8).bytes);
+ try testing.expectEqualStrings("1\n", rd(p, mark, 0, 8).bytes);
+ try testing.expectEqualStrings("1\n", rd(p, scroll, 0, 8).bytes);
+
+ try testing.expectEqual(Status.ok, wr(p, dirty, "1\n").reply.status);
+ try testing.expect(dirtyOf(pane));
+ try testing.expectEqualStrings("1\n", rd(p, dirty, 0, 8).bytes);
+ _ = wr(p, dirty, "0");
+ try testing.expect(!dirtyOf(pane));
+
+ _ = wr(p, mark, "0");
+ _ = wr(p, scroll, "0");
+ try testing.expect(pf.nomark and pf.noscroll);
+ try testing.expectEqualStrings("0\n", rd(p, mark, 0, 8).bytes);
+ try testing.expectEqualStrings("0\n", rd(p, scroll, 0, 8).bytes);
+ _ = wr(p, mark, "1");
+ _ = wr(p, scroll, "1");
+ try testing.expect(!pf.nomark and !pf.noscroll);
+
+ // A terminal has no file, so its dirty flag is readable but not writable.
+ const term = try th.withTerm(gpa);
+ defer term.deinit();
+ try testing.expectEqualStrings("0\n", rd(term, Node.of(serialOf(term), .dirty), 0, 8).bytes);
+ try testing.expectEqual(E.INVAL, wr(term, Node.of(serialOf(term), .dirty), "1").errno());
+}
+
+test "truncating the tag clears its editable tail" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const pane = p.panes[0].?;
+ const tag = Node.of(pane.serial, .tag);
+ _ = wr(p, tag, " Mine");
+ try testing.expect(pane.tag_tail_len > 0);
+ const cleared = call(p, .{ .tag = 1, .op = .setattr, .node = tag, .truncate = true });
+ try testing.expectEqual(Status.ok, cleared.reply.status);
+ try testing.expectEqual(@as(usize, 0), pane.tag_tail_len);
+ try testing.expect(std.mem.indexOf(u8, rd(p, tag, 0, 4096).bytes, " Mine") == null);
+}
+
test "name reads the file name and writing it promotes a scratch without touching its body" {
const gpa = testing.allocator;
const p = try withFile(gpa, "opener\n");
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index e6696b85..1b71a8d3 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -76,6 +76,9 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool {
return true;
}
+/// Three right-aligned fields: cols, rows and whether the host holds the tty.
+pub const status_len: u64 = 36;
+
pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
const out = p.fs.stage(p.gpa);
out.print(p.gpa, "{d:>11} {d:>11} {d:>11} ", .{
@@ -146,7 +149,7 @@ test "a pane that is not a terminal has no pty/ at all" {
var buf: [32]th.Dirent = undefined;
const files = th.dirents(rdir(p, dir, 0).bytes, &buf);
- try testing.expectEqual(@as(usize, 10), files.len);
+ try testing.expectEqual(@as(usize, 17), files.len);
try testing.expect(th.nameAt(files, "pty") == null);
try testing.expectEqual(E.NOENT, look_up(p, dir, "pty_ctl").errno());
@@ -167,7 +170,7 @@ test "a terminal pane's pty/ holds exactly ctl, status and data" {
var buf: [32]th.Dirent = undefined;
const files = th.dirents(rdir(p, dir, 0).bytes, &buf);
- try testing.expectEqual(@as(usize, 11), files.len);
+ try testing.expectEqual(@as(usize, 18), files.len);
try testing.expect(th.nameAt(files, "pty").?.dir);
const inside = th.dirents(rdir(p, Node.of(serial, .pty), 0).bytes, &buf);
@@ -247,7 +250,7 @@ test "every pty/ctl verb, and every refusal" {
"cooked",
"winsize 80 24\nbogus", // a good verb beside a bad one
"bogus\nwinsize 80 24",
- "look x", // a `ctl` verb; the two files share no vocabulary
+ "look x", // a pane command; pty/ctl shares none of that vocabulary
"exec Del",
}) |bad| {
const a = wr(p, ctl, bad);
diff --git a/src/ninep/screen.zig b/src/ninep/screen.zig
index 5b68b34c..2684aa41 100644
--- a/src/ninep/screen.zig
+++ b/src/ninep/screen.zig
@@ -83,22 +83,33 @@ pub fn releaseSnapshot(p: *Pardes, req: Req) bool {
return true;
}
-pub fn readListeners(p: *Pardes, req: Req) Reply {
- var buf: [512]u8 = undefined;
- var text = std.Io.Writer.fixed(&buf);
+const listeners_cap = 512;
+
+fn listenersText(p: *Pardes, buf: []u8) []const u8 {
+ var text = std.Io.Writer.fixed(buf);
if (p.fs.socket_path.len != 0)
- text.print("unix!{s}\n", .{p.fs.socket_path}) catch return Reply.fail(req.tag, E.IO);
+ text.print("unix!{s}\n", .{p.fs.socket_path}) catch return text.buffered();
for ([_]?std.Io.net.IpAddress{ p.fs.tcp_address, p.fs.quic_address }, [_][]const u8{ "tcp", "quic" }) |maybe, transport| {
const address = maybe orelse continue;
switch (address) {
.ip4 => |ip| text.print("{s}!{d}.{d}.{d}.{d}!{d}\n", .{ transport, ip.bytes[0], ip.bytes[1], ip.bytes[2], ip.bytes[3], ip.port }) catch
- return Reply.fail(req.tag, E.IO),
+ return text.buffered(),
.ip6 => |ip| text.print("{s}!{f}!{d}\n", .{ transport, std.Io.net.Ip6Address.Unresolved{ .bytes = ip.bytes, .interface_name = null }, ip.port }) catch
- return Reply.fail(req.tag, E.IO),
+ return text.buffered(),
}
}
+ return text.buffered();
+}
+
+pub fn listenersLen(p: *Pardes) u64 {
+ var buf: [listeners_cap]u8 = undefined;
+ return listenersText(p, &buf).len;
+}
+
+pub fn readListeners(p: *Pardes, req: Req) Reply {
+ var buf: [listeners_cap]u8 = undefined;
const out = p.fs.stage(p.gpa);
- out.appendSlice(p.gpa, text.buffered()) catch return Reply.fail(req.tag, E.NOMEM);
+ out.appendSlice(p.gpa, listenersText(p, &buf)) catch return Reply.fail(req.tag, E.NOMEM);
return tree.stagedReply(p, req);
}
diff --git a/src/ninep/testing.zig b/src/ninep/testing.zig
index ba26cb38..ad1d84e4 100644
--- a/src/ninep/testing.zig
+++ b/src/ninep/testing.zig
@@ -69,13 +69,30 @@ pub fn look_up(p: *Pardes, dir: u64, name: []const u8) Answer {
return call(p, .{ .tag = 3, .op = .lookup, .node = dir, .data = name });
}
-/// Opens /new once, as a client would, and returns the pane it made.
+/// Creates a pane in /pane, as a client's mkdir does, and returns its serial.
pub fn newPane(p: *Pardes) !u32 {
- const new = @intFromEnum(tree.TopFile.new);
- const opened = call(p, .{ .tag = 9, .op = .open, .node = new });
- if (opened.reply.status != .ok or opened.reply.handle == 0) return error.NoPane;
- _ = call(p, .{ .tag = 9, .op = .release, .node = new, .handle = opened.reply.handle });
- return opened.reply.handle;
+ const made = mkdir(p, "scratch");
+ if (made.reply.status != .ok) return error.NoPane;
+ const target = tree.Node.target(made.reply.attr.node) orelse return error.NoPane;
+ return switch (target) {
+ .pane => |t| t.serial,
+ .top => error.NoPane,
+ };
+}
+
+pub fn mkdir(p: *Pardes, name: []const u8) Answer {
+ return call(p, .{
+ .tag = 9,
+ .op = .open,
+ .node = @intFromEnum(tree.TopFile.pane),
+ .data = name,
+ .create = true,
+ .perm = 0x8000_0000 | 0o755,
+ });
+}
+
+pub fn rmdir(p: *Pardes, node: u64) Answer {
+ return call(p, .{ .tag = 9, .op = .release, .node = node, .remove = true });
}
pub fn withFile(gpa: std.mem.Allocator, text: []const u8) !*Pardes {
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index 56c3112d..46609649 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -4,8 +4,10 @@
//! extended here with the editor's reply payload; fs.zig keeps host access
//! and mounts.
//!
-//! /README /index /ctl /new /log /screen /listeners
-//! /pane/<serial>/{name,body,tag,ctl,addr,data,xdata,sel,errors,event,pty/}
+//! /README /index /status /look /exec /log /screen /listeners
+//! /pane/ mkdir makes a pane, rmdir closes it
+//! /pane/<serial>/{name,body,tag,ctl,addr,dot,limit,data,xdata,sel,
+//! dirty,mark,scroll,errors,event,look,exec,pty/}
//! /os/... the host filesystem /src/... embedded sources (opt-in)
const std = @import("std");
const cloud9 = @import("cloud9");
@@ -47,11 +49,16 @@ pub const Payload = union(enum) {
pub const Reply = cloud9.fs.ReplyWith(Payload);
+/// Tcreate opens a pane and Tremove closes one; nothing else in the tree is
+/// created or destroyed by the protocol, and wstat stays a truncation.
+pub const features: cloud9.fs.Features = .{ .create = true, .remove = true };
+
pub fn changesPane(req: Req) bool {
return switch (req.op) {
.write, .setattr => true,
- .open => req.node == @intFromEnum(TopFile.new),
- .lookup, .getattr, .read, .release, .readdir => false,
+ .open => req.create,
+ .release => req.remove,
+ .lookup, .getattr, .read, .readdir => false,
};
}
@@ -63,8 +70,9 @@ pub const TopFile = enum(u4) {
root = 1,
README,
index,
- ctl,
- new,
+ status,
+ look,
+ exec,
log,
screen,
listeners,
@@ -77,8 +85,8 @@ pub const TopFile = enum(u4) {
pub fn mode(f: TopFile) u16 {
return switch (f) {
.root, .pane => 0o755,
- .ctl => 0o666,
- .README, .index, .new, .log, .screen, .listeners => 0o444,
+ .look, .exec => 0o666,
+ .README, .index, .status, .log, .screen, .listeners => 0o444,
};
}
@@ -89,18 +97,26 @@ pub const TopFile = enum(u4) {
pub const root: u64 = @intFromEnum(TopFile.root);
-pub const PaneFile = enum(u4) {
+/// Declaration order is readdir order.
+pub const PaneFile = enum(u5) {
dir = 0,
name,
body,
tag,
ctl,
addr,
+ dot,
+ limit,
data,
xdata,
sel,
+ dirty,
+ mark,
+ scroll,
errors,
event,
+ look,
+ exec,
pty,
pty_ctl,
pty_status,
@@ -138,8 +154,8 @@ pub const PaneFile = enum(u4) {
};
pub const Node = packed struct(u64) {
- file: u4 = 0,
- serial: u60 = 0,
+ file: u5 = 0,
+ serial: u59 = 0,
pub fn of(serial: u32, file: PaneFile) u64 {
std.debug.assert(serial != 0);
@@ -240,8 +256,13 @@ pub fn stagedReply(p: *Pardes, req: Req) Reply {
// ---- dispatch ----
pub fn handle(p: *Pardes, req: Req) Reply {
- if (req.node == fs.os_root or req.node & fs.os_node != 0) return fs.osHandle(p, req);
- if (req.node & sources.archive_node != 0) return sources.handle(p, req);
+ const host = req.node == fs.os_root or req.node & fs.os_node != 0;
+ const archive = req.node & sources.archive_node != 0;
+ // Only /pane is created in and removed from. The host tree and the
+ // embedded sources say so, rather than quietly doing nothing.
+ if ((host or archive) and (req.create or req.remove)) return Reply.fail(req.tag, E.PERM);
+ if (host) return fs.osHandle(p, req);
+ if (archive) return sources.handle(p, req);
const target = Node.target(req.node) orelse return Reply.fail(req.tag, E.NOENT);
if (req.op == .write or req.op == .setattr) p.fs.origin = switch (target) {
.pane => |t| @as(u8, if (t.file == .body or t.file == .tag) 'E' else 'F'),
@@ -280,6 +301,7 @@ fn attrOf(p: *Pardes, target: Target) ?Reply.Attr {
.mode = t.file.mode(),
.size = pane.fileSize(p, id, t.file),
.mtime = pane.mtimeOf(p, pn),
+ .version = pane.versionOf(pn, t.file),
};
},
}
@@ -292,13 +314,14 @@ fn attrReply(p: *Pardes, tag: u64, target: Target) Reply {
fn topSize(p: *Pardes, f: TopFile) u64 {
return switch (f) {
- .root, .pane, .new, .log, .screen, .listeners => 0,
+ // ponytail: /screen has no length until an open renders its frame.
+ .root, .pane, .screen => 0,
.index => pane.indexLen(p),
.README => fs.help.len,
- .ctl => ctl: {
- var buf: [ctl.root_text_cap]u8 = undefined;
- break :ctl ctl.rootText(p, &buf).len;
- },
+ .status => ctl.statusLen(p),
+ .look, .exec => ctl.resultsLen(p),
+ .log => events.pending(&p.fs.log),
+ .listeners => screen.listenersLen(p),
};
}
@@ -366,7 +389,7 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply {
switch (target) {
.top => |f| switch (f) {
.root => {
- inline for (.{ TopFile.README, TopFile.index, TopFile.ctl, TopFile.new, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.pane }) |t| {
+ inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.pane }) |t| {
if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(t), t.dir(), t.fileName());
}
if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, fs.os_root, true, "os");
@@ -403,21 +426,27 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply {
return .{ .tag = req.tag, .payload = .{ .staged = @intCast(out.items.len) } };
}
-fn newPane(p: *Pardes) ?u32 {
- const slot = p.freeSlot() orelse return null;
+/// Tcreate in /pane opens a pane, the way mkdir opens a directory. The name
+/// asked for is ignored: a pane is named by the serial the editor gives it,
+/// which the reply carries back and /index lists last.
+fn create(p: *Pardes, req: Req, target: Target) Reply {
+ switch (target) {
+ .top => |f| if (f != .pane) return Reply.fail(req.tag, E.PERM),
+ .pane => return Reply.fail(req.tag, E.PERM),
+ }
+ if (req.perm & cloud9.dmdir == 0) return Reply.fail(req.tag, E.PERM);
+ const slot = p.freeSlot() orelse return Reply.fail(req.tag, E.NFILE);
p.newScratchBelow(p.active);
- const pn = p.panes[slot] orelse return null;
- return pn.serial;
+ const made = p.panes[slot] orelse return Reply.fail(req.tag, E.NFILE);
+ const attr = attrOf(p, .{ .pane = .{ .serial = made.serial, .file = .dir } }) orelse
+ return Reply.fail(req.tag, E.NFILE);
+ return .{ .tag = req.tag, .handle = 1, .attr = attr };
}
fn open(p: *Pardes, req: Req, target: Target) Reply {
+ if (req.create) return create(p, req, target);
switch (target) {
.top => |f| switch (f) {
- // The clone idiom: each open makes one empty pane; reads answer its serial.
- .new => {
- const serial = newPane(p) orelse return Reply.fail(req.tag, E.NFILE);
- return .{ .tag = req.tag, .handle = serial };
- },
.screen => return screen.openSnapshot(p, req, true),
.log => p.fs.log_readers +|= 1,
else => {},
@@ -429,11 +458,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply {
if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT);
switch (t.file) {
.body => if (pn.isTerminal()) return screen.openSnapshot(p, req, false),
- .ctl => pf.limit = null,
- .addr => {
- pf.addr = .{};
- pf.limit = null;
- },
+ .addr => pf.addr = .{},
.event => {
pf.readers +|= 1;
p.fs.listeners +|= 1;
@@ -446,7 +471,27 @@ fn open(p: *Pardes, req: Req, target: Target) Reply {
return .{ .tag = req.tag, .handle = 1 };
}
+/// Tremove closes a pane. Nothing else in the tree can be removed, and this
+/// is also where an ORCLOSE open lands, so every node is checked here.
+fn remove(p: *Pardes, req: Req) Reply {
+ const target = Node.target(req.node) orelse return Reply.fail(req.tag, E.NOENT);
+ const t = switch (target) {
+ .pane => |t| t,
+ .top => return Reply.fail(req.tag, E.PERM),
+ };
+ if (t.file != .dir) return Reply.fail(req.tag, E.PERM);
+ const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT);
+ p.removePane(id) catch return Reply.fail(req.tag, E.IO);
+ return .{ .tag = req.tag };
+}
+
fn release(p: *Pardes, req: Req) Reply {
+ // The handle's bookkeeping runs whether or not the removal is allowed.
+ const done = releaseHandle(p, req);
+ return if (req.remove) remove(p, req) else done;
+}
+
+fn releaseHandle(p: *Pardes, req: Req) Reply {
if (screen.releaseSnapshot(p, req)) return .{ .tag = req.tag };
const target = Node.target(req.node) orelse return .{ .tag = req.tag };
switch (target) {
@@ -502,12 +547,8 @@ fn read(p: *Pardes, req: Req, target: Target) Reply {
}
break :index stagedReply(p, req);
},
- .ctl => ctl.readRoot(p, req),
- .new => new: {
- if (req.handle == 0) break :new Reply.fail(req.tag, E.INVAL);
- p.fs.stage(p.gpa).print(p.gpa, "{d}\n", .{req.handle}) catch break :new Reply.fail(req.tag, E.NOMEM);
- break :new stagedReply(p, req);
- },
+ .status => ctl.readStatus(p, req),
+ .look, .exec => ctl.readResults(p, req),
.log => events.readQueue(p, req, &p.fs.log),
.screen => screen.readSnapshot(p, req, null),
.listeners => screen.readListeners(p, req),
@@ -525,7 +566,7 @@ fn read(p: *Pardes, req: Req, target: Target) Reply {
fn write(p: *Pardes, req: Req, target: Target) Reply {
switch (target) {
.top => |f| return switch (f) {
- .ctl => ctl.writeRoot(p, req),
+ .look, .exec => ctl.command(p, req, null, f == .exec),
else => Reply.fail(req.tag, E.PERM),
},
.pane => |t| {
@@ -587,8 +628,8 @@ test "filesystem inspection preserves pending and displayed Look hover" {
const preview = p.look_hover_preview;
const requests = [_]Req{
.{ .tag = 1, .op = .lookup, .node = root, .data = "screen" },
- .{ .tag = 1, .op = .lookup, .node = root, .data = "new" },
- .{ .tag = 1, .op = .getattr, .node = @intFromEnum(TopFile.new) },
+ .{ .tag = 1, .op = .lookup, .node = root, .data = "status" },
+ .{ .tag = 1, .op = .getattr, .node = @intFromEnum(TopFile.pane) },
.{ .tag = 2, .op = .getattr, .node = body },
.{ .tag = 3, .op = .open, .node = body },
.{ .tag = 4, .op = .read, .node = body, .size = 5 },
@@ -614,7 +655,7 @@ test "filesystem inspection preserves pending and displayed Look hover" {
test "filesystem pane creation and truncation cancel Look hover" {
const requests = [_]Req{
- .{ .tag = 2, .op = .open, .node = @intFromEnum(TopFile.new) },
+ .{ .tag = 2, .op = .open, .node = @intFromEnum(TopFile.pane), .data = "x", .create = true, .perm = cloud9.dmdir | 0o755 },
.{ .tag = 3, .op = .setattr, .node = 0, .truncate = true },
};
for (requests) |request| {
@@ -644,13 +685,13 @@ test "readdir lists the root and a pane directory without creating anything" {
var top_bytes: [4096]u8 = undefined;
@memcpy(top_bytes[0..top_reply.bytes.len], top_reply.bytes);
const top = th.dirents(top_bytes[0..top_reply.bytes.len], &buf);
- try testing.expect(top.len >= 9);
- for ([_][]const u8{ "README", "index", "ctl", "new", "log", "screen", "listeners", "pane", "os" }, 0..) |name, i| {
+ try testing.expect(top.len >= 10);
+ for ([_][]const u8{ "README", "index", "status", "look", "exec", "log", "screen", "listeners", "pane", "os" }, 0..) |name, i| {
try testing.expectEqualStrings(name, top[i].name);
- try testing.expectEqual(i >= 7, top[i].dir);
+ try testing.expectEqual(i >= 8, top[i].dir);
}
- try testing.expectEqual(@intFromEnum(TopFile.pane), top[7].node);
- try testing.expectEqual(fs.os_root, top[8].node);
+ try testing.expectEqual(@intFromEnum(TopFile.pane), top[8].node);
+ try testing.expectEqual(fs.os_root, top[9].node);
var idbuf: [16]u8 = undefined;
try testing.expect(th.nameAt(top, try std.fmt.bufPrint(&idbuf, "{d}", .{serial})) == null);
try testing.expect(th.nameAt(top, "self") == null);
@@ -674,13 +715,18 @@ test "readdir lists the root and a pane directory without creating anything" {
}
const panes_dir = rdir(p, @intFromEnum(TopFile.pane), 0);
const listed = th.dirents(panes_dir.bytes, &buf);
- try testing.expectEqual(@as(usize, 1), listed.len);
- try testing.expectEqual(Node.of(serial, .dir), listed[0].node);
+ var idname: [16]u8 = undefined;
+ const own = th.nameAt(listed, try std.fmt.bufPrint(&idname, "{d}", .{serial})).?;
+ try testing.expectEqual(Node.of(serial, .dir), own.node);
+ try testing.expect(own.dir);
const dir = rdir(p, Node.of(serial, .dir), 0);
const files = th.dirents(dir.bytes, &buf);
- try testing.expectEqual(@as(usize, 10), files.len);
- for ([_][]const u8{ "name", "body", "tag", "ctl", "addr", "data", "xdata", "sel", "errors", "event" }, 0..) |name, i|
+ try testing.expectEqual(@as(usize, 17), files.len);
+ for ([_][]const u8{
+ "name", "body", "tag", "ctl", "addr", "dot", "limit", "data", "xdata",
+ "sel", "dirty", "mark", "scroll", "errors", "event", "look", "exec",
+ }, 0..) |name, i|
try testing.expectEqualStrings(name, files[i].name);
try testing.expect(th.nameAt(files, ".") == null);
try testing.expect(th.nameAt(files, "rdsel") == null);
@@ -703,8 +749,10 @@ test "lookup resolves top files, pane serials and pane files" {
const serial = serialOf(p);
try testing.expectEqual(@as(u64, @intFromEnum(TopFile.index)), look_up(p, root, "index").reply.attr.node);
- try testing.expect(!look_up(p, root, "new").reply.attr.dir);
- try testing.expectEqual(@as(u16, 0o444), look_up(p, root, "new").reply.attr.mode);
+ try testing.expect(!look_up(p, root, "status").reply.attr.dir);
+ try testing.expectEqual(@as(u16, 0o444), look_up(p, root, "status").reply.attr.mode);
+ try testing.expectEqual(@as(u16, 0o666), look_up(p, root, "look").reply.attr.mode);
+ try testing.expectEqual(E.NOENT, look_up(p, root, "new").errno());
try testing.expect(look_up(p, root, "pane").reply.attr.dir);
try testing.expect(look_up(p, root, "os").reply.attr.dir);
try testing.expectEqual(E.NOENT, look_up(p, root, "nosuchthing").errno());
@@ -732,38 +780,46 @@ test "lookup resolves top files, pane serials and pane files" {
try testing.expectEqual(E.NOTDIR, look_up(p, Node.of(serial, .body), "x").errno());
}
-test "each open of new creates exactly one pane whose serial the read answers" {
+test "creating in the pane directory opens a pane and removing one closes it" {
const gpa = testing.allocator;
const p = try withFile(gpa, "first\n");
defer p.deinit();
const before = p.next_serial;
- const new = @intFromEnum(TopFile.new);
+ const panes_dir = @intFromEnum(TopFile.pane);
- try testing.expectEqual(Status.ok, look_up(p, root, "new").reply.status);
- try testing.expectEqual(Status.ok, call(p, .{ .tag = 1, .op = .getattr, .node = new }).reply.status);
+ // Browsing /pane creates nothing; only a create does.
+ try testing.expectEqual(Status.ok, rdir(p, panes_dir, 0).reply.status);
try testing.expectEqual(before, p.next_serial);
- const opened = call(p, .{ .tag = 1, .op = .open, .node = new });
- try testing.expectEqual(Status.ok, opened.reply.status);
+ const made = th.mkdir(p, "scratch");
+ try testing.expectEqual(Status.ok, made.reply.status);
try testing.expectEqual(before + 1, p.next_serial);
- try testing.expectEqual(before + 1, opened.reply.handle);
- try testing.expectEqual(@as(u64, 0), opened.reply.attr.node);
+ const serial = before + 1;
+ try testing.expectEqual(Node.of(serial, .dir), made.reply.attr.node);
+ try testing.expect(made.reply.attr.dir);
var expected: [16]u8 = undefined;
- const line = try std.fmt.bufPrint(&expected, "{d}\n", .{opened.reply.handle});
- try testing.expectEqualStrings(line, call(p, .{ .tag = 2, .op = .read, .node = new, .handle = opened.reply.handle, .size = 64 }).bytes);
- try testing.expectEqualStrings(line[1..], call(p, .{ .tag = 2, .op = .read, .node = new, .handle = opened.reply.handle, .off = 1, .size = 64 }).bytes);
- try testing.expectEqualStrings("", call(p, .{ .tag = 2, .op = .read, .node = new, .handle = opened.reply.handle, .off = line.len, .size = 64 }).bytes);
- try testing.expectEqual(before + 1, p.next_serial);
- _ = call(p, .{ .tag = 3, .op = .release, .node = new, .handle = opened.reply.handle });
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}", .{serial}), made.reply.attr.name);
- const id = p.paneBySerial(opened.reply.handle).?;
+ const id = p.paneBySerial(serial).?;
try testing.expectEqualStrings("", p.panes[id].?.file.?.content);
- _ = wr(p, Node.of(opened.reply.handle, .body), "hi");
- const second = call(p, .{ .tag = 4, .op = .open, .node = new });
+ _ = wr(p, Node.of(serial, .body), "hi");
+ const second = th.mkdir(p, "another");
try testing.expectEqual(Status.ok, second.reply.status);
- try testing.expect(second.reply.handle != opened.reply.handle);
+ try testing.expect(second.reply.attr.node != made.reply.attr.node);
try testing.expectEqualStrings("hi", p.panes[id].?.file.?.content);
- try testing.expectEqual(E.INVAL, call(p, .{ .tag = 5, .op = .read, .node = new, .size = 64 }).errno());
+
+ // A plain file, and a create anywhere else, are refused.
+ try testing.expectEqual(E.PERM, call(p, .{ .tag = 1, .op = .open, .node = panes_dir, .data = "f", .create = true, .perm = 0o666 }).errno());
+ try testing.expectEqual(E.PERM, call(p, .{ .tag = 1, .op = .open, .node = root, .data = "d", .create = true, .perm = cloud9.dmdir | 0o755 }).errno());
+ try testing.expectEqual(E.PERM, call(p, .{ .tag = 1, .op = .open, .node = Node.of(serial, .dir), .data = "d", .create = true, .perm = cloud9.dmdir | 0o755 }).errno());
+
+ // Tremove closes the pane it names, and nothing else in the tree.
+ try testing.expectEqual(E.PERM, th.rmdir(p, Node.of(serial, .body)).errno());
+ try testing.expectEqual(E.PERM, th.rmdir(p, @intFromEnum(TopFile.index)).errno());
+ try testing.expect(p.paneBySerial(serial) != null);
+ try testing.expectEqual(Status.ok, th.rmdir(p, Node.of(serial, .dir)).reply.status);
+ try testing.expect(p.paneBySerial(serial) == null);
+ try testing.expectEqual(E.NOENT, th.rmdir(p, Node.of(serial, .dir)).errno());
}
test "directories walk to their parents" {
@@ -791,14 +847,15 @@ test "editor paths resolve to the same nodes the wire serves" {
try testing.expectEqual(root, resolveSelf(p, "").?);
try testing.expectEqual(root, resolveSelf(p, "/./pane/../").?);
try testing.expectEqual(@intFromEnum(TopFile.index), resolveSelf(p, "index").?);
- try testing.expectEqual(@intFromEnum(TopFile.new), resolveSelf(p, "new").?);
+ try testing.expectEqual(@intFromEnum(TopFile.status), resolveSelf(p, "status").?);
+ try testing.expect(resolveSelf(p, "new") == null);
try testing.expectEqual(@intFromEnum(TopFile.pane), resolveSelf(p, "pane").?);
try testing.expectEqual(Node.of(serial, .dir), resolveSelf(p, try std.fmt.bufPrint(&path, "pane/{d}", .{serial})).?);
try testing.expectEqual(Node.of(serial, .sel), resolveSelf(p, try std.fmt.bufPrint(&path, "pane/{d}/sel", .{serial})).?);
try testing.expect(resolveSelf(p, try std.fmt.bufPrint(&path, "pane/{d}/pty/ctl", .{serial})) == null);
try testing.expect(resolveSelf(p, try std.fmt.bufPrint(&path, "pane/{d}/rdsel", .{serial})) == null);
try testing.expect(resolveSelf(p, "self/index") == null);
- try testing.expect(resolveSelf(p, "new/ctl") == null);
+ try testing.expect(resolveSelf(p, "status/ctl") == null);
try testing.expect(resolveSelf(p, "cons") == null);
try testing.expect(resolveSelf(p, "pane/0") == null);
}