summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 09:41:16 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit2c03f710535a0b0aa143cd6c1fad04a45857a777 (patch)
tree085190ba2564435286a448253368fa96fcb5c36f
parentfcc95542a40cc065503d0f6d8eb4c40e87e68d36 (diff)
downloadpardes-2c03f710535a0b0aa143cd6c1fad04a45857a777.tar.gz
pardes-2c03f710535a0b0aa143cd6c1fad04a45857a777.zip
Edit written to /tagexec or a column's exec is refused as the other pane words are
Edit was let through the pane-word check (the root's ctl takes it at the active pane), so `Edit ,d` written to a tag no pane owns wiped the pane with the keyboard. Every pane-scope word a tag does not hold is refused there now, Edit included; a test enumerates them all. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md2
-rw-r--r--src/ninep/cols.zig38
2 files changed, 35 insertions, 5 deletions
diff --git a/docs/fs.md b/docs/fs.md
index cb9d7a45..a45bff17 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -132,7 +132,7 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC.
pane/new and a look place a pane next (- when there is none)
/tag the workspace tag; > replaces it, >> appends, one line
/tagexec write a word: a middle click on it in the workspace tag; read as /exec. A
- pane's word (Undo, Msg, Save) is refused there and at a column's exec,
+ pane's word (Undo, Msg, Save, Edit too) is refused there and at a column's exec,
`not a session control message "Undo": write it to pane/<n>/ctl` (EINVAL),
never done at the pane with the keyboard; a tag's own words (New, Tty,
Find, Grep in a column's) run
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig
index 6fdcda07..3eeadfdc 100644
--- a/src/ninep/cols.zig
+++ b/src/ninep/cols.zig
@@ -208,14 +208,15 @@ 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.
+/// Whether `line` names a builtin that acts on a pane (every one of pane
+/// scope, Edit too, which the root's ctl takes at the active pane) and is
+/// not one of `own`, the words its tag holds by default: those a tag no
+/// pane owns refuses, never doing them at the pane with the keyboard.
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;
+ if (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;
@@ -236,6 +237,35 @@ test "a pane's word written to /tagexec or a column's exec is refused, and acts
try testing.expectEqual(tree.Status.ok, th.wr(p, col_exec, "New\n").reply.status);
}
+test "every pane word a tag no pane owns does not hold is refused there, Edit included, and the pane is untouched" {
+ const p = try th.withFile(testing.allocator, "keep me\n");
+ defer p.deinit();
+ const Builtin = pardes.builtins.registry.Builtin();
+ const col_exec = tree.Node.ofCol(layout.columnSerial(p, 0), .exec);
+ for ([_]struct { node: u64, own: []const u8 }{
+ .{ .node = @intFromEnum(tree.TopFile.tagexec), .own = pardes.config.topbar_str },
+ .{ .node = col_exec, .own = tagline.column_tag_default },
+ }) |tag_file| {
+ var checked: usize = 0;
+ for (std.enums.values(Builtin)) |b| {
+ if (pardes.builtins.registry.scope(b) == .session) continue;
+ var owned = false;
+ var words = std.mem.tokenizeScalar(u8, tag_file.own, ' ');
+ while (words.next()) |w| owned = owned or std.mem.eql(u8, w, @tagName(b));
+ if (owned) continue;
+ var line: [64]u8 = undefined;
+ const text = try std.fmt.bufPrint(&line, "{s}{s}\n", .{ @tagName(b), if (b == .Edit) " ,d" else if (pardes.builtins.registry.requiresArg(b)) " x" else "" });
+ const refused = th.wr(p, tag_file.node, text);
+ try testing.expectEqual(E.INVAL, refused.errno());
+ try testing.expectStringStartsWith(refused.reply.ename, "not a session control message");
+ checked += 1;
+ }
+ try testing.expect(checked > 20);
+ }
+ // `Edit ,d` wiped the pane with the keyboard: nothing touched it.
+ try testing.expectEqualStrings("keep me\n", p.panes[0].?.file.?.content);
+}
+
/// 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 {