//! Bounded text-terminal engine shared by live terminal sessions, recovery,//! replay, and deterministic rendering tests. Graphical terminal protocols//! are intentionally outside this module's contract. const std = @import("std");const display_width = @import("../shared/display_width.zig");const contracts = @import("contracts.zig"); const Allocator = std.mem.Allocator; pub const checkpoint_schema_revision: u16 = 1;const checkpoint_magic = "FXTE";pub const max_reply_effects: usize = 16;pub const max_reply_effect_bytes: usize = 256;pub const max_reply_effect_total_bytes: usize = 4096;const max_csi_params: usize = 16;const max_csi_intermediates: usize = 2;const max_string_bytes: usize = 4096;const max_sync_bytes: usize = 1024 * 1024;const max_pool_entries: usize = 65_535;const max_hyperlink_pool_bytes: usize = 4 * 1024 * 1024;const max_combining_pool_bytes: usize = 4 * 1024 * 1024; pub const FeedMode = enum { native_live, tmux_live, journal_replay,}; pub const ProtocolReply = struct { bytes: []u8,}; pub const FeedResult = struct { stats: FeedStats = .{}, replies: std.ArrayList(ProtocolReply) = .empty, reply_bytes: usize = 0, pub fn deinit(self: *FeedResult, alloc: Allocator) void { for (self.replies.items) |reply| alloc.free(reply.bytes); self.replies.deinit(alloc); self.* = undefined; }}; pub const Color = union(enum) { default, /// Indexed palette colour (0..255) from `\x1b[3Nm`, `\x1b[4Nm`, /// `\x1b[9Nm`, `\x1b[10Nm`, or `\x1b[38;5;N` / `\x1b[48;5;N`. indexed: u8, /// Truecolor from `\x1b[38;2;R;G;Bm` / `\x1b[48;2;R;G;Bm`. rgb: struct { r: u8, g: u8, b: u8 }, pub fn eql(a: Color, b: Color) bool { return switch (a) { .default => b == .default, .indexed => |ai| switch (b) { .indexed => |bi| ai == bi, else => false, }, .rgb => |ar| switch (b) { .rgb => |br| ar.r == br.r and ar.g == br.g and ar.b == br.b, else => false, }, }; }}; pub const StyleFlags = packed struct(u8) { bold: bool = false, dim: bool = false, italic: bool = false, underline: bool = false, reverse: bool = false, strike: bool = false, _pad: u2 = 0, pub fn eql(a: StyleFlags, b: StyleFlags) bool { return @as(u8, @bitCast(a)) == @as(u8, @bitCast(b)); }}; pub const Style = struct { fg: Color = .default, bg: Color = .default, flags: StyleFlags = .{}, /// Index into `Grid.hyperlink_pool`. `0` means no OSC 8 hyperlink; /// IDs `>= 1` map to a URL string the cell should be wrapped with /// when re-emitted by `diffBand`. IDs are grid-local; they survive /// `Grid.clone` because the pool is dup'd in lockstep. hyperlink_id: u32 = 0, pub fn eql(a: Style, b: Style) bool { return a.fg.eql(b.fg) and a.bg.eql(b.bg) and a.flags.eql(b.flags) and a.hyperlink_id == b.hyperlink_id; }}; pub const Cell = struct { /// Unicode codepoint in this cell. Zero for the trailing half of a /// wide (double-width) cell so callers can tell single from wide. codepoint: u21 = ' ', width: u8 = 1, /// Index into `Grid.combining_suffix_pool`. `0` means the lead /// codepoint has no remaining display-unit UTF-8 bytes. combining_suffix_id: u32 = 0, /// Style applied to this cell. Blank cells from erasures inherit /// the current bg so a `\x1b[48;5;236m\x1b[K` clears into the /// configured background, matching real terminals. style: Style = .{},}; pub const FeedStats = struct { max_row_touched: u16 = 0, scrolled: bool = false, scroll_rows: u16 = 0, fn touchRow(self: *FeedStats, row: u16) void { self.max_row_touched = @max(self.max_row_touched, row); }}; const State = enum { normal, escape, csi, osc, dcs,}; const SavedCursor = struct { row: u16, col: u16, pending_wrap: bool, style: Style, origin_mode: bool,}; const SavedScreen = struct { rows: u16, cols: u16, cells: []Cell, row_origin: u16, cursor_row: u16, cursor_col: u16, autowrap: bool, pending_wrap: bool, cursor_visible: bool, cursor_shape: contracts.CursorShape, cursor_blinking: bool, current_style: Style, active_hyperlink_params: []const u8, last_printable_idx: ?usize, scroll_top: u16, scroll_bottom: u16, origin_mode: bool, insert_mode: bool, saved_cursor: ?SavedCursor, /// Take ownership of the grid's live screen state, including its /// `cells` and `active_hyperlink_params` allocations. fn capture(grid: Grid) SavedScreen { return .{ .rows = grid.rows, .cols = grid.cols, .cells = grid.cells, .row_origin = grid.row_origin, .cursor_row = grid.cursor_row, .cursor_col = grid.cursor_col, .autowrap = grid.autowrap, .pending_wrap = grid.pending_wrap, .cursor_visible = grid.cursor_visible, .cursor_shape = grid.cursor_shape, .cursor_blinking = grid.cursor_blinking, .current_style = grid.current_style, .active_hyperlink_params = grid.active_hyperlink_params, .last_printable_idx = grid.last_printable_idx, .scroll_top = grid.scroll_top, .scroll_bottom = grid.scroll_bottom, .origin_mode = grid.origin_mode, .insert_mode = grid.insert_mode, .saved_cursor = grid.saved_cursor, }; } /// Hand the saved state back to the grid, transferring ownership /// of the allocations. The caller frees the grid's previous ones. fn restoreTo(saved: SavedScreen, grid: *Grid) void { grid.rows = saved.rows; grid.cols = saved.cols; grid.cells = saved.cells; grid.row_origin = saved.row_origin; grid.cursor_row = saved.cursor_row; grid.cursor_col = saved.cursor_col; grid.autowrap = saved.autowrap; grid.pending_wrap = saved.pending_wrap; grid.cursor_shape = saved.cursor_shape; grid.cursor_blinking = saved.cursor_blinking; grid.current_style = saved.current_style; grid.active_hyperlink_params = saved.active_hyperlink_params; grid.last_printable_idx = saved.last_printable_idx; grid.scroll_top = saved.scroll_top; grid.scroll_bottom = saved.scroll_bottom; grid.origin_mode = saved.origin_mode; grid.insert_mode = saved.insert_mode; grid.saved_cursor = saved.saved_cursor; } /// Deep copy. New fields are carried automatically; only the two /// allocations need duplicating. fn dupe(saved: SavedScreen, alloc: Allocator) !SavedScreen { var copy = saved; copy.cells = try alloc.dupe(Cell, saved.cells); errdefer alloc.free(copy.cells); copy.active_hyperlink_params = if (saved.active_hyperlink_params.len > 0) try alloc.dupe(u8, saved.active_hyperlink_params) else &.{}; return copy; }}; pub const Grid = struct { alloc: Allocator, rows: u16, cols: u16, cells: []Cell, /// Physical row backing logical terminal row one. Scrolling rotates /// this origin instead of shifting the complete cell grid. row_origin: u16 = 0, /// 1-indexed to match ANSI. cursor_row: u16 = 1, cursor_col: u16 = 1, autowrap: bool = true, /// Lazy-wrap flag: set when the cursor fills the last column. The /// next printable write will consume the flag and wrap before /// writing. Any cursor-moving sequence (CR, LF, CUP, backspace, /// ...) clears it without wrapping. pending_wrap: bool = false, cursor_visible: bool = true, cursor_shape: contracts.CursorShape = .block, cursor_blinking: bool = true, scroll_top: u16, scroll_bottom: u16, origin_mode: bool = false, insert_mode: bool = false, bracketed_paste: bool = false, mouse_modes: u16 = 0, focus_tracking: bool = false, application_cursor_keys: bool = false, application_keypad: bool = false, keyboard_protocol: bool = false, tab_stops: []bool, sync_active: bool = false, sync_buffer: std.ArrayList(u8) = .empty, /// Controls whether DEC-2026 blocks apply atomically or immediately. /// Shadow grids disable buffering so clones include in-block clears; /// otherwise DECRST could erase content omitted from the subsequent diff. defer_sync_updates: bool = true, /// Active SGR state. Updated by `\x1b[Nm` and applied to any cell /// the cursor writes into (and to the bg of cells cleared by EL/ED). current_style: Style = .{}, state: State = .normal, /// CSI parameters accumulated from the digit/';' bytes after `\x1b[`. csi_params: [max_csi_params]u16 = [_]u16{0} ** max_csi_params, csi_param_count: u8 = 0, csi_has_digit: bool = false, csi_private: u8 = 0, csi_intermediates: [max_csi_intermediates]u8 = [_]u8{0} ** max_csi_intermediates, csi_intermediate_count: u8 = 0, osc_saw_esc: bool = false, /// Captured OSC payload bytes (between `\x1b]` and the terminator). /// Retained to interpret OSC 8 (hyperlink) sequences; other OSC /// codes are still ignored. osc_buffer: std.ArrayList(u8) = .empty, dcs_saw_esc: bool = false, dcs_buffer: std.ArrayList(u8) = .empty, utf8_buffer: [4]u8 = [_]u8{0} ** 4, utf8_len: u8 = 0, utf8_expected: u8 = 0, /// Allocator-owned URL strings indexed by `Style.hyperlink_id`. /// `id == 0` is the sentinel "no hyperlink" and is NOT stored here /// (lookup is `id - 1`). hyperlink_pool: std.ArrayList([]u8) = .empty, hyperlink_pool_bytes: usize = 0, /// Allocator-owned ordered UTF-8 suffixes indexed by /// `Cell.combining_suffix_id`. IDs are grid-local. combining_suffix_pool: std.ArrayList([]u8) = .empty, combining_pool_bytes: usize = 0, /// Exact OSC 8 parameter bytes for the currently active hyperlink. /// These are presentation state, not part of URI interning. active_hyperlink_params: []const u8 = &.{}, /// Normal terminal buffer state saved by DECSET ?1049. The current /// fields represent the alternate buffer while this is non-null. saved_normal_screen: ?SavedScreen = null, saved_cursor: ?SavedCursor = null, /// Lead cell written by the most recent printable input byte /// sequence. A following width-zero codepoint modifies this cell. last_printable_idx: ?usize = null, feed_stats: ?*FeedStats = null, feed_mode: FeedMode = .journal_replay, feed_result: ?*FeedResult = null, pub fn init(alloc: Allocator, cols: u16, rows: u16) !Grid { if (cols == 0 or rows == 0) return error.InvalidGridSize; const cell_count = std.math.mul( usize, @intCast(cols), @intCast(rows), ) catch return error.InvalidGridSize; if (cell_count > contracts.max_render_cells) { return error.InvalidGridSize; } const cells = try alloc.alloc(Cell, cell_count); errdefer alloc.free(cells); @memset(cells, .{}); const tab_stops = try alloc.alloc(bool, cols); initializeTabStops(tab_stops); return .{ .alloc = alloc, .rows = rows, .cols = cols, .cells = cells, .scroll_top = 1, .scroll_bottom = rows, .tab_stops = tab_stops, }; } pub fn deinit(self: *Grid) void { if (self.saved_normal_screen) |saved| { self.alloc.free(saved.cells); if (saved.active_hyperlink_params.len > 0) { self.alloc.free(saved.active_hyperlink_params); } } self.alloc.free(self.cells); self.sync_buffer.deinit(self.alloc); self.osc_buffer.deinit(self.alloc); self.dcs_buffer.deinit(self.alloc); self.alloc.free(self.tab_stops); for (self.hyperlink_pool.items) |url| self.alloc.free(url); self.hyperlink_pool.deinit(self.alloc); for (self.combining_suffix_pool.items) |suffix| self.alloc.free(suffix); self.combining_suffix_pool.deinit(self.alloc); if (self.active_hyperlink_params.len > 0) { self.alloc.free(self.active_hyperlink_params); } } /// Resize the grid. Keeps top-left content, clips anything outside /// the new bounds, fills any grown area with blanks. Matches what a /// real terminal does when the pane shrinks or grows — content is /// not auto-cleared, so the caller (fx) is responsible for /// repainting. pub fn resize(self: *Grid, cols: u16, rows: u16) !void { if (cols == 0 or rows == 0) return error.InvalidGridSize; if (cols == self.cols and rows == self.rows) return; const cell_count = std.math.mul( usize, @intCast(cols), @intCast(rows), ) catch return error.InvalidGridSize; if (cell_count > contracts.max_render_cells) { return error.InvalidGridSize; } const new_cells = try self.resizedCells( self.cells, self.cols, self.rows, self.row_origin, cols, rows, ); errdefer self.alloc.free(new_cells); const normal_cells = if (self.saved_normal_screen) |saved| try self.resizedCells( saved.cells, saved.cols, saved.rows, saved.row_origin, cols, rows, ) else null; errdefer if (normal_cells) |cells| self.alloc.free(cells); const new_tab_stops = try self.resizedTabStops(cols); errdefer self.alloc.free(new_tab_stops); self.alloc.free(self.cells); self.alloc.free(self.tab_stops); self.cells = new_cells; self.tab_stops = new_tab_stops; self.cols = cols; self.rows = rows; self.row_origin = 0; if (self.cursor_row > rows) self.cursor_row = rows; if (self.cursor_col > cols) self.cursor_col = cols; self.last_printable_idx = null; self.scroll_top = 1; self.scroll_bottom = rows; self.origin_mode = false; if (self.saved_normal_screen) |*saved| { self.alloc.free(saved.cells); saved.cells = normal_cells.?; saved.rows = rows; saved.cols = cols; saved.row_origin = 0; if (saved.cursor_row > rows) saved.cursor_row = rows; if (saved.cursor_col > cols) saved.cursor_col = cols; saved.last_printable_idx = null; saved.scroll_top = 1; saved.scroll_bottom = rows; saved.origin_mode = false; } } fn resizedTabStops(self: Grid, cols: u16) ![]bool { const stops = try self.alloc.alloc(bool, cols); initializeTabStops(stops); @memcpy(stops[0..@min(stops.len, self.tab_stops.len)], self.tab_stops[0..@min(stops.len, self.tab_stops.len)]); return stops; } fn resizedCells( self: *Grid, source: []const Cell, source_cols: u16, source_rows: u16, source_row_origin: u16, cols: u16, rows: u16, ) ![]Cell { const cells = try self.alloc.alloc(Cell, @as(usize, cols) * @as(usize, rows)); errdefer self.alloc.free(cells); @memset(cells, .{}); const copy_rows = @min(source_rows, rows); const copy_cols = @min(source_cols, cols); var row: u16 = 0; while (row < copy_rows) : (row += 1) { const source_physical_row = physicalRowIndex( source_row_origin, row, source_rows, ); const src_base: usize = source_physical_row * @as(usize, source_cols); const dst_base: usize = @as(usize, row) * @as(usize, cols); @memcpy(cells[dst_base .. dst_base + copy_cols], source[src_base .. src_base + copy_cols]); } repairWideCells(cells, cols, rows); return cells; } fn rowBase(self: Grid, row: u16) usize { std.debug.assert(row >= 1 and row <= self.rows); const physical_row = physicalRowIndex( self.row_origin, row - 1, self.rows, ); return physical_row * @as(usize, self.cols); } fn cellIndex(self: Grid, row: u16, col: u16) usize { std.debug.assert(col >= 1 and col <= self.cols); return self.rowBase(row) + @as(usize, col) - 1; } fn physicalIndexForLogicalOffset(self: Grid, offset: usize) usize { std.debug.assert(offset < self.cells.len); const cols: usize = self.cols; const logical_row: u16 = @intCast(offset / cols); const col = offset % cols; const physical_row = physicalRowIndex( self.row_origin, logical_row, self.rows, ); return physical_row * cols + col; } /// Feeds output through the emulator, preserving partial control sequences. /// DEC-2026 blocks are buffered until the matching DECRST when enabled. pub fn feed(self: *Grid, bytes: []const u8) !void { var result = try self.feedMode(bytes, .journal_replay); result.deinit(self.alloc); } pub fn feedMode(self: *Grid, bytes: []const u8, mode: FeedMode) !FeedResult { var result = FeedResult{}; errdefer result.deinit(self.alloc); const previous_stats = self.feed_stats; const previous_mode = self.feed_mode; const previous_result = self.feed_result; self.feed_stats = &result.stats;Lines 1–500 of 4576. Load more
//! Bounded text-terminal engine shared by live terminal sessions, recovery, //! replay, and deterministic rendering tests. Graphical terminal protocols //! are intentionally outside this module's contract. const std = @import("std"); const display_width = @import("../shared/display_width.zig"); const contracts = @import("contracts.zig"); const Allocator = std.mem.Allocator; pub const checkpoint_schema_revision: u16 = 1; const checkpoint_magic = "FXTE"; pub const max_reply_effects: usize = 16; pub const max_reply_effect_bytes: usize = 256; pub const max_reply_effect_total_bytes: usize = 4096; const max_csi_params: usize = 16; const max_csi_intermediates: usize = 2; const max_string_bytes: usize = 4096; const max_sync_bytes: usize = 1024 * 1024; const max_pool_entries: usize = 65_535; const max_hyperlink_pool_bytes: usize = 4 * 1024 * 1024; const max_combining_pool_bytes: usize = 4 * 1024 * 1024; pub const FeedMode = enum { native_live, tmux_live, journal_replay, }; pub const ProtocolReply = struct { bytes: []u8, }; pub const FeedResult = struct { stats: FeedStats = .{}, replies: std.ArrayList(ProtocolReply) = .empty, reply_bytes: usize = 0, pub fn deinit(self: *FeedResult, alloc: Allocator) void { for (self.replies.items) |reply| alloc.free(reply.bytes); self.replies.deinit(alloc); self.* = undefined; } }; pub const Color = union(enum) { default, /// Indexed palette colour (0..255) from `\x1b[3Nm`, `\x1b[4Nm`, /// `\x1b[9Nm`, `\x1b[10Nm`, or `\x1b[38;5;N` / `\x1b[48;5;N`. indexed: u8, /// Truecolor from `\x1b[38;2;R;G;Bm` / `\x1b[48;2;R;G;Bm`. rgb: struct { r: u8, g: u8, b: u8 }, pub fn eql(a: Color, b: Color) bool { return switch (a) { .default => b == .default, .indexed => |ai| switch (b) { .indexed => |bi| ai == bi, else => false, }, .rgb => |ar| switch (b) { .rgb => |br| ar.r == br.r and ar.g == br.g and ar.b == br.b, else => false, }, }; } }; pub const StyleFlags = packed struct(u8) { bold: bool = false, dim: bool = false, italic: bool = false, underline: bool = false, reverse: bool = false, strike: bool = false, _pad: u2 = 0, pub fn eql(a: StyleFlags, b: StyleFlags) bool { return @as(u8, @bitCast(a)) == @as(u8, @bitCast(b)); } }; pub const Style = struct { fg: Color = .default, bg: Color = .default, flags: StyleFlags = .{}, /// Index into `Grid.hyperlink_pool`. `0` means no OSC 8 hyperlink; /// IDs `>= 1` map to a URL string the cell should be wrapped with /// when re-emitted by `diffBand`. IDs are grid-local; they survive /// `Grid.clone` because the pool is dup'd in lockstep. hyperlink_id: u32 = 0, pub fn eql(a: Style, b: Style) bool { return a.fg.eql(b.fg) and a.bg.eql(b.bg) and a.flags.eql(b.flags) and a.hyperlink_id == b.hyperlink_id; } }; pub const Cell = struct { /// Unicode codepoint in this cell. Zero for the trailing half of a /// wide (double-width) cell so callers can tell single from wide. codepoint: u21 = ' ', width: u8 = 1, /// Index into `Grid.combining_suffix_pool`. `0` means the lead /// codepoint has no remaining display-unit UTF-8 bytes. combining_suffix_id: u32 = 0, /// Style applied to this cell. Blank cells from erasures inherit /// the current bg so a `\x1b[48;5;236m\x1b[K` clears into the /// configured background, matching real terminals. style: Style = .{}, }; pub const FeedStats = struct { max_row_touched: u16 = 0, scrolled: bool = false, scroll_rows: u16 = 0, fn touchRow(self: *FeedStats, row: u16) void { self.max_row_touched = @max(self.max_row_touched, row); } }; const State = enum { normal, escape, csi, osc, dcs, }; const SavedCursor = struct { row: u16, col: u16, pending_wrap: bool, style: Style, origin_mode: bool, }; const SavedScreen = struct { rows: u16, cols: u16, cells: []Cell, row_origin: u16, cursor_row: u16, cursor_col: u16, autowrap: bool, pending_wrap: bool, cursor_visible: bool, cursor_shape: contracts.CursorShape, cursor_blinking: bool, current_style: Style, active_hyperlink_params: []const u8, last_printable_idx: ?usize, scroll_top: u16, scroll_bottom: u16, origin_mode: bool, insert_mode: bool, saved_cursor: ?SavedCursor, /// Take ownership of the grid's live screen state, including its /// `cells` and `active_hyperlink_params` allocations. fn capture(grid: Grid) SavedScreen { return .{ .rows = grid.rows, .cols = grid.cols, .cells = grid.cells, .row_origin = grid.row_origin, .cursor_row = grid.cursor_row, .cursor_col = grid.cursor_col, .autowrap = grid.autowrap, .pending_wrap = grid.pending_wrap, .cursor_visible = grid.cursor_visible, .cursor_shape = grid.cursor_shape, .cursor_blinking = grid.cursor_blinking, .current_style = grid.current_style, .active_hyperlink_params = grid.active_hyperlink_params, .last_printable_idx = grid.last_printable_idx, .scroll_top = grid.scroll_top, .scroll_bottom = grid.scroll_bottom, .origin_mode = grid.origin_mode, .insert_mode = grid.insert_mode, .saved_cursor = grid.saved_cursor, }; } /// Hand the saved state back to the grid, transferring ownership /// of the allocations. The caller frees the grid's previous ones. fn restoreTo(saved: SavedScreen, grid: *Grid) void { grid.rows = saved.rows; grid.cols = saved.cols; grid.cells = saved.cells; grid.row_origin = saved.row_origin; grid.cursor_row = saved.cursor_row; grid.cursor_col = saved.cursor_col; grid.autowrap = saved.autowrap; grid.pending_wrap = saved.pending_wrap; grid.cursor_shape = saved.cursor_shape; grid.cursor_blinking = saved.cursor_blinking; grid.current_style = saved.current_style; grid.active_hyperlink_params = saved.active_hyperlink_params; grid.last_printable_idx = saved.last_printable_idx; grid.scroll_top = saved.scroll_top; grid.scroll_bottom = saved.scroll_bottom; grid.origin_mode = saved.origin_mode; grid.insert_mode = saved.insert_mode; grid.saved_cursor = saved.saved_cursor; } /// Deep copy. New fields are carried automatically; only the two /// allocations need duplicating. fn dupe(saved: SavedScreen, alloc: Allocator) !SavedScreen { var copy = saved; copy.cells = try alloc.dupe(Cell, saved.cells); errdefer alloc.free(copy.cells); copy.active_hyperlink_params = if (saved.active_hyperlink_params.len > 0) try alloc.dupe(u8, saved.active_hyperlink_params) else &.{}; return copy; } }; pub const Grid = struct { alloc: Allocator, rows: u16, cols: u16, cells: []Cell, /// Physical row backing logical terminal row one. Scrolling rotates /// this origin instead of shifting the complete cell grid. row_origin: u16 = 0, /// 1-indexed to match ANSI. cursor_row: u16 = 1, cursor_col: u16 = 1, autowrap: bool = true, /// Lazy-wrap flag: set when the cursor fills the last column. The /// next printable write will consume the flag and wrap before /// writing. Any cursor-moving sequence (CR, LF, CUP, backspace, /// ...) clears it without wrapping. pending_wrap: bool = false, cursor_visible: bool = true, cursor_shape: contracts.CursorShape = .block, cursor_blinking: bool = true, scroll_top: u16, scroll_bottom: u16, origin_mode: bool = false, insert_mode: bool = false, bracketed_paste: bool = false, mouse_modes: u16 = 0, focus_tracking: bool = false, application_cursor_keys: bool = false, application_keypad: bool = false, keyboard_protocol: bool = false, tab_stops: []bool, sync_active: bool = false, sync_buffer: std.ArrayList(u8) = .empty, /// Controls whether DEC-2026 blocks apply atomically or immediately. /// Shadow grids disable buffering so clones include in-block clears; /// otherwise DECRST could erase content omitted from the subsequent diff. defer_sync_updates: bool = true, /// Active SGR state. Updated by `\x1b[Nm` and applied to any cell /// the cursor writes into (and to the bg of cells cleared by EL/ED). current_style: Style = .{}, state: State = .normal, /// CSI parameters accumulated from the digit/';' bytes after `\x1b[`. csi_params: [max_csi_params]u16 = [_]u16{0} ** max_csi_params, csi_param_count: u8 = 0, csi_has_digit: bool = false, csi_private: u8 = 0, csi_intermediates: [max_csi_intermediates]u8 = [_]u8{0} ** max_csi_intermediates, csi_intermediate_count: u8 = 0, osc_saw_esc: bool = false, /// Captured OSC payload bytes (between `\x1b]` and the terminator). /// Retained to interpret OSC 8 (hyperlink) sequences; other OSC /// codes are still ignored. osc_buffer: std.ArrayList(u8) = .empty, dcs_saw_esc: bool = false, dcs_buffer: std.ArrayList(u8) = .empty, utf8_buffer: [4]u8 = [_]u8{0} ** 4, utf8_len: u8 = 0, utf8_expected: u8 = 0, /// Allocator-owned URL strings indexed by `Style.hyperlink_id`. /// `id == 0` is the sentinel "no hyperlink" and is NOT stored here /// (lookup is `id - 1`). hyperlink_pool: std.ArrayList([]u8) = .empty, hyperlink_pool_bytes: usize = 0, /// Allocator-owned ordered UTF-8 suffixes indexed by /// `Cell.combining_suffix_id`. IDs are grid-local. combining_suffix_pool: std.ArrayList([]u8) = .empty, combining_pool_bytes: usize = 0, /// Exact OSC 8 parameter bytes for the currently active hyperlink. /// These are presentation state, not part of URI interning. active_hyperlink_params: []const u8 = &.{}, /// Normal terminal buffer state saved by DECSET ?1049. The current /// fields represent the alternate buffer while this is non-null. saved_normal_screen: ?SavedScreen = null, saved_cursor: ?SavedCursor = null, /// Lead cell written by the most recent printable input byte /// sequence. A following width-zero codepoint modifies this cell. last_printable_idx: ?usize = null, feed_stats: ?*FeedStats = null, feed_mode: FeedMode = .journal_replay, feed_result: ?*FeedResult = null, pub fn init(alloc: Allocator, cols: u16, rows: u16) !Grid { if (cols == 0 or rows == 0) return error.InvalidGridSize; const cell_count = std.math.mul( usize, @intCast(cols), @intCast(rows), ) catch return error.InvalidGridSize; if (cell_count > contracts.max_render_cells) { return error.InvalidGridSize; } const cells = try alloc.alloc(Cell, cell_count); errdefer alloc.free(cells); @memset(cells, .{}); const tab_stops = try alloc.alloc(bool, cols); initializeTabStops(tab_stops); return .{ .alloc = alloc, .rows = rows, .cols = cols, .cells = cells, .scroll_top = 1, .scroll_bottom = rows, .tab_stops = tab_stops, }; } pub fn deinit(self: *Grid) void { if (self.saved_normal_screen) |saved| { self.alloc.free(saved.cells); if (saved.active_hyperlink_params.len > 0) { self.alloc.free(saved.active_hyperlink_params); } } self.alloc.free(self.cells); self.sync_buffer.deinit(self.alloc); self.osc_buffer.deinit(self.alloc); self.dcs_buffer.deinit(self.alloc); self.alloc.free(self.tab_stops); for (self.hyperlink_pool.items) |url| self.alloc.free(url); self.hyperlink_pool.deinit(self.alloc); for (self.combining_suffix_pool.items) |suffix| self.alloc.free(suffix); self.combining_suffix_pool.deinit(self.alloc); if (self.active_hyperlink_params.len > 0) { self.alloc.free(self.active_hyperlink_params); } } /// Resize the grid. Keeps top-left content, clips anything outside /// the new bounds, fills any grown area with blanks. Matches what a /// real terminal does when the pane shrinks or grows — content is /// not auto-cleared, so the caller (fx) is responsible for /// repainting. pub fn resize(self: *Grid, cols: u16, rows: u16) !void { if (cols == 0 or rows == 0) return error.InvalidGridSize; if (cols == self.cols and rows == self.rows) return; const cell_count = std.math.mul( usize, @intCast(cols), @intCast(rows), ) catch return error.InvalidGridSize; if (cell_count > contracts.max_render_cells) { return error.InvalidGridSize; } const new_cells = try self.resizedCells( self.cells, self.cols, self.rows, self.row_origin, cols, rows, ); errdefer self.alloc.free(new_cells); const normal_cells = if (self.saved_normal_screen) |saved| try self.resizedCells( saved.cells, saved.cols, saved.rows, saved.row_origin, cols, rows, ) else null; errdefer if (normal_cells) |cells| self.alloc.free(cells); const new_tab_stops = try self.resizedTabStops(cols); errdefer self.alloc.free(new_tab_stops); self.alloc.free(self.cells); self.alloc.free(self.tab_stops); self.cells = new_cells; self.tab_stops = new_tab_stops; self.cols = cols; self.rows = rows; self.row_origin = 0; if (self.cursor_row > rows) self.cursor_row = rows; if (self.cursor_col > cols) self.cursor_col = cols; self.last_printable_idx = null; self.scroll_top = 1; self.scroll_bottom = rows; self.origin_mode = false; if (self.saved_normal_screen) |*saved| { self.alloc.free(saved.cells); saved.cells = normal_cells.?; saved.rows = rows; saved.cols = cols; saved.row_origin = 0; if (saved.cursor_row > rows) saved.cursor_row = rows; if (saved.cursor_col > cols) saved.cursor_col = cols; saved.last_printable_idx = null; saved.scroll_top = 1; saved.scroll_bottom = rows; saved.origin_mode = false; } } fn resizedTabStops(self: Grid, cols: u16) ![]bool { const stops = try self.alloc.alloc(bool, cols); initializeTabStops(stops); @memcpy(stops[0..@min(stops.len, self.tab_stops.len)], self.tab_stops[0..@min(stops.len, self.tab_stops.len)]); return stops; } fn resizedCells( self: *Grid, source: []const Cell, source_cols: u16, source_rows: u16, source_row_origin: u16, cols: u16, rows: u16, ) ![]Cell { const cells = try self.alloc.alloc(Cell, @as(usize, cols) * @as(usize, rows)); errdefer self.alloc.free(cells); @memset(cells, .{}); const copy_rows = @min(source_rows, rows); const copy_cols = @min(source_cols, cols); var row: u16 = 0; while (row < copy_rows) : (row += 1) { const source_physical_row = physicalRowIndex( source_row_origin, row, source_rows, ); const src_base: usize = source_physical_row * @as(usize, source_cols); const dst_base: usize = @as(usize, row) * @as(usize, cols); @memcpy(cells[dst_base .. dst_base + copy_cols], source[src_base .. src_base + copy_cols]); } repairWideCells(cells, cols, rows); return cells; } fn rowBase(self: Grid, row: u16) usize { std.debug.assert(row >= 1 and row <= self.rows); const physical_row = physicalRowIndex( self.row_origin, row - 1, self.rows, ); return physical_row * @as(usize, self.cols); } fn cellIndex(self: Grid, row: u16, col: u16) usize { std.debug.assert(col >= 1 and col <= self.cols); return self.rowBase(row) + @as(usize, col) - 1; } fn physicalIndexForLogicalOffset(self: Grid, offset: usize) usize { std.debug.assert(offset < self.cells.len); const cols: usize = self.cols; const logical_row: u16 = @intCast(offset / cols); const col = offset % cols; const physical_row = physicalRowIndex( self.row_origin, logical_row, self.rows, ); return physical_row * cols + col; } /// Feeds output through the emulator, preserving partial control sequences. /// DEC-2026 blocks are buffered until the matching DECRST when enabled. pub fn feed(self: *Grid, bytes: []const u8) !void { var result = try self.feedMode(bytes, .journal_replay); result.deinit(self.alloc); } pub fn feedMode(self: *Grid, bytes: []const u8, mode: FeedMode) !FeedResult { var result = FeedResult{}; errdefer result.deinit(self.alloc); const previous_stats = self.feed_stats; const previous_mode = self.feed_mode; const previous_result = self.feed_result; self.feed_stats = &result.stats;