diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 10:13:45 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 87bb23d39df92db0b383dd300a9ebd1ad9cb2b7a (patch) | |
| tree | a54e857ae9f999ef9eee96b07d6761a86ccc7b1b | |
| parent | ae7aba431c1a152dc999e485e8285ebd285811f8 (diff) | |
| download | pardes-87bb23d39df92db0b383dd300a9ebd1ad9cb2b7a.tar.gz pardes-87bb23d39df92db0b383dd300a9ebd1ad9cb2b7a.zip | |
Focus gives the keyboard only, reads empty while a header has it, and sits in the root listing's natural place
A write to /focus leaves a folded pane folded, as rio keeps current apart
from unhide; while a column or workspace tag has the keyboard no pane
does, so /focus reads empty and every pane's ctl says notcurrent (an
empty read also no longer answers stale staged bytes). focus, ctl and
commands now list after status, and the discovery test checks the root
as a set rather than by position.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | .agents/skills/pardes-9p/SKILL.md | 3 | ||||
| -rw-r--r-- | docs/fs.md | 6 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 27 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 18 | ||||
| -rw-r--r-- | test/fs.py | 5 | ||||
| -rw-r--r-- | test/fs_bench.zig | 6 |
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) @@ -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); @@ -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; } |
