vt: use get/set pattern for row iterator data access

Replace ghostty_render_state_row_dirty_get and
ghostty_render_state_row_dirty_set with generic
ghostty_render_state_row_get and ghostty_render_state_row_set
functions using enum-dispatched data/option kinds.
This commit is contained in:
Mitchell Hashimoto
2026-03-20 07:03:43 -07:00
parent 459583a6c3
commit 33e81ffb75
6 changed files with 219 additions and 46 deletions

View File

@@ -124,6 +124,32 @@ typedef enum {
GHOSTTY_RENDER_STATE_OPTION_DIRTY = 0,
} GhosttyRenderStateOption;
/**
* Queryable data kinds for ghostty_render_state_row_get().
*
* @ingroup render
*/
typedef enum {
/** Invalid / sentinel value. */
GHOSTTY_RENDER_STATE_ROW_DATA_INVALID = 0,
/** Whether the current row is dirty (bool). */
GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY = 1,
/** The raw row value (GhosttyRow). */
GHOSTTY_RENDER_STATE_ROW_DATA_RAW = 2,
} GhosttyRenderStateRowData;
/**
* Settable options for ghostty_render_state_row_set().
*
* @ingroup render
*/
typedef enum {
/** Set dirty state for the current row (bool). */
GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY = 0,
} GhosttyRenderStateRowOption;
/**
* Render-state color information.
*
@@ -302,37 +328,47 @@ void ghostty_render_state_row_iterator_free(GhosttyRenderStateRowIterator iterat
bool ghostty_render_state_row_iterator_next(GhosttyRenderStateRowIterator iterator);
/**
* Get the dirty state of the current row in a render-state row iterator.
* Get a value from the current row in a render-state row iterator.
*
* This reads the dirty flag at the iterator's current row position.
* The `out` pointer must point to a value of the type corresponding to the
* requested data kind (see GhosttyRenderStateRowData).
* Call ghostty_render_state_row_iterator_next() at least once before
* calling this function.
*
* @param iterator The iterator handle to query (may be NULL)
* @return true if the current row is dirty, false if the row is clean,
* `iterator` is NULL, or the iterator is not positioned on a row
*
* @ingroup render
*/
bool ghostty_render_state_row_dirty_get(GhosttyRenderStateRowIterator iterator);
/**
* Set the dirty state of the current row in a render-state row iterator.
*
* This writes the dirty flag at the iterator's current row position.
* Call ghostty_render_state_row_iterator_next() at least once before
* calling this function.
*
* @param iterator The iterator handle to update (may be NULL)
* @param dirty The dirty state to set for the current row
* @param iterator The iterator handle to query (NULL returns GHOSTTY_INVALID_VALUE)
* @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
* `iterator` is NULL or the iterator is not positioned on a row
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_row_dirty_set(
GhosttyResult ghostty_render_state_row_get(
GhosttyRenderStateRowIterator iterator,
bool dirty);
GhosttyRenderStateRowData data,
void* out);
/**
* Set an option on the current row in a render-state row iterator.
*
* The `value` pointer must point to a value of the type corresponding to the
* requested option kind (see GhosttyRenderStateRowOption).
* Call ghostty_render_state_row_iterator_next() at least once before
* calling this function.
*
* @param iterator The iterator handle to update (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
* `iterator` is NULL or the iterator is not positioned on a row
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_row_set(
GhosttyRenderStateRowIterator iterator,
GhosttyRenderStateRowOption option,
const void* value);
/** @} */

View File

