summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 05:14:06 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit429c1e5e65afa4c65fa42bbb117c4b32a46a9470 (patch)
tree3621a08103b25ea6cbe44d100d09c51adb639256
parente933535c9c78695dbcc469fff6a966601fd6c8f6 (diff)
downloadpardes-429c1e5e65afa4c65fa42bbb117c4b32a46a9470.tar.gz
pardes-429c1e5e65afa4c65fa42bbb117c4b32a46a9470.zip
A chorded record written back with its two follow-ups runs once, with its argument
Writing a chorded exec's three records back as read ran the argument and its location as shell commands. The first record (flag 8) now takes the next record as its argument when it comes in the same write, else the one kept from the click (or, with none kept, waits for it); its two follow-up records, however they are written, are consumed as its, never run. Tested written back together and one per write. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md6
-rw-r--r--src/mouse.zig17
-rw-r--r--src/ninep/events.zig50
-rw-r--r--src/ninep/pane.zig8
4 files changed, 73 insertions, 8 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 1e65bd00..ce33ac7c 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -713,7 +713,11 @@ was expanded from a click (a second record gives the original range), 4 (a
look) the text is a file name or address, 8 (an exec) chorded: two records
follow, the argument's text and where it came from, `<file>:#q0,#q1`.
Written back, an `X`/`x` record executes and an `L`/`l` record looks, as
-the click would have (a chorded one with its argument); the origin letter
+the click would have. A chorded one (flag 8) runs with its argument: the
+record after it in the same write, else the one kept from the click; its
+two follow-up records written back after it, together or in writes of
+their own, are consumed as its, never run as commands (written back alone
+with no argument kept, it waits for its argument record); the origin letter
written back is ignored but for `F` on `X`, which runs as the command it
was rather than going to a bound REPL; `I`, `D`, `i` and `d` are reports and
are refused. Read `n` bytes of the text, not up to a newline -- bytes here, where acme counts runes. Every offset and
diff --git a/src/mouse.zig b/src/mouse.zig
index 9f054e2e..0d1bfadd 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -1676,5 +1676,22 @@ test "a chorded exec an event reader holds is told with its argument, and writte
try std.testing.expectEqual(ctlfs.Status.ok, th.call(p, .{ .tag = 3, .op = .write, .node = event, .handle = h, .data = line }).reply.status);
const pane = p.panes[0].?;
try std.testing.expectEqualStrings("arg-text", pane.msg[0..pane.msg_len]);
+ // All three records written back as read, in one write or in three:
+ // the two after the first are its argument and where it came from,
+ // never commands of their own.
+ for ([_]bool{ true, false }) |together| {
+ pane.msg_len = 0;
+ var all: [256]u8 = undefined;
+ var w = std.Io.Writer.fixed(&all);
+ for (records) |r| try w.print("M{s}", .{r[1..]});
+ if (together) {
+ _ = th.call(p, .{ .tag = 5, .op = .write, .node = event, .handle = h, .data = w.buffered() });
+ } else for (records) |r| {
+ var one: [80]u8 = undefined;
+ _ = th.call(p, .{ .tag = 6, .op = .write, .node = event, .handle = h, .data = try std.fmt.bufPrint(&one, "M{s}", .{r[1..]}) });
+ }
+ try std.testing.expectEqualStrings("arg-text", pane.msg[0..pane.msg_len]);
+ try std.testing.expect(!th.logHas(p, "run "));
+ }
_ = th.call(p, .{ .tag = 4, .op = .release, .node = event, .handle = h });
}
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 1067b60e..7a06bd04 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -536,7 +536,7 @@ pub fn notePtyOutput(p: *Pardes, id: usize, bytes: []const u8) void {
p.fs.news = true;
}
-const EventRecord = struct { origin: u8, action: Action, q0: u32, q1: u32, text: ?[]const u8 = null };
+const EventRecord = struct { origin: u8, action: Action, q0: u32, q1: u32, flag: u32 = 0, text: ?[]const u8 = null };
const EventReader = struct {
data: []const u8,
@@ -557,8 +557,9 @@ const EventReader = struct {
// 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;
+ var flag: u32 = 0;
if (i < er.data.len and er.data[i] != '\n') {
- _ = scanNumber(er.data, &i) orelse return null;
+ flag = 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];
@@ -566,7 +567,7 @@ const EventReader = struct {
}
if (i >= er.data.len or er.data[i] != '\n') return null;
er.i = i + 1;
- return .{ .origin = origin, .action = action, .q0 = q0, .q1 = q1, .text = text };
+ return .{ .origin = origin, .action = action, .q0 = q0, .q1 = q1, .flag = flag, .text = text };
}
};
@@ -616,17 +617,52 @@ pub fn writeEvent(p: *Pardes, req: Req, id: usize) Reply {
// never a click, and runs as the command it was.
// The chorded exec a reader was told of runs with its argument
// (mouse.reportGesture), when it is that range written back.
- .body_exec => {
+ .body_exec, .tag_exec => {
const pf = &pane.fs;
- const arg = if (pf.chord_arg) |a| (if (pf.chord_at.q0 == r.q0 and pf.chord_at.q1 == r.q1) a else null) else null;
- _ = exec.executeFrom(p, live, exec.withArg(p, text, arg), r.origin != 'F');
+ // The two records that follow a chorded exec (its argument
+ // and where it came from), written back after it, are its:
+ // consumed, never run as commands of their own.
+ if (pf.chord_follow > 0 and r.q0 == 0 and r.q1 == 0 and r.flag == 0) {
+ pf.chord_follow -= 1;
+ // The argument a command written back alone waited for.
+ if (pf.chord_pending) |command| {
+ pf.chord_pending = null;
+ defer p.gpa.free(command);
+ const line = exec.withArg(p, command, r.text orelse "");
+ if (pf.chord_pending_tag) _ = exec.execute(p, live, line) else _ = exec.executeFrom(p, live, line, true);
+ }
+ continue;
+ }
+ var arg = if (pf.chord_arg) |a| (if (pf.chord_at.q0 == r.q0 and pf.chord_at.q1 == r.q1) a else null) else null;
+ if (r.flag & flag_chorded != 0) {
+ // Its argument is the next record when it came in the
+ // same write; the rest that follow are consumed.
+ pf.chord_follow = 2;
+ const save = run.i;
+ if (run.next()) |following| if (following.q0 == 0 and following.q1 == 0 and following.flag == 0) {
+ if (following.text) |t| arg = p.scratch.allocator().dupe(u8, t) catch arg;
+ pf.chord_follow = 1;
+ } else {
+ run.i = save;
+ };
+ // Alone, with no argument kept: it waits for its own.
+ if (arg == null) {
+ if (pf.chord_pending) |old| p.gpa.free(old);
+ pf.chord_pending = p.gpa.dupe(u8, text) catch null;
+ pf.chord_pending_tag = r.action == .tag_exec;
+ continue;
+ }
+ }
+ if (r.action == .tag_exec)
+ _ = exec.execute(p, live, exec.withArg(p, text, arg))
+ else
+ _ = exec.executeFrom(p, live, exec.withArg(p, text, arg), r.origin != 'F');
if (arg != null) if (p.paneBySerial(serial)) |still| {
const spf = &p.panes[still].?.fs;
if (spf.chord_arg) |a| p.gpa.free(a);
spf.chord_arg = null;
};
},
- .tag_exec => _ = exec.execute(p, live, text),
// What it found is what the look file reads back next, as a
// look written there would leave (ctl.run).
.body_look, .tag_look => {
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 4a50b9a1..899d8528 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -81,11 +81,19 @@ pub const State = struct {
/// for the record's write-back at `chord_at`, which runs with it.
chord_arg: ?[]u8 = null,
chord_at: Range = .{},
+ /// A chorded record written back: how many of its two follow-up records
+ /// (argument, location) are still to be consumed rather than run.
+ chord_follow: u8 = 0,
+ /// A chorded record written back alone with no argument kept: the
+ /// command, run when its argument record comes.
+ chord_pending: ?[]u8 = null,
+ chord_pending_tag: bool = false,
pub const Range = struct { q0: u32 = 0, q1: u32 = 0 };
pub fn deinit(pf: *State, gpa: std.mem.Allocator) void {
if (pf.chord_arg) |a| gpa.free(a);
+ if (pf.chord_pending) |c| gpa.free(c);
pf.events.deinit(gpa);
pf.pty_out.deinit(gpa);
pf.tag_snap.deinit(gpa);