diff --git a/include/ghostty/vt/render.h b/include/ghostty/vt/render.h index d49b552f2..1738198ce 100644 --- a/include/ghostty/vt/render.h +++ b/include/ghostty/vt/render.h @@ -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); /** @} */ diff --git a/src/lib_vt.zig b/src/lib_vt.zig index e9887968e..3eb2dbec7 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -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" }); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 2f6792921..d1771928f 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -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; diff --git a/src/terminal/c/render.zig b/src/terminal/c/render.zig index a8a0308d9..740fcaa09 100644 --- a/src/terminal/c/render.zig +++ b/src/terminal/c/render.zig @@ -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" { diff --git a/src/terminal/c/row.zig b/src/terminal/c/row.zig index b67c98b3c..6614d922e 100644 --- a/src/terminal/c/row.zig +++ b/src/terminal/c/row.zig @@ -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) { diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 3e7ca9ac3..d013d1590 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -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 {