summaryrefslogtreecommitdiff
path: root/src/ninep/cols.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 09:07:10 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit44aa1ff5bf19b4020e9b4600e5286dea35a3ba5c (patch)
treef17c7ee8dd46e126b1a0fc1df5b05129e1e5d8e6 /src/ninep/cols.zig
parent7b727114350030fa34281803cc12a1dbb7e6f7fc (diff)
downloadpardes-44aa1ff5bf19b4020e9b4600e5286dea35a3ba5c.tar.gz
pardes-44aa1ff5bf19b4020e9b4600e5286dea35a3ba5c.zip
A pane's word written to /tagexec or a column's exec is refused, not done at the focused pane
Undo, Msg and the other pane words written to a tag no pane owns ran at whichever pane had the keyboard. They are refused as the root's ctl refuses them, EINVAL; the tag's own words (a column's New, Tty, Find, Grep) still run. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/cols.zig')
-rw-r--r--src/ninep/cols.zig43
1 files changed, 39 insertions, 4 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig
index efb62cfe..7f78522a 100644
--- a/src/ninep/cols.zig
+++ b/src/ninep/cols.zig
@@ -168,6 +168,12 @@ pub fn writeExec(p: *Pardes, req: Req, serial: ?u32) Reply {
if (line.len == 0) continue;
for (line) |c| if (c < ' ' and c != '\t') return tree.failText(req.tag, E.INVAL, pardes.ctlfs.ctl.e_control);
if (pardes.ctlfs.ctl.tooLong(req, line)) |refusal| return refusal;
+ // A pane's word (Undo, Msg, Save) is no word of a tag no pane owns:
+ // refused as the root's ctl refuses it, never done at whichever
+ // pane has the keyboard. The tag's own words (New, Tty, Find, Grep
+ // in a column's) are its to run.
+ if (paneWord(line, if (serial == null) pardes.config.topbar_str else tagline.column_tag_default))
+ return pardes.ctlfs.ctl.refuseTo(p, req, "not a session control message", line, "pane/<n>/ctl");
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;
@@ -202,6 +208,34 @@ test "New written to a column's ctl reads back the new pane alone, as its exec d
try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{p.panes[p.active].?.serial}), th.rd(p, root_exec, 0, 64).bytes);
}
+/// Whether `line` names a builtin that acts on a pane and is not one of
+/// `own`, the words its tag holds by default. Edit, which the root's ctl
+/// takes at the active pane, is taken here too.
+fn paneWord(line: []const u8, own: []const u8) bool {
+ const cmd = pardes.exec.commandText(line);
+ const word = cmd[0 .. std.mem.indexOfAny(u8, cmd, " \t+") orelse cmd.len];
+ const b = std.meta.stringToEnum(pardes.builtins.registry.Builtin(), word) orelse return false;
+ if (b == .Edit or pardes.builtins.registry.scope(b) == .session) return false;
+ var words = std.mem.tokenizeScalar(u8, own, ' ');
+ while (words.next()) |w| if (std.mem.eql(u8, w, word)) return false;
+ return true;
+}
+
+test "a pane's word written to /tagexec or a column's exec is refused, and acts on no pane" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const col_exec = tree.Node.ofCol(layout.columnSerial(p, 0), .exec);
+ for ([_]u64{ @intFromEnum(tree.TopFile.tagexec), col_exec }) |node| {
+ for ([_][]const u8{ "Undo\n", "Msg hi\n", "Redo\n" }) |line| {
+ const refused = th.wr(p, node, line);
+ try testing.expectEqual(E.INVAL, refused.errno());
+ try testing.expectStringStartsWith(refused.reply.ename, "not a session control message");
+ }
+ }
+ // The column tag's own words still run there.
+ try testing.expectEqual(tree.Status.ok, th.wr(p, col_exec, "New\n").reply.status);
+}
+
/// rmdir of a column closes it when it is empty; one with panes is
/// refused, saying so (close them, or Delcol on its ctl, which asks first).
pub fn remove(p: *Pardes, req: Req, serial: u32, file: tree.ColFile) Reply {
@@ -336,8 +370,10 @@ test "tagexec runs a word as a click in the workspace tag; exec files read back
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"));
+ // What it says is the session's (Newcol announcing itself); a pane's
+ // word such as Msg is refused there.
+ try testing.expect(th.logHas(p, "msg - Newcol\n"));
+ try testing.expectEqual(E.INVAL, th.wr(p, tagexec, "Msg from the top\n").errno());
// 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);
@@ -346,8 +382,7 @@ test "tagexec runs a word as a click in the workspace tag; exec files read back
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"));
+ try testing.expect(th.logHas(p, "msg - New\n"));
}
test "a word run from the workspace's or a column's tag runs in the session's directory, a pane's in its own" {