summaryrefslogtreecommitdiff
path: root/src/ninep/ctl.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-21 20:07:43 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit9070942b29bd10dddcdecdb0e88ba0fb40608467 (patch)
treefa1fc5984c7847c52bf4e78d277586edce2d8325 /src/ninep/ctl.zig
parent0122e94fb37422085325f2dc78ca015051ce5f91 (diff)
downloadpardes-9070942b29bd10dddcdecdb0e88ba0fb40608467.tar.gz
pardes-9070942b29bd10dddcdecdb0e88ba0fb40608467.zip
Plan 9 idiom for the control filesystem, and the regressions a624a56 left
The 9P tree stops being a command language wearing a filesystem. /new created a pane as a side effect of a *read*; it is now Tcreate in /pane, with Tremove to close, which cloud9's engine has always supported and the editor never declared: tree.zig now says `features = .{ .create = true, .remove = true }`. Eleven pane ctl verbs become files that can be read as well as written -- dot, limit, dirty, mark, scroll, look, exec -- leaving ctl with `get`, the one verb no file would say better. Root /ctl splits into a read-only /status and the /look and /exec files whose write IS the click. stat carries real sizes where it used to answer 0, and qid versions track a pane's revision, so a client can poll for change without re-reading the body. Commit a624a56 moved raw-tty keys to an early-return branch that knew only Ctrl-B and bare Escape, and in the same edit deleted the paste branch below it. That cost Shift-Escape (the unconditional way out of tty mode) and both paste chords: Ctrl-V and Ctrl-Shift-V reached the child as keystrokes, so an agent CLI running in a pane took Ctrl-V for its image-paste binding and answered "No image found in clipboard". Both are restored, with tests. Nested detection was not subtly broken but deleted: 60367d8 removed nested.zig's process-ancestry walk and left "am I inside pardes" derived from PARDES_FORWARD_LOOK, which read "0" both for --nested and for "the listener did not come up". PARDES_PID now answers that question on its own, checked with kill(pid, 0); PARDES_9P and PARDES_PANE answer how to reach it; the flag is gone. The posted-9P registry also self-heals now -- a session that aborts cannot unlink its own socket, so posting sweeps entries whose target refuses a connection, symlinks only and on a definite ECONNREFUSED only. Elsewhere: tty scrolling is sticky-bottom, following new output only from the last row, with typing and entering raw mode snapping back to live; the boot layouts are a Boot enum instead of a chain of ifs, and the bare tty startup (Boot.tty, which main.zig names) opens an empty text pane under the shell while tests keep Boot.tty_shell; builtins announce themselves on the message row under a Verbose setting that is on by default; Config prints each setting the way you would type it back, so WindowOpacity 70 rather than "WindowOpacity: 70%"; LocationsConfig opens its window only when called bare; every tagline puts the word that closes the thing last, and a column now outlives its panes -- closing the last one leaves an empty pane, and only Delcol, newly on the column tagline, takes the column away. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'src/ninep/ctl.zig')
-rw-r--r--src/ninep/ctl.zig317
1 files changed, 129 insertions, 188 deletions
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 } });