// Host contract:// - fields: shadow_vt, transcript, entries, layout, viewport_top_row,// owned_top_row, pending_scroll_compact,// min_visible_viewport_rows, reflow_clear_guard_rows,// viewport_clear_pending,// replaceable_last_line, replaceable_row, replaceable_start,// last_rendered_cols, last_paint_bottom_reserved_rows,// last_visible_transcript_*, last_viewport_selection,// paint_trace, has_painted_transcript, resize_bottom_anchor_pending,// footer_viewport, full_transcript, folded_command_blocks// - methods: ensurePaintReservation, beginPaint, endPaint,// resetCursorTracking, advanceCursor,// compactAfterCollapsedResize, footerTopRowForExtra const std = @import("std");const debug_trace = @import("../../core/shared/debug_trace.zig");const types = @import("../../core/shared/types.zig");const command_output_content = @import("../../core/tooling/command_output_content.zig");const render_engine = @import("../render_engine.zig");const source_preparation = @import("source_preparation.zig");const transcript_release = @import("../../core/output/transcript_release.zig");const transcript_writer = @import("writer.zig");const vt_emulator = @import("../../core/terminal/engine.zig"); const Allocator = std.mem.Allocator;const Metrics = types.Metrics;const frame_surface = render_engine.frame_surface;const paint_plan = render_engine.paint_plan;const transcript_blocks = render_engine.transcript_blocks;const viewport_selection = render_engine.viewport_selection; const HardLineStarts = viewport_selection.HardLineStarts;const LineRef = viewport_selection.LineRef;const TranscriptBuffer = viewport_selection.TranscriptBuffer;const TranscriptRef = viewport_selection.TranscriptRef;const VisibleTranscriptLine = viewport_selection.VisibleTranscriptLine;const VisibleTranscriptBatch = viewport_selection.VisibleTranscriptBatch;const ViewportSelection = viewport_selection.ViewportSelection;const buildHardLineStarts = viewport_selection.buildHardLineStarts;const hardLineRefAt = viewport_selection.hardLineRefAt;const selectedSourceLineIndex = viewport_selection.selectedSourceLineIndex;const visibleFromTranscript = viewport_selection.visibleFromTranscript;const buildViewportSelection = viewport_selection.buildViewportSelection;const isVisuallyBlankLine = transcript_blocks.isVisuallyBlankLine;const nextCursorColForLine = transcript_blocks.nextCursorColForLine;const skipVisualRowsInLine = transcript_blocks.skipVisualRowsInLine;const stripTrailingNewline = transcript_blocks.stripTrailingNewline;const visualRowsForLine = transcript_blocks.visualRowsForLine; const blank_transcript_row_bytes = "\x1b[2K"; const ResizeHistoryPolicy = enum { apply, skip,}; fn rowForVisibleLineIndex( selection: ViewportSelection, line_visual_rows: []const u16, target_line: usize, max_row: u16,) ?u16 { if (target_line >= line_visual_rows.len) return null; if (selection.split_active) { const prefix_lines = selection.split_prefix_lines; const suffix_start = selection.split_suffix_start_line; var row = selection.top_row; var line_index: usize = 0; const target_visible = if (target_line < prefix_lines) target_line else blk: { if (target_line < suffix_start) return null; while (line_index < prefix_lines) : (line_index += 1) { row +|= line_visual_rows[line_index]; } line_index = suffix_start; break :blk target_line; }; while (line_index < target_visible) : (line_index += 1) { row +|= line_visual_rows[line_index]; } if (row > max_row) return null; return row; } if (target_line < selection.start_line) return null; var row = selection.top_row; var line_index = selection.start_line; var skip_rows = selection.partial_skip_rows; while (line_index < target_line) : (line_index += 1) { const rows = line_visual_rows[line_index]; if (skip_rows >= rows) { skip_rows -= rows; continue; } row +|= rows - skip_rows; skip_rows = 0; } if (row > max_row) return null; return row;} const RenderedTranscriptRow = struct { row: u16, line_index: usize, bytes: []const u8, rows_painted: u16, partial_skip_rows: u16, folded: bool, blank: bool,}; const RenderedTranscriptRows = struct { rows: std.ArrayList(RenderedTranscriptRow) = .empty, owned_bytes: std.ArrayList([]u8) = .empty, first_row: u16 = 0, last_visible_line: []const u8 = "", last_visible_row: u16 = 0, last_visible_row_blank: bool = false, last_line_is_folded: bool = false, replaceable_start_row: u16 = 0, replaceable_line_index: usize = 0, painted_rows: u16 = 0, fn deinit(self: *RenderedTranscriptRows, alloc: Allocator) void { for (self.owned_bytes.items) |bytes| alloc.free(bytes); self.owned_bytes.deinit(alloc); self.rows.deinit(alloc); } fn rememberOwnedBytes(self: *RenderedTranscriptRows, alloc: Allocator, bytes: []u8) ![]const u8 { errdefer alloc.free(bytes); try self.owned_bytes.append(alloc, bytes); return bytes; }}; fn applyRenderedRowsToSelection(selection: *ViewportSelection, rendered_rows: *const RenderedTranscriptRows) void { selection.last_visible_row = rendered_rows.last_visible_row; selection.last_visible_row_blank = rendered_rows.last_visible_row_blank; selection.replaceable_start_row = rendered_rows.replaceable_start_row; if (rendered_rows.last_visible_row > 0) { selection.bottom_row = rendered_rows.last_visible_row; }} const ViewportCursorResult = struct { cursor_row: u16, cursor_col: u16, replaceable_row: u16,}; const TranscriptPreparationSource = source_preparation.TranscriptPreparationSource; pub const MeasuredHistoryEndpoint = struct { span_start_flow_offset: usize, flow_offset: usize, hard_row_cols: u16,}; pub const MeasuredHistoryOrigin = struct { source_cols: u16, source_visual_offset: u32, endpoint: ?MeasuredHistoryEndpoint = null,}; pub const MeasuredHistoryOriginProjection = struct { origin: MeasuredHistoryOrigin, current_cols: u16,}; pub const MeasuredResizeTargetProvenance = enum { none, stable, recovering,}; fn renderTranscriptRows( self: anytype, alloc: Allocator, batch: VisibleTranscriptBatch, line_visual_rows: []const u16, total_lines: usize, start_line: usize, partial_skip_rows: u16, top_row: u16, bottom_row: u16, max_visual_rows: ?u16, projection_resume_bytes: []const u8, projection_start_flow_offset: ?usize,) !RenderedTranscriptRows { var rendered: RenderedTranscriptRows = .{ .first_row = top_row, .replaceable_start_row = top_row, .replaceable_line_index = total_lines, }; errdefer rendered.deinit(alloc); var current_row: u32 = top_row; var line_index: usize = start_line; var remaining_visual_rows = max_visual_rows; if (remaining_visual_rows == null and projection_start_flow_offset != null) { remaining_visual_rows = bottom_row - top_row + 1; } while (line_index < total_lines and current_row <= @as(u32, bottom_row) and (remaining_visual_rows == null or remaining_visual_rows.? > 0)) : (line_index += 1) { const vis = batch.lines[line_index]; const is_partial_tail = line_index == start_line and partial_skip_rows > 0; const line_rows = line_visual_rows[line_index]; std.debug.assert(!is_partial_tail or partial_skip_rows < line_rows); const exact_tail = if (line_index == start_line) switch (vis) { .transcript => |line| projectionTailFromFlowOffset( batch, line.ref.ref, projection_start_flow_offset, ), .folded_line => null, } else null; const available_rows: u16 = if (exact_tail) |tail| visualRowsForLine(tail, self.layout.cols) else if (is_partial_tail) line_rows - partial_skip_rows else line_rows; const rows_painted = if (remaining_visual_rows) |remaining| @min(available_rows, remaining) else available_rows; const projection_truncates_line = rows_painted < available_rows; const paint_row: u16 = @intCast(current_row); switch (vis) { .transcript => |line| { const text = batch.resolve(vis); const tail = exact_tail orelse transcriptRowBytes(self, text, is_partial_tail, partial_skip_rows); const visible_bytes = if (projection_truncates_line) tail[0..skipVisualRowsInLine(tail, self.layout.cols, rows_painted)] else tail; const paint_bytes = if (visible_bytes.len == 0) blank_transcript_row_bytes else if (isVisuallyBlankLine(visible_bytes)) blk: { const materialized = try std.mem.concat( alloc, u8, &.{ visible_bytes, blank_transcript_row_bytes }, ); break :blk try rendered.rememberOwnedBytes(alloc, materialized); } else visible_bytes; var emitted_bytes = paint_bytes; if (line_index == start_line and projection_resume_bytes.len > 0) { const resumed = try alloc.alloc( u8, projection_resume_bytes.len + paint_bytes.len, ); @memcpy(resumed[0..projection_resume_bytes.len], projection_resume_bytes); @memcpy(resumed[projection_resume_bytes.len..], paint_bytes); emitted_bytes = try rendered.rememberOwnedBytes(alloc, resumed); } const cursor_line = if (projection_truncates_line or exact_tail != null) visible_bytes else text; const blank = isVisuallyBlankLine(if (exact_tail != null) cursor_line else text); logRenderedTranscriptRow(paint_row, rows_painted, line_index, "transcript", if (is_partial_tail) partial_skip_rows else 0, text); try rendered.rows.append(alloc, .{ .row = paint_row, .line_index = line_index, .bytes = emitted_bytes, .rows_painted = rows_painted, .partial_skip_rows = if (is_partial_tail) partial_skip_rows else 0, .folded = false, .blank = blank, }); if (self.replaceable_last_line and line.ref.ref.start <= self.replaceable_start) { rendered.replaceable_line_index = line_index; } rendered.last_visible_line = cursor_line; rendered.last_line_is_folded = false; rendered.last_visible_row_blank = blank; }, .folded_line => |folded| { var folded_bytes = try foldedLineBytes(alloc, folded.text, folded.stream); var folded_bytes_needs_free = true; errdefer if (folded_bytes_needs_free) alloc.free(folded_bytes); const rows_touched = visualRowsForLine(folded_bytes, self.layout.cols); if (rows_touched < rows_painted) { const padded = try padAnsiBytesToRows(alloc, folded_bytes, rows_painted - rows_touched); alloc.free(folded_bytes); folded_bytes = padded; } folded_bytes_needs_free = false; const owned = try rendered.rememberOwnedBytes(alloc, folded_bytes); const tail = if (is_partial_tail) foldedPartialTailBytes(self, owned, partial_skip_rows) else owned; const bytes = if (projection_truncates_line) tail[0..skipVisualRowsInLine(tail, self.layout.cols, rows_painted)] else tail; logRenderedTranscriptRow( paint_row, rows_painted, line_index, if (folded.stream == .stderr) "folded_stderr" else "folded_stdout", if (is_partial_tail) partial_skip_rows else 0, folded.text, ); try rendered.rows.append(alloc, .{ .row = paint_row, .line_index = line_index, .bytes = if (bytes.len == 0) blank_transcript_row_bytes else bytes, .rows_painted = rows_painted, .partial_skip_rows = if (is_partial_tail) partial_skip_rows else 0, .folded = true, .blank = false, }); rendered.last_visible_line = if (projection_truncates_line) bytes else ""; rendered.last_line_is_folded = !projection_truncates_line; rendered.last_visible_row_blank = false; }, } if (line_index == rendered.replaceable_line_index) { rendered.replaceable_start_row = paint_row; } rendered.last_visible_row = @intCast( current_row + @as(u32, rows_painted) - 1, ); rendered.painted_rows += rows_painted; current_row += @as(u32, rows_painted); if (remaining_visual_rows) |*remaining| remaining.* -= rows_painted; } return rendered;} fn projectionTailFromFlowOffset( batch: VisibleTranscriptBatch, line: LineRef, flow_offset: ?usize,) ?[]const u8 { const offset = flow_offset orelse return null; const start: usize = line.start; const end = start + @as(usize, line.len); if (offset < start or offset > end) return null; return batch.transcript.bytes[offset..end];} fn transcriptRowBytes(self: anytype, text: []const u8, is_partial_tail: bool, partial_skip_rows: u16) []const u8 { if (!is_partial_tail or text.len == 0) return text; const offset = skipVisualRowsInLine(text, self.layout.cols, partial_skip_rows); const tail_bytes: usize = if (offset < text.len) text.len - offset else 0; debug_trace.logf( "transcript.sgr_drop_partial_tail", "active sgr before skip offset not walked, rows_skipped={d} cols={d} tail_bytes={d}", .{ partial_skip_rows, self.layout.cols, tail_bytes }, ); return if (offset < text.len) text[offset..] else "";} fn foldedPartialTailBytes(self: anytype, bytes: []const u8, partial_skip_rows: u16) []const u8 { if (partial_skip_rows == 0 or bytes.len == 0) return bytes; const offset = skipVisualRowsInLine(bytes, self.layout.cols, partial_skip_rows); return if (offset < bytes.len) bytes[offset..] else "";} fn foldedLineBytes(alloc: Allocator, text: []const u8, stream: command_output_content.Stream) ![]u8 { const style = if (stream == .stderr) "\x1b[38;5;252m" else "\x1b[38;5;245m"; const trimmed = stripTrailingNewline(text); const reset = "\x1b[0m"; const bytes = try alloc.alloc(u8, style.len + "│ ".len + trimmed.len + reset.len); var offset: usize = 0; @memcpy(bytes[offset .. offset + style.len], style); offset += style.len; @memcpy(bytes[offset .. offset + "│ ".len], "│ "); offset += "│ ".len; @memcpy(bytes[offset .. offset + trimmed.len], trimmed); offset += trimmed.len; @memcpy(bytes[offset .. offset + reset.len], reset); return bytes;} fn padAnsiBytesToRows(alloc: Allocator, bytes: []const u8, missing_rows: u16) ![]u8 { std.debug.assert(missing_rows > 0); return std.fmt.allocPrint(alloc, "{s}\x1b[{d}B\x1b[2K", .{ bytes, missing_rows });} fn logRenderedTranscriptRow(row: u16, rows_painted: u16, line_index: usize, kind_name: []const u8, partial_skip_rows: u16, text: []const u8) void { debug_trace.logf( "paint.rows", "transcript_line row={d} rows={d} line_index={d} kind={s} partial_skip={d} bytes={d} hash={x}", .{ row, rows_painted, line_index, kind_name, partial_skip_rows, text.len, std.hash.Wyhash.hash(0, text), }, );} fn calculateViewportCursor( layout: types.Layout, bottom_row: u16, total_lines: usize, start_line: usize, repl_line_idx: usize, replaceable_start_row: u16, last_visible_line: []const u8, last_visible_row: u16, last_line_is_folded: bool, top_row: u16, transcript_ends_with_newline: bool, replaceable_last_line: bool, current_replaceable_row: u16,) ViewportCursorResult { _ = bottom_row; var cursor_row = top_row; var cursor_col: u16 = 1; var replaceable_row = current_replaceable_row; if (total_lines == 0) return .{ .cursor_row = cursor_row, .cursor_col = cursor_col, .replaceable_row = replaceable_row }; const treat_last_row_as_terminated = last_line_is_folded or transcript_ends_with_newline; if (treat_last_row_as_terminated and last_visible_row < layout.content_bottom) { cursor_row = last_visible_row + 1; cursor_col = 1; } else { cursor_row = last_visible_row; cursor_col = nextCursorColForLine(last_visible_line, layout.cols); } if (cursor_row < 1) cursor_row = 1; if (cursor_col < 1) cursor_col = 1; if (replaceable_last_line) { replaceable_row = if (repl_line_idx < start_line or repl_line_idx >= total_lines) top_row else replaceable_start_row; } return .{ .cursor_row = cursor_row, .cursor_col = cursor_col, .replaceable_row = replaceable_row };} fn recordPreFrameTranscriptInvalidation( self: anytype, reason: paint_plan.FrameInvalidationReason, top: u16, bottom: u16,) void { if (top == 0 or bottom == 0 or bottom < top) return; const Shell = @TypeOf(self.*); if (comptime !@hasDecl(Shell, "recordFrameInvalidation")) return; self.recordFrameInvalidation(.{ .reason = reason, .top = top, .bottom = bottom }) catch |err| { debug_trace.logf( "frame_plan", "pre_frame_transcript_invalidation_drop reason={s} top={d} bottom={d} err={s}", .{ @tagName(reason), top, bottom, @errorName(err) }, ); };} fn compactAfterCollapsedResize(self: anytype, metrics: *Metrics) !void { _ = metrics; if (!self.pending_scroll_compact) return; self.pending_scroll_compact = false; if (self.owned_top_row <= 1) return; const scroll_rows = self.owned_top_row - 1; debug_trace.logf("render", "viewport_scroll_compact rows={d} owned_top={d} layout={d}x{d}", .{ scroll_rows, self.owned_top_row, self.layout.cols, self.layout.rows }); self.owned_top_row = 1; self.viewport_top_row = 1; self.cursor_row = 1; self.cursor_col = 1; recordPreFrameTranscriptInvalidation(self, .viewport_reanchor, 1, self.layout.rows); debug_trace.logf( "scroll", "viewport_scroll_compact_frame_deferred rows={d} layout={d}x{d}", .{ scroll_rows, self.layout.cols, self.layout.rows }, );} fn visualOffsetForSelection(line_visual_rows: []const u16, selection: ViewportSelection) u32 { var offset: u32 = 0; const end = @min(selection.start_line, line_visual_rows.len); for (line_visual_rows[0..end]) |rows| offset += rows; return offset + selection.partial_skip_rows;} fn totalVisualRows(line_visual_rows: []const u16) u32 { var total: u32 = 0;Lines 1–500 of 4250. Load more
// Host contract: // - fields: shadow_vt, transcript, entries, layout, viewport_top_row, // owned_top_row, pending_scroll_compact, // min_visible_viewport_rows, reflow_clear_guard_rows, // viewport_clear_pending, // replaceable_last_line, replaceable_row, replaceable_start, // last_rendered_cols, last_paint_bottom_reserved_rows, // last_visible_transcript_*, last_viewport_selection, // paint_trace, has_painted_transcript, resize_bottom_anchor_pending, // footer_viewport, full_transcript, folded_command_blocks // - methods: ensurePaintReservation, beginPaint, endPaint, // resetCursorTracking, advanceCursor, // compactAfterCollapsedResize, footerTopRowForExtra const std = @import("std"); const debug_trace = @import("../../core/shared/debug_trace.zig"); const types = @import("../../core/shared/types.zig"); const command_output_content = @import("../../core/tooling/command_output_content.zig"); const render_engine = @import("../render_engine.zig"); const source_preparation = @import("source_preparation.zig"); const transcript_release = @import("../../core/output/transcript_release.zig"); const transcript_writer = @import("writer.zig"); const vt_emulator = @import("../../core/terminal/engine.zig"); const Allocator = std.mem.Allocator; const Metrics = types.Metrics; const frame_surface = render_engine.frame_surface; const paint_plan = render_engine.paint_plan; const transcript_blocks = render_engine.transcript_blocks; const viewport_selection = render_engine.viewport_selection; const HardLineStarts = viewport_selection.HardLineStarts; const LineRef = viewport_selection.LineRef; const TranscriptBuffer = viewport_selection.TranscriptBuffer; const TranscriptRef = viewport_selection.TranscriptRef; const VisibleTranscriptLine = viewport_selection.VisibleTranscriptLine; const VisibleTranscriptBatch = viewport_selection.VisibleTranscriptBatch; const ViewportSelection = viewport_selection.ViewportSelection; const buildHardLineStarts = viewport_selection.buildHardLineStarts; const hardLineRefAt = viewport_selection.hardLineRefAt; const selectedSourceLineIndex = viewport_selection.selectedSourceLineIndex; const visibleFromTranscript = viewport_selection.visibleFromTranscript; const buildViewportSelection = viewport_selection.buildViewportSelection; const isVisuallyBlankLine = transcript_blocks.isVisuallyBlankLine; const nextCursorColForLine = transcript_blocks.nextCursorColForLine; const skipVisualRowsInLine = transcript_blocks.skipVisualRowsInLine; const stripTrailingNewline = transcript_blocks.stripTrailingNewline; const visualRowsForLine = transcript_blocks.visualRowsForLine; const blank_transcript_row_bytes = "\x1b[2K"; const ResizeHistoryPolicy = enum { apply, skip, }; fn rowForVisibleLineIndex( selection: ViewportSelection, line_visual_rows: []const u16, target_line: usize, max_row: u16, ) ?u16 { if (target_line >= line_visual_rows.len) return null; if (selection.split_active) { const prefix_lines = selection.split_prefix_lines; const suffix_start = selection.split_suffix_start_line; var row = selection.top_row; var line_index: usize = 0; const target_visible = if (target_line < prefix_lines) target_line else blk: { if (target_line < suffix_start) return null; while (line_index < prefix_lines) : (line_index += 1) { row +|= line_visual_rows[line_index]; } line_index = suffix_start; break :blk target_line; }; while (line_index < target_visible) : (line_index += 1) { row +|= line_visual_rows[line_index]; } if (row > max_row) return null; return row; } if (target_line < selection.start_line) return null; var row = selection.top_row; var line_index = selection.start_line; var skip_rows = selection.partial_skip_rows; while (line_index < target_line) : (line_index += 1) { const rows = line_visual_rows[line_index]; if (skip_rows >= rows) { skip_rows -= rows; continue; } row +|= rows - skip_rows; skip_rows = 0; } if (row > max_row) return null; return row; } const RenderedTranscriptRow = struct { row: u16, line_index: usize, bytes: []const u8, rows_painted: u16, partial_skip_rows: u16, folded: bool, blank: bool, }; const RenderedTranscriptRows = struct { rows: std.ArrayList(RenderedTranscriptRow) = .empty, owned_bytes: std.ArrayList([]u8) = .empty, first_row: u16 = 0, last_visible_line: []const u8 = "", last_visible_row: u16 = 0, last_visible_row_blank: bool = false, last_line_is_folded: bool = false, replaceable_start_row: u16 = 0, replaceable_line_index: usize = 0, painted_rows: u16 = 0, fn deinit(self: *RenderedTranscriptRows, alloc: Allocator) void { for (self.owned_bytes.items) |bytes| alloc.free(bytes); self.owned_bytes.deinit(alloc); self.rows.deinit(alloc); } fn rememberOwnedBytes(self: *RenderedTranscriptRows, alloc: Allocator, bytes: []u8) ![]const u8 { errdefer alloc.free(bytes); try self.owned_bytes.append(alloc, bytes); return bytes; } }; fn applyRenderedRowsToSelection(selection: *ViewportSelection, rendered_rows: *const RenderedTranscriptRows) void { selection.last_visible_row = rendered_rows.last_visible_row; selection.last_visible_row_blank = rendered_rows.last_visible_row_blank; selection.replaceable_start_row = rendered_rows.replaceable_start_row; if (rendered_rows.last_visible_row > 0) { selection.bottom_row = rendered_rows.last_visible_row; } } const ViewportCursorResult = struct { cursor_row: u16, cursor_col: u16, replaceable_row: u16, }; const TranscriptPreparationSource = source_preparation.TranscriptPreparationSource; pub const MeasuredHistoryEndpoint = struct { span_start_flow_offset: usize, flow_offset: usize, hard_row_cols: u16, }; pub const MeasuredHistoryOrigin = struct { source_cols: u16, source_visual_offset: u32, endpoint: ?MeasuredHistoryEndpoint = null, }; pub const MeasuredHistoryOriginProjection = struct { origin: MeasuredHistoryOrigin, current_cols: u16, }; pub const MeasuredResizeTargetProvenance = enum { none, stable, recovering, }; fn renderTranscriptRows( self: anytype, alloc: Allocator, batch: VisibleTranscriptBatch, line_visual_rows: []const u16, total_lines: usize, start_line: usize, partial_skip_rows: u16, top_row: u16, bottom_row: u16, max_visual_rows: ?u16, projection_resume_bytes: []const u8, projection_start_flow_offset: ?usize, ) !RenderedTranscriptRows { var rendered: RenderedTranscriptRows = .{ .first_row = top_row, .replaceable_start_row = top_row, .replaceable_line_index = total_lines, }; errdefer rendered.deinit(alloc); var current_row: u32 = top_row; var line_index: usize = start_line; var remaining_visual_rows = max_visual_rows; if (remaining_visual_rows == null and projection_start_flow_offset != null) { remaining_visual_rows = bottom_row - top_row + 1; } while (line_index < total_lines and current_row <= @as(u32, bottom_row) and (remaining_visual_rows == null or remaining_visual_rows.? > 0)) : (line_index += 1) { const vis = batch.lines[line_index]; const is_partial_tail = line_index == start_line and partial_skip_rows > 0; const line_rows = line_visual_rows[line_index]; std.debug.assert(!is_partial_tail or partial_skip_rows < line_rows); const exact_tail = if (line_index == start_line) switch (vis) { .transcript => |line| projectionTailFromFlowOffset( batch, line.ref.ref, projection_start_flow_offset, ), .folded_line => null, } else null; const available_rows: u16 = if (exact_tail) |tail| visualRowsForLine(tail, self.layout.cols) else if (is_partial_tail) line_rows - partial_skip_rows else line_rows; const rows_painted = if (remaining_visual_rows) |remaining| @min(available_rows, remaining) else available_rows; const projection_truncates_line = rows_painted < available_rows; const paint_row: u16 = @intCast(current_row); switch (vis) { .transcript => |line| { const text = batch.resolve(vis); const tail = exact_tail orelse transcriptRowBytes(self, text, is_partial_tail, partial_skip_rows); const visible_bytes = if (projection_truncates_line) tail[0..skipVisualRowsInLine(tail, self.layout.cols, rows_painted)] else tail; const paint_bytes = if (visible_bytes.len == 0) blank_transcript_row_bytes else if (isVisuallyBlankLine(visible_bytes)) blk: { const materialized = try std.mem.concat( alloc, u8, &.{ visible_bytes, blank_transcript_row_bytes }, ); break :blk try rendered.rememberOwnedBytes(alloc, materialized); } else visible_bytes; var emitted_bytes = paint_bytes; if (line_index == start_line and projection_resume_bytes.len > 0) { const resumed = try alloc.alloc( u8, projection_resume_bytes.len + paint_bytes.len, ); @memcpy(resumed[0..projection_resume_bytes.len], projection_resume_bytes); @memcpy(resumed[projection_resume_bytes.len..], paint_bytes); emitted_bytes = try rendered.rememberOwnedBytes(alloc, resumed); } const cursor_line = if (projection_truncates_line or exact_tail != null) visible_bytes else text; const blank = isVisuallyBlankLine(if (exact_tail != null) cursor_line else text); logRenderedTranscriptRow(paint_row, rows_painted, line_index, "transcript", if (is_partial_tail) partial_skip_rows else 0, text); try rendered.rows.append(alloc, .{ .row = paint_row, .line_index = line_index, .bytes = emitted_bytes, .rows_painted = rows_painted, .partial_skip_rows = if (is_partial_tail) partial_skip_rows else 0, .folded = false, .blank = blank, }); if (self.replaceable_last_line and line.ref.ref.start <= self.replaceable_start) { rendered.replaceable_line_index = line_index; } rendered.last_visible_line = cursor_line; rendered.last_line_is_folded = false; rendered.last_visible_row_blank = blank; }, .folded_line => |folded| { var folded_bytes = try foldedLineBytes(alloc, folded.text, folded.stream); var folded_bytes_needs_free = true; errdefer if (folded_bytes_needs_free) alloc.free(folded_bytes); const rows_touched = visualRowsForLine(folded_bytes, self.layout.cols); if (rows_touched < rows_painted) { const padded = try padAnsiBytesToRows(alloc, folded_bytes, rows_painted - rows_touched); alloc.free(folded_bytes); folded_bytes = padded; } folded_bytes_needs_free = false; const owned = try rendered.rememberOwnedBytes(alloc, folded_bytes); const tail = if (is_partial_tail) foldedPartialTailBytes(self, owned, partial_skip_rows) else owned; const bytes = if (projection_truncates_line) tail[0..skipVisualRowsInLine(tail, self.layout.cols, rows_painted)] else tail; logRenderedTranscriptRow( paint_row, rows_painted, line_index, if (folded.stream == .stderr) "folded_stderr" else "folded_stdout", if (is_partial_tail) partial_skip_rows else 0, folded.text, ); try rendered.rows.append(alloc, .{ .row = paint_row, .line_index = line_index, .bytes = if (bytes.len == 0) blank_transcript_row_bytes else bytes, .rows_painted = rows_painted, .partial_skip_rows = if (is_partial_tail) partial_skip_rows else 0, .folded = true, .blank = false, }); rendered.last_visible_line = if (projection_truncates_line) bytes else ""; rendered.last_line_is_folded = !projection_truncates_line; rendered.last_visible_row_blank = false; }, } if (line_index == rendered.replaceable_line_index) { rendered.replaceable_start_row = paint_row; } rendered.last_visible_row = @intCast( current_row + @as(u32, rows_painted) - 1, ); rendered.painted_rows += rows_painted; current_row += @as(u32, rows_painted); if (remaining_visual_rows) |*remaining| remaining.* -= rows_painted; } return rendered; } fn projectionTailFromFlowOffset( batch: VisibleTranscriptBatch, line: LineRef, flow_offset: ?usize, ) ?[]const u8 { const offset = flow_offset orelse return null; const start: usize = line.start; const end = start + @as(usize, line.len); if (offset < start or offset > end) return null; return batch.transcript.bytes[offset..end]; } fn transcriptRowBytes(self: anytype, text: []const u8, is_partial_tail: bool, partial_skip_rows: u16) []const u8 { if (!is_partial_tail or text.len == 0) return text; const offset = skipVisualRowsInLine(text, self.layout.cols, partial_skip_rows); const tail_bytes: usize = if (offset < text.len) text.len - offset else 0; debug_trace.logf( "transcript.sgr_drop_partial_tail", "active sgr before skip offset not walked, rows_skipped={d} cols={d} tail_bytes={d}", .{ partial_skip_rows, self.layout.cols, tail_bytes }, ); return if (offset < text.len) text[offset..] else ""; } fn foldedPartialTailBytes(self: anytype, bytes: []const u8, partial_skip_rows: u16) []const u8 { if (partial_skip_rows == 0 or bytes.len == 0) return bytes; const offset = skipVisualRowsInLine(bytes, self.layout.cols, partial_skip_rows); return if (offset < bytes.len) bytes[offset..] else ""; } fn foldedLineBytes(alloc: Allocator, text: []const u8, stream: command_output_content.Stream) ![]u8 { const style = if (stream == .stderr) "\x1b[38;5;252m" else "\x1b[38;5;245m"; const trimmed = stripTrailingNewline(text); const reset = "\x1b[0m"; const bytes = try alloc.alloc(u8, style.len + "│ ".len + trimmed.len + reset.len); var offset: usize = 0; @memcpy(bytes[offset .. offset + style.len], style); offset += style.len; @memcpy(bytes[offset .. offset + "│ ".len], "│ "); offset += "│ ".len; @memcpy(bytes[offset .. offset + trimmed.len], trimmed); offset += trimmed.len; @memcpy(bytes[offset .. offset + reset.len], reset); return bytes; } fn padAnsiBytesToRows(alloc: Allocator, bytes: []const u8, missing_rows: u16) ![]u8 { std.debug.assert(missing_rows > 0); return std.fmt.allocPrint(alloc, "{s}\x1b[{d}B\x1b[2K", .{ bytes, missing_rows }); } fn logRenderedTranscriptRow(row: u16, rows_painted: u16, line_index: usize, kind_name: []const u8, partial_skip_rows: u16, text: []const u8) void { debug_trace.logf( "paint.rows", "transcript_line row={d} rows={d} line_index={d} kind={s} partial_skip={d} bytes={d} hash={x}", .{ row, rows_painted, line_index, kind_name, partial_skip_rows, text.len, std.hash.Wyhash.hash(0, text), }, ); } fn calculateViewportCursor( layout: types.Layout, bottom_row: u16, total_lines: usize, start_line: usize, repl_line_idx: usize, replaceable_start_row: u16, last_visible_line: []const u8, last_visible_row: u16, last_line_is_folded: bool, top_row: u16, transcript_ends_with_newline: bool, replaceable_last_line: bool, current_replaceable_row: u16, ) ViewportCursorResult { _ = bottom_row; var cursor_row = top_row; var cursor_col: u16 = 1; var replaceable_row = current_replaceable_row; if (total_lines == 0) return .{ .cursor_row = cursor_row, .cursor_col = cursor_col, .replaceable_row = replaceable_row }; const treat_last_row_as_terminated = last_line_is_folded or transcript_ends_with_newline; if (treat_last_row_as_terminated and last_visible_row < layout.content_bottom) { cursor_row = last_visible_row + 1; cursor_col = 1; } else { cursor_row = last_visible_row; cursor_col = nextCursorColForLine(last_visible_line, layout.cols); } if (cursor_row < 1) cursor_row = 1; if (cursor_col < 1) cursor_col = 1; if (replaceable_last_line) { replaceable_row = if (repl_line_idx < start_line or repl_line_idx >= total_lines) top_row else replaceable_start_row; } return .{ .cursor_row = cursor_row, .cursor_col = cursor_col, .replaceable_row = replaceable_row }; } fn recordPreFrameTranscriptInvalidation( self: anytype, reason: paint_plan.FrameInvalidationReason, top: u16, bottom: u16, ) void { if (top == 0 or bottom == 0 or bottom < top) return; const Shell = @TypeOf(self.*); if (comptime !@hasDecl(Shell, "recordFrameInvalidation")) return; self.recordFrameInvalidation(.{ .reason = reason, .top = top, .bottom = bottom }) catch |err| { debug_trace.logf( "frame_plan", "pre_frame_transcript_invalidation_drop reason={s} top={d} bottom={d} err={s}", .{ @tagName(reason), top, bottom, @errorName(err) }, ); }; } fn compactAfterCollapsedResize(self: anytype, metrics: *Metrics) !void { _ = metrics; if (!self.pending_scroll_compact) return; self.pending_scroll_compact = false; if (self.owned_top_row <= 1) return; const scroll_rows = self.owned_top_row - 1; debug_trace.logf("render", "viewport_scroll_compact rows={d} owned_top={d} layout={d}x{d}", .{ scroll_rows, self.owned_top_row, self.layout.cols, self.layout.rows }); self.owned_top_row = 1; self.viewport_top_row = 1; self.cursor_row = 1; self.cursor_col = 1; recordPreFrameTranscriptInvalidation(self, .viewport_reanchor, 1, self.layout.rows); debug_trace.logf( "scroll", "viewport_scroll_compact_frame_deferred rows={d} layout={d}x{d}", .{ scroll_rows, self.layout.cols, self.layout.rows }, ); } fn visualOffsetForSelection(line_visual_rows: []const u16, selection: ViewportSelection) u32 { var offset: u32 = 0; const end = @min(selection.start_line, line_visual_rows.len); for (line_visual_rows[0..end]) |rows| offset += rows; return offset + selection.partial_skip_rows; } fn totalVisualRows(line_visual_rows: []const u16) u32 { var total: u32 = 0;