Something went wrong. Try again.
Ghostty for the web with xterm.js API compatibility
Something went wrong. Try again.
33 kB · 1006 lines
at patch
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007From bfd067dacae5bd42e78ce710872cdbe4be7072f5 Mon Sep 17 00:00:00 2001From: Jon Ayers <jon@coder.com>Date: Thu, 13 Nov 2025 16:59:28 +0000Subject: [PATCH] Expose more API in wasm for terminal integrationMIME-Version: 1.0Content-Type: text/plain; charset=UTF-8Content-Transfer-Encoding: 8bit
* Add complete C API (include/ghostty/vt/terminal.h) - 16 functions covering lifecycle, I/O, queries, cell access, dirty tracking - Configurable scrollback and colors
* Implement Terminal C wrapper (src/terminal/c/terminal.zig) - 461 lines with comprehensive error handling - Color resolution (palette → RGB) - Dirty tracking for efficient rendering - Cell width mapping corrections for renderer - Built-in unit tests
* Wire up all 16 exports in main.zig and lib_vt.zig
* Build WASM successfully (404 KB, under 500 KB target) - All 16 terminal_* exports verified - Existing exports (sgr, key, osc) preserved--- .gitignore | 1 + include/ghostty/vt.h | 2 + include/ghostty/vt/terminal.h | 373 ++++++++++++++++++++++++++ src/lib_vt.zig | 16 ++ src/terminal/c/main.zig | 19 ++ src/terminal/c/terminal.zig | 484 ++++++++++++++++++++++++++++++++++ 6 files changed, 895 insertions(+) create mode 100644 include/ghostty/vt/terminal.h create mode 100644 src/terminal/c/terminal.zig
diff --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..bfa28e317--- /dev/null+++ b/include/ghostty/vt/terminal.h@@ -0,0 +1,373 @@+/**+ * @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);++/**+ * 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..fd037a6dd 100644--- a/src/lib_vt.zig+++ b/src/lib_vt.zig@@ -137,6 +137,22 @@ 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_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..f55dc1326 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,23 @@ 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_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 +77,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..6461dc9a9--- /dev/null+++ b/src/terminal/c/terminal.zig@@ -0,0 +1,484 @@+//! 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 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 {+ // Not implemented - scrollback access is deferred to Phase 2+ _ = ptr;+ _ = y;+ _ = out_buffer;+ _ = buffer_size;+ return -1;+}++// ============================================================================+// 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));+}--2.51.2