summaryrefslogtreecommitdiff
path: root/src/ninep/pty.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 16:52:18 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit0e2afeecf6b8c0e9ac81da51f5f8c23a5be1a5ae (patch)
treeffe26f39f88daf89647d077c2fce888f64569018 /src/ninep/pty.zig
parentc9d5d97e4d487b92a52d765dd9c467423ddd1de1 (diff)
downloadpardes-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/pty.zig')
-rw-r--r--src/ninep/pty.zig28
1 files changed, 26 insertions, 2 deletions
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);