mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
vt: expose packed cell layout
GhosttyCell was exposed as a raw integer while its manifest entry was only an alias, forcing bulk-read consumers to duplicate the internal cell bit layout.\n\nAdd reflection helpers for packed structs and tagged unions, and keep the C-facing layout metadata next to Cell itself. Extend the ABI manifest and schema with recursive bit descriptors so every content arm, including palette and RGB backgrounds, can be decoded without hardcoded masks.\n\nDocument manifest-driven cell decoding and test the metadata against Zig reflection and real cell values.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
* <a href="types.schema.json">libghostty-vt ABI manifest JSON Schema</a>.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
290
src/lib/packed.zig
Normal file
290
src/lib/packed.zig
Normal file
@@ -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));
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user