diff options
| -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; } |
