// Host contract: // - fields: transcript, entries, max_transcript_bytes, replaceable_last_line, // replaceable_start, cursor_row, cursor_col, layout, viewport_top_row, // owned_top_row, pending_scroll_compact, // has_painted_transcript, transcript_band_dirty, last_rendered_cols, // transcript_cache_origin_untrimmed // - methods: forgetShimmer, assertCanMutateTranscript, advanceCursor, // ensurePaintReservation, rebuildTranscriptCacheFromEntries, // recomputeCursorFromTranscript // This module owns transcript writes plus scroll coordination during writes. const std = @import("std"); const debug_trace = @import("../../core/shared/debug_trace.zig"); const types = @import("../../core/shared/types.zig"); const render_engine = @import("../render_engine.zig"); const transcript_store = @import("store.zig"); const Allocator = std.mem.Allocator; const Metrics = types.Metrics; const transcript_blocks = render_engine.transcript_blocks; const RawEntryClass = transcript_blocks.RawEntryClass; fn requestTranscriptPaint( self: anytype, content_changed: bool, dirty_entry_id: ?u32, ) void { const Runtime = @TypeOf(self.*); if (content_changed) { if (dirty_entry_id) |entry_id| { if (comptime @hasDecl(Runtime, "markTranscriptContentDirtyFrom")) { self.markTranscriptContentDirtyFrom(entry_id); return; } } if (comptime @hasDecl(Runtime, "markTranscriptContentDirty")) { self.markTranscriptContentDirty(); return; } } if (comptime @hasDecl(Runtime, "markTranscriptDirty")) { self.markTranscriptDirty(); } else { self.transcript_band_dirty = true; } } pub fn writeTranscriptBytes(self: anytype, alloc: Allocator, metrics: *Metrics, text: []const u8, record: bool) !void { return writeTranscriptBytesInternal(self, alloc, metrics, text, record, null); } pub fn writeTranscriptBytesFrom( self: anytype, alloc: Allocator, metrics: *Metrics, text: []const u8, record: bool, entry_id: u32, ) !void { return writeTranscriptBytesInternal(self, alloc, metrics, text, record, entry_id); } fn writeTranscriptBytesInternal( self: anytype, alloc: Allocator, metrics: *Metrics, text: []const u8, record: bool, dirty_entry_id: ?u32, ) !void { if (text.len == 0) return; _ = metrics; self.forgetShimmer(); // `record` gates transcript-buffer and replaceable-state // bookkeeping only. Cursor geometry still advances so frame // preparation sees the same pending layout as recorded writes. if (record) { try self.assertCanMutateTranscript(); self.transcript_cache_origin_untrimmed = false; _ = try transcript_store.appendCappedWithinCapacity( &self.transcript, alloc, text, self.max_transcript_bytes, &self.replaceable_last_line, &self.replaceable_start, ); if (self.replaceable_last_line) { debug_trace.logf("transcript.replaceable_flip", "writeTranscriptBytes appended non-replaceable, text_bytes={d}", .{text.len}); } self.replaceable_last_line = false; self.replaceable_start = 0; } // Cursor tracking stays so the pre-paint heuristics in main.zig // (e.g. the `cursor_col != 1` leading-newline test) still see // sensible values between ticks. The paint pass will // authoritatively overwrite these when it runs. const cursor_row_before: u32 = @as(u32, self.cursor_row); const walk = self.advanceCursor(text); std.debug.assert(walk.end_row >= cursor_row_before); // Viewport selection converts overflow into frame-owned scroll. const content_bottom_u32: u32 = @as(u32, self.layout.content_bottom); if (walk.end_row > content_bottom_u32) { const total_scroll_u32 = walk.end_row - content_bottom_u32; const total_scroll: u16 = @intCast(@min(total_scroll_u32, @as(u32, std.math.maxInt(u16)))); self.cursor_row = self.layout.content_bottom; debug_trace.logf( "scroll", "cursor_overflow_deferred_to_selection total_scroll={d} viewport_top={d} owned_top={d} cursor_row_after={d} walk_end_row={d} text_bytes={d} has_painted={s}", .{ total_scroll, self.viewport_top_row, self.owned_top_row, self.cursor_row, walk.end_row, text.len, if (self.has_painted_transcript) "true" else "false" }, ); } requestTranscriptPaint(self, record, dirty_entry_id); } fn recordTranscriptFrameInvalidation(self: anytype, reason: render_engine.paint_plan.FrameInvalidationReason) void { if (self.layout.rows == 0) return; const Shell = @TypeOf(self.*); if (comptime !@hasDecl(Shell, "recordFrameInvalidation")) return; const top = if (self.owned_top_row == 0 or self.owned_top_row > self.layout.rows) @as(u16, 1) else self.owned_top_row; self.recordFrameInvalidation(.{ .reason = reason, .top = top, .bottom = self.layout.rows, }) catch |err| { debug_trace.logf( "frame_plan", "transcript_invalidation_drop reason={s} top={d} bottom={d} err={s}", .{ @tagName(reason), top, self.layout.rows, @errorName(err) }, ); }; } pub fn writeTranscript(self: anytype, alloc: Allocator, metrics: *Metrics, text: []const u8, record: bool) !void { _ = try writeTranscriptReturningEntryIdClassified(self, alloc, metrics, text, record, .unknown_raw); } pub fn writeTranscriptClassified( self: anytype, alloc: Allocator, metrics: *Metrics, text: []const u8, record: bool, class: RawEntryClass, ) !void { _ = try writeTranscriptReturningEntryIdClassified(self, alloc, metrics, text, record, class); } pub fn writeTranscriptReturningEntryIdClassified( self: anytype, alloc: Allocator, metrics: *Metrics, text: []const u8, record: bool, class: RawEntryClass, ) !u32 { if (text.len == 0) return 0; if (!record) { try self.writeTranscriptBytes(alloc, metrics, text, false); return 0; } const entry_id = try transcript_store.writeRecordedTranscriptClassifiedAtomic( self, alloc, metrics, text, class, ); self.forgetShimmer(); return entry_id; } pub fn reconstructivePaint(self: anytype, alloc: Allocator, metrics: *Metrics) !void { _ = metrics; debug_trace.logf("reconstructive", "enter cols={d} rows={d} content_bottom={d} entries={d} cap={d}", .{ self.layout.cols, self.layout.rows, self.layout.content_bottom, self.entries.items.len, self.max_transcript_bytes }); try self.ensurePaintReservation(alloc); try self.rebuildTranscriptCacheFromEntries(alloc, "reconstructive paint"); self.recomputeCursorFromTranscript(); self.pending_scroll_compact = false; self.invalidateTranscriptAnchor("reconstructive_paint"); self.viewport_clear_pending = true; requestTranscriptPaint(self, false, null); recordTranscriptFrameInvalidation(self, .diagnostic_wipe); debug_trace.logf( "frame_plan", "reconstructive_paint_frame_deferred rows={d} entries={d}", .{ self.layout.rows, self.entries.items.len }, ); debug_trace.logf("reconstructive", "exit cursor={d},{d} viewport_top={d} transcript_bytes={d} replaceable={s}", .{ self.cursor_row, self.cursor_col, self.viewport_top_row, self.transcript.items.len, if (self.replaceable_last_line) "true" else "false" }); }