@@ -194,8 +194,8 @@ comptime {
@export(&c.render_state_colors_get, .{ .name = "ghostty_render_state_colors_get" });
@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" });
@export(&c.render_state_row_dirty_set, .{ .name = "ghostty_render_state_row_dirty_set" });
@export(&c.render_state_row_get, .{ .name = "ghostty_render_state_row_get" });
@export(&c.render_state_row_set, .{ .name = "ghostty_render_state_row_set" });
@export(&c.render_state_row_iterator_free, .{ .name = "ghostty_render_state_row_iterator_free" });
@export(&c.render_state_free, .{ .name = "ghostty_render_state_free" });
@export(&c.terminal_new, .{ .name = "ghostty_terminal_new" });

View File

@@ -44,8 +44,8 @@ pub const render_state_set = render.set;
pub const render_state_colors_get = render.colors_get;
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;
pub const render_state_row_dirty_set = render.row_dirty_set;
pub const render_state_row_get = render.row_get;
pub const render_state_row_set = render.row_set;
pub const render_state_row_iterator_free = render.row_iterator_free;
pub const sgr_new = sgr.new;

View File

@@ -11,6 +11,7 @@ const Style = @import("../style.zig").Style;
const terminal_c = @import("terminal.zig");
const renderpkg = @import("../render.zig");
const Result = @import("result.zig").Result;
const row = @import("row.zig");
const log = std.log.scoped(.render_state_c);
@@ -309,19 +310,103 @@ pub fn row_iterator_next(iterator_: RowIterator) callconv(.c) bool {
return true;
}
pub fn row_dirty_get(iterator_: RowIterator) callconv(.c) bool {
const it = iterator_ orelse return false;
const y = it.y orelse return false;
return it.dirty[y];
/// C: GhosttyRenderStateRowData
pub const RowData = enum(c_int) {
invalid = 0,
dirty = 1,
raw = 2,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: RowData) type {
return switch (self) {
.invalid => void,
.dirty => bool,
.raw => row.CRow,
};
}
};
/// C: GhosttyRenderStateRowOption
pub const RowOption = enum(c_int) {
dirty = 0,
/// Input type expected for setting the option.
pub fn InType(comptime self: RowOption) type {
return switch (self) {
.dirty => bool,
};
}
};
pub fn row_get(
iterator_: RowIterator,
data: RowData,
out: ?*anyopaque,
) callconv(.c) Result {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(RowData, @intFromEnum(data)) catch {
log.warn("render_state_row_get invalid data value={d}", .{@intFromEnum(data)});
return .invalid_value;
};
}
return switch (data) {
inline else => |comptime_data| rowGetTyped(
iterator_,
comptime_data,
@ptrCast(@alignCast(out)),
),
};
}
pub fn row_dirty_set(
fn rowGetTyped(
iterator_: RowIterator,
dirty: bool,
) callconv(.c) Result {
comptime data: RowData,
out: *data.OutType(),
) Result {
const it = iterator_ orelse return .invalid_value;
const y = it.y orelse return .invalid_value;
it.dirty[y] = dirty;
switch (data) {
.invalid => return .invalid_value,
.dirty => out.* = it.dirty[y],
.raw => out.* = it.raws[y].cval(),
}
return .success;
}
pub fn row_set(
iterator_: RowIterator,
option: RowOption,
value: ?*const anyopaque,
) callconv(.c) Result {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(RowOption, @intFromEnum(option)) catch {
log.warn("render_state_row_set invalid option value={d}", .{@intFromEnum(option)});
return .invalid_value;
};
}
return switch (option) {
inline else => |comptime_option| rowSetTyped(
iterator_,
comptime_option,
@ptrCast(@alignCast(value orelse return .invalid_value)),
),
};
}
fn rowSetTyped(
iterator_: RowIterator,
comptime option: RowOption,
value: *const option.InType(),
) Result {
const it = iterator_ orelse return .invalid_value;
const y = it.y orelse return .invalid_value;
switch (option) {
.dirty => it.dirty[y] = value.*,
}
return .success;
}
@@ -495,13 +580,12 @@ test "render: row iterator next null" {
try testing.expect(!row_iterator_next(null));
}
test "render: row iterator dirty get null" {
try testing.expect(!row_dirty_get(null));
test "render: row get null" {
var dirty: bool = undefined;
try testing.expectEqual(Result.invalid_value, row_get(null, .dirty, @ptrCast(&dirty)));
}
test "render: row iterator dirty set invalid value" {
try testing.expectEqual(Result.invalid_value, row_dirty_set(null, false));
test "render: row get invalid data" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
@@ -531,10 +615,16 @@ test "render: row iterator dirty set invalid value" {
));
defer row_iterator_free(iterator);
try testing.expectEqual(Result.invalid_value, row_dirty_set(iterator, false));
try testing.expect(row_iterator_next(iterator));
try testing.expectEqual(Result.invalid_value, row_get(iterator, .invalid, null));
}
test "render: row iterator dirty get before iteration" {
test "render: row set null" {
const dirty = false;
try testing.expectEqual(Result.invalid_value, row_set(null, .dirty, @ptrCast(&dirty)));
}
test "render: row set before iteration" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
@@ -564,10 +654,45 @@ test "render: row iterator dirty get before iteration" {
));
defer row_iterator_free(iterator);
try testing.expect(!row_dirty_get(iterator));
const dirty = false;
try testing.expectEqual(Result.invalid_value, row_set(iterator, .dirty, @ptrCast(&dirty)));
}
test "render: row iterator dirty get" {
test "render: row get before iteration" {
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 iterator: RowIterator = null;
try testing.expectEqual(Result.success, row_iterator_new(
&lib_alloc.test_allocator,
state,
&iterator,
));
defer row_iterator_free(iterator);
var dirty: bool = undefined;
try testing.expectEqual(Result.invalid_value, row_get(iterator, .dirty, @ptrCast(&dirty)));
}
test "render: row get/set dirty" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
@@ -603,10 +728,13 @@ test "render: row iterator dirty get" {
defer row_iterator_free(it);
try testing.expect(row_iterator_next(it));
try testing.expect(row_dirty_get(it));
var dirty: bool = undefined;
try testing.expectEqual(Result.success, row_get(it, .dirty, @ptrCast(&dirty)));
try testing.expect(dirty);
// Clear dirty on this row.
try testing.expectEqual(Result.success, row_dirty_set(it, false));
const dirty_false = false;
try testing.expectEqual(Result.success, row_set(it, .dirty, @ptrCast(&dirty_false)));
// It should not be dirty anymore.
var it2: RowIterator = null;
@@ -618,7 +746,8 @@ test "render: row iterator dirty get" {
defer row_iterator_free(it2);
try testing.expect(row_iterator_next(it2));
try testing.expect(!row_dirty_get(it2));
try testing.expectEqual(Result.success, row_get(it2, .dirty, @ptrCast(&dirty)));
try testing.expect(!dirty);
}
test "render: row iterator next" {

View File

@@ -5,7 +5,7 @@ const Row = page.Row;
const Result = @import("result.zig").Result;
/// C: GhosttyRow
pub const CRow = u64;
pub const CRow = Row.C;
/// C: GhosttyRowSemanticPrompt
pub const SemanticPrompt = enum(c_int) {

View File

@@ -1947,6 +1947,14 @@ pub const Row = packed struct(u64) {
prompt_continuation = 2,
};
/// C ABI type.
pub const C = u64;
/// Returns this row as a C ABI value.
pub fn cval(self: Row) C {
return @bitCast(self);
}
/// Returns true if this row has any managed memory outside of the
/// row structure (graphemes, styles, etc.)
pub inline fn managedMemory(self: Row) bool {