diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 16:52:18 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 0e2afeecf6b8c0e9ac81da51f5f8c23a5be1a5ae (patch) | |
| tree | ffe26f39f88daf89647d077c2fce888f64569018 /src/ninep | |
| parent | c9d5d97e4d487b92a52d765dd9c467423ddd1de1 (diff) | |
| download | pardes-0e2afeecf6b8c0e9ac81da51f5f8c23a5be1a5ae.tar.gz pardes-0e2afeecf6b8c0e9ac81da51f5f8c23a5be1a5ae.zip | |
Errors are said in words, and a refusal names what it takes
A failure read `Del k|j: InvalidArgument`, `Kill: NoSuchCommand`, `ill-formed control message`, or nothing at all. reportError now spells an error's name as words, and the refusals name their accepted forms: pty/ctl lists its verbs, a flag file takes 1 or 0, a setting word takes on or off, Del takes k, j or nothing, Repl takes - or a language. Kill with nothing to stop says so, /focus says pane, a Look that found nothing says no match, and pty/run on a command pane says it is not a shell. A ctl write's failure has room for the Repl list and is cut between words, the Repl word at a character.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep')
| -rw-r--r-- | src/ninep/ctl.zig | 6 | ||||
| -rw-r--r-- | src/ninep/events.zig | 2 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 6 | ||||
| -rw-r--r-- | src/ninep/pty.zig | 28 |
4 files changed, 34 insertions, 8 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 4e1c039b..680a09b0 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -200,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); } @@ -399,7 +399,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; @@ -666,7 +666,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); diff --git a/src/ninep/events.zig b/src/ninep/events.zig index cff42c55..ffdea95b 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -877,7 +877,7 @@ test "a refused or failed write is an err record in the log, saying which file a _ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus"); try testing.expectEqualStrings("err - ctl: unknown control message \"Bogus\"\n", next.record(p, f, frozen)); _ = wr(p, @intFromEnum(tree.TopFile.focus), "99999"); - try testing.expectEqualStrings("err - focus: no such window\n", next.record(p, f, frozen)); + try testing.expectEqualStrings("err - focus: no such pane\n", next.record(p, f, frozen)); // A write that succeeds says nothing. _ = wr(p, Node.of(serial, .addr), "#0"); try testing.expectEqual(Status.again, call(p, .{ .tag = 5, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).reply.status); diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 77be80c6..d2b2a791 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -507,7 +507,7 @@ fn writeRange(req: Req, pane: *Pane, file: PaneFile) Reply { fn writeFlag(p: *Pardes, req: Req, pane: *Pane, file: PaneFile) Reply { const word = std.mem.trim(u8, req.data, " \t\r\n"); - if (!std.mem.eql(u8, word, "1") and !std.mem.eql(u8, word, "0")) return Reply.fail(req.tag, E.INVAL); + if (!std.mem.eql(u8, word, "1") and !std.mem.eql(u8, word, "0")) return tree.failText(req.tag, E.INVAL, "takes 1 or 0"); const on = word[0] == '1'; const pf = &pane.fs; switch (file) { @@ -873,8 +873,10 @@ test "the flag files read and write the state acme kept behind ctl verbs" { for ([_]u64{ dirty, mark, scroll }) |node| { try testing.expectEqual(@as(u64, 2), call(p, .{ .tag = 1, .op = .getattr, .node = node }).reply.attr.size); - for ([_][]const u8{ "", "yes", "2", "0 1", "true" }) |bad| + for ([_][]const u8{ "", "yes", "2", "0 1", "true" }) |bad| { try testing.expectEqual(E.INVAL, wr(p, node, bad).errno()); + try testing.expectEqualStrings("takes 1 or 0", wr(p, node, bad).reply.ename); + } } try testing.expectEqualStrings("0\n", rd(p, dirty, 0, 8).bytes); diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig index 88dafdfc..a2a40811 100644 --- a/src/ninep/pty.zig +++ b/src/ninep/pty.zig @@ -43,7 +43,7 @@ pub fn writeCtl(p: *Pardes, req: Req, id: usize) Reply { while (it.next()) |raw| { const line = std.mem.trim(u8, raw, " \t\r"); if (line.len == 0) continue; - if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, tree.e_bad_ctl); + if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, e_bad_pty_ctl); } } return .{ .tag = req.tag, .written = @intCast(req.data.len) }; @@ -165,7 +165,10 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { const pf = &pane.fs; const state = pane.terminal orelse return tree.failText(req.tag, E.INVAL, e_bad_line); const marks = &state.stream.handler; - if (pf.unmarked) { + if (pane.command != null) { + // Its child is the command, and no shell is there to take a line. + answer(p, slot, "error {s}", .{if (pane.command_done) "command done; not a shell" else "a command runs here, not a shell"}); + } else if (pf.unmarked) { answer(p, slot, "error no prompt marks", .{}); } else if (waitingRun(p, pane) != null or (marks.prompts > 0 and (marks.phase != .input or !pardes.panes.Terminal.promptInputEmpty(pane) or p.hostTtyTaken(id)))) @@ -335,6 +338,7 @@ fn waitingRun(p: *Pardes, pane: *Pane) ?*Run { } const e_bad_line = "bad command line"; +const e_bad_pty_ctl = tree.e_bad_ctl ++ "; takes winsize COLS ROWS, sig INT|TERM|HUP|QUIT|KILL, or exec"; pub fn readData(p: *Pardes, req: Req, pf: *pane_files.State) Reply { if (pf.pty_out.empty()) return .{ .tag = req.tag, .status = .again }; @@ -505,6 +509,7 @@ test "every pty/ctl verb, and every refusal" { }) |bad| { const a = wr(p, ctl, bad); try testing.expectEqual(E.INVAL, a.errno()); + try testing.expectEqualStrings(e_bad_pty_ctl, a.reply.ename); // names what it takes try testing.expect(a.winsize == null); try testing.expect(a.signal == null); try testing.expect(!a.spawned); @@ -743,6 +748,25 @@ test "run answers how its command ended once the shell is back at a prompt" { for (p.fs.opens) |o| try testing.expect(o.node == 0); } +test "run on a command pane says no shell is there" { + const gpa = testing.allocator; + const p = try withTerm(gpa); + defer p.deinit(); + const node = Node.of(serialOf(p), .pty_run); + const pane = p.panes[0].?; + pane.command = try gpa.dupe(u8, "make"); + for ([_]struct { bool, []const u8 }{ + .{ false, "error a command runs here, not a shell\n" }, + .{ true, "error command done; not a shell\n" }, + }) |case| { + pane.command_done = case[0]; + const o = call(p, .{ .tag = 1, .op = .open, .node = node }); + _ = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = o.reply.handle, .data = "ls\n" }); + try testing.expectEqualStrings(case[1], call(p, .{ .tag = 3, .op = .read, .node = node, .handle = o.reply.handle, .size = 64 }).bytes); + _ = call(p, .{ .tag = 4, .op = .release, .node = node, .handle = o.reply.handle }); + } +} + test "a run's answer says cut when its output's start is gone, and reads a bounded tail" { const gpa = testing.allocator; const p = try withTerm(gpa); |
