vt: expose render state colors in C API
Add a C-facing GhosttyRenderStateColors sized struct and a ghostty_render_state_colors_get accessor so renderers can read background, foreground, cursor color state, and palette data directly from the render state.pull/11664/head
parent
b830a0ee1d
commit
b35f8ed16e
|
|
@ -100,6 +100,7 @@ extern "C" {
|
|||
|
||||
#include <ghostty/vt/types.h>
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/color.h>
|
||||
#include <ghostty/vt/focus.h>
|
||||
#include <ghostty/vt/formatter.h>
|
||||
#include <ghostty/vt/render.h>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#define GHOSTTY_VT_RENDER_H
|
||||
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/color.h>
|
||||
#include <ghostty/vt/terminal.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
|
||||
|
|
@ -65,6 +66,45 @@ typedef enum {
|
|||
GHOSTTY_RENDER_STATE_DIRTY_FULL = 2,
|
||||
} GhosttyRenderStateDirty;
|
||||
|
||||
/**
|
||||
* Render-state color information.
|
||||
*
|
||||
* This struct uses the sized-struct ABI pattern. Initialize with
|
||||
* GHOSTTY_INIT_SIZED(GhosttyRenderStateColors) before calling
|
||||
* ghostty_render_state_colors_get().
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* GhosttyRenderStateColors colors = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors);
|
||||
* GhosttyResult result = ghostty_render_state_colors_get(state, &colors);
|
||||
* @endcode
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. Must be set to sizeof(GhosttyRenderStateColors). */
|
||||
size_t size;
|
||||
|
||||
/** The default/current background color for the render state. */
|
||||
GhosttyColorRgb background;
|
||||
|
||||
/** The default/current foreground color for the render state. */
|
||||
GhosttyColorRgb foreground;
|
||||
|
||||
/** The cursor color when explicitly set by terminal state. */
|
||||
GhosttyColorRgb cursor;
|
||||
|
||||
/**
|
||||
* True when cursor contains a valid explicit cursor color value.
|
||||
* If this is false, the cursor color should be ignored; it will
|
||||
* contain undefined data.
|
||||
* */
|
||||
bool cursor_has_value;
|
||||
|
||||
/** The active 256-color palette for this render state. */
|
||||
GhosttyColorRgb palette[256];
|
||||
} GhosttyRenderStateColors;
|
||||
|
||||
/**
|
||||
* Create a new render state instance.
|
||||
*
|
||||
|
|
@ -113,6 +153,24 @@ GhosttyResult ghostty_render_state_size_get(GhosttyRenderState state,
|
|||
uint16_t* out_cols,
|
||||
uint16_t* out_rows);
|
||||
|
||||
/**
|
||||
* Get the current color information from a render state.
|
||||
*
|
||||
* This writes as many fields as fit in the caller-provided sized struct.
|
||||
* `out_colors->size` must be set by the caller (typically via
|
||||
* GHOSTTY_INIT_SIZED(GhosttyRenderStateColors)).
|
||||
*
|
||||
* @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param[out] out_colors Sized output struct to receive render-state colors
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or
|
||||
* `out_colors` is NULL, or if `out_colors->size` is smaller than
|
||||
* `sizeof(size_t)`
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
GhosttyResult ghostty_render_state_colors_get(GhosttyRenderState state,
|
||||
GhosttyRenderStateColors* out_colors);
|
||||
|
||||
/**
|
||||
* Get the current dirty state of a render state.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const std = @import("std");
|
||||
const enumpkg = @import("enum.zig");
|
||||
const structpkg = @import("struct.zig");
|
||||
const types = @import("types.zig");
|
||||
const unionpkg = @import("union.zig");
|
||||
|
||||
|
|
@ -7,7 +8,8 @@ pub const allocator = @import("allocator.zig");
|
|||
pub const Enum = enumpkg.Enum;
|
||||
pub const checkGhosttyHEnum = enumpkg.checkGhosttyHEnum;
|
||||
pub const String = types.String;
|
||||
pub const Struct = @import("struct.zig").Struct;
|
||||
pub const Struct = structpkg.Struct;
|
||||
pub const structSizedFieldFits = structpkg.sizedFieldFits;
|
||||
pub const Target = @import("target.zig").Target;
|
||||
pub const TaggedUnion = unionpkg.TaggedUnion;
|
||||
pub const cutPrefix = @import("string.zig").cutPrefix;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,52 @@ pub fn Struct(
|
|||
};
|
||||
}
|
||||
|
||||
/// Returns true if a struct of type `T` with size `size` can set
|
||||
/// field `field` (if it fits within the size). This is used for ABI
|
||||
/// compatibility for structs that have an explicit size field.
|
||||
pub fn sizedFieldFits(
|
||||
comptime T: type,
|
||||
size: usize,
|
||||
comptime field: []const u8,
|
||||
) bool {
|
||||
const offset = @offsetOf(T, field);
|
||||
const field_size = @sizeOf(@FieldType(T, field));
|
||||
return size >= offset + field_size;
|
||||
}
|
||||
|
||||
test "sizedFieldFits boundary checks" {
|
||||
const Sized = extern struct {
|
||||
size: usize,
|
||||
a: u8,
|
||||
b: u32,
|
||||
};
|
||||
|
||||
const size_required = @offsetOf(Sized, "size") + @sizeOf(@FieldType(Sized, "size"));
|
||||
const a_required = @offsetOf(Sized, "a") + @sizeOf(@FieldType(Sized, "a"));
|
||||
const b_required = @offsetOf(Sized, "b") + @sizeOf(@FieldType(Sized, "b"));
|
||||
|
||||
try testing.expect(sizedFieldFits(Sized, size_required, "size"));
|
||||
try testing.expect(!sizedFieldFits(Sized, size_required - 1, "size"));
|
||||
|
||||
try testing.expect(sizedFieldFits(Sized, a_required, "a"));
|
||||
try testing.expect(!sizedFieldFits(Sized, a_required - 1, "a"));
|
||||
|
||||
try testing.expect(sizedFieldFits(Sized, b_required, "b"));
|
||||
try testing.expect(!sizedFieldFits(Sized, b_required - 1, "b"));
|
||||
}
|
||||
|
||||
test "sizedFieldFits respects alignment padding" {
|
||||
const Sized = extern struct {
|
||||
size: usize,
|
||||
a: u8,
|
||||
b: u32,
|
||||
};
|
||||
|
||||
const up_to_padding = @offsetOf(Sized, "b");
|
||||
try testing.expect(sizedFieldFits(Sized, up_to_padding, "a"));
|
||||
try testing.expect(!sizedFieldFits(Sized, up_to_padding, "b"));
|
||||
}
|
||||
|
||||
test "packed struct converts to extern with full-size bools" {
|
||||
const Packed = packed struct {
|
||||
flag1: bool,
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ comptime {
|
|||
@export(&c.render_state_new, .{ .name = "ghostty_render_state_new" });
|
||||
@export(&c.render_state_update, .{ .name = "ghostty_render_state_update" });
|
||||
@export(&c.render_state_size_get, .{ .name = "ghostty_render_state_size_get" });
|
||||
@export(&c.render_state_colors_get, .{ .name = "ghostty_render_state_colors_get" });
|
||||
@export(&c.render_state_dirty_get, .{ .name = "ghostty_render_state_dirty_get" });
|
||||
@export(&c.render_state_dirty_set, .{ .name = "ghostty_render_state_dirty_set" });
|
||||
@export(&c.render_state_free, .{ .name = "ghostty_render_state_free" });
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ pub const render_state_new = render.new;
|
|||
pub const render_state_free = render.free;
|
||||
pub const render_state_update = render.update;
|
||||
pub const render_state_size_get = render.size_get;
|
||||
pub const render_state_colors_get = render.colors_get;
|
||||
pub const render_state_dirty_get = render.dirty_get;
|
||||
pub const render_state_dirty_set = render.dirty_set;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const lib = @import("../../lib/main.zig");
|
||||
const lib_alloc = @import("../../lib/allocator.zig");
|
||||
const CAllocator = lib_alloc.Allocator;
|
||||
const colorpkg = @import("../color.zig");
|
||||
const size = @import("../size.zig");
|
||||
const terminal_c = @import("terminal.zig");
|
||||
const renderpkg = @import("../render.zig");
|
||||
|
|
@ -18,6 +20,16 @@ pub const RenderState = ?*RenderStateWrapper;
|
|||
/// C: GhosttyRenderStateDirty
|
||||
pub const Dirty = renderpkg.RenderState.Dirty;
|
||||
|
||||
/// C: GhosttyRenderStateColors
|
||||
pub const Colors = extern struct {
|
||||
size: usize = @sizeOf(Colors),
|
||||
background: colorpkg.RGB.C,
|
||||
foreground: colorpkg.RGB.C,
|
||||
cursor: colorpkg.RGB.C,
|
||||
cursor_has_value: bool,
|
||||
palette: [256]colorpkg.RGB.C,
|
||||
};
|
||||
|
||||
pub fn new(
|
||||
alloc_: ?*const CAllocator,
|
||||
result: *RenderState,
|
||||
|
|
@ -65,6 +77,68 @@ pub fn size_get(
|
|||
return .success;
|
||||
}
|
||||
|
||||
pub fn colors_get(
|
||||
state_: RenderState,
|
||||
out_colors_: ?*Colors,
|
||||
) callconv(.c) Result {
|
||||
const state = state_ orelse return .invalid_value;
|
||||
const out_colors = out_colors_ orelse return .invalid_value;
|
||||
const out_size = out_colors.size;
|
||||
if (out_size < @sizeOf(usize)) return .invalid_value;
|
||||
|
||||
const colors = state.state.colors;
|
||||
if (lib.structSizedFieldFits(
|
||||
Colors,
|
||||
out_size,
|
||||
"background",
|
||||
)) {
|
||||
out_colors.background = colors.background.cval();
|
||||
}
|
||||
|
||||
if (lib.structSizedFieldFits(
|
||||
Colors,
|
||||
out_size,
|
||||
"foreground",
|
||||
)) {
|
||||
out_colors.foreground = colors.foreground.cval();
|
||||
}
|
||||
|
||||
if (colors.cursor) |cursor| {
|
||||
if (lib.structSizedFieldFits(
|
||||
Colors,
|
||||
out_size,
|
||||
"cursor",
|
||||
)) {
|
||||
out_colors.cursor = cursor.cval();
|
||||
}
|
||||
}
|
||||
|
||||
if (lib.structSizedFieldFits(
|
||||
Colors,
|
||||
out_size,
|
||||
"cursor_has_value",
|
||||
)) {
|
||||
out_colors.cursor_has_value = colors.cursor != null;
|
||||
}
|
||||
|
||||
if (lib.structSizedFieldFits(
|
||||
Colors,
|
||||
out_size,
|
||||
"palette",
|
||||
)) {
|
||||
const palette_offset = @offsetOf(Colors, "palette");
|
||||
if (out_size > palette_offset) {
|
||||
const available = out_size - palette_offset;
|
||||
const max_entries = @min(colors.palette.len, available / @sizeOf(colorpkg.RGB.C));
|
||||
for (0..max_entries) |i| {
|
||||
out_colors.palette[i] = colors.palette[i].cval();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn dirty_get(
|
||||
state_: RenderState,
|
||||
out_dirty: *Dirty,
|
||||
|
|
@ -145,6 +219,24 @@ test "render: size get invalid value" {
|
|||
));
|
||||
}
|
||||
|
||||
test "render: colors get invalid value" {
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&state,
|
||||
));
|
||||
defer free(state);
|
||||
|
||||
var colors: Colors = std.mem.zeroes(Colors);
|
||||
colors.size = @sizeOf(Colors);
|
||||
|
||||
try testing.expectEqual(Result.invalid_value, colors_get(null, &colors));
|
||||
try testing.expectEqual(Result.invalid_value, colors_get(state, null));
|
||||
|
||||
colors.size = @sizeOf(usize) - 1;
|
||||
try testing.expectEqual(Result.invalid_value, colors_get(state, &colors));
|
||||
}
|
||||
|
||||
test "render: dirty get/set invalid value" {
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
|
|
@ -234,3 +326,80 @@ test "render: update" {
|
|||
try testing.expectEqual(@as(size.CellCountInt, 80), cols);
|
||||
try testing.expectEqual(@as(size.CellCountInt, 24), rows);
|
||||
}
|
||||
|
||||
test "render: colors get" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib_alloc.test_allocator,
|
||||
&terminal,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 10_000,
|
||||
},
|
||||
));
|
||||
defer terminal_c.free(terminal);
|
||||
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&state,
|
||||
));
|
||||
defer free(state);
|
||||
|
||||
try testing.expectEqual(Result.success, update(state, terminal));
|
||||
|
||||
var colors: Colors = std.mem.zeroes(Colors);
|
||||
colors.size = @sizeOf(Colors);
|
||||
try testing.expectEqual(Result.success, colors_get(state, &colors));
|
||||
|
||||
const state_colors = &state.?.state.colors;
|
||||
try testing.expectEqual(state_colors.background.cval(), colors.background);
|
||||
try testing.expectEqual(state_colors.foreground.cval(), colors.foreground);
|
||||
|
||||
if (state_colors.cursor) |cursor| {
|
||||
try testing.expect(colors.cursor_has_value);
|
||||
try testing.expectEqual(cursor.cval(), colors.cursor);
|
||||
} else {
|
||||
try testing.expect(!colors.cursor_has_value);
|
||||
}
|
||||
|
||||
for (state_colors.palette, colors.palette) |expected, actual| {
|
||||
try testing.expectEqual(expected.cval(), actual);
|
||||
}
|
||||
}
|
||||
|
||||
test "render: colors get supports truncated sized struct" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib_alloc.test_allocator,
|
||||
&terminal,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 10_000,
|
||||
},
|
||||
));
|
||||
defer terminal_c.free(terminal);
|
||||
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&state,
|
||||
));
|
||||
defer free(state);
|
||||
|
||||
try testing.expectEqual(Result.success, update(state, terminal));
|
||||
|
||||
var colors: Colors = std.mem.zeroes(Colors);
|
||||
const sentinel: colorpkg.RGB.C = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC };
|
||||
for (&colors.palette) |*entry| entry.* = sentinel;
|
||||
|
||||
colors.size = @offsetOf(Colors, "palette") + @sizeOf(colorpkg.RGB.C) * 2;
|
||||
try testing.expectEqual(Result.success, colors_get(state, &colors));
|
||||
|
||||
const state_colors = &state.?.state.colors;
|
||||
try testing.expectEqual(state_colors.palette[0].cval(), colors.palette[0]);
|
||||
try testing.expectEqual(state_colors.palette[1].cval(), colors.palette[1]);
|
||||
try testing.expectEqual(sentinel, colors.palette[2]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue