diff --git a/example/c-vt-render/src/main.c b/example/c-vt-render/src/main.c index cb9e7e8c2..ede168033 100644 --- a/example/c-vt-render/src/main.c +++ b/example/c-vt-render/src/main.c @@ -97,7 +97,8 @@ int main(void) { // state. These are needed to resolve palette-indexed cell colors. GhosttyRenderStateColors colors = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors); - result = ghostty_render_state_colors_get(render_state, &colors); + result = ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_COLORS, &colors); assert(result == GHOSTTY_SUCCESS); printf("Background: #%02x%02x%02x\n", @@ -107,31 +108,16 @@ int main(void) { //! [render-colors] //! [render-cursor] - // Read cursor position and visual style from the render state. - bool cursor_visible = false; - ghostty_render_state_get( - render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE, - &cursor_visible); - - bool cursor_in_viewport = false; - ghostty_render_state_get( - render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE, - &cursor_in_viewport); - - if (cursor_visible && cursor_in_viewport) { - uint16_t cx, cy; - ghostty_render_state_get( - render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X, &cx); - ghostty_render_state_get( - render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y, &cy); - - GhosttyRenderStateCursorVisualStyle style; - ghostty_render_state_get( - render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE, - &style); + // Read all cursor state in one call. + GhosttyRenderStateCursor cursor = + GHOSTTY_INIT_SIZED(GhosttyRenderStateCursor); + result = ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR, &cursor); + assert(result == GHOSTTY_SUCCESS); + if (cursor.visible && cursor.viewport_has_value) { const char* style_name = "unknown"; - switch (style) { + switch (cursor.visual_style) { case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR: style_name = "bar"; break; @@ -145,7 +131,8 @@ int main(void) { style_name = "hollow"; break; } - printf("Cursor at (%u, %u), style: %s\n", cx, cy, style_name); + printf("Cursor at (%u, %u), style: %s\n", + cursor.viewport_x, cursor.viewport_y, style_name); } //! [render-cursor] diff --git a/include/ghostty/vt/render.h b/include/ghostty/vt/render.h index 044d1734b..5329a1d9a 100644 --- a/include/ghostty/vt/render.h +++ b/include/ghostty/vt/render.h @@ -199,6 +199,14 @@ typedef enum GHOSTTY_ENUM_TYPED { /** Whether the cursor is on the tail of a wide character (bool). * Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */ GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL = 17, + + /** All cursor state in one sized struct (GhosttyRenderStateCursor). + * Initialize the output with GHOSTTY_INIT_SIZED before querying. */ + GHOSTTY_RENDER_STATE_DATA_CURSOR = 18, + + /** All render-state colors in one sized struct (GhosttyRenderStateColors). + * Initialize the output with GHOSTTY_INIT_SIZED before querying. */ + GHOSTTY_RENDER_STATE_DATA_COLORS = 19, GHOSTTY_RENDER_STATE_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyRenderStateData; @@ -286,17 +294,59 @@ typedef struct { uint16_t end_x; } GhosttyRenderStateRowSelection; +/** + * Render-state cursor information. + * + * This struct uses the sized-struct ABI pattern. Initialize with + * GHOSTTY_INIT_SIZED(GhosttyRenderStateCursor) before querying + * GHOSTTY_RENDER_STATE_DATA_CURSOR. + * + * When viewport_has_value is false, viewport_x, viewport_y, and wide_tail + * contain undefined data and must not be read. + * + * @ingroup render + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyRenderStateCursor). */ + size_t size; + + /** Whether the cursor is visible within the viewport. */ + bool viewport_has_value; + + /** Cursor viewport x position in cells. */ + uint16_t viewport_x; + + /** Cursor viewport y position in cells. */ + uint16_t viewport_y; + + /** Whether the cursor is on the tail of a wide character. */ + bool wide_tail; + + /** Whether the cursor is visible based on terminal modes. */ + bool visible; + + /** Whether the cursor should blink based on terminal modes. */ + bool blinking; + + /** Whether the cursor is at a password input field. */ + bool password_input; + + /** The visual style of the cursor. */ + GhosttyRenderStateCursorVisualStyle visual_style; +} GhosttyRenderStateCursor; + /** * 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(). + * GHOSTTY_INIT_SIZED(GhosttyRenderStateColors) before querying + * GHOSTTY_RENDER_STATE_DATA_COLORS. * * Example: * @code * GhosttyRenderStateColors colors = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors); - * GhosttyResult result = ghostty_render_state_colors_get(state, &colors); + * GhosttyResult result = ghostty_render_state_get( + * state, GHOSTTY_RENDER_STATE_DATA_COLORS, &colors); * @endcode * * @ingroup render @@ -430,8 +480,9 @@ GHOSTTY_API GhosttyResult ghostty_render_state_end_update(GhosttyRenderState sta * @param state The render state handle (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 `state` is - * NULL or `data` is not a recognized enum value + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or + * `out` is NULL, `data` is not a recognized enum value, or a sized + * output struct is smaller than `sizeof(size_t)` * * @ingroup render */ @@ -486,24 +537,6 @@ GHOSTTY_API GhosttyResult ghostty_render_state_set(GhosttyRenderState state, GhosttyRenderStateOption option, const void* value); -/** - * 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 - */ -GHOSTTY_API GhosttyResult ghostty_render_state_colors_get(GhosttyRenderState state, - GhosttyRenderStateColors* out_colors); - /** * Create a new row iterator instance. * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index f7ce5b386..8faf86471 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -286,7 +286,6 @@ comptime { @export(&c.render_state_get, .{ .name = "ghostty_render_state_get" }); @export(&c.render_state_get_multi, .{ .name = "ghostty_render_state_get_multi" }); @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_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_get, .{ .name = "ghostty_render_state_row_get" }); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 3615984cc..d45bbfe26 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -90,7 +90,6 @@ pub const render_state_end_update = render.end_update; pub const render_state_get = render.get; pub const render_state_get_multi = render.get_multi; 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_get = render.row_get; diff --git a/src/terminal/c/render.zig b/src/terminal/c/render.zig index c5383558c..377767532 100644 --- a/src/terminal/c/render.zig +++ b/src/terminal/c/render.zig @@ -100,6 +100,19 @@ pub const CursorVisualStyle = enum(c_int) { } }; +/// C: GhosttyRenderStateCursor +pub const Cursor = extern struct { + size: usize = @sizeOf(Cursor), + viewport_has_value: bool, + viewport_x: u16, + viewport_y: u16, + wide_tail: bool, + visible: bool, + blinking: bool, + password_input: bool, + visual_style: CursorVisualStyle, +}; + /// C: GhosttyRenderStateData pub const Data = enum(c_int) { invalid = 0, @@ -120,6 +133,8 @@ pub const Data = enum(c_int) { cursor_viewport_x = 15, cursor_viewport_y = 16, cursor_viewport_wide_tail = 17, + cursor = 18, + colors = 19, /// Output type expected for querying the data of the given kind. pub fn OutType(comptime self: Data) type { @@ -135,6 +150,8 @@ pub const Data = enum(c_int) { .cursor_visible, .cursor_blinking, .cursor_password_input => bool, .cursor_viewport_has_value, .cursor_viewport_wide_tail => bool, .cursor_viewport_x, .cursor_viewport_y => size.CellCountInt, + .cursor => Cursor, + .colors => Colors, }; } }; @@ -270,12 +287,13 @@ inline fn getDispatch( }; } + const out_ptr = out orelse return .invalid_value; return switch (data) { .invalid => .invalid_value, inline else => |comptime_data| getTyped( state, comptime_data, - @ptrCast(@alignCast(out)), + @ptrCast(@alignCast(out_ptr)), ), }; } @@ -328,6 +346,8 @@ fn getTyped( const vp = state.state.cursor.viewport orelse return .invalid_value; out.* = vp.wide_tail; }, + .cursor => return writeCursor(state, out), + .colors => return writeColors(state, out), } return .success; @@ -367,12 +387,87 @@ fn setTyped( return .success; } -pub fn colors_get( - state_: RenderState, - out_colors_: ?*Colors, -) callconv(lib.calling_conv) Result { - const state = state_ orelse return .invalid_value; - const out_colors = out_colors_ orelse return .invalid_value; +fn writeCursor( + state: *RenderStateWrapper, + out_cursor: *Cursor, +) Result { + const out_size = out_cursor.size; + if (out_size < @sizeOf(usize)) return .invalid_value; + + const cursor = &state.state.cursor; + if (lib.structSizedFieldFits( + Cursor, + out_size, + "viewport_has_value", + )) { + out_cursor.viewport_has_value = cursor.viewport != null; + } + + if (cursor.viewport) |viewport| { + if (lib.structSizedFieldFits( + Cursor, + out_size, + "viewport_x", + )) { + out_cursor.viewport_x = viewport.x; + } + + if (lib.structSizedFieldFits( + Cursor, + out_size, + "viewport_y", + )) { + out_cursor.viewport_y = viewport.y; + } + + if (lib.structSizedFieldFits( + Cursor, + out_size, + "wide_tail", + )) { + out_cursor.wide_tail = viewport.wide_tail; + } + } + + if (lib.structSizedFieldFits( + Cursor, + out_size, + "visible", + )) { + out_cursor.visible = cursor.visible; + } + + if (lib.structSizedFieldFits( + Cursor, + out_size, + "blinking", + )) { + out_cursor.blinking = cursor.blinking; + } + + if (lib.structSizedFieldFits( + Cursor, + out_size, + "password_input", + )) { + out_cursor.password_input = cursor.password_input; + } + + if (lib.structSizedFieldFits( + Cursor, + out_size, + "visual_style", + )) { + out_cursor.visual_style = CursorVisualStyle.fromCursorStyle(cursor.visual_style); + } + + return .success; +} + +fn writeColors( + state: *RenderStateWrapper, + out_colors: *Colors, +) Result { const out_size = out_colors.size; if (out_size < @sizeOf(usize)) return .invalid_value; @@ -997,6 +1092,14 @@ test "render: begin/end update" { test "render: get invalid value" { var cols: size.CellCountInt = 0; try testing.expectEqual(Result.invalid_value, get(null, .cols, @ptrCast(&cols))); + + var state: RenderState = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &state, + )); + defer free(state); + try testing.expectEqual(Result.invalid_value, get(state, .cols, null)); } test "render: get invalid data" { @@ -1010,7 +1113,7 @@ test "render: get invalid data" { try testing.expectEqual(Result.invalid_value, get(state, .invalid, null)); } -test "render: colors get invalid value" { +test "render: aggregate get invalid value" { var state: RenderState = null; try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, @@ -1021,11 +1124,20 @@ test "render: colors get invalid value" { 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)); + try testing.expectEqual(Result.invalid_value, get(null, .colors, &colors)); + try testing.expectEqual(Result.invalid_value, get(state, .colors, null)); colors.size = @sizeOf(usize) - 1; - try testing.expectEqual(Result.invalid_value, colors_get(state, &colors)); + try testing.expectEqual(Result.invalid_value, get(state, .colors, &colors)); + + var cursor: Cursor = std.mem.zeroes(Cursor); + cursor.size = @sizeOf(Cursor); + + try testing.expectEqual(Result.invalid_value, get(null, .cursor, &cursor)); + try testing.expectEqual(Result.invalid_value, get(state, .cursor, null)); + + cursor.size = @sizeOf(usize) - 1; + try testing.expectEqual(Result.invalid_value, get(state, .cursor, &cursor)); } test "render: get/set dirty invalid value" { @@ -1738,7 +1850,7 @@ test "render: update" { try testing.expectEqual(@as(size.CellCountInt, 24), rows_val); } -test "render: colors get" { +test "render: colors data get" { var terminal: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, @@ -1759,7 +1871,7 @@ test "render: colors get" { var colors: Colors = std.mem.zeroes(Colors); colors.size = @sizeOf(Colors); - try testing.expectEqual(Result.success, colors_get(state, &colors)); + try testing.expectEqual(Result.success, get(state, .colors, &colors)); const state_colors = &state.?.state.colors; try testing.expectEqual(state_colors.background.cval(), colors.background); @@ -1777,6 +1889,121 @@ test "render: colors get" { } } +test "render: cursor data get matches scalar getters" { + var terminal: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib.alloc.test_allocator, + &terminal, + 80, + 24, + )); + 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 cursor: Cursor = std.mem.zeroes(Cursor); + cursor.size = @sizeOf(Cursor); + try testing.expectEqual(Result.success, get(state, .cursor, &cursor)); + + var viewport_has_value: bool = undefined; + var viewport_x: u16 = undefined; + var viewport_y: u16 = undefined; + var wide_tail: bool = undefined; + var visible: bool = undefined; + var blinking: bool = undefined; + var password_input: bool = undefined; + var visual_style: CursorVisualStyle = undefined; + try testing.expectEqual(Result.success, get(state, .cursor_viewport_has_value, &viewport_has_value)); + try testing.expectEqual(Result.success, get(state, .cursor_viewport_x, &viewport_x)); + try testing.expectEqual(Result.success, get(state, .cursor_viewport_y, &viewport_y)); + try testing.expectEqual(Result.success, get(state, .cursor_viewport_wide_tail, &wide_tail)); + try testing.expectEqual(Result.success, get(state, .cursor_visible, &visible)); + try testing.expectEqual(Result.success, get(state, .cursor_blinking, &blinking)); + try testing.expectEqual(Result.success, get(state, .cursor_password_input, &password_input)); + try testing.expectEqual(Result.success, get(state, .cursor_visual_style, &visual_style)); + + try testing.expectEqual(viewport_has_value, cursor.viewport_has_value); + try testing.expectEqual(viewport_x, cursor.viewport_x); + try testing.expectEqual(viewport_y, cursor.viewport_y); + try testing.expectEqual(wide_tail, cursor.wide_tail); + try testing.expectEqual(visible, cursor.visible); + try testing.expectEqual(blinking, cursor.blinking); + try testing.expectEqual(password_input, cursor.password_input); + try testing.expectEqual(visual_style, cursor.visual_style); +} + +test "render: cursor data get without viewport" { + var state: RenderState = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &state, + )); + defer free(state); + + state.?.state.cursor.viewport = null; + state.?.state.cursor.visible = true; + state.?.state.cursor.blinking = true; + state.?.state.cursor.password_input = true; + state.?.state.cursor.visual_style = .underline; + + var cursor: Cursor = std.mem.zeroes(Cursor); + cursor.size = @sizeOf(Cursor); + cursor.viewport_x = 0xAAAA; + cursor.viewport_y = 0xBBBB; + cursor.wide_tail = true; + try testing.expectEqual(Result.success, get(state, .cursor, &cursor)); + + try testing.expect(!cursor.viewport_has_value); + try testing.expectEqual(@as(u16, 0xAAAA), cursor.viewport_x); + try testing.expectEqual(@as(u16, 0xBBBB), cursor.viewport_y); + try testing.expect(cursor.wide_tail); + try testing.expect(cursor.visible); + try testing.expect(cursor.blinking); + try testing.expect(cursor.password_input); + try testing.expectEqual(CursorVisualStyle.underline, cursor.visual_style); +} + +test "render: cursor data get supports truncated sized struct" { + var terminal: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib.alloc.test_allocator, + &terminal, + 80, + 24, + )); + 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)); + const expected = state.?.state.cursor; + const viewport = expected.viewport.?; + + var cursor: Cursor = std.mem.zeroes(Cursor); + cursor.size = @offsetOf(Cursor, "viewport_y") + @sizeOf(u16); + cursor.wide_tail = !viewport.wide_tail; + cursor.visible = !expected.visible; + try testing.expectEqual(Result.success, get(state, .cursor, &cursor)); + + try testing.expect(cursor.viewport_has_value); + try testing.expectEqual(viewport.x, cursor.viewport_x); + try testing.expectEqual(viewport.y, cursor.viewport_y); + try testing.expectEqual(!viewport.wide_tail, cursor.wide_tail); + try testing.expectEqual(!expected.visible, cursor.visible); +} + test "render: row cells bg_color no background" { var terminal: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( @@ -2020,7 +2247,7 @@ test "render: row cells fg_color from style" { try testing.expectEqual(@as(u8, 30), fg.b); } -test "render: colors get supports truncated sized struct" { +test "render: colors data get supports truncated sized struct" { var terminal: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, @@ -2044,7 +2271,7 @@ test "render: colors get supports truncated sized struct" { 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)); + try testing.expectEqual(Result.success, get(state, .colors, &colors)); const state_colors = &state.?.state.colors; try testing.expectEqual(state_colors.palette[0].cval(), colors.palette[0]); diff --git a/src/terminal/c/types.zig b/src/terminal/c/types.zig index 2f8a011cf..681556666 100644 --- a/src/terminal/c/types.zig +++ b/src/terminal/c/types.zig @@ -70,6 +70,7 @@ pub const structs: std.StaticStringMap(StructInfo) = structs: { .{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) }, .{ "GhosttyReader", StructInfo.init(io.Reader) }, .{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) }, + .{ "GhosttyRenderStateCursor", StructInfo.init(render.Cursor) }, .{ "GhosttyRenderStateRowSelection", StructInfo.init(render.RowSelection) }, .{ "GhosttySelectionGestureBehaviors", StructInfo.init(selection_gesture.Behaviors) }, .{ "GhosttySelectionGestureGeometry", StructInfo.init(selection_gesture.Geometry) }, @@ -257,6 +258,7 @@ test "json parses" { "GhosttyPointCoordinate", "GhosttyReader", "GhosttyRenderStateColors", + "GhosttyRenderStateCursor", "GhosttyRenderStateRowSelection", "GhosttySelection", "GhosttySelectionGestureBehaviors", @@ -309,6 +311,18 @@ test "json parses" { try std.testing.expect(unknown_string_fields.contains("truncated")); try std.testing.expect(unknown_string_fields.contains("content")); + const render_cursor_fields = root.get("GhosttyRenderStateCursor").?.object + .get("fields").?.object; + try std.testing.expect(render_cursor_fields.contains("size")); + try std.testing.expect(render_cursor_fields.contains("viewport_has_value")); + try std.testing.expect(render_cursor_fields.contains("viewport_x")); + try std.testing.expect(render_cursor_fields.contains("viewport_y")); + try std.testing.expect(render_cursor_fields.contains("wide_tail")); + try std.testing.expect(render_cursor_fields.contains("visible")); + try std.testing.expect(render_cursor_fields.contains("blinking")); + try std.testing.expect(render_cursor_fields.contains("password_input")); + try std.testing.expect(render_cursor_fields.contains("visual_style")); + const reader_fields = root.get("GhosttyReader").?.object .get("fields").?.object; try std.testing.expect(reader_fields.contains("read"));