summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 13:00:06 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit047a2b50a7ac0162d7ef38f912d41c15a47479c7 (patch)
treea2fb902819402ac11093a23cf5dc72f2bae68f0f
parent6fc9f416938b59b665b1fddc052402770a21cc3f (diff)
downloadpardes-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.md11
-rw-r--r--docs/fs.md9
-rw-r--r--src/ninep/ctl.zig40
-rw-r--r--src/ninep/events.zig21
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.
diff --git a/docs/fs.md b/docs/fs.md
index f377d32a..ccf06dd1 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -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.