summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 16:53:27 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit916615648fbef6998244f23a2b1045de7edb3a75 (patch)
treefd9d132d18e86d9a128e97ae012a43a6e0e7a9f8
parent16d5415c508fea0b5dc90dd2cc40866beecf216b (diff)
downloadpardes-916615648fbef6998244f23a2b1045de7edb3a75.tar.gz
pardes-916615648fbef6998244f23a2b1045de7edb3a75.zip
A pane's ctl takes acme's ctl words, done by the files that replace them
name, put, clean, dirty, del, delete, dot=addr, addr=dot, limit=addr, mark, nomark, show and cleartag were unknown control messages. Each is now done by the file or builtin that replaces it (name, Save, dirty, Del, a Del that does not ask, dot and addr and limit, mark, the tag), checked with the rest of the write before any runs; dump, dumpdir, font, menu and nomenu are refused, EINVAL, saying where pardes does it or that it has no such thing. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md7
-rw-r--r--src/ninep/ctl.zig117
2 files changed, 113 insertions, 11 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 4fb15eb8..dfd887ac 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -276,7 +276,12 @@ below, `Save f`, `Collapse`, which folds that pane, `Undo` and
`Redo`, which step its body through its last 256 edits -- with none left
they say `Undo: nothing to undo` and the write still succeeds, as acme's
Undo is silent --, `Find pat`)
-beside acme's `get`, `lock` and `unlock`. The column words are pane words
+beside acme's `get`, `lock` and `unlock`; acme's other ctl words are taken
+too, done by the file or builtin that replaces each: `name x` (the `name`
+file), `put` (`Save`), `clean` and `dirty` (`dirty`), `del` (`Del`),
+`delete` (a `Del` that does not ask), `dot=addr`, `addr=dot`,
+`limit=addr`, `mark`, `nomark` (`mark`), `show` and `cleartag`; `dump`,
+`dumpdir`, `font`, `menu` and `nomenu` are refused with why, EINVAL. The column words are pane words
too, acting on the column that pane is in: `Delcol`, `DelAbove`, `DelBelow`
and the focus moves `Left`/`Right`/`Up`/`Down` from it. `Joincol` and
`Newcol` are the root's: they act on the column of the pane with the
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index a8d4e8e0..403100f2 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -784,10 +784,19 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply {
if (apply) p.fs.results_len = 0;
var held = !other and pane.fs.lock != null;
var it = Messages.init(p, req.data);
- while (it.next()) |line| {
- if (line.len == 0) continue;
+ while (it.next()) |line_in| {
+ if (line_in.len == 0) continue;
// A line before this one may have closed the pane (`Del`).
if (apply and p.paneBySerial(serial) == null) return Reply.fail(req.tag, E.NOENT);
+ // acme's ctl words: done by the file that replaces each, or the
+ // builtin that does it, checked with the rest before any runs.
+ var line = line_in;
+ switch (acmeCtl(p, req, serial, line_in, apply)) {
+ .no => {},
+ .done => continue,
+ .refused => |r| return r,
+ .builtin => |word| line = word,
+ }
if (std.mem.eql(u8, line, "get")) {
if (!apply) continue;
// Quoting the line, as a builtin's refusal (Save's) does.
@@ -861,6 +870,72 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply {
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
+/// acme's pane ctl vocabulary (editors/acme/xfid.c xfidctlwrite): each word
+/// done here by the file that replaces it, or run as the builtin that does
+/// it; those with no place here refused with why.
+const AcmeCtl = union(enum) { no, done, refused: Reply, builtin: []const u8 };
+
+fn acmeCtl(p: *Pardes, req: Req, serial: u32, line: []const u8, apply: bool) AcmeCtl {
+ const id = p.paneBySerial(serial) orelse return .no;
+ const pane = p.panes[id].?;
+ const word = line[0 .. std.mem.indexOfScalar(u8, line, ' ') orelse line.len];
+ const rest = std.mem.trim(u8, line[word.len..], " ");
+ const File = tree.PaneFile;
+ // A word written to one of the pane's own files, as acme's is to its.
+ const Into = struct { file: File, data: []const u8 };
+ const into: ?Into = if (std.mem.eql(u8, word, "name") and rest.len > 0)
+ .{ .file = .name, .data = rest }
+ else if (std.mem.eql(u8, line, "clean"))
+ .{ .file = .dirty, .data = "0" }
+ else if (std.mem.eql(u8, line, "dirty"))
+ .{ .file = .dirty, .data = "1" }
+ else if (std.mem.eql(u8, line, "mark"))
+ .{ .file = .mark, .data = "1" }
+ else if (std.mem.eql(u8, line, "nomark"))
+ .{ .file = .mark, .data = "0" }
+ else
+ null;
+ if (into) |w| {
+ if (!apply) return .done;
+ const r = pane_files.write(p, .{ .tag = req.tag, .op = .write, .node = tree.Node.of(serial, w.file), .data = w.data }, id, pane, w.file);
+ return if (r.status == .err) .{ .refused = r } else .done;
+ }
+ if (std.mem.eql(u8, line, "put")) return .{ .builtin = "Save" };
+ if (std.mem.eql(u8, line, "del")) return .{ .builtin = "Del" };
+ if (std.mem.eql(u8, line, "delete")) return .{ .builtin = "Delete" };
+ const pf = &pane.fs;
+ if (std.mem.eql(u8, line, "dot=addr")) {
+ if (apply) pane_files.setDot(pane, pf.addr);
+ return .done;
+ }
+ if (std.mem.eql(u8, line, "addr=dot")) {
+ if (apply) pf.addr = pane_files.dotOf(pane);
+ return .done;
+ }
+ if (std.mem.eql(u8, line, "limit=addr")) {
+ if (apply) pf.limit = pf.addr;
+ return .done;
+ }
+ if (std.mem.eql(u8, line, "show")) {
+ if (apply) pane_files.showOffset(pane, pane_files.dotOf(pane).q0);
+ return .done;
+ }
+ if (std.mem.eql(u8, line, "cleartag")) {
+ if (apply) _ = pane_files.truncate(p, pane, .tag);
+ return .done;
+ }
+ const none: ?[]const u8 = if (std.mem.eql(u8, word, "dump") or std.mem.eql(u8, word, "dumpdir"))
+ "a pane is dumped with the session: Dump and DumpDir on /ctl"
+ else if (std.mem.eql(u8, word, "font"))
+ "the font is the session's: Font on /ctl"
+ else if (std.mem.eql(u8, line, "menu") or std.mem.eql(u8, line, "nomenu"))
+ "pardes has no menu"
+ else
+ null;
+ if (none) |why| return .{ .refused = tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "invalid: acme's {s}: {s}", .{ word, why }) catch "invalid: acme's ctl word") };
+ return .no;
+}
+
const e_not_locked = "pane not locked by this open";
/// Who holds it is another open of this ctl, whose client pardes cannot name.
const e_locked = tree.e_in_use ++ ": another open of this ctl holds the lock";
@@ -1041,14 +1116,9 @@ test "the pane ctl takes acme's verbs and the pane's builtins, and refuses the r
defer p.deinit();
const serial = serialOf(p);
const ctl_node = Node.of(serial, .ctl);
- // acme's other ctl words have files of their own here, and a builtin is
- // its own capitalised word: `GET`, `DEL` and `del` are none of them
- // (acme's own `Get` is refused as acme's).
- for ([_][]const u8{
- "menu", "nomenu", "dump echo hi", "font Go Mono", "lock x", "bogus", "DEL",
- "put", "del", "delete", "clean", "dirty", "show", "mark",
- "GET", "limit=addr", "get x", "look /tmp", "exec Del",
- }) |bad| {
+ // A builtin is its own capitalised word: `GET` and `DEL` are none;
+ // acme's own words are done or refused as acme's (the test below).
+ for ([_][]const u8{ "lock x", "bogus", "DEL", "GET", "get x", "look /tmp", "exec Del" }) |bad| {
const refused = wr(p, ctl_node, bad);
try testing.expectEqual(E.INVAL, refused.errno());
var want: [64]u8 = undefined;
@@ -2302,6 +2372,33 @@ test "acme's words run as pardes's (Put is Save, Delete a Del that does not ask)
try testing.expect(p.paneBySerial(made) == null);
}
+test "a pane's ctl takes acme's words: done by the files that replace them, or refused with why" {
+ const p = try withFile(testing.allocator, "one two\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const ctl_node = Node.of(serial, .ctl);
+ const pane = p.panes[p.paneBySerial(serial).?].?;
+ try testing.expectEqual(Status.ok, wr(p, ctl_node, "name /tmp/pardes-acme-ctl.txt\n").reply.status);
+ try testing.expectEqualStrings("/tmp/pardes-acme-ctl.txt", pane.file.?.path);
+ try testing.expectEqual(Status.ok, wr(p, ctl_node, "dirty\n").reply.status);
+ try testing.expectEqualStrings("1", std.mem.trimEnd(u8, rd(p, Node.of(serial, .dirty), 0, 8).bytes, "\n"));
+ try testing.expectEqual(Status.ok, wr(p, ctl_node, "clean\n").reply.status);
+ try testing.expectEqualStrings("0", std.mem.trimEnd(u8, rd(p, Node.of(serial, .dirty), 0, 8).bytes, "\n"));
+ _ = wr(p, Node.of(serial, .addr), "/two/");
+ try testing.expectEqual(Status.ok, wr(p, ctl_node, "dot=addr\nshow\nnomark\nmark\nlimit=addr\n").reply.status);
+ try testing.expectEqual(@as(u32, 4), pane_files.dotOf(pane).q0);
+ try testing.expect(pane.fs.limit != null);
+ try testing.expectEqual(Status.ok, wr(p, ctl_node, "cleartag\n").reply.status);
+ for ([_][]const u8{ "menu", "nomenu", "dump echo hi", "dumpdir /tmp", "font Go Mono" }) |line| {
+ const r = wr(p, ctl_node, line);
+ try testing.expectEqual(E.INVAL, r.errno());
+ try testing.expect(std.mem.startsWith(u8, r.reply.ename, "invalid: acme's "));
+ }
+ // Checked whole: a refused word anywhere and nothing ran.
+ try testing.expectEqual(E.INVAL, wr(p, ctl_node, "dirty\nmenu\n").errno());
+ try testing.expectEqualStrings("0", std.mem.trimEnd(u8, rd(p, Node.of(serial, .dirty), 0, 8).bytes, "\n"));
+}
+
test "every EINVAL a write gets says why, in its err record too; DEL is a control character in a line" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();