diff --git a/include/ghostty/vt/render.h b/include/ghostty/vt/render.h index 728966f48..f6b5e2222 100644 --- a/include/ghostty/vt/render.h +++ b/include/ghostty/vt/render.h @@ -256,9 +256,12 @@ typedef enum GHOSTTY_ENUM_TYPED { * * This is the bulk alternative to iterating cells one at a time. * It lets callers with expensive call boundaries (e.g. WebAssembly - * embedders) read an entire row with a single call, then drill - * into the cells iterator only for cells that need managed data - * (styles, graphemes). */ + * embedders) read an entire row with a single call. + * + * Bit positions aren't protected by ABI, so callers should parse them + * out of the manifest from `ghostty_type_json`. Callers with access + * to the C header or without high FFI costs should use `ghostty_cell_get`. + */ GHOSTTY_RENDER_STATE_ROW_DATA_CELLS_RAW = 5, GHOSTTY_RENDER_STATE_ROW_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyRenderStateRowData; diff --git a/include/ghostty/vt/screen.h b/include/ghostty/vt/screen.h index 0d009a781..2899989dd 100644 --- a/include/ghostty/vt/screen.h +++ b/include/ghostty/vt/screen.h @@ -21,19 +21,21 @@ extern "C" { * Terminal screen cell and row types. * * These types represent the contents of a terminal screen. A GhosttyCell - * is a single grid cell and a GhosttyRow is a single row. Both are opaque - * values whose fields are accessed via ghostty_cell_get() and - * ghostty_row_get() respectively. + * is a single grid cell and a GhosttyRow is a single row. Cell fields can + * be accessed via ghostty_cell_get() or decoded from the packed layout in + * ghostty_type_json(). Rows are opaque and accessed via ghostty_row_get(). * * @{ */ /** - * Opaque cell value. + * Packed cell value. * - * Represents a single terminal cell. The internal layout is opaque and - * must be queried via ghostty_cell_get(). Obtain cell values from - * terminal query APIs. + * Represents a single terminal cell. Portable callers can query fields via + * ghostty_cell_get(). Boundary-sensitive callers can decode the packed value + * using the GhosttyCell descriptor returned by ghostty_type_json(). The + * manifest is authoritative for the linked build; hardcoding bit positions + * is unsupported. * * @ingroup screen */ @@ -55,7 +57,8 @@ typedef uint64_t GhosttyRow; * * The memory is not owned by this struct. The pointer is only valid * for the lifetime documented by the API that produces it. Each value - * is queried via ghostty_cell_get() like any other GhosttyCell. + * can be queried via ghostty_cell_get() or decoded using the GhosttyCell + * packed descriptor returned by ghostty_type_json(). * * @ingroup screen */ diff --git a/include/ghostty/vt/types.h b/include/ghostty/vt/types.h index 787cc0148..a0621945e 100644 --- a/include/ghostty/vt/types.h +++ b/include/ghostty/vt/types.h @@ -342,6 +342,13 @@ typedef struct { * this manifest rather than hardcoding them. Consumers should reject unknown * schema versions and verify the descriptors they require at initialization. * + * Packed type descriptors define fields using `lsb` and `width`. `lsb` is + * relative to bit zero of the containing numerical value; for nested packed + * layouts it is relative to the immediate containing field. Tagged packed + * unions select an inline arm layout using the named tag field. These layouts + * describe the current linked build and are not a cross-version stability + * promise. + * * The formal format is defined by the * libghostty-vt ABI manifest JSON Schema. * diff --git a/src/lib/main.zig b/src/lib/main.zig index 45cdb71d4..573007bd9 100644 --- a/src/lib/main.zig +++ b/src/lib/main.zig @@ -1,5 +1,6 @@ const std = @import("std"); const enumpkg = @import("enum.zig"); +const packedpkg = @import("packed.zig"); const structpkg = @import("struct.zig"); const types = @import("types.zig"); const unionpkg = @import("union.zig"); @@ -8,6 +9,8 @@ pub const allocator = @import("allocator.zig"); pub const TinyIo = @import("TinyIo.zig"); pub const Buffer = types.Buffer; pub const Enum = enumpkg.Enum; +pub const Packed = packedpkg.Packed; +pub const PackedTaggedUnion = packedpkg.PackedTaggedUnion; pub const checkGhosttyHEnum = enumpkg.checkGhosttyHEnum; pub const String = types.String; pub const Struct = structpkg.Struct; diff --git a/src/lib/packed.zig b/src/lib/packed.zig new file mode 100644 index 000000000..b210242d8 --- /dev/null +++ b/src/lib/packed.zig @@ -0,0 +1,290 @@ +const std = @import("std"); +const testing = std.testing; + +/// Metadata for a field in a packed layout. +pub const FieldOptions = struct { + /// Public name for this field. The Zig field name is used by default. + name: ?[]const u8 = null, + + /// Public type name for scalar fields. The Zig type is used by default. + type_name: ?[]const u8 = null, + + /// Omit this field from public metadata. This is intended for padding. + omit: bool = false, + + /// How the field is represented in public metadata. + encoding: Encoding = .scalar, + + pub const Encoding = union(enum) { + scalar, + @"packed": type, + tagged_union: type, + }; +}; + +/// Create metadata for a packed struct. Physical layout information is always +/// reflected from `T`; options only provide public names and relationships. +pub fn Packed( + comptime T: type, + comptime options: PackedOptions(T), +) type { + const info = packedStructInfo(T); + const FieldT = std.meta.FieldEnum(T); + + return struct { + pub const Zig = T; + + // I know this is an insane amount of validation but getting + // this correct is critical to C APIs working so we go overboard. + comptime { + for (info.fields, 0..) |field, i| { + const field_options = @field(options.fields, field.name); + if (field_options.omit) { + if (field_options.name != null or + field_options.type_name != null or + field_options.encoding != .scalar) + { + @compileError("omitted packed field has other options: " ++ field.name); + } + continue; + } + + if (field_options.name) |name| { + if (name.len == 0) + @compileError("packed field name cannot be empty: " ++ field.name); + } + + switch (field_options.encoding) { + .scalar => switch (@typeInfo(field.type)) { + .bool, .int, .@"enum" => {}, + else => @compileError("packed field requires an explicit encoding: " ++ field.name), + }, + .@"packed" => |Layout| { + if (Layout.Zig != field.type) + @compileError("nested packed layout has the wrong Zig type: " ++ field.name); + if (field_options.type_name != null) + @compileError("nested packed field cannot have a scalar type name: " ++ field.name); + }, + .tagged_union => |Layout| { + if (Layout.Owner != T or Layout.Union != field.type or + Layout.union_field != @field(FieldT, field.name)) + { + @compileError("tagged union layout does not match packed field: " ++ field.name); + } + if (field_options.type_name != null) + @compileError("tagged union field cannot have a scalar type name: " ++ field.name); + }, + } + + const public_name = field_options.name orelse field.name; + for (info.fields[0..i]) |previous| { + const previous_options = @field(options.fields, previous.name); + if (previous_options.omit) continue; + const previous_name = previous_options.name orelse previous.name; + if (std.mem.eql(u8, public_name, previous_name)) + @compileError("duplicate public packed field name: " ++ public_name); + } + } + } + + pub const Backing = info.backing_integer.?; + pub const Field = std.meta.FieldEnum(T); + + pub fn fieldOptions(comptime field: Field) FieldOptions { + return @field(options.fields, @tagName(field)); + } + + pub fn fieldName(comptime field: Field) ?[]const u8 { + const field_options = fieldOptions(field); + if (field_options.omit) return null; + return field_options.name orelse @tagName(field); + } + + pub fn bitOffset(comptime field: Field) usize { + return @bitOffsetOf(T, @tagName(field)); + } + + pub fn bitWidth(comptime field: Field) usize { + return @bitSizeOf(@FieldType(T, @tagName(field))); + } + }; +} + +/// Options for a packed struct. A field is generated for every field in `T` +/// so unknown field names are rejected by normal Zig type checking. +pub fn PackedOptions(comptime T: type) type { + const fields = packedStructInfo(T).fields; + const default_options: FieldOptions = .{}; + + var names: [fields.len][]const u8 = undefined; + var types: [fields.len]type = undefined; + var attrs: [fields.len]std.builtin.Type.StructField.Attributes = undefined; + + for (fields, 0..) |field, i| { + names[i] = field.name; + types[i] = FieldOptions; + attrs[i] = .{ .default_value_ptr = &default_options }; + } + + const Fields = @Struct(.auto, null, &names, &types, &attrs); + return struct { + fields: Fields = .{}, + }; +} + +/// Describe a packed union field whose active arm is selected by another +/// field in the containing packed struct. Each tag maps to a reflected packed +/// arm layout. Multiple tags may map to the same source union field. +pub fn PackedTaggedUnion( + comptime OwnerT: type, + comptime union_field_value: std.meta.FieldEnum(OwnerT), + comptime tag_field_value: std.meta.FieldEnum(OwnerT), + comptime options: PackedTaggedUnionOptions( + @FieldType(OwnerT, @tagName(union_field_value)), + @FieldType(OwnerT, @tagName(tag_field_value)), + ), +) type { + _ = packedStructInfo(OwnerT); + const UnionT = @FieldType(OwnerT, @tagName(union_field_value)); + const union_info = @typeInfo(UnionT).@"union"; + if (union_info.layout != .@"packed") + @compileError("packed tagged union value must be a packed union"); + + const TagT = @FieldType(OwnerT, @tagName(tag_field_value)); + const tag_info = @typeInfo(TagT).@"enum"; + const ArmT = PackedTaggedUnionArm(UnionT); + + return struct { + pub const Owner = OwnerT; + pub const Union = UnionT; + pub const Tag = TagT; + pub const Arm = ArmT; + pub const union_field = union_field_value; + pub const tag_field = tag_field_value; + + comptime { + for (tag_info.fields) |tag| { + const arm_value = @field(options.arms, tag.name) orelse continue; + switch (arm_value) { + inline else => |Layout, source| { + const source_name = @tagName(source); + if (Layout.Zig != @FieldType(UnionT, source_name)) + @compileError("packed union arm layout has the wrong Zig type for tag: " ++ tag.name); + if (@bitSizeOf(Layout.Zig) > @bitSizeOf(UnionT)) + @compileError("packed union arm is wider than its union: " ++ tag.name); + }, + } + } + } + + pub fn arm(comptime tag: Tag) ?Arm { + return @field(options.arms, @tagName(tag)); + } + }; +} + +/// Options for mapping tag values to packed union arms. +pub fn PackedTaggedUnionOptions(comptime Union: type, comptime Tag: type) type { + const union_info = @typeInfo(Union).@"union"; + if (union_info.layout != .@"packed") + @compileError("packed tagged union value must be a packed union"); + const tag_fields = @typeInfo(Tag).@"enum".fields; + const Arm = PackedTaggedUnionArm(Union); + const default_arm: ?Arm = 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] = ?Arm; + attrs[i] = .{ .default_value_ptr = &default_arm }; + } + + const Arms = @Struct(.auto, null, &names, &types, &attrs); + return struct { + arms: Arms = .{}, + }; +} + +fn PackedTaggedUnionArm(comptime Union: type) type { + const fields = @typeInfo(Union).@"union".fields; + const Tag = std.meta.FieldEnum(Union); + + var names: [fields.len][]const u8 = undefined; + var types: [fields.len]type = undefined; + var attrs: [fields.len]std.builtin.Type.UnionField.Attributes = undefined; + + for (fields, 0..) |field, i| { + names[i] = field.name; + types[i] = type; + attrs[i] = .{ .@"align" = 1 }; + } + + return @Union(.auto, Tag, &names, &types, &attrs); +} + +fn packedStructInfo(comptime T: type) std.builtin.Type.Struct { + const info = @typeInfo(T).@"struct"; + if (info.layout != .@"packed") + @compileError("Packed requires a packed struct"); + if (info.backing_integer == null) + @compileError("Packed requires an integer-backed packed struct"); + return info; +} + +test "Packed: reflected fields and nested tagged union" { + const Tag = enum(u2) { first, second, third }; + const Value = packed union { + number: packed struct(u4) { + data: u3, + _pad: u1, + }, + flags: packed struct(u4) { + a: bool, + b: bool, + _pad: u2, + }, + }; + const Nested = packed struct(u1) { enabled: bool }; + const Subject = packed struct(u8) { + tag: Tag, + value: Value, + nested: Nested, + _padding: u1, + }; + + const Number = Packed(@FieldType(Value, "number"), .{ .fields = .{ + .data = .{ .name = "number" }, + ._pad = .{ .omit = true }, + } }); + const Flags = Packed(@FieldType(Value, "flags"), .{ .fields = .{ + ._pad = .{ .omit = true }, + } }); + const NestedLayout = Packed(Nested, .{}); + const ValueLayout = PackedTaggedUnion(Subject, .value, .tag, .{ .arms = .{ + .first = .{ .number = Number }, + .second = .{ .number = Number }, + .third = .{ .flags = Flags }, + } }); + const Layout = Packed(Subject, .{ .fields = .{ + .value = .{ .encoding = .{ .tagged_union = ValueLayout } }, + .nested = .{ .encoding = .{ .@"packed" = NestedLayout } }, + ._padding = .{ .omit = true }, + } }); + + try testing.expectEqual(@bitOffsetOf(Subject, "value"), Layout.bitOffset(.value)); + try testing.expectEqual(@bitSizeOf(Value), Layout.bitWidth(.value)); + try testing.expectEqualStrings("number", Number.fieldName(.data).?); + try testing.expect(Number.fieldName(._pad) == null); + switch (ValueLayout.arm(.first).?) { + .number => |ArmLayout| try testing.expect(ArmLayout == Number), + else => return error.TestUnexpectedResult, + } + switch (ValueLayout.arm(.second).?) { + .number => |ArmLayout| try testing.expect(ArmLayout == Number), + else => return error.TestUnexpectedResult, + } + try testing.expectEqual(@as(usize, 1), NestedLayout.bitWidth(.enabled)); +} diff --git a/src/terminal/c/types.schema.json b/src/terminal/c/types.schema.json index 7020cb172..7d784153b 100644 --- a/src/terminal/c/types.schema.json +++ b/src/terminal/c/types.schema.json @@ -45,7 +45,11 @@ }, "typeReference": { "type": "string", - "pattern": "^(Ghostty[A-Za-z0-9_]+|bool|f32|f64|function|i8|i16|i32|i64|opaque|u8|u16|u32|u64|void)$" + "pattern": "^(Ghostty[A-Za-z0-9_]+|bool|f32|f64|function|[iu][1-9][0-9]*|opaque|void)$" + }, + "integerTypeReference": { + "type": "string", + "pattern": "^[iu][1-9][0-9]*$" }, "abi": { "type": "object", @@ -208,6 +212,129 @@ "$ref": "#/$defs/field" } }, + "bitBase": { + "type": "object", + "required": ["lsb", "width"], + "properties": { + "lsb": { + "description": "Least-significant bit relative to the containing numerical value.", + "$ref": "#/$defs/nonNegativeInteger" + }, + "width": { + "description": "Physical width of this field in bits.", + "$ref": "#/$defs/positiveInteger" + } + } + }, + "scalarBit": { + "allOf": [ + { + "$ref": "#/$defs/bitBase" + }, + { + "required": ["type"], + "properties": { + "type": { + "$ref": "#/$defs/typeReference" + } + } + } + ], + "unevaluatedProperties": false + }, + "packedBit": { + "allOf": [ + { + "$ref": "#/$defs/bitBase" + }, + { + "required": ["kind", "bits"], + "properties": { + "kind": { + "const": "packed" + }, + "bits": { + "$ref": "#/$defs/bits" + } + } + } + ], + "unevaluatedProperties": false + }, + "packedArm": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "width", "bits"], + "properties": { + "kind": { + "const": "packed" + }, + "width": { + "description": "Physical width of this inline arm in bits.", + "$ref": "#/$defs/positiveInteger" + }, + "bits": { + "$ref": "#/$defs/bits" + } + } + }, + "taggedUnionBit": { + "allOf": [ + { + "$ref": "#/$defs/bitBase" + }, + { + "required": ["kind", "tag", "arms"], + "properties": { + "kind": { + "const": "union" + }, + "tag": { + "type": "string", + "minLength": 1 + }, + "arms": { + "type": "object", + "minProperties": 1, + "propertyNames": { + "pattern": "^[A-Z][A-Z0-9_]*$" + }, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/$defs/packedArm" + }, + { + "type": "null" + } + ] + } + } + } + } + ], + "unevaluatedProperties": false + }, + "bit": { + "oneOf": [ + { + "$ref": "#/$defs/scalarBit" + }, + { + "$ref": "#/$defs/packedBit" + }, + { + "$ref": "#/$defs/taggedUnionBit" + } + ] + }, + "bits": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/$defs/bit" + } + }, "descriptorBase": { "type": "object", "required": ["kind", "size", "align"], @@ -291,6 +418,29 @@ ], "unevaluatedProperties": false }, + "packedDescriptor": { + "allOf": [ + { + "$ref": "#/$defs/descriptorBase" + }, + { + "required": ["underlying", "bits"], + "properties": { + "kind": { + "const": "packed" + }, + "underlying": { + "description": "Integer backing type used to interpret the packed value.", + "$ref": "#/$defs/integerTypeReference" + }, + "bits": { + "$ref": "#/$defs/bits" + } + } + } + ], + "unevaluatedProperties": false + }, "aliasDescriptor": { "allOf": [ { @@ -336,6 +486,9 @@ { "$ref": "#/$defs/enumDescriptor" }, + { + "$ref": "#/$defs/packedDescriptor" + }, { "$ref": "#/$defs/aliasDescriptor" }, diff --git a/src/terminal/c/types.zig b/src/terminal/c/types.zig index 2242b1d50..578900a2b 100644 --- a/src/terminal/c/types.zig +++ b/src/terminal/c/types.zig @@ -16,6 +16,7 @@ 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 page = @import("../page.zig"); const point = @import("../point.zig"); const Selection = @import("../Selection.zig"); const sgr = @import("../sgr.zig"); @@ -72,6 +73,7 @@ const TypeDecl = struct { @"struct", @"union", @"enum", + @"packed", alias, @"opaque", }; @@ -146,6 +148,10 @@ const TypeDecl = struct { }; } + fn initPacked(comptime name: []const u8, comptime T: type) TypeDecl { + return .{ .name = name, .T = T, .kind = .@"packed" }; + } + fn initAlias( comptime name: []const u8, comptime T: type, @@ -297,7 +303,7 @@ const type_decls = [_]TypeDecl{ .initEnum("GhosttyTerminalScrollViewportTag", terminal.ZigTerminal.ScrollViewport.Tag, "GHOSTTY_SCROLL_VIEWPORT_"), .initEnum("GhosttyTerminalUnknownSequenceTag", terminal.UnknownSequence.Tag, "GHOSTTY_TERMINAL_UNKNOWN_SEQUENCE_"), - .initAlias("GhosttyCell", u64, "u64"), + .initPacked("GhosttyCell", page.Cell.CLayout), .initAlias("GhosttyColorPaletteIndex", u8, "u8"), .initAlias("GhosttyKittyKeyFlags", u8, "u8"), .initAlias("GhosttyMode", u16, "u16"), @@ -438,6 +444,7 @@ const Json = struct { try jws.write(std.math.maxInt(c_int)); try jws.endObject(); }, + .@"packed" => try writePackedType(decl.T, jws), .alias => { try writeSizeAlign(decl.T, jws); try jws.objectField("type"); @@ -448,6 +455,98 @@ const Json = struct { try jws.endObject(); } + fn writePackedType( + comptime Layout: type, + jws: *std.json.Stringify, + ) std.Io.Writer.Error!void { + try writeSizeAlign(Layout.Zig, jws); + try jws.objectField("underlying"); + try jws.write(publicTypeName(Layout.Backing)); + try jws.objectField("bits"); + try jws.beginObject(); + try writePackedBits(Layout, jws); + try jws.endObject(); + } + + fn writePackedBits( + comptime Layout: type, + jws: *std.json.Stringify, + ) std.Io.Writer.Error!void { + inline for (@typeInfo(Layout.Zig).@"struct".fields) |field| { + const field_tag = @field(Layout.Field, field.name); + const name = comptime Layout.fieldName(field_tag) orelse continue; + const options = Layout.fieldOptions(field_tag); + + try jws.objectField(name); + try jws.beginObject(); + try jws.objectField("lsb"); + try jws.write(Layout.bitOffset(field_tag)); + try jws.objectField("width"); + try jws.write(Layout.bitWidth(field_tag)); + + switch (options.encoding) { + .scalar => { + try jws.objectField("type"); + try jws.write(options.type_name orelse publicTypeName(field.type)); + }, + .@"packed" => |Nested| { + try jws.objectField("kind"); + try jws.write("packed"); + try jws.objectField("bits"); + try jws.beginObject(); + try writePackedBits(Nested, jws); + try jws.endObject(); + }, + .tagged_union => |UnionLayout| try writePackedTaggedUnion(Layout, UnionLayout, jws), + } + try jws.endObject(); + } + } + + fn writePackedTaggedUnion( + comptime Layout: type, + comptime UnionLayout: type, + jws: *std.json.Stringify, + ) std.Io.Writer.Error!void { + try jws.objectField("kind"); + try jws.write("union"); + try jws.objectField("tag"); + try jws.write(Layout.fieldName(UnionLayout.tag_field).?); + try jws.objectField("arms"); + try jws.beginObject(); + + const tag_options = Layout.fieldOptions(UnionLayout.tag_field); + const tag_type_name = tag_options.type_name orelse publicTypeName(UnionLayout.Tag); + inline for (@typeInfo(UnionLayout.Tag).@"enum".fields) |tag| { + try writeEnumObjectField(tag_type_name, tag.name, jws); + const arm = UnionLayout.arm(@field(UnionLayout.Tag, tag.name)) orelse { + try jws.write(null); + continue; + }; + switch (arm) { + inline else => |ArmLayout| try writePackedArm(ArmLayout, jws), + } + } + + try jws.endObject(); + } + + fn writePackedArm( + comptime Layout: type, + jws: *std.json.Stringify, + ) std.Io.Writer.Error!void { + try jws.beginObject(); + try jws.objectField("kind"); + try jws.write("packed"); + try jws.objectField("width"); + try jws.write(@bitSizeOf(Layout.Zig)); + try jws.objectField("bits"); + try jws.beginObject(); + try writePackedBits(Layout, jws); + try jws.endObject(); + try jws.endObject(); + } + fn writeField( comptime decl: TypeDecl, comptime name: []const u8, @@ -653,7 +752,7 @@ const Json = struct { 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" => {}, + .@"packed", .alias, .@"opaque" => {}, }; return switch (@typeInfo(T)) { .bool => "bool", @@ -681,14 +780,14 @@ const Json = struct { 16 => "i16", 32 => "i32", 64 => "i64", - else => "opaque", + else => std.fmt.comptimePrint("i{d}", .{bits}), }, .unsigned => switch (bits) { 8 => "u8", 16 => "u16", 32 => "u32", 64 => "u64", - else => "opaque", + else => std.fmt.comptimePrint("u{d}", .{bits}), }, }; } @@ -728,6 +827,12 @@ const Json = struct { for (builtins) |builtin_name| { if (std.mem.eql(u8, name, builtin_name)) return true; } + if (name.len > 1 and (name[0] == 'i' or name[0] == 'u') and + name[1] >= '1' and name[1] <= '9') + { + for (name[2..]) |c| if (!std.ascii.isDigit(c)) return false; + return true; + } return false; } }; @@ -779,6 +884,87 @@ test "manifest describes enums, arrays, and tagged unions" { try std.testing.expect(value.get("arms").?.object.get("NONE").? == .null); } +test "manifest describes the complete packed cell layout" { + 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 descriptor = manifest_types.get("GhosttyCell").?.object; + + try std.testing.expectEqualStrings("packed", descriptor.get("kind").?.string); + try std.testing.expectEqual(@as(i64, @sizeOf(page.Cell.CLayout.Zig)), descriptor.get("size").?.integer); + try std.testing.expectEqualStrings("u64", descriptor.get("underlying").?.string); + + const bits = descriptor.get("bits").?.object; + inline for (@typeInfo(page.Cell.CLayout.Zig).@"struct".fields) |field| { + const field_tag = @field(page.Cell.CLayout.Field, field.name); + const name = comptime page.Cell.CLayout.fieldName(field_tag); + if (name) |public_name| { + const bit = bits.get(public_name).?.object; + try std.testing.expectEqual( + @as(i64, @intCast(@bitOffsetOf(page.Cell.CLayout.Zig, field.name))), + bit.get("lsb").?.integer, + ); + try std.testing.expectEqual( + @as(i64, @intCast(@bitSizeOf(field.type))), + bit.get("width").?.integer, + ); + } else { + try std.testing.expect(!bits.contains(field.name)); + } + } + + const content = bits.get("content").?.object; + try std.testing.expectEqualStrings("union", content.get("kind").?.string); + try std.testing.expectEqualStrings("content_tag", content.get("tag").?.string); + const arms = content.get("arms").?.object; + + const Content = @FieldType(page.Cell.CLayout.Zig, "content"); + const Codepoint = @FieldType(Content, "codepoint"); + const codepoint = arms.get("CODEPOINT").?.object; + const grapheme = arms.get("CODEPOINT_GRAPHEME").?.object; + try expectPackedArmField(codepoint, "codepoint", Codepoint, "data", "u21"); + try expectPackedArmField(grapheme, "codepoint", Codepoint, "data", "u21"); + try expectPackedArmField( + arms.get("BG_COLOR_PALETTE").?.object, + "index", + @FieldType(Content, "color_palette"), + "data", + "GhosttyColorPaletteIndex", + ); + + const rgb = arms.get("BG_COLOR_RGB").?.object; + const Rgb = @FieldType(Content, "color_rgb"); + inline for (@typeInfo(Rgb).@"struct".fields) |field| + try expectPackedArmField(rgb, field.name, Rgb, field.name, "u8"); +} + +test "manifest packed cell layouts decode real values" { + 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_bits = manifest_types.get("GhosttyCell").?.object.get("bits").?.object; + + var codepoint = page.Cell.CLayout.Zig.init('A'); + try expectDecodedCellContent(manifest_types, cell_bits, @bitCast(codepoint), "codepoint", 'A'); + + codepoint.content_tag = .codepoint_grapheme; + try expectDecodedCellContent(manifest_types, cell_bits, @bitCast(codepoint), "codepoint", 'A'); + + var palette: page.Cell.CLayout.Zig = @bitCast(@as(u64, 0)); + palette.content_tag = .bg_color_palette; + palette.content = .{ .color_palette = .{ .data = 173 } }; + try expectDecodedCellContent(manifest_types, cell_bits, @bitCast(palette), "index", 173); + + var rgb: page.Cell.CLayout.Zig = @bitCast(@as(u64, 0)); + rgb.content_tag = .bg_color_rgb; + rgb.content = .{ .color_rgb = .{ .r = 0x12, .g = 0x34, .b = 0x56 } }; + const rgb_arm = activeCellContentArm(manifest_types, cell_bits, @bitCast(rgb)); + const content = extractManifestBits(@bitCast(rgb), cell_bits.get("content").?.object); + try std.testing.expectEqual(@as(u64, 0x12), extractManifestBits(content, rgb_arm.get("bits").?.object.get("r").?.object)); + try std.testing.expectEqual(@as(u64, 0x34), extractManifestBits(content, rgb_arm.get("bits").?.object.get("g").?.object)); + try std.testing.expectEqual(@as(u64, 0x56), extractManifestBits(content, rgb_arm.get("bits").?.object.get("b").?.object)); +} + test "manifest uses public enum names" { const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, json, .{}); defer parsed.deinit(); @@ -827,6 +1013,13 @@ test "manifest named references resolve" { var type_iterator = manifest_types.iterator(); while (type_iterator.next()) |type_entry| { const descriptor = type_entry.value_ptr.object; + if (descriptor.get("bits")) |bits_value| { + try expectManifestBitsValid( + manifest_types, + bits_value.object, + @intCast(descriptor.get("size").?.integer * 8), + ); + } const fields_value = descriptor.get("fields") orelse continue; var field_iterator = fields_value.object.iterator(); while (field_iterator.next()) |field_entry| { @@ -857,3 +1050,102 @@ test "manifest named references resolve" { } } } + +fn expectPackedArmField( + arm: std.json.ObjectMap, + manifest_name: []const u8, + comptime T: type, + comptime zig_name: []const u8, + expected_type: []const u8, +) !void { + try std.testing.expectEqualStrings("packed", arm.get("kind").?.string); + try std.testing.expectEqual(@as(i64, @bitSizeOf(T)), arm.get("width").?.integer); + const bit = arm.get("bits").?.object.get(manifest_name).?.object; + try std.testing.expectEqual(@as(i64, @bitOffsetOf(T, zig_name)), bit.get("lsb").?.integer); + try std.testing.expectEqual(@as(i64, @bitSizeOf(@FieldType(T, zig_name))), bit.get("width").?.integer); + try std.testing.expectEqualStrings(expected_type, bit.get("type").?.string); +} + +fn extractManifestBits(value: u64, bit: std.json.ObjectMap) u64 { + const lsb: u6 = @intCast(bit.get("lsb").?.integer); + const width: u7 = @intCast(bit.get("width").?.integer); + const mask = if (width == 64) + std.math.maxInt(u64) + else + (@as(u64, 1) << @intCast(width)) - 1; + return (value >> lsb) & mask; +} + +fn activeCellContentArm( + manifest_types: std.json.ObjectMap, + cell_bits: std.json.ObjectMap, + raw: u64, +) std.json.ObjectMap { + const content_tag = cell_bits.get("content_tag").?.object; + const value = extractManifestBits(raw, content_tag); + const enum_values = manifest_types.get(content_tag.get("type").?.string).?.object + .get("values").?.object; + var iterator = enum_values.iterator(); + while (iterator.next()) |entry| { + if (entry.value_ptr.integer == value) + return cell_bits.get("content").?.object.get("arms").?.object + .get(entry.key_ptr.*).?.object; + } + unreachable; +} + +fn expectDecodedCellContent( + manifest_types: std.json.ObjectMap, + cell_bits: std.json.ObjectMap, + raw: u64, + field_name: []const u8, + expected: u64, +) !void { + const arm = activeCellContentArm(manifest_types, cell_bits, raw); + const content = extractManifestBits(raw, cell_bits.get("content").?.object); + try std.testing.expectEqual( + expected, + extractManifestBits(content, arm.get("bits").?.object.get(field_name).?.object), + ); +} + +fn expectManifestBitsValid( + manifest_types: std.json.ObjectMap, + bits: std.json.ObjectMap, + container_width: usize, +) !void { + var iterator = bits.iterator(); + while (iterator.next()) |entry| { + const bit = entry.value_ptr.object; + const lsb: usize = @intCast(bit.get("lsb").?.integer); + const width: usize = @intCast(bit.get("width").?.integer); + try std.testing.expect(lsb + width <= container_width); + + if (bit.get("type")) |type_value| { + const type_name = type_value.string; + try std.testing.expect(Json.isBuiltinType(type_name) or manifest_types.contains(type_name)); + continue; + } + + const kind = bit.get("kind").?.string; + if (std.mem.eql(u8, kind, "packed")) { + try expectManifestBitsValid(manifest_types, bit.get("bits").?.object, width); + continue; + } + + try std.testing.expectEqualStrings("union", kind); + const tag_name = bit.get("tag").?.string; + const tag = bits.get(tag_name).?.object; + const enum_values = manifest_types.get(tag.get("type").?.string).?.object + .get("values").?.object; + var arm_iterator = bit.get("arms").?.object.iterator(); + while (arm_iterator.next()) |arm_entry| { + try std.testing.expect(enum_values.contains(arm_entry.key_ptr.*)); + if (arm_entry.value_ptr.* == .null) continue; + const arm = arm_entry.value_ptr.object; + const arm_width: usize = @intCast(arm.get("width").?.integer); + try std.testing.expect(arm_width <= width); + try expectManifestBitsValid(manifest_types, arm.get("bits").?.object, arm_width); + } + } +} diff --git a/src/terminal/page.zig b/src/terminal/page.zig index c2814ae98..d19860697 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -1,6 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const build_options = @import("terminal_options"); +const lib = @import("../lib/main.zig"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const assert = @import("../quirks.zig").inlineAssert; @@ -2144,6 +2145,51 @@ pub const Cell = packed struct(u64) { prompt = 2, }; + /// Metadata for the C representation. All physical bit offsets and + /// widths are reflected from Cell. + pub const CLayout = lib.Packed(Cell, .{ .fields = .{ + .content_tag = .{ .type_name = "GhosttyCellContentTag" }, + .content = .{ .encoding = .{ .tagged_union = lib.PackedTaggedUnion( + Cell, + .content, + .content_tag, + .{ .arms = .{ + .codepoint = .{ .codepoint = lib.Packed( + @FieldType(@FieldType(Cell, "content"), "codepoint"), + .{ .fields = .{ + .data = .{ .name = "codepoint" }, + ._pad = .{ .omit = true }, + } }, + ) }, + .codepoint_grapheme = .{ .codepoint = lib.Packed( + @FieldType(@FieldType(Cell, "content"), "codepoint"), + .{ .fields = .{ + .data = .{ .name = "codepoint" }, + ._pad = .{ .omit = true }, + } }, + ) }, + .bg_color_palette = .{ .color_palette = lib.Packed( + @FieldType(@FieldType(Cell, "content"), "color_palette"), + .{ .fields = .{ + .data = .{ + .name = "index", + .type_name = "GhosttyColorPaletteIndex", + }, + ._pad = .{ .omit = true }, + } }, + ) }, + .bg_color_rgb = .{ .color_rgb = lib.Packed( + @FieldType(@FieldType(Cell, "content"), "color_rgb"), + .{}, + ) }, + } }, + ) } }, + .style_id = .{ .type_name = "GhosttyStyleId" }, + .wide = .{ .type_name = "GhosttyCellWide" }, + .semantic_content = .{ .type_name = "GhosttyCellSemanticContent" }, + ._padding = .{ .omit = true }, + } }); + /// The backing integer of this packed struct. Prefer this over /// hardcoding the integer type so that code is resilient to the /// size changing.