summaryrefslogtreecommitdiff
path: root/src/panes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-27 18:07:06 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit6f7beca8d27e2e00f7b90bb75015637a9aa4c950 (patch)
tree823e143a5e29aca8049c905213e40520f3354383 /src/panes.zig
parentf4412d6dfdb1e2bd8549a7edba785ee242fd38f2 (diff)
downloadpardes-6f7beca8d27e2e00f7b90bb75015637a9aa4c950.tar.gz
pardes-6f7beca8d27e2e00f7b90bb75015637a9aa4c950.zip
Run a line at a terminal's prompt through pty/run and read how it ended
/pane/<n>/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 <[email protected]>
Diffstat (limited to 'src/panes.zig')
-rw-r--r--src/panes.zig74
1 files changed, 69 insertions, 5 deletions
diff --git a/src/panes.zig b/src/panes.zig
index 156d443a..3b1bb1b4 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -6502,7 +6502,59 @@ pub const Terminal = struct {
pub const history_max = if (enabled) 64 else 8;
pub const VtSlot = if (enabled) ghostty_vt.Terminal else void;
- pub const StreamSlot = if (enabled) ghostty_vt.TerminalStream else void;
+ pub const StreamSlot = if (enabled) ghostty_vt.Stream(MarkedHandler) else void;
+
+ /// The `aid` pardes's injected bash and fish marks carry (host_io.zig).
+ pub const prompt_aid = "pardes";
+
+ /// ghostty's handler, plus where the shell is in its command cycle as
+ /// its OSC 133 marks tell it: ghostty parses D's exit status and drops
+ /// it, and /pane/<n>/pty/run waits on exactly that. Only marks carrying
+ /// pardes's own `aid` move it, so fish's native marks, a nested shell's
+ /// and a stray `133;D` in some file being printed all pass it by.
+ pub const MarkedHandler = if (enabled) struct {
+ inner: ghostty_vt.TerminalStream.Handler,
+ phase: enum { none, prompt, input, running } = .none,
+ /// Counts commands started (C at the input phase).
+ started: u32 = 0,
+ /// Counts prompts drawn (B), so a line the shell refused without
+ /// running (a fish syntax error: no C, no D, a new prompt) shows.
+ prompts: u32 = 0,
+ /// The newest command to end: which one, and its status if given.
+ finished: ?struct { seq: u32, status: ?i32 } = null,
+
+ const Action = ghostty_vt.TerminalStream.Action;
+
+ pub fn vt(self: *@This(), comptime action: Action.Tag, value: Action.Value(action)) void {
+ if (comptime action == .semantic_prompt) mark: {
+ const aid = value.readOption(.aid) orelse break :mark;
+ if (!std.mem.eql(u8, aid, prompt_aid)) break :mark;
+ switch (value.action) {
+ .fresh_line_new_prompt => self.phase = .prompt,
+ .end_prompt_start_input => {
+ self.phase = .input;
+ self.prompts +%= 1;
+ },
+ // bash's DEBUG trap also fires on its own PROMPT_COMMAND,
+ // outside the input phase; only a C that ends input counts
+ .end_input_start_output => if (self.phase == .input) {
+ self.phase = .running;
+ self.started +%= 1;
+ },
+ .end_command => if (self.phase == .running) {
+ self.phase = .none;
+ self.finished = .{ .seq = self.started, .status = value.readOption(.exit_code) };
+ },
+ else => {},
+ }
+ }
+ self.inner.vt(action, value);
+ }
+
+ pub fn deinit(self: *@This()) void {
+ self.inner.deinit();
+ }
+ } else void;
pub const Replay = struct {
bytes: [1024 * 1024]u8 = undefined,
@@ -6784,12 +6836,12 @@ pub const Terminal = struct {
.rows = rows,
.max_scrollback = 16 * 1024 * 1024,
});
- state.stream = state.vt.vtStream();
+ state.stream = .initAlloc(gpa, .{ .inner = state.vt.vtHandler() });
state.replay.head = 0;
state.replay.len = 0;
state.reply_len = 0;
- state.stream.handler.effects.write_pty = ptyReport;
- state.stream.handler.effects.device_attributes = ptyDeviceAttrs;
+ state.stream.handler.inner.effects.write_pty = ptyReport;
+ state.stream.handler.inner.effects.device_attributes = ptyDeviceAttrs;
pane.terminal = state;
pane.tty_filter = true;
return pane;
@@ -6938,7 +6990,7 @@ pub const Terminal = struct {
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
const state = pane.terminal.?;
- try std.testing.expect(state.stream.handler.terminal == &state.vt);
+ try std.testing.expect(state.stream.handler.inner.terminal == &state.vt);
p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[" } });
while (p.nextEffect()) |effect| {
if (effect == .write) return error.PrematureTerminalReply;
@@ -7294,6 +7346,18 @@ pub const Terminal = struct {
state.vt.screens.active.cursor.semantic_content == .input;
}
+ /// At a prompt with nothing typed on its line, wherever the cursor is
+ /// (someone may have typed and pressed Ctrl-A). fish suggests nothing on
+ /// an empty line, so its autosuggestions never make this false.
+ pub fn promptInputEmpty(pane: *const Pane) bool {
+ if (comptime !enabled) return false;
+ if (!promptInputReady(pane)) return false;
+ const cursor = &pane.terminal.?.vt.screens.active.cursor;
+ for (cursor.page_pin.cells(.all)) |cell|
+ if (cell.semantic_content == .input and !cell.isEmpty()) return false;
+ return true;
+ }
+
/// Encode one key for the program that owns a raw terminal and queue its pty
/// write, respecting the application's keyboard protocol and terminal modes.
pub fn forwardKey(p: *Pardes, id: usize, key: Key) void {