summaryrefslogtreecommitdiff
path: root/src/Terminal.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 00:45:33 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commite563be42a5b3e9243e2c01d34cf8e94d2047b753 (patch)
treed71c8265c353f41cdb49b20f3a7e4d6e8d9baab2 /src/Terminal.zig
parent0fe78621cc0db8b835ebcb7c51bbd2d20d6dbcc2 (diff)
downloadpardes-e563be42a5b3e9243e2c01d34cf8e94d2047b753.tar.gz
pardes-e563be42a5b3e9243e2c01d34cf8e94d2047b753.zip
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 <[email protected]>
Diffstat (limited to 'src/Terminal.zig')
-rw-r--r--src/Terminal.zig60
1 files changed, 50 insertions, 10 deletions
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());
}