summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.agents/skills/pardes-9p/SKILL.md3
-rw-r--r--docs/fs.md6
-rw-r--r--src/ninep/ctl.zig27
-rw-r--r--src/ninep/tree.zig18
-rw-r--r--test/fs.py5
-rw-r--r--test/fs_bench.zig6
6 files changed, 44 insertions, 21 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md
index a17a5dac..ef37db6f 100644
--- a/.agents/skills/pardes-9p/SKILL.md
+++ b/.agents/skills/pardes-9p/SKILL.md
@@ -52,7 +52,8 @@ $m/log recent events, then EOF: new|del|rename|save <serial> <name>, msg
(exec 3<>$m/log; echo follow >&3; cat <&3 waits for new ones; tail -f does not)
$m/screen the rendered screen as JSON, frozen per open
$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/focus the serial of the pane with the keyboard (empty while a column/workspace tag has it);
+ echo a serial into it to move the keyboard (a folded pane stays folded)
$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)
diff --git a/docs/fs.md b/docs/fs.md
index c08500f1..5c06d206 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -135,7 +135,11 @@ 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
-tree since there is one keyboard. A serial no pane has fails with `no such
+tree since there is one keyboard. While a column's or the workspace's tag has
+the keyboard no pane does: `/focus` reads empty and every pane's `ctl` says
+`notcurrent`. A write gives the keyboard and nothing else: a folded pane
+stays folded (unfold it with `Collapse` on its ctl), as rio keeps `current`
+apart from `unhide`. A serial no pane has fails with `no such
window`; anything but a number, with `ill-formed control message`.
A pane is made by **opening** `/pane/new`, and closed by Tremove on
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index e47dec16..4afc82dc 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -12,6 +12,7 @@ const panes = @import("../panes.zig");
const config = @import("../config.zig");
const fs = @import("../fs.zig");
const tagline = @import("../tagline.zig");
+const layout = @import("../layout.zig");
const builtins = @import("../builtins.zig");
const exec_line = @import("../exec.zig");
const tree = @import("tree.zig");
@@ -162,7 +163,9 @@ pub fn paneText(p: *Pardes, pane: *Pane, buf: []u8) []const u8 {
}) catch return w.buffered();
const font = p.settings.font.effective_name.get();
writeQuoted(&w, if (font.len == 0) "default" else font) catch return w.buffered();
- w.print(" {d:>11} {s} ", .{ config.tab_width, if (p.panes[p.active] == pane) "current" else "notcurrent" }) catch {};
+ // While a column's or the workspace's tag has the keyboard, no pane does.
+ const current = p.panes[p.active] == pane and !p.header_focus;
+ w.print(" {d:>11} {s} ", .{ config.tab_width, if (current) "current" else "notcurrent" }) catch {};
return w.buffered();
}
@@ -333,10 +336,15 @@ pub fn readCommands(p: *Pardes, req: Req) Reply {
// ---- /focus ----
/// The pane with the keyboard, which a write of a serial moves, the way
-/// writing `current` to rio's wctl does (rio(4)).
+/// writing `current` to rio's wctl does (rio(4)). While a column's or the
+/// workspace's tag has it, no pane does, and this reads empty. A write
+/// gives the keyboard and nothing else: a folded pane stays folded, as
+/// rio keeps `current` apart from `unhide`.
pub fn readFocus(p: *Pardes, req: Req) Reply {
+ const out = p.fs.stage(p.gpa);
+ if (p.header_focus) return tree.stagedReply(p, req);
const pane = p.panes[p.active] orelse return tree.stagedReply(p, req);
- p.fs.stage(p.gpa).print(p.gpa, "{d}\n", .{pane.serial}) catch return Reply.fail(req.tag, E.NOMEM);
+ out.print(p.gpa, "{d}\n", .{pane.serial}) catch return Reply.fail(req.tag, E.NOMEM);
return tree.stagedReply(p, req);
}
@@ -506,8 +514,11 @@ test "focus reads the pane with the keyboard, and a serial written to it moves t
try testing.expectEqual(@as(u64, rd(p, focus, 0, 64).bytes.len), call(p, .{ .tag = 1, .op = .getattr, .node = focus }).reply.attr.size);
try testing.expect(std.mem.endsWith(u8, rd(p, Node.of(first, .ctl), 0, 4096).bytes, " notcurrent "));
- // A header had the keyboard: the write takes it back to the pane.
+ // A header has the keyboard: no pane does, and a write takes it back.
tagline.enterHeader(p, null);
+ try testing.expectEqualStrings("", rd(p, focus, 0, 64).bytes);
+ try testing.expectEqual(@as(u64, 0), call(p, .{ .tag = 1, .op = .getattr, .node = focus }).reply.attr.size);
+ try testing.expect(std.mem.endsWith(u8, rd(p, Node.of(second, .ctl), 0, 4096).bytes, " notcurrent "));
try testing.expectEqual(Status.ok, wr(p, focus, try std.fmt.bufPrint(&line, "{d}\n", .{first})).reply.status);
try testing.expect(!p.header_focus);
try testing.expectEqual(p.paneBySerial(first).?, p.active);
@@ -518,6 +529,14 @@ test "focus reads the pane with the keyboard, and a serial written to it moves t
try testing.expectEqual(E.NOENT, wr(p, focus, "99999").errno());
for ([_][]const u8{ "", "two", "-1", "1 2" }) |bad| try testing.expectEqual(E.INVAL, wr(p, focus, bad).errno());
try testing.expectEqual(p.paneBySerial(first).?, p.active);
+
+ // Focus gives the keyboard only: a folded pane stays folded.
+ const folded = p.paneBySerial(second).?;
+ layout.toggleCollapse(p, folded);
+ try testing.expect(p.panes[folded].?.collapsed);
+ try testing.expectEqual(Status.ok, wr(p, focus, try std.fmt.bufPrint(&line, "{d}", .{second})).reply.status);
+ try testing.expectEqual(folded, p.active);
+ try testing.expect(p.panes[folded].?.collapsed);
}
test "the pane ctl takes acme's verbs and the pane's builtins, and refuses the rest in Plan 9's words" {
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index 27ad02c8..e9c6b8ea 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -438,6 +438,7 @@ fn topSize(p: *Pardes, f: TopFile) u64 {
.ctl => ctl.rootLen(p),
.commands => ctl.commandsLen(),
.focus => focus: {
+ if (p.header_focus) break :focus 0;
const pn = p.panes[p.active] orelse break :focus 0;
var digits: [16]u8 = undefined;
break :focus (std.fmt.bufPrint(&digits, "{d}\n", .{pn.serial}) catch unreachable).len;
@@ -516,14 +517,12 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply {
switch (target) {
.top => |f| switch (f) {
.root => {
- inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.pane }) |t| {
+ // The guide, then what describes the session, then the
+ // files that act, then the directories.
+ inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.focus, TopFile.ctl, TopFile.commands, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.pane }) |t| {
if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(t), t.dir(), t.fileName());
}
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, TopFile.commands }) |t| {
- if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(t), t.dir(), t.fileName());
- }
sources.stage(p, out, "", &skip);
},
.pane => {
@@ -901,13 +900,12 @@ test "readdir lists the root and a pane directory without creating anything" {
@memcpy(top_bytes[0..top_reply.bytes.len], top_reply.bytes);
const top = th.dirents(top_bytes[0..top_reply.bytes.len], &buf);
try testing.expect(top.len >= 10);
- for ([_][]const u8{ "README", "index", "status", "look", "exec", "log", "screen", "listeners", "pane", "os" }, 0..) |name, i| {
+ for ([_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "look", "exec", "log", "screen", "listeners", "pane", "os" }, 0..) |name, i| {
try testing.expectEqualStrings(name, top[i].name);
- try testing.expectEqual(i >= 8, top[i].dir);
+ try testing.expectEqual(i >= 11, top[i].dir);
}
- try testing.expectEqual(@intFromEnum(TopFile.pane), top[8].node);
- try testing.expectEqual(fs.os_root, top[9].node);
- try testing.expectEqualStrings("focus", top[10].name);
+ try testing.expectEqual(@intFromEnum(TopFile.pane), top[11].node);
+ try testing.expectEqual(fs.os_root, top[12].node);
var idbuf: [16]u8 = undefined;
try testing.expect(th.nameAt(top, try std.fmt.bufPrint(&idbuf, "{d}", .{serial})) == null);
try testing.expect(th.nameAt(top, "self") == null);
diff --git a/test/fs.py b/test/fs.py
index bafe665c..c7552a2b 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -144,8 +144,9 @@ def discovery(binary, embedded=False):
root = Path(directory)
with session(binary, root, 'discovery') as (client, address):
top = client.list('/')
- assert top[:10] == ['README', 'index', 'status', 'look', 'exec', 'log', 'screen',
- 'listeners', 'pane', 'os'], top
+ # A set: which files the root holds, not the order a listing gives.
+ assert set(top) - {'src', 'shaders'} == {'README', 'index', 'status', 'focus', 'ctl', 'commands',
+ 'look', 'exec', 'log', 'screen', 'listeners', 'pane', 'os'}, top
assert 'self' not in top and 'cons' not in top and 'new' not in top, top
assert ('src' in top) == embedded, (top, embedded)
before = client.read('/index')
diff --git a/test/fs_bench.zig b/test/fs_bench.zig
index 75b2eb11..746fe737 100644
--- a/test/fs_bench.zig
+++ b/test/fs_bench.zig
@@ -133,8 +133,8 @@ fn checkReply(s: *Session, req: ctlfs.Req, reply: ctlfs.Reply) !void {
const bytes = s.core.fs.out.items;
if (reply.payload != .staged or reply.payload.staged != bytes.len or bytes.len > req.size)
return error.IncorrectReply;
- const names = [_][]const u8{ "README", "index", "status", "look", "exec", "log", "screen", "listeners", "pane", "os" };
- const nodes = [_]u64{ @intFromEnum(ctlfs.TopFile.README), @intFromEnum(ctlfs.TopFile.index), @intFromEnum(ctlfs.TopFile.status), @intFromEnum(ctlfs.TopFile.look), @intFromEnum(ctlfs.TopFile.exec), @intFromEnum(ctlfs.TopFile.log), @intFromEnum(ctlfs.TopFile.screen), @intFromEnum(ctlfs.TopFile.listeners), @intFromEnum(ctlfs.TopFile.pane), filesystem.os_root };
+ const names = [_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "look", "exec", "log", "screen", "listeners", "pane", "os" };
+ const nodes = [_]u64{ @intFromEnum(ctlfs.TopFile.README), @intFromEnum(ctlfs.TopFile.index), @intFromEnum(ctlfs.TopFile.status), @intFromEnum(ctlfs.TopFile.focus), @intFromEnum(ctlfs.TopFile.ctl), @intFromEnum(ctlfs.TopFile.commands), @intFromEnum(ctlfs.TopFile.look), @intFromEnum(ctlfs.TopFile.exec), @intFromEnum(ctlfs.TopFile.log), @intFromEnum(ctlfs.TopFile.screen), @intFromEnum(ctlfs.TopFile.listeners), @intFromEnum(ctlfs.TopFile.pane), filesystem.os_root };
var off: usize = 0;
var entry: usize = 0;
while (off < bytes.len) : (entry += 1) {
@@ -144,7 +144,7 @@ fn checkReply(s: *Session, req: ctlfs.Req, reply: ctlfs.Reply) !void {
const size: usize = bytes[off + 9];
off += 10;
if (dir > 1 or size == 0 or size > bytes.len - off) return error.IncorrectReply;
- if (entry < names.len and (node != nodes[entry] or dir != @intFromBool(entry >= 8) or
+ if (entry < names.len and (node != nodes[entry] or dir != @intFromBool(entry >= 11) or
!std.mem.eql(u8, names[entry], bytes[off..][0..size]))) return error.IncorrectReply;
off += size;
}