Something went wrong. Try again.
Ghostty for the web with xterm.js API compatibility
Something went wrong. Try again.
34 kB · 1029 lines
at vimscroll
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030diff --git a/.gitignore b/.gitignoreindex e451b171a..89c623d8b 100644--- a/.gitignore+++ b/.gitignore@@ -23,3 +23,4 @@ glad.zip /ghostty.qcow2 vgcore.*+node_modules/diff --git a/include/ghostty/vt.h b/include/ghostty/vt.hindex 4f8fef88e..ca9fb1d4d 100644--- a/include/ghostty/vt.h+++ b/include/ghostty/vt.h@@ -28,6 +28,7 @@ * @section groups_sec API Reference * * The API is organized into the following groups:+ * - @ref terminal "Terminal Emulator" - Complete terminal emulator with VT parsing * - @ref key "Key Encoding" - Encode key events into terminal sequences * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences * - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences@@ -74,6 +75,7 @@ extern "C" { #include <ghostty/vt/result.h> #include <ghostty/vt/allocator.h>+#include <ghostty/vt/terminal.h> #include <ghostty/vt/osc.h> #include <ghostty/vt/sgr.h> #include <ghostty/vt/key.h>diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.hnew file mode 100644index 000000000..26ee23ed5--- /dev/null+++ b/include/ghostty/vt/terminal.h@@ -0,0 +1,383 @@+/**+ * @file terminal.h+ *+ * Complete terminal emulator API for WASM integration.+ */++#ifndef GHOSTTY_VT_TERMINAL_H+#define GHOSTTY_VT_TERMINAL_H++/** @defgroup terminal Terminal Emulator+ *+ * Complete terminal emulator with VT100/ANSI parsing and screen buffer management.+ *+ * This API exports Ghostty's production-tested terminal emulator for use in+ * WASM environments. It handles all VT sequence parsing, screen buffer management,+ * scrollback, cursor positioning, and text styling.+ *+ * ## Basic Usage+ *+ * 1. Create a terminal with ghostty_terminal_new()+ * 2. Write data with ghostty_terminal_write() (parses VT sequences)+ * 3. Read screen content with ghostty_terminal_get_line()+ * 4. Query cursor position with ghostty_terminal_get_cursor_x/y()+ * 5. Free with ghostty_terminal_free() when done+ *+ * ## Example+ *+ * @code{.c}+ * #include <ghostty/vt.h>+ * #include <string.h>+ *+ * int main() {+ * // Create 80x24 terminal+ * GhosttyTerminal term = ghostty_terminal_new(80, 24);+ * if (!term) return 1;+ *+ * // Write some text with color+ * const char* data = "Hello \x1b[31mRed\x1b[0m World!";+ * ghostty_terminal_write(term, (const uint8_t*)data, strlen(data));+ *+ * // Read first line+ * GhosttyCell cells[80];+ * int count = ghostty_terminal_get_line(term, 0, cells, 80);+ *+ * // Check cursor position+ * int x = ghostty_terminal_get_cursor_x(term);+ * int y = ghostty_terminal_get_cursor_y(term);+ *+ * // Cleanup+ * ghostty_terminal_free(term);+ * return 0;+ * }+ * @endcode+ *+ * @{+ */++#include <ghostty/vt/allocator.h>+#include <ghostty/vt/result.h>+#include <stdbool.h>+#include <stddef.h>+#include <stdint.h>++#ifdef __cplusplus+extern "C" {+#endif++/**+ * Opaque terminal handle.+ *+ * Represents a terminal emulator instance. Create with ghostty_terminal_new()+ * and free with ghostty_terminal_free().+ */+typedef void* GhosttyTerminal;++/**+ * Terminal configuration options.+ *+ * Used when creating a new terminal to specify behavior and limits.+ */+typedef struct {+ /**+ * Maximum scrollback lines (0 = unlimited, default = 10000).+ *+ * Limits memory usage by restricting how many lines of history are kept.+ * For WASM environments, a reasonable limit is recommended.+ */+ uint32_t scrollback_limit;++ /**+ * Initial foreground color (RGB, 0xRRGGBB format, 0 = use default).+ */+ uint32_t fg_color;++ /**+ * Initial background color (RGB, 0xRRGGBB format, 0 = use default).+ */+ uint32_t bg_color;+} GhosttyTerminalConfig;++/**+ * Cell structure - represents a single character position.+ *+ * This is designed to be simple and C-compatible. Colors are always+ * exported as RGB (terminal color palette is resolved internally).+ *+ * Size: 16 bytes (efficient for bulk transfers across WASM boundary)+ */+typedef struct {+ /** Unicode codepoint (0 = empty cell) */+ uint32_t codepoint;++ /** Foreground color - Red component (0-255) */+ uint8_t fg_r;+ /** Foreground color - Green component (0-255) */+ uint8_t fg_g;+ /** Foreground color - Blue component (0-255) */+ uint8_t fg_b;++ /** Background color - Red component (0-255) */+ uint8_t bg_r;+ /** Background color - Green component (0-255) */+ uint8_t bg_g;+ /** Background color - Blue component (0-255) */+ uint8_t bg_b;++ /** Style flags (see GHOSTTY_CELL_* constants) */+ uint8_t flags;++ /** Character width: 0=combining, 1=normal, 2=wide (CJK) */+ uint8_t width;+} GhosttyCell;++/** Cell flag: Bold text */+#define GHOSTTY_CELL_BOLD (1 << 0)+/** Cell flag: Italic text */+#define GHOSTTY_CELL_ITALIC (1 << 1)+/** Cell flag: Underlined text */+#define GHOSTTY_CELL_UNDERLINE (1 << 2)+/** Cell flag: Strikethrough text */+#define GHOSTTY_CELL_STRIKETHROUGH (1 << 3)+/** Cell flag: Inverse video (swap fg/bg) */+#define GHOSTTY_CELL_INVERSE (1 << 4)+/** Cell flag: Invisible text */+#define GHOSTTY_CELL_INVISIBLE (1 << 5)+/** Cell flag: Blinking text */+#define GHOSTTY_CELL_BLINK (1 << 6)+/** Cell flag: Faint/dim text */+#define GHOSTTY_CELL_FAINT (1 << 7)++/* ============================================================================+ * Lifecycle Management+ * ========================================================================= */++/**+ * Create a new terminal instance with default configuration.+ *+ * Creates an 80x24 terminal with default settings (10,000 line scrollback,+ * standard color palette, autowrap enabled).+ *+ * @param cols Number of columns (typically 80, minimum 1)+ * @param rows Number of rows (typically 24, minimum 1)+ * @return Terminal handle, or NULL on allocation failure+ *+ * @see ghostty_terminal_new_with_config() for custom configuration+ * @see ghostty_terminal_free()+ */+GhosttyTerminal ghostty_terminal_new(int cols, int rows);++/**+ * Create a new terminal instance with custom configuration.+ *+ * @param cols Number of columns (typically 80, minimum 1)+ * @param rows Number of rows (typically 24, minimum 1)+ * @param config Configuration options (NULL = use defaults)+ * @return Terminal handle, or NULL on allocation failure+ *+ * @see ghostty_terminal_new()+ * @see ghostty_terminal_free()+ */+GhosttyTerminal ghostty_terminal_new_with_config(+ int cols,+ int rows,+ const GhosttyTerminalConfig* config+);++/**+ * Free a terminal instance.+ *+ * Releases all memory associated with the terminal. The handle becomes+ * invalid after this call.+ *+ * @param term Terminal to free (NULL is safe)+ */+void ghostty_terminal_free(GhosttyTerminal term);++/**+ * Resize the terminal.+ *+ * Changes the terminal dimensions. Content is preserved where possible,+ * with appropriate reflowing or truncation.+ *+ * @param term Terminal instance+ * @param cols New column count (minimum 1)+ * @param rows New row count (minimum 1)+ */+void ghostty_terminal_resize(GhosttyTerminal term, int cols, int rows);++/* ============================================================================+ * Input/Output+ * ========================================================================= */++/**+ * Write data to terminal (parses VT sequences and updates screen).+ *+ * This is the main entry point - all terminal output goes through here.+ * The data is parsed as VT100/ANSI escape sequences and the screen+ * buffer is updated accordingly.+ *+ * Supports:+ * - Text output (UTF-8)+ * - CSI sequences (colors, cursor movement, etc.)+ * - OSC sequences (title, colors, etc.)+ * - All standard VT100/xterm sequences+ *+ * @param term Terminal instance+ * @param data UTF-8 encoded data (may contain VT sequences)+ * @param len Length of data in bytes+ *+ * @note This function marks affected rows as dirty for rendering optimization+ */+void ghostty_terminal_write(GhosttyTerminal term, const uint8_t* data, size_t len);++/* ============================================================================+ * Screen Queries+ * ========================================================================= */++/**+ * Get terminal width in columns.+ *+ * @param term Terminal instance+ * @return Number of columns, or 0 if term is NULL+ */+int ghostty_terminal_get_cols(GhosttyTerminal term);++/**+ * Get terminal height in rows.+ *+ * @param term Terminal instance+ * @return Number of rows, or 0 if term is NULL+ */+int ghostty_terminal_get_rows(GhosttyTerminal term);++/**+ * Get cursor X position (column).+ *+ * @param term Terminal instance+ * @return Column position (0-indexed), or 0 if term is NULL+ */+int ghostty_terminal_get_cursor_x(GhosttyTerminal term);++/**+ * Get cursor Y position (row).+ *+ * @param term Terminal instance+ * @return Row position (0-indexed), or 0 if term is NULL+ */+int ghostty_terminal_get_cursor_y(GhosttyTerminal term);++/**+ * Get cursor visibility state.+ *+ * @param term Terminal instance+ * @return true if cursor is visible, false otherwise+ */+bool ghostty_terminal_get_cursor_visible(GhosttyTerminal term);++/**+ * Check if terminal is in alternate screen buffer mode.+ */+bool ghostty_terminal_is_alternate_screen(GhosttyTerminal term);++/**+ * Check if a row is wrapped from the previous row.+ */+bool ghostty_terminal_is_row_wrapped(GhosttyTerminal term, int row);++/**+ * Get scrollback length (number of lines in history).+ *+ * @param term Terminal instance+ * @return Number of scrollback lines, or 0 if none/NULL+ */+int ghostty_terminal_get_scrollback_length(GhosttyTerminal term);++/* ============================================================================+ * Cell Data Access+ * ========================================================================= */++/**+ * Get a line of cells from the visible screen.+ *+ * Retrieves an entire row of cells at once for efficient rendering.+ * Colors are returned as RGB values (palette indices are resolved).+ *+ * @param term Terminal instance+ * @param y Line number (0-indexed, 0 = top visible line)+ * @param out_buffer Output buffer (must have space for at least 'cols' cells)+ * @param buffer_size Size of output buffer in cells (should be >= cols)+ * @return Number of cells written (equals cols on success), or -1 on error+ *+ * @note Always writes exactly 'cols' cells, padding with empty cells if needed+ */+int ghostty_terminal_get_line(+ GhosttyTerminal term,+ int y,+ GhosttyCell* out_buffer,+ size_t buffer_size+);++/**+ * Get a line from scrollback history.+ *+ * @param term Terminal instance+ * @param y Line number (0 = oldest scrollback line)+ * @param out_buffer Output buffer+ * @param buffer_size Size of output buffer in cells+ * @return Number of cells written, or -1 on error/not implemented+ *+ * @note Currently not implemented - returns -1+ */+int ghostty_terminal_get_scrollback_line(+ GhosttyTerminal term,+ int y,+ GhosttyCell* out_buffer,+ size_t buffer_size+);++/* ============================================================================+ * Dirty Tracking (for efficient rendering)+ * ========================================================================= */++/**+ * Check if any part of the screen is dirty.+ *+ * Dirty tracking helps optimize rendering by identifying what changed.+ * After writing to the terminal, check which rows are dirty and only+ * re-render those.+ *+ * @param term Terminal instance+ * @return true if any row needs redrawing, false otherwise+ *+ * @see ghostty_terminal_is_row_dirty()+ * @see ghostty_terminal_clear_dirty()+ */+bool ghostty_terminal_is_dirty(GhosttyTerminal term);++/**+ * Check if a specific row is dirty.+ *+ * @param term Terminal instance+ * @param y Row number (0-indexed)+ * @return true if row needs redrawing, false otherwise+ */+bool ghostty_terminal_is_row_dirty(GhosttyTerminal term, int y);++/**+ * Clear all dirty flags (call after rendering).+ *+ * After reading dirty rows and re-rendering them, call this to mark+ * the screen as clean.+ *+ * @param term Terminal instance+ */+void ghostty_terminal_clear_dirty(GhosttyTerminal term);++#ifdef __cplusplus+}+#endif++/** @} */++#endif /* GHOSTTY_VT_TERMINAL_H */diff --git a/src/lib_vt.zig b/src/lib_vt.zigindex e95eee5f4..4b57361f6 100644--- a/src/lib_vt.zig+++ b/src/lib_vt.zig@@ -137,6 +137,24 @@ comptime { @export(&c.sgr_unknown_partial, .{ .name = "ghostty_sgr_unknown_partial" }); @export(&c.sgr_attribute_tag, .{ .name = "ghostty_sgr_attribute_tag" }); @export(&c.sgr_attribute_value, .{ .name = "ghostty_sgr_attribute_value" });+ @export(&c.terminal_new, .{ .name = "ghostty_terminal_new" });+ @export(&c.terminal_new_with_config, .{ .name = "ghostty_terminal_new_with_config" });+ @export(&c.terminal_free, .{ .name = "ghostty_terminal_free" });+ @export(&c.terminal_resize, .{ .name = "ghostty_terminal_resize" });+ @export(&c.terminal_write, .{ .name = "ghostty_terminal_write" });+ @export(&c.terminal_get_cols, .{ .name = "ghostty_terminal_get_cols" });+ @export(&c.terminal_get_rows, .{ .name = "ghostty_terminal_get_rows" });+ @export(&c.terminal_get_cursor_x, .{ .name = "ghostty_terminal_get_cursor_x" });+ @export(&c.terminal_get_cursor_y, .{ .name = "ghostty_terminal_get_cursor_y" });+ @export(&c.terminal_get_cursor_visible, .{ .name = "ghostty_terminal_get_cursor_visible" });+ @export(&c.terminal_is_alternate_screen, .{ .name = "ghostty_terminal_is_alternate_screen" });+ @export(&c.terminal_is_row_wrapped, .{ .name = "ghostty_terminal_is_row_wrapped" });+ @export(&c.terminal_get_scrollback_length, .{ .name = "ghostty_terminal_get_scrollback_length" });+ @export(&c.terminal_get_line, .{ .name = "ghostty_terminal_get_line" });+ @export(&c.terminal_get_scrollback_line, .{ .name = "ghostty_terminal_get_scrollback_line" });+ @export(&c.terminal_is_dirty, .{ .name = "ghostty_terminal_is_dirty" });+ @export(&c.terminal_is_row_dirty, .{ .name = "ghostty_terminal_is_row_dirty" });+ @export(&c.terminal_clear_dirty, .{ .name = "ghostty_terminal_clear_dirty" }); // On Wasm we need to export our allocator convenience functions. if (builtin.target.cpu.arch.isWasm()) {diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zigindex bc92597f5..3ce57e206 100644--- a/src/terminal/c/main.zig+++ b/src/terminal/c/main.zig@@ -4,6 +4,7 @@ pub const key_event = @import("key_event.zig"); pub const key_encode = @import("key_encode.zig"); pub const paste = @import("paste.zig"); pub const sgr = @import("sgr.zig");+pub const terminal = @import("terminal.zig"); // The full C API, unexported. pub const osc_new = osc.new;@@ -52,6 +53,25 @@ pub const key_encoder_encode = key_encode.encode; pub const paste_is_safe = paste.is_safe; +pub const terminal_new = terminal.new;+pub const terminal_new_with_config = terminal.newWithConfig;+pub const terminal_free = terminal.free;+pub const terminal_resize = terminal.resize;+pub const terminal_write = terminal.write;+pub const terminal_get_cols = terminal.getCols;+pub const terminal_get_rows = terminal.getRows;+pub const terminal_get_cursor_x = terminal.getCursorX;+pub const terminal_get_cursor_y = terminal.getCursorY;+pub const terminal_get_cursor_visible = terminal.getCursorVisible;+pub const terminal_is_alternate_screen = terminal.isAlternateScreen;+pub const terminal_is_row_wrapped = terminal.isRowWrapped;+pub const terminal_get_scrollback_length = terminal.getScrollbackLength;+pub const terminal_get_line = terminal.getLine;+pub const terminal_get_scrollback_line = terminal.getScrollbackLine;+pub const terminal_is_dirty = terminal.isDirty;+pub const terminal_is_row_dirty = terminal.isRowDirty;+pub const terminal_clear_dirty = terminal.clearDirty;+ test { _ = color; _ = osc;@@ -59,6 +79,7 @@ test { _ = key_encode; _ = paste; _ = sgr;+ _ = terminal; // We want to make sure we run the tests for the C allocator interface. _ = @import("../../lib/allocator.zig");diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zignew file mode 100644index 000000000..d195e1acf--- /dev/null+++ b/src/terminal/c/terminal.zig@@ -0,0 +1,530 @@+//! C API wrapper for Terminal+//!+//! This provides a C-compatible interface to Ghostty's Terminal for WASM export.++const std = @import("std");+const Allocator = std.mem.Allocator;+const assert = std.debug.assert;+const builtin = @import("builtin");++const Terminal = @import("../Terminal.zig");+const ReadonlyStream = @import("../stream_readonly.zig").Stream;+const size = @import("../size.zig");+const pagepkg = @import("../page.zig");+const Cell = pagepkg.Cell;+const PageList = @import("../PageList.zig");+const color = @import("../color.zig");+const point = @import("../point.zig");+const style = @import("../style.zig");++const log = std.log.scoped(.terminal_c);++/// Wrapper struct that owns both the Terminal and its allocator.+/// This is what we return as an opaque pointer to C.+const TerminalWrapper = struct {+ /// The allocator that owns all terminal memory+ alloc: Allocator,++ /// The terminal instance+ terminal: Terminal,++ /// Dirty tracking - which rows have changed since last clear+ dirty_rows: []bool,++ /// Configuration used for terminal+ config: Config,++ const Config = struct {+ scrollback_limit: u32,+ fg_color: u32,+ bg_color: u32,+ };+};++/// C-compatible cell structure+pub const GhosttyCell = extern struct {+ codepoint: u32,+ fg_r: u8,+ fg_g: u8,+ fg_b: u8,+ bg_r: u8,+ bg_g: u8,+ bg_b: u8,+ flags: u8,+ width: u8,+};++/// C-compatible terminal configuration+pub const GhosttyTerminalConfig = extern struct {+ scrollback_limit: u32,+ fg_color: u32,+ bg_color: u32,+};++// ============================================================================+// Lifecycle Management+// ============================================================================++pub fn new(cols: c_int, rows: c_int) callconv(.c) ?*anyopaque {+ return newWithConfig(cols, rows, null);+}++pub fn newWithConfig(+ cols: c_int,+ rows: c_int,+ config_: ?*const GhosttyTerminalConfig,+) callconv(.c) ?*anyopaque {+ // Use WASM allocator for WASM builds, GPA otherwise+ const alloc = if (builtin.target.cpu.arch.isWasm())+ std.heap.wasm_allocator+ else+ std.heap.c_allocator;++ // Parse configuration+ const config: TerminalWrapper.Config = if (config_) |cfg| .{+ .scrollback_limit = cfg.scrollback_limit,+ .fg_color = cfg.fg_color,+ .bg_color = cfg.bg_color,+ } else .{+ .scrollback_limit = 10_000,+ .fg_color = 0,+ .bg_color = 0,+ };++ // Allocate wrapper+ const wrapper = alloc.create(TerminalWrapper) catch |err| {+ log.err("Failed to allocate TerminalWrapper: {}", .{err});+ return null;+ };++ // Setup terminal colors+ var colors = Terminal.Colors.default;+ if (config.fg_color != 0) {+ const rgb = color.RGB{+ .r = @truncate((config.fg_color >> 16) & 0xFF),+ .g = @truncate((config.fg_color >> 8) & 0xFF),+ .b = @truncate(config.fg_color & 0xFF),+ };+ colors.foreground = color.DynamicRGB.init(rgb);+ }+ if (config.bg_color != 0) {+ const rgb = color.RGB{+ .r = @truncate((config.bg_color >> 16) & 0xFF),+ .g = @truncate((config.bg_color >> 8) & 0xFF),+ .b = @truncate(config.bg_color & 0xFF),+ };+ colors.background = color.DynamicRGB.init(rgb);+ }++ // Create terminal+ const terminal = Terminal.init(+ alloc,+ .{+ .cols = @intCast(cols),+ .rows = @intCast(rows),+ .max_scrollback = if (config.scrollback_limit == 0)+ std.math.maxInt(usize)+ else+ config.scrollback_limit,+ .colors = colors,+ },+ ) catch |err| {+ log.err("Failed to initialize Terminal: {}", .{err});+ alloc.destroy(wrapper);+ return null;+ };++ // Allocate dirty tracking+ const rows_usize: usize = @intCast(rows);+ const dirty_rows = alloc.alloc(bool, rows_usize) catch |err| {+ log.err("Failed to allocate dirty tracking: {}", .{err});+ // Note: terminal.deinit() requires the allocator be passed+ var term_mut = terminal;+ term_mut.deinit(alloc);+ alloc.destroy(wrapper);+ return null;+ };+ @memset(dirty_rows, true); // Initially all dirty++ wrapper.* = .{+ .alloc = alloc,+ .terminal = terminal,+ .dirty_rows = dirty_rows,+ .config = config,+ };++ return @ptrCast(wrapper);+}++pub fn free(ptr: ?*anyopaque) callconv(.c) void {+ const wrapper: *TerminalWrapper = @ptrCast(@alignCast(ptr orelse return));+ const alloc = wrapper.alloc;++ alloc.free(wrapper.dirty_rows);+ wrapper.terminal.deinit(alloc);+ alloc.destroy(wrapper);+}++pub fn resize(ptr: ?*anyopaque, cols: c_int, rows: c_int) callconv(.c) void {+ const wrapper: *TerminalWrapper = @ptrCast(@alignCast(ptr orelse return));++ // Resize terminal+ wrapper.terminal.resize(+ wrapper.alloc,+ @intCast(cols),+ @intCast(rows),+ ) catch |err| {+ log.err("Resize failed: {}", .{err});+ return;+ };++ // Reallocate dirty tracking+ const rows_usize: usize = @intCast(rows);+ const new_dirty = wrapper.alloc.realloc(wrapper.dirty_rows, rows_usize) catch |err| {+ log.err("Failed to reallocate dirty tracking: {}", .{err});+ return;+ };+ wrapper.dirty_rows = new_dirty;+ @memset(new_dirty, true); // All dirty after resize+}++// ============================================================================+// Input/Output+// ============================================================================++pub fn write(ptr: ?*anyopaque, data: [*]const u8, len: usize) callconv(.c) void {+ const wrapper: *TerminalWrapper = @ptrCast(@alignCast(ptr orelse return));++ // Create stream for this write operation+ var stream = wrapper.terminal.vtStream();++ // Write data to terminal stream (this parses VT sequences)+ const slice = data[0..len];+ stream.nextSlice(slice) catch |err| {+ log.err("Write failed: {}", .{err});+ return;+ };++ // Mark all visible rows as dirty (conservative approach)+ @memset(wrapper.dirty_rows, true);+}++// ============================================================================+// Screen Queries+// ============================================================================++pub fn getCols(ptr: ?*anyopaque) callconv(.c) c_int {+ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return 0));+ return @intCast(wrapper.terminal.cols);+}++pub fn getRows(ptr: ?*anyopaque) callconv(.c) c_int {+ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return 0));+ return @intCast(wrapper.terminal.rows);+}++pub fn getCursorX(ptr: ?*anyopaque) callconv(.c) c_int {+ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return 0));+ return @intCast(wrapper.terminal.screen.cursor.x);+}++pub fn getCursorY(ptr: ?*anyopaque) callconv(.c) c_int {+ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return 0));+ return @intCast(wrapper.terminal.screen.cursor.y);+}++pub fn getCursorVisible(ptr: ?*anyopaque) callconv(.c) bool {+ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return false));+ // Check if cursor is visible based on modes+ return wrapper.terminal.modes.get(.cursor_visible);+}++pub fn isAlternateScreen(ptr: ?*anyopaque) callconv(.c) bool {+ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return false));+ return wrapper.terminal.active_screen == .alternate;+}++pub fn isRowWrapped(ptr: ?*anyopaque, row: c_int) callconv(.c) bool {+ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return false));+ if (row < 0 or row >= wrapper.terminal.rows) return false;+ if (row == 0) return false;+ const pt = point.Point{ .viewport = .{ .x = 0, .y = @intCast(row) }};+ const list_cell = wrapper.terminal.screen.pages.getCell(pt) orelse return false;+ return list_cell.row.wrap;+}++pub fn getScrollbackLength(ptr: ?*anyopaque) callconv(.c) c_int {+ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return 0));+ // Calculate scrollback length as difference between history top and active top+ const active_pin = wrapper.terminal.screen.pages.getTopLeft(.active);+ const screen_pin = wrapper.terminal.screen.pages.getTopLeft(.screen);++ // Count rows between screen top and active top+ var count: c_int = 0;+ var pin = screen_pin;+ while (pin.node != active_pin.node or pin.y != active_pin.y) {+ count += 1;+ pin = pin.down(1) orelse break;+ if (count > 100000) break; // Safety limit+ }+ return count;+}++// ============================================================================+// Cell Data Access+// ============================================================================++/// Convert Ghostty's internal Cell to C-compatible GhosttyCell+/// list_cell_opt can be null if we're using a default empty cell+fn convertCell(wrapper: *const TerminalWrapper, cell: Cell, list_cell_opt: @TypeOf(wrapper.terminal.screen.pages.getCell(.{.viewport = .{}}))) GhosttyCell {+ const terminal = &wrapper.terminal;+ const palette = &terminal.colors.palette.current;++ // Get codepoint+ const cp = cell.content.codepoint;++ // Get the style - either from the page or use default+ const cell_style: style.Style = if (cell.style_id == style.default_id)+ .{}+ else if (list_cell_opt) |list_cell|+ list_cell.node.data.styles.get(list_cell.node.data.memory, cell.style_id).*+ else+ .{};++ // Resolve foreground color+ const fg_rgb: color.RGB = fg: {+ switch (cell_style.fg_color) {+ .none => {+ // Use default foreground+ if (terminal.colors.foreground.get()) |rgb| {+ break :fg rgb;+ } else {+ // Default to white+ break :fg .{ .r = 0xEA, .g = 0xEA, .b = 0xEA };+ }+ },+ .palette => |idx| break :fg palette[idx],+ .rgb => |rgb| break :fg rgb,+ }+ };++ // Resolve background color+ const bg_rgb: color.RGB = bg: {+ // Check for cell-level color override+ if (cell_style.bg(&cell, palette)) |rgb| {+ break :bg rgb;+ }++ // Use default background+ if (terminal.colors.background.get()) |rgb| {+ break :bg rgb;+ } else {+ // Default to black+ break :bg .{ .r = 0x1D, .g = 0x1F, .b = 0x21 };+ }+ };++ // Build flags bitfield+ var flags: u8 = 0;+ if (cell_style.flags.bold) flags |= 1 << 0;+ if (cell_style.flags.italic) flags |= 1 << 1;+ if (cell_style.flags.underline != .none) flags |= 1 << 2;+ if (cell_style.flags.strikethrough) flags |= 1 << 3;+ if (cell_style.flags.inverse) flags |= 1 << 4;+ if (cell_style.flags.invisible) flags |= 1 << 5;+ if (cell_style.flags.blink) flags |= 1 << 6;+ if (cell_style.flags.faint) flags |= 1 << 7;++ // Map cell.wide enum to actual character width+ // narrow = 0 -> width 1, wide = 1 -> width 2, spacer_tail = 2 -> width 0+ const width: u8 = switch (cell.wide) {+ .narrow => 1,+ .wide => 2,+ .spacer_tail => 0,+ .spacer_head => 0,+ };++ return .{+ .codepoint = cp,+ .fg_r = fg_rgb.r,+ .fg_g = fg_rgb.g,+ .fg_b = fg_rgb.b,+ .bg_r = bg_rgb.r,+ .bg_g = bg_rgb.g,+ .bg_b = bg_rgb.b,+ .flags = flags,+ .width = width,+ };+}++pub fn getLine(+ ptr: ?*anyopaque,+ y: c_int,+ out_buffer: [*]GhosttyCell,+ buffer_size: usize,+) callconv(.c) c_int {+ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return -1));++ const y_usize: usize = @intCast(y);+ if (y_usize >= wrapper.terminal.rows) return -1;++ const cols = wrapper.terminal.cols;+ if (buffer_size < cols) return -1;++ // Get cells from the screen using viewport coordinates+ var x: usize = 0;+ while (x < cols) : (x += 1) {+ const pt = point.Point{ .viewport = .{+ .x = @intCast(x),+ .y = @intCast(y),+ } };++ const list_cell = wrapper.terminal.screen.pages.getCell(pt);+ const cell = if (list_cell) |lc| lc.cell.* else Cell{};+ out_buffer[x] = convertCell(wrapper, cell, list_cell);+ }++ return @intCast(cols);+}++pub fn getScrollbackLine(+ ptr: ?*anyopaque,+ y: c_int,+ out_buffer: [*]GhosttyCell,+ buffer_size: usize,+) callconv(.c) c_int {+ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return -1));++ const y_usize: usize = @intCast(y);+ + // Get scrollback length to validate bounds+ const active_pin = wrapper.terminal.screen.pages.getTopLeft(.active);+ const screen_pin = wrapper.terminal.screen.pages.getTopLeft(.screen);+ + // Count total scrollback rows+ var scrollback_len: usize = 0;+ var pin = screen_pin;+ while (pin.node != active_pin.node or pin.y != active_pin.y) {+ scrollback_len += 1;+ pin = pin.down(1) orelse break;+ if (scrollback_len > 100000) return -1; // Safety limit+ }+ + // Validate y is within scrollback bounds+ if (y_usize >= scrollback_len) return -1;+ + const cols = wrapper.terminal.cols;+ if (buffer_size < cols) return -1;+ + // Get cells from scrollback using screen coordinates+ // Screen coordinates: y=0 is the oldest line in scrollback, increasing toward active area+ var x: usize = 0;+ while (x < cols) : (x += 1) {+ const pt = point.Point{ .screen = .{+ .x = @intCast(x),+ .y = @intCast(y_usize),+ } };+ + const list_cell = wrapper.terminal.screen.pages.getCell(pt);+ const cell = if (list_cell) |lc| lc.cell.* else Cell{};+ out_buffer[x] = convertCell(wrapper, cell, list_cell);+ }+ + return @intCast(cols);+}++// ============================================================================+// Dirty Tracking+// ============================================================================++pub fn isDirty(ptr: ?*anyopaque) callconv(.c) bool {+ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return false));++ for (wrapper.dirty_rows) |dirty| {+ if (dirty) return true;+ }+ return false;+}++pub fn isRowDirty(ptr: ?*anyopaque, y: c_int) callconv(.c) bool {+ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return false));++ const y_usize: usize = @intCast(y);+ if (y_usize >= wrapper.dirty_rows.len) return false;++ return wrapper.dirty_rows[y_usize];+}++pub fn clearDirty(ptr: ?*anyopaque) callconv(.c) void {+ const wrapper: *TerminalWrapper = @ptrCast(@alignCast(ptr orelse return));+ @memset(wrapper.dirty_rows, false);+}++// ============================================================================+// Tests+// ============================================================================++test "terminal lifecycle" {+ const term = new(80, 24);+ defer free(term);++ try std.testing.expect(term != null);+ try std.testing.expectEqual(@as(c_int, 80), getCols(term));+ try std.testing.expectEqual(@as(c_int, 24), getRows(term));+}++test "terminal write and read" {+ const term = new(80, 24);+ defer free(term);++ // Write "Hello"+ const data = "Hello";+ write(term, data.ptr, data.len);++ // Read first line+ var cells: [80]GhosttyCell = undefined;+ const count = getLine(term, 0, &cells, 80);+ try std.testing.expectEqual(@as(c_int, 80), count);++ // Check first few characters+ try std.testing.expectEqual(@as(u32, 'H'), cells[0].codepoint);+ try std.testing.expectEqual(@as(u32, 'e'), cells[1].codepoint);+ try std.testing.expectEqual(@as(u32, 'l'), cells[2].codepoint);+ try std.testing.expectEqual(@as(u32, 'l'), cells[3].codepoint);+ try std.testing.expectEqual(@as(u32, 'o'), cells[4].codepoint);+}++test "terminal cursor position" {+ const term = new(80, 24);+ defer free(term);++ // Initially at 0, 0+ try std.testing.expectEqual(@as(c_int, 0), getCursorX(term));+ try std.testing.expectEqual(@as(c_int, 0), getCursorY(term));++ // Write "Hello" (5 chars)+ const data = "Hello";+ write(term, data.ptr, data.len);++ // Cursor should have moved+ try std.testing.expectEqual(@as(c_int, 5), getCursorX(term));+ try std.testing.expectEqual(@as(c_int, 0), getCursorY(term));+}++test "terminal dirty tracking" {+ const term = new(80, 24);+ defer free(term);++ // Initially dirty+ try std.testing.expect(isDirty(term));++ // Clear dirty+ clearDirty(term);+ try std.testing.expect(!isDirty(term));++ // Write makes it dirty again+ const data = "X";+ write(term, data.ptr, data.len);+ try std.testing.expect(isDirty(term));+ try std.testing.expect(isRowDirty(term, 0));+}