From 459583a6c396f04d303d24dd635bc947b9204e90 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Mar 2026 06:58:28 -0700 Subject: [PATCH] vt: use get/set pattern for render state data access Replace the individual ghostty_render_state_size_get, ghostty_render_state_dirty_get, and ghostty_render_state_dirty_set functions with generic ghostty_render_state_get and ghostty_render_state_set functions that use enum-dispatched data kinds and option kinds, following the same InType/OutType pattern used by the terminal and mouse encoder C APIs. --- include/ghostty/vt/render.h | 96 ++++++++++------- src/lib_vt.zig | 5 +- src/terminal/c/main.zig | 5 +- src/terminal/c/render.zig | 199 ++++++++++++++++++++++-------------- 4 files changed, 183 insertions(+), 122 deletions(-) diff --git a/include/ghostty/vt/render.h b/include/ghostty/vt/render.h index da9849332..d49b552f2 100644 --- a/include/ghostty/vt/render.h +++ b/include/ghostty/vt/render.h @@ -95,6 +95,35 @@ typedef enum { GHOSTTY_RENDER_STATE_DIRTY_FULL = 2, } GhosttyRenderStateDirty; +/** + * Queryable data kinds for ghostty_render_state_get(). + * + * @ingroup render + */ +typedef enum { + /** Invalid / sentinel value. */ + GHOSTTY_RENDER_STATE_DATA_INVALID = 0, + + /** Viewport width in cells (uint16_t). */ + GHOSTTY_RENDER_STATE_DATA_COLS = 1, + + /** Viewport height in cells (uint16_t). */ + GHOSTTY_RENDER_STATE_DATA_ROWS = 2, + + /** Current dirty state (GhosttyRenderStateDirty). */ + GHOSTTY_RENDER_STATE_DATA_DIRTY = 3, +} GhosttyRenderStateData; + +/** + * Settable options for ghostty_render_state_set(). + * + * @ingroup render + */ +typedef enum { + /** Set dirty state (GhosttyRenderStateDirty). */ + GHOSTTY_RENDER_STATE_OPTION_DIRTY = 0, +} GhosttyRenderStateOption; + /** * Render-state color information. * @@ -177,22 +206,41 @@ GhosttyResult ghostty_render_state_update(GhosttyRenderState state, GhosttyTerminal terminal); /** - * Get the current viewport size from a render state. + * Get a value from a render state. * - * The returned values are the render-state dimensions in cells. These - * match the active viewport size from the most recent successful update. + * The `out` pointer must point to a value of the type corresponding to the + * requested data kind (see GhosttyRenderStateData). * * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) - * @param[out] out_cols On success, receives the viewport width in cells - * @param[out] out_rows On success, receives the viewport height in cells - * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state`, - * `out_cols`, or `out_rows` is NULL + * @param data The data kind to query + * @param[out] out Pointer to receive the queried value + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` is + * NULL or `data` is not a recognized enum value * * @ingroup render */ -GhosttyResult ghostty_render_state_size_get(GhosttyRenderState state, - uint16_t* out_cols, - uint16_t* out_rows); +GhosttyResult ghostty_render_state_get(GhosttyRenderState state, + GhosttyRenderStateData data, + void* out); + +/** + * Set an option on a render state. + * + * The `value` pointer must point to a value of the type corresponding to the + * requested option kind (see GhosttyRenderStateOption). + * + * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param option The option to set + * @param[in] value Pointer to the value to set (NULL returns + * GHOSTTY_INVALID_VALUE) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or + * `value` is NULL + * + * @ingroup render + */ +GhosttyResult ghostty_render_state_set(GhosttyRenderState state, + GhosttyRenderStateOption option, + const void* value); /** * Get the current color information from a render state. @@ -212,34 +260,6 @@ GhosttyResult ghostty_render_state_size_get(GhosttyRenderState state, GhosttyResult ghostty_render_state_colors_get(GhosttyRenderState state, GhosttyRenderStateColors* out_colors); -/** - * Get the current dirty state of a render state. - * - * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) - * @param[out] out_dirty On success, receives the current dirty state - * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` is - * NULL - * - * @ingroup render - */ -GhosttyResult ghostty_render_state_dirty_get(GhosttyRenderState state, - GhosttyRenderStateDirty* out_dirty); - -/** - * Set the dirty state of a render state. - * - * This can be used by callers to clear dirty state after handling updates. - * - * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) - * @param dirty The dirty state to set - * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` is - * NULL or `dirty` is not a recognized enum value - * - * @ingroup render - */ -GhosttyResult ghostty_render_state_dirty_set(GhosttyRenderState state, - GhosttyRenderStateDirty dirty); - /** * Create a row iterator for a render state. * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 9d983a6c6..e9887968e 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -189,10 +189,9 @@ comptime { @export(&c.formatter_free, .{ .name = "ghostty_formatter_free" }); @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_get, .{ .name = "ghostty_render_state_get" }); + @export(&c.render_state_set, .{ .name = "ghostty_render_state_set" }); @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_row_iterator_new, .{ .name = "ghostty_render_state_row_iterator_new" }); @export(&c.render_state_row_iterator_next, .{ .name = "ghostty_render_state_row_iterator_next" }); @export(&c.render_state_row_dirty_get, .{ .name = "ghostty_render_state_row_dirty_get" }); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 7e1b653d9..2f6792921 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -39,10 +39,9 @@ pub const formatter_free = formatter.free; pub const render_state_new = render.new; pub const render_state_free = render.free; pub const render_state_update = render.update; -pub const render_state_size_get = render.size_get; +pub const render_state_get = render.get; +pub const render_state_set = render.set; pub const render_state_colors_get = render.colors_get; -pub const render_state_dirty_get = render.dirty_get; -pub const render_state_dirty_set = render.dirty_set; pub const render_state_row_iterator_new = render.row_iterator_new; pub const render_state_row_iterator_next = render.row_iterator_next; pub const render_state_row_dirty_get = render.row_dirty_get; diff --git a/src/terminal/c/render.zig b/src/terminal/c/render.zig index 480bf886b..a8a0308d9 100644 --- a/src/terminal/c/render.zig +++ b/src/terminal/c/render.zig @@ -12,6 +12,8 @@ const terminal_c = @import("terminal.zig"); const renderpkg = @import("../render.zig"); const Result = @import("result.zig").Result; +const log = std.log.scoped(.render_state_c); + const RenderStateWrapper = struct { alloc: std.mem.Allocator, state: renderpkg.RenderState = .empty, @@ -38,6 +40,35 @@ pub const RowIterator = ?*RowIteratorWrapper; /// C: GhosttyRenderStateDirty pub const Dirty = renderpkg.RenderState.Dirty; +/// C: GhosttyRenderStateData +pub const Data = enum(c_int) { + invalid = 0, + cols = 1, + rows = 2, + dirty = 3, + + /// Output type expected for querying the data of the given kind. + pub fn OutType(comptime self: Data) type { + return switch (self) { + .invalid => void, + .cols, .rows => size.CellCountInt, + .dirty => Dirty, + }; + } +}; + +/// C: GhosttyRenderStateOption +pub const SetOption = enum(c_int) { + dirty = 0, + + /// Input type expected for setting the option. + pub fn InType(comptime self: SetOption) type { + return switch (self) { + .dirty => Dirty, + }; + } +}; + /// C: GhosttyRenderStateColors pub const Colors = extern struct { size: usize = @sizeOf(Colors), @@ -88,17 +119,74 @@ pub fn update( return .success; } -pub fn size_get( +pub fn get( state_: RenderState, - out_cols_: ?*size.CellCountInt, - out_rows_: ?*size.CellCountInt, + data: Data, + out: ?*anyopaque, ) callconv(.c) Result { - const state = state_ orelse return .invalid_value; - const out_cols = out_cols_ orelse return .invalid_value; - const out_rows = out_rows_ orelse return .invalid_value; + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(Data, @intFromEnum(data)) catch { + log.warn("render_state_get invalid data value={d}", .{@intFromEnum(data)}); + return .invalid_value; + }; + } + + return switch (data) { + inline else => |comptime_data| getTyped( + state_, + comptime_data, + @ptrCast(@alignCast(out)), + ), + }; +} + +fn getTyped( + state_: RenderState, + comptime data: Data, + out: *data.OutType(), +) Result { + const state = state_ orelse return .invalid_value; + switch (data) { + .invalid => return .invalid_value, + .cols => out.* = state.state.cols, + .rows => out.* = state.state.rows, + .dirty => out.* = state.state.dirty, + } + + return .success; +} + +pub fn set( + state_: RenderState, + option: SetOption, + value: ?*const anyopaque, +) callconv(.c) Result { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(SetOption, @intFromEnum(option)) catch { + log.warn("render_state_set invalid option value={d}", .{@intFromEnum(option)}); + return .invalid_value; + }; + } + + return switch (option) { + inline else => |comptime_option| setTyped( + state_, + comptime_option, + @ptrCast(@alignCast(value orelse return .invalid_value)), + ), + }; +} + +fn setTyped( + state_: RenderState, + comptime option: SetOption, + value: *const option.InType(), +) Result { + const state = state_ orelse return .invalid_value; + switch (option) { + .dirty => state.state.dirty = value.*, + } - out_cols.* = state.state.cols; - out_rows.* = state.state.rows; return .success; } @@ -164,25 +252,7 @@ pub fn colors_get( return .success; } -pub fn dirty_get( - state_: RenderState, - out_dirty: *Dirty, -) callconv(.c) Result { - const state = state_ orelse return .invalid_value; - out_dirty.* = state.state.dirty; - return .success; -} -pub fn dirty_set( - state_: RenderState, - dirty_: c_int, -) callconv(.c) Result { - const state = state_ orelse return .invalid_value; - const dirty = std.meta.intToEnum(Dirty, dirty_) catch - return .invalid_value; - state.state.dirty = dirty; - return .success; -} pub fn row_iterator_new( alloc_: ?*const CAllocator, @@ -281,7 +351,12 @@ test "render: update invalid value" { try testing.expectEqual(Result.invalid_value, update(state, null)); } -test "render: size get invalid value" { +test "render: get invalid value" { + var cols: size.CellCountInt = 0; + try testing.expectEqual(Result.invalid_value, get(null, .cols, @ptrCast(&cols))); +} + +test "render: get invalid data" { var state: RenderState = null; try testing.expectEqual(Result.success, new( &lib_alloc.test_allocator, @@ -289,23 +364,7 @@ test "render: size get invalid value" { )); defer free(state); - var cols: size.CellCountInt = 0; - var rows: size.CellCountInt = 0; - try testing.expectEqual(Result.invalid_value, size_get( - null, - &cols, - &rows, - )); - try testing.expectEqual(Result.invalid_value, size_get( - state, - null, - &rows, - )); - try testing.expectEqual(Result.invalid_value, size_get( - state, - &cols, - null, - )); + try testing.expectEqual(Result.invalid_value, get(state, .invalid, null)); } test "render: colors get invalid value" { @@ -326,23 +385,14 @@ test "render: colors get invalid value" { 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( - &lib_alloc.test_allocator, - &state, - )); - defer free(state); - +test "render: get/set dirty invalid value" { var dirty: Dirty = .false; - try testing.expectEqual(Result.invalid_value, dirty_get(null, &dirty)); - try testing.expectEqual(Result.invalid_value, dirty_set( - null, - @intFromEnum(Dirty.full), - )); + try testing.expectEqual(Result.invalid_value, get(null, .dirty, @ptrCast(&dirty))); + const dirty_full: Dirty = .full; + try testing.expectEqual(Result.invalid_value, set(null, .dirty, @ptrCast(&dirty_full))); } -test "render: dirty get/set" { +test "render: get/set dirty" { var state: RenderState = null; try testing.expectEqual(Result.success, new( &lib_alloc.test_allocator, @@ -351,25 +401,21 @@ test "render: dirty get/set" { defer free(state); var dirty: Dirty = undefined; - try testing.expectEqual(Result.success, dirty_get(state, &dirty)); + try testing.expectEqual(Result.success, get(state, .dirty, @ptrCast(&dirty))); try testing.expectEqual(Dirty.false, dirty); - try testing.expectEqual(Result.success, dirty_set( - state, - @intFromEnum(Dirty.partial), - )); - try testing.expectEqual(Result.success, dirty_get(state, &dirty)); + const dirty_partial: Dirty = .partial; + try testing.expectEqual(Result.success, set(state, .dirty, @ptrCast(&dirty_partial))); + try testing.expectEqual(Result.success, get(state, .dirty, @ptrCast(&dirty))); try testing.expectEqual(Dirty.partial, dirty); - try testing.expectEqual(Result.success, dirty_set( - state, - @intFromEnum(Dirty.full), - )); - try testing.expectEqual(Result.success, dirty_get(state, &dirty)); + const dirty_full: Dirty = .full; + try testing.expectEqual(Result.success, set(state, .dirty, @ptrCast(&dirty_full))); + try testing.expectEqual(Result.success, get(state, .dirty, @ptrCast(&dirty))); try testing.expectEqual(Dirty.full, dirty); } -test "render: dirty set invalid enum value" { +test "render: set null value" { var state: RenderState = null; try testing.expectEqual(Result.success, new( &lib_alloc.test_allocator, @@ -377,7 +423,7 @@ test "render: dirty set invalid enum value" { )); defer free(state); - try testing.expectEqual(Result.invalid_value, dirty_set(state, 99)); + try testing.expectEqual(Result.invalid_value, set(state, .dirty, null)); } test "render: row iterator new invalid value" { @@ -650,14 +696,11 @@ test "render: update" { try testing.expectEqual(Result.success, update(state, terminal)); var cols: size.CellCountInt = 0; - var rows: size.CellCountInt = 0; - try testing.expectEqual(Result.success, size_get( - state, - &cols, - &rows, - )); + var rows_val: size.CellCountInt = 0; + try testing.expectEqual(Result.success, get(state, .cols, @ptrCast(&cols))); + try testing.expectEqual(Result.success, get(state, .rows, @ptrCast(&rows_val))); try testing.expectEqual(@as(size.CellCountInt, 80), cols); - try testing.expectEqual(@as(size.CellCountInt, 24), rows); + try testing.expectEqual(@as(size.CellCountInt, 24), rows_val); } test "render: colors get" {