4576 lines · 165.3 KB · Zig
RawDownload
//! 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.