diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 09:50:21 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 12d387e4a03204a2b6d0c51633e228063e883fad (patch) | |
| tree | 9d1af954cacbbc90f60bd85499088865c786f993 | |
| parent | f5eaddb2cf28a9b72d5dd5b3b10502a316d4de85 (diff) | |
| download | pardes-12d387e4a03204a2b6d0c51633e228063e883fad.tar.gz pardes-12d387e4a03204a2b6d0c51633e228063e883fad.zip | |
A root commands file lists every builtin, its argument and which ctl takes it
A script could learn the builtin words only from source or the Help
window. /commands is generated from the registry: one line per builtin,
Word or Word arg, then root or pane for the ctl that takes it, so it is
always this build's own list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | .agents/skills/pardes-9p/SKILL.md | 1 | ||||
| -rw-r--r-- | docs/fs.md | 6 | ||||
| -rw-r--r-- | src/fs-help.txt | 4 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 50 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 7 | ||||
| -rw-r--r-- | test/fs.py | 6 |
6 files changed, 69 insertions, 5 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md index 5540db5f..ec5f811f 100644 --- a/.agents/skills/pardes-9p/SKILL.md +++ b/.agents/skills/pardes-9p/SKILL.md @@ -55,6 +55,7 @@ $m/listeners this session's dial addresses $m/focus the serial of the pane with the keyboard; echo a serial into it to move the keyboard $m/ctl the settings, one a line as a write takes them; write a setting or a session builtin (Newcol, Dump, Kill, Theme x); a pane's builtins (Del, Save f) go to $m/pane/<n>/ctl +$m/commands every builtin: `Word`, `Word arg`, then `root` or `pane` (which ctl takes it) $m/pane/new open it to make a pane, read names it; rmdir $m/pane/<n> closes it $m/os/ the host filesystem ``` @@ -82,6 +82,7 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC. /listeners the session's dial addresses /focus the serial of the pane with the keyboard; write a serial to give it the keyboard /ctl the settings, one a line as a write takes them; write a setting or a session builtin +/commands every builtin: word, `arg` if it takes one, and `root` or `pane`, the ctl that takes it /pane/new open it to make a pane; the read answers that pane's serial /pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll errors event look exec, plus pty/{ctl,status,data} on terminals @@ -116,6 +117,11 @@ though a click on the same word written to `exec` still opens it. Like any write, a ctl write answers once the editor has performed what it asked for (a save written, a shell started). +`/commands` lists every builtin the registry holds, in registry order, one +a line: its word, `arg` when it takes one, and `root` or `pane` for the ctl +that takes it, e.g. `Newcol root`, `Save arg pane`, `Verbose arg root`. It is +generated from the registry, so it is always this build's own list. + `/focus` reads the serial of the pane with the keyboard, and a serial written to it gives that pane the keyboard, off any column or workspace tag that had it -- rio's `current` written to a window's `wctl`, named once for the whole diff --git a/src/fs-help.txt b/src/fs-help.txt index f9e9c6bd..44f8f407 100644 --- a/src/fs-help.txt +++ b/src/fs-help.txt @@ -5,12 +5,12 @@ index one line per pane: serial, kind (text|term|pdf|image), dirty flag, n status pid, version and pane count look write a line: a right click on it at the active pane; read: the serials it touched exec write a line: a middle click, an editor command word or a shell line; read the same -log recent events, one a line: new/del/rename/save <serial> <name>, and - msg <serial|-> <text>, what the editor said; write follow to wait (tail -f won't) +log recent events: new/del/rename/save <serial> <name>, msg <serial|-> <text>; write follow to wait screen rendered screen as JSON, frozen from open to close listeners the session's dial addresses focus the serial of the pane with the keyboard; write a serial to give it the keyboard ctl the settings, one a line as a write takes them; write one, or Newcol, Dump, Kill... +commands every builtin: its word, arg if it takes one, and root or pane: which ctl takes it pane/new open it to make a pane; the read answers that pane's serial pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll errors event look exec, and pty/ for terminals; rmdir closes it diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index b43d7b20..14f1ae4f 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -308,6 +308,35 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply { return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } +// ---- /commands ---- + +/// Every builtin the registry holds, one a line: its word, `arg` when it +/// takes one, and which ctl takes it, `root` or `pane` -- the registry +/// itself, so a new builtin is listed the moment it exists. +fn writeCommands(w: *std.Io.Writer) !void { + for (std.enums.values(Builtin)) |b| { + try w.print("{s}{s} {s}\n", .{ + @tagName(b), + if (builtins.registry.takesArg(b)) " arg" else "", + if (builtins.registry.scope(b) == .session) "root" else "pane", + }); + } +} + +pub fn commandsLen() u64 { + var buf: [256]u8 = undefined; + var counting: std.Io.Writer.Discarding = .init(&buf); + writeCommands(&counting.writer) catch return 0; + return counting.fullCount(); +} + +pub fn readCommands(p: *Pardes, req: Req) Reply { + var text: std.Io.Writer.Allocating = .init(p.scratch.allocator()); + writeCommands(&text.writer) catch return Reply.fail(req.tag, E.NOMEM); + p.fs.stage(p.gpa).appendSlice(p.gpa, text.written()) catch return Reply.fail(req.tag, E.NOMEM); + return tree.stagedReply(p, req); +} + // ---- /focus ---- /// The pane with the keyboard, which a write of a serial moves, the way @@ -452,6 +481,27 @@ test "pane ctl read is index's five fields plus width in cells, font, tab width try testing.expectEqualStrings("'it''s'", w.buffered()); } +test "commands lists every builtin with its argument and the ctl that takes it" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const node = @intFromEnum(tree.TopFile.commands); + const listed = rd(p, node, 0, 1 << 16); + try testing.expectEqual(@as(u64, listed.bytes.len), call(p, .{ .tag = 1, .op = .getattr, .node = node }).reply.attr.size); + try testing.expectEqual(E.PERM, wr(p, node, "Del\n").errno()); + var lines: usize = 0; + var it = std.mem.tokenizeScalar(u8, listed.bytes, '\n'); + while (it.next()) |line| : (lines += 1) { + var words = std.mem.tokenizeScalar(u8, line, ' '); + const b = std.meta.stringToEnum(Builtin, words.next().?).?; + if (builtins.registry.takesArg(b)) try testing.expectEqualStrings("arg", words.next().?); + try testing.expectEqualStrings(if (builtins.registry.scope(b) == .session) "root" else "pane", words.next().?); + try testing.expect(words.next() == null); + } + try testing.expectEqual(std.enums.values(Builtin).len, lines); + for ([_][]const u8{ "\nDel arg pane\n", "\nNewcol root\n", "\nSave arg pane\n", "\nVerbose arg root\n" }) |row| + try testing.expect(std.mem.indexOf(u8, listed.bytes, row) != null); +} + test "focus reads the pane with the keyboard, and a serial written to it moves the keyboard there" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 13dfb0cf..53472a60 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -176,6 +176,7 @@ pub const TopFile = enum(u4) { new, focus, ctl, + commands, pub fn fileName(f: TopFile) []const u8 { return if (f == .root) "/" else @tagName(f); @@ -185,7 +186,7 @@ pub const TopFile = enum(u4) { return switch (f) { .root, .pane => 0o755, .look, .exec, .log, .focus, .ctl => 0o666, - .README, .index, .status, .screen, .listeners, .new => 0o444, + .README, .index, .status, .screen, .listeners, .new, .commands => 0o444, }; } @@ -447,6 +448,7 @@ fn topSize(p: *Pardes, f: TopFile) u64 { .log => events.logLen(&p.fs.log), .listeners => screen.listenersLen(p), .ctl => ctl.rootLen(p), + .commands => ctl.commandsLen(), .focus => focus: { const pn = p.panes[p.active] orelse break :focus 0; var digits: [16]u8 = undefined; @@ -525,7 +527,7 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply { } if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, fs.os_root, true, "os"); // After the first ten, which older clients list by position. - inline for (.{ TopFile.focus, TopFile.ctl }) |t| { + inline for (.{ TopFile.focus, TopFile.ctl, TopFile.commands }) |t| { if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(t), t.dir(), t.fileName()); } sources.stage(p, out, "", &skip); @@ -719,6 +721,7 @@ fn read(p: *Pardes, req: Req, target: Target) Reply { .listeners => screen.readListeners(p, req), .focus => ctl.readFocus(p, req), .ctl => ctl.readRoot(p, req), + .commands => ctl.readCommands(p, req), // The serial the open handed this fid, so that two reads of one // fid answer the same pane: the read observes, the open acted. .new => serial: { @@ -261,6 +261,10 @@ def discovery(binary, embedded=False): raise AssertionError(f'{path} took {line!r}') except OSError as refused: assert why in str(refused), refused + commands = client.read('/commands').decode().splitlines() + assert 'Newcol root' in commands and 'Save arg pane' in commands, commands + assert all(row.split()[-1] in ('root', 'pane') for row in commands), commands + assert client.stat('/commands')['length'] == len('\n'.join(commands)) + 1 client.write(f'/pane/{first}/ctl', b'Msg said through ctl\n') assert 'said through ctl' in ''.join(cell[0] for cell in client.screen()['cells']) assert set(client.list('/pane')) == {'new', str(fixture), str(first), str(second)} @@ -333,7 +337,7 @@ def discovery(binary, embedded=False): assert client.read('/src/pardes.zig').startswith(b'const std') assert client.stat('/src/pardes.zig')['mode'] == 0o444 assert b'pub const Pardes' in client.read(f'/pane/{look(client, "/virtual/src/pardes.zig")}/body') - print('9P discovery: listing/stat/find are inert; new, remove, look, exec, name, sel, log, ctl lock, focus and the ctl split behave') + print('9P discovery: listing/stat/find are inert; new, remove, look, exec, name, sel, log, ctl lock, focus, the ctl split and commands behave') def run_file(binary): |
