diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-27 20:35:01 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | c3e2cd3b854964ca7db3798cb8750ee6d0833057 (patch) | |
| tree | 0073c6bb6ec4cd78fffa89b2880dd83b4a106e1d /src/panes.zig | |
| parent | 5d9a56d47a9cd5eefc4c8bf03709f2e96febb212 (diff) | |
| download | pardes-c3e2cd3b854964ca7db3798cb8750ee6d0833057.tar.gz pardes-c3e2cd3b854964ca7db3798cb8750ee6d0833057.zip | |
pty/run says cut when its output's start is gone, and reads only a bounded tail
A clear or reset while the command ran, a start or end mark that came on
the alternate screen, or a garbage end pin read as a complete answer;
now any of them, like a start that scrolled out, answers exit N cut.
The output is read from at most output_rows rows above its end instead
of the whole history on the editor's thread (cut if that clips it), a D
with no status answers exit ? rather than exit 0, and running out of
memory answers error out of memory rather than cut.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/panes.zig')
| -rw-r--r-- | src/panes.zig | 62 |
1 files changed, 47 insertions, 15 deletions
diff --git a/src/panes.zig b/src/panes.zig index 08310830..0fb49636 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -6535,6 +6535,11 @@ pub const Terminal = struct { /// D) on the primary screen, pinned so scrolling keeps them. out_start: ?*ghostty_vt.Pin = null, out_end: ?*ghostty_vt.Pin = null, + /// The running command's output was erased under it -- a clear, ED 3, + /// a reset -- so the pins no longer span what it printed: ED 3 moves + /// a pin to the next page without flagging it `garbage` + /// (PageList.erasePage). + lost: bool = false, /// The newest command to end: which one, and its status if given. finished: ?struct { seq: u32, status: ?i32 } = null, @@ -6569,6 +6574,7 @@ pub const Terminal = struct { .end_input_start_output => if (self.phase == .input) { self.phase = .running; self.started +%= 1; + self.lost = false; self.pinCursor(&self.out_start, true); self.pinCursor(&self.out_end, false); }, @@ -6582,6 +6588,9 @@ pub const Terminal = struct { else => {}, } } + if (comptime action == .erase_display_complete or action == .erase_display_scrollback or action == .full_reset) { + if (self.phase == .running) self.lost = true; + } self.inner.vt(action, value); } @@ -7380,23 +7389,46 @@ pub const Terminal = struct { state.vt.screens.active.cursor.semantic_content == .input; } + pub const CommandOutput = struct { + /// Owned by the caller. + text: [:0]const u8, + /// Whether `text` starts where the command did. It does not when that + /// start is not there to read: cut off by `max_rows`, scrolled out of + /// the history (ghostty moved the pin to the oldest row it kept and + /// flagged it `garbage`), erased under it (`MarkedHandler.lost`), or + /// never pinned (a C or D that came on the alternate screen). + whole: bool, + }; + /// What the newest finished command printed: the screen text between its - /// C and D marks, owned by the caller. When its start has scrolled out of - /// the history, ghostty moved that pin to the oldest row it kept (and - /// flagged it `garbage`), so this is the part that survived. - pub fn commandOutput(pane: *Pane, gpa: std.mem.Allocator) ![:0]const u8 { - if (comptime enabled) printed: { - const state = pane.terminal orelse break :printed; - const start = state.stream.handler.out_start orelse break :printed; - const end = state.stream.handler.out_end orelse break :printed; - if (end.garbage) break :printed; - const screen = state.vt.screens.get(.primary) orelse break :printed; + /// C and D marks, read from at most `max_rows` rows above its end, so a + /// command that printed the whole history costs no more than its tail. + pub fn commandOutput(pane: *Pane, gpa: std.mem.Allocator, max_rows: usize) !CommandOutput { + // Unless there is text to read, whether the nothing it printed is the + // whole of it. + const whole = if (comptime enabled) printed: { + const state = pane.terminal orelse break :printed true; + const marks = &state.stream.handler; + const start = marks.out_start orelse break :printed false; + const end = marks.out_end orelse break :printed false; + const screen = state.vt.screens.get(.primary) orelse break :printed false; + if (end.garbage) break :printed false; + var whole = !marks.lost and !start.garbage; // D is where the next prompt begins: the output ends a cell before. - const last = end.leftWrap(1) orelse break :printed; - if (start.eql(end.*) or last.before(start.*)) break :printed; - return try screen.selectionString(gpa, .{ .sel = ghostty_vt.Selection.init(start.*, last, false) }); - } - return try gpa.dupeZ(u8, ""); // it printed nothing + const last = end.leftWrap(1) orelse break :printed whole; + if (start.eql(end.*) or last.before(start.*)) break :printed whole; + var from = start.*; + if (last.up(max_rows)) |floor| if (from.before(floor)) { + from = floor; + from.x = 0; + whole = false; + }; + return .{ + .text = try screen.selectionString(gpa, .{ .sel = ghostty_vt.Selection.init(from, last, false) }), + .whole = whole, + }; + } else true; + return .{ .text = try gpa.dupeZ(u8, ""), .whole = whole }; } /// At a prompt with nothing typed on its line: no input left of the |
