diff options
Diffstat (limited to 'src/ninep/ctl.zig')
| -rw-r--r-- | src/ninep/ctl.zig | 170 |
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)); +} |
