From 9070942b29bd10dddcdecdb0e88ba0fb40608467 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 21 Sep 2026 20:07:43 -0300 Subject: 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) --- src/ninep/ctl.zig | 319 ++++++++++++++++++++++-------------------------------- 1 file changed, 130 insertions(+), 189 deletions(-) (limited to 'src/ninep/ctl.zig') 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//; 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.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.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_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 } }); -- cgit v1.3