summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/fs.md4
-rw-r--r--src/fs-help.txt2
-rw-r--r--src/ninep/ctl.zig48
-rw-r--r--test/fs.py10
4 files changed, 54 insertions, 10 deletions
diff --git a/docs/fs.md b/docs/fs.md
index f61bbbe6..04cef2f9 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -109,7 +109,9 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC.
(`--detach`, 80x24 until then; refused while a frontend owns the size),
from 20x6 to 4096x4096, and refused when the panes it has would not each
keep their tag and 2 rows; a terminal's pty follows its pane on every resize
-/commands every builtin: word, `arg` if it takes one, `root` or `pane` (the ctl that takes it), a setting's values
+/commands every builtin: word, `arg` if it takes one, `root`, `pane` or `both` (the ctl that takes it:
+ Edit is both, at the active pane from the root; a pane's word such as Undo or Msg
+ is refused at the root), a setting's values, then ` -- ` and what it does
/layout one line per column, left to right: serial index x width current|notcurrent
(the column with the keyboard now) empty|full pane-serials...; then active
<serial>: acme's activecol, which the keyboard leaving for another column's
diff --git a/src/fs-help.txt b/src/fs-help.txt
index 9e6a9240..dc974683 100644
--- a/src/fs-help.txt
+++ b/src/fs-help.txt
@@ -10,7 +10,7 @@ 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 (empty on a column tag); write a serial to give it the keyboard
ctl settings a line; or Newcol, Dump, Restore [f], Exit (asks once), Kill [w], Placement acme|pardes, size C R (--detach)
-commands every builtin: its word, arg if it takes one, and root or pane: which ctl takes it
+commands every builtin: word, arg if it takes one, root|pane|both (the ctl that takes it), -- what it does
layout a line per column: serial index x width current|notcurrent empty|full pane-serials; active <serial>
tag the workspace tag, tagexec a word as a click in it; col/<serial>/ a column's tag, ctl (Delcol Joincol New Tty), exec; rmdir: empty only
pane/new open it to make a pane (named +New) in the active column, filling it if empty; the read answers its serial
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index f8623903..1f0195d0 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -540,12 +540,47 @@ fn sizeOf(line: []const u8) ?[2]u16 {
/// 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.
+/// A builtin's doc comment, its first sentence, from the source itself: a
+/// line of /commands says what each word does without a second copy of it.
+fn summaryOf(word: []const u8, buf: []u8) []const u8 {
+ const source = @embedFile("../builtins.zig");
+ var needle_buf: [64]u8 = undefined;
+ const needle = std.fmt.bufPrint(&needle_buf, "\npub const {s} = struct", .{word}) catch return "";
+ const at = std.mem.indexOf(u8, source, needle) orelse return "";
+ // Back over the `///` lines just above it.
+ var start = at + 1;
+ while (start > 0) {
+ const prev_end = start - 1;
+ const prev_start = if (std.mem.lastIndexOfScalar(u8, source[0..prev_end], '\n')) |nl| nl + 1 else 0;
+ if (!std.mem.startsWith(u8, std.mem.trimStart(u8, source[prev_start..prev_end], " "), "///")) break;
+ start = prev_start;
+ }
+ var w = std.Io.Writer.fixed(buf);
+ var lines = std.mem.splitScalar(u8, source[start .. at + 1], '\n');
+ while (lines.next()) |line| {
+ const text = std.mem.trim(u8, std.mem.trimStart(u8, std.mem.trimStart(u8, line, " "), "/"), " ");
+ if (text.len == 0) continue;
+ if (w.buffered().len > 0) w.writeByte(' ') catch break;
+ w.writeAll(text) catch break;
+ }
+ const all = w.buffered();
+ // The first sentence, whole: to a `.` that ends one, or the whole cut
+ // between words.
+ var i: usize = 0;
+ while (std.mem.indexOfScalarPos(u8, all, i, '.')) |dot| : (i = dot + 1) {
+ if (dot + 1 == all.len or all[dot + 1] == ' ') return all[0 .. dot + 1];
+ }
+ return all;
+}
+
fn writeCommands(w: *std.Io.Writer) !void {
for (std.enums.values(Builtin)) |b| {
+ // Edit is a pane's word the root's ctl takes too, at the active pane.
+ const scope: []const u8 = if (b == .Edit) "both" else if (builtins.registry.scope(b) == .session) "root" else "pane";
try w.print("{s}{s} {s}", .{
@tagName(b),
if (builtins.registry.takesArg(b)) " arg" else "",
- if (builtins.registry.scope(b) == .session) "root" else "pane",
+ scope,
});
// A setting that chooses among words: the words, comma-joined. Bare,
// it steps to the next of them (a two-valued one flips).
@@ -555,6 +590,10 @@ fn writeCommands(w: *std.Io.Writer) !void {
var first = true;
while (parts.next()) |v| : (first = false) try w.print("{s}{s}", .{ if (first) "" else ",", v });
};
+ // Then ` -- ` and what it does, when its source says.
+ var summary_buf: [2048]u8 = undefined;
+ const summary = summaryOf(@tagName(b), &summary_buf);
+ if (summary.len > 0) try w.print(" -- {s}", .{@import("../Messages.zig").clip(summary, 160)});
try w.writeByte('\n');
}
}
@@ -786,12 +825,13 @@ test "commands lists every builtin with its argument and the ctl that takes it"
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.expectEqualStrings(if (b == .Edit) "both" else if (builtins.registry.scope(b) == .session) "root" else "pane", words.next().?);
if (config.Runtime.find(@tagName(b))) |s| if (config.Runtime.choices(s.action) != null) try testing.expect(words.next() != null);
- try testing.expect(words.next() == null);
+ // Then, where the source has one, ` -- ` and what it does.
+ if (words.next()) |dashes| try testing.expectEqualStrings("--", dashes);
}
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 on,off\n", "\nPlacement arg root acme,pardes\n", "\nUndo pane\n", "\nRedo pane\n" }) |row|
+ for ([_][]const u8{ "\nDel arg pane -- ", "\nNewcol root -- ", "\nSave arg pane", "\nVerbose arg root on,off", "\nPlacement arg root acme,pardes", "\nUndo pane -- ", "\nEdit arg both -- acme's Edit" }) |row|
try testing.expect(std.mem.indexOf(u8, listed.bytes, row) != null);
}
diff --git a/test/fs.py b/test/fs.py
index 09d366b5..9fb560e0 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -270,10 +270,12 @@ def discovery(binary, embedded=False):
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
- # word, `arg` if it takes one, its ctl, then a setting's values
- assert all(row.split()[2 if row.split()[1] == 'arg' else 1] in ('root', 'pane') for row in commands), commands
- assert 'Placement arg root acme,pardes' in commands, commands
+ def row(start):
+ return any(r == start or r.startswith(start + ' ') for r in commands)
+ assert row('Newcol root') and row('Save arg pane') and row('Edit arg both'), commands
+ # word, `arg` if it takes one, its ctl, a setting's values, then `-- ` what it does
+ assert all(r.split()[2 if r.split()[1] == 'arg' else 1] in ('root', 'pane', 'both') for r in commands), commands
+ assert row('Placement arg root acme,pardes'), 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'])