diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 13:00:06 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 047a2b50a7ac0162d7ef38f912d41c15a47479c7 (patch) | |
| tree | a2fb902819402ac11093a23cf5dc72f2bae68f0f | |
| parent | 6fc9f416938b59b665b1fddc052402770a21cc3f (diff) | |
| download | pardes-047a2b50a7ac0162d7ef38f912d41c15a47479c7.tar.gz pardes-047a2b50a7ac0162d7ef38f912d41c15a47479c7.zip | |
A line written to a pane's own look or exec goes to the event reader holding the pane
A script writing to pane/N/exec bypassed the client holding that pane's event
file, which a middle click on it would not: acme sends a window's clicks to its
event reader (look.c:53, exec.c:173). Such a line is now an F record at 0 0
carrying its text; the root's look and exec still act at once. acme takes back
only origin, action and range, which a record with no place in the text cannot
use, so the whole record as read is taken too, and for an empty range acts on
its text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | .agents/skills/pardes-9p/SKILL.md | 11 | ||||
| -rw-r--r-- | docs/fs.md | 9 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 40 | ||||
| -rw-r--r-- | src/ninep/events.zig | 21 |
4 files changed, 68 insertions, 13 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md index 5e3bc39c..c9efec0b 100644 --- a/.agents/skills/pardes-9p/SKILL.md +++ b/.agents/skills/pardes-9p/SKILL.md @@ -210,11 +210,12 @@ For live terminal output or plugin events, use `open` / `read_fid` / `close`, not the read-until-EOF helper. `pty/data` captures output while held open; it is not a history replay. Both files are shared, consuming queues, not per-client broadcasts, so a slow reader loses older data. Holding `event` open -intercepts that pane's Look and Exec clicks, so it is not a passive logger: for -Look/Exec records whose offsets identify the intended text, forward the short -record `<origin><action><q0> <q1>\n`, not the whole report with flags and text. -Expansion and chord reports need explicit handling, and terminal-body events -can carry text only in the report, which a short writeback cannot reproduce. +intercepts that pane's Look and Exec clicks -- and lines written to that pane's +own `look`/`exec`, as `F` records at `0 0` with the text -- so it is not a +passive logger. To have a record done, write it back: the short form +`<origin><action><q0> <q1>\n` acts on that range's text, and the whole record +as read acts on its text when the range is empty (the only way for a record +at `0 0`). Chord reports need explicit handling. Read the event implementation before building an interceptor. Close handles in `finally`, and disconnect after a socket timeout. The service shares four connection slots and 32 screen/terminal-history snapshot handles. @@ -245,8 +245,13 @@ writing it renames the buffer; a relative name resolves against the pane's directory. `body` appends on write and replaces on truncating open. `sel` reads the selected text and writing it replaces the selection. `errors` appends to the directory's `+Errors` pane. Holding `event` open redirects the -pane's Look and Exec clicks to that client; writing a record back performs the -action. `ctl` reads acme's window status line — serial, tag length, body +pane's Look and Exec clicks to that client, and so does a line written to the +pane's own `look` or `exec`, a click with no place in the text: an `F` record +at `0 0` carrying the line (the root's `look` and `exec` still act at once); +writing a record back performs the action. acme takes back only `<origin> +<action><q0> <q1>`, the text of that range; pardes takes the record whole as +it was read too, and for an empty range acts on its text, which is how such +a line is done. `ctl` reads acme's window status line — serial, tag length, body length, a reserved zero, the dirty flag, the width in cells, the font and the tab width — followed by rio's `current` or `notcurrent` (rio(4), `wctl`): whether the pane has the keyboard. It takes the pane's builtins (below), diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index c1f78bab..70e06d4b 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -51,8 +51,20 @@ 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). -fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { +fn run(p: *Pardes, id: usize, exec: bool, text: []const u8, own: 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 its own look or exec is a click on it; the + // reader writes the record back to have it done. + if (own 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; + if (!exec and !named and std.mem.indexOfAny(u8, text, "/.:") != null) flag |= pardes.ctlfs.events.flag_filename; + _ = pardes.ctlfs.events.noteAction(p, id, if (exec) .body_exec else .body_look, 0, 0, flag, text); + return noteResult(p, context); + } const before = Serials.snapshot(p); if (exec) _ = pardes.exec.execute(p, id, text) else look.lookAt(p, id, text); const after = Serials.snapshot(p); @@ -82,7 +94,7 @@ pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool) Reply { if (!apply) continue; 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); - run(p, id, exec, text); + run(p, id, exec, text, serial != null); } } return .{ .tag = req.tag, .written = @intCast(req.data.len) }; @@ -858,3 +870,27 @@ test "exec Save writes the named file through the host" { try testing.expect(!pane_files.dirtyOf(pane)); try testing.expectEqualStrings(pane.file.?.content, p.fallback.get("/tmp/renamed.txt").?); } + +test "a pane's own look and exec go to the event reader holding it, which writes them back to act" { + const gpa = testing.allocator; + const p = try withFile(gpa, "Msg routed\n"); + defer p.deinit(); + const serial = serialOf(p); + const event = Node.of(serial, .event); + const h = call(p, .{ .tag = 1, .op = .open, .node = event }).reply.handle; + try testing.expectEqual(tree.Status.ok, wr(p, Node.of(serial, .exec), "Msg routed\n").reply.status); + // Not done: the reader has it, as it would a middle click. + const pane = p.panes[0].?; + try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "routed") == null); + const record = call(p, .{ .tag = 2, .op = .read, .node = event, .handle = h, .size = 256 }).bytes; + try testing.expectEqualStrings("FX0 0 2 10 Msg routed\n", record); + // Written back, it is done. + try testing.expectEqual(tree.Status.ok, wr(p, event, record).reply.status); + try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "routed") != null); + try testing.expectEqual(tree.Status.ok, wr(p, Node.of(serial, .look), "one.txt:3\n").reply.status); + try testing.expectEqualStrings("FL0 0 6 9 one.txt:3\n", call(p, .{ .tag = 3, .op = .read, .node = event, .handle = h, .size = 256 }).bytes); + _ = call(p, .{ .tag = 4, .op = .release, .node = event, .handle = h }); + // With no reader, the root's and the pane's act at once. + try testing.expectEqual(tree.Status.ok, wr(p, Node.of(serial, .exec), "Msg direct\n").reply.status); + try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "direct") != null); +} diff --git a/src/ninep/events.zig b/src/ninep/events.zig index 1cab5605..3f36c65f 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -468,7 +468,7 @@ pub fn notePtyOutput(p: *Pardes, id: usize, bytes: []const u8) void { p.fs.news = true; } -const EventRecord = struct { action: Action, q0: u32, q1: u32 }; +const EventRecord = struct { action: Action, q0: u32, q1: u32, text: ?[]const u8 = null }; const EventReader = struct { data: []const u8, @@ -484,9 +484,20 @@ const EventReader = struct { const q0 = scanNumber(er.data, &i) orelse return null; const q1 = scanNumber(er.data, &i) orelse return null; while (i < er.data.len and er.data[i] == ' ') i += 1; + // acme takes only `origin action q0 q1`; the record whole, as read, + // is taken too, so a click with no place in the text (one written + // to a pane's own look or exec) can be written back and done. + var text: ?[]const u8 = null; + if (i < er.data.len and er.data[i] != '\n') { + _ = scanNumber(er.data, &i) orelse return null; + const n = scanNumber(er.data, &i) orelse return null; + if (i >= er.data.len or er.data[i] != ' ' or er.data.len - (i + 1) < n) return null; + text = er.data[i + 1 ..][0..n]; + i += 1 + n; + } if (i >= er.data.len or er.data[i] != '\n') return null; er.i = i + 1; - return .{ .action = action, .q0 = q0, .q1 = q1 }; + return .{ .action = action, .q0 = q0, .q1 = q1, .text = text }; } }; @@ -525,7 +536,8 @@ pub fn writeEvent(p: *Pardes, req: Req, id: usize) Reply { const whole = if (r.action.onTag()) pane_files.tagOf(p, pane) else pane_files.bodyOf(pane); const lo = @min(@as(usize, r.q0), whole.len); const hi = @max(lo, @min(@as(usize, r.q1), whole.len)); - const text = p.scratch.allocator().dupe(u8, whole[lo..hi]) catch continue; + const said = if (r.q0 == r.q1) r.text orelse whole[lo..hi] else whole[lo..hi]; + const text = p.scratch.allocator().dupe(u8, said) catch continue; switch (r.action) { .body_exec, .tag_exec => _ = exec.execute(p, live, text), .body_look, .tag_look => look.lookAt(p, live, text), @@ -672,7 +684,8 @@ test "a pane deleted while its event file is open leaves no suppression behind" const b = call(p, .{ .tag = 19, .op = .open, .node = event, .omode = 1 }); try testing.expectEqual(@as(u16, 2), p.fs.listeners); - _ = wr(p, Node.of(serial, .exec), "Del\n"); + // Through ctl: a line written to its exec would go to the reader. + _ = wr(p, Node.of(serial, .ctl), "Del\n"); try testing.expect(p.paneBySerial(serial) == null); try testing.expectEqual(@as(u16, 0), p.fs.listeners); // The reader's next read hears what acme says of a window gone under it. |
