diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 01:48:39 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 5b11fc35b961eb03c8816bf957bbf8e27a22ae3e (patch) | |
| tree | 3a0022699f4c92ec86b2249cf3455139809d56a4 /src/ninep | |
| parent | 5b5896965758ff8f2077c8e8e2f0250f580967c3 (diff) | |
| download | pardes-5b11fc35b961eb03c8816bf957bbf8e27a22ae3e.tar.gz pardes-5b11fc35b961eb03c8816bf957bbf8e27a22ae3e.zip | |
The workspace tag has /tagexec, every exec file reads back what it touched, and a tag's words log as the session's
/tagexec runs a word as a click in the workspace tag, as col/<serial>/exec
does in a column's. A column's exec was write-only; it and /tagexec now
read back the serials the last write touched, as /exec does. A click in a
tag no longer goes to the focused pane's event reader, and what a word
run from a column's ctl or exec or /tagexec says is logged msg -, not
against the focused pane. The README, fs.md and the skill say so, and
document the answer record.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep')
| -rw-r--r-- | src/ninep/cols.zig | 42 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 7 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 36 |
3 files changed, 63 insertions, 22 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index 49df715a..8a3474b3 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -123,8 +123,11 @@ pub fn released(p: *Pardes, serial: ?u32) void { } /// A column's ctl: the words that act on a column, as its tag's clicks do -- -/// Delcol, Joincol, New, Tty -- one a line, each run as from that tag. +/// Delcol, Joincol, New, Tty -- one a line, each run as from that tag. What +/// they say is the session's in the log (`msg -`), not the focused pane's. pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply { + p.fs.session_write = true; + defer p.fs.session_write = false; var lines = std.mem.splitScalar(u8, req.data, '\n'); while (lines.next()) |raw| { const line = std.mem.trim(u8, raw, " \t\r"); @@ -142,18 +145,24 @@ pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply { return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } -/// A column's exec: each line run as a middle click on it in that column's -/// tag, acting on that column. -pub fn writeExec(p: *Pardes, req: Req, serial: u32) Reply { +/// A column's exec (`serial`), or the workspace tag's (/tagexec, null): +/// each line run as a middle click on it in that tag, a column's acting on +/// that column. A read answers the panes the last write touched, as /exec's +/// does; what the words say is the session's in the log (`msg -`). +pub fn writeExec(p: *Pardes, req: Req, serial: ?u32) Reply { + p.fs.results_len = 0; + p.fs.session_write = true; + defer p.fs.session_write = false; var lines = std.mem.splitScalar(u8, req.data, '\n'); while (lines.next()) |raw| { const line = std.mem.trim(u8, raw, " \t\r"); if (line.len == 0) continue; for (line) |c| if (c < ' ' and c != '\t') return Reply.fail(req.tag, E.INVAL); - const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT); + const col = if (serial) |s| layout.columnBySerial(p, s) orelse return Reply.fail(req.tag, E.NOENT) else null; + if (p.panes[p.active] == null) return Reply.fail(req.tag, E.NOENT); p.exec_column = col; defer p.exec_column = null; - pardes.ctlfs.ctl.run(p, p.active, true, line); + pardes.ctlfs.ctl.run(p, p.active, true, line, true); } return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } @@ -284,3 +293,24 @@ test "a column's ctl and exec act on it as its tag would, and rmdir closes it on // The root's ctl points a column word at the column's own. try testing.expect(std.mem.endsWith(u8, th.wr(p, @intFromEnum(tree.TopFile.ctl), "Delcol\n").reply.ename, "write it to col/<serial>/ctl")); } + +test "tagexec runs a word as a click in the workspace tag; exec files read back what they touched; their messages are the session's" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const tagexec = @intFromEnum(tree.TopFile.tagexec); + try testing.expectEqual(tree.Status.ok, th.look_up(p, tree.root, "tagexec").reply.status); + try testing.expectEqual(tree.Status.ok, th.wr(p, tagexec, "Newcol\n").reply.status); + try testing.expectEqual(@as(usize, 2), p.ncol); + try testing.expectEqual(tree.Status.ok, th.wr(p, tagexec, "Msg from the top\n").reply.status); + try testing.expect(th.logHas(p, "msg - from the top\n")); + // A column's exec makes a pane there, reads it back, and its Msg is + // the session's too. + const right = layout.columnSerial(p, 1); + try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(right, .exec), "New\n").reply.status); + const made = p.panes[p.col_panes[1][0]].?.serial; + var want: [16]u8 = undefined; + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), th.rd(p, Node.ofCol(right, .exec), 0, 64).bytes); + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), th.rd(p, tagexec, 0, 64).bytes); + try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(right, .exec), "Msg from a column\n").reply.status); + try testing.expect(th.logHas(p, "msg - from a column\n")); +} diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 65878feb..c3c70667 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -51,14 +51,15 @@ fn noteResult(p: *Pardes, serial: u32) void { /// Runs one look or exec from the pane `id`, then records the panes it /// created; failing that, a look records the pane it focused and an exec /// the pane it acted on (even one it closed). -pub fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { +pub fn run(p: *Pardes, id: usize, exec: bool, text: []const u8, in_tag: bool) void { const context = p.panes[id].?.serial; // A pane an event reader holds hears its clicks instead of acting on // them (editors/acme/look.c:53 and exec.c:173 send them to winevent), // and a line written to a look or exec is a click: on that pane, or for // the root's, on the pane with the keyboard. The reader writes the // record back to have it done. - if (p.panes[id].?.fs.readers > 0) { + // A click in a column's or the workspace's tag is not the pane's. + if (!in_tag and p.panes[id].?.fs.readers > 0) { const named = std.meta.stringToEnum(Builtin, exec_line.commandText(text)) != null; var flag: u32 = pardes.ctlfs.events.flag_expansion; if (named) flag |= pardes.ctlfs.events.flag_builtin; @@ -139,7 +140,7 @@ pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool) Reply { const id = if (serial) |s| p.paneBySerial(s) orelse break else p.active; if (p.panes[id] == null) return Reply.fail(req.tag, E.NOENT); p.fs.no_pane_slot = false; - run(p, id, exec, text); + run(p, id, exec, text, false); // A pane it would have opened had no slot: the write fails, and // a look reads back nothing. if (p.fs.no_pane_slot) { diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index baf8c772..085d03a3 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -191,6 +191,7 @@ pub const TopFile = enum(u5) { layout, tag, col, + tagexec, pub fn fileName(f: TopFile) []const u8 { return if (f == .root) "/" else @tagName(f); @@ -199,7 +200,7 @@ pub const TopFile = enum(u5) { pub fn mode(f: TopFile) u16 { return switch (f) { .root, .pane, .col => 0o755, - .look, .exec, .log, .focus, .ctl, .tag => 0o666, + .look, .exec, .log, .focus, .ctl, .tag, .tagexec => 0o666, .README, .index, .status, .screen, .listeners, .new, .commands, .layout => 0o444, }; } @@ -468,10 +469,14 @@ fn attrOf(p: *Pardes, target: Target) ?Reply.Attr { .dir = c.file == .dir, .mode = switch (c.file) { .dir => 0o755, - .ctl, .exec => 0o222, - .tag => 0o666, + .ctl => 0o222, + .tag, .exec => 0o666, + }, + .size = switch (c.file) { + .tag => cols.headerText(p, c.serial).?.len + 1, + .exec => ctl.resultsLen(p), + else => 0, }, - .size = if (c.file == .tag) cols.headerText(p, c.serial).?.len + 1 else 0, .mtime = p.fs.started, .append = c.file == .tag, }; @@ -513,7 +518,7 @@ fn topSize(p: *Pardes, f: TopFile) u64 { .index => pane.indexLen(p), .README => fs.help.len, .status => ctl.statusLen(p), - .look, .exec => ctl.resultsLen(p), + .look, .exec, .tagexec => ctl.resultsLen(p), .log => events.logLen(&p.fs.log), .listeners => listeners: { var buf: [screen.listeners_cap]u8 = undefined; @@ -618,7 +623,7 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply { .root => { // 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.layout, TopFile.tag, TopFile.pane, TopFile.col }) |t| { + 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.layout, TopFile.tag, TopFile.tagexec, TopFile.pane, TopFile.col }) |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"); @@ -861,7 +866,7 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { break :index stagedReply(p, req); }, .status => ctl.readStatus(p, req), - .look, .exec => ctl.readResults(p, req), + .look, .exec, .tagexec => ctl.readResults(p, req), .log => events.readLog(p, req), .screen => screen.readSnapshot(p, req, null), .listeners => screen.readListeners(p, req), @@ -899,7 +904,11 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT); return pane.read(p, req, id, pn, t.file); }, - .col => |c| return if (c.file == .tag) cols.read(p, req, c.serial) else Reply.fail(req.tag, E.PERM), + .col => |c| return switch (c.file) { + .tag => cols.read(p, req, c.serial), + .exec => ctl.readResults(p, req), + else => Reply.fail(req.tag, E.PERM), + }, } } @@ -911,6 +920,7 @@ fn write(p: *Pardes, req: Req, target: Target) Reply { .focus => ctl.writeFocus(p, req), .ctl => ctl.writeRoot(p, req), .tag => cols.write(p, req, null), + .tagexec => cols.writeExec(p, req, null), else => Reply.fail(req.tag, E.PERM), }, .pane => |t| { @@ -1069,13 +1079,13 @@ 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", "focus", "ctl", "commands", "look", "exec", "log", "screen", "listeners", "layout", "tag", "pane", "col", "os" }, 0..) |name, i| { + for ([_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "look", "exec", "log", "screen", "listeners", "layout", "tag", "tagexec", "pane", "col", "os" }, 0..) |name, i| { try testing.expectEqualStrings(name, top[i].name); - try testing.expectEqual(i >= 13, top[i].dir); + try testing.expectEqual(i >= 14, top[i].dir); } - try testing.expectEqual(@intFromEnum(TopFile.pane), top[13].node); - try testing.expectEqual(@intFromEnum(TopFile.col), top[14].node); - try testing.expectEqual(fs.os_root, top[15].node); + try testing.expectEqual(@intFromEnum(TopFile.pane), top[14].node); + try testing.expectEqual(@intFromEnum(TopFile.col), top[15].node); + try testing.expectEqual(fs.os_root, top[16].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); |
