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 | |
| 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')
| -rw-r--r-- | src/fs-help.txt | 8 | ||||
| -rw-r--r-- | src/ninep/cols.zig | 42 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 7 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 36 |
4 files changed, 67 insertions, 26 deletions
diff --git a/src/fs-help.txt b/src/fs-help.txt index 2492e933..3d84fca2 100644 --- a/src/fs-help.txt +++ b/src/fs-help.txt @@ -4,15 +4,15 @@ README this guide index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name (+New if unnamed), column serial status pid, version and pane count look write a line: a right click on it at the active pane; read: the serials it touched -exec write a line: a middle click, an editor command word or a shell line; read the same -log events: new del rename save newcol delcol run exit send ask dump restore restored restoredcol msg err +exec write a line: a middle click, an editor command word or a shell line; read the same (every exec file) +log events: new del rename save newcol delcol run exit send ask answer dump restore restored restoredcol msg err 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, one a line; or Newcol (an empty column), Dump, Exit (quits, asks once), Kill [w] (stops what pardes ran, w: by first word) commands every builtin: its word, arg if it takes one, and root or pane: which ctl takes it layout a line per column: serial index x width current|notcurrent empty|full pane-serials; active <serial> -tag the workspace tag; col/<serial>/ a column's tag, ctl (Delcol Joincol New Tty) and exec; rmdir: empty only +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 pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll errors (write-only: appended to its directory's +Errors pane) event look exec, pty/ on terminals; rmdir closes @@ -40,6 +40,6 @@ Pitfalls, one each: tag reads the path, then its own text; > replaces that text (default words too), >> appends. A terminal's body is a history snapshot frozen per open, wrapped rows joined into lines; pty/data is the live stream. exec: a non-builtin line runs as a command pane (ctl's Shell -c; exit N shown, logged), a shell's typed in. - Through a mount a malformed write is EINVAL, a well-formed one that fails EIO: log's err says why. + Through a mount a malformed write is EINVAL, a failed one EIO or an errno that fits (ENOENT): log's err says why. lock/unlock need a held ctl fd (exec 3>ctl); a held lock fails: retry. fs.md has the rest. Repl python on a terminal's ctl: a .py body's clicks go to it (Repl - unbinds, Repl says it); tags stay commands. 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); |
