diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 16:53:27 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 916615648fbef6998244f23a2b1045de7edb3a75 (patch) | |
| tree | fd9d132d18e86d9a128e97ae012a43a6e0e7a9f8 | |
| parent | 16d5415c508fea0b5dc90dd2cc40866beecf216b (diff) | |
| download | pardes-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.md | 7 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 117 |
2 files changed, 113 insertions, 11 deletions
@@ -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(); |
