From 9673a22b01317ac6493240694a2451c8287253d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 15 Aug 2026 20:06:51 -0700 Subject: [PATCH] libghostty: expand ABI type metadata The type metadata export only described extern struct layouts, leaving embedders to mirror enum values and tagged union relationships. Describe every public C type in a versioned manifest with target and build metadata. Keep union field renames alongside their source tagged unions so the manifest uses public C names without changing Zig value layouts. --- example/wasm-vt/index.html | 28 +- include/ghostty/vt/osc.h | 3 + include/ghostty/vt/types.h | 47 +- src/lib/union.zig | 94 ++- src/terminal/Terminal.zig | 2 +- src/terminal/c/osc.zig | 5 +- src/terminal/c/terminal.zig | 2 +- src/terminal/c/types.zig | 1066 ++++++++++++++++++++++++++--------- src/terminal/point.zig | 10 +- src/terminal/sgr.zig | 13 +- src/terminal/stream.zig | 2 +- 11 files changed, 945 insertions(+), 327 deletions(-) diff --git a/example/wasm-vt/index.html b/example/wasm-vt/index.html index 81a033737..76c9b9bbd 100644 --- a/example/wasm-vt/index.html +++ b/example/wasm-vt/index.html @@ -156,7 +156,7 @@ // Look up a field's offset and DataView setter from the type layout JSON. function fieldInfo(structName, fieldName) { - const field = typeLayout[structName].fields[fieldName]; + const field = typeLayout.types[structName].fields[fieldName]; return field; } @@ -166,9 +166,16 @@ switch (field.type) { case 'u8': case 'bool': view.setUint8(field.offset, value); break; case 'u16': view.setUint16(field.offset, value, true); break; - case 'u32': case 'enum': view.setUint32(field.offset, value, true); break; + case 'u32': view.setUint32(field.offset, value, true); break; case 'u64': view.setBigUint64(field.offset, BigInt(value), true); break; - default: throw new Error(`Unsupported field type: ${field.type}`); + default: { + const type = typeLayout.types[field.type]; + if (type?.kind === 'enum' && type.underlying === 'i32') { + view.setInt32(field.offset, value, true); + break; + } + throw new Error(`Unsupported field type: ${field.type}`); + } } } @@ -186,15 +193,14 @@ .replace(/\\\\/g, '\\'); } - // GHOSTTY_FORMATTER_FORMAT_PLAIN = 0 - const GHOSTTY_FORMATTER_FORMAT_PLAIN = 0; - // GHOSTTY_SUCCESS = 0 - const GHOSTTY_SUCCESS = 0; - function run() { const outputDiv = document.getElementById('output'); try { + const GHOSTTY_FORMATTER_FORMAT_PLAIN = + typeLayout.types.GhosttyFormatterFormat.values.PLAIN; + const GHOSTTY_SUCCESS = + typeLayout.types.GhosttyResult.values.SUCCESS; const cols = parseInt(document.getElementById('cols').value, 10); const rows = parseInt(document.getElementById('rows').value, 10); const vtText = parseEscapes(document.getElementById('vtInput').value); @@ -224,7 +230,7 @@ wasmInstance.exports.ghostty_wasm_free_u8_array(dataPtr, vtBytes.length); // Create a plain-text formatter - const FMT_OPTS_SIZE = typeLayout['GhosttyFormatterTerminalOptions'].size; + const FMT_OPTS_SIZE = typeLayout.types.GhosttyFormatterTerminalOptions.size; const fmtOptsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(FMT_OPTS_SIZE); new Uint8Array(getBuffer(), fmtOptsPtr, FMT_OPTS_SIZE).fill(0); const fmtOptsView = new DataView(getBuffer(), fmtOptsPtr, FMT_OPTS_SIZE); @@ -235,12 +241,12 @@ // Set the nested sized-struct `size` fields for extra and extra.screen const extraOffset = fieldInfo('GhosttyFormatterTerminalOptions', 'extra').offset; - const extraSize = typeLayout['GhosttyFormatterTerminalExtra'].size; + const extraSize = typeLayout.types.GhosttyFormatterTerminalExtra.size; const extraSizeField = fieldInfo('GhosttyFormatterTerminalExtra', 'size'); fmtOptsView.setUint32(extraOffset + extraSizeField.offset, extraSize, true); const screenOffset = fieldInfo('GhosttyFormatterTerminalExtra', 'screen').offset; - const screenSize = typeLayout['GhosttyFormatterScreenExtra'].size; + const screenSize = typeLayout.types.GhosttyFormatterScreenExtra.size; const screenSizeField = fieldInfo('GhosttyFormatterScreenExtra', 'size'); fmtOptsView.setUint32(extraOffset + screenOffset + screenSizeField.offset, screenSize, true); diff --git a/include/ghostty/vt/osc.h b/include/ghostty/vt/osc.h index 9409ebc73..f7626383c 100644 --- a/include/ghostty/vt/osc.h +++ b/include/ghostty/vt/osc.h @@ -63,6 +63,9 @@ typedef enum GHOSTTY_ENUM_TYPED { GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20, GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21, GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22, + GHOSTTY_OSC_COMMAND_KITTY_CLIPBOARD_PROTOCOL = 23, + GHOSTTY_OSC_COMMAND_KITTY_DND_PROTOCOL = 24, + GHOSTTY_OSC_COMMAND_CONTEXT_SIGNAL = 25, GHOSTTY_OSC_COMMAND_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyOscCommandType; diff --git a/include/ghostty/vt/types.h b/include/ghostty/vt/types.h index c66ee89d2..bc1c6a69b 100644 --- a/include/ghostty/vt/types.h +++ b/include/ghostty/vt/types.h @@ -332,29 +332,40 @@ typedef struct { #endif /** - * Return a pointer to a null-terminated JSON string describing the - * layout of every C API struct for the current target. + * Return the versioned libghostty-vt C type manifest for the current target. * - * This is primarily useful for language bindings that can't easily - * set C struct fields and need to do so via byte offsets. For example, - * WebAssembly modules can't share struct definitions with the host. + * The manifest defines all the public types available in the linked + * build. The types contain their layouts, enum values, union fields, and more. + * + * Language bindings, such as WebAssembly hosts, should obtain offsets, + * sizes, alignments, array shapes, enum constants, and tagged-union arms from + * this manifest rather than hardcoding them. Consumers should reject unknown + * schema versions and verify the descriptors they require at initialization. * * Example (abbreviated): * @code{.json} * { - * "GhosttyMouseEncoderSize": { - * "size": 40, - * "align": 8, - * "fields": { - * "size": { "offset": 0, "size": 8, "type": "u64" }, - * "screen_width": { "offset": 8, "size": 4, "type": "u32" }, - * "screen_height": { "offset": 12, "size": 4, "type": "u32" }, - * "cell_width": { "offset": 16, "size": 4, "type": "u32" }, - * "cell_height": { "offset": 20, "size": 4, "type": "u32" }, - * "padding_top": { "offset": 24, "size": 4, "type": "u32" }, - * "padding_bottom": { "offset": 28, "size": 4, "type": "u32" }, - * "padding_right": { "offset": 32, "size": 4, "type": "u32" }, - * "padding_left": { "offset": 36, "size": 4, "type": "u32" } + * "schema": 1, + * "abi": { + * "target": "wasm32", "os": "freestanding", "environment": "none", + * "pointer_size": 4, "usize_size": 4, "endian": "little" + * }, + * "types": { + * "GhosttyRenderStateData": { + * "kind": "enum", "size": 4, "align": 4, + * "underlying": "i32", "prefix": "GHOSTTY_RENDER_STATE_DATA_", + * "values": { "INVALID": 0, "DIRTY": 3, "MAX_VALUE": 2147483647 } + * }, + * "GhosttyStyleColor": { + * "kind": "struct", "size": 16, "align": 8, + * "fields": { + * "tag": { "offset": 0, "size": 4, + * "type": "GhosttyStyleColorTag" }, + * "value": { "offset": 8, "size": 8, + * "type": "GhosttyStyleColorValue", "tag": "tag", + * "arms": { "NONE": null, "PALETTE": "palette", + * "RGB": "rgb" } } + * } * } * } * } diff --git a/src/lib/union.zig b/src/lib/union.zig index 63a34a8cb..728b13304 100644 --- a/src/lib/union.zig +++ b/src/lib/union.zig @@ -15,14 +15,18 @@ const Target = @import("target.zig").Target; /// be an enum created with the `Enum` function in this library, so that /// automatic C ABI compatibility is ensured. /// -/// The `Padding` type is a type that is always added to the C union -/// with the key `_padding`. This should be set to a type that has the size -/// and alignment needed to pad the C union to the expected size. This -/// should never change to ensure ABI compatibility. +/// `options.padding` is a type that is always added to the C union with the key +/// `_padding`. It should have the size and alignment needed to pad the C union +/// to the expected size and should never change to ensure ABI compatibility. +/// +/// Each tag has an optional field in `options.field_renames` that may rename its +/// C value field. Multiple tags may map to the same field when their C value +/// types are identical. Tags with no payload are omitted. This is used +/// for metadata and should match the C headers directly. pub fn TaggedUnion( comptime target: Target, comptime Union: type, - comptime Padding: type, + comptime options: TaggedUnionOptions(Union), ) type { return struct { comptime { @@ -30,7 +34,7 @@ pub fn TaggedUnion( .zig => {}, // For ABI compatibility, we expect that this is our union size. - .c => if (@sizeOf(CValue) != @sizeOf(Padding)) { + .c => if (@sizeOf(CValue) != @sizeOf(options.padding)) { @compileLog(@sizeOf(CValue)); @compileError("TaggedUnion CValue size does not match expected fixed size"); }, @@ -49,11 +53,25 @@ pub fn TaggedUnion( .c => extern struct { tag: Tag, value: CValue, + + /// Returns the public C value-union field name for `tag`, or + /// null when the tag has no value field. This is metadata only; + /// it does not rename fields in `CValue`. + pub fn cFieldRename(comptime tag: Tag) ?[]const u8 { + const tag_name = @tagName(tag); + const value = @field(@unionInit(Union, tag_name, undefined), tag_name); + + if (@field(options.field_renames, tag_name)) |name| return name; + if (@sizeOf(@TypeOf(value)) == 0) return null; + + return tag_name; + } }, }; /// The C ABI compatible union value type. pub const CValue = cvalue: { + @setEvalBranchQuota(10_000); switch (target) { .zig => break :cvalue extern struct {}, .c => {}, @@ -83,8 +101,8 @@ pub fn TaggedUnion( } names[tag_fields.len] = "_padding"; - types[tag_fields.len] = Padding; - attrs[tag_fields.len] = .{ .@"align" = @alignOf(Padding) }; + types[tag_fields.len] = options.padding; + attrs[tag_fields.len] = .{ .@"align" = @alignOf(options.padding) }; break :cvalue @Union(.@"extern", null, &names, &types, &attrs); }; @@ -125,6 +143,30 @@ pub fn TaggedUnion( }; } +/// Options for generating the C representation of a tagged union. +pub fn TaggedUnionOptions(comptime Union: type) type { + const Tag = @typeInfo(Union).@"union".tag_type.?; + const tag_fields = @typeInfo(Tag).@"enum".fields; + const FieldRenames: type = field_renames: { + const default_rename: ?[]const u8 = null; + + var names: [tag_fields.len][]const u8 = undefined; + var types: [tag_fields.len]type = undefined; + var attrs: [tag_fields.len]std.builtin.Type.StructField.Attributes = undefined; + + for (tag_fields, 0..) |field, i| { + names[i] = field.name; + types[i] = ?[]const u8; + attrs[i] = .{ .default_value_ptr = &default_rename }; + } + + break :field_renames @Struct(.auto, null, &names, &types, &attrs); + }; + return struct { + padding: type, + field_renames: FieldRenames = .{}, + }; +} test "TaggedUnion: matching size" { const Tag = enum(c_int) { a, b }; const U = TaggedUnion( @@ -133,7 +175,7 @@ test "TaggedUnion: matching size" { a: u32, b: u64, }, - u64, + .{ .padding = u64 }, ); try testing.expectEqual(8, @sizeOf(U.CValue)); @@ -146,7 +188,7 @@ test "TaggedUnion: padded size" { union(Tag) { a: u32, }, - u64, + .{ .padding = u64 }, ); try testing.expectEqual(8, @sizeOf(U.CValue)); @@ -157,9 +199,39 @@ test "TaggedUnion: c conversion" { const U = TaggedUnion(.c, union(Tag) { a: u32, b: u64, - }, u64); + }, .{ .padding = u64 }); const c = U.cval(.{ .a = 42 }); try testing.expectEqual(Tag.a, c.tag); try testing.expectEqual(42, c.value.a); } + +test "TaggedUnion: custom C value fields" { + const Tag = enum(c_int) { a, b, none }; + const Union = union(Tag) { + a: u32, + b: u32, + none, + }; + const U = TaggedUnion(.c, Union, .{ + .padding = u64, + .field_renames = .{ + .a = "value", + .b = "value", + }, + }); + + try testing.expect(!@hasField(U.CValue, "value")); + try testing.expect(@hasField(U.CValue, "a")); + try testing.expect(@hasField(U.CValue, "b")); + try testing.expect(@hasField(U.CValue, "none")); + try testing.expectEqualStrings("value", U.C.cFieldRename(.a).?); + try testing.expect(U.C.cFieldRename(.none) == null); + + const a = U.cval(.{ .a = 42 }); + try testing.expectEqual(Tag.a, a.tag); + try testing.expectEqual(42, a.value.a); + + const none = U.cval(.none); + try testing.expectEqual(Tag.none, none.tag); +} diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 1e974cdd5..b935b505f 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -2590,7 +2590,7 @@ pub const ScrollViewport = union(Tag) { @This(), // Padding: largest variant is isize (8 bytes on 64-bit). // Use [2]u64 (16 bytes) for future expansion. - [2]u64, + .{ .padding = [2]u64 }, ); pub const C = c_union.C; pub const CValue = c_union.CValue; diff --git a/src/terminal/c/osc.zig b/src/terminal/c/osc.zig index 8eca343c2..2ecc9c975 100644 --- a/src/terminal/c/osc.zig +++ b/src/terminal/c/osc.zig @@ -12,6 +12,9 @@ pub const Parser = ?*osc.Parser; /// C: GhosttyOscCommand pub const Command = ?*osc.Command; +/// C: GhosttyOscCommandType +pub const CommandType = osc.Command.Key; + pub fn new( alloc_: ?*const CAllocator, result: *Parser, @@ -44,7 +47,7 @@ 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 { +pub fn commandType(command_: Command) callconv(lib.calling_conv) CommandType { const command = command_ orelse return .invalid; return command.*; } diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 7dcca5f02..55d488158 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -182,7 +182,7 @@ pub const UnknownSequence = union(Tag) { // A future borrowed CSI payload may need parameter, separator, and // intermediate arrays. Reserve 128 bytes so that representation and // other structured sequence types can be added without an ABI break. - [16]u64, + .{ .padding = [16]u64 }, ); pub const C = c_union.C; pub const CValue = c_union.CValue; diff --git a/src/terminal/c/types.zig b/src/terminal/c/types.zig index 681556666..88759ef1c 100644 --- a/src/terminal/c/types.zig +++ b/src/terminal/c/types.zig @@ -1,29 +1,49 @@ -//! Comptime-generated metadata describing the layout of all C API -//! extern structs for the current target. +//! Comptime-generated ABI metadata for the public libghostty-vt C types. //! -//! 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. +//! The manifest is embedded in the library and returned by +//! `ghostty_type_json`. It is intended for FFI consumers that cannot use the +//! C headers directly, most notably WebAssembly hosts. const std = @import("std"); +const builtin = @import("builtin"); +const build_options = @import("terminal_options"); const lib = @import("../lib.zig"); -const color = @import("../color.zig"); -const sgr = @import("../sgr.zig"); -const color_c = @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 color = @import("../color.zig"); +const clipboard = @import("../clipboard.zig"); +const device_status = @import("../device_status.zig"); +const focus_pkg = @import("../focus.zig"); +const formatter_pkg = @import("../formatter.zig"); +const modes_pkg = @import("../modes.zig"); +const mouse_pkg = @import("../mouse.zig"); +const point = @import("../point.zig"); +const Selection = @import("../Selection.zig"); +const sgr = @import("../sgr.zig"); + +const input_config = @import("../../input/config.zig"); +const input_key = @import("../../input/key.zig"); +const input_mouse = @import("../../input/mouse.zig"); + +const build_info = @import("build_info.zig"); +const cell = @import("cell.zig"); +const color_c = @import("color.zig"); const formatter = @import("formatter.zig"); -const selection = @import("selection.zig"); -const selection_gesture = @import("selection_gesture.zig"); -const render = @import("render.zig"); -const style_c = @import("style.zig"); -const kitty_graphics = @import("kitty_graphics.zig"); -const mouse_encode = @import("mouse_encode.zig"); const grid_ref = @import("grid_ref.zig"); const io = @import("io.zig"); +const key_encode = @import("key_encode.zig"); +const kitty_graphics = @import("kitty_graphics.zig"); +const mouse_encode = @import("mouse_encode.zig"); +const mouse_event = @import("mouse_event.zig"); +const osc = @import("osc.zig"); +const render = @import("render.zig"); +const result = @import("result.zig"); +const row = @import("row.zig"); +const selection = @import("selection.zig"); +const selection_gesture = @import("selection_gesture.zig"); +const size_report = @import("size_report.zig"); +const snapshot = @import("snapshot.zig"); +const style = @import("style.zig"); const sys = @import("sys.zig"); +const terminal = @import("terminal.zig"); /// C: GhosttySurfacePosition pub const SurfacePosition = extern struct { @@ -37,318 +57,802 @@ pub const Codepoints = extern struct { len: usize = 0, }; -/// All C API structs and their Ghostty C names. -pub const structs: std.StaticStringMap(StructInfo) = structs: { - @setEvalBranchQuota(10_000); - break :structs .initComptime(.{ - .{ "GhosttyAllocator", StructInfo.init(lib.alloc.Allocator) }, - .{ "GhosttyAllocatorVtable", StructInfo.init(lib.alloc.VTable) }, - .{ "GhosttyBuffer", StructInfo.init(lib.Buffer) }, - .{ "GhosttyClipboardContent", StructInfo.init(terminal.ClipboardContent) }, - .{ "GhosttyClipboardWrite", StructInfo.init(terminal.ClipboardWrite) }, - .{ "GhosttyCodepoints", StructInfo.init(Codepoints) }, - .{ "GhosttyColorPaletteMask", StructInfo.init(color_c.PaletteMask) }, - .{ "GhosttyColorRgb", StructInfo.init(color.RGB.C) }, - .{ "GhosttyColorX11Entry", StructInfo.init(color_c.X11Entry) }, - .{ "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) }, - .{ "GhosttySelection", StructInfo.init(selection.CSelection) }, - .{ "GhosttyTerminalSelectWordOptions", StructInfo.init(selection.SelectWordOptions) }, - .{ "GhosttyTerminalSelectWordBetweenOptions", StructInfo.init(selection.SelectWordBetweenOptions) }, - .{ "GhosttyTerminalSelectLineOptions", StructInfo.init(selection.SelectLineOptions) }, - .{ "GhosttyTerminalSelectionFormatOptions", StructInfo.init(selection.FormatOptions) }, - .{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) }, - .{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) }, - .{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) }, - .{ "GhosttyKittyGraphicsPlacementRenderInfo", StructInfo.init(kitty_graphics.PlacementRenderInfo) }, - .{ "GhosttyMouseEncoderSize", StructInfo.init(mouse_encode.Size) }, - .{ "GhosttyMousePosition", StructInfo.init(mouse_event.Position) }, - .{ "GhosttyPoint", StructInfo.init(point.Point.C) }, - .{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) }, - .{ "GhosttyReader", StructInfo.init(io.Reader) }, - .{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) }, - .{ "GhosttyRenderStateCursor", StructInfo.init(render.Cursor) }, - .{ "GhosttyRenderStateRowSelection", StructInfo.init(render.RowSelection) }, - .{ "GhosttySelectionGestureBehaviors", StructInfo.init(selection_gesture.Behaviors) }, - .{ "GhosttySelectionGestureGeometry", StructInfo.init(selection_gesture.Geometry) }, - .{ "GhosttySgrAttribute", StructInfo.init(sgr.Attribute.C) }, - .{ "GhosttySgrUnknown", StructInfo.init(sgr.Attribute.Unknown.C) }, - .{ "GhosttySizeReportSize", StructInfo.init(size_report.Size) }, - .{ "GhosttyString", StructInfo.init(lib.String) }, - .{ "GhosttySurfacePosition", StructInfo.init(SurfacePosition) }, - .{ "GhosttyStyle", StructInfo.init(style_c.Style) }, - .{ "GhosttyStyleColor", StructInfo.init(style_c.Color) }, - .{ "GhosttySysImage", StructInfo.init(sys.Image) }, - .{ "GhosttyTerminalDesktopNotification", StructInfo.init(terminal.DesktopNotification) }, - .{ "GhosttyTerminalModeConfig", StructInfo.init(terminal.ModeConfig) }, - .{ "GhosttyTerminalProgressReport", StructInfo.init(terminal.ProgressReport) }, - .{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) }, - .{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) }, - .{ "GhosttyTerminalUnknownSequence", StructInfo.init(terminal.UnknownSequence.C) }, - .{ "GhosttyTerminalUnknownStringSequence", StructInfo.init(terminal.UnknownStringSequence) }, - .{ "GhosttyWriter", StructInfo.init(io.Writer) }, - }); +const TypeDecl = struct { + name: []const u8, + T: type, + kind: Kind, + prefix: []const u8 = "", + sentinel_suffix: []const u8 = "MAX_VALUE", + alias_type: []const u8 = "", + tagged_union: ?TaggedUnion = null, + union_field_names_T: ?type = null, + + const Kind = enum { + @"struct", + @"union", + @"enum", + alias, + @"opaque", + }; + + const TaggedUnion = struct { + tag_field: []const u8, + value_field: []const u8, + arm_source: ArmSource, + + const ArmSource = enum { + fields, + generated, + }; + }; + + fn initStruct(comptime name: []const u8, comptime T: type) TypeDecl { + return .{ .name = name, .T = T, .kind = .@"struct" }; + } + + fn initTaggedStruct( + comptime name: []const u8, + comptime T: type, + comptime tag_field: []const u8, + comptime value_field: []const u8, + comptime arm_source: TaggedUnion.ArmSource, + ) TypeDecl { + return .{ + .name = name, + .T = T, + .kind = .@"struct", + .tagged_union = .{ + .tag_field = tag_field, + .value_field = value_field, + .arm_source = arm_source, + }, + }; + } + + fn initUnion( + comptime name: []const u8, + comptime T: type, + comptime field_names_T: ?type, + ) TypeDecl { + return .{ + .name = name, + .T = T, + .kind = .@"union", + .union_field_names_T = field_names_T, + }; + } + + fn initEnum( + comptime name: []const u8, + comptime T: type, + comptime prefix: []const u8, + ) TypeDecl { + return .{ .name = name, .T = T, .kind = .@"enum", .prefix = prefix }; + } + + fn initEnumSentinel( + comptime name: []const u8, + comptime T: type, + comptime prefix: []const u8, + comptime sentinel_suffix: []const u8, + ) TypeDecl { + return .{ + .name = name, + .T = T, + .kind = .@"enum", + .prefix = prefix, + .sentinel_suffix = sentinel_suffix, + }; + } + + fn initAlias( + comptime name: []const u8, + comptime T: type, + comptime alias_type: []const u8, + ) TypeDecl { + return .{ .name = name, .T = T, .kind = .alias, .alias_type = alias_type }; + } + + fn initOpaque(comptime name: []const u8) TypeDecl { + return .{ .name = name, .T = *anyopaque, .kind = .@"opaque" }; + } }; -/// The comptime-generated JSON string of all structs. +/// Public C types. Names and enum prefixes are intentionally explicit because +/// Zig identifiers are not the C API contract. +const type_decls = [_]TypeDecl{ + .initStruct("GhosttyAllocator", lib.alloc.Allocator), + .initStruct("GhosttyAllocatorVtable", lib.alloc.VTable), + .initStruct("GhosttyBuffer", lib.Buffer), + .initStruct("GhosttyCellsView", cell.CellsView), + .initStruct("GhosttyClipboardContent", terminal.ClipboardContent), + .initStruct("GhosttyClipboardWrite", terminal.ClipboardWrite), + .initStruct("GhosttyCodepoints", Codepoints), + .initStruct("GhosttyColorPaletteMask", color_c.PaletteMask), + .initStruct("GhosttyColorRgb", color.RGB.C), + .initStruct("GhosttyColorX11Entry", color_c.X11Entry), + .initStruct("GhosttyDeviceAttributes", terminal.DeviceAttributes), + .initStruct("GhosttyDeviceAttributesPrimary", terminal.DeviceAttributes.Primary), + .initStruct("GhosttyDeviceAttributesSecondary", terminal.DeviceAttributes.Secondary), + .initStruct("GhosttyDeviceAttributesTertiary", terminal.DeviceAttributes.Tertiary), + .initStruct("GhosttyFormatterScreenExtra", formatter.ScreenOptions.Extra), + .initStruct("GhosttyFormatterTerminalExtra", formatter.TerminalOptions.Extra), + .initStruct("GhosttyFormatterTerminalOptions", formatter.TerminalOptions), + .initStruct("GhosttyGridRef", grid_ref.CGridRef), + .initStruct("GhosttyKittyGraphicsPlacementRenderInfo", kitty_graphics.PlacementRenderInfo), + .initStruct("GhosttyMouseEncoderSize", mouse_encode.Size), + .initStruct("GhosttyMousePosition", mouse_event.Position), + .initTaggedStruct("GhosttyPoint", point.Point.C, "tag", "value", .generated), + .initStruct("GhosttyPointCoordinate", point.Coordinate), + .initUnion("GhosttyPointValue", point.Point.CValue, point.Point.C), + .initStruct("GhosttyReader", io.Reader), + .initStruct("GhosttyRenderStateColors", render.Colors), + .initStruct("GhosttyRenderStateCursor", render.Cursor), + .initStruct("GhosttyRenderStateRowSelection", render.RowSelection), + .initStruct("GhosttySelection", selection.CSelection), + .initStruct("GhosttySelectionGestureBehaviors", selection_gesture.Behaviors), + .initStruct("GhosttySelectionGestureGeometry", selection_gesture.Geometry), + .initTaggedStruct("GhosttySgrAttribute", sgr.Attribute.C, "tag", "value", .generated), + .initStruct("GhosttySgrUnknown", sgr.Attribute.Unknown.C), + .initUnion("GhosttySgrAttributeValue", sgr.Attribute.CValue, sgr.Attribute.C), + .initStruct("GhosttySizeReportSize", size_report.Size), + .initStruct("GhosttyString", lib.String), + .initStruct("GhosttySurfacePosition", SurfacePosition), + .initStruct("GhosttyStyle", style.Style), + .initTaggedStruct("GhosttyStyleColor", style.Color, "tag", "value", .fields), + .initUnion("GhosttyStyleColorValue", style.ColorValue, null), + .initStruct("GhosttySysImage", sys.Image), + .initStruct("GhosttyTerminalDesktopNotification", terminal.DesktopNotification), + .initStruct("GhosttyTerminalModeConfig", terminal.ModeConfig), + .initStruct("GhosttyTerminalProgressReport", terminal.ProgressReport), + .initStruct("GhosttyTerminalScrollbar", terminal.TerminalScrollbar), + .initTaggedStruct("GhosttyTerminalScrollViewport", terminal.ScrollViewport, "tag", "value", .generated), + .initUnion( + "GhosttyTerminalScrollViewportValue", + terminal.ZigTerminal.ScrollViewport.CValue, + terminal.ZigTerminal.ScrollViewport.C, + ), + .initStruct("GhosttyTerminalSelectLineOptions", selection.SelectLineOptions), + .initStruct("GhosttyTerminalSelectWordBetweenOptions", selection.SelectWordBetweenOptions), + .initStruct("GhosttyTerminalSelectWordOptions", selection.SelectWordOptions), + .initStruct("GhosttyTerminalSelectionFormatOptions", selection.FormatOptions), + .initTaggedStruct("GhosttyTerminalUnknownSequence", terminal.UnknownSequence.C, "tag", "value", .generated), + .initStruct("GhosttyTerminalUnknownStringSequence", terminal.UnknownStringSequence), + .initUnion( + "GhosttyTerminalUnknownSequenceValue", + terminal.UnknownSequence.CValue, + terminal.UnknownSequence.C, + ), + .initStruct("GhosttyWriter", io.Writer), + + .initEnumSentinel("GhosttyResult", result.Result, "GHOSTTY_", "RESULT_MAX_VALUE"), + .initEnum("GhosttyBuildInfo", build_info.BuildInfo, "GHOSTTY_BUILD_INFO_"), + .initEnumSentinel("GhosttyOptimizeMode", build_info.OptimizeMode, "GHOSTTY_OPTIMIZE_", "MODE_MAX_VALUE"), + .initEnumSentinel("GhosttyCellContentTag", cell.ContentTag, "GHOSTTY_CELL_CONTENT_", "TAG_MAX_VALUE"), + .initEnum("GhosttyCellData", cell.CellData, "GHOSTTY_CELL_DATA_"), + .initEnum("GhosttyCellSemanticContent", cell.SemanticContent, "GHOSTTY_CELL_SEMANTIC_"), + .initEnum("GhosttyCellWide", cell.Wide, "GHOSTTY_CELL_WIDE_"), + .initEnum("GhosttyClipboardLocation", clipboard.Location, "GHOSTTY_CLIPBOARD_LOCATION_"), + .initEnum("GhosttyClipboardWriteResult", clipboard.WriteResult, "GHOSTTY_CLIPBOARD_WRITE_RESULT_"), + .initEnum("GhosttyColorScheme", device_status.ColorScheme, "GHOSTTY_COLOR_SCHEME_"), + .initEnum("GhosttyFocusEvent", focus_pkg.Event, "GHOSTTY_FOCUS_"), + .initEnum("GhosttyFormatterFormat", formatter_pkg.Format, "GHOSTTY_FORMATTER_FORMAT_"), + .initEnum("GhosttyKey", input_key.Key, "GHOSTTY_KEY_"), + .initEnum("GhosttyKeyAction", input_key.Action, "GHOSTTY_KEY_ACTION_"), + .initEnum("GhosttyKeyEncoderOption", key_encode.Option, "GHOSTTY_KEY_ENCODER_OPT_"), + .initEnum("GhosttyKittyGraphicsData", kitty_graphics.Data, "GHOSTTY_KITTY_GRAPHICS_DATA_"), + .initEnum("GhosttyKittyGraphicsImageData", kitty_graphics.ImageData, "GHOSTTY_KITTY_IMAGE_DATA_"), + .initEnum("GhosttyKittyGraphicsPlacementData", kitty_graphics.PlacementData, "GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_"), + .initEnum("GhosttyKittyGraphicsPlacementIteratorOption", kitty_graphics.PlacementIteratorOption, "GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_"), + .initEnum("GhosttyKittyImageCompression", kitty_graphics.ImageCompression, "GHOSTTY_KITTY_IMAGE_COMPRESSION_"), + .initEnum("GhosttyKittyImageFormat", kitty_graphics.ImageFormat, "GHOSTTY_KITTY_IMAGE_FORMAT_"), + .initEnum("GhosttyKittyPlacementLayer", kitty_graphics.PlacementLayer, "GHOSTTY_KITTY_PLACEMENT_LAYER_"), + .initEnum("GhosttyModeReportState", modes_pkg.Report.State, "GHOSTTY_MODE_REPORT_"), + .initEnum("GhosttyMouseAction", input_mouse.Action, "GHOSTTY_MOUSE_ACTION_"), + .initEnum("GhosttyMouseButton", input_mouse.Button, "GHOSTTY_MOUSE_BUTTON_"), + .initEnum("GhosttyMouseEncoderOption", mouse_encode.Option, "GHOSTTY_MOUSE_ENCODER_OPT_"), + .initEnum("GhosttyMouseFormat", mouse_pkg.Format, "GHOSTTY_MOUSE_FORMAT_"), + .initEnum("GhosttyMouseTrackingMode", mouse_pkg.Event, "GHOSTTY_MOUSE_TRACKING_"), + .initEnum("GhosttyOptionAsAlt", input_config.OptionAsAlt, "GHOSTTY_OPTION_AS_ALT_"), + .initEnum("GhosttyOscCommandData", osc.CommandData, "GHOSTTY_OSC_DATA_"), + .initEnumSentinel( + "GhosttyOscCommandType", + osc.CommandType, + "GHOSTTY_OSC_COMMAND_", + "TYPE_MAX_VALUE", + ), + .initEnum("GhosttyPointTag", point.Tag, "GHOSTTY_POINT_TAG_"), + .initEnum("GhosttyRenderStateCursorVisualStyle", render.CursorVisualStyle, "GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_"), + .initEnum("GhosttyRenderStateData", render.Data, "GHOSTTY_RENDER_STATE_DATA_"), + .initEnum("GhosttyRenderStateDirty", render.Dirty, "GHOSTTY_RENDER_STATE_DIRTY_"), + .initEnum("GhosttyRenderStateOption", render.SetOption, "GHOSTTY_RENDER_STATE_OPTION_"), + .initEnum("GhosttyRenderStateRowCellsData", render.RowCellsData, "GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_"), + .initEnum("GhosttyRenderStateRowData", render.RowData, "GHOSTTY_RENDER_STATE_ROW_DATA_"), + .initEnum("GhosttyRenderStateRowOption", render.RowOption, "GHOSTTY_RENDER_STATE_ROW_OPTION_"), + .initEnum("GhosttyRowData", row.RowData, "GHOSTTY_ROW_DATA_"), + .initEnum("GhosttyRowSemanticPrompt", row.SemanticPrompt, "GHOSTTY_ROW_SEMANTIC_"), + .initEnum("GhosttySelectionAdjust", Selection.Adjustment, "GHOSTTY_SELECTION_ADJUST_"), + .initEnum("GhosttySelectionGestureAutoscroll", selection_gesture.Autoscroll, "GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_"), + .initEnum("GhosttySelectionGestureBehavior", selection_gesture.Behavior, "GHOSTTY_SELECTION_GESTURE_BEHAVIOR_"), + .initEnum("GhosttySelectionGestureData", selection_gesture.Data, "GHOSTTY_SELECTION_GESTURE_DATA_"), + .initEnum("GhosttySelectionGestureEventOption", selection_gesture.EventOption, "GHOSTTY_SELECTION_GESTURE_EVENT_OPT_"), + .initEnum("GhosttySelectionGestureEventType", selection_gesture.EventType, "GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_"), + .initEnum("GhosttySelectionOrder", Selection.Order, "GHOSTTY_SELECTION_ORDER_"), + .initEnum("GhosttySgrAttributeTag", sgr.Attribute.Tag, "GHOSTTY_SGR_ATTR_"), + .initEnum("GhosttySgrUnderline", sgr.Attribute.Underline, "GHOSTTY_SGR_UNDERLINE_"), + .initEnumSentinel("GhosttySizeReportStyle", size_report.Style, "GHOSTTY_SIZE_REPORT_", "STYLE_MAX_VALUE"), + .initEnum("GhosttySnapshotDecoderData", snapshot.DecoderData, "GHOSTTY_SNAPSHOT_DECODER_DATA_"), + .initEnum("GhosttySnapshotDecoderOption", snapshot.DecoderOption, "GHOSTTY_SNAPSHOT_DECODER_OPT_"), + .initEnumSentinel("GhosttyStyleColorTag", style.ColorTag, "GHOSTTY_STYLE_COLOR_", "TAG_MAX_VALUE"), + .initEnum("GhosttySysLogLevel", sys.LogLevel, "GHOSTTY_SYS_LOG_LEVEL_"), + .initEnum("GhosttySysOption", sys.Option, "GHOSTTY_SYS_OPT_"), + .initEnum("GhosttyTerminalCompressionMode", terminal.CompressionMode, "GHOSTTY_TERMINAL_COMPRESSION_MODE_"), + .initEnum("GhosttyTerminalCompressionResult", terminal.CompressionResult, "GHOSTTY_TERMINAL_COMPRESSION_RESULT_"), + .initEnum("GhosttyTerminalCursorStyle", terminal.TerminalCursorStyle, "GHOSTTY_TERMINAL_CURSOR_STYLE_"), + .initEnum("GhosttyTerminalData", terminal.TerminalData, "GHOSTTY_TERMINAL_DATA_"), + .initEnum("GhosttyTerminalOption", terminal.Option, "GHOSTTY_TERMINAL_OPT_"), + .initEnum("GhosttyTerminalProgressState", terminal.ProgressState, "GHOSTTY_TERMINAL_PROGRESS_STATE_"), + .initEnum("GhosttyTerminalScreen", terminal.TerminalScreen, "GHOSTTY_TERMINAL_SCREEN_"), + .initEnum("GhosttyTerminalScrollViewportTag", terminal.ZigTerminal.ScrollViewport.Tag, "GHOSTTY_SCROLL_VIEWPORT_"), + .initEnum("GhosttyTerminalUnknownSequenceTag", terminal.UnknownSequence.Tag, "GHOSTTY_TERMINAL_UNKNOWN_SEQUENCE_"), + + .initAlias("GhosttyCell", u64, "u64"), + .initAlias("GhosttyColorPaletteIndex", u8, "u8"), + .initAlias("GhosttyKittyKeyFlags", u8, "u8"), + .initAlias("GhosttyMode", u16, "u16"), + .initAlias("GhosttyMods", u16, "u16"), + .initAlias("GhosttyRow", u64, "u64"), + .initAlias("GhosttyStyleId", u16, "u16"), + + .initOpaque("GhosttyFormatter"), + .initOpaque("GhosttyKeyEncoder"), + .initOpaque("GhosttyKeyEvent"), + .initOpaque("GhosttyKittyGraphics"), + .initOpaque("GhosttyKittyGraphicsImage"), + .initOpaque("GhosttyKittyGraphicsPlacementIterator"), + .initOpaque("GhosttyMouseEncoder"), + .initOpaque("GhosttyMouseEvent"), + .initOpaque("GhosttyOscCommand"), + .initOpaque("GhosttyOscParser"), + .initOpaque("GhosttyRenderState"), + .initOpaque("GhosttyRenderStateRowCells"), + .initOpaque("GhosttyRenderStateRowIterator"), + .initOpaque("GhosttySelectionGesture"), + .initOpaque("GhosttySelectionGestureEvent"), + .initOpaque("GhosttySgrParser"), + .initOpaque("GhosttySnapshotDecoder"), + .initOpaque("GhosttyTerminal"), + .initOpaque("GhosttyTrackedGridRef"), +}; + +comptime { + @setEvalBranchQuota(100_000); + for (type_decls, 0..) |decl, i| { + for (type_decls[0..i]) |previous| { + if (std.mem.eql(u8, decl.name, previous.name)) + @compileError("duplicate public ABI type: " ++ decl.name); + } + } +} + pub const json: [:0]const u8 = json: { - @setEvalBranchQuota(200_000); + @setEvalBranchQuota(1_000_000); var counter: std.Io.Writer.Discarding = .init(&.{}); - jsonWriteAll(&counter.writer) catch unreachable; + Json.writeAll(&counter.writer) catch unreachable; var buf: [counter.count:0]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); - jsonWriteAll(&writer) catch unreachable; + Json.writeAll(&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, +const Json = struct { + fn writeAll(writer: *std.Io.Writer) std.Io.Writer.Error!void { + var jws: std.json.Stringify = .{ .writer = writer }; + try jws.beginObject(); + try jws.objectField("schema"); + try jws.write(1); - pub const FieldInfo = struct { - name: []const u8, - offset: usize, - size: usize, - type: []const u8, - }; + try jws.objectField("abi"); + try jws.beginObject(); + try jws.objectField("target"); + try jws.write(@tagName(builtin.target.cpu.arch)); + try jws.objectField("os"); + try jws.write(@tagName(builtin.target.os.tag)); + try jws.objectField("environment"); + try jws.write(@tagName(builtin.target.abi)); + try jws.objectField("pointer_size"); + try jws.write(@sizeOf(*anyopaque)); + try jws.objectField("usize_size"); + try jws.write(@sizeOf(usize)); + try jws.objectField("endian"); + try jws.write(@tagName(builtin.target.cpu.arch.endian())); + try jws.endObject(); - 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; - }; + try jws.objectField("library_version"); + try jws.write(build_options.version_string); + try jws.objectField("commit"); + if (build_options.version_build) |commit| try jws.write(commit) else try jws.write(null); + try jws.objectField("dirty"); + try jws.write(null); - return .{ - .name = @typeName(T), - .size = @sizeOf(T), - .@"align" = @alignOf(T), - .fields = &field_infos, - }; + try jws.objectField("types"); + try jws.beginObject(); + inline for (type_decls) |decl| { + try jws.objectField(decl.name); + try writeType(decl, &jws); } + try jws.endObject(); + try jws.endObject(); } - pub fn jsonStringify( - self: *const StructInfo, - jws: anytype, + fn writeType(comptime decl: TypeDecl, jws: *std.json.Stringify) std.Io.Writer.Error!void { + try jws.beginObject(); + try jws.objectField("kind"); + try jws.write(@tagName(decl.kind)); + + switch (decl.kind) { + .@"struct", .@"union" => { + try writeSizeAlign(decl.T, jws); + try jws.objectField("fields"); + try jws.beginObject(); + if (decl.union_field_names_T != null) { + try writeMappedUnionFields(decl, jws); + } else { + const fields = switch (decl.kind) { + .@"struct" => @typeInfo(decl.T).@"struct".fields, + .@"union" => @typeInfo(decl.T).@"union".fields, + else => unreachable, + }; + inline for (fields) |field| { + try writeField( + decl, + field.name, + field.type, + if (decl.kind == .@"union") 0 else @offsetOf(decl.T, field.name), + jws, + ); + } + } + try jws.endObject(); + }, + .@"enum" => { + try writeSizeAlign(c_int, jws); + try jws.objectField("underlying"); + try jws.write("i32"); + try jws.objectField("prefix"); + try jws.write(decl.prefix); + try jws.objectField("values"); + try jws.beginObject(); + inline for (@typeInfo(decl.T).@"enum".fields) |field| { + try writeEnumObjectField(decl.name, field.name, jws); + try jws.write(field.value); + } + try jws.objectField(decl.sentinel_suffix); + try jws.write(std.math.maxInt(c_int)); + try jws.endObject(); + }, + .alias => { + try writeSizeAlign(decl.T, jws); + try jws.objectField("type"); + try jws.write(decl.alias_type); + }, + .@"opaque" => try writeSizeAlign(*anyopaque, jws), + } + try jws.endObject(); + } + + fn writeField( + comptime decl: TypeDecl, + comptime name: []const u8, + comptime T: type, + comptime offset: usize, + jws: *std.json.Stringify, ) std.Io.Writer.Error!void { + try jws.objectField(name); try jws.beginObject(); + try jws.objectField("offset"); + try jws.write(offset); 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.write(@sizeOf(T)); + try writeFieldType(decl.name, name, T, jws); + if (decl.tagged_union) |tagged| { + if (std.mem.eql(u8, name, tagged.value_field)) { + try jws.objectField("tag"); + try jws.write(tagged.tag_field); + try writeTaggedArms(decl, tagged, jws); + } } try jws.endObject(); + } + + fn writeMappedUnionFields( + comptime decl: TypeDecl, + jws: *std.json.Stringify, + ) std.Io.Writer.Error!void { + const FieldNames = decl.union_field_names_T.?; + const Tag = @FieldType(FieldNames, "tag"); + const tag_fields = @typeInfo(Tag).@"enum".fields; + + inline for (tag_fields, 0..) |field, i| { + const name = comptime FieldNames.cFieldRename(@field(Tag, field.name)) orelse continue; + if (comptime mappedUnionFieldIsDuplicate(decl, i, name)) continue; + try writeField(decl, name, @FieldType(decl.T, field.name), 0, jws); + } + + if (@hasField(decl.T, "_padding")) + try writeField(decl, "_padding", @FieldType(decl.T, "_padding"), 0, jws); + } + + fn mappedUnionFieldIsDuplicate( + comptime decl: TypeDecl, + comptime index: usize, + comptime name: []const u8, + ) bool { + const FieldNames = decl.union_field_names_T.?; + const Tag = @FieldType(FieldNames, "tag"); + const tag_fields = @typeInfo(Tag).@"enum".fields; + const T = @FieldType(decl.T, tag_fields[index].name); + + inline for (tag_fields[0..index]) |previous| { + const previous_name = FieldNames.cFieldRename(@field(Tag, previous.name)) orelse continue; + if (std.mem.eql(u8, previous_name, name)) { + if (@FieldType(decl.T, previous.name) != T) + @compileError("tagged union metadata maps different field types to " ++ name); + return true; + } + } + + return false; + } + + fn writeSizeAlign(comptime T: type, jws: *std.json.Stringify) std.Io.Writer.Error!void { + try jws.objectField("size"); + try jws.write(@sizeOf(T)); + try jws.objectField("align"); + try jws.write(@alignOf(T)); + } + + fn writeFieldType( + comptime owner: []const u8, + comptime field_name: []const u8, + comptime T: type, + jws: *std.json.Stringify, + ) std.Io.Writer.Error!void { + if (comptime std.mem.eql(u8, owner, "GhosttyCellsView") and + std.mem.eql(u8, field_name, "ptr")) + { + try writePointerTypeNamed(T, true, "GhosttyCell", jws); + return; + } + if (comptime std.mem.eql(u8, owner, "GhosttyGridRef") and + std.mem.eql(u8, field_name, "node")) + { + try writePointerTypeNamed(T, true, "opaque", jws); + return; + } + + if (comptime fieldTypeOverride(owner, field_name)) |name| { + try jws.objectField("type"); + try jws.write(name); + return; + } + + switch (@typeInfo(T)) { + .array => |info| { + try jws.objectField("type"); + try jws.write("array"); + try jws.objectField("elem"); + try jws.write(publicTypeName(info.child)); + try jws.objectField("count"); + try jws.write(info.len); + }, + .optional => |info| try writePointerType(info.child, true, jws), + .pointer => try writePointerType(T, false, jws), + else => { + try jws.objectField("type"); + try jws.write(publicTypeName(T)); + }, + } + } + + fn writePointerType( + comptime T: type, + comptime nullable: bool, + jws: *std.json.Stringify, + ) std.Io.Writer.Error!void { + const info = @typeInfo(T).pointer; + try jws.objectField("type"); + try jws.write("pointer"); + try jws.objectField("elem"); + try jws.write(publicTypeName(info.child)); + try jws.objectField("const"); + try jws.write(info.is_const); + if (nullable) { + try jws.objectField("nullable"); + try jws.write(true); + } + } + + fn writePointerTypeNamed( + comptime T: type, + comptime nullable: bool, + comptime elem: []const u8, + jws: *std.json.Stringify, + ) std.Io.Writer.Error!void { + const pointer_type = switch (@typeInfo(T)) { + .optional => |info| info.child, + .pointer => T, + else => unreachable, + }; + const info = @typeInfo(pointer_type).pointer; + try jws.objectField("type"); + try jws.write("pointer"); + try jws.objectField("elem"); + try jws.write(elem); + try jws.objectField("const"); + try jws.write(info.is_const); + if (nullable) { + try jws.objectField("nullable"); + try jws.write(true); + } + } + + fn fieldTypeOverride(comptime owner: []const u8, comptime field_name: []const u8) ?[]const u8 { + if (std.mem.eql(u8, field_name, "value")) { + if (std.mem.eql(u8, owner, "GhosttyPoint")) return "GhosttyPointValue"; + if (std.mem.eql(u8, owner, "GhosttySgrAttribute")) return "GhosttySgrAttributeValue"; + if (std.mem.eql(u8, owner, "GhosttyStyleColor")) return "GhosttyStyleColorValue"; + if (std.mem.eql(u8, owner, "GhosttyTerminalScrollViewport")) return "GhosttyTerminalScrollViewportValue"; + if (std.mem.eql(u8, owner, "GhosttyTerminalUnknownSequence")) return "GhosttyTerminalUnknownSequenceValue"; + } + if (std.mem.eql(u8, owner, "GhosttyStyleColorValue") and std.mem.eql(u8, field_name, "palette")) + return "GhosttyColorPaletteIndex"; + if (std.mem.eql(u8, owner, "GhosttySgrAttributeValue")) { + if (std.mem.eql(u8, field_name, "underline")) return "GhosttySgrUnderline"; + if (std.mem.endsWith(u8, field_name, "_8") or std.mem.endsWith(u8, field_name, "_256")) + return "GhosttyColorPaletteIndex"; + } + if (std.mem.eql(u8, owner, "GhosttyTerminalModeConfig") and std.mem.eql(u8, field_name, "mode")) + return "GhosttyMode"; + return null; + } + + fn writeTaggedArms( + comptime decl: TypeDecl, + comptime tagged: TypeDecl.TaggedUnion, + jws: *std.json.Stringify, + ) std.Io.Writer.Error!void { + const Tag = @FieldType(decl.T, tagged.tag_field); + try jws.objectField("arms"); + try jws.beginObject(); + inline for (@typeInfo(Tag).@"enum".fields) |field| { + try writeEnumObjectField(publicTypeName(Tag), field.name, jws); + if (comptime taggedArm(decl, field.name)) |arm| try jws.write(arm) else try jws.write(null); + } 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); + fn taggedArm(comptime decl: TypeDecl, comptime tag_name: []const u8) ?[]const u8 { + const tagged = decl.tagged_union.?; + const Tag = @FieldType(decl.T, tagged.tag_field); + if (tagged.arm_source == .generated) + return decl.T.cFieldRename(@field(Tag, tag_name)); + + const Value = @FieldType(decl.T, tagged.value_field); + if (!@hasField(Value, tag_name)) return null; + return if (@sizeOf(@FieldType(Value, tag_name)) == 0) null else tag_name; } - try jws.endObject(); -} -fn typeName(comptime T: type) []const u8 { - return switch (@typeInfo(T)) { - .bool => "bool", - .float => |info| switch (info.bits) { - 32 => "f32", - 64 => "f64", - else => @compileError("unsupported float size"), - }, - .int => |info| switch (info.signedness) { - .signed => switch (info.bits) { + fn publicTypeName(comptime T: type) []const u8 { + inline for (type_decls) |decl| switch (decl.kind) { + .@"struct", .@"union", .@"enum" => if (T == decl.T) return decl.name, + .alias, .@"opaque" => {}, + }; + return switch (@typeInfo(T)) { + .bool => "bool", + .float => |info| switch (info.bits) { + 32 => "f32", + 64 => "f64", + else => "opaque", + }, + .int => |info| intName(info.signedness, info.bits), + .comptime_int => "comptime_int", + .void => "void", + .@"opaque" => "opaque", + .@"fn" => "function", + .@"enum" => "enum", + .@"struct" => "struct", + .@"union" => "union", + else => "opaque", + }; + } + + fn intName(comptime signedness: std.builtin.Signedness, comptime bits: u16) []const u8 { + return switch (signedness) { + .signed => switch (bits) { 8 => "i8", 16 => "i16", 32 => "i32", 64 => "i64", - else => @compileError("unsupported signed int size"), + else => "opaque", }, - .unsigned => switch (info.bits) { + .unsigned => switch (bits) { 8 => "u8", 16 => "u16", 32 => "u32", 64 => "u64", - else => @compileError("unsupported unsigned int size"), + else => "opaque", }, - }, - .@"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, - .{}, - ); + fn writeUpperObjectField(comptime name: []const u8, jws: *std.json.Stringify) std.Io.Writer.Error!void { + var upper: [name.len]u8 = undefined; + for (name, 0..) |c, i| upper[i] = std.ascii.toUpper(c); + try jws.objectField(&upper); + } + + fn writeEnumObjectField( + comptime enum_name: []const u8, + comptime zig_name: []const u8, + jws: *std.json.Stringify, + ) std.Io.Writer.Error!void { + if (std.mem.eql(u8, enum_name, "GhosttyKey") and std.mem.startsWith(u8, zig_name, "key_")) + return writeUpperObjectField(zig_name["key_".len..], jws); + if (std.mem.eql(u8, enum_name, "GhosttySgrAttributeTag")) { + if (std.mem.eql(u8, zig_name, "256_underline_color")) return jws.objectField("UNDERLINE_COLOR_256"); + if (std.mem.eql(u8, zig_name, "8_bg")) return jws.objectField("BG_8"); + if (std.mem.eql(u8, zig_name, "8_fg")) return jws.objectField("FG_8"); + if (std.mem.eql(u8, zig_name, "8_bright_bg")) return jws.objectField("BRIGHT_BG_8"); + if (std.mem.eql(u8, zig_name, "8_bright_fg")) return jws.objectField("BRIGHT_FG_8"); + if (std.mem.eql(u8, zig_name, "256_bg")) return jws.objectField("BG_256"); + if (std.mem.eql(u8, zig_name, "256_fg")) return jws.objectField("FG_256"); + } + if (std.mem.eql(u8, enum_name, "GhosttyTerminalOption") and std.mem.eql(u8, zig_name, "size_cb")) + return jws.objectField("SIZE"); + return writeUpperObjectField(zig_name, jws); + } + + fn isBuiltinType(name: []const u8) bool { + const builtins = [_][]const u8{ + "array", "bool", "f32", "f64", "function", "i8", "i16", "i32", + "i64", "opaque", "pointer", "u8", "u16", "u32", "u64", "void", + }; + for (builtins) |builtin_name| { + if (std.mem.eql(u8, name, builtin_name)) return true; + } + return false; + } +}; + +test "manifest parses and is versioned" { + const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); defer parsed.deinit(); - const root = parsed.value.object; + try std.testing.expectEqual(@as(i64, 1), root.get("schema").?.integer); + try std.testing.expect(root.contains("abi")); + try std.testing.expect(root.contains("library_version")); + const manifest_types = root.get("types").?.object; + try std.testing.expectEqual(type_decls.len, manifest_types.count()); + inline for (type_decls) |decl| try std.testing.expect(manifest_types.contains(decl.name)); - // Verify we have every public struct declared by the C API headers. - const expected_structs = [_][]const u8{ - "GhosttyAllocator", - "GhosttyAllocatorVtable", - "GhosttyBuffer", - "GhosttyClipboardContent", - "GhosttyClipboardWrite", - "GhosttyCodepoints", - "GhosttyColorPaletteMask", - "GhosttyColorRgb", - "GhosttyColorX11Entry", - "GhosttyDeviceAttributes", - "GhosttyDeviceAttributesPrimary", - "GhosttyDeviceAttributesSecondary", - "GhosttyDeviceAttributesTertiary", - "GhosttyFormatterScreenExtra", - "GhosttyFormatterTerminalExtra", - "GhosttyFormatterTerminalOptions", - "GhosttyGridRef", - "GhosttyKittyGraphicsPlacementRenderInfo", - "GhosttyMouseEncoderSize", - "GhosttyMousePosition", - "GhosttyPoint", - "GhosttyPointCoordinate", - "GhosttyReader", - "GhosttyRenderStateColors", - "GhosttyRenderStateCursor", - "GhosttyRenderStateRowSelection", - "GhosttySelection", - "GhosttySelectionGestureBehaviors", - "GhosttySelectionGestureGeometry", - "GhosttySgrAttribute", - "GhosttySgrUnknown", - "GhosttySizeReportSize", - "GhosttyString", - "GhosttyStyle", - "GhosttyStyleColor", - "GhosttySurfacePosition", - "GhosttySysImage", - "GhosttyTerminalDesktopNotification", - "GhosttyTerminalModeConfig", - "GhosttyTerminalProgressReport", - "GhosttyTerminalScrollbar", - "GhosttyTerminalScrollViewport", - "GhosttyTerminalSelectLineOptions", - "GhosttyTerminalSelectWordBetweenOptions", - "GhosttyTerminalSelectWordOptions", - "GhosttyTerminalSelectionFormatOptions", - "GhosttyTerminalUnknownSequence", - "GhosttyTerminalUnknownStringSequence", - "GhosttyWriter", - }; - try std.testing.expectEqual(expected_structs.len, root.count()); - for (expected_structs) |name| { - try std.testing.expect(root.contains(name)); - } - - const clipboard_content = root.get("GhosttyClipboardContent").?.object; - const clipboard_content_fields = clipboard_content.get("fields").?.object; - try std.testing.expect(clipboard_content_fields.contains("mime")); - try std.testing.expect(clipboard_content_fields.contains("data")); - - const clipboard_write = root.get("GhosttyClipboardWrite").?.object; - const clipboard_write_fields = clipboard_write.get("fields").?.object; - try std.testing.expect(clipboard_write_fields.contains("size")); - try std.testing.expect(clipboard_write_fields.contains("location")); - try std.testing.expect(clipboard_write_fields.contains("contents")); - try std.testing.expect(clipboard_write_fields.contains("contents_len")); - - const unknown_sequence = root.get("GhosttyTerminalUnknownSequence").?.object; - const unknown_sequence_fields = unknown_sequence.get("fields").?.object; - try std.testing.expect(unknown_sequence_fields.contains("tag")); - try std.testing.expect(unknown_sequence_fields.contains("value")); - - const unknown_string = root.get("GhosttyTerminalUnknownStringSequence").?.object; - const unknown_string_fields = unknown_string.get("fields").?.object; - try std.testing.expect(unknown_string_fields.contains("truncated")); - try std.testing.expect(unknown_string_fields.contains("content")); - - const render_cursor_fields = root.get("GhosttyRenderStateCursor").?.object + const cursor_fields = manifest_types.get("GhosttyRenderStateCursor").?.object .get("fields").?.object; - try std.testing.expect(render_cursor_fields.contains("size")); - try std.testing.expect(render_cursor_fields.contains("viewport_has_value")); - try std.testing.expect(render_cursor_fields.contains("viewport_x")); - try std.testing.expect(render_cursor_fields.contains("viewport_y")); - try std.testing.expect(render_cursor_fields.contains("wide_tail")); - try std.testing.expect(render_cursor_fields.contains("visible")); - try std.testing.expect(render_cursor_fields.contains("blinking")); - try std.testing.expect(render_cursor_fields.contains("password_input")); - try std.testing.expect(render_cursor_fields.contains("visual_style")); - - const reader_fields = root.get("GhosttyReader").?.object - .get("fields").?.object; - try std.testing.expect(reader_fields.contains("read")); - try std.testing.expect(reader_fields.contains("userdata")); - - const writer_fields = root.get("GhosttyWriter").?.object - .get("fields").?.object; - try std.testing.expect(writer_fields.contains("write")); - try std.testing.expect(writer_fields.contains("userdata")); - - try std.testing.expect(!root.contains("GhosttyTerminalOptions")); + try std.testing.expect(cursor_fields.contains("size")); + try std.testing.expect(cursor_fields.contains("viewport_has_value")); + try std.testing.expect(cursor_fields.contains("viewport_x")); + try std.testing.expect(cursor_fields.contains("viewport_y")); + try std.testing.expect(cursor_fields.contains("wide_tail")); + try std.testing.expect(cursor_fields.contains("visible")); + try std.testing.expect(cursor_fields.contains("blinking")); + try std.testing.expect(cursor_fields.contains("password_input")); + try std.testing.expect(cursor_fields.contains("visual_style")); } -test "struct sizes are non-zero" { - const parsed = try std.json.parseFromSlice( - std.json.Value, - std.testing.allocator, - json, - .{}, - ); +test "manifest describes enums, arrays, and tagged unions" { + const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); defer parsed.deinit(); + const manifest_types = parsed.value.object.get("types").?.object; - 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); + const data = manifest_types.get("GhosttyRenderStateData").?.object; + try std.testing.expectEqualStrings("enum", data.get("kind").?.string); + try std.testing.expectEqual(@as(i64, @intFromEnum(render.Data.dirty)), data.get("values").?.object.get("DIRTY").?.integer); + + const colors = manifest_types.get("GhosttyRenderStateColors").?.object; + const palette = colors.get("fields").?.object.get("palette").?.object; + try std.testing.expectEqualStrings("array", palette.get("type").?.string); + try std.testing.expectEqualStrings("GhosttyColorRgb", palette.get("elem").?.string); + try std.testing.expectEqual(@as(i64, 256), palette.get("count").?.integer); + + const style_color = manifest_types.get("GhosttyStyleColor").?.object; + const value = style_color.get("fields").?.object.get("value").?.object; + try std.testing.expectEqualStrings("GhosttyStyleColorValue", value.get("type").?.string); + try std.testing.expectEqualStrings("tag", value.get("tag").?.string); + try std.testing.expectEqualStrings("palette", value.get("arms").?.object.get("PALETTE").?.string); + try std.testing.expect(value.get("arms").?.object.get("NONE").? == .null); +} + +test "manifest uses public enum names" { + const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); + defer parsed.deinit(); + const manifest_types = parsed.value.object.get("types").?.object; + + const cell_content = manifest_types.get("GhosttyCellContentTag").?.object.get("values").?.object; + try std.testing.expect(cell_content.contains("TAG_MAX_VALUE")); + try std.testing.expect(!cell_content.contains("MAX_VALUE")); + + const sgr_values = manifest_types.get("GhosttySgrAttributeTag").?.object.get("values").?.object; + try std.testing.expectEqual(@as(i64, 9), sgr_values.get("UNDERLINE_COLOR_256").?.integer); + try std.testing.expectEqual(@as(i64, 23), sgr_values.get("BG_8").?.integer); + try std.testing.expect(!sgr_values.contains("256_UNDERLINE_COLOR")); + + const sgr_arms = manifest_types.get("GhosttySgrAttribute").?.object + .get("fields").?.object.get("value").?.object.get("arms").?.object; + try std.testing.expectEqualStrings("underline_color_256", sgr_arms.get("UNDERLINE_COLOR_256").?.string); + try std.testing.expectEqualStrings("bg_8", sgr_arms.get("BG_8").?.string); + + const terminal_values = manifest_types.get("GhosttyTerminalOption").?.object.get("values").?.object; + try std.testing.expectEqual(@as(i64, 6), terminal_values.get("SIZE").?.integer); + try std.testing.expect(!terminal_values.contains("SIZE_CB")); + + const osc_values = manifest_types.get("GhosttyOscCommandType").?.object.get("values").?.object; + try std.testing.expectEqual(@as(i64, 22), osc_values.get("KITTY_TEXT_SIZING").?.integer); + try std.testing.expectEqual(@as(i64, 23), osc_values.get("KITTY_CLIPBOARD_PROTOCOL").?.integer); + try std.testing.expectEqual(@as(i64, 24), osc_values.get("KITTY_DND_PROTOCOL").?.integer); + try std.testing.expectEqual(@as(i64, 25), osc_values.get("CONTEXT_SIGNAL").?.integer); + try std.testing.expect(osc_values.contains("TYPE_MAX_VALUE")); + try std.testing.expect(!osc_values.contains("MAX_VALUE")); + + const key_values = manifest_types.get("GhosttyKey").?.object.get("values").?.object; + try std.testing.expect(key_values.contains("A")); + try std.testing.expect(!key_values.contains("KEY_A")); + + const mode_values = manifest_types.get("GhosttyModeReportState").?.object.get("values").?.object; + try std.testing.expectEqual(@as(i64, 0), mode_values.get("NOT_RECOGNIZED").?.integer); + try std.testing.expectEqual(@as(i64, 4), mode_values.get("PERMANENTLY_RESET").?.integer); +} + +test "manifest named references resolve" { + const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); + defer parsed.deinit(); + const manifest_types = parsed.value.object.get("types").?.object; + + var type_iterator = manifest_types.iterator(); + while (type_iterator.next()) |type_entry| { + const descriptor = type_entry.value_ptr.object; + const fields_value = descriptor.get("fields") orelse continue; + var field_iterator = fields_value.object.iterator(); + while (field_iterator.next()) |field_entry| { + const field = field_entry.value_ptr.object; + const field_type = field.get("type").?.string; + if (!Json.isBuiltinType(field_type)) + try std.testing.expect(manifest_types.contains(field_type)); + + if (field.get("elem")) |elem_value| { + const elem = elem_value.string; + if (!Json.isBuiltinType(elem)) + try std.testing.expect(manifest_types.contains(elem)); + } + + if (field.get("arms")) |arms_value| { + const union_descriptor = manifest_types.get(field_type).?.object; + const union_fields = union_descriptor.get("fields").?.object; + const tag_field_name = field.get("tag").?.string; + const tag_type_name = fields_value.object.get(tag_field_name).?.object.get("type").?.string; + const tag_values = manifest_types.get(tag_type_name).?.object.get("values").?.object; + var arm_iterator = arms_value.object.iterator(); + while (arm_iterator.next()) |arm_entry| { + try std.testing.expect(tag_values.contains(arm_entry.key_ptr.*)); + if (arm_entry.value_ptr.* == .null) continue; + try std.testing.expect(union_fields.contains(arm_entry.value_ptr.string)); + } + } + } } } diff --git a/src/terminal/point.zig b/src/terminal/point.zig index f6bf25c39..5937576cf 100644 --- a/src/terminal/point.zig +++ b/src/terminal/point.zig @@ -71,7 +71,15 @@ pub const Point = union(Tag) { @This(), // Padding: largest variant is Coordinate (u16 + u32 = 6 bytes). // Use [2]u64 (16 bytes) for future expansion. - [2]u64, + .{ + .padding = [2]u64, + .field_renames = .{ + .active = "coordinate", + .viewport = "coordinate", + .screen = "coordinate", + .history = "coordinate", + }, + }, ); pub const C = c_union.C; pub const CValue = c_union.CValue; diff --git a/src/terminal/sgr.zig b/src/terminal/sgr.zig index e9b9354c3..3fecd4469 100644 --- a/src/terminal/sgr.zig +++ b/src/terminal/sgr.zig @@ -161,7 +161,18 @@ pub const Attribute = union(Tag) { // Largest variant is Unknown.C: 2 pointers + 2 usize = 32 bytes on 64-bit. // We use [8]u64 (64 bytes) to allow room for future expansion while // maintaining ABI compatibility. - [8]u64, + .{ + .padding = [8]u64, + .field_renames = .{ + .@"256_underline_color" = "underline_color_256", + .@"8_bg" = "bg_8", + .@"8_fg" = "fg_8", + .@"8_bright_bg" = "bright_bg_8", + .@"8_bright_fg" = "bright_fg_8", + .@"256_bg" = "bg_256", + .@"256_fg" = "fg_256", + }, + }, ); pub const Value = c_union.Value; pub const C = c_union.C; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index afbfa719c..105cd1e4f 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -236,7 +236,7 @@ pub const Action = union(Key) { @This(), // TODO: Before shipping an ABI-compatible libghostty, verify this. // This was just arbitrarily chosen for now. - [16]u64, + .{ .padding = [16]u64 }, ); pub const Tag = c_union.Tag; pub const Value = c_union.Value;