summaryrefslogtreecommitdiff
path: root/src/ninep/ctl.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/ctl.zig')
-rw-r--r--src/ninep/ctl.zig170
1 files changed, 163 insertions, 7 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index cd327d03..a64521e7 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -53,8 +53,26 @@ fn noteResult(p: *Pardes, serial: u32) void {
/// the pane it acted on (even one it closed).
fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) 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 a look or exec is a click: on that pane, or for
+ // the root's, on the pane with the keyboard. The reader writes the
+ // record back to have it done.
+ if (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);
+ // An exec answers the pane it went to: a command pane it reused, or
+ // the terminal it typed into.
+ const went: ?usize = if (exec) pardes.exec.execute(p, id, text) else blk: {
+ look.lookAt(p, id, text);
+ break :blk null;
+ };
const after = Serials.snapshot(p);
var created = false;
for (after.list[0..after.len]) |serial| if (std.mem.indexOfScalar(u32, before.list[0..before.len], serial) == null) {
@@ -62,6 +80,7 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void {
created = true;
};
if (created) return;
+ if (went) |dst| if (p.panes[dst]) |pane| return noteResult(p, pane.serial);
noteResult(p, if (exec) context else (p.panes[p.active] orelse return).serial);
}
@@ -144,7 +163,7 @@ fn writeQuoted(w: *std.Io.Writer, s: []const u8) !void {
try w.writeByte('\'');
}
-pub const pane_text_cap = 7 * 12 + 2 * 256 + 4 + "notcurrent ".len;
+pub const pane_text_cap = 7 * 12 + 2 * 256 + 4 + "notcurrent ".len + 48;
/// acme's winctlprint: serial, tag length, body length, a reserved zero, the
/// dirty flag, then the width in cells, the font and the tab width. Then
@@ -167,6 +186,10 @@ pub fn paneText(p: *Pardes, pane: *Pane, buf: []u8) []const u8 {
// While a column's or the workspace's tag has the keyboard, no pane does.
const current = p.panes[p.active] == pane and !p.header_focus;
w.print(" {d:>11} {s} ", .{ config.tab_width, if (current) "current" else "notcurrent" }) catch {};
+ // A terminal bound as a REPL ends with its id, `python-a`.
+ var idbuf: [48]u8 = undefined;
+ const id = exec_line.replId(&idbuf, pane);
+ if (id.len > 0) w.print("{s} ", .{id}) catch {};
return w.buffered();
}
@@ -177,7 +200,7 @@ const Builtin = builtins.registry.Builtin();
/// Refuses a control message, quoting it the way Plan 9's cmderror does
/// (kernel/misc/parse.c:82): `unknown control message "Bogus 3"`.
fn refuse(p: *Pardes, req: Req, why: []const u8, line: []const u8) Reply {
- const room = p.fs.ename.len - why.len - 3;
+ const room = p.fs.ename.len -| (why.len + 3);
const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\"", .{ why, line[0..@min(line.len, room)] }) catch why;
return tree.failText(req.tag, E.INVAL, text);
}
@@ -201,7 +224,12 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ?
const takes = switch (setting.action) {
.theme => for (pardes.themes) |t| {
if (std.mem.eql(u8, t.name, arg)) break true;
- } else false,
+ } else {
+ // `refuse` quotes the line after it, in what is left of 128.
+ var near: [40]u8 = undefined;
+ var why: [104]u8 = undefined;
+ return refuse(p, req, std.fmt.bufPrint(&why, "bad value in control message; like it: {s}; ThemeSel lists all", .{pardes.colors.themesNear(&near, arg)}) catch "bad value in control message", line);
+ },
.font => config.Runtime.FontSpec.parse(arg) != null,
else => probe: {
var probe = p.settings;
@@ -220,7 +248,18 @@ fn runBuiltin(p: *Pardes, req: Req, id: usize, line: []const u8) ?Reply {
p.fs.refused = false;
p.fs.failure_len = 0;
defer p.fs.no_prompt = false;
+ // What a ctl write touched reads back from /look and /exec as a click's
+ // does (`run`), so a script that made a pane with `Tty` finds it there.
+ const context = p.panes[id].?.serial;
+ const before = Serials.snapshot(p);
_ = exec_line.executeBuiltinLine(p, id, line);
+ const after = Serials.snapshot(p);
+ var created = false;
+ for (after.list[0..after.len]) |serial| if (std.mem.indexOfScalar(u32, before.list[0..before.len], serial) == null) {
+ noteResult(p, serial);
+ created = true;
+ };
+ if (!created) noteResult(p, context);
if (p.fs.refused) return refuse(p, req, e_prompt, line);
if (p.fs.failure_len == 0) return null;
const refusal = refuse(p, req, p.fs.failure[0..p.fs.failure_len], line);
@@ -292,6 +331,7 @@ pub fn readRoot(p: *Pardes, req: Req) Reply {
/// runs, as the root's look and exec click there; all are checked first.
pub fn writeRoot(p: *Pardes, req: Req) Reply {
for ([2]bool{ false, true }) |apply| {
+ if (apply) p.fs.results_len = 0;
var it = std.mem.splitScalar(u8, req.data, '\n');
while (it.next()) |raw| {
const line = std.mem.trim(u8, raw, " \t\r");
@@ -361,7 +401,7 @@ pub fn writeFocus(p: *Pardes, req: Req) Reply {
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
-const e_no_pane = "no such window";
+const e_no_pane = "no such pane";
pub fn readPane(p: *Pardes, req: Req, pane: *Pane) Reply {
var buf: [pane_text_cap]u8 = undefined;
@@ -399,6 +439,7 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply {
// Checked whole before anything applies, so a write that must wait for
// the lock has done nothing yet when it goes again.
for ([2]bool{ false, true }) |apply| {
+ if (apply) p.fs.results_len = 0;
var held = !other and pane.fs.lock != null;
var it = std.mem.splitScalar(u8, req.data, '\n');
while (it.next()) |raw| {
@@ -615,7 +656,9 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi
try testing.expectEqualStrings("not a session control message \"Del\"", wr(p, root_ctl, "Del").reply.ename);
try testing.expectEqualStrings("unknown control message \"Nonsense 1\"", wr(p, root_ctl, "Nonsense 1").reply.ename);
try testing.expectEqualStrings("bad value in control message \"Verbose maybe\"", wr(p, root_ctl, "Verbose maybe").reply.ename);
- try testing.expectEqualStrings("bad value in control message \"Theme no-such-theme\"", wr(p, root_ctl, "Theme no-such-theme").reply.ename);
+ const no_theme = wr(p, root_ctl, "Theme no-such-theme").reply.ename;
+ try testing.expectStringStartsWith(no_theme, "bad value in control message; like it: n");
+ try testing.expect(std.mem.endsWith(u8, no_theme, "; ThemeSel lists all \"Theme no-such-theme\""));
try testing.expectEqualStrings("wrong #args in control message \"Newcol 2\"", wr(p, root_ctl, "Newcol 2").reply.ename);
try testing.expectEqualStrings("wrong #args in control message \"Theme\"", wr(p, root_ctl, "Theme").reply.ename);
// A bare required word fails the check, so the line before never runs.
@@ -625,7 +668,7 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi
// its line; the lines before it have taken effect, as in acme.
const failed = wr(p, root_ctl, "Verbose on\nMount x\nVerbose off");
try testing.expectEqual(E.IO, failed.errno());
- try testing.expectEqualStrings("Mount name dial: MissingArgument \"Mount x\"", failed.reply.ename);
+ try testing.expectEqualStrings("Mount name dial: missing argument \"Mount x\"", failed.reply.ename);
try testing.expect(p.settings.verbose);
try testing.expect(!p.fs.no_prompt);
try testing.expectEqual(Status.ok, wr(p, root_ctl, "Verbose off").reply.status);
@@ -860,3 +903,116 @@ 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);
+ // The root's is a click at the pane with the keyboard, this one.
+ try testing.expectEqual(tree.Status.ok, wr(p, root_exec, "Msg rooted\n").reply.status);
+ try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "rooted") == null);
+ try testing.expectEqualStrings("FX0 0 2 10 Msg rooted\n", call(p, .{ .tag = 5, .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);
+}
+
+test "look and exec read back what the last command touched, a ctl write's too" {
+ const gpa = testing.allocator;
+ const p = try withFile(gpa, "x\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ var want: [32]u8 = undefined;
+ _ = wr(p, root_exec, "Msg first\n");
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{serial}), rd(p, root_exec, 0, 64).bytes);
+ // A pane made through ctl is what both read next, not the Msg's pane.
+ try testing.expectEqual(tree.Status.ok, wr(p, Node.of(serial, .ctl), "Tty\n").reply.status);
+ const made = p.panes[p.active].?.serial;
+ try testing.expect(made != serial);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), rd(p, root_exec, 0, 64).bytes);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), rd(p, @intFromEnum(tree.TopFile.look), 0, 64).bytes);
+ // A ctl write that makes nothing names the pane it ran at.
+ try testing.expectEqual(tree.Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Verbose off\n").reply.status);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), rd(p, root_exec, 0, 64).bytes);
+}
+
+test "an error's words give the errno a mount reads: EINVAL for what is malformed, EIO for what failed" {
+ // 9ns (cloud9's 9ns/src/nine.zig) maps an error to an errno by its
+ // words, and EIO when none of them match: these words must not.
+ const addressing = @import("addr.zig");
+ const einval = [_][]const u8{ "control message", "invalid", "bad " };
+ const other = [_][]const u8{ "interrupt", "shut down", "not exist", "not found", "no such", "exists", "not empty", "not a dir", "is a dir", "permission", "denied", "read-only", "read only", "no space", "not allowed", "not permitted", "cannot", "fid", "busy", "in use", "too long", "not supported", "unsupported" };
+ const Case = struct { text: []const u8, malformed: bool };
+ for ([_]Case{
+ .{ .text = addressing.e_syntax, .malformed = true },
+ .{ .text = addressing.e_regexp, .malformed = true },
+ .{ .text = e_prompt, .malformed = true },
+ .{ .text = addressing.e_no_match, .malformed = false },
+ .{ .text = addressing.e_range, .malformed = false },
+ .{ .text = addressing.e_order, .malformed = false },
+ .{ .text = addressing.e_slow, .malformed = false },
+ .{ .text = "/tmp/x.txt: Modified (Exit again to discard)", .malformed = false },
+ }) |c| {
+ const says_einval = for (einval) |w| {
+ if (std.ascii.findIgnoreCase(c.text, w) != null) break true;
+ } else false;
+ try testing.expectEqual(c.malformed, says_einval);
+ for (other) |w| try testing.expect(std.ascii.findIgnoreCase(c.text, w) == null);
+ }
+}
+
+test "Joincol with no column to the right and Theme with no such theme say so" {
+ const gpa = testing.allocator;
+ const p = try withFile(gpa, "x\n");
+ defer p.deinit();
+ const root_ctl = @intFromEnum(tree.TopFile.ctl);
+ const joined = wr(p, root_ctl, "Joincol\n");
+ try testing.expectEqual(tree.Status.err, joined.reply.status);
+ try testing.expectStringStartsWith(joined.reply.ename, "Joincol: no column to the right");
+ const themed = wr(p, root_ctl, "Theme drak\n");
+ try testing.expectEqual(E.INVAL, themed.errno());
+ try testing.expectStringStartsWith(themed.reply.ename, "bad value in control message; like it: ");
+ try testing.expect(std.mem.indexOf(u8, themed.reply.ename, " dark") != null or std.mem.indexOf(u8, themed.reply.ename, ": dark") != null);
+ try testing.expect(std.mem.endsWith(u8, themed.reply.ename, "\"Theme drak\""));
+ // A click on the word says it on the message row.
+ _ = wr(p, Node.of(serialOf(p), .exec), "Theme drak\n");
+ const pane = p.panes[p.active].?;
+ try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "no theme \"drak\"") != null);
+}
+
+test "a look that fills a column's stillborn scratch is answered with the scratch already gone" {
+ const gpa = testing.allocator;
+ const p = try withFile(gpa, "x\n");
+ defer p.deinit();
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "file\n" });
+ var dir: [4096]u8 = undefined;
+ const at = dir[0..try tmp.dir.realPath(testing.io, &dir)];
+ var line: [4200]u8 = undefined;
+ // Newcol makes a column of one empty +New; a file looked from it
+ // joins that column and takes the scratch's place.
+ try testing.expectEqual(tree.Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Newcol\n").reply.status);
+ const scratch = p.panes[p.active].?.serial;
+ try testing.expectEqual(tree.Status.ok, wr(p, Node.of(scratch, .look), try std.fmt.bufPrint(&line, "{s}/f.txt\n", .{at})).reply.status);
+ try testing.expect(p.paneBySerial(scratch) == null);
+ var want: [32]u8 = undefined;
+ const gone = try std.fmt.bufPrint(&want, "{d} ", .{scratch});
+ var rows = std.mem.splitScalar(u8, rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes, '\n');
+ while (rows.next()) |row| try testing.expect(!std.mem.startsWith(u8, row, gone));
+}