aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/c
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/c
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/c')
-rw-r--r--vendor/libghostty-vt/src/terminal/c/AGENTS.md22
-rw-r--r--vendor/libghostty-vt/src/terminal/c/allocator.zig67
-rw-r--r--vendor/libghostty-vt/src/terminal/c/build_info.zig162
-rw-r--r--vendor/libghostty-vt/src/terminal/c/cell.zig178
-rw-r--r--vendor/libghostty-vt/src/terminal/c/color.zig13
-rw-r--r--vendor/libghostty-vt/src/terminal/c/focus.zig55
-rw-r--r--vendor/libghostty-vt/src/terminal/c/formatter.zig417
-rw-r--r--vendor/libghostty-vt/src/terminal/c/grid_ref.zig156
-rw-r--r--vendor/libghostty-vt/src/terminal/c/key_encode.zig356
-rw-r--r--vendor/libghostty-vt/src/terminal/c/key_event.zig268
-rw-r--r--vendor/libghostty-vt/src/terminal/c/main.zig178
-rw-r--r--vendor/libghostty-vt/src/terminal/c/modes.zig104
-rw-r--r--vendor/libghostty-vt/src/terminal/c/mouse_encode.zig530
-rw-r--r--vendor/libghostty-vt/src/terminal/c/mouse_event.zig155
-rw-r--r--vendor/libghostty-vt/src/terminal/c/osc.zig140
-rw-r--r--vendor/libghostty-vt/src/terminal/c/paste.zig129
-rw-r--r--vendor/libghostty-vt/src/terminal/c/render.zig1382
-rw-r--r--vendor/libghostty-vt/src/terminal/c/result.zig8
-rw-r--r--vendor/libghostty-vt/src/terminal/c/row.zig132
-rw-r--r--vendor/libghostty-vt/src/terminal/c/sgr.zig179
-rw-r--r--vendor/libghostty-vt/src/terminal/c/size_report.zig82
-rw-r--r--vendor/libghostty-vt/src/terminal/c/style.zig134
-rw-r--r--vendor/libghostty-vt/src/terminal/c/terminal.zig2358
-rw-r--r--vendor/libghostty-vt/src/terminal/c/types.zig215
24 files changed, 7420 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/c/AGENTS.md b/vendor/libghostty-vt/src/terminal/c/AGENTS.md
new file mode 100644
index 00000000..63f7fc6c
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/AGENTS.md
@@ -0,0 +1,22 @@
+# libghostty-vt C API
+
+- C API must be designed with ABI compatibility in mind
+- Zig tagged unions must be converted to C ABI compatible unions
+ via `lib.TaggedUnion`.
+- Any functions must be updated all the way through from here to
+ `src/terminal/c/main.zig` to `src/lib_vt.zig` and the headers
+ in `include/ghostty/vt.h`.
+- In `include/ghostty/vt.h`, always sort the header contents by:
+ (1) macros, (2) forward declarations, (3) types, (4) functions
+
+## ABI Compatibility
+
+- Prefer opaque pointers for long-lived objects, such as
+ `GhosttyTerminal`.
+- Structs:
+ - May contain padding bytes if we're confident we'll never grow
+ beyond a certain size.
+ - May use the "sized struct" pattern: an `extern struct` with
+ `size: usize = @sizeOf(Self)` as the first field. In the C header,
+ callers use `GHOSTTY_INIT_SIZED` from `types.h` to zero-initialize and
+ set the size.
diff --git a/vendor/libghostty-vt/src/terminal/c/allocator.zig b/vendor/libghostty-vt/src/terminal/c/allocator.zig
new file mode 100644
index 00000000..ca698f3c
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/allocator.zig
@@ -0,0 +1,67 @@
+const std = @import("std");
+const testing = std.testing;
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+
+/// Allocate a buffer of `len` bytes using the given allocator
+/// (or the default allocator if NULL).
+///
+/// Returns a pointer to the allocated buffer, or NULL if the
+/// allocation failed.
+pub fn alloc(
+ alloc_: ?*const CAllocator,
+ len: usize,
+) callconv(lib.calling_conv) ?[*]u8 {
+ const allocator = lib.alloc.default(alloc_);
+ const buf = allocator.alloc(u8, len) catch return null;
+ return buf.ptr;
+}
+
+/// Free memory that was allocated by a libghostty-vt function.
+///
+/// This must be used to free buffers returned by functions like
+/// `format_alloc`. Pass the same allocator (or NULL for the default)
+/// that was used for the allocation.
+pub fn free(
+ alloc_: ?*const CAllocator,
+ ptr: ?[*]u8,
+ len: usize,
+) callconv(lib.calling_conv) void {
+ const mem = ptr orelse return;
+ const allocator = lib.alloc.default(alloc_);
+ allocator.free(mem[0..len]);
+}
+
+test "alloc returns non-null" {
+ const ptr = alloc(&lib.alloc.test_allocator, 16);
+ try testing.expect(ptr != null);
+ free(&lib.alloc.test_allocator, ptr, 16);
+}
+
+test "alloc with null allocator" {
+ const ptr = alloc(null, 8);
+ try testing.expect(ptr != null);
+ free(null, ptr, 8);
+}
+
+test "alloc zero length" {
+ const ptr = alloc(&lib.alloc.test_allocator, 0);
+ defer free(&lib.alloc.test_allocator, ptr, 0);
+}
+
+test "free null pointer" {
+ free(&lib.alloc.test_allocator, null, 0);
+}
+
+test "free allocated memory" {
+ const allocator = lib.alloc.default(&lib.alloc.test_allocator);
+ const mem = try allocator.alloc(u8, 16);
+ free(&lib.alloc.test_allocator, mem.ptr, mem.len);
+}
+
+test "free with null allocator" {
+ // null allocator falls back to the default (test allocator in tests)
+ const allocator = lib.alloc.default(null);
+ const mem = try allocator.alloc(u8, 8);
+ free(null, mem.ptr, mem.len);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/build_info.zig b/vendor/libghostty-vt/src/terminal/c/build_info.zig
new file mode 100644
index 00000000..bef6760f
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/build_info.zig
@@ -0,0 +1,162 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const lib = @import("../lib.zig");
+const build_options = @import("terminal_options");
+const Result = @import("result.zig").Result;
+
+const log = std.log.scoped(.build_info_c);
+
+/// C: GhosttyOptimizeMode
+pub const OptimizeMode = enum(c_int) {
+ debug = 0,
+ release_safe = 1,
+ release_small = 2,
+ release_fast = 3,
+};
+
+/// C: GhosttyBuildInfo
+pub const BuildInfo = enum(c_int) {
+ invalid = 0,
+ simd = 1,
+ kitty_graphics = 2,
+ tmux_control_mode = 3,
+ optimize = 4,
+ version_string = 5,
+ version_major = 6,
+ version_minor = 7,
+ version_patch = 8,
+ version_build = 9,
+
+ /// Output type expected for querying the data of the given kind.
+ pub fn OutType(comptime self: BuildInfo) type {
+ return switch (self) {
+ .invalid => void,
+ .simd, .kitty_graphics, .tmux_control_mode => bool,
+ .optimize => OptimizeMode,
+ .version_string, .version_build => lib.String,
+ .version_major, .version_minor, .version_patch => usize,
+ };
+ }
+};
+
+pub fn get(
+ data: BuildInfo,
+ out: ?*anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(BuildInfo, @intFromEnum(data)) catch {
+ log.warn("build_info invalid data value={d}", .{@intFromEnum(data)});
+ return .invalid_value;
+ };
+ }
+
+ return switch (data) {
+ .invalid => .invalid_value,
+ inline else => |comptime_data| getTyped(
+ comptime_data,
+ @ptrCast(@alignCast(out)),
+ ),
+ };
+}
+
+fn getTyped(
+ comptime data: BuildInfo,
+ out: *data.OutType(),
+) Result {
+ switch (data) {
+ .invalid => return .invalid_value,
+ .simd => out.* = build_options.simd,
+ .kitty_graphics => out.* = build_options.kitty_graphics,
+ .tmux_control_mode => out.* = build_options.tmux_control_mode,
+ .optimize => out.* = switch (builtin.mode) {
+ .Debug => .debug,
+ .ReleaseSafe => .release_safe,
+ .ReleaseSmall => .release_small,
+ .ReleaseFast => .release_fast,
+ },
+ .version_string => out.* = .{ .ptr = build_options.version_string.ptr, .len = build_options.version_string.len },
+ .version_major => out.* = build_options.version_major,
+ .version_minor => out.* = build_options.version_minor,
+ .version_patch => out.* = build_options.version_patch,
+ .version_build => {
+ if (build_options.version_build) |b| {
+ out.* = .{ .ptr = b.ptr, .len = b.len };
+ } else {
+ out.* = .{ .ptr = "", .len = 0 };
+ }
+ },
+ }
+
+ return .success;
+}
+
+test "get simd" {
+ const testing = std.testing;
+ var value: bool = undefined;
+ try testing.expectEqual(Result.success, get(.simd, @ptrCast(&value)));
+ try testing.expectEqual(build_options.simd, value);
+}
+
+test "get kitty_graphics" {
+ const testing = std.testing;
+ var value: bool = undefined;
+ try testing.expectEqual(Result.success, get(.kitty_graphics, @ptrCast(&value)));
+ try testing.expectEqual(build_options.kitty_graphics, value);
+}
+
+test "get tmux_control_mode" {
+ const testing = std.testing;
+ var value: bool = undefined;
+ try testing.expectEqual(Result.success, get(.tmux_control_mode, @ptrCast(&value)));
+ try testing.expectEqual(build_options.tmux_control_mode, value);
+}
+
+test "get optimize" {
+ const testing = std.testing;
+ var value: OptimizeMode = undefined;
+ try testing.expectEqual(Result.success, get(.optimize, @ptrCast(&value)));
+ try testing.expectEqual(switch (builtin.mode) {
+ .Debug => .debug,
+ .ReleaseSafe => .release_safe,
+ .ReleaseSmall => .release_small,
+ .ReleaseFast => .release_fast,
+ }, value);
+}
+
+test "get version_string" {
+ const testing = std.testing;
+ var value: lib.String = undefined;
+ try testing.expectEqual(Result.success, get(.version_string, @ptrCast(&value)));
+ try testing.expect(value.len > 0);
+}
+
+test "get version_major" {
+ const testing = std.testing;
+ var value: usize = undefined;
+ try testing.expectEqual(Result.success, get(.version_major, @ptrCast(&value)));
+ try testing.expectEqual(build_options.version_major, value);
+}
+
+test "get version_minor" {
+ const testing = std.testing;
+ var value: usize = undefined;
+ try testing.expectEqual(Result.success, get(.version_minor, @ptrCast(&value)));
+ try testing.expectEqual(build_options.version_minor, value);
+}
+
+test "get version_patch" {
+ const testing = std.testing;
+ var value: usize = undefined;
+ try testing.expectEqual(Result.success, get(.version_patch, @ptrCast(&value)));
+ try testing.expectEqual(build_options.version_patch, value);
+}
+
+test "get version_build" {
+ const testing = std.testing;
+ var value: lib.String = undefined;
+ try testing.expectEqual(Result.success, get(.version_build, @ptrCast(&value)));
+}
+
+test "get invalid" {
+ try std.testing.expectEqual(Result.invalid_value, get(.invalid, null));
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/cell.zig b/vendor/libghostty-vt/src/terminal/c/cell.zig
new file mode 100644
index 00000000..c9746da7
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/cell.zig
@@ -0,0 +1,178 @@
+const std = @import("std");
+const testing = std.testing;
+const lib = @import("../lib.zig");
+const page = @import("../page.zig");
+const Cell = page.Cell;
+const color = @import("../color.zig");
+const style_c = @import("style.zig");
+const Result = @import("result.zig").Result;
+
+/// C: GhosttyCell
+pub const CCell = u64;
+
+/// C: GhosttyCellContentTag
+pub const ContentTag = enum(c_int) {
+ codepoint = 0,
+ codepoint_grapheme = 1,
+ bg_color_palette = 2,
+ bg_color_rgb = 3,
+};
+
+/// C: GhosttyCellWide
+pub const Wide = enum(c_int) {
+ narrow = 0,
+ wide = 1,
+ spacer_tail = 2,
+ spacer_head = 3,
+};
+
+/// C: GhosttyCellSemanticContent
+pub const SemanticContent = enum(c_int) {
+ output = 0,
+ input = 1,
+ prompt = 2,
+};
+
+/// C: GhosttyCellData
+pub const CellData = enum(c_int) {
+ invalid = 0,
+
+ /// The codepoint of the cell (0 if empty or bg-color-only).
+ /// Output type: uint32_t * (stored as u21, zero-extended)
+ codepoint = 1,
+
+ /// The content tag describing what kind of content is in the cell.
+ /// Output type: GhosttyCellContentTag *
+ content_tag = 2,
+
+ /// The wide property of the cell.
+ /// Output type: GhosttyCellWide *
+ wide = 3,
+
+ /// Whether the cell has text to render.
+ /// Output type: bool *
+ has_text = 4,
+
+ /// Whether the cell has styling (non-default style).
+ /// Output type: bool *
+ has_styling = 5,
+
+ /// The style ID for the cell (for use with style lookups).
+ /// Output type: uint16_t *
+ style_id = 6,
+
+ /// Whether the cell has a hyperlink.
+ /// Output type: bool *
+ has_hyperlink = 7,
+
+ /// Whether the cell is protected.
+ /// Output type: bool *
+ protected = 8,
+
+ /// The semantic content type of the cell (from OSC 133).
+ /// Output type: GhosttyCellSemanticContent *
+ semantic_content = 9,
+
+ /// The palette index for the cell's background color.
+ /// Only valid when content_tag is bg_color_palette.
+ /// Output type: GhosttyColorPaletteIndex *
+ color_palette = 10,
+
+ /// The RGB value for the cell's background color.
+ /// Only valid when content_tag is bg_color_rgb.
+ /// Output type: GhosttyColorRgb *
+ color_rgb = 11,
+
+ /// Output type expected for querying the data of the given kind.
+ pub fn OutType(comptime self: CellData) type {
+ return switch (self) {
+ .invalid => void,
+ .codepoint => u32,
+ .content_tag => ContentTag,
+ .wide => Wide,
+ .has_text, .has_styling, .has_hyperlink, .protected => bool,
+ .style_id => u16,
+ .semantic_content => SemanticContent,
+ .color_palette => u8,
+ .color_rgb => color.RGB.C,
+ };
+ }
+};
+
+pub fn get(
+ cell_: CCell,
+ data: CellData,
+ out: ?*anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(CellData, @intFromEnum(data)) catch {
+ return .invalid_value;
+ };
+ }
+
+ return switch (data) {
+ .invalid => .invalid_value,
+ inline else => |comptime_data| getTyped(
+ cell_,
+ comptime_data,
+ @ptrCast(@alignCast(out)),
+ ),
+ };
+}
+
+fn getTyped(
+ cell_: CCell,
+ comptime data: CellData,
+ out: *data.OutType(),
+) Result {
+ const cell: Cell = @bitCast(cell_);
+ switch (data) {
+ .invalid => return .invalid_value,
+ .codepoint => out.* = @intCast(cell.codepoint()),
+ .content_tag => out.* = @enumFromInt(@intFromEnum(cell.content_tag)),
+ .wide => out.* = @enumFromInt(@intFromEnum(cell.wide)),
+ .has_text => out.* = cell.hasText(),
+ .has_styling => out.* = cell.hasStyling(),
+ .style_id => out.* = cell.style_id,
+ .has_hyperlink => out.* = cell.hyperlink,
+ .protected => out.* = cell.protected,
+ .semantic_content => out.* = @enumFromInt(@intFromEnum(cell.semantic_content)),
+ .color_palette => out.* = cell.content.color_palette,
+ .color_rgb => {
+ const rgb = cell.content.color_rgb;
+ out.* = .{ .r = rgb.r, .g = rgb.g, .b = rgb.b };
+ },
+ }
+
+ return .success;
+}
+
+test "get codepoint" {
+ const cell: CCell = @bitCast(Cell.init('A'));
+ var cp: u32 = 0;
+ try testing.expectEqual(Result.success, get(cell, .codepoint, @ptrCast(&cp)));
+ try testing.expectEqual(@as(u32, 'A'), cp);
+}
+
+test "get has_text" {
+ const cell: CCell = @bitCast(Cell.init('A'));
+ var has: bool = false;
+ try testing.expectEqual(Result.success, get(cell, .has_text, @ptrCast(&has)));
+ try testing.expect(has);
+}
+
+test "get empty cell" {
+ const cell: CCell = @bitCast(Cell.init(0));
+ var has: bool = true;
+ try testing.expectEqual(Result.success, get(cell, .has_text, @ptrCast(&has)));
+ try testing.expect(!has);
+}
+
+test "get wide" {
+ var zig_cell = Cell.init('A');
+ zig_cell.wide = .wide;
+ const cell: CCell = @bitCast(zig_cell);
+ var w: Wide = .narrow;
+ try testing.expectEqual(Result.success, get(cell, .wide, @ptrCast(&w)));
+ try testing.expectEqual(Wide.wide, w);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/color.zig b/vendor/libghostty-vt/src/terminal/c/color.zig
new file mode 100644
index 00000000..3d244a19
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/color.zig
@@ -0,0 +1,13 @@
+const lib = @import("../lib.zig");
+const color = @import("../color.zig");
+
+pub fn rgb_get(
+ c: color.RGB.C,
+ r: *u8,
+ g: *u8,
+ b: *u8,
+) callconv(lib.calling_conv) void {
+ r.* = c.r;
+ g.* = c.g;
+ b.* = c.b;
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/focus.zig b/vendor/libghostty-vt/src/terminal/c/focus.zig
new file mode 100644
index 00000000..2c53191d
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/focus.zig
@@ -0,0 +1,55 @@
+const std = @import("std");
+const lib = @import("../lib.zig");
+const terminal_focus = @import("../focus.zig");
+const Result = @import("result.zig").Result;
+
+pub fn encode(
+ event: terminal_focus.Event,
+ out_: ?[*]u8,
+ out_len: usize,
+ out_written: *usize,
+) callconv(lib.calling_conv) Result {
+ var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{});
+ terminal_focus.encode(&writer, event) catch |err| switch (err) {
+ error.WriteFailed => {
+ var discarding: std.Io.Writer.Discarding = .init(&.{});
+ terminal_focus.encode(&discarding.writer, event) catch unreachable;
+ out_written.* = @intCast(discarding.count);
+ return .out_of_space;
+ },
+ };
+
+ out_written.* = writer.end;
+ return .success;
+}
+
+test "encode focus gained" {
+ var buf: [terminal_focus.max_encode_size]u8 = undefined;
+ var written: usize = 0;
+ const result = encode(.gained, &buf, buf.len, &written);
+ try std.testing.expectEqual(.success, result);
+ try std.testing.expectEqualStrings("\x1B[I", buf[0..written]);
+}
+
+test "encode focus lost" {
+ var buf: [terminal_focus.max_encode_size]u8 = undefined;
+ var written: usize = 0;
+ const result = encode(.lost, &buf, buf.len, &written);
+ try std.testing.expectEqual(.success, result);
+ try std.testing.expectEqualStrings("\x1B[O", buf[0..written]);
+}
+
+test "encode with insufficient buffer" {
+ var buf: [1]u8 = undefined;
+ var written: usize = 0;
+ const result = encode(.gained, &buf, buf.len, &written);
+ try std.testing.expectEqual(.out_of_space, result);
+ try std.testing.expectEqual(terminal_focus.max_encode_size, written);
+}
+
+test "encode with null buffer" {
+ var written: usize = 0;
+ const result = encode(.gained, null, 0, &written);
+ try std.testing.expectEqual(.out_of_space, result);
+ try std.testing.expectEqual(terminal_focus.max_encode_size, written);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/formatter.zig b/vendor/libghostty-vt/src/terminal/c/formatter.zig
new file mode 100644
index 00000000..11717bc2
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/formatter.zig
@@ -0,0 +1,417 @@
+const std = @import("std");
+const testing = std.testing;
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const terminal_c = @import("terminal.zig");
+const ZigTerminal = @import("../Terminal.zig");
+const formatterpkg = @import("../formatter.zig");
+const Result = @import("result.zig").Result;
+
+/// Wrapper around formatter that tracks the allocator for C API usage.
+const FormatterWrapper = struct {
+ kind: Kind,
+ alloc: std.mem.Allocator,
+
+ const Kind = union(enum) {
+ terminal: formatterpkg.TerminalFormatter,
+ };
+};
+
+/// C: GhosttyFormatter
+pub const Formatter = ?*FormatterWrapper;
+
+/// C: GhosttyFormatterFormat
+pub const Format = formatterpkg.Format;
+
+/// C: GhosttyFormatterScreenOptions
+pub const ScreenOptions = extern struct {
+ /// C: GhosttyFormatterScreenExtra
+ pub const Extra = extern struct {
+ size: usize = @sizeOf(Extra),
+ cursor: bool,
+ style: bool,
+ hyperlink: bool,
+ protection: bool,
+ kitty_keyboard: bool,
+ charsets: bool,
+
+ comptime {
+ for (std.meta.fieldNames(formatterpkg.ScreenFormatter.Extra)) |name| {
+ if (!@hasField(Extra, name))
+ @compileError("ScreenOptions.Extra missing field: " ++ name);
+ }
+ }
+
+ fn toZig(self: Extra) formatterpkg.ScreenFormatter.Extra {
+ return .{
+ .cursor = self.cursor,
+ .style = self.style,
+ .hyperlink = self.hyperlink,
+ .protection = self.protection,
+ .kitty_keyboard = self.kitty_keyboard,
+ .charsets = self.charsets,
+ };
+ }
+ };
+};
+
+/// C: GhosttyFormatterTerminalOptions
+pub const TerminalOptions = extern struct {
+ size: usize = @sizeOf(TerminalOptions),
+ emit: Format,
+ unwrap: bool,
+ trim: bool,
+ extra: Extra,
+
+ /// C: GhosttyFormatterTerminalExtra
+ pub const Extra = extern struct {
+ size: usize = @sizeOf(Extra),
+ palette: bool,
+ modes: bool,
+ scrolling_region: bool,
+ tabstops: bool,
+ pwd: bool,
+ keyboard: bool,
+ screen: ScreenOptions.Extra,
+
+ comptime {
+ for (std.meta.fieldNames(formatterpkg.TerminalFormatter.Extra)) |name| {
+ if (!@hasField(Extra, name))
+ @compileError("TerminalOptions.Extra missing field: " ++ name);
+ }
+ }
+
+ fn toZig(self: Extra) formatterpkg.TerminalFormatter.Extra {
+ return .{
+ .palette = self.palette,
+ .modes = self.modes,
+ .scrolling_region = self.scrolling_region,
+ .tabstops = self.tabstops,
+ .pwd = self.pwd,
+ .keyboard = self.keyboard,
+ .screen = self.screen.toZig(),
+ };
+ }
+ };
+};
+
+pub fn terminal_new(
+ alloc_: ?*const CAllocator,
+ result: *Formatter,
+ terminal_: terminal_c.Terminal,
+ opts: TerminalOptions,
+) callconv(lib.calling_conv) Result {
+ result.* = terminal_new_(
+ alloc_,
+ terminal_,
+ opts,
+ ) catch |err| {
+ result.* = null;
+ return switch (err) {
+ error.InvalidValue => .invalid_value,
+ error.OutOfMemory => .out_of_memory,
+ };
+ };
+
+ return .success;
+}
+
+fn terminal_new_(
+ alloc_: ?*const CAllocator,
+ terminal_: terminal_c.Terminal,
+ opts: TerminalOptions,
+) error{
+ InvalidValue,
+ OutOfMemory,
+}!*FormatterWrapper {
+ const t: *ZigTerminal = (terminal_ orelse return error.InvalidValue).terminal;
+
+ const alloc = lib.alloc.default(alloc_);
+ const ptr = alloc.create(FormatterWrapper) catch
+ return error.OutOfMemory;
+ errdefer alloc.destroy(ptr);
+
+ var formatter: formatterpkg.TerminalFormatter = .init(t, .{
+ .emit = opts.emit,
+ .unwrap = opts.unwrap,
+ .trim = opts.trim,
+ });
+ formatter.extra = opts.extra.toZig();
+
+ ptr.* = .{
+ .kind = .{ .terminal = formatter },
+ .alloc = alloc,
+ };
+
+ return ptr;
+}
+
+pub fn format_buf(
+ formatter_: Formatter,
+ out_: ?[*]u8,
+ out_len: usize,
+ out_written: *usize,
+) callconv(lib.calling_conv) Result {
+ const wrapper = formatter_ orelse return .invalid_value;
+
+ var writer: std.Io.Writer = .fixed(if (out_) |out|
+ out[0..out_len]
+ else
+ &.{});
+
+ switch (wrapper.kind) {
+ .terminal => |*t| t.format(&writer) catch |err| switch (err) {
+ error.WriteFailed => {
+ // On write failed we always report how much
+ // space we actually needed.
+ var discarding: std.Io.Writer.Discarding = .init(&.{});
+ t.format(&discarding.writer) catch unreachable;
+ out_written.* = @intCast(discarding.count);
+ return .out_of_space;
+ },
+ },
+ }
+
+ out_written.* = writer.end;
+ return .success;
+}
+
+pub fn format_alloc(
+ formatter_: Formatter,
+ alloc_: ?*const CAllocator,
+ out_ptr: *?[*]u8,
+ out_len: *usize,
+) callconv(lib.calling_conv) Result {
+ const wrapper = formatter_ orelse return .invalid_value;
+ const alloc = lib.alloc.default(alloc_);
+
+ var aw: std.Io.Writer.Allocating = .init(alloc);
+ defer aw.deinit();
+
+ switch (wrapper.kind) {
+ .terminal => |*t| t.format(&aw.writer) catch return .out_of_memory,
+ }
+
+ const buf = aw.toOwnedSlice() catch return .out_of_memory;
+ out_ptr.* = buf.ptr;
+ out_len.* = buf.len;
+ return .success;
+}
+
+pub fn free(formatter_: Formatter) callconv(lib.calling_conv) void {
+ const wrapper = formatter_ orelse return;
+ const alloc = wrapper.alloc;
+ alloc.destroy(wrapper);
+}
+
+test "terminal_new/free" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ var f: Formatter = null;
+ try testing.expectEqual(Result.success, terminal_new(
+ &lib.alloc.test_allocator,
+ &f,
+ t,
+ .{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
+ ));
+ try testing.expect(f != null);
+ free(f);
+}
+
+test "terminal_new invalid_value on null terminal" {
+ var f: Formatter = null;
+ try testing.expectEqual(Result.invalid_value, terminal_new(
+ &lib.alloc.test_allocator,
+ &f,
+ null,
+ .{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
+ ));
+ try testing.expect(f == null);
+}
+
+test "free null" {
+ free(null);
+}
+
+test "format plain" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ terminal_c.vt_write(t, "Hello", 5);
+
+ var f: Formatter = null;
+ try testing.expectEqual(Result.success, terminal_new(
+ &lib.alloc.test_allocator,
+ &f,
+ t,
+ .{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
+ ));
+ defer free(f);
+
+ var buf: [1024]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
+ try testing.expectEqualStrings("Hello", buf[0..written]);
+}
+
+test "format reflects terminal changes" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ terminal_c.vt_write(t, "Hello", 5);
+
+ var f: Formatter = null;
+ try testing.expectEqual(Result.success, terminal_new(
+ &lib.alloc.test_allocator,
+ &f,
+ t,
+ .{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
+ ));
+ defer free(f);
+
+ var buf: [1024]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
+ try testing.expectEqualStrings("Hello", buf[0..written]);
+
+ // Write more data and re-format
+ terminal_c.vt_write(t, "\r\nWorld", 7);
+
+ try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
+ try testing.expectEqualStrings("Hello\nWorld", buf[0..written]);
+}
+
+test "format null returns required size" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ terminal_c.vt_write(t, "Hello", 5);
+
+ var f: Formatter = null;
+ try testing.expectEqual(Result.success, terminal_new(
+ &lib.alloc.test_allocator,
+ &f,
+ t,
+ .{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
+ ));
+ defer free(f);
+
+ // Pass null buffer to query required size
+ var required: usize = 0;
+ try testing.expectEqual(Result.out_of_space, format_buf(f, null, 0, &required));
+ try testing.expect(required > 0);
+
+ // Now allocate and format
+ var buf: [1024]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
+ try testing.expectEqual(required, written);
+}
+
+test "format buffer too small" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ terminal_c.vt_write(t, "Hello", 5);
+
+ var f: Formatter = null;
+ try testing.expectEqual(Result.success, terminal_new(
+ &lib.alloc.test_allocator,
+ &f,
+ t,
+ .{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
+ ));
+ defer free(f);
+
+ // Buffer too small
+ var buf: [2]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.out_of_space, format_buf(f, &buf, buf.len, &written));
+ // written contains the required size
+ try testing.expectEqual(@as(usize, 5), written);
+}
+
+test "format null formatter" {
+ var written: usize = 0;
+ try testing.expectEqual(Result.invalid_value, format_buf(null, null, 0, &written));
+}
+
+test "format vt" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ terminal_c.vt_write(t, "Test", 4);
+
+ var f: Formatter = null;
+ try testing.expectEqual(Result.success, terminal_new(
+ &lib.alloc.test_allocator,
+ &f,
+ t,
+ .{ .emit = .vt, .unwrap = false, .trim = true, .extra = .{ .palette = true, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = true, .hyperlink = true, .protection = false, .kitty_keyboard = false, .charsets = false } } },
+ ));
+ defer free(f);
+
+ var buf: [65536]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
+ try testing.expect(written > 0);
+ try testing.expect(std.mem.indexOf(u8, buf[0..written], "Test") != null);
+}
+
+test "format html" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ terminal_c.vt_write(t, "Html", 4);
+
+ var f: Formatter = null;
+ try testing.expectEqual(Result.success, terminal_new(
+ &lib.alloc.test_allocator,
+ &f,
+ t,
+ .{ .emit = .html, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
+ ));
+ defer free(f);
+
+ var buf: [65536]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
+ try testing.expect(written > 0);
+ try testing.expect(std.mem.indexOf(u8, buf[0..written], "Html") != null);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/grid_ref.zig b/vendor/libghostty-vt/src/terminal/c/grid_ref.zig
new file mode 100644
index 00000000..d029c595
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/grid_ref.zig
@@ -0,0 +1,156 @@
+const std = @import("std");
+const testing = std.testing;
+const lib = @import("../lib.zig");
+const page = @import("../page.zig");
+const PageList = @import("../PageList.zig");
+const size = @import("../size.zig");
+const stylepkg = @import("../style.zig");
+const cell_c = @import("cell.zig");
+const row_c = @import("row.zig");
+const style_c = @import("style.zig");
+const Result = @import("result.zig").Result;
+
+/// C: GhosttyGridRef
+///
+/// A sized struct that holds a reference to a position in the terminal grid.
+/// The ref points to a specific cell position within the terminal's
+/// internal page structure.
+pub const CGridRef = extern struct {
+ size: usize = @sizeOf(CGridRef),
+ node: ?*PageList.List.Node = null,
+ x: size.CellCountInt = 0,
+ y: size.CellCountInt = 0,
+
+ pub fn fromPin(pin: PageList.Pin) CGridRef {
+ return .{
+ .node = pin.node,
+ .x = pin.x,
+ .y = pin.y,
+ };
+ }
+
+ fn toPin(self: CGridRef) ?PageList.Pin {
+ return .{
+ .node = self.node orelse return null,
+ .x = self.x,
+ .y = self.y,
+ };
+ }
+};
+
+pub fn grid_ref_cell(
+ ref: *const CGridRef,
+ out: ?*cell_c.CCell,
+) callconv(lib.calling_conv) Result {
+ const p = ref.toPin() orelse return .invalid_value;
+ if (out) |o| o.* = @bitCast(p.rowAndCell().cell.*);
+ return .success;
+}
+
+pub fn grid_ref_row(
+ ref: *const CGridRef,
+ out: ?*row_c.CRow,
+) callconv(lib.calling_conv) Result {
+ const p = ref.toPin() orelse return .invalid_value;
+ if (out) |o| o.* = @bitCast(p.rowAndCell().row.*);
+ return .success;
+}
+
+pub fn grid_ref_graphemes(
+ ref: *const CGridRef,
+ out_buf: ?[*]u32,
+ buf_len: usize,
+ out_len: *usize,
+) callconv(lib.calling_conv) Result {
+ const p = ref.toPin() orelse return .invalid_value;
+ const cell = p.rowAndCell().cell;
+
+ if (!cell.hasText()) {
+ out_len.* = 0;
+ return .success;
+ }
+
+ const cp = cell.codepoint();
+ const extra = if (cell.hasGrapheme()) p.grapheme(cell) else null;
+ const total = 1 + if (extra) |e| e.len else 0;
+
+ if (out_buf == null or buf_len < total) {
+ out_len.* = total;
+ return .out_of_space;
+ }
+
+ const buf = out_buf.?[0..buf_len];
+ buf[0] = cp;
+ if (extra) |e| for (e, 1..) |c, i| {
+ buf[i] = c;
+ };
+
+ out_len.* = total;
+ return .success;
+}
+
+pub fn grid_ref_style(
+ ref: *const CGridRef,
+ out: ?*style_c.Style,
+) callconv(lib.calling_conv) Result {
+ const p = ref.toPin() orelse return .invalid_value;
+ if (out) |o| {
+ const cell = p.rowAndCell().cell;
+ if (cell.style_id == stylepkg.default_id) {
+ o.* = .fromStyle(.{});
+ } else {
+ o.* = .fromStyle(p.node.data.styles.get(
+ p.node.data.memory,
+ cell.style_id,
+ ).*);
+ }
+ }
+ return .success;
+}
+
+test "grid_ref_cell null node" {
+ const ref = CGridRef{};
+ var out: cell_c.CCell = undefined;
+ try testing.expectEqual(Result.invalid_value, grid_ref_cell(&ref, &out));
+}
+
+test "grid_ref_row null node" {
+ const ref = CGridRef{};
+ var out: row_c.CRow = undefined;
+ try testing.expectEqual(Result.invalid_value, grid_ref_row(&ref, &out));
+}
+
+test "grid_ref_cell null out" {
+ const ref = CGridRef{};
+ try testing.expectEqual(Result.invalid_value, grid_ref_cell(&ref, null));
+}
+
+test "grid_ref_row null out" {
+ const ref = CGridRef{};
+ try testing.expectEqual(Result.invalid_value, grid_ref_row(&ref, null));
+}
+
+test "grid_ref_graphemes null node" {
+ const ref = CGridRef{};
+ var len: usize = undefined;
+ try testing.expectEqual(Result.invalid_value, grid_ref_graphemes(&ref, null, 0, &len));
+}
+
+test "grid_ref_graphemes null buf returns out_of_space" {
+ const ref = CGridRef{};
+ var len: usize = undefined;
+ // With null node this returns invalid_value before checking the buffer,
+ // so we can only test null node here. Full buffer tests require a real page.
+ try testing.expectEqual(Result.invalid_value, grid_ref_graphemes(&ref, null, 0, &len));
+}
+
+test "grid_ref_style null node" {
+ const ref = CGridRef{};
+ var out: style_c.Style = undefined;
+ try testing.expectEqual(Result.invalid_value, grid_ref_style(&ref, &out));
+}
+
+test "grid_ref_style null out" {
+ const ref = CGridRef{};
+ try testing.expectEqual(Result.invalid_value, grid_ref_style(&ref, null));
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/key_encode.zig b/vendor/libghostty-vt/src/terminal/c/key_encode.zig
new file mode 100644
index 00000000..15fa74dd
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/key_encode.zig
@@ -0,0 +1,356 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const key_encode = @import("../../input/key_encode.zig");
+const key_event = @import("key_event.zig");
+const KittyFlags = @import("../../terminal/kitty/key.zig").Flags;
+const OptionAsAlt = @import("../../input/config.zig").OptionAsAlt;
+const Result = @import("result.zig").Result;
+const KeyEvent = @import("key_event.zig").Event;
+const Terminal = @import("terminal.zig").Terminal;
+const ZigTerminal = @import("../Terminal.zig");
+
+const log = std.log.scoped(.key_encode);
+
+/// Wrapper around key encoding options that tracks the allocator for C API usage.
+const KeyEncoderWrapper = struct {
+ opts: key_encode.Options,
+ alloc: Allocator,
+};
+
+/// C: GhosttyKeyEncoder
+pub const Encoder = ?*KeyEncoderWrapper;
+
+pub fn new(
+ alloc_: ?*const CAllocator,
+ result: *Encoder,
+) callconv(lib.calling_conv) Result {
+ const alloc = lib.alloc.default(alloc_);
+ const ptr = alloc.create(KeyEncoderWrapper) catch
+ return .out_of_memory;
+ ptr.* = .{
+ .opts = .{},
+ .alloc = alloc,
+ };
+ result.* = ptr;
+ return .success;
+}
+
+pub fn free(encoder_: Encoder) callconv(lib.calling_conv) void {
+ const wrapper = encoder_ orelse return;
+ const alloc = wrapper.alloc;
+ alloc.destroy(wrapper);
+}
+
+/// C: GhosttyKeyEncoderOption
+pub const Option = enum(c_int) {
+ cursor_key_application = 0,
+ keypad_key_application = 1,
+ ignore_keypad_with_numlock = 2,
+ alt_esc_prefix = 3,
+ modify_other_keys_state_2 = 4,
+ kitty_flags = 5,
+ macos_option_as_alt = 6,
+
+ /// Input type expected for setting the option.
+ pub fn InType(comptime self: Option) type {
+ return switch (self) {
+ .cursor_key_application,
+ .keypad_key_application,
+ .ignore_keypad_with_numlock,
+ .alt_esc_prefix,
+ .modify_other_keys_state_2,
+ => bool,
+ .kitty_flags => u8,
+ .macos_option_as_alt => OptionAsAlt,
+ };
+ }
+};
+
+pub fn setopt(
+ encoder_: Encoder,
+ option: Option,
+ value: ?*const anyopaque,
+) callconv(lib.calling_conv) void {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(Option, @intFromEnum(option)) catch {
+ log.warn("setopt invalid option value={d}", .{@intFromEnum(option)});
+ return;
+ };
+ }
+
+ return switch (option) {
+ inline else => |comptime_option| setoptTyped(
+ encoder_,
+ comptime_option,
+ @ptrCast(@alignCast(value orelse return)),
+ ),
+ };
+}
+
+fn setoptTyped(
+ encoder_: Encoder,
+ comptime option: Option,
+ value: *const option.InType(),
+) void {
+ const opts = &encoder_.?.opts;
+ switch (option) {
+ .cursor_key_application => opts.cursor_key_application = value.*,
+ .keypad_key_application => opts.keypad_key_application = value.*,
+ .ignore_keypad_with_numlock => opts.ignore_keypad_with_numlock = value.*,
+ .alt_esc_prefix => opts.alt_esc_prefix = value.*,
+ .modify_other_keys_state_2 => opts.modify_other_keys_state_2 = value.*,
+ .kitty_flags => opts.kitty_flags = flags: {
+ const bits: u5 = @truncate(value.*);
+ break :flags @bitCast(bits);
+ },
+ .macos_option_as_alt => {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(OptionAsAlt, @intFromEnum(value.*)) catch {
+ log.warn("setopt invalid OptionAsAlt value={d}", .{@intFromEnum(value.*)});
+ return;
+ };
+ }
+ opts.macos_option_as_alt = value.*;
+ },
+ }
+}
+
+pub fn setopt_from_terminal(
+ encoder_: Encoder,
+ terminal_: Terminal,
+) callconv(lib.calling_conv) void {
+ const wrapper = encoder_ orelse return;
+ const t: *ZigTerminal = (terminal_ orelse return).terminal;
+ wrapper.opts = .fromTerminal(t);
+}
+
+pub fn encode(
+ encoder_: Encoder,
+ event_: KeyEvent,
+ out_: ?[*]u8,
+ out_len: usize,
+ out_written: *usize,
+) callconv(lib.calling_conv) Result {
+ // Attempt to write to this buffer
+ var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{});
+ key_encode.encode(
+ &writer,
+ event_.?.event,
+ encoder_.?.opts,
+ ) catch |err| switch (err) {
+ error.WriteFailed => {
+ // If we don't have space, use a discarding writer to count
+ // how much space we would have needed.
+ var discarding: std.Io.Writer.Discarding = .init(&.{});
+ key_encode.encode(
+ &discarding.writer,
+ event_.?.event,
+ encoder_.?.opts,
+ ) catch unreachable;
+
+ // Discarding always uses a u64. If we're on 32-bit systems
+ // we cast down. We should make this safer in the future.
+ out_written.* = @intCast(discarding.count);
+ return .out_of_space;
+ },
+ };
+
+ out_written.* = writer.end;
+ return .success;
+}
+
+test "alloc" {
+ const testing = std.testing;
+ var e: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ free(e);
+}
+
+test "setopt bool" {
+ const testing = std.testing;
+ var e: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ defer free(e);
+
+ // Test setting bool options
+ const val_true: bool = true;
+ setopt(e, .cursor_key_application, &val_true);
+ try testing.expect(e.?.opts.cursor_key_application);
+
+ const val_false: bool = false;
+ setopt(e, .cursor_key_application, &val_false);
+ try testing.expect(!e.?.opts.cursor_key_application);
+
+ setopt(e, .keypad_key_application, &val_true);
+ try testing.expect(e.?.opts.keypad_key_application);
+}
+
+test "setopt kitty flags" {
+ const testing = std.testing;
+ var e: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ defer free(e);
+
+ // Test setting kitty flags
+ const flags: KittyFlags = .{
+ .disambiguate = true,
+ .report_events = true,
+ };
+ const flags_int: u8 = @intCast(flags.int());
+ setopt(e, .kitty_flags, &flags_int);
+ try testing.expect(e.?.opts.kitty_flags.disambiguate);
+ try testing.expect(e.?.opts.kitty_flags.report_events);
+ try testing.expect(!e.?.opts.kitty_flags.report_alternates);
+}
+
+test "setopt macos option as alt" {
+ const testing = std.testing;
+ var e: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ defer free(e);
+
+ // Test setting option as alt
+ const opt_left: OptionAsAlt = .left;
+ setopt(e, .macos_option_as_alt, &opt_left);
+ try testing.expectEqual(OptionAsAlt.left, e.?.opts.macos_option_as_alt);
+
+ const opt_true: OptionAsAlt = .true;
+ setopt(e, .macos_option_as_alt, &opt_true);
+ try testing.expectEqual(OptionAsAlt.true, e.?.opts.macos_option_as_alt);
+}
+
+test "setopt_from_terminal" {
+ const testing = std.testing;
+ const terminal_c = @import("terminal.zig");
+
+ // Create encoder
+ var e: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ defer free(e);
+
+ // Create terminal
+ var t: Terminal = undefined;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ // Apply terminal state to encoder
+ setopt_from_terminal(e, t);
+
+ // Options should reflect defaults from a fresh terminal
+ try testing.expect(!e.?.opts.cursor_key_application);
+ try testing.expect(e.?.opts.alt_esc_prefix);
+ try testing.expectEqual(KittyFlags.disabled, e.?.opts.kitty_flags);
+ try testing.expectEqual(OptionAsAlt.false, e.?.opts.macos_option_as_alt);
+}
+
+test "setopt_from_terminal null" {
+ // Both null should be no-ops
+ setopt_from_terminal(null, null);
+
+ const testing = std.testing;
+
+ // Encoder null with valid terminal
+ const terminal_c = @import("terminal.zig");
+ var t: Terminal = undefined;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 0 },
+ ));
+ defer terminal_c.free(t);
+ setopt_from_terminal(null, t);
+
+ // Valid encoder with null terminal
+ var e: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ defer free(e);
+ setopt_from_terminal(e, null);
+}
+
+test "encode: kitty ctrl release with ctrl mod set" {
+ const testing = std.testing;
+
+ // Create encoder
+ var encoder: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &encoder,
+ ));
+ defer free(encoder);
+
+ // Set kitty flags with all features enabled
+ {
+ const flags: KittyFlags = .{
+ .disambiguate = true,
+ .report_events = true,
+ .report_alternates = true,
+ .report_all = true,
+ .report_associated = true,
+ };
+ const flags_int: u8 = @intCast(flags.int());
+ setopt(encoder, .kitty_flags, &flags_int);
+ }
+
+ // Create key event
+ var event: key_event.Event = undefined;
+ try testing.expectEqual(Result.success, key_event.new(
+ &lib.alloc.test_allocator,
+ &event,
+ ));
+ defer key_event.free(event);
+
+ // Set event properties: release action, ctrl key, ctrl modifier
+ key_event.set_action(event, .release);
+ key_event.set_key(event, .control_left);
+ key_event.set_mods(event, .{ .ctrl = true });
+
+ // Encode null should give us the length required
+ var required: usize = 0;
+ try testing.expectEqual(Result.out_of_space, encode(
+ encoder,
+ event,
+ null,
+ 0,
+ &required,
+ ));
+
+ // Encode the key event
+ var buf: [128]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.success, encode(
+ encoder,
+ event,
+ &buf,
+ buf.len,
+ &written,
+ ));
+ try testing.expectEqual(required, written);
+
+ // Expected: ESC[57442;5:3u (ctrl key code with mods and release event)
+ const actual = buf[0..written];
+ try testing.expectEqualStrings("\x1b[57442;5:3u", actual);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/key_event.zig b/vendor/libghostty-vt/src/terminal/c/key_event.zig
new file mode 100644
index 00000000..1feac9ac
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/key_event.zig
@@ -0,0 +1,268 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const key = @import("../../input/key.zig");
+const Result = @import("result.zig").Result;
+
+const log = std.log.scoped(.key_event);
+
+/// Wrapper around KeyEvent that tracks the allocator for C API usage.
+/// The UTF-8 text is not owned by this wrapper - the caller is responsible
+/// for ensuring the lifetime of any UTF-8 text set via set_utf8.
+const KeyEventWrapper = struct {
+ event: key.KeyEvent = .{},
+ alloc: Allocator,
+};
+
+/// C: GhosttyKeyEvent
+pub const Event = ?*KeyEventWrapper;
+
+pub fn new(
+ alloc_: ?*const CAllocator,
+ result: *Event,
+) callconv(lib.calling_conv) Result {
+ const alloc = lib.alloc.default(alloc_);
+ const ptr = alloc.create(KeyEventWrapper) catch
+ return .out_of_memory;
+ ptr.* = .{ .alloc = alloc };
+ result.* = ptr;
+ return .success;
+}
+
+pub fn free(event_: Event) callconv(lib.calling_conv) void {
+ const wrapper = event_ orelse return;
+ const alloc = wrapper.alloc;
+ alloc.destroy(wrapper);
+}
+
+pub fn set_action(event_: Event, action: key.Action) callconv(lib.calling_conv) void {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(key.Action, @intFromEnum(action)) catch {
+ log.warn("set_action invalid action value={d}", .{@intFromEnum(action)});
+ return;
+ };
+ }
+
+ const event: *key.KeyEvent = &event_.?.event;
+ event.action = action;
+}
+
+pub fn get_action(event_: Event) callconv(lib.calling_conv) key.Action {
+ const event: *key.KeyEvent = &event_.?.event;
+ return event.action;
+}
+
+pub fn set_key(event_: Event, k: key.Key) callconv(lib.calling_conv) void {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(key.Key, @intFromEnum(k)) catch {
+ log.warn("set_key invalid key value={d}", .{@intFromEnum(k)});
+ return;
+ };
+ }
+
+ const event: *key.KeyEvent = &event_.?.event;
+ event.key = k;
+}
+
+pub fn get_key(event_: Event) callconv(lib.calling_conv) key.Key {
+ const event: *key.KeyEvent = &event_.?.event;
+ return event.key;
+}
+
+pub fn set_mods(event_: Event, mods: key.Mods) callconv(lib.calling_conv) void {
+ const event: *key.KeyEvent = &event_.?.event;
+ event.mods = mods;
+}
+
+pub fn get_mods(event_: Event) callconv(lib.calling_conv) key.Mods {
+ const event: *key.KeyEvent = &event_.?.event;
+ return event.mods;
+}
+
+pub fn set_consumed_mods(event_: Event, consumed_mods: key.Mods) callconv(lib.calling_conv) void {
+ const event: *key.KeyEvent = &event_.?.event;
+ event.consumed_mods = consumed_mods;
+}
+
+pub fn get_consumed_mods(event_: Event) callconv(lib.calling_conv) key.Mods {
+ const event: *key.KeyEvent = &event_.?.event;
+ return event.consumed_mods;
+}
+
+pub fn set_composing(event_: Event, composing: bool) callconv(lib.calling_conv) void {
+ const event: *key.KeyEvent = &event_.?.event;
+ event.composing = composing;
+}
+
+pub fn get_composing(event_: Event) callconv(lib.calling_conv) bool {
+ const event: *key.KeyEvent = &event_.?.event;
+ return event.composing;
+}
+
+pub fn set_utf8(event_: Event, utf8: ?[*]const u8, len: usize) callconv(lib.calling_conv) void {
+ const event: *key.KeyEvent = &event_.?.event;
+ event.utf8 = if (utf8) |ptr| ptr[0..len] else "";
+}
+
+pub fn get_utf8(event_: Event, len: ?*usize) callconv(lib.calling_conv) ?[*]const u8 {
+ const event: *key.KeyEvent = &event_.?.event;
+ if (len) |l| l.* = event.utf8.len;
+ return if (event.utf8.len == 0) null else event.utf8.ptr;
+}
+
+pub fn set_unshifted_codepoint(event_: Event, codepoint: u32) callconv(lib.calling_conv) void {
+ const event: *key.KeyEvent = &event_.?.event;
+ event.unshifted_codepoint = @truncate(codepoint);
+}
+
+pub fn get_unshifted_codepoint(event_: Event) callconv(lib.calling_conv) u32 {
+ const event: *key.KeyEvent = &event_.?.event;
+ return event.unshifted_codepoint;
+}
+
+test "alloc" {
+ const testing = std.testing;
+ var e: Event = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ free(e);
+}
+
+test "set" {
+ const testing = std.testing;
+ var e: Event = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ defer free(e);
+
+ // Test action
+ set_action(e, .press);
+ try testing.expectEqual(key.Action.press, e.?.event.action);
+
+ // Test key
+ set_key(e, .key_a);
+ try testing.expectEqual(key.Key.key_a, e.?.event.key);
+
+ // Test mods
+ const mods: key.Mods = .{ .shift = true, .ctrl = true };
+ set_mods(e, mods);
+ try testing.expect(e.?.event.mods.shift);
+ try testing.expect(e.?.event.mods.ctrl);
+
+ // Test consumed mods
+ const consumed: key.Mods = .{ .shift = true };
+ set_consumed_mods(e, consumed);
+ try testing.expect(e.?.event.consumed_mods.shift);
+ try testing.expect(!e.?.event.consumed_mods.ctrl);
+
+ // Test composing
+ set_composing(e, true);
+ try testing.expect(e.?.event.composing);
+
+ // Test UTF-8
+ const text = "hello";
+ set_utf8(e, text.ptr, text.len);
+ try testing.expectEqualStrings(text, e.?.event.utf8);
+
+ // Test UTF-8 null
+ set_utf8(e, null, 0);
+ try testing.expectEqualStrings("", e.?.event.utf8);
+
+ // Test unshifted codepoint
+ set_unshifted_codepoint(e, 'a');
+ try testing.expectEqual(@as(u21, 'a'), e.?.event.unshifted_codepoint);
+}
+
+test "get" {
+ const testing = std.testing;
+ var e: Event = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ defer free(e);
+
+ // Set some values
+ set_action(e, .repeat);
+ set_key(e, .key_z);
+
+ const mods: key.Mods = .{ .alt = true, .super = true };
+ set_mods(e, mods);
+
+ const consumed: key.Mods = .{ .alt = true };
+ set_consumed_mods(e, consumed);
+
+ set_composing(e, true);
+
+ const text = "test";
+ set_utf8(e, text.ptr, text.len);
+
+ set_unshifted_codepoint(e, 'z');
+
+ // Get them back
+ try testing.expectEqual(key.Action.repeat, get_action(e));
+ try testing.expectEqual(key.Key.key_z, get_key(e));
+
+ const got_mods = get_mods(e);
+ try testing.expect(got_mods.alt);
+ try testing.expect(got_mods.super);
+
+ const got_consumed = get_consumed_mods(e);
+ try testing.expect(got_consumed.alt);
+ try testing.expect(!got_consumed.super);
+
+ try testing.expect(get_composing(e));
+
+ var utf8_len: usize = undefined;
+ const got_utf8 = get_utf8(e, &utf8_len);
+ try testing.expect(got_utf8 != null);
+ try testing.expectEqual(@as(usize, 4), utf8_len);
+ try testing.expectEqualStrings("test", got_utf8.?[0..utf8_len]);
+
+ try testing.expectEqual(@as(u32, 'z'), get_unshifted_codepoint(e));
+}
+
+test "complete key event" {
+ const testing = std.testing;
+ var e: Event = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ defer free(e);
+
+ // Build a complete key event for shift+a
+ set_action(e, .press);
+ set_key(e, .key_a);
+
+ const mods: key.Mods = .{ .shift = true };
+ set_mods(e, mods);
+
+ const consumed: key.Mods = .{ .shift = true };
+ set_consumed_mods(e, consumed);
+
+ const text = "A";
+ set_utf8(e, text.ptr, text.len);
+
+ set_unshifted_codepoint(e, 'a');
+
+ // Verify all fields
+ try testing.expectEqual(key.Action.press, e.?.event.action);
+ try testing.expectEqual(key.Key.key_a, e.?.event.key);
+ try testing.expect(e.?.event.mods.shift);
+ try testing.expect(e.?.event.consumed_mods.shift);
+ try testing.expectEqualStrings("A", e.?.event.utf8);
+ try testing.expectEqual(@as(u21, 'a'), e.?.event.unshifted_codepoint);
+
+ // Also test the getter
+ var utf8_len: usize = undefined;
+ const got_utf8 = get_utf8(e, &utf8_len);
+ try testing.expect(got_utf8 != null);
+ try testing.expectEqual(@as(usize, 1), utf8_len);
+ try testing.expectEqualStrings("A", got_utf8.?[0..utf8_len]);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/main.zig b/vendor/libghostty-vt/src/terminal/c/main.zig
new file mode 100644
index 00000000..17056779
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/main.zig
@@ -0,0 +1,178 @@
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+
+const buildpkg = @import("build_info.zig");
+pub const allocator = @import("allocator.zig");
+pub const cell = @import("cell.zig");
+pub const color = @import("color.zig");
+pub const focus = @import("focus.zig");
+pub const formatter = @import("formatter.zig");
+pub const grid_ref = @import("grid_ref.zig");
+pub const types = @import("types.zig");
+pub const modes = @import("modes.zig");
+pub const osc = @import("osc.zig");
+pub const render = @import("render.zig");
+pub const key_event = @import("key_event.zig");
+pub const key_encode = @import("key_encode.zig");
+pub const mouse_event = @import("mouse_event.zig");
+pub const mouse_encode = @import("mouse_encode.zig");
+pub const paste = @import("paste.zig");
+pub const row = @import("row.zig");
+pub const sgr = @import("sgr.zig");
+pub const size_report = @import("size_report.zig");
+pub const style = @import("style.zig");
+pub const terminal = @import("terminal.zig");
+
+// The full C API, unexported.
+pub const build_info = buildpkg.get;
+
+pub const osc_new = osc.new;
+pub const osc_free = osc.free;
+pub const osc_reset = osc.reset;
+pub const osc_next = osc.next;
+pub const osc_end = osc.end;
+pub const osc_command_type = osc.commandType;
+pub const osc_command_data = osc.commandData;
+
+pub const color_rgb_get = color.rgb_get;
+
+pub const focus_encode = focus.encode;
+
+pub const mode_report_encode = modes.report_encode;
+
+pub const formatter_terminal_new = formatter.terminal_new;
+pub const formatter_format_buf = formatter.format_buf;
+pub const formatter_format_alloc = formatter.format_alloc;
+pub const formatter_free = formatter.free;
+
+pub const render_state_new = render.new;
+pub const render_state_free = render.free;
+pub const render_state_update = render.update;
+pub const render_state_get = render.get;
+pub const render_state_set = render.set;
+pub const render_state_colors_get = render.colors_get;
+pub const render_state_row_iterator_new = render.row_iterator_new;
+pub const render_state_row_iterator_next = render.row_iterator_next;
+pub const render_state_row_get = render.row_get;
+pub const render_state_row_set = render.row_set;
+pub const render_state_row_iterator_free = render.row_iterator_free;
+pub const render_state_row_cells_new = render.row_cells_new;
+pub const render_state_row_cells_next = render.row_cells_next;
+pub const render_state_row_cells_select = render.row_cells_select;
+pub const render_state_row_cells_get = render.row_cells_get;
+pub const render_state_row_cells_free = render.row_cells_free;
+
+pub const sgr_new = sgr.new;
+pub const sgr_free = sgr.free;
+pub const sgr_reset = sgr.reset;
+pub const sgr_set_params = sgr.setParams;
+pub const sgr_next = sgr.next;
+pub const sgr_unknown_full = sgr.unknown_full;
+pub const sgr_unknown_partial = sgr.unknown_partial;
+pub const sgr_attribute_tag = sgr.attribute_tag;
+pub const sgr_attribute_value = sgr.attribute_value;
+pub const wasm_alloc_sgr_attribute = sgr.wasm_alloc_attribute;
+pub const wasm_free_sgr_attribute = sgr.wasm_free_attribute;
+
+pub const key_event_new = key_event.new;
+pub const key_event_free = key_event.free;
+pub const key_event_set_action = key_event.set_action;
+pub const key_event_get_action = key_event.get_action;
+pub const key_event_set_key = key_event.set_key;
+pub const key_event_get_key = key_event.get_key;
+pub const key_event_set_mods = key_event.set_mods;
+pub const key_event_get_mods = key_event.get_mods;
+pub const key_event_set_consumed_mods = key_event.set_consumed_mods;
+pub const key_event_get_consumed_mods = key_event.get_consumed_mods;
+pub const key_event_set_composing = key_event.set_composing;
+pub const key_event_get_composing = key_event.get_composing;
+pub const key_event_set_utf8 = key_event.set_utf8;
+pub const key_event_get_utf8 = key_event.get_utf8;
+pub const key_event_set_unshifted_codepoint = key_event.set_unshifted_codepoint;
+pub const key_event_get_unshifted_codepoint = key_event.get_unshifted_codepoint;
+
+pub const key_encoder_new = key_encode.new;
+pub const key_encoder_free = key_encode.free;
+pub const key_encoder_setopt = key_encode.setopt;
+pub const key_encoder_setopt_from_terminal = key_encode.setopt_from_terminal;
+pub const key_encoder_encode = key_encode.encode;
+
+pub const mouse_event_new = mouse_event.new;
+pub const mouse_event_free = mouse_event.free;
+pub const mouse_event_set_action = mouse_event.set_action;
+pub const mouse_event_get_action = mouse_event.get_action;
+pub const mouse_event_set_button = mouse_event.set_button;
+pub const mouse_event_clear_button = mouse_event.clear_button;
+pub const mouse_event_get_button = mouse_event.get_button;
+pub const mouse_event_set_mods = mouse_event.set_mods;
+pub const mouse_event_get_mods = mouse_event.get_mods;
+pub const mouse_event_set_position = mouse_event.set_position;
+pub const mouse_event_get_position = mouse_event.get_position;
+
+pub const mouse_encoder_new = mouse_encode.new;
+pub const mouse_encoder_free = mouse_encode.free;
+pub const mouse_encoder_setopt = mouse_encode.setopt;
+pub const mouse_encoder_setopt_from_terminal = mouse_encode.setopt_from_terminal;
+pub const mouse_encoder_reset = mouse_encode.reset;
+pub const mouse_encoder_encode = mouse_encode.encode;
+
+pub const paste_is_safe = paste.is_safe;
+pub const paste_encode = paste.encode;
+
+pub const alloc_alloc = allocator.alloc;
+pub const alloc_free = allocator.free;
+
+pub const size_report_encode = size_report.encode;
+
+pub const cell_get = cell.get;
+
+pub const row_get = row.get;
+
+pub const style_default = style.default_style;
+pub const style_is_default = style.style_is_default;
+
+pub const terminal_new = terminal.new;
+pub const terminal_free = terminal.free;
+pub const terminal_reset = terminal.reset;
+pub const terminal_resize = terminal.resize;
+pub const terminal_set = terminal.set;
+pub const terminal_vt_write = terminal.vt_write;
+pub const terminal_scroll_viewport = terminal.scroll_viewport;
+pub const terminal_mode_get = terminal.mode_get;
+pub const terminal_mode_set = terminal.mode_set;
+pub const terminal_get = terminal.get;
+pub const terminal_grid_ref = terminal.grid_ref;
+
+pub const type_json = types.get_json;
+
+pub const grid_ref_cell = grid_ref.grid_ref_cell;
+pub const grid_ref_row = grid_ref.grid_ref_row;
+pub const grid_ref_graphemes = grid_ref.grid_ref_graphemes;
+pub const grid_ref_style = grid_ref.grid_ref_style;
+
+test {
+ _ = allocator;
+ _ = buildpkg;
+ _ = cell;
+ _ = color;
+ _ = grid_ref;
+ _ = row;
+ _ = focus;
+ _ = formatter;
+ _ = modes;
+ _ = osc;
+ _ = render;
+ _ = key_event;
+ _ = key_encode;
+ _ = mouse_event;
+ _ = mouse_encode;
+ _ = paste;
+ _ = sgr;
+ _ = size_report;
+ _ = style;
+ _ = terminal;
+ _ = types;
+
+ // We want to make sure we run the tests for the C allocator interface.
+ _ = @import("../../lib/allocator.zig");
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/modes.zig b/vendor/libghostty-vt/src/terminal/c/modes.zig
new file mode 100644
index 00000000..449ce63e
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/modes.zig
@@ -0,0 +1,104 @@
+const std = @import("std");
+const lib = @import("../lib.zig");
+const modes = @import("../modes.zig");
+const Result = @import("result.zig").Result;
+
+/// C: GhosttyModeReportState
+pub const ReportState = enum(c_int) {
+ _,
+
+ fn toZig(self: ReportState) ?modes.Report.State {
+ return std.meta.intToEnum(
+ modes.Report.State,
+ @intFromEnum(self),
+ ) catch null;
+ }
+};
+
+pub fn report_encode(
+ tag: modes.ModeTag.Backing,
+ state: ReportState,
+ out_: ?[*]u8,
+ out_len: usize,
+ out_written: *usize,
+) callconv(lib.calling_conv) Result {
+ const mode_tag: modes.ModeTag = @bitCast(tag);
+ const report: modes.Report = .{
+ .tag = mode_tag,
+ .state = state.toZig() orelse return .invalid_value,
+ };
+
+ var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{});
+ report.encode(&writer) catch |err| switch (err) {
+ error.WriteFailed => {
+ var discarding: std.Io.Writer.Discarding = .init(&.{});
+ report.encode(&discarding.writer) catch unreachable;
+ out_written.* = @intCast(discarding.count);
+ return .out_of_space;
+ },
+ };
+
+ out_written.* = writer.end;
+ return .success;
+}
+
+test "encode DEC mode set" {
+ var buf: [modes.Report.max_size]u8 = undefined;
+ var written: usize = 0;
+ const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1, .ansi = false });
+ const result = report_encode(tag, @enumFromInt(1), &buf, buf.len, &written);
+ try std.testing.expectEqual(.success, result);
+ try std.testing.expectEqualStrings("\x1B[?1;1$y", buf[0..written]);
+}
+
+test "encode DEC mode reset" {
+ var buf: [modes.Report.max_size]u8 = undefined;
+ var written: usize = 0;
+ const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1, .ansi = false });
+ const result = report_encode(tag, @enumFromInt(2), &buf, buf.len, &written);
+ try std.testing.expectEqual(.success, result);
+ try std.testing.expectEqualStrings("\x1B[?1;2$y", buf[0..written]);
+}
+
+test "encode ANSI mode" {
+ var buf: [modes.Report.max_size]u8 = undefined;
+ var written: usize = 0;
+ const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 4, .ansi = true });
+ const result = report_encode(tag, @enumFromInt(1), &buf, buf.len, &written);
+ try std.testing.expectEqual(.success, result);
+ try std.testing.expectEqualStrings("\x1B[4;1$y", buf[0..written]);
+}
+
+test "encode not recognized" {
+ var buf: [modes.Report.max_size]u8 = undefined;
+ var written: usize = 0;
+ const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 9999, .ansi = false });
+ const result = report_encode(tag, @enumFromInt(0), &buf, buf.len, &written);
+ try std.testing.expectEqual(.success, result);
+ try std.testing.expectEqualStrings("\x1B[?9999;0$y", buf[0..written]);
+}
+
+test "encode with insufficient buffer" {
+ var buf: [1]u8 = undefined;
+ var written: usize = 0;
+ const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1, .ansi = false });
+ const result = report_encode(tag, @enumFromInt(1), &buf, buf.len, &written);
+ try std.testing.expectEqual(.out_of_space, result);
+ try std.testing.expect(written > 1);
+}
+
+test "encode with invalid state" {
+ var buf: [modes.Report.max_size]u8 = undefined;
+ var written: usize = 0;
+ const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1, .ansi = false });
+ const result = report_encode(tag, @enumFromInt(99), &buf, buf.len, &written);
+ try std.testing.expectEqual(.invalid_value, result);
+}
+
+test "encode with null buffer" {
+ var written: usize = 0;
+ const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1, .ansi = false });
+ const result = report_encode(tag, @enumFromInt(1), null, 0, &written);
+ try std.testing.expectEqual(.out_of_space, result);
+ try std.testing.expect(written > 0);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/mouse_encode.zig b/vendor/libghostty-vt/src/terminal/c/mouse_encode.zig
new file mode 100644
index 00000000..331dd606
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/mouse_encode.zig
@@ -0,0 +1,530 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const testing = std.testing;
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const input_mouse_encode = @import("../../input/mouse_encode.zig");
+const renderer_size = @import("../../renderer/size.zig");
+const point = @import("../point.zig");
+const terminal_mouse = @import("../mouse.zig");
+const mouse_event = @import("mouse_event.zig");
+const Result = @import("result.zig").Result;
+const Event = mouse_event.Event;
+const Terminal = @import("terminal.zig").Terminal;
+const ZigTerminal = @import("../Terminal.zig");
+
+const log = std.log.scoped(.mouse_encode);
+
+/// Wrapper around mouse encoding options that tracks the allocator for C API usage.
+const MouseEncoderWrapper = struct {
+ opts: input_mouse_encode.Options,
+ track_last_cell: bool = false,
+ last_cell: ?point.Coordinate = null,
+ alloc: Allocator,
+};
+
+/// C: GhosttyMouseEncoder
+pub const Encoder = ?*MouseEncoderWrapper;
+
+/// C: GhosttyMouseTrackingMode
+pub const TrackingMode = terminal_mouse.Event;
+
+/// C: GhosttyMouseFormat
+pub const Format = terminal_mouse.Format;
+
+/// C: GhosttyMouseEncoderSize
+pub const Size = extern struct {
+ size: usize = @sizeOf(Size),
+ screen_width: u32,
+ screen_height: u32,
+ cell_width: u32,
+ cell_height: u32,
+ padding_top: u32,
+ padding_bottom: u32,
+ padding_right: u32,
+ padding_left: u32,
+
+ fn toRenderer(self: Size) ?renderer_size.Size {
+ if (self.cell_width == 0 or self.cell_height == 0) return null;
+ return .{
+ .screen = .{
+ .width = self.screen_width,
+ .height = self.screen_height,
+ },
+ .cell = .{
+ .width = self.cell_width,
+ .height = self.cell_height,
+ },
+ .padding = .{
+ .top = self.padding_top,
+ .bottom = self.padding_bottom,
+ .right = self.padding_right,
+ .left = self.padding_left,
+ },
+ };
+ }
+};
+
+/// C: GhosttyMouseEncoderOption
+pub const Option = enum(c_int) {
+ event = 0,
+ format = 1,
+ size = 2,
+ any_button_pressed = 3,
+ track_last_cell = 4,
+
+ /// Input type expected for setting the option.
+ pub fn InType(comptime self: Option) type {
+ return switch (self) {
+ .event => TrackingMode,
+ .format => Format,
+ .size => Size,
+ .any_button_pressed,
+ .track_last_cell,
+ => bool,
+ };
+ }
+};
+
+pub fn new(
+ alloc_: ?*const CAllocator,
+ result: *Encoder,
+) callconv(lib.calling_conv) Result {
+ const alloc = lib.alloc.default(alloc_);
+ const ptr = alloc.create(MouseEncoderWrapper) catch
+ return .out_of_memory;
+ ptr.* = .{
+ .opts = .{ .size = defaultSize() },
+ .alloc = alloc,
+ };
+ result.* = ptr;
+ return .success;
+}
+
+pub fn free(encoder_: Encoder) callconv(lib.calling_conv) void {
+ const wrapper = encoder_ orelse return;
+ const alloc = wrapper.alloc;
+ alloc.destroy(wrapper);
+}
+
+pub fn setopt(
+ encoder_: Encoder,
+ option: Option,
+ value: ?*const anyopaque,
+) callconv(lib.calling_conv) void {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(Option, @intFromEnum(option)) catch {
+ log.warn("setopt invalid option value={d}", .{@intFromEnum(option)});
+ return;
+ };
+ }
+
+ return switch (option) {
+ inline else => |comptime_option| setoptTyped(
+ encoder_,
+ comptime_option,
+ @ptrCast(@alignCast(value orelse return)),
+ ),
+ };
+}
+
+fn setoptTyped(
+ encoder_: Encoder,
+ comptime option: Option,
+ value: *const option.InType(),
+) void {
+ const wrapper = encoder_.?;
+ switch (option) {
+ .event => {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(TrackingMode, @intFromEnum(value.*)) catch {
+ log.warn("setopt invalid TrackingMode value={d}", .{@intFromEnum(value.*)});
+ return;
+ };
+ }
+
+ if (wrapper.opts.event != value.*) wrapper.last_cell = null;
+ wrapper.opts.event = value.*;
+ },
+
+ .format => {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(Format, @intFromEnum(value.*)) catch {
+ log.warn("setopt invalid Format value={d}", .{@intFromEnum(value.*)});
+ return;
+ };
+ }
+
+ if (wrapper.opts.format != value.*) wrapper.last_cell = null;
+ wrapper.opts.format = value.*;
+ },
+
+ .size => {
+ if (value.size < @sizeOf(Size)) {
+ log.warn("setopt size struct too small size={d} expected>={d}", .{
+ value.size,
+ @sizeOf(Size),
+ });
+ return;
+ }
+
+ wrapper.opts.size = value.toRenderer() orelse {
+ log.warn("setopt invalid size values (cell width and height must be non-zero)", .{});
+ return;
+ };
+ wrapper.last_cell = null;
+ },
+
+ .any_button_pressed => wrapper.opts.any_button_pressed = value.*,
+
+ .track_last_cell => {
+ wrapper.track_last_cell = value.*;
+ if (!value.*) wrapper.last_cell = null;
+ },
+ }
+}
+
+pub fn setopt_from_terminal(
+ encoder_: Encoder,
+ terminal_: Terminal,
+) callconv(lib.calling_conv) void {
+ const wrapper = encoder_ orelse return;
+ const t: *ZigTerminal = (terminal_ orelse return).terminal;
+ wrapper.opts.event = t.flags.mouse_event;
+ wrapper.opts.format = t.flags.mouse_format;
+ wrapper.last_cell = null;
+}
+
+pub fn reset(encoder_: Encoder) callconv(lib.calling_conv) void {
+ const wrapper = encoder_ orelse return;
+ wrapper.last_cell = null;
+}
+
+pub fn encode(
+ encoder_: Encoder,
+ event_: Event,
+ out_: ?[*]u8,
+ out_len: usize,
+ out_written: *usize,
+) callconv(lib.calling_conv) Result {
+ const wrapper = encoder_ orelse return .invalid_value;
+ const event = event_ orelse return .invalid_value;
+
+ const prev_last_cell = wrapper.last_cell;
+
+ var opts = wrapper.opts;
+ opts.last_cell = if (wrapper.track_last_cell) &wrapper.last_cell else null;
+
+ var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{});
+ input_mouse_encode.encode(
+ &writer,
+ event.event,
+ opts,
+ ) catch |err| switch (err) {
+ error.WriteFailed => {
+ // Failed writes should not mutate motion dedupe state because no
+ // complete sequence was produced.
+ wrapper.last_cell = prev_last_cell;
+
+ // Use a discarding writer to count how much space we would have needed.
+ var count_last_cell = prev_last_cell;
+ var count_opts = wrapper.opts;
+ count_opts.last_cell = if (wrapper.track_last_cell) &count_last_cell else null;
+
+ var discarding: std.Io.Writer.Discarding = .init(&.{});
+ input_mouse_encode.encode(
+ &discarding.writer,
+ event.event,
+ count_opts,
+ ) catch unreachable;
+
+ // Discarding always uses a u64. If we're on 32-bit systems
+ // we cast down. We should make this safer in the future.
+ out_written.* = @intCast(discarding.count);
+ return .out_of_space;
+ },
+ };
+
+ out_written.* = writer.end;
+ return .success;
+}
+
+fn defaultSize() renderer_size.Size {
+ return .{
+ .screen = .{ .width = 1, .height = 1 },
+ .cell = .{ .width = 1, .height = 1 },
+ .padding = .{},
+ };
+}
+
+fn testSize() Size {
+ return .{
+ .size = @sizeOf(Size),
+ .screen_width = 1_000,
+ .screen_height = 1_000,
+ .cell_width = 1,
+ .cell_height = 1,
+ .padding_top = 0,
+ .padding_bottom = 0,
+ .padding_right = 0,
+ .padding_left = 0,
+ };
+}
+
+test "alloc" {
+ var e: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ free(e);
+}
+
+test "setopt" {
+ var e: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ defer free(e);
+
+ const event_mode: TrackingMode = .any;
+ setopt(e, .event, &event_mode);
+ try testing.expectEqual(TrackingMode.any, e.?.opts.event);
+
+ const format_mode: Format = .sgr;
+ setopt(e, .format, &format_mode);
+ try testing.expectEqual(Format.sgr, e.?.opts.format);
+
+ const size = testSize();
+ setopt(e, .size, &size);
+ try testing.expectEqual(size.screen_width, e.?.opts.size.screen.width);
+ try testing.expectEqual(size.screen_height, e.?.opts.size.screen.height);
+
+ const any_button_pressed = true;
+ setopt(e, .any_button_pressed, &any_button_pressed);
+ try testing.expect(e.?.opts.any_button_pressed);
+
+ const track_last_cell = true;
+ setopt(e, .track_last_cell, &track_last_cell);
+ try testing.expect(e.?.track_last_cell);
+}
+
+test "setopt_from_terminal" {
+ const terminal_c = @import("terminal.zig");
+
+ var e: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ defer free(e);
+
+ var t: Terminal = undefined;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ const event_mode: TrackingMode = .any;
+ setopt(e, .event, &event_mode);
+ const format_mode: Format = .sgr;
+ setopt(e, .format, &format_mode);
+
+ setopt_from_terminal(e, t);
+ try testing.expectEqual(TrackingMode.none, e.?.opts.event);
+ try testing.expectEqual(Format.x10, e.?.opts.format);
+}
+
+test "setopt_from_terminal null" {
+ setopt_from_terminal(null, null);
+
+ const terminal_c = @import("terminal.zig");
+ var t: Terminal = undefined;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 0 },
+ ));
+ defer terminal_c.free(t);
+ setopt_from_terminal(null, t);
+
+ var e: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ defer free(e);
+ setopt_from_terminal(e, null);
+}
+
+test "encode: sgr press left" {
+ var encoder: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &encoder,
+ ));
+ defer free(encoder);
+
+ const event_mode: TrackingMode = .any;
+ setopt(encoder, .event, &event_mode);
+ const format_mode: Format = .sgr;
+ setopt(encoder, .format, &format_mode);
+ const size = testSize();
+ setopt(encoder, .size, &size);
+
+ var event: Event = undefined;
+ try testing.expectEqual(Result.success, mouse_event.new(
+ &lib.alloc.test_allocator,
+ &event,
+ ));
+ defer mouse_event.free(event);
+
+ mouse_event.set_action(event, .press);
+ mouse_event.set_button(event, .left);
+ mouse_event.set_position(event, .{ .x = 0, .y = 0 });
+
+ var required: usize = 0;
+ try testing.expectEqual(Result.out_of_space, encode(
+ encoder,
+ event,
+ null,
+ 0,
+ &required,
+ ));
+ try testing.expectEqual(@as(usize, 9), required);
+
+ var buf: [32]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.success, encode(
+ encoder,
+ event,
+ &buf,
+ buf.len,
+ &written,
+ ));
+ try testing.expectEqual(required, written);
+ try testing.expectEqualStrings("\x1b[<0;1;1M", buf[0..written]);
+}
+
+test "encode: motion dedupe and reset" {
+ var encoder: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &encoder,
+ ));
+ defer free(encoder);
+
+ const event_mode: TrackingMode = .any;
+ setopt(encoder, .event, &event_mode);
+ const format_mode: Format = .sgr;
+ setopt(encoder, .format, &format_mode);
+ const size = testSize();
+ setopt(encoder, .size, &size);
+ const track_last_cell = true;
+ setopt(encoder, .track_last_cell, &track_last_cell);
+
+ var event: Event = undefined;
+ try testing.expectEqual(Result.success, mouse_event.new(
+ &lib.alloc.test_allocator,
+ &event,
+ ));
+ defer mouse_event.free(event);
+
+ mouse_event.set_action(event, .motion);
+ mouse_event.set_button(event, .left);
+ mouse_event.set_position(event, .{ .x = 5, .y = 6 });
+
+ {
+ var buf: [32]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.success, encode(
+ encoder,
+ event,
+ &buf,
+ buf.len,
+ &written,
+ ));
+ try testing.expect(written > 0);
+ }
+
+ {
+ var buf: [32]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.success, encode(
+ encoder,
+ event,
+ &buf,
+ buf.len,
+ &written,
+ ));
+ try testing.expectEqual(@as(usize, 0), written);
+ }
+
+ reset(encoder);
+
+ {
+ var buf: [32]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.success, encode(
+ encoder,
+ event,
+ &buf,
+ buf.len,
+ &written,
+ ));
+ try testing.expect(written > 0);
+ }
+}
+
+test "encode: querying required size doesn't update dedupe state" {
+ var encoder: Encoder = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &encoder,
+ ));
+ defer free(encoder);
+
+ const event_mode: TrackingMode = .any;
+ setopt(encoder, .event, &event_mode);
+ const format_mode: Format = .sgr;
+ setopt(encoder, .format, &format_mode);
+ const size = testSize();
+ setopt(encoder, .size, &size);
+ const track_last_cell = true;
+ setopt(encoder, .track_last_cell, &track_last_cell);
+
+ var event: Event = undefined;
+ try testing.expectEqual(Result.success, mouse_event.new(
+ &lib.alloc.test_allocator,
+ &event,
+ ));
+ defer mouse_event.free(event);
+
+ mouse_event.set_action(event, .motion);
+ mouse_event.set_button(event, .left);
+ mouse_event.set_position(event, .{ .x = 5, .y = 6 });
+
+ var required: usize = 0;
+ try testing.expectEqual(Result.out_of_space, encode(
+ encoder,
+ event,
+ null,
+ 0,
+ &required,
+ ));
+ try testing.expect(required > 0);
+
+ var buf: [32]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.success, encode(
+ encoder,
+ event,
+ &buf,
+ buf.len,
+ &written,
+ ));
+ try testing.expect(written > 0);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/mouse_event.zig b/vendor/libghostty-vt/src/terminal/c/mouse_event.zig
new file mode 100644
index 00000000..b3c19dd3
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/mouse_event.zig
@@ -0,0 +1,155 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const testing = std.testing;
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const key = @import("../../input/key.zig");
+const mouse = @import("../../input/mouse.zig");
+const mouse_encode = @import("../../input/mouse_encode.zig");
+const Result = @import("result.zig").Result;
+
+const log = std.log.scoped(.mouse_event);
+
+/// Wrapper around mouse event that tracks the allocator for C API usage.
+const MouseEventWrapper = struct {
+ event: mouse_encode.Event = .{},
+ alloc: Allocator,
+};
+
+/// C: GhosttyMouseEvent
+pub const Event = ?*MouseEventWrapper;
+
+/// C: GhosttyMouseAction
+pub const Action = mouse.Action;
+
+/// C: GhosttyMouseButton
+pub const Button = mouse.Button;
+
+/// C: GhosttyMousePosition
+pub const Position = mouse_encode.Event.Pos;
+
+/// C: GhosttyMods
+pub const Mods = key.Mods;
+
+pub fn new(
+ alloc_: ?*const CAllocator,
+ result: *Event,
+) callconv(lib.calling_conv) Result {
+ const alloc = lib.alloc.default(alloc_);
+ const ptr = alloc.create(MouseEventWrapper) catch
+ return .out_of_memory;
+ ptr.* = .{ .alloc = alloc };
+ result.* = ptr;
+ return .success;
+}
+
+pub fn free(event_: Event) callconv(lib.calling_conv) void {
+ const wrapper = event_ orelse return;
+ const alloc = wrapper.alloc;
+ alloc.destroy(wrapper);
+}
+
+pub fn set_action(event_: Event, action: Action) callconv(lib.calling_conv) void {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(Action, @intFromEnum(action)) catch {
+ log.warn("set_action invalid action value={d}", .{@intFromEnum(action)});
+ return;
+ };
+ }
+
+ event_.?.event.action = action;
+}
+
+pub fn get_action(event_: Event) callconv(lib.calling_conv) Action {
+ return event_.?.event.action;
+}
+
+pub fn set_button(event_: Event, button: Button) callconv(lib.calling_conv) void {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(Button, @intFromEnum(button)) catch {
+ log.warn("set_button invalid button value={d}", .{@intFromEnum(button)});
+ return;
+ };
+ }
+
+ event_.?.event.button = button;
+}
+
+pub fn clear_button(event_: Event) callconv(lib.calling_conv) void {
+ event_.?.event.button = null;
+}
+
+pub fn get_button(event_: Event, out: ?*Button) callconv(lib.calling_conv) bool {
+ if (event_.?.event.button) |button| {
+ if (out) |ptr| ptr.* = button;
+ return true;
+ }
+
+ return false;
+}
+
+pub fn set_mods(event_: Event, mods: Mods) callconv(lib.calling_conv) void {
+ event_.?.event.mods = mods;
+}
+
+pub fn get_mods(event_: Event) callconv(lib.calling_conv) Mods {
+ return event_.?.event.mods;
+}
+
+pub fn set_position(event_: Event, pos: Position) callconv(lib.calling_conv) void {
+ event_.?.event.pos = pos;
+}
+
+pub fn get_position(event_: Event) callconv(lib.calling_conv) Position {
+ return event_.?.event.pos;
+}
+
+test "alloc" {
+ var e: Event = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ free(e);
+}
+
+test "free null" {
+ free(null);
+}
+
+test "set/get" {
+ var e: Event = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &e,
+ ));
+ defer free(e);
+
+ // Action
+ set_action(e, .motion);
+ try testing.expectEqual(Action.motion, get_action(e));
+
+ // Button
+ set_button(e, .left);
+ var button: Button = .unknown;
+ try testing.expect(get_button(e, &button));
+ try testing.expectEqual(Button.left, button);
+ try testing.expect(get_button(e, null));
+
+ clear_button(e);
+ try testing.expect(!get_button(e, &button));
+
+ // Mods
+ const mods: Mods = .{ .shift = true, .ctrl = true };
+ set_mods(e, mods);
+ const got_mods = get_mods(e);
+ try testing.expect(got_mods.shift);
+ try testing.expect(got_mods.ctrl);
+ try testing.expect(!got_mods.alt);
+
+ // Position
+ set_position(e, .{ .x = 12.5, .y = -4.0 });
+ const pos = get_position(e);
+ try testing.expectEqual(@as(f32, 12.5), pos.x);
+ try testing.expectEqual(@as(f32, -4.0), pos.y);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/osc.zig b/vendor/libghostty-vt/src/terminal/c/osc.zig
new file mode 100644
index 00000000..7ba32300
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/osc.zig
@@ -0,0 +1,140 @@
+const std = @import("std");
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const osc = @import("../osc.zig");
+const Result = @import("result.zig").Result;
+
+const log = std.log.scoped(.osc);
+
+/// C: GhosttyOscParser
+pub const Parser = ?*osc.Parser;
+
+/// C: GhosttyOscCommand
+pub const Command = ?*osc.Command;
+
+pub fn new(
+ alloc_: ?*const CAllocator,
+ result: *Parser,
+) callconv(lib.calling_conv) Result {
+ const alloc = lib.alloc.default(alloc_);
+ const ptr = alloc.create(osc.Parser) catch
+ return .out_of_memory;
+ ptr.* = .init(alloc);
+ result.* = ptr;
+ return .success;
+}
+
+pub fn free(parser_: Parser) callconv(lib.calling_conv) void {
+ // C-built parsers always have an associated allocator.
+ const parser = parser_ orelse return;
+ const alloc = parser.alloc.?;
+ parser.deinit();
+ alloc.destroy(parser);
+}
+
+pub fn reset(parser_: Parser) callconv(lib.calling_conv) void {
+ parser_.?.reset();
+}
+
+pub fn next(parser_: Parser, byte: u8) callconv(lib.calling_conv) void {
+ parser_.?.next(byte);
+}
+
+pub fn end(parser_: Parser, terminator: u8) callconv(lib.calling_conv) Command {
+ return parser_.?.end(terminator);
+}
+
+pub fn commandType(command_: Command) callconv(lib.calling_conv) osc.Command.Key {
+ const command = command_ orelse return .invalid;
+ return command.*;
+}
+
+/// C: GhosttyOscCommandData
+pub const CommandData = enum(c_int) {
+ invalid = 0,
+ change_window_title_str = 1,
+
+ /// Output type expected for querying the data of the given kind.
+ pub fn OutType(comptime self: CommandData) type {
+ return switch (self) {
+ .invalid => void,
+ .change_window_title_str => [*:0]const u8,
+ };
+ }
+};
+
+pub fn commandData(
+ command_: Command,
+ data: CommandData,
+ out: ?*anyopaque,
+) callconv(lib.calling_conv) bool {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(CommandData, @intFromEnum(data)) catch {
+ log.warn("commandData invalid data value={d}", .{@intFromEnum(data)});
+ return false;
+ };
+ }
+
+ return switch (data) {
+ .invalid => false,
+ inline else => |comptime_data| commandDataTyped(
+ command_,
+ comptime_data,
+ @ptrCast(@alignCast(out)),
+ ),
+ };
+}
+
+fn commandDataTyped(
+ command_: Command,
+ comptime data: CommandData,
+ out: *data.OutType(),
+) bool {
+ const command = command_.?;
+ switch (data) {
+ .invalid => return false,
+ .change_window_title_str => switch (command.*) {
+ .change_window_title => |v| out.* = v.ptr,
+ else => return false,
+ },
+ }
+
+ return true;
+}
+
+test "alloc" {
+ const testing = std.testing;
+ var p: Parser = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &p,
+ ));
+ free(p);
+}
+
+test "command type null" {
+ const testing = std.testing;
+ try testing.expectEqual(.invalid, commandType(null));
+}
+
+test "change window title" {
+ const testing = std.testing;
+ var p: Parser = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &p,
+ ));
+ defer free(p);
+
+ // Parse it
+ next(p, '0');
+ next(p, ';');
+ next(p, 'a');
+ const cmd = end(p, 0);
+ try testing.expectEqual(.change_window_title, commandType(cmd));
+
+ // Extract the title
+ var title: [*:0]const u8 = undefined;
+ try testing.expect(commandData(cmd, .change_window_title_str, @ptrCast(&title)));
+ try testing.expectEqualStrings("a", std.mem.span(title));
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/paste.zig b/vendor/libghostty-vt/src/terminal/c/paste.zig
new file mode 100644
index 00000000..bce6a565
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/paste.zig
@@ -0,0 +1,129 @@
+const std = @import("std");
+const lib = @import("../lib.zig");
+const paste = @import("../../input/paste.zig");
+const Result = @import("result.zig").Result;
+
+pub fn is_safe(data: ?[*]const u8, len: usize) callconv(lib.calling_conv) bool {
+ const slice: []const u8 = if (data) |v| v[0..len] else &.{};
+ return paste.isSafe(slice);
+}
+
+pub fn encode(
+ data: ?[*]u8,
+ data_len: usize,
+ bracketed: bool,
+ out_: ?[*]u8,
+ out_len: usize,
+ out_written: *usize,
+) callconv(lib.calling_conv) Result {
+ const slice: []u8 = if (data) |v| v[0..data_len] else &.{};
+ const result = paste.encode(slice, .{ .bracketed = bracketed });
+
+ const total = result[0].len + result[1].len + result[2].len;
+ out_written.* = total;
+
+ const out: []u8 = if (out_) |o| o[0..out_len] else &.{};
+ if (out.len < total) return .out_of_space;
+
+ var offset: usize = 0;
+ for (result) |segment| {
+ @memcpy(out[offset..][0..segment.len], segment);
+ offset += segment.len;
+ }
+
+ return .success;
+}
+
+test "encode bracketed" {
+ const testing = std.testing;
+ const input = try testing.allocator.dupe(u8, "hello");
+ defer testing.allocator.free(input);
+ var buf: [64]u8 = undefined;
+ var written: usize = 0;
+ const result = encode(input.ptr, input.len, true, &buf, buf.len, &written);
+ try testing.expectEqual(.success, result);
+ try testing.expectEqualStrings("\x1b[200~hello\x1b[201~", buf[0..written]);
+}
+
+test "encode unbracketed no newlines" {
+ const testing = std.testing;
+ const input = try testing.allocator.dupe(u8, "hello");
+ defer testing.allocator.free(input);
+ var buf: [64]u8 = undefined;
+ var written: usize = 0;
+ const result = encode(input.ptr, input.len, false, &buf, buf.len, &written);
+ try testing.expectEqual(.success, result);
+ try testing.expectEqualStrings("hello", buf[0..written]);
+}
+
+test "encode unbracketed newlines" {
+ const testing = std.testing;
+ const input = try testing.allocator.dupe(u8, "hello\nworld");
+ defer testing.allocator.free(input);
+ var buf: [64]u8 = undefined;
+ var written: usize = 0;
+ const result = encode(input.ptr, input.len, false, &buf, buf.len, &written);
+ try testing.expectEqual(.success, result);
+ try testing.expectEqualStrings("hello\rworld", buf[0..written]);
+}
+
+test "encode strip unsafe bytes" {
+ const testing = std.testing;
+ const input = try testing.allocator.dupe(u8, "hel\x1blo\x00world");
+ defer testing.allocator.free(input);
+ var buf: [64]u8 = undefined;
+ var written: usize = 0;
+ const result = encode(input.ptr, input.len, true, &buf, buf.len, &written);
+ try testing.expectEqual(.success, result);
+ try testing.expectEqualStrings("\x1b[200~hel lo world\x1b[201~", buf[0..written]);
+}
+
+test "encode with insufficient buffer" {
+ const testing = std.testing;
+ const input = try testing.allocator.dupe(u8, "hello");
+ defer testing.allocator.free(input);
+ var buf: [1]u8 = undefined;
+ var written: usize = 0;
+ const result = encode(input.ptr, input.len, true, &buf, buf.len, &written);
+ try testing.expectEqual(.out_of_space, result);
+ try testing.expectEqual(17, written);
+}
+
+test "encode with null buffer" {
+ const testing = std.testing;
+ const input = try testing.allocator.dupe(u8, "hello");
+ defer testing.allocator.free(input);
+ var written: usize = 0;
+ const result = encode(input.ptr, input.len, true, null, 0, &written);
+ try testing.expectEqual(.out_of_space, result);
+ try testing.expectEqual(17, written);
+}
+
+test "is_safe with safe data" {
+ const testing = std.testing;
+ const safe = "hello world";
+ try testing.expect(is_safe(safe.ptr, safe.len));
+}
+
+test "is_safe with newline" {
+ const testing = std.testing;
+ const unsafe = "hello\nworld";
+ try testing.expect(!is_safe(unsafe.ptr, unsafe.len));
+}
+
+test "is_safe with bracketed paste end" {
+ const testing = std.testing;
+ const unsafe = "hello\x1b[201~world";
+ try testing.expect(!is_safe(unsafe.ptr, unsafe.len));
+}
+
+test "is_safe with empty data" {
+ const testing = std.testing;
+ const empty = "";
+ try testing.expect(is_safe(empty.ptr, 0));
+}
+
+test "is_safe with null empty data" {
+ const testing = std.testing;
+ try testing.expect(is_safe(null, 0));
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/render.zig b/vendor/libghostty-vt/src/terminal/c/render.zig
new file mode 100644
index 00000000..b95bedd5
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/render.zig
@@ -0,0 +1,1382 @@
+const std = @import("std");
+const testing = std.testing;
+const Allocator = std.mem.Allocator;
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const colorpkg = @import("../color.zig");
+const cursorpkg = @import("../cursor.zig");
+const page = @import("../page.zig");
+const size = @import("../size.zig");
+const Style = @import("../style.zig").Style;
+const terminal_c = @import("terminal.zig");
+const ZigTerminal = @import("../Terminal.zig");
+const renderpkg = @import("../render.zig");
+const Result = @import("result.zig").Result;
+const row = @import("row.zig");
+const style_c = @import("style.zig");
+
+const log = std.log.scoped(.render_state_c);
+
+const RenderStateWrapper = struct {
+ alloc: std.mem.Allocator,
+ state: renderpkg.RenderState = .empty,
+};
+
+const RowIteratorWrapper = struct {
+ alloc: std.mem.Allocator,
+
+ /// The current index (also y value) into the row list.
+ y: ?size.CellCountInt,
+
+ /// These are the raw pointers into the render state data.
+ raws: []const page.Row,
+ cells: []const std.MultiArrayList(renderpkg.RenderState.Cell),
+ dirty: []bool,
+
+ /// The color palette from the render state, needed to resolve
+ /// palette-indexed background colors on cells.
+ palette: *const colorpkg.Palette,
+};
+
+const RowCellsWrapper = struct {
+ alloc: std.mem.Allocator,
+ x: ?size.CellCountInt,
+ raws: []const page.Cell,
+ graphemes: []const []const u21,
+ styles: []const Style,
+
+ /// The color palette, needed to resolve palette-indexed background colors.
+ palette: *const colorpkg.Palette,
+};
+
+/// C: GhosttyRenderState
+pub const RenderState = ?*RenderStateWrapper;
+
+/// C: GhosttyRenderStateRowIterator
+pub const RowIterator = ?*RowIteratorWrapper;
+
+/// C: GhosttyRenderStateRowCells
+pub const RowCells = ?*RowCellsWrapper;
+
+/// C: GhosttyRenderStateDirty
+pub const Dirty = renderpkg.RenderState.Dirty;
+
+/// C: GhosttyRenderStateCursorVisualStyle
+pub const CursorVisualStyle = enum(c_int) {
+ bar = 0,
+ block = 1,
+ underline = 2,
+ block_hollow = 3,
+
+ pub fn fromCursorStyle(s: cursorpkg.Style) CursorVisualStyle {
+ return switch (s) {
+ .bar => .bar,
+ .block => .block,
+ .underline => .underline,
+ .block_hollow => .block_hollow,
+ };
+ }
+};
+
+/// C: GhosttyRenderStateData
+pub const Data = enum(c_int) {
+ invalid = 0,
+ cols = 1,
+ rows = 2,
+ dirty = 3,
+ row_iterator = 4,
+ color_background = 5,
+ color_foreground = 6,
+ color_cursor = 7,
+ color_cursor_has_value = 8,
+ color_palette = 9,
+ cursor_visual_style = 10,
+ cursor_visible = 11,
+ cursor_blinking = 12,
+ cursor_password_input = 13,
+ cursor_viewport_has_value = 14,
+ cursor_viewport_x = 15,
+ cursor_viewport_y = 16,
+ cursor_viewport_wide_tail = 17,
+
+ /// Output type expected for querying the data of the given kind.
+ pub fn OutType(comptime self: Data) type {
+ return switch (self) {
+ .invalid => void,
+ .cols, .rows => size.CellCountInt,
+ .dirty => Dirty,
+ .row_iterator => RowIterator,
+ .color_background, .color_foreground, .color_cursor => colorpkg.RGB.C,
+ .color_cursor_has_value => bool,
+ .color_palette => colorpkg.PaletteC,
+ .cursor_visual_style => CursorVisualStyle,
+ .cursor_visible, .cursor_blinking, .cursor_password_input => bool,
+ .cursor_viewport_has_value, .cursor_viewport_wide_tail => bool,
+ .cursor_viewport_x, .cursor_viewport_y => size.CellCountInt,
+ };
+ }
+};
+
+/// C: GhosttyRenderStateOption
+pub const SetOption = enum(c_int) {
+ dirty = 0,
+
+ /// Input type expected for setting the option.
+ pub fn InType(comptime self: SetOption) type {
+ return switch (self) {
+ .dirty => Dirty,
+ };
+ }
+};
+
+/// C: GhosttyRenderStateColors
+pub const Colors = extern struct {
+ size: usize = @sizeOf(Colors),
+ background: colorpkg.RGB.C,
+ foreground: colorpkg.RGB.C,
+ cursor: colorpkg.RGB.C,
+ cursor_has_value: bool,
+ palette: colorpkg.PaletteC,
+};
+
+pub fn new(
+ alloc_: ?*const CAllocator,
+ result: *RenderState,
+) callconv(lib.calling_conv) Result {
+ result.* = new_(alloc_) catch |err| {
+ result.* = null;
+ return switch (err) {
+ error.OutOfMemory => .out_of_memory,
+ };
+ };
+
+ return .success;
+}
+
+fn new_(alloc_: ?*const CAllocator) error{OutOfMemory}!*RenderStateWrapper {
+ const alloc = lib.alloc.default(alloc_);
+ const ptr = alloc.create(RenderStateWrapper) catch
+ return error.OutOfMemory;
+ ptr.* = .{ .alloc = alloc };
+ return ptr;
+}
+
+pub fn free(state_: RenderState) callconv(lib.calling_conv) void {
+ const state = state_ orelse return;
+ const alloc = state.alloc;
+ state.state.deinit(alloc);
+ alloc.destroy(state);
+}
+
+pub fn update(
+ state_: RenderState,
+ terminal_: terminal_c.Terminal,
+) callconv(lib.calling_conv) Result {
+ const state = state_ orelse return .invalid_value;
+ const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
+
+ state.state.update(state.alloc, t) catch return .out_of_memory;
+ return .success;
+}
+
+pub fn get(
+ state_: RenderState,
+ data: Data,
+ out: ?*anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(Data, @intFromEnum(data)) catch {
+ log.warn("render_state_get invalid data value={d}", .{@intFromEnum(data)});
+ return .invalid_value;
+ };
+ }
+
+ return switch (data) {
+ .invalid => .invalid_value,
+ inline else => |comptime_data| getTyped(
+ state_,
+ comptime_data,
+ @ptrCast(@alignCast(out)),
+ ),
+ };
+}
+
+fn getTyped(
+ state_: RenderState,
+ comptime data: Data,
+ out: *data.OutType(),
+) Result {
+ const state = state_ orelse return .invalid_value;
+ switch (data) {
+ .invalid => return .invalid_value,
+ .cols => out.* = state.state.cols,
+ .rows => out.* = state.state.rows,
+ .dirty => out.* = state.state.dirty,
+ .row_iterator => {
+ const it = out.* orelse return .invalid_value;
+ const row_data = state.state.row_data.slice();
+ it.* = .{
+ .alloc = it.alloc,
+ .y = null,
+ .raws = row_data.items(.raw),
+ .cells = row_data.items(.cells),
+ .dirty = row_data.items(.dirty),
+ .palette = &state.state.colors.palette,
+ };
+ },
+ .color_background => out.* = state.state.colors.background.cval(),
+ .color_foreground => out.* = state.state.colors.foreground.cval(),
+ .color_cursor => {
+ const cursor = state.state.colors.cursor orelse return .invalid_value;
+ out.* = cursor.cval();
+ },
+ .color_cursor_has_value => out.* = state.state.colors.cursor != null,
+ .color_palette => out.* = colorpkg.paletteCval(&state.state.colors.palette),
+ .cursor_visual_style => out.* = CursorVisualStyle.fromCursorStyle(state.state.cursor.visual_style),
+ .cursor_visible => out.* = state.state.cursor.visible,
+ .cursor_blinking => out.* = state.state.cursor.blinking,
+ .cursor_password_input => out.* = state.state.cursor.password_input,
+ .cursor_viewport_has_value => out.* = state.state.cursor.viewport != null,
+ .cursor_viewport_x => {
+ const vp = state.state.cursor.viewport orelse return .invalid_value;
+ out.* = vp.x;
+ },
+ .cursor_viewport_y => {
+ const vp = state.state.cursor.viewport orelse return .invalid_value;
+ out.* = vp.y;
+ },
+ .cursor_viewport_wide_tail => {
+ const vp = state.state.cursor.viewport orelse return .invalid_value;
+ out.* = vp.wide_tail;
+ },
+ }
+
+ return .success;
+}
+
+pub fn set(
+ state_: RenderState,
+ option: SetOption,
+ value: ?*const anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(SetOption, @intFromEnum(option)) catch {
+ log.warn("render_state_set invalid option value={d}", .{@intFromEnum(option)});
+ return .invalid_value;
+ };
+ }
+
+ return switch (option) {
+ inline else => |comptime_option| setTyped(
+ state_,
+ comptime_option,
+ @ptrCast(@alignCast(value orelse return .invalid_value)),
+ ),
+ };
+}
+
+fn setTyped(
+ state_: RenderState,
+ comptime option: SetOption,
+ value: *const option.InType(),
+) Result {
+ const state = state_ orelse return .invalid_value;
+ switch (option) {
+ .dirty => state.state.dirty = value.*,
+ }
+
+ return .success;
+}
+
+pub fn colors_get(
+ state_: RenderState,
+ out_colors_: ?*Colors,
+) callconv(lib.calling_conv) Result {
+ const state = state_ orelse return .invalid_value;
+ const out_colors = out_colors_ orelse return .invalid_value;
+ const out_size = out_colors.size;
+ if (out_size < @sizeOf(usize)) return .invalid_value;
+
+ const colors = state.state.colors;
+ if (lib.structSizedFieldFits(
+ Colors,
+ out_size,
+ "background",
+ )) {
+ out_colors.background = colors.background.cval();
+ }
+
+ if (lib.structSizedFieldFits(
+ Colors,
+ out_size,
+ "foreground",
+ )) {
+ out_colors.foreground = colors.foreground.cval();
+ }
+
+ if (colors.cursor) |cursor| {
+ if (lib.structSizedFieldFits(
+ Colors,
+ out_size,
+ "cursor",
+ )) {
+ out_colors.cursor = cursor.cval();
+ }
+ }
+
+ if (lib.structSizedFieldFits(
+ Colors,
+ out_size,
+ "cursor_has_value",
+ )) {
+ out_colors.cursor_has_value = colors.cursor != null;
+ }
+
+ {
+ const palette_offset = @offsetOf(Colors, "palette");
+ if (out_size > palette_offset) {
+ const available = out_size - palette_offset;
+ const max_entries = @min(colors.palette.len, available / @sizeOf(colorpkg.RGB.C));
+ for (0..max_entries) |i| {
+ out_colors.palette[i] = colors.palette[i].cval();
+ }
+ }
+ }
+
+ return .success;
+}
+
+pub fn row_iterator_new(
+ alloc_: ?*const CAllocator,
+ result: *RowIterator,
+) callconv(lib.calling_conv) Result {
+ const alloc = lib.alloc.default(alloc_);
+ const ptr = alloc.create(RowIteratorWrapper) catch {
+ result.* = null;
+ return .out_of_memory;
+ };
+ ptr.* = .{
+ .alloc = alloc,
+ .y = undefined,
+ .raws = undefined,
+ .cells = undefined,
+ .dirty = undefined,
+ .palette = undefined,
+ };
+ result.* = ptr;
+ return .success;
+}
+
+pub fn row_iterator_free(iterator_: RowIterator) callconv(lib.calling_conv) void {
+ const iterator = iterator_ orelse return;
+ const alloc = iterator.alloc;
+ alloc.destroy(iterator);
+}
+
+pub fn row_iterator_next(iterator_: RowIterator) callconv(lib.calling_conv) bool {
+ const it = iterator_ orelse return false;
+ const next_y: size.CellCountInt = if (it.y) |y| y + 1 else 0;
+ if (next_y >= it.raws.len) return false;
+ it.y = next_y;
+ return true;
+}
+
+pub fn row_cells_new(
+ alloc_: ?*const CAllocator,
+ result: *RowCells,
+) callconv(lib.calling_conv) Result {
+ const alloc = lib.alloc.default(alloc_);
+ const ptr = alloc.create(RowCellsWrapper) catch {
+ result.* = null;
+ return .out_of_memory;
+ };
+ ptr.* = .{
+ .alloc = alloc,
+ .x = undefined,
+ .raws = undefined,
+ .graphemes = undefined,
+ .styles = undefined,
+ .palette = undefined,
+ };
+ result.* = ptr;
+ return .success;
+}
+
+pub fn row_cells_next(cells_: RowCells) callconv(lib.calling_conv) bool {
+ const cells = cells_ orelse return false;
+ const next_x: size.CellCountInt = if (cells.x) |x| x + 1 else 0;
+ if (next_x >= cells.raws.len) return false;
+ cells.x = next_x;
+ return true;
+}
+
+pub fn row_cells_select(cells_: RowCells, x: size.CellCountInt) callconv(lib.calling_conv) Result {
+ const cells = cells_ orelse return .invalid_value;
+ if (x >= cells.raws.len) return .invalid_value;
+ cells.x = x;
+ return .success;
+}
+
+pub fn row_cells_free(cells_: RowCells) callconv(lib.calling_conv) void {
+ const cells = cells_ orelse return;
+ const alloc = cells.alloc;
+ alloc.destroy(cells);
+}
+
+/// C: GhosttyRenderStateRowCellsData
+pub const RowCellsData = enum(c_int) {
+ invalid = 0,
+ raw = 1,
+ style = 2,
+ graphemes_len = 3,
+ graphemes_buf = 4,
+ bg_color = 5,
+ fg_color = 6,
+
+ /// Output type expected for querying the data of the given kind.
+ pub fn OutType(comptime self: RowCellsData) type {
+ return switch (self) {
+ .invalid => void,
+ .raw => page.Cell.C,
+ .style => style_c.Style,
+ .graphemes_len => u32,
+ .graphemes_buf => u32,
+ .bg_color, .fg_color => colorpkg.RGB.C,
+ };
+ }
+};
+
+pub fn row_cells_get(
+ cells_: RowCells,
+ data: RowCellsData,
+ out: ?*anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(RowCellsData, @intFromEnum(data)) catch {
+ log.warn("render_state_row_cells_get invalid data value={d}", .{@intFromEnum(data)});
+ return .invalid_value;
+ };
+ }
+
+ return switch (data) {
+ .invalid => .invalid_value,
+ inline else => |comptime_data| rowCellsGetTyped(
+ cells_,
+ comptime_data,
+ @ptrCast(@alignCast(out)),
+ ),
+ };
+}
+
+fn rowCellsGetTyped(
+ cells_: RowCells,
+ comptime data: RowCellsData,
+ out: *data.OutType(),
+) Result {
+ const cells = cells_ orelse return .invalid_value;
+ const x = cells.x orelse return .invalid_value;
+ const cell = cells.raws[x];
+ switch (data) {
+ .invalid => return .invalid_value,
+ .raw => out.* = cell.cval(),
+ .style => out.* = if (cell.hasStyling())
+ style_c.Style.fromStyle(cells.styles[x])
+ else
+ style_c.Style.fromStyle(.{}),
+ .graphemes_len => {
+ if (!cell.hasText()) {
+ out.* = 0;
+ return .success;
+ }
+ const extra = if (cell.hasGrapheme()) cells.graphemes[x] else &[_]u21{};
+ out.* = @intCast(1 + extra.len);
+ },
+ .graphemes_buf => {
+ if (!cell.hasText()) return .success;
+ const extra = if (cell.hasGrapheme()) cells.graphemes[x] else &[_]u21{};
+ const buf: [*]u32 = @ptrCast(out);
+ buf[0] = cell.codepoint();
+ for (extra, 1..) |cp, i| {
+ buf[i] = cp;
+ }
+ },
+ .bg_color => {
+ const s: Style = if (cell.hasStyling()) cells.styles[x] else .{};
+ const bg = s.bg(&cell, cells.palette) orelse return .invalid_value;
+ out.* = bg.cval();
+ },
+ .fg_color => {
+ const s: Style = if (cell.hasStyling()) cells.styles[x] else .{};
+ if (s.fg_color == .none) return .invalid_value;
+ const fg = s.fg(.{ .default = .{}, .palette = cells.palette });
+ out.* = fg.cval();
+ },
+ }
+
+ return .success;
+}
+
+/// C: GhosttyRenderStateRowData
+pub const RowData = enum(c_int) {
+ invalid = 0,
+ dirty = 1,
+ raw = 2,
+ cells = 3,
+
+ /// Output type expected for querying the data of the given kind.
+ pub fn OutType(comptime self: RowData) type {
+ return switch (self) {
+ .invalid => void,
+ .dirty => bool,
+ .raw => row.CRow,
+ .cells => RowCells,
+ };
+ }
+};
+
+/// C: GhosttyRenderStateRowOption
+pub const RowOption = enum(c_int) {
+ dirty = 0,
+
+ /// Input type expected for setting the option.
+ pub fn InType(comptime self: RowOption) type {
+ return switch (self) {
+ .dirty => bool,
+ };
+ }
+};
+
+pub fn row_get(
+ iterator_: RowIterator,
+ data: RowData,
+ out: ?*anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(RowData, @intFromEnum(data)) catch {
+ log.warn("render_state_row_get invalid data value={d}", .{@intFromEnum(data)});
+ return .invalid_value;
+ };
+ }
+
+ return switch (data) {
+ .invalid => .invalid_value,
+ inline else => |comptime_data| rowGetTyped(
+ iterator_,
+ comptime_data,
+ @ptrCast(@alignCast(out)),
+ ),
+ };
+}
+
+fn rowGetTyped(
+ iterator_: RowIterator,
+ comptime data: RowData,
+ out: *data.OutType(),
+) Result {
+ const it = iterator_ orelse return .invalid_value;
+ const y = it.y orelse return .invalid_value;
+ switch (data) {
+ .invalid => return .invalid_value,
+ .dirty => out.* = it.dirty[y],
+ .raw => out.* = it.raws[y].cval(),
+ .cells => {
+ const cells = out.* orelse return .invalid_value;
+ const cell_data = it.cells[y].slice();
+ cells.* = .{
+ .alloc = cells.alloc,
+ .x = null,
+ .raws = cell_data.items(.raw),
+ .graphemes = cell_data.items(.grapheme),
+ .styles = cell_data.items(.style),
+ .palette = it.palette,
+ };
+ },
+ }
+
+ return .success;
+}
+
+pub fn row_set(
+ iterator_: RowIterator,
+ option: RowOption,
+ value: ?*const anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(RowOption, @intFromEnum(option)) catch {
+ log.warn("render_state_row_set invalid option value={d}", .{@intFromEnum(option)});
+ return .invalid_value;
+ };
+ }
+
+ return switch (option) {
+ inline else => |comptime_option| rowSetTyped(
+ iterator_,
+ comptime_option,
+ @ptrCast(@alignCast(value orelse return .invalid_value)),
+ ),
+ };
+}
+
+fn rowSetTyped(
+ iterator_: RowIterator,
+ comptime option: RowOption,
+ value: *const option.InType(),
+) Result {
+ const it = iterator_ orelse return .invalid_value;
+ const y = it.y orelse return .invalid_value;
+ switch (option) {
+ .dirty => it.dirty[y] = value.*,
+ }
+
+ return .success;
+}
+
+test "render: new/free" {
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ try testing.expect(state != null);
+ free(state);
+}
+
+test "render: free null" {
+ free(null);
+}
+
+test "render: update invalid value" {
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.invalid_value, update(null, null));
+ try testing.expectEqual(Result.invalid_value, update(state, null));
+}
+
+test "render: get invalid value" {
+ var cols: size.CellCountInt = 0;
+ try testing.expectEqual(Result.invalid_value, get(null, .cols, @ptrCast(&cols)));
+}
+
+test "render: get invalid data" {
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.invalid_value, get(state, .invalid, null));
+}
+
+test "render: colors get invalid value" {
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ var colors: Colors = std.mem.zeroes(Colors);
+ colors.size = @sizeOf(Colors);
+
+ try testing.expectEqual(Result.invalid_value, colors_get(null, &colors));
+ try testing.expectEqual(Result.invalid_value, colors_get(state, null));
+
+ colors.size = @sizeOf(usize) - 1;
+ try testing.expectEqual(Result.invalid_value, colors_get(state, &colors));
+}
+
+test "render: get/set dirty invalid value" {
+ var dirty: Dirty = .false;
+ try testing.expectEqual(Result.invalid_value, get(null, .dirty, @ptrCast(&dirty)));
+ const dirty_full: Dirty = .full;
+ try testing.expectEqual(Result.invalid_value, set(null, .dirty, @ptrCast(&dirty_full)));
+}
+
+test "render: get/set dirty" {
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ var dirty: Dirty = undefined;
+ try testing.expectEqual(Result.success, get(state, .dirty, @ptrCast(&dirty)));
+ try testing.expectEqual(Dirty.false, dirty);
+
+ const dirty_partial: Dirty = .partial;
+ try testing.expectEqual(Result.success, set(state, .dirty, @ptrCast(&dirty_partial)));
+ try testing.expectEqual(Result.success, get(state, .dirty, @ptrCast(&dirty)));
+ try testing.expectEqual(Dirty.partial, dirty);
+
+ const dirty_full: Dirty = .full;
+ try testing.expectEqual(Result.success, set(state, .dirty, @ptrCast(&dirty_full)));
+ try testing.expectEqual(Result.success, get(state, .dirty, @ptrCast(&dirty)));
+ try testing.expectEqual(Dirty.full, dirty);
+}
+
+test "render: set null value" {
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.invalid_value, set(state, .dirty, null));
+}
+
+test "render: row iterator get invalid value" {
+ var iterator: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &iterator,
+ ));
+ defer row_iterator_free(iterator);
+
+ try testing.expectEqual(Result.invalid_value, get(null, .row_iterator, @ptrCast(&iterator)));
+}
+
+test "render: row iterator new/free" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var iterator: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &iterator,
+ ));
+ defer row_iterator_free(iterator);
+
+ try testing.expect(iterator != null);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&iterator)));
+
+ const iterator_ptr = iterator.?;
+ const row_data = state.?.state.row_data.slice();
+
+ try testing.expectEqual(@as(?size.CellCountInt, null), iterator_ptr.y);
+ try testing.expectEqual(row_data.items(.raw).len, iterator_ptr.raws.len);
+ try testing.expectEqual(row_data.items(.cells).len, iterator_ptr.cells.len);
+ try testing.expectEqual(row_data.items(.dirty).len, iterator_ptr.dirty.len);
+}
+
+test "render: row iterator free null" {
+ row_iterator_free(null);
+}
+
+test "render: row iterator next null" {
+ try testing.expect(!row_iterator_next(null));
+}
+
+test "render: row get null" {
+ var dirty: bool = undefined;
+ try testing.expectEqual(Result.invalid_value, row_get(null, .dirty, @ptrCast(&dirty)));
+}
+
+test "render: row get invalid data" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var iterator: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &iterator,
+ ));
+ defer row_iterator_free(iterator);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&iterator)));
+ try testing.expect(row_iterator_next(iterator));
+ try testing.expectEqual(Result.invalid_value, row_get(iterator, .invalid, null));
+}
+
+test "render: row set null" {
+ const dirty = false;
+ try testing.expectEqual(Result.invalid_value, row_set(null, .dirty, @ptrCast(&dirty)));
+}
+
+test "render: row set before iteration" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var iterator: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &iterator,
+ ));
+ defer row_iterator_free(iterator);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&iterator)));
+ const dirty = false;
+ try testing.expectEqual(Result.invalid_value, row_set(iterator, .dirty, @ptrCast(&dirty)));
+}
+
+test "render: row get before iteration" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var iterator: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &iterator,
+ ));
+ defer row_iterator_free(iterator);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&iterator)));
+ var dirty: bool = undefined;
+ try testing.expectEqual(Result.invalid_value, row_get(iterator, .dirty, @ptrCast(&dirty)));
+}
+
+test "render: row get/set dirty" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ // Dirty the first row so the iterator has at least one dirty row to observe.
+ terminal_c.vt_write(terminal, "hello", 5);
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ // Create an iterator and verify it is dirty.
+ var it: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it,
+ ));
+ defer row_iterator_free(it);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
+ try testing.expect(row_iterator_next(it));
+ var dirty: bool = undefined;
+ try testing.expectEqual(Result.success, row_get(it, .dirty, @ptrCast(&dirty)));
+ try testing.expect(dirty);
+
+ // Clear dirty on this row.
+ const dirty_false = false;
+ try testing.expectEqual(Result.success, row_set(it, .dirty, @ptrCast(&dirty_false)));
+
+ // It should not be dirty anymore.
+ var it2: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it2,
+ ));
+ defer row_iterator_free(it2);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it2)));
+ try testing.expect(row_iterator_next(it2));
+ try testing.expectEqual(Result.success, row_get(it2, .dirty, @ptrCast(&dirty)));
+ try testing.expect(!dirty);
+}
+
+test "render: row iterator next" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var iterator: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &iterator,
+ ));
+ defer row_iterator_free(iterator);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&iterator)));
+
+ const rows = state.?.state.rows;
+ if (rows == 0) {
+ try testing.expect(!row_iterator_next(iterator));
+ return;
+ }
+
+ try testing.expect(row_iterator_next(iterator));
+ try testing.expectEqual(@as(?size.CellCountInt, 0), iterator.?.y);
+
+ var i: size.CellCountInt = 1;
+ while (i < rows) : (i += 1) {
+ try testing.expect(row_iterator_next(iterator));
+ try testing.expectEqual(@as(?size.CellCountInt, i), iterator.?.y);
+ }
+
+ try testing.expect(!row_iterator_next(iterator));
+ try testing.expectEqual(@as(?size.CellCountInt, rows - 1), iterator.?.y);
+}
+
+test "render: update" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ terminal_c.vt_write(terminal, "hello", 5);
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var cols: size.CellCountInt = 0;
+ var rows_val: size.CellCountInt = 0;
+ try testing.expectEqual(Result.success, get(state, .cols, @ptrCast(&cols)));
+ try testing.expectEqual(Result.success, get(state, .rows, @ptrCast(&rows_val)));
+ try testing.expectEqual(@as(size.CellCountInt, 80), cols);
+ try testing.expectEqual(@as(size.CellCountInt, 24), rows_val);
+}
+
+test "render: colors get" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var colors: Colors = std.mem.zeroes(Colors);
+ colors.size = @sizeOf(Colors);
+ try testing.expectEqual(Result.success, colors_get(state, &colors));
+
+ const state_colors = &state.?.state.colors;
+ try testing.expectEqual(state_colors.background.cval(), colors.background);
+ try testing.expectEqual(state_colors.foreground.cval(), colors.foreground);
+
+ if (state_colors.cursor) |cursor| {
+ try testing.expect(colors.cursor_has_value);
+ try testing.expectEqual(cursor.cval(), colors.cursor);
+ } else {
+ try testing.expect(!colors.cursor_has_value);
+ }
+
+ for (state_colors.palette, colors.palette) |expected, actual| {
+ try testing.expectEqual(expected.cval(), actual);
+ }
+}
+
+test "render: row cells bg_color no background" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ // Write plain text (no background color set).
+ terminal_c.vt_write(terminal, "hello", 5);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var it: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it,
+ ));
+ defer row_iterator_free(it);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
+ try testing.expect(row_iterator_next(it));
+
+ var cells: RowCells = null;
+ try testing.expectEqual(Result.success, row_cells_new(
+ &lib.alloc.test_allocator,
+ &cells,
+ ));
+ defer row_cells_free(cells);
+
+ try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
+ try testing.expect(row_cells_next(cells));
+
+ // No background set, should return invalid_value.
+ var bg: colorpkg.RGB.C = undefined;
+ try testing.expectEqual(Result.invalid_value, row_cells_get(cells, .bg_color, @ptrCast(&bg)));
+}
+
+test "render: row cells bg_color from style" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ // Set an RGB background via SGR 48;2;R;G;B and write text.
+ terminal_c.vt_write(terminal, "\x1b[48;2;10;20;30mA", 18);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var it: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it,
+ ));
+ defer row_iterator_free(it);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
+ try testing.expect(row_iterator_next(it));
+
+ var cells: RowCells = null;
+ try testing.expectEqual(Result.success, row_cells_new(
+ &lib.alloc.test_allocator,
+ &cells,
+ ));
+ defer row_cells_free(cells);
+
+ try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
+ try testing.expect(row_cells_next(cells));
+
+ var bg: colorpkg.RGB.C = undefined;
+ try testing.expectEqual(Result.success, row_cells_get(cells, .bg_color, @ptrCast(&bg)));
+ try testing.expectEqual(@as(u8, 10), bg.r);
+ try testing.expectEqual(@as(u8, 20), bg.g);
+ try testing.expectEqual(@as(u8, 30), bg.b);
+}
+
+test "render: row cells bg_color from content tag" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ // Set an RGB background and then erase the line. The erased cells
+ // should carry the background color via the content tag (bg_color_rgb)
+ // rather than through the style.
+ terminal_c.vt_write(terminal, "\x1b[48;2;10;20;30m\x1b[2K", 21);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var it: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it,
+ ));
+ defer row_iterator_free(it);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
+ try testing.expect(row_iterator_next(it));
+
+ var cells: RowCells = null;
+ try testing.expectEqual(Result.success, row_cells_new(
+ &lib.alloc.test_allocator,
+ &cells,
+ ));
+ defer row_cells_free(cells);
+
+ try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
+ try testing.expect(row_cells_next(cells));
+
+ var bg: colorpkg.RGB.C = undefined;
+ try testing.expectEqual(Result.success, row_cells_get(cells, .bg_color, @ptrCast(&bg)));
+ try testing.expectEqual(@as(u8, 10), bg.r);
+ try testing.expectEqual(@as(u8, 20), bg.g);
+ try testing.expectEqual(@as(u8, 30), bg.b);
+}
+
+test "render: row cells fg_color no foreground" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ // Write plain text (no foreground color set).
+ terminal_c.vt_write(terminal, "hello", 5);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var it: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it,
+ ));
+ defer row_iterator_free(it);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
+ try testing.expect(row_iterator_next(it));
+
+ var cells: RowCells = null;
+ try testing.expectEqual(Result.success, row_cells_new(
+ &lib.alloc.test_allocator,
+ &cells,
+ ));
+ defer row_cells_free(cells);
+
+ try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
+ try testing.expect(row_cells_next(cells));
+
+ // No foreground set, should return invalid_value.
+ var fg: colorpkg.RGB.C = undefined;
+ try testing.expectEqual(Result.invalid_value, row_cells_get(cells, .fg_color, @ptrCast(&fg)));
+}
+
+test "render: row cells fg_color from style" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ // Set an RGB foreground via SGR 38;2;R;G;B and write text.
+ terminal_c.vt_write(terminal, "\x1b[38;2;10;20;30mA", 18);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var it: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it,
+ ));
+ defer row_iterator_free(it);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
+ try testing.expect(row_iterator_next(it));
+
+ var cells: RowCells = null;
+ try testing.expectEqual(Result.success, row_cells_new(
+ &lib.alloc.test_allocator,
+ &cells,
+ ));
+ defer row_cells_free(cells);
+
+ try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
+ try testing.expect(row_cells_next(cells));
+
+ var fg: colorpkg.RGB.C = undefined;
+ try testing.expectEqual(Result.success, row_cells_get(cells, .fg_color, @ptrCast(&fg)));
+ try testing.expectEqual(@as(u8, 10), fg.r);
+ try testing.expectEqual(@as(u8, 20), fg.g);
+ try testing.expectEqual(@as(u8, 30), fg.b);
+}
+
+test "render: colors get supports truncated sized struct" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var colors: Colors = std.mem.zeroes(Colors);
+ const sentinel: colorpkg.RGB.C = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC };
+ for (&colors.palette) |*entry| entry.* = sentinel;
+
+ colors.size = @offsetOf(Colors, "palette") + @sizeOf(colorpkg.RGB.C) * 2;
+ try testing.expectEqual(Result.success, colors_get(state, &colors));
+
+ const state_colors = &state.?.state.colors;
+ try testing.expectEqual(state_colors.palette[0].cval(), colors.palette[0]);
+ try testing.expectEqual(state_colors.palette[1].cval(), colors.palette[1]);
+ try testing.expectEqual(sentinel, colors.palette[2]);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/result.zig b/vendor/libghostty-vt/src/terminal/c/result.zig
new file mode 100644
index 00000000..32866328
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/result.zig
@@ -0,0 +1,8 @@
+/// C: GhosttyResult
+pub const Result = enum(c_int) {
+ success = 0,
+ out_of_memory = -1,
+ invalid_value = -2,
+ out_of_space = -3,
+ no_value = -4,
+};
diff --git a/vendor/libghostty-vt/src/terminal/c/row.zig b/vendor/libghostty-vt/src/terminal/c/row.zig
new file mode 100644
index 00000000..d4532706
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/row.zig
@@ -0,0 +1,132 @@
+const std = @import("std");
+const testing = std.testing;
+const lib = @import("../lib.zig");
+const page = @import("../page.zig");
+const Row = page.Row;
+const Result = @import("result.zig").Result;
+
+/// C: GhosttyRow
+pub const CRow = Row.C;
+
+/// C: GhosttyRowSemanticPrompt
+pub const SemanticPrompt = enum(c_int) {
+ none = 0,
+ prompt = 1,
+ prompt_continuation = 2,
+};
+
+/// C: GhosttyRowData
+pub const RowData = enum(c_int) {
+ invalid = 0,
+
+ /// Whether this row is soft-wrapped.
+ /// Output type: bool *
+ wrap = 1,
+
+ /// Whether this row is a continuation of a soft-wrapped row.
+ /// Output type: bool *
+ wrap_continuation = 2,
+
+ /// Whether any cells in this row have grapheme clusters.
+ /// Output type: bool *
+ grapheme = 3,
+
+ /// Whether any cells in this row have styling (may have false positives).
+ /// Output type: bool *
+ styled = 4,
+
+ /// Whether any cells in this row have hyperlinks (may have false positives).
+ /// Output type: bool *
+ hyperlink = 5,
+
+ /// The semantic prompt state of this row.
+ /// Output type: GhosttyRowSemanticPrompt *
+ semantic_prompt = 6,
+
+ /// Whether this row contains a Kitty virtual placeholder.
+ /// Output type: bool *
+ kitty_virtual_placeholder = 7,
+
+ /// Whether this row is dirty and requires a redraw.
+ /// Output type: bool *
+ dirty = 8,
+
+ /// Output type expected for querying the data of the given kind.
+ pub fn OutType(comptime self: RowData) type {
+ return switch (self) {
+ .invalid => void,
+ .wrap, .wrap_continuation, .grapheme, .styled, .hyperlink => bool,
+ .kitty_virtual_placeholder, .dirty => bool,
+ .semantic_prompt => SemanticPrompt,
+ };
+ }
+};
+
+pub fn get(
+ row_: CRow,
+ data: RowData,
+ out: ?*anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(RowData, @intFromEnum(data)) catch {
+ return .invalid_value;
+ };
+ }
+
+ return switch (data) {
+ .invalid => .invalid_value,
+ inline else => |comptime_data| getTyped(
+ row_,
+ comptime_data,
+ @ptrCast(@alignCast(out)),
+ ),
+ };
+}
+
+fn getTyped(
+ row_: CRow,
+ comptime data: RowData,
+ out: *data.OutType(),
+) Result {
+ const row: Row = @bitCast(row_);
+ switch (data) {
+ .invalid => return .invalid_value,
+ .wrap => out.* = row.wrap,
+ .wrap_continuation => out.* = row.wrap_continuation,
+ .grapheme => out.* = row.grapheme,
+ .styled => out.* = row.styled,
+ .hyperlink => out.* = row.hyperlink,
+ .semantic_prompt => out.* = @enumFromInt(@intFromEnum(row.semantic_prompt)),
+ .kitty_virtual_placeholder => out.* = row.kitty_virtual_placeholder,
+ .dirty => out.* = row.dirty,
+ }
+
+ return .success;
+}
+
+test "get wrap" {
+ var zig_row: Row = @bitCast(@as(u64, 0));
+ zig_row.wrap = true;
+ const row: CRow = @bitCast(zig_row);
+ var wrap: bool = false;
+ try testing.expectEqual(Result.success, get(row, .wrap, @ptrCast(&wrap)));
+ try testing.expect(wrap);
+}
+
+test "get semantic_prompt" {
+ var zig_row: Row = @bitCast(@as(u64, 0));
+ zig_row.semantic_prompt = .prompt;
+ const row: CRow = @bitCast(zig_row);
+ var sp: SemanticPrompt = .none;
+ try testing.expectEqual(Result.success, get(row, .semantic_prompt, @ptrCast(&sp)));
+ try testing.expectEqual(SemanticPrompt.prompt, sp);
+}
+
+test "get dirty" {
+ var zig_row: Row = @bitCast(@as(u64, 0));
+ zig_row.dirty = true;
+ const row: CRow = @bitCast(zig_row);
+ var dirty: bool = false;
+ try testing.expectEqual(Result.success, get(row, .dirty, @ptrCast(&dirty)));
+ try testing.expect(dirty);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/sgr.zig b/vendor/libghostty-vt/src/terminal/c/sgr.zig
new file mode 100644
index 00000000..b0ef23d0
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/sgr.zig
@@ -0,0 +1,179 @@
+const std = @import("std");
+const testing = std.testing;
+const Allocator = std.mem.Allocator;
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const sgr = @import("../sgr.zig");
+const Result = @import("result.zig").Result;
+
+const log = std.log.scoped(.sgr);
+
+/// Wrapper around parser that tracks the allocator for C API usage.
+const ParserWrapper = struct {
+ parser: sgr.Parser,
+ alloc: Allocator,
+};
+
+/// C: GhosttySgrParser
+pub const Parser = ?*ParserWrapper;
+
+pub fn new(
+ alloc_: ?*const CAllocator,
+ result: *Parser,
+) callconv(lib.calling_conv) Result {
+ const alloc = lib.alloc.default(alloc_);
+ const ptr = alloc.create(ParserWrapper) catch
+ return .out_of_memory;
+ ptr.* = .{
+ .parser = .empty,
+ .alloc = alloc,
+ };
+ result.* = ptr;
+ return .success;
+}
+
+pub fn free(parser_: Parser) callconv(lib.calling_conv) void {
+ const wrapper = parser_ orelse return;
+ const alloc = wrapper.alloc;
+ const parser: *sgr.Parser = &wrapper.parser;
+ if (parser.params.len > 0) alloc.free(parser.params);
+ alloc.destroy(wrapper);
+}
+
+pub fn reset(parser_: Parser) callconv(lib.calling_conv) void {
+ const wrapper = parser_ orelse return;
+ const parser: *sgr.Parser = &wrapper.parser;
+ parser.idx = 0;
+}
+
+pub fn setParams(
+ parser_: Parser,
+ params: [*]const u16,
+ seps_: ?[*]const u8,
+ len: usize,
+) callconv(lib.calling_conv) Result {
+ const wrapper = parser_ orelse return .invalid_value;
+ const alloc = wrapper.alloc;
+ const parser: *sgr.Parser = &wrapper.parser;
+
+ // Copy our new parameters
+ const params_slice = alloc.dupe(u16, params[0..len]) catch
+ return .out_of_memory;
+ if (parser.params.len > 0) alloc.free(parser.params);
+ parser.params = params_slice;
+
+ // If we have separators, set that state too.
+ parser.params_sep = .initEmpty();
+ if (seps_) |seps| {
+ if (len > @TypeOf(parser.params_sep).bit_length) {
+ log.warn("ghostty_sgr_set_params: separators length {} exceeds max supported length {}", .{
+ len,
+ @TypeOf(parser.params_sep).bit_length,
+ });
+ return .invalid_value;
+ }
+
+ for (seps[0..len], 0..) |sep, i| {
+ if (sep == ':') parser.params_sep.set(i);
+ }
+ }
+
+ // Reset our parsing state
+ parser.idx = 0;
+
+ return .success;
+}
+
+pub fn next(
+ parser_: Parser,
+ result: *sgr.Attribute.C,
+) callconv(lib.calling_conv) bool {
+ const wrapper = parser_ orelse return false;
+ const parser: *sgr.Parser = &wrapper.parser;
+ if (parser.next()) |attr| {
+ result.* = attr.cval();
+ return true;
+ }
+
+ return false;
+}
+
+pub fn unknown_full(
+ unknown: sgr.Attribute.Unknown.C,
+ ptr: ?*[*]const u16,
+) callconv(lib.calling_conv) usize {
+ if (ptr) |p| p.* = unknown.full_ptr;
+ return unknown.full_len;
+}
+
+pub fn unknown_partial(
+ unknown: sgr.Attribute.Unknown.C,
+ ptr: ?*[*]const u16,
+) callconv(lib.calling_conv) usize {
+ if (ptr) |p| p.* = unknown.partial_ptr;
+ return unknown.partial_len;
+}
+
+pub fn attribute_tag(
+ attr: sgr.Attribute.C,
+) callconv(lib.calling_conv) sgr.Attribute.Tag {
+ return attr.tag;
+}
+
+pub fn attribute_value(
+ attr: *sgr.Attribute.C,
+) callconv(lib.calling_conv) *sgr.Attribute.CValue {
+ return &attr.value;
+}
+
+pub fn wasm_alloc_attribute() callconv(lib.calling_conv) *sgr.Attribute.C {
+ const alloc = std.heap.wasm_allocator;
+ const ptr = alloc.create(sgr.Attribute.C) catch @panic("out of memory");
+ return ptr;
+}
+
+pub fn wasm_free_attribute(attr: *sgr.Attribute.C) callconv(lib.calling_conv) void {
+ const alloc = std.heap.wasm_allocator;
+ alloc.destroy(attr);
+}
+
+test "alloc" {
+ var p: Parser = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &p,
+ ));
+ free(p);
+}
+
+test "simple params, no seps" {
+ var p: Parser = undefined;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &p,
+ ));
+ defer free(p);
+
+ try testing.expectEqual(Result.success, setParams(
+ p,
+ &.{1},
+ null,
+ 1,
+ ));
+
+ // Set it twice on purpose to make sure we don't leak.
+ try testing.expectEqual(Result.success, setParams(
+ p,
+ &.{1},
+ null,
+ 1,
+ ));
+
+ // Verify we get bold
+ var attr: sgr.Attribute.C = undefined;
+ try testing.expect(next(p, &attr));
+ try testing.expectEqual(.bold, attr.tag);
+
+ // Nothing else
+ try testing.expect(!next(p, &attr));
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/size_report.zig b/vendor/libghostty-vt/src/terminal/c/size_report.zig
new file mode 100644
index 00000000..1c350c8f
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/size_report.zig
@@ -0,0 +1,82 @@
+const std = @import("std");
+const lib = @import("../lib.zig");
+const terminal_size_report = @import("../size_report.zig");
+const Result = @import("result.zig").Result;
+
+/// C: GhosttySizeReportStyle
+pub const Style = terminal_size_report.Style;
+
+/// C: GhosttySizeReportSize
+pub const Size = terminal_size_report.Size;
+
+pub fn encode(
+ style: Style,
+ size: Size,
+ out_: ?[*]u8,
+ out_len: usize,
+ out_written: *usize,
+) callconv(lib.calling_conv) Result {
+ var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{});
+ terminal_size_report.encode(&writer, style, size) catch |err| switch (err) {
+ error.WriteFailed => {
+ var discarding: std.Io.Writer.Discarding = .init(&.{});
+ terminal_size_report.encode(&discarding.writer, style, size) catch unreachable;
+ out_written.* = @intCast(discarding.count);
+ return .out_of_space;
+ },
+ };
+
+ out_written.* = writer.end;
+ return .success;
+}
+
+test "encode mode 2048" {
+ var buf: [64]u8 = undefined;
+ var written: usize = 0;
+ const result = encode(.mode_2048, .{
+ .rows = 24,
+ .columns = 80,
+ .cell_width = 9,
+ .cell_height = 18,
+ }, &buf, buf.len, &written);
+ try std.testing.expectEqual(.success, result);
+ try std.testing.expectEqualStrings("\x1B[48;24;80;432;720t", buf[0..written]);
+}
+
+test "encode csi 14 t" {
+ var buf: [64]u8 = undefined;
+ var written: usize = 0;
+ const result = encode(.csi_14_t, .{
+ .rows = 24,
+ .columns = 80,
+ .cell_width = 9,
+ .cell_height = 18,
+ }, &buf, buf.len, &written);
+ try std.testing.expectEqual(.success, result);
+ try std.testing.expectEqualStrings("\x1b[4;432;720t", buf[0..written]);
+}
+
+test "encode with insufficient buffer" {
+ var buf: [1]u8 = undefined;
+ var written: usize = 0;
+ const result = encode(.csi_18_t, .{
+ .rows = 24,
+ .columns = 80,
+ .cell_width = 9,
+ .cell_height = 18,
+ }, &buf, buf.len, &written);
+ try std.testing.expectEqual(.out_of_space, result);
+ try std.testing.expect(written > 1);
+}
+
+test "encode with null buffer" {
+ var written: usize = 0;
+ const result = encode(.csi_18_t, .{
+ .rows = 24,
+ .columns = 80,
+ .cell_width = 9,
+ .cell_height = 18,
+ }, null, 0, &written);
+ try std.testing.expectEqual(.out_of_space, result);
+ try std.testing.expect(written > 0);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/style.zig b/vendor/libghostty-vt/src/terminal/c/style.zig
new file mode 100644
index 00000000..82729fa9
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/style.zig
@@ -0,0 +1,134 @@
+const std = @import("std");
+const assert = std.debug.assert;
+const testing = std.testing;
+const lib = @import("../lib.zig");
+const style = @import("../style.zig");
+const color = @import("../color.zig");
+const sgr = @import("../sgr.zig");
+
+/// C: GhosttyStyleColorTag
+pub const ColorTag = enum(c_int) {
+ none = 0,
+ palette = 1,
+ rgb = 2,
+};
+
+/// C: GhosttyStyleColorValue
+pub const ColorValue = extern union {
+ palette: u8,
+ rgb: color.RGB.C,
+ _padding: u64,
+};
+
+/// C: GhosttyStyleColor
+pub const Color = extern struct {
+ tag: ColorTag,
+ value: ColorValue,
+
+ pub fn fromColor(c: style.Style.Color) Color {
+ return switch (c) {
+ .none => .{
+ .tag = .none,
+ .value = .{ ._padding = 0 },
+ },
+ .palette => |idx| .{
+ .tag = .palette,
+ .value = .{ .palette = idx },
+ },
+ .rgb => |rgb| .{
+ .tag = .rgb,
+ .value = .{ .rgb = rgb.cval() },
+ },
+ };
+ }
+};
+
+/// C: GhosttyStyle
+pub const Style = extern struct {
+ size: usize = @sizeOf(Style),
+ fg_color: Color,
+ bg_color: Color,
+ underline_color: Color,
+ bold: bool,
+ italic: bool,
+ faint: bool,
+ blink: bool,
+ inverse: bool,
+ invisible: bool,
+ strikethrough: bool,
+ overline: bool,
+ underline: c_int,
+
+ pub fn fromStyle(s: style.Style) Style {
+ return .{
+ .fg_color = .fromColor(s.fg_color),
+ .bg_color = .fromColor(s.bg_color),
+ .underline_color = .fromColor(s.underline_color),
+ .bold = s.flags.bold,
+ .italic = s.flags.italic,
+ .faint = s.flags.faint,
+ .blink = s.flags.blink,
+ .inverse = s.flags.inverse,
+ .invisible = s.flags.invisible,
+ .strikethrough = s.flags.strikethrough,
+ .overline = s.flags.overline,
+ .underline = @intFromEnum(s.flags.underline),
+ };
+ }
+};
+
+/// Returns the default style.
+pub fn default_style(result: *Style) callconv(lib.calling_conv) void {
+ result.* = .fromStyle(.{});
+ assert(result.size == @sizeOf(Style));
+}
+
+/// Returns true if the style is the default style.
+pub fn style_is_default(s: *const Style) callconv(lib.calling_conv) bool {
+ assert(s.size == @sizeOf(Style));
+ return s.fg_color.tag == .none and
+ s.bg_color.tag == .none and
+ s.underline_color.tag == .none and
+ s.bold == false and
+ s.italic == false and
+ s.faint == false and
+ s.blink == false and
+ s.inverse == false and
+ s.invisible == false and
+ s.strikethrough == false and
+ s.overline == false and
+ s.underline == 0;
+}
+
+test "default style" {
+ var s: Style = undefined;
+ default_style(&s);
+ try testing.expect(style_is_default(&s));
+ try testing.expectEqual(ColorTag.none, s.fg_color.tag);
+ try testing.expectEqual(ColorTag.none, s.bg_color.tag);
+ try testing.expectEqual(ColorTag.none, s.underline_color.tag);
+ try testing.expect(!s.bold);
+ try testing.expect(!s.italic);
+ try testing.expectEqual(@as(c_int, 0), s.underline);
+}
+
+test "convert style with colors" {
+ const zig_style: style.Style = .{
+ .fg_color = .{ .palette = 42 },
+ .bg_color = .{ .rgb = .{ .r = 255, .g = 128, .b = 64 } },
+ .underline_color = .none,
+ .flags = .{ .bold = true, .underline = .curly },
+ };
+
+ const c_style: Style = .fromStyle(zig_style);
+ try testing.expectEqual(ColorTag.palette, c_style.fg_color.tag);
+ try testing.expectEqual(@as(u8, 42), c_style.fg_color.value.palette);
+ try testing.expectEqual(ColorTag.rgb, c_style.bg_color.tag);
+ try testing.expectEqual(@as(u8, 255), c_style.bg_color.value.rgb.r);
+ try testing.expectEqual(@as(u8, 128), c_style.bg_color.value.rgb.g);
+ try testing.expectEqual(@as(u8, 64), c_style.bg_color.value.rgb.b);
+ try testing.expectEqual(ColorTag.none, c_style.underline_color.tag);
+ try testing.expect(c_style.bold);
+ try testing.expectEqual(@as(c_int, 3), c_style.underline);
+ try testing.expect(!style_is_default(&c_style));
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/terminal.zig b/vendor/libghostty-vt/src/terminal/c/terminal.zig
new file mode 100644
index 00000000..ec749ffa
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/terminal.zig
@@ -0,0 +1,2358 @@
+const std = @import("std");
+const testing = std.testing;
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const ZigTerminal = @import("../Terminal.zig");
+const Stream = @import("../stream_terminal.zig").Stream;
+const ScreenSet = @import("../ScreenSet.zig");
+const PageList = @import("../PageList.zig");
+const kitty = @import("../kitty/key.zig");
+const modes = @import("../modes.zig");
+const point = @import("../point.zig");
+const size = @import("../size.zig");
+const device_attributes = @import("../device_attributes.zig");
+const device_status = @import("../device_status.zig");
+const size_report = @import("../size_report.zig");
+const cell_c = @import("cell.zig");
+const row_c = @import("row.zig");
+const grid_ref_c = @import("grid_ref.zig");
+const style_c = @import("style.zig");
+const color = @import("../color.zig");
+const Result = @import("result.zig").Result;
+
+const Handler = @import("../stream_terminal.zig").Handler;
+
+const log = std.log.scoped(.terminal_c);
+
+/// Wrapper around ZigTerminal that tracks additional state for C API usage,
+/// such as the persistent VT stream needed to handle escape sequences split
+/// across multiple vt_write calls.
+const TerminalWrapper = struct {
+ terminal: *ZigTerminal,
+ stream: Stream,
+ effects: Effects = .{},
+};
+
+/// C callback state for terminal effects. Trampolines are always
+/// installed on the stream handler; they check these fields and
+/// no-op when the corresponding callback is null.
+const Effects = struct {
+ userdata: ?*anyopaque = null,
+ write_pty: ?WritePtyFn = null,
+ bell: ?BellFn = null,
+ color_scheme: ?ColorSchemeFn = null,
+ device_attributes_cb: ?DeviceAttributesFn = null,
+ enquiry: ?EnquiryFn = null,
+ xtversion: ?XtversionFn = null,
+ title_changed: ?TitleChangedFn = null,
+ size_cb: ?SizeFn = null,
+
+ /// Scratch buffer for DA1 feature codes. The device attributes
+ /// trampoline converts C feature codes into this buffer and returns
+ /// a slice pointing into it. Storing it here ensures the slice
+ /// remains valid after the trampoline returns, since the caller
+ /// (`reportDeviceAttributes`) reads it before any re-entrant call.
+ da_features_buf: [64]device_attributes.Primary.Feature = undefined,
+
+ /// C function pointer type for the write_pty callback.
+ pub const WritePtyFn = *const fn (Terminal, ?*anyopaque, [*]const u8, usize) callconv(lib.calling_conv) void;
+
+ /// C function pointer type for the bell callback.
+ pub const BellFn = *const fn (Terminal, ?*anyopaque) callconv(lib.calling_conv) void;
+
+ /// C function pointer type for the color_scheme callback.
+ /// Returns true and fills out_scheme if a color scheme is available,
+ /// or returns false to silently ignore the query.
+ pub const ColorSchemeFn = *const fn (Terminal, ?*anyopaque, *device_status.ColorScheme) callconv(lib.calling_conv) bool;
+
+ /// C function pointer type for the enquiry callback.
+ /// Returns the response bytes. The memory must remain valid
+ /// until the callback returns.
+ pub const EnquiryFn = *const fn (Terminal, ?*anyopaque) callconv(lib.calling_conv) lib.String;
+
+ /// C function pointer type for the xtversion callback.
+ /// Returns the version string (e.g. "ghostty 1.2.3"). The memory
+ /// must remain valid until the callback returns. An empty string
+ /// (len=0) causes the default "libghostty" to be reported.
+ pub const XtversionFn = *const fn (Terminal, ?*anyopaque) callconv(lib.calling_conv) lib.String;
+
+ /// C function pointer type for the title_changed callback.
+ pub const TitleChangedFn = *const fn (Terminal, ?*anyopaque) callconv(lib.calling_conv) void;
+
+ /// C function pointer type for the size callback.
+ /// Returns true and fills out_size if size is available,
+ /// or returns false to silently ignore the query.
+ pub const SizeFn = *const fn (Terminal, ?*anyopaque, *size_report.Size) callconv(lib.calling_conv) bool;
+
+ /// C function pointer type for the device_attributes callback.
+ /// Returns true and fills out_attrs if attributes are available,
+ /// or returns false to silently ignore the query.
+ pub const DeviceAttributesFn = *const fn (Terminal, ?*anyopaque, *CDeviceAttributes) callconv(lib.calling_conv) bool;
+
+ /// C-compatible device attributes struct.
+ /// C: GhosttyDeviceAttributes
+ pub const CDeviceAttributes = extern struct {
+ primary: Primary,
+ secondary: Secondary,
+ tertiary: Tertiary,
+
+ pub const Primary = extern struct {
+ conformance_level: u16,
+ features: [64]u16,
+ num_features: usize,
+ };
+
+ pub const Secondary = extern struct {
+ device_type: u16,
+ firmware_version: u16,
+ rom_cartridge: u16,
+ };
+
+ pub const Tertiary = extern struct {
+ unit_id: u32,
+ };
+ };
+
+ fn writePtyTrampoline(handler: *Handler, data: [:0]const u8) void {
+ const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
+ const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
+ const func = wrapper.effects.write_pty orelse return;
+ func(@ptrCast(wrapper), wrapper.effects.userdata, data.ptr, data.len);
+ }
+
+ fn bellTrampoline(handler: *Handler) void {
+ const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
+ const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
+ const func = wrapper.effects.bell orelse return;
+ func(@ptrCast(wrapper), wrapper.effects.userdata);
+ }
+
+ fn colorSchemeTrampoline(handler: *Handler) ?device_status.ColorScheme {
+ const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
+ const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
+ const func = wrapper.effects.color_scheme orelse return null;
+ var scheme: device_status.ColorScheme = undefined;
+ if (func(@ptrCast(wrapper), wrapper.effects.userdata, &scheme)) return scheme;
+ return null;
+ }
+
+ fn deviceAttributesTrampoline(handler: *Handler) device_attributes.Attributes {
+ const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
+ const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
+ const func = wrapper.effects.device_attributes_cb orelse return .{};
+
+ // Get our attributes from the callback.
+ var c_attrs: CDeviceAttributes = undefined;
+ if (!func(@ptrCast(wrapper), wrapper.effects.userdata, &c_attrs)) return .{};
+
+ // Note below we use a lot of enumFromInt but its always safe
+ // because all our types are non-exhaustive enums.
+
+ const n: usize = @min(c_attrs.primary.num_features, 64);
+ for (0..n) |i| wrapper.effects.da_features_buf[i] = @enumFromInt(c_attrs.primary.features[i]);
+
+ return .{
+ .primary = .{
+ .conformance_level = @enumFromInt(c_attrs.primary.conformance_level),
+ .features = wrapper.effects.da_features_buf[0..n],
+ },
+ .secondary = .{
+ .device_type = @enumFromInt(c_attrs.secondary.device_type),
+ .firmware_version = c_attrs.secondary.firmware_version,
+ .rom_cartridge = c_attrs.secondary.rom_cartridge,
+ },
+ .tertiary = .{
+ .unit_id = c_attrs.tertiary.unit_id,
+ },
+ };
+ }
+
+ fn enquiryTrampoline(handler: *Handler) []const u8 {
+ const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
+ const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
+ const func = wrapper.effects.enquiry orelse return "";
+ const result = func(@ptrCast(wrapper), wrapper.effects.userdata);
+ if (result.len == 0) return "";
+ return result.ptr[0..result.len];
+ }
+
+ fn xtversionTrampoline(handler: *Handler) []const u8 {
+ const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
+ const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
+ const func = wrapper.effects.xtversion orelse return "";
+ const result = func(@ptrCast(wrapper), wrapper.effects.userdata);
+ if (result.len == 0) return "";
+ return result.ptr[0..result.len];
+ }
+
+ fn titleChangedTrampoline(handler: *Handler) void {
+ const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
+ const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
+ const func = wrapper.effects.title_changed orelse return;
+ func(@ptrCast(wrapper), wrapper.effects.userdata);
+ }
+
+ fn sizeTrampoline(handler: *Handler) ?size_report.Size {
+ const stream_ptr: *Stream = @fieldParentPtr("handler", handler);
+ const wrapper: *TerminalWrapper = @fieldParentPtr("stream", stream_ptr);
+ const func = wrapper.effects.size_cb orelse return null;
+ var s: size_report.Size = undefined;
+ if (func(@ptrCast(wrapper), wrapper.effects.userdata, &s)) return s;
+ return null;
+ }
+};
+
+/// C: GhosttyTerminal
+pub const Terminal = ?*TerminalWrapper;
+
+/// C: GhosttyTerminalOptions
+pub const Options = extern struct {
+ cols: size.CellCountInt,
+ rows: size.CellCountInt,
+ max_scrollback: usize,
+};
+
+const NewError = error{
+ InvalidValue,
+ OutOfMemory,
+};
+
+pub fn new(
+ alloc_: ?*const CAllocator,
+ result: *Terminal,
+ opts: Options,
+) callconv(lib.calling_conv) Result {
+ result.* = new_(alloc_, opts) catch |err| {
+ result.* = null;
+ return switch (err) {
+ error.InvalidValue => .invalid_value,
+ error.OutOfMemory => .out_of_memory,
+ };
+ };
+
+ return .success;
+}
+
+fn new_(
+ alloc_: ?*const CAllocator,
+ opts: Options,
+) NewError!*TerminalWrapper {
+ if (opts.cols == 0 or opts.rows == 0) return error.InvalidValue;
+
+ const alloc = lib.alloc.default(alloc_);
+ const t = alloc.create(ZigTerminal) catch
+ return error.OutOfMemory;
+ errdefer alloc.destroy(t);
+
+ const wrapper = alloc.create(TerminalWrapper) catch
+ return error.OutOfMemory;
+ errdefer alloc.destroy(wrapper);
+
+ // Setup our terminal
+ t.* = try .init(alloc, .{
+ .cols = opts.cols,
+ .rows = opts.rows,
+ .max_scrollback = opts.max_scrollback,
+ });
+ errdefer t.deinit(alloc);
+
+ // Setup our stream with trampolines always installed so that
+ // setting C callbacks at any time takes effect immediately.
+ var handler: Stream.Handler = t.vtHandler();
+ handler.effects = .{
+ .write_pty = &Effects.writePtyTrampoline,
+ .bell = &Effects.bellTrampoline,
+ .color_scheme = &Effects.colorSchemeTrampoline,
+ .device_attributes = &Effects.deviceAttributesTrampoline,
+ .enquiry = &Effects.enquiryTrampoline,
+ .xtversion = &Effects.xtversionTrampoline,
+ .title_changed = &Effects.titleChangedTrampoline,
+ .size = &Effects.sizeTrampoline,
+ };
+
+ wrapper.* = .{
+ .terminal = t,
+ .stream = .initAlloc(alloc, handler),
+ };
+
+ return wrapper;
+}
+
+pub fn vt_write(
+ terminal_: Terminal,
+ ptr: [*]const u8,
+ len: usize,
+) callconv(lib.calling_conv) void {
+ const wrapper = terminal_ orelse return;
+ wrapper.stream.nextSlice(ptr[0..len]);
+}
+
+/// C: GhosttyTerminalOption
+pub const Option = enum(c_int) {
+ userdata = 0,
+ write_pty = 1,
+ bell = 2,
+ enquiry = 3,
+ xtversion = 4,
+ title_changed = 5,
+ size_cb = 6,
+ color_scheme = 7,
+ device_attributes = 8,
+ title = 9,
+ pwd = 10,
+ color_foreground = 11,
+ color_background = 12,
+ color_cursor = 13,
+ color_palette = 14,
+
+ /// Input type expected for setting the option.
+ pub fn InType(comptime self: Option) type {
+ return switch (self) {
+ .userdata => ?*const anyopaque,
+ .write_pty => ?Effects.WritePtyFn,
+ .bell => ?Effects.BellFn,
+ .color_scheme => ?Effects.ColorSchemeFn,
+ .device_attributes => ?Effects.DeviceAttributesFn,
+ .enquiry => ?Effects.EnquiryFn,
+ .xtversion => ?Effects.XtversionFn,
+ .title_changed => ?Effects.TitleChangedFn,
+ .size_cb => ?Effects.SizeFn,
+ .title, .pwd => ?*const lib.String,
+ .color_foreground, .color_background, .color_cursor => ?*const color.RGB.C,
+ .color_palette => ?*const color.PaletteC,
+ };
+ }
+};
+
+pub fn set(
+ terminal_: Terminal,
+ option: Option,
+ value: ?*const anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(Option, @intFromEnum(option)) catch {
+ log.warn("terminal_set invalid option value={d}", .{@intFromEnum(option)});
+ return .invalid_value;
+ };
+ }
+
+ const wrapper = terminal_ orelse return .invalid_value;
+
+ return switch (option) {
+ inline else => |comptime_option| setTyped(
+ wrapper,
+ comptime_option,
+ @ptrCast(@alignCast(value)),
+ ),
+ };
+}
+
+fn setTyped(
+ wrapper: *TerminalWrapper,
+ comptime option: Option,
+ value: option.InType(),
+) Result {
+ switch (option) {
+ .userdata => wrapper.effects.userdata = @constCast(value),
+ .write_pty => wrapper.effects.write_pty = value,
+ .bell => wrapper.effects.bell = value,
+ .color_scheme => wrapper.effects.color_scheme = value,
+ .device_attributes => wrapper.effects.device_attributes_cb = value,
+ .enquiry => wrapper.effects.enquiry = value,
+ .xtversion => wrapper.effects.xtversion = value,
+ .title_changed => wrapper.effects.title_changed = value,
+ .size_cb => wrapper.effects.size_cb = value,
+ .title => {
+ const str = if (value) |v| v.ptr[0..v.len] else "";
+ wrapper.terminal.setTitle(str) catch return .out_of_memory;
+ },
+ .pwd => {
+ const str = if (value) |v| v.ptr[0..v.len] else "";
+ wrapper.terminal.setPwd(str) catch return .out_of_memory;
+ },
+ .color_foreground => {
+ wrapper.terminal.colors.foreground.default = if (value) |v| .fromC(v.*) else null;
+ wrapper.terminal.flags.dirty.palette = true;
+ },
+ .color_background => {
+ wrapper.terminal.colors.background.default = if (value) |v| .fromC(v.*) else null;
+ wrapper.terminal.flags.dirty.palette = true;
+ },
+ .color_cursor => {
+ wrapper.terminal.colors.cursor.default = if (value) |v| .fromC(v.*) else null;
+ wrapper.terminal.flags.dirty.palette = true;
+ },
+ .color_palette => {
+ wrapper.terminal.colors.palette.changeDefault(
+ if (value) |v| color.paletteZval(v) else color.default,
+ );
+ wrapper.terminal.flags.dirty.palette = true;
+ },
+ }
+ return .success;
+}
+
+/// C: GhosttyDeviceAttributes
+pub const DeviceAttributes = Effects.CDeviceAttributes;
+
+/// C: GhosttyTerminalScrollViewport
+pub const ScrollViewport = ZigTerminal.ScrollViewport.C;
+
+pub fn scroll_viewport(
+ terminal_: Terminal,
+ behavior: ScrollViewport,
+) callconv(lib.calling_conv) void {
+ const t: *ZigTerminal = (terminal_ orelse return).terminal;
+ t.scrollViewport(switch (behavior.tag) {
+ .top => .top,
+ .bottom => .bottom,
+ .delta => .{ .delta = behavior.value.delta },
+ });
+}
+
+pub fn resize(
+ terminal_: Terminal,
+ cols: size.CellCountInt,
+ rows: size.CellCountInt,
+ cell_width_px: u32,
+ cell_height_px: u32,
+) callconv(lib.calling_conv) Result {
+ const wrapper = terminal_ orelse return .invalid_value;
+ const t = wrapper.terminal;
+ if (cols == 0 or rows == 0) return .invalid_value;
+ t.resize(t.gpa(), cols, rows) catch return .out_of_memory;
+
+ // Update pixel sizes
+ t.width_px = std.math.mul(u32, cols, cell_width_px) catch std.math.maxInt(u32);
+ t.height_px = std.math.mul(u32, rows, cell_height_px) catch std.math.maxInt(u32);
+
+ // Disable synchronized output mode so that we show changes
+ // immediately for a resize. This is allowed by the spec.
+ t.modes.set(.synchronized_output, false);
+
+ // If we have in-band size reporting enabled, send a report.
+ if (t.modes.get(.in_band_size_reports)) in_band: {
+ const func = wrapper.effects.write_pty orelse break :in_band;
+
+ var buf: [1024]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ size_report.encode(&writer, .mode_2048, .{
+ .rows = rows,
+ .columns = cols,
+ .cell_width = cell_width_px,
+ .cell_height = cell_height_px,
+ }) catch break :in_band;
+
+ const data = writer.buffered();
+ func(@ptrCast(wrapper), wrapper.effects.userdata, data.ptr, data.len);
+ }
+
+ return .success;
+}
+
+pub fn reset(terminal_: Terminal) callconv(lib.calling_conv) void {
+ const t: *ZigTerminal = (terminal_ orelse return).terminal;
+ t.fullReset();
+}
+
+pub fn mode_get(
+ terminal_: Terminal,
+ tag: modes.ModeTag.Backing,
+ out_value: *bool,
+) callconv(lib.calling_conv) Result {
+ const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
+ const mode_tag: modes.ModeTag = @bitCast(tag);
+ const mode = modes.modeFromInt(mode_tag.value, mode_tag.ansi) orelse return .invalid_value;
+ out_value.* = t.modes.get(mode);
+ return .success;
+}
+
+pub fn mode_set(
+ terminal_: Terminal,
+ tag: modes.ModeTag.Backing,
+ value: bool,
+) callconv(lib.calling_conv) Result {
+ const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
+ const mode_tag: modes.ModeTag = @bitCast(tag);
+ const mode = modes.modeFromInt(mode_tag.value, mode_tag.ansi) orelse return .invalid_value;
+ t.modes.set(mode, value);
+ return .success;
+}
+
+/// C: GhosttyTerminalScreen
+pub const TerminalScreen = ScreenSet.Key;
+
+/// C: GhosttyTerminalScrollbar
+pub const TerminalScrollbar = PageList.Scrollbar.C;
+
+/// C: GhosttyTerminalData
+pub const TerminalData = enum(c_int) {
+ invalid = 0,
+ cols = 1,
+ rows = 2,
+ cursor_x = 3,
+ cursor_y = 4,
+ cursor_pending_wrap = 5,
+ active_screen = 6,
+ cursor_visible = 7,
+ kitty_keyboard_flags = 8,
+ scrollbar = 9,
+ cursor_style = 10,
+ mouse_tracking = 11,
+ title = 12,
+ pwd = 13,
+ total_rows = 14,
+ scrollback_rows = 15,
+ width_px = 16,
+ height_px = 17,
+ color_foreground = 18,
+ color_background = 19,
+ color_cursor = 20,
+ color_palette = 21,
+ color_foreground_default = 22,
+ color_background_default = 23,
+ color_cursor_default = 24,
+ color_palette_default = 25,
+
+ /// Output type expected for querying the data of the given kind.
+ pub fn OutType(comptime self: TerminalData) type {
+ return switch (self) {
+ .invalid => void,
+ .cols, .rows, .cursor_x, .cursor_y => size.CellCountInt,
+ .cursor_pending_wrap, .cursor_visible, .mouse_tracking => bool,
+ .active_screen => TerminalScreen,
+ .kitty_keyboard_flags => u8,
+ .scrollbar => TerminalScrollbar,
+ .cursor_style => style_c.Style,
+ .title, .pwd => lib.String,
+ .total_rows, .scrollback_rows => usize,
+ .width_px, .height_px => u32,
+ .color_foreground,
+ .color_background,
+ .color_cursor,
+ .color_foreground_default,
+ .color_background_default,
+ .color_cursor_default,
+ => color.RGB.C,
+ .color_palette, .color_palette_default => color.PaletteC,
+ };
+ }
+};
+
+pub fn get(
+ terminal_: Terminal,
+ data: TerminalData,
+ out: ?*anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(TerminalData, @intFromEnum(data)) catch {
+ log.warn("terminal_get invalid data value={d}", .{@intFromEnum(data)});
+ return .invalid_value;
+ };
+ }
+
+ return switch (data) {
+ .invalid => .invalid_value,
+ inline else => |comptime_data| getTyped(
+ terminal_,
+ comptime_data,
+ @ptrCast(@alignCast(out)),
+ ),
+ };
+}
+
+fn getTyped(
+ terminal_: Terminal,
+ comptime data: TerminalData,
+ out: *data.OutType(),
+) Result {
+ const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
+ switch (data) {
+ .invalid => return .invalid_value,
+ .cols => out.* = t.cols,
+ .rows => out.* = t.rows,
+ .cursor_x => out.* = t.screens.active.cursor.x,
+ .cursor_y => out.* = t.screens.active.cursor.y,
+ .cursor_pending_wrap => out.* = t.screens.active.cursor.pending_wrap,
+ .active_screen => out.* = t.screens.active_key,
+ .cursor_visible => out.* = t.modes.get(.cursor_visible),
+ .kitty_keyboard_flags => out.* = @as(u8, t.screens.active.kitty_keyboard.current().int()),
+ .scrollbar => out.* = t.screens.active.pages.scrollbar().cval(),
+ .cursor_style => out.* = .fromStyle(t.screens.active.cursor.style),
+ .mouse_tracking => out.* = t.modes.get(.mouse_event_x10) or
+ t.modes.get(.mouse_event_normal) or
+ t.modes.get(.mouse_event_button) or
+ t.modes.get(.mouse_event_any),
+ .title => {
+ const title = t.getTitle() orelse "";
+ out.* = .{ .ptr = title.ptr, .len = title.len };
+ },
+ .pwd => {
+ const pwd = t.getPwd() orelse "";
+ out.* = .{ .ptr = pwd.ptr, .len = pwd.len };
+ },
+ .total_rows => out.* = t.screens.active.pages.total_rows,
+ .scrollback_rows => out.* = t.screens.active.pages.total_rows - t.rows,
+ .width_px => out.* = t.width_px,
+ .height_px => out.* = t.height_px,
+ .color_foreground => out.* = (t.colors.foreground.get() orelse return .no_value).cval(),
+ .color_background => out.* = (t.colors.background.get() orelse return .no_value).cval(),
+ .color_cursor => out.* = (t.colors.cursor.get() orelse return .no_value).cval(),
+ .color_foreground_default => out.* = (t.colors.foreground.default orelse return .no_value).cval(),
+ .color_background_default => out.* = (t.colors.background.default orelse return .no_value).cval(),
+ .color_cursor_default => out.* = (t.colors.cursor.default orelse return .no_value).cval(),
+ .color_palette => out.* = color.paletteCval(&t.colors.palette.current),
+ .color_palette_default => out.* = color.paletteCval(&t.colors.palette.original),
+ }
+
+ return .success;
+}
+
+pub fn grid_ref(
+ terminal_: Terminal,
+ pt: point.Point.C,
+ out_ref: ?*grid_ref_c.CGridRef,
+) callconv(lib.calling_conv) Result {
+ const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
+ const zig_pt: point.Point = switch (pt.tag) {
+ .active => .{ .active = pt.value.active },
+ .viewport => .{ .viewport = pt.value.viewport },
+ .screen => .{ .screen = pt.value.screen },
+ .history => .{ .history = pt.value.history },
+ };
+ const p = t.screens.active.pages.pin(zig_pt) orelse
+ return .invalid_value;
+ if (out_ref) |out| out.* = grid_ref_c.CGridRef.fromPin(p);
+ return .success;
+}
+
+pub fn free(terminal_: Terminal) callconv(lib.calling_conv) void {
+ const wrapper = terminal_ orelse return;
+ const t = wrapper.terminal;
+
+ wrapper.stream.deinit();
+ const alloc = t.gpa();
+ t.deinit(alloc);
+ alloc.destroy(t);
+ alloc.destroy(wrapper);
+}
+
+test "new/free" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+
+ try testing.expect(t != null);
+ free(t);
+}
+
+test "new invalid value" {
+ var t: Terminal = null;
+
+ try testing.expectEqual(Result.invalid_value, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 0,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ try testing.expect(t == null);
+
+ try testing.expectEqual(Result.invalid_value, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 0,
+ .max_scrollback = 10_000,
+ },
+ ));
+ try testing.expect(t == null);
+}
+
+test "free null" {
+ free(null);
+}
+
+test "scroll_viewport" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 5,
+ .rows = 2,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer free(t);
+
+ const zt = t.?.terminal;
+
+ // Write "hello" on the first line
+ vt_write(t, "hello", 5);
+
+ // Push "hello" into scrollback with 3 newlines (index = ESC D)
+ vt_write(t, "\x1bD\x1bD\x1bD", 6);
+ {
+ // Viewport should be empty now since hello scrolled off
+ const str = try zt.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("", str);
+ }
+
+ // Scroll to top: "hello" should be visible again
+ scroll_viewport(t, .{ .tag = .top, .value = undefined });
+ {
+ const str = try zt.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("hello", str);
+ }
+
+ // Scroll to bottom: viewport should be empty again
+ scroll_viewport(t, .{ .tag = .bottom, .value = undefined });
+ {
+ const str = try zt.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("", str);
+ }
+
+ // Scroll up by delta to bring "hello" back into view
+ scroll_viewport(t, .{ .tag = .delta, .value = .{ .delta = -3 } });
+ {
+ const str = try zt.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("hello", str);
+ }
+}
+
+test "scroll_viewport null" {
+ scroll_viewport(null, .{ .tag = .top, .value = undefined });
+}
+
+test "reset" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer free(t);
+
+ vt_write(t, "Hello", 5);
+ reset(t);
+
+ const str = try t.?.terminal.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("", str);
+}
+
+test "reset null" {
+ reset(null);
+}
+
+test "resize" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer free(t);
+
+ try testing.expectEqual(Result.success, resize(t, 40, 12, 9, 18));
+ try testing.expectEqual(40, t.?.terminal.cols);
+ try testing.expectEqual(12, t.?.terminal.rows);
+}
+
+test "resize null" {
+ try testing.expectEqual(Result.invalid_value, resize(null, 80, 24, 9, 18));
+}
+
+test "resize invalid value" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer free(t);
+
+ try testing.expectEqual(Result.invalid_value, resize(t, 0, 24, 9, 18));
+ try testing.expectEqual(Result.invalid_value, resize(t, 80, 0, 9, 18));
+}
+
+test "mode_get and mode_set" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ var value: bool = undefined;
+
+ // DEC mode 25 (cursor_visible) defaults to true
+ const cursor_visible: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 25, .ansi = false });
+ try testing.expectEqual(Result.success, mode_get(t, cursor_visible, &value));
+ try testing.expect(value);
+
+ // Set it to false
+ try testing.expectEqual(Result.success, mode_set(t, cursor_visible, false));
+ try testing.expectEqual(Result.success, mode_get(t, cursor_visible, &value));
+ try testing.expect(!value);
+
+ // ANSI mode 4 (insert) defaults to false
+ const insert: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 4, .ansi = true });
+ try testing.expectEqual(Result.success, mode_get(t, insert, &value));
+ try testing.expect(!value);
+
+ try testing.expectEqual(Result.success, mode_set(t, insert, true));
+ try testing.expectEqual(Result.success, mode_get(t, insert, &value));
+ try testing.expect(value);
+}
+
+test "mode_get null" {
+ var value: bool = undefined;
+ const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 25, .ansi = false });
+ try testing.expectEqual(Result.invalid_value, mode_get(null, tag, &value));
+}
+
+test "mode_set null" {
+ const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 25, .ansi = false });
+ try testing.expectEqual(Result.invalid_value, mode_set(null, tag, true));
+}
+
+test "mode_get unknown mode" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ var value: bool = undefined;
+ const unknown: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 9999, .ansi = false });
+ try testing.expectEqual(Result.invalid_value, mode_get(t, unknown, &value));
+}
+
+test "mode_set unknown mode" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const unknown: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 9999, .ansi = false });
+ try testing.expectEqual(Result.invalid_value, mode_set(t, unknown, true));
+}
+
+test "vt_write" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer free(t);
+
+ vt_write(t, "Hello", 5);
+
+ const str = try t.?.terminal.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("Hello", str);
+}
+
+test "vt_write split escape sequence" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer free(t);
+
+ // Write "Hello" in bold by splitting the CSI bold sequence across two writes.
+ // ESC [ 1 m = bold on, ESC [ 0 m = reset
+ // Split ESC from the rest of the CSI sequence.
+ vt_write(t, "Hello \x1b", 7);
+ vt_write(t, "[1mBold\x1b[0m", 10);
+
+ const str = try t.?.terminal.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ // If the escape sequence leaked, we'd see "[1mBold" as literal text.
+ try testing.expectEqualStrings("Hello Bold", str);
+}
+
+test "get cols and rows" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ var cols: size.CellCountInt = undefined;
+ var rows: size.CellCountInt = undefined;
+ try testing.expectEqual(Result.success, get(t, .cols, @ptrCast(&cols)));
+ try testing.expectEqual(Result.success, get(t, .rows, @ptrCast(&rows)));
+ try testing.expectEqual(80, cols);
+ try testing.expectEqual(24, rows);
+}
+
+test "get cursor position" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ vt_write(t, "Hello", 5);
+
+ var x: size.CellCountInt = undefined;
+ var y: size.CellCountInt = undefined;
+ try testing.expectEqual(Result.success, get(t, .cursor_x, @ptrCast(&x)));
+ try testing.expectEqual(Result.success, get(t, .cursor_y, @ptrCast(&y)));
+ try testing.expectEqual(5, x);
+ try testing.expectEqual(0, y);
+}
+
+test "get null" {
+ var cols: size.CellCountInt = undefined;
+ try testing.expectEqual(Result.invalid_value, get(null, .cols, @ptrCast(&cols)));
+}
+
+test "get cursor_visible" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ var visible: bool = undefined;
+ try testing.expectEqual(Result.success, get(t, .cursor_visible, @ptrCast(&visible)));
+ try testing.expect(visible);
+
+ // DEC mode 25 controls cursor visibility
+ const cursor_visible_mode: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 25, .ansi = false });
+ try testing.expectEqual(Result.success, mode_set(t, cursor_visible_mode, false));
+ try testing.expectEqual(Result.success, get(t, .cursor_visible, @ptrCast(&visible)));
+ try testing.expect(!visible);
+}
+
+test "get active_screen" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ var screen: TerminalScreen = undefined;
+ try testing.expectEqual(Result.success, get(t, .active_screen, @ptrCast(&screen)));
+ try testing.expectEqual(.primary, screen);
+}
+
+test "get kitty_keyboard_flags" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ var flags: u8 = undefined;
+ try testing.expectEqual(Result.success, get(t, .kitty_keyboard_flags, @ptrCast(&flags)));
+ try testing.expectEqual(0, flags);
+
+ // Push kitty flags via VT sequence: CSI > 3 u (push disambiguate | report_events)
+ vt_write(t, "\x1b[>3u", 5);
+
+ try testing.expectEqual(Result.success, get(t, .kitty_keyboard_flags, @ptrCast(&flags)));
+ try testing.expectEqual(3, flags);
+}
+
+test "get mouse_tracking" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ var tracking: bool = undefined;
+ try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking)));
+ try testing.expect(!tracking);
+
+ // Enable X10 mouse (DEC mode 9)
+ const x10_mode: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 9, .ansi = false });
+ try testing.expectEqual(Result.success, mode_set(t, x10_mode, true));
+ try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking)));
+ try testing.expect(tracking);
+
+ // Disable X10, enable normal mouse (DEC mode 1000)
+ try testing.expectEqual(Result.success, mode_set(t, x10_mode, false));
+ const normal_mode: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1000, .ansi = false });
+ try testing.expectEqual(Result.success, mode_set(t, normal_mode, true));
+ try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking)));
+ try testing.expect(tracking);
+
+ // Disable normal, enable button mouse (DEC mode 1002)
+ try testing.expectEqual(Result.success, mode_set(t, normal_mode, false));
+ const button_mode: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1002, .ansi = false });
+ try testing.expectEqual(Result.success, mode_set(t, button_mode, true));
+ try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking)));
+ try testing.expect(tracking);
+
+ // Disable button, enable any mouse (DEC mode 1003)
+ try testing.expectEqual(Result.success, mode_set(t, button_mode, false));
+ const any_mode: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1003, .ansi = false });
+ try testing.expectEqual(Result.success, mode_set(t, any_mode, true));
+ try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking)));
+ try testing.expect(tracking);
+
+ // Disable all - should be false again
+ try testing.expectEqual(Result.success, mode_set(t, any_mode, false));
+ try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking)));
+ try testing.expect(!tracking);
+}
+
+test "get total_rows" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer free(t);
+
+ var total: usize = undefined;
+ try testing.expectEqual(Result.success, get(t, .total_rows, @ptrCast(&total)));
+ try testing.expect(total >= 24);
+}
+
+test "get scrollback_rows" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 3,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer free(t);
+
+ var scrollback: usize = undefined;
+ try testing.expectEqual(Result.success, get(t, .scrollback_rows, @ptrCast(&scrollback)));
+ try testing.expectEqual(@as(usize, 0), scrollback);
+
+ // Write enough lines to push content into scrollback
+ vt_write(t, "line1\r\nline2\r\nline3\r\nline4\r\nline5\r\n", 34);
+
+ try testing.expectEqual(Result.success, get(t, .scrollback_rows, @ptrCast(&scrollback)));
+ try testing.expectEqual(@as(usize, 2), scrollback);
+}
+
+test "get invalid" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ try testing.expectEqual(Result.invalid_value, get(t, .invalid, null));
+}
+
+test "grid_ref" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ vt_write(t, "Hello", 5);
+
+ var out_ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 0, .y = 0 } },
+ }, &out_ref));
+
+ // Extract cell from grid ref and verify it contains 'H'
+ var out_cell: cell_c.CCell = undefined;
+ try testing.expectEqual(Result.success, grid_ref_c.grid_ref_cell(&out_ref, &out_cell));
+
+ var cp: u32 = 0;
+ try testing.expectEqual(Result.success, cell_c.get(out_cell, .codepoint, @ptrCast(&cp)));
+ try testing.expectEqual(@as(u32, 'H'), cp);
+}
+
+test "grid_ref null terminal" {
+ var out_ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.invalid_value, grid_ref(null, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 0, .y = 0 } },
+ }, &out_ref));
+}
+
+test "set write_pty callback" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var last_data: ?[]u8 = null;
+ var last_userdata: ?*anyopaque = null;
+
+ fn deinit() void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = null;
+ last_userdata = null;
+ }
+
+ fn writePty(_: Terminal, ud: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
+ last_userdata = ud;
+ }
+ };
+ defer S.deinit();
+
+ // Set userdata and write_pty callback
+ var sentinel: u8 = 42;
+ try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&sentinel)));
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+
+ // DECRQM for wraparound mode (mode 7, set by default) should trigger write_pty
+ vt_write(t, "\x1B[?7$p", 6);
+ try testing.expect(S.last_data != null);
+ try testing.expectEqualStrings("\x1B[?7;1$y", S.last_data.?);
+ try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
+}
+
+test "set write_pty without callback ignores queries" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ // Without setting a callback, DECRQM should be silently ignored (no crash)
+ vt_write(t, "\x1B[?7$p", 6);
+}
+
+test "set write_pty null clears callback" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var called: bool = false;
+ fn writePty(_: Terminal, _: ?*anyopaque, _: [*]const u8, _: usize) callconv(lib.calling_conv) void {
+ called = true;
+ }
+ };
+ S.called = false;
+
+ // Set then clear the callback
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+ try testing.expectEqual(Result.success, set(t, .write_pty, null));
+
+ vt_write(t, "\x1B[?7$p", 6);
+ try testing.expect(!S.called);
+}
+
+test "set bell callback" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var bell_count: usize = 0;
+ var last_userdata: ?*anyopaque = null;
+
+ fn bell(_: Terminal, ud: ?*anyopaque) callconv(lib.calling_conv) void {
+ bell_count += 1;
+ last_userdata = ud;
+ }
+ };
+ S.bell_count = 0;
+ S.last_userdata = null;
+
+ // Set userdata and bell callback
+ var sentinel: u8 = 99;
+ try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&sentinel)));
+ try testing.expectEqual(Result.success, set(t, .bell, @ptrCast(&S.bell)));
+
+ // Single BEL
+ vt_write(t, "\x07", 1);
+ try testing.expectEqual(@as(usize, 1), S.bell_count);
+ try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
+
+ // Multiple BELs
+ vt_write(t, "\x07\x07", 2);
+ try testing.expectEqual(@as(usize, 3), S.bell_count);
+}
+
+test "bell without callback is silent" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ // BEL without a callback should not crash
+ vt_write(t, "\x07", 1);
+}
+
+test "set enquiry callback" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var last_data: ?[]u8 = null;
+
+ fn deinit() void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = null;
+ }
+
+ fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
+ }
+
+ const response = "OK";
+ fn enquiry(_: Terminal, _: ?*anyopaque) callconv(lib.calling_conv) lib.String {
+ return .{ .ptr = response, .len = response.len };
+ }
+ };
+ defer S.deinit();
+
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+ try testing.expectEqual(Result.success, set(t, .enquiry, @ptrCast(&S.enquiry)));
+
+ // ENQ (0x05) should trigger the enquiry callback and write response via write_pty
+ vt_write(t, "\x05", 1);
+ try testing.expect(S.last_data != null);
+ try testing.expectEqualStrings("OK", S.last_data.?);
+}
+
+test "enquiry without callback is silent" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ // ENQ without a callback should not crash
+ vt_write(t, "\x05", 1);
+}
+
+test "set xtversion callback" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var last_data: ?[]u8 = null;
+
+ fn deinit() void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = null;
+ }
+
+ fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
+ }
+
+ const version = "myterm 1.0";
+ fn xtversion(_: Terminal, _: ?*anyopaque) callconv(lib.calling_conv) lib.String {
+ return .{ .ptr = version, .len = version.len };
+ }
+ };
+ defer S.deinit();
+
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+ try testing.expectEqual(Result.success, set(t, .xtversion, @ptrCast(&S.xtversion)));
+
+ // XTVERSION: CSI > q
+ vt_write(t, "\x1B[>q", 4);
+ try testing.expect(S.last_data != null);
+ // Response should be DCS >| version ST
+ try testing.expectEqualStrings("\x1BP>|myterm 1.0\x1B\\", S.last_data.?);
+}
+
+test "xtversion without callback reports default" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var last_data: ?[]u8 = null;
+
+ fn deinit() void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = null;
+ }
+
+ fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
+ }
+ };
+ defer S.deinit();
+
+ // Set write_pty but not xtversion — should get default "libghostty"
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+
+ vt_write(t, "\x1B[>q", 4);
+ try testing.expect(S.last_data != null);
+ try testing.expectEqualStrings("\x1BP>|libghostty\x1B\\", S.last_data.?);
+}
+
+test "set title_changed callback" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var title_count: usize = 0;
+ var last_userdata: ?*anyopaque = null;
+
+ fn titleChanged(_: Terminal, ud: ?*anyopaque) callconv(lib.calling_conv) void {
+ title_count += 1;
+ last_userdata = ud;
+ }
+ };
+ S.title_count = 0;
+ S.last_userdata = null;
+
+ var sentinel: u8 = 77;
+ try testing.expectEqual(Result.success, set(t, .userdata, @ptrCast(&sentinel)));
+ try testing.expectEqual(Result.success, set(t, .title_changed, @ptrCast(&S.titleChanged)));
+
+ // OSC 2 ; title ST — set window title
+ vt_write(t, "\x1B]2;Hello\x1B\\", 10);
+ try testing.expectEqual(@as(usize, 1), S.title_count);
+ try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
+
+ // Another title change
+ vt_write(t, "\x1B]2;World\x1B\\", 10);
+ try testing.expectEqual(@as(usize, 2), S.title_count);
+}
+
+test "title_changed without callback is silent" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ // OSC 2 without a callback should not crash
+ vt_write(t, "\x1B]2;Hello\x1B\\", 10);
+}
+
+test "set size callback" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var last_data: ?[]u8 = null;
+
+ fn deinit() void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = null;
+ }
+
+ fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
+ }
+
+ fn sizeCb(_: Terminal, _: ?*anyopaque, out_size: *size_report.Size) callconv(lib.calling_conv) bool {
+ out_size.* = .{
+ .rows = 24,
+ .columns = 80,
+ .cell_width = 8,
+ .cell_height = 16,
+ };
+ return true;
+ }
+ };
+ defer S.deinit();
+
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+ try testing.expectEqual(Result.success, set(t, .size_cb, @ptrCast(&S.sizeCb)));
+
+ // CSI 18 t — report text area size in characters
+ vt_write(t, "\x1B[18t", 5);
+ try testing.expect(S.last_data != null);
+ try testing.expectEqualStrings("\x1b[8;24;80t", S.last_data.?);
+}
+
+test "size without callback is silent" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ // CSI 18 t without a size callback should not crash
+ vt_write(t, "\x1B[18t", 5);
+}
+
+test "set device_attributes callback primary" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var last_data: ?[]u8 = null;
+
+ fn deinit() void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = null;
+ }
+
+ fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
+ }
+
+ fn da(_: Terminal, _: ?*anyopaque, out: *Effects.CDeviceAttributes) callconv(lib.calling_conv) bool {
+ out.* = .{
+ .primary = .{
+ .conformance_level = 64,
+ .features = .{ 22, 52 } ++ .{0} ** 62,
+ .num_features = 2,
+ },
+ .secondary = .{
+ .device_type = 1,
+ .firmware_version = 10,
+ .rom_cartridge = 0,
+ },
+ .tertiary = .{ .unit_id = 0 },
+ };
+ return true;
+ }
+ };
+ defer S.deinit();
+
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+ try testing.expectEqual(Result.success, set(t, .device_attributes, @ptrCast(&S.da)));
+
+ // CSI c — primary DA
+ vt_write(t, "\x1B[c", 3);
+ try testing.expect(S.last_data != null);
+ try testing.expectEqualStrings("\x1b[?64;22;52c", S.last_data.?);
+}
+
+test "set device_attributes callback secondary" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var last_data: ?[]u8 = null;
+
+ fn deinit() void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = null;
+ }
+
+ fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
+ }
+
+ fn da(_: Terminal, _: ?*anyopaque, out: *Effects.CDeviceAttributes) callconv(lib.calling_conv) bool {
+ out.* = .{
+ .primary = .{
+ .conformance_level = 62,
+ .features = .{22} ++ .{0} ** 63,
+ .num_features = 1,
+ },
+ .secondary = .{
+ .device_type = 1,
+ .firmware_version = 10,
+ .rom_cartridge = 0,
+ },
+ .tertiary = .{ .unit_id = 0 },
+ };
+ return true;
+ }
+ };
+ defer S.deinit();
+
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+ try testing.expectEqual(Result.success, set(t, .device_attributes, @ptrCast(&S.da)));
+
+ // CSI > c — secondary DA
+ vt_write(t, "\x1B[>c", 4);
+ try testing.expect(S.last_data != null);
+ try testing.expectEqualStrings("\x1b[>1;10;0c", S.last_data.?);
+}
+
+test "set device_attributes callback tertiary" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var last_data: ?[]u8 = null;
+
+ fn deinit() void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = null;
+ }
+
+ fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
+ }
+
+ fn da(_: Terminal, _: ?*anyopaque, out: *Effects.CDeviceAttributes) callconv(lib.calling_conv) bool {
+ out.* = .{
+ .primary = .{
+ .conformance_level = 62,
+ .features = .{0} ** 64,
+ .num_features = 0,
+ },
+ .secondary = .{
+ .device_type = 1,
+ .firmware_version = 0,
+ .rom_cartridge = 0,
+ },
+ .tertiary = .{ .unit_id = 0xAABBCCDD },
+ };
+ return true;
+ }
+ };
+ defer S.deinit();
+
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+ try testing.expectEqual(Result.success, set(t, .device_attributes, @ptrCast(&S.da)));
+
+ // CSI = c — tertiary DA
+ vt_write(t, "\x1B[=c", 4);
+ try testing.expect(S.last_data != null);
+ try testing.expectEqualStrings("\x1bP!|AABBCCDD\x1b\\", S.last_data.?);
+}
+
+test "device_attributes without callback uses default" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var last_data: ?[]u8 = null;
+
+ fn deinit() void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = null;
+ }
+
+ fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
+ }
+ };
+ defer S.deinit();
+
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+
+ // Without setting a device_attributes callback, DA1 should return the default
+ vt_write(t, "\x1B[c", 3);
+ try testing.expect(S.last_data != null);
+ try testing.expectEqualStrings("\x1b[?62;22c", S.last_data.?);
+}
+
+test "device_attributes callback returns false uses default" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var last_data: ?[]u8 = null;
+
+ fn deinit() void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = null;
+ }
+
+ fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
+ }
+
+ fn da(_: Terminal, _: ?*anyopaque, _: *Effects.CDeviceAttributes) callconv(lib.calling_conv) bool {
+ return false;
+ }
+ };
+ defer S.deinit();
+
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+ try testing.expectEqual(Result.success, set(t, .device_attributes, @ptrCast(&S.da)));
+
+ // Callback returns false, should use default response
+ vt_write(t, "\x1B[c", 3);
+ try testing.expect(S.last_data != null);
+ try testing.expectEqualStrings("\x1b[?62;22c", S.last_data.?);
+}
+
+test "set and get title" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ // No title set yet — should return empty string
+ var title: lib.String = undefined;
+ try testing.expectEqual(Result.success, get(t, .title, @ptrCast(&title)));
+ try testing.expectEqual(@as(usize, 0), title.len);
+
+ // Set title via option
+ const hello: lib.String = .{ .ptr = "Hello", .len = 5 };
+ try testing.expectEqual(Result.success, set(t, .title, @ptrCast(&hello)));
+
+ try testing.expectEqual(Result.success, get(t, .title, @ptrCast(&title)));
+ try testing.expectEqualStrings("Hello", title.ptr[0..title.len]);
+
+ // Overwrite title
+ const world: lib.String = .{ .ptr = "World", .len = 5 };
+ try testing.expectEqual(Result.success, set(t, .title, @ptrCast(&world)));
+
+ try testing.expectEqual(Result.success, get(t, .title, @ptrCast(&title)));
+ try testing.expectEqualStrings("World", title.ptr[0..title.len]);
+
+ // Clear title with NULL
+ try testing.expectEqual(Result.success, set(t, .title, null));
+
+ try testing.expectEqual(Result.success, get(t, .title, @ptrCast(&title)));
+ try testing.expectEqual(@as(usize, 0), title.len);
+}
+
+test "set and get pwd" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ // No pwd set yet — should return empty string
+ var pwd: lib.String = undefined;
+ try testing.expectEqual(Result.success, get(t, .pwd, @ptrCast(&pwd)));
+ try testing.expectEqual(@as(usize, 0), pwd.len);
+
+ // Set pwd via option
+ const home: lib.String = .{ .ptr = "/home/user", .len = 10 };
+ try testing.expectEqual(Result.success, set(t, .pwd, @ptrCast(&home)));
+
+ try testing.expectEqual(Result.success, get(t, .pwd, @ptrCast(&pwd)));
+ try testing.expectEqualStrings("/home/user", pwd.ptr[0..pwd.len]);
+
+ // Clear pwd with NULL
+ try testing.expectEqual(Result.success, set(t, .pwd, null));
+
+ try testing.expectEqual(Result.success, get(t, .pwd, @ptrCast(&pwd)));
+ try testing.expectEqual(@as(usize, 0), pwd.len);
+}
+
+test "get title set via vt_write" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ // Set title via OSC 2
+ vt_write(t, "\x1B]2;VT Title\x1B\\", 14);
+
+ var title: lib.String = undefined;
+ try testing.expectEqual(Result.success, get(t, .title, @ptrCast(&title)));
+ try testing.expectEqualStrings("VT Title", title.ptr[0..title.len]);
+}
+
+test "resize updates pixel dimensions" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
+
+ const zt = t.?.terminal;
+ try testing.expectEqual(@as(u32, 100 * 9), zt.width_px);
+ try testing.expectEqual(@as(u32, 40 * 18), zt.height_px);
+}
+
+test "resize pixel overflow saturates" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ try testing.expectEqual(Result.success, resize(t, 100, 40, std.math.maxInt(u32), std.math.maxInt(u32)));
+
+ const zt = t.?.terminal;
+ try testing.expectEqual(std.math.maxInt(u32), zt.width_px);
+ try testing.expectEqual(std.math.maxInt(u32), zt.height_px);
+}
+
+test "resize disables synchronized output" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const zt = t.?.terminal;
+ zt.modes.set(.synchronized_output, true);
+
+ try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
+ try testing.expect(!zt.modes.get(.synchronized_output));
+}
+
+test "resize sends in-band size report" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var last_data: ?[]u8 = null;
+
+ fn deinit() void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = null;
+ }
+
+ fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
+ if (last_data) |d| testing.allocator.free(d);
+ last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
+ }
+ };
+ defer S.deinit();
+
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+
+ // Enable in-band size reports (mode 2048)
+ t.?.terminal.modes.set(.in_band_size_reports, true);
+
+ try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
+
+ // Expected: \x1B[48;rows;cols;height_px;width_pxt
+ // height_px = 40*18 = 720, width_px = 100*9 = 900
+ try testing.expect(S.last_data != null);
+ try testing.expectEqualStrings("\x1B[48;40;100;720;900t", S.last_data.?);
+}
+
+test "resize no size report without mode 2048" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const S = struct {
+ var called: bool = false;
+ fn writePty(_: Terminal, _: ?*anyopaque, _: [*]const u8, _: usize) callconv(lib.calling_conv) void {
+ called = true;
+ }
+ };
+ S.called = false;
+
+ try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
+
+ // in_band_size_reports is off by default
+ try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
+ try testing.expect(!S.called);
+}
+
+test "resize in-band report without write_pty callback" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ // Enable mode 2048 but don't set a write_pty callback — should not crash
+ t.?.terminal.modes.set(.in_band_size_reports, true);
+ try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
+}
+
+test "resize null terminal" {
+ try testing.expectEqual(Result.invalid_value, resize(null, 100, 40, 9, 18));
+}
+
+test "resize zero cols" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ try testing.expectEqual(Result.invalid_value, resize(t, 0, 40, 9, 18));
+}
+
+test "resize zero rows" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ try testing.expectEqual(Result.invalid_value, resize(t, 100, 0, 9, 18));
+}
+
+test "grid_ref out of bounds" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ var out_ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.invalid_value, grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 100, .y = 0 } },
+ }, &out_ref));
+}
+
+test "set and get color_foreground" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ // Initially unset
+ var rgb: color.RGB.C = undefined;
+ try testing.expectEqual(Result.no_value, get(t, .color_foreground, @ptrCast(&rgb)));
+
+ // Set a value
+ const fg: color.RGB.C = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC };
+ try testing.expectEqual(Result.success, set(t, .color_foreground, @ptrCast(&fg)));
+ try testing.expectEqual(Result.success, get(t, .color_foreground, @ptrCast(&rgb)));
+ try testing.expectEqual(fg, rgb);
+
+ // Clear with null
+ try testing.expectEqual(Result.success, set(t, .color_foreground, null));
+ try testing.expectEqual(Result.no_value, get(t, .color_foreground, @ptrCast(&rgb)));
+}
+
+test "set and get color_background" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ var rgb: color.RGB.C = undefined;
+ try testing.expectEqual(Result.no_value, get(t, .color_background, @ptrCast(&rgb)));
+
+ const bg: color.RGB.C = .{ .r = 0x11, .g = 0x22, .b = 0x33 };
+ try testing.expectEqual(Result.success, set(t, .color_background, @ptrCast(&bg)));
+ try testing.expectEqual(Result.success, get(t, .color_background, @ptrCast(&rgb)));
+ try testing.expectEqual(bg, rgb);
+
+ try testing.expectEqual(Result.success, set(t, .color_background, null));
+ try testing.expectEqual(Result.no_value, get(t, .color_background, @ptrCast(&rgb)));
+}
+
+test "set and get color_cursor" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ var rgb: color.RGB.C = undefined;
+ try testing.expectEqual(Result.no_value, get(t, .color_cursor, @ptrCast(&rgb)));
+
+ const cur: color.RGB.C = .{ .r = 0xFF, .g = 0x00, .b = 0x88 };
+ try testing.expectEqual(Result.success, set(t, .color_cursor, @ptrCast(&cur)));
+ try testing.expectEqual(Result.success, get(t, .color_cursor, @ptrCast(&rgb)));
+ try testing.expectEqual(cur, rgb);
+
+ try testing.expectEqual(Result.success, set(t, .color_cursor, null));
+ try testing.expectEqual(Result.no_value, get(t, .color_cursor, @ptrCast(&rgb)));
+}
+
+test "set and get color_palette" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ // Get default palette
+ var palette: color.PaletteC = undefined;
+ try testing.expectEqual(Result.success, get(t, .color_palette, @ptrCast(&palette)));
+ try testing.expectEqual(color.default[0].cval(), palette[0]);
+
+ // Set custom palette
+ var custom: color.PaletteC = color.paletteCval(&color.default);
+ custom[0] = .{ .r = 0x12, .g = 0x34, .b = 0x56 };
+ try testing.expectEqual(Result.success, set(t, .color_palette, @ptrCast(&custom)));
+ try testing.expectEqual(Result.success, get(t, .color_palette, @ptrCast(&palette)));
+ try testing.expectEqual(custom[0], palette[0]);
+
+ // Reset with null restores default
+ try testing.expectEqual(Result.success, set(t, .color_palette, null));
+ try testing.expectEqual(Result.success, get(t, .color_palette, @ptrCast(&palette)));
+ try testing.expectEqual(color.default[0].cval(), palette[0]);
+}
+
+test "get color default vs effective with override" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const zt = t.?.terminal;
+ var rgb: color.RGB.C = undefined;
+
+ // Set defaults
+ const fg: color.RGB.C = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC };
+ const bg: color.RGB.C = .{ .r = 0x11, .g = 0x22, .b = 0x33 };
+ const cur: color.RGB.C = .{ .r = 0xFF, .g = 0x00, .b = 0x88 };
+ try testing.expectEqual(Result.success, set(t, .color_foreground, @ptrCast(&fg)));
+ try testing.expectEqual(Result.success, set(t, .color_background, @ptrCast(&bg)));
+ try testing.expectEqual(Result.success, set(t, .color_cursor, @ptrCast(&cur)));
+
+ // Simulate OSC overrides
+ const override: color.RGB = .{ .r = 0x00, .g = 0x00, .b = 0x00 };
+ zt.colors.foreground.override = override;
+ zt.colors.background.override = override;
+ zt.colors.cursor.override = override;
+
+ // Effective returns override
+ try testing.expectEqual(Result.success, get(t, .color_foreground, @ptrCast(&rgb)));
+ try testing.expectEqual(override.cval(), rgb);
+ try testing.expectEqual(Result.success, get(t, .color_background, @ptrCast(&rgb)));
+ try testing.expectEqual(override.cval(), rgb);
+ try testing.expectEqual(Result.success, get(t, .color_cursor, @ptrCast(&rgb)));
+ try testing.expectEqual(override.cval(), rgb);
+
+ // Default returns original
+ try testing.expectEqual(Result.success, get(t, .color_foreground_default, @ptrCast(&rgb)));
+ try testing.expectEqual(fg, rgb);
+ try testing.expectEqual(Result.success, get(t, .color_background_default, @ptrCast(&rgb)));
+ try testing.expectEqual(bg, rgb);
+ try testing.expectEqual(Result.success, get(t, .color_cursor_default, @ptrCast(&rgb)));
+ try testing.expectEqual(cur, rgb);
+}
+
+test "get color default returns no_value when unset" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ var rgb: color.RGB.C = undefined;
+ try testing.expectEqual(Result.no_value, get(t, .color_foreground_default, @ptrCast(&rgb)));
+ try testing.expectEqual(Result.no_value, get(t, .color_background_default, @ptrCast(&rgb)));
+ try testing.expectEqual(Result.no_value, get(t, .color_cursor_default, @ptrCast(&rgb)));
+}
+
+test "get color_palette_default vs current" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const zt = t.?.terminal;
+
+ // Set a custom default palette
+ var custom: color.PaletteC = color.paletteCval(&color.default);
+ custom[0] = .{ .r = 0x12, .g = 0x34, .b = 0x56 };
+ try testing.expectEqual(Result.success, set(t, .color_palette, @ptrCast(&custom)));
+
+ // Simulate OSC override on index 0
+ zt.colors.palette.set(0, .{ .r = 0xFF, .g = 0xFF, .b = 0xFF });
+
+ // Current palette returns the override
+ var palette: color.PaletteC = undefined;
+ try testing.expectEqual(Result.success, get(t, .color_palette, @ptrCast(&palette)));
+ try testing.expectEqual(color.RGB.C{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, palette[0]);
+
+ // Default palette returns the original
+ try testing.expectEqual(Result.success, get(t, .color_palette_default, @ptrCast(&palette)));
+ try testing.expectEqual(custom[0], palette[0]);
+}
+
+test "set color sets dirty flag" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ const zt = t.?.terminal;
+ zt.flags.dirty.palette = false;
+
+ const fg: color.RGB.C = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF };
+ try testing.expectEqual(Result.success, set(t, .color_foreground, @ptrCast(&fg)));
+ try testing.expect(zt.flags.dirty.palette);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/types.zig b/vendor/libghostty-vt/src/terminal/c/types.zig
new file mode 100644
index 00000000..b808bf38
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/types.zig
@@ -0,0 +1,215 @@
+//! Comptime-generated metadata describing the layout of all C API
+//! extern structs for the current target.
+//!
+//! This is embedded in the binary as a const string and exposed via
+//! `ghostty_type_json` so that WASM (and other FFI) consumers can
+//! build structs without hardcoding byte offsets.
+const std = @import("std");
+const lib = @import("../lib.zig");
+const color = @import("../color.zig");
+const mouse_event = @import("mouse_event.zig");
+const point = @import("../point.zig");
+const size_report = @import("size_report.zig");
+
+const terminal = @import("terminal.zig");
+const formatter = @import("formatter.zig");
+const render = @import("render.zig");
+const style_c = @import("style.zig");
+const mouse_encode = @import("mouse_encode.zig");
+const grid_ref = @import("grid_ref.zig");
+
+/// All C API structs and their Ghostty C names.
+pub const structs: std.StaticStringMap(StructInfo) = .initComptime(.{
+ .{ "GhosttyColorRgb", StructInfo.init(color.RGB.C) },
+ .{ "GhosttyDeviceAttributes", StructInfo.init(terminal.DeviceAttributes) },
+ .{ "GhosttyDeviceAttributesPrimary", StructInfo.init(terminal.DeviceAttributes.Primary) },
+ .{ "GhosttyDeviceAttributesSecondary", StructInfo.init(terminal.DeviceAttributes.Secondary) },
+ .{ "GhosttyDeviceAttributesTertiary", StructInfo.init(terminal.DeviceAttributes.Tertiary) },
+ .{ "GhosttyFormatterTerminalOptions", StructInfo.init(formatter.TerminalOptions) },
+ .{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
+ .{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },
+ .{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) },
+ .{ "GhosttyMouseEncoderSize", StructInfo.init(mouse_encode.Size) },
+ .{ "GhosttyMousePosition", StructInfo.init(mouse_event.Position) },
+ .{ "GhosttyPoint", StructInfo.init(point.Point.C) },
+ .{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) },
+ .{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) },
+ .{ "GhosttySizeReportSize", StructInfo.init(size_report.Size) },
+ .{ "GhosttyString", StructInfo.init(lib.String) },
+ .{ "GhosttyStyle", StructInfo.init(style_c.Style) },
+ .{ "GhosttyStyleColor", StructInfo.init(style_c.Color) },
+ .{ "GhosttyTerminalOptions", StructInfo.init(terminal.Options) },
+ .{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) },
+ .{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) },
+});
+
+/// The comptime-generated JSON string of all structs.
+pub const json: [:0]const u8 = json: {
+ @setEvalBranchQuota(100000);
+ var counter: std.Io.Writer.Discarding = .init(&.{});
+ jsonWriteAll(&counter.writer) catch unreachable;
+
+ var buf: [counter.count:0]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ jsonWriteAll(&writer) catch unreachable;
+ const final = buf;
+ break :json final[0..writer.end :0];
+};
+
+/// Returns a pointer to the comptime-generated JSON string describing
+/// the layout of all C API extern structs, and writes its length to `len`.
+/// Exported as `ghostty_type_json` for FFI consumers.
+pub fn get_json() callconv(lib.calling_conv) [*:0]const u8 {
+ return json.ptr;
+}
+
+/// Meta information about a struct that we expose to ease writing
+/// bindings in some languages, particularly WASM where we can't
+/// easily share struct definitions and need to hardcode byte offsets.
+pub const StructInfo = struct {
+ name: []const u8,
+ size: usize,
+ @"align": usize,
+ fields: []const FieldInfo,
+
+ pub const FieldInfo = struct {
+ name: []const u8,
+ offset: usize,
+ size: usize,
+ type: []const u8,
+ };
+
+ pub fn init(comptime T: type) StructInfo {
+ comptime {
+ const fields = @typeInfo(T).@"struct".fields;
+ const field_infos: [fields.len]FieldInfo = blk: {
+ var infos: [fields.len]FieldInfo = undefined;
+ for (fields, 0..) |field, i| infos[i] = .{
+ .name = field.name,
+ .offset = @offsetOf(T, field.name),
+ .size = @sizeOf(field.type),
+ .type = typeName(field.type),
+ };
+ break :blk infos;
+ };
+
+ return .{
+ .name = @typeName(T),
+ .size = @sizeOf(T),
+ .@"align" = @alignOf(T),
+ .fields = &field_infos,
+ };
+ }
+ }
+
+ pub fn jsonStringify(
+ self: *const StructInfo,
+ jws: anytype,
+ ) std.Io.Writer.Error!void {
+ try jws.beginObject();
+ try jws.objectField("size");
+ try jws.write(self.size);
+ try jws.objectField("align");
+ try jws.write(self.@"align");
+ try jws.objectField("fields");
+ try jws.beginObject();
+ for (self.fields) |field| {
+ try jws.objectField(field.name);
+ try jws.beginObject();
+ try jws.objectField("offset");
+ try jws.write(field.offset);
+ try jws.objectField("size");
+ try jws.write(field.size);
+ try jws.objectField("type");
+ try jws.write(field.type);
+ try jws.endObject();
+ }
+ try jws.endObject();
+ try jws.endObject();
+ }
+};
+
+fn jsonWriteAll(writer: *std.Io.Writer) std.Io.Writer.Error!void {
+ var jws: std.json.Stringify = .{ .writer = writer };
+ try jws.beginObject();
+ for (structs.keys(), structs.values()) |name, *info| {
+ try jws.objectField(name);
+ try info.jsonStringify(&jws);
+ }
+ try jws.endObject();
+}
+
+fn typeName(comptime T: type) []const u8 {
+ return switch (@typeInfo(T)) {
+ .bool => "bool",
+ .int => |info| switch (info.signedness) {
+ .signed => switch (info.bits) {
+ 8 => "i8",
+ 16 => "i16",
+ 32 => "i32",
+ 64 => "i64",
+ else => @compileError("unsupported signed int size"),
+ },
+ .unsigned => switch (info.bits) {
+ 8 => "u8",
+ 16 => "u16",
+ 32 => "u32",
+ 64 => "u64",
+ else => @compileError("unsupported unsigned int size"),
+ },
+ },
+ .@"enum" => "enum",
+ .@"struct" => "struct",
+ .pointer => "pointer",
+ .array => "array",
+ else => "opaque",
+ };
+}
+
+test "json parses" {
+ const parsed = try std.json.parseFromSlice(
+ std.json.Value,
+ std.testing.allocator,
+ json,
+ .{},
+ );
+ defer parsed.deinit();
+
+ const root = parsed.value.object;
+
+ // Verify we have all expected structs
+ try std.testing.expect(root.contains("GhosttyTerminalOptions"));
+ try std.testing.expect(root.contains("GhosttyFormatterTerminalOptions"));
+
+ // Verify GhosttyTerminalOptions fields
+ const term_opts = root.get("GhosttyTerminalOptions").?.object;
+ try std.testing.expect(term_opts.contains("size"));
+ try std.testing.expect(term_opts.contains("align"));
+ try std.testing.expect(term_opts.contains("fields"));
+
+ const fields = term_opts.get("fields").?.object;
+ try std.testing.expect(fields.contains("cols"));
+ try std.testing.expect(fields.contains("rows"));
+ try std.testing.expect(fields.contains("max_scrollback"));
+
+ // Verify field offsets make sense (cols should be at 0)
+ const cols = fields.get("cols").?.object;
+ try std.testing.expectEqual(0, cols.get("offset").?.integer);
+}
+
+test "struct sizes are non-zero" {
+ const parsed = try std.json.parseFromSlice(
+ std.json.Value,
+ std.testing.allocator,
+ json,
+ .{},
+ );
+ defer parsed.deinit();
+
+ var it = parsed.value.object.iterator();
+ while (it.next()) |entry| {
+ const struct_info = entry.value_ptr.object;
+ const size = struct_info.get("size").?.integer;
+ try std.testing.expect(size > 0);
+ }
+}