From 6f7beca8d27e2e00f7b90bb75015637a9aa4c950 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 27 Sep 2026 18:07:06 -0300 Subject: Run a line at a terminal's prompt through pty/run and read how it ended /pane//pty/run takes factotum's rpc shape: write one line on an open, read the answer on that same open. The answer is exit N once the command ended and the shell is back at a prompt; busy at once when a command runs, text is typed at the prompt, or the shell has not drawn its first prompt; error not run when the shell refused the line without running it (fish on a syntax error keeps it on the prompt, so it is taken back with Ctrl-U); error shell gone when the pane closes or its shell is replaced; error no prompt marks for a shell pardes could not instrument. The end comes from the shell's OSC 133 marks. ghostty parses D's exit status and drops it, so the stream now runs through a handler that wraps ghostty's and follows prompt -> input -> running -> done. The marks pardes injects into bash and fish carry aid=pardes and only those count, so fish 4's own marks (which doubled ours), a nested shell's, and a stray 133;D in printed output are ignored. Checked end to end against real bash and fish: false, exit 7, a syntax error, sleep, busy while running, and the pane closing mid-command. Co-Authored-By: Claude Opus 5.5 --- src/ninep/tree.zig | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/ninep/tree.zig') diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index a85a1873..0d328594 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -167,6 +167,7 @@ pub const PaneFile = enum(u5) { pty_ctl, pty_status, pty_data, + pty_run, pub fn fileName(f: PaneFile) []const u8 { return switch (f) { @@ -174,6 +175,7 @@ pub const PaneFile = enum(u5) { .pty_ctl => "ctl", .pty_status => "status", .pty_data => "data", + .pty_run => "run", else => @tagName(f), }; } @@ -193,7 +195,7 @@ pub const PaneFile = enum(u5) { pub fn inPty(f: PaneFile) bool { return switch (f) { - .pty, .pty_ctl, .pty_status, .pty_data => true, + .pty, .pty_ctl, .pty_status, .pty_data, .pty_run => true, else => false, }; } @@ -531,6 +533,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { pf.event_reader = pf.event_reader or reader; if (reader) return .{ .tag = req.tag, .handle = reader_handle }; }, + .pty_run => return pty.openRun(p, req, t.serial), .pty_data => if (reads(req.omode)) { if (pf.pty_readers > 0) return failText(req.tag, e_busy, e_in_use); pf.pty_readers = 1; @@ -569,6 +572,8 @@ fn releaseHandle(p: *Pardes, req: Req) Reply { switch (target) { .top => {}, .pane => |t| { + // A run's open outlives its pane: it still holds the answer. + if (t.file == .pty_run) return pty.releaseRun(p, req); if (t.file != .event and t.file != .pty_data) return .{ .tag = req.tag }; const id = p.paneBySerial(t.serial) orelse return .{ .tag = req.tag }; const pf = &p.fs.panes[id]; @@ -633,6 +638,8 @@ fn read(p: *Pardes, req: Req, target: Target) Reply { .root, .pane => Reply.fail(req.tag, E.PERM), }, .pane => |t| { + // A run's answer outlives the pane it ran in. + if (t.file == .pty_run and req.handle != 0) return pty.readRun(p, req); const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT); const pn = p.panes[id].?; if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT); -- cgit v1.3