From e563be42a5b3e9243e2c01d34cf8e94d2047b753 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 00:45:33 -0300 Subject: A terminal's body reads as logical lines, the last one ending in a newline A terminal's body came back as the screen's rows, a long line cut where the terminal wrapped it and the last line with no newline. The body (and a terminal's Save, and the length its ctl reports) now joins wrapped rows by ghostty's wrap flags, as pty/run's output does, and ends the last line with a newline. Co-Authored-By: Claude Opus 5.5 --- src/Terminal.zig | 60 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 10 deletions(-) (limited to 'src/Terminal.zig') diff --git a/src/Terminal.zig b/src/Terminal.zig index 504ab887..4808298e 100644 --- a/src/Terminal.zig +++ b/src/Terminal.zig @@ -385,26 +385,66 @@ pub fn mouseFormatSgr(pane: *const Pane) bool { return state.vt.modes.get(.mouse_format_sgr); } -/// The whole scrollback as plain text, `gpa`-owned: what `Save` writes out. +/// The whole scrollback as plain text, `gpa`-owned: what a terminal's body +/// reads and `Save` writes out. Logical lines: rows the terminal wrapped +/// are joined again (ghostty's wrap flags, as pty/run's output is), and +/// the last line ends with a newline. pub fn screenTextAlloc(pane: *Pane, gpa: std.mem.Allocator) ![]const u8 { if (comptime !enabled) return &.{}; - const state = pane.terminal orelse return &.{}; - return state.vt.screens.active.dumpStringAlloc(gpa, .{ .screen = .{} }); + var out: std.Io.Writer.Allocating = .init(gpa); + errdefer out.deinit(); + try writeScreenText(pane, &out.writer); + return out.toOwnedSlice(); } +fn writeScreenText(pane: *Pane, w: *std.Io.Writer) !void { + if (comptime !enabled) return; + const state = pane.terminal orelse return; + const screen = state.vt.screens.active; + const br = screen.pages.getBottomRight(.screen) orelse return; + var both: TeeWriter = .{ .a = w }; + try screen.dumpString(&both.writer, .{ .tl = screen.pages.getTopLeft(.screen), .br = br, .unwrap = true }); + if (both.count > 0 and both.final != '\n') try w.writeByte('\n'); +} + +/// Passes bytes through, remembering how many and the last one. +const TeeWriter = struct { + a: *std.Io.Writer, + count: usize = 0, + final: u8 = 0, + writer: std.Io.Writer = .{ .vtable = &.{ .drain = drain }, .buffer = &.{} }, + + fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { + const t: *TeeWriter = @alignCast(@fieldParentPtr("writer", w)); + var n: usize = 0; + for (data[0 .. data.len - 1]) |bytes| { + try t.a.writeAll(bytes); + t.note(bytes); + n += bytes.len; + } + const pattern = data[data.len - 1]; + for (0..splat) |_| { + try t.a.writeAll(pattern); + t.note(pattern); + n += pattern.len; + } + return n; + } + + fn note(t: *TeeWriter, bytes: []const u8) void { + if (bytes.len == 0) return; + t.count += bytes.len; + t.final = bytes[bytes.len - 1]; + } +}; + /// How long `screenTextAlloc` would be, counted without keeping it: the /// length a terminal's body reads as, which its ctl line reports. pub fn screenTextLen(pane: *Pane) usize { if (comptime !enabled) return 0; - const state = pane.terminal orelse return 0; - const screen = state.vt.screens.active; var buf: [256]u8 = undefined; var counting: std.Io.Writer.Discarding = .init(&buf); - screen.dumpString(&counting.writer, .{ - .tl = screen.pages.getTopLeft(.screen), - .br = screen.pages.getBottomRight(.screen) orelse return 0, - .unwrap = false, - }) catch return 0; + writeScreenText(pane, &counting.writer) catch return 0; return @intCast(counting.fullCount()); } -- cgit v1.3