From 549824842dd72b2e77caf0d443a3b3951480c764 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Mar 2026 19:45:19 -0700 Subject: [PATCH] vt: add style and grapheme accessors Add ghostty_grid_ref_style and ghostty_grid_ref_graphemes to the grid ref C API, allowing callers to extract the full style and grapheme cluster directly from a grid reference without manually resolving the page internals. --- include/ghostty/vt/grid_ref.h | 41 ++++++++++++++++++ include/ghostty/vt/style.h | 10 +++++ src/lib_vt.zig | 2 + src/terminal/c/grid_ref.zig | 79 +++++++++++++++++++++++++++++++++++ src/terminal/c/main.zig | 2 + 5 files changed, 134 insertions(+) diff --git a/include/ghostty/vt/grid_ref.h b/include/ghostty/vt/grid_ref.h index 51260e35f..0b196dce5 100644 --- a/include/ghostty/vt/grid_ref.h +++ b/include/ghostty/vt/grid_ref.h @@ -12,6 +12,7 @@ #include #include #include +#include #ifdef __cplusplus extern "C" { @@ -77,6 +78,46 @@ GhosttyResult ghostty_grid_ref_cell(const GhosttyGridRef *ref, GhosttyResult ghostty_grid_ref_row(const GhosttyGridRef *ref, GhosttyRow *out_row); +/** + * Get the grapheme cluster codepoints for the cell at the grid reference's + * position. + * + * Writes the full grapheme cluster (the cell's primary codepoint followed by + * any combining codepoints) into the provided buffer. If the cell has no text, + * out_len is set to 0 and GHOSTTY_SUCCESS is returned. + * + * If the buffer is too small (or NULL), the function returns + * GHOSTTY_OUT_OF_SPACE and writes the required number of codepoints to + * out_len. The caller can then retry with a sufficiently sized buffer. + * + * @param ref Pointer to the grid reference + * @param buf Output buffer of uint32_t codepoints (may be NULL) + * @param buf_len Number of uint32_t elements in the buffer + * @param[out] out_len On success, the number of codepoints written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size in codepoints. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's + * node is NULL, GHOSTTY_OUT_OF_SPACE if the buffer is too small + * + * @ingroup grid_ref + */ +GhosttyResult ghostty_grid_ref_graphemes(const GhosttyGridRef *ref, + uint32_t *buf, + size_t buf_len, + size_t *out_len); + +/** + * Get the style of the cell at the grid reference's position. + * + * @param ref Pointer to the grid reference + * @param[out] out_style On success, set to the cell's style (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's + * node is NULL + * + * @ingroup grid_ref + */ +GhosttyResult ghostty_grid_ref_style(const GhosttyGridRef *ref, + GhosttyStyle *out_style); + /** @} */ #ifdef __cplusplus diff --git a/include/ghostty/vt/style.h b/include/ghostty/vt/style.h index 603f88d01..ac5cd2ad6 100644 --- a/include/ghostty/vt/style.h +++ b/include/ghostty/vt/style.h @@ -28,6 +28,16 @@ extern "C" { * @{ */ +/** + * Style identifier type. + * + * Used to look up the full style from a grid reference. + * Obtain this from a cell via GHOSTTY_CELL_DATA_STYLE_ID. + * + * @ingroup style + */ +typedef uint16_t GhosttyStyleId; + /** * Style color tags. * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 1b17eed99..d6cfe49ea 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -199,6 +199,8 @@ comptime { @export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" }); @export(&c.grid_ref_cell, .{ .name = "ghostty_grid_ref_cell" }); @export(&c.grid_ref_row, .{ .name = "ghostty_grid_ref_row" }); + @export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" }); + @export(&c.grid_ref_style, .{ .name = "ghostty_grid_ref_style" }); // On Wasm we need to export our allocator convenience functions. if (builtin.target.cpu.arch.isWasm()) { diff --git a/src/terminal/c/grid_ref.zig b/src/terminal/c/grid_ref.zig index 592ee135f..d6afb0c45 100644 --- a/src/terminal/c/grid_ref.zig +++ b/src/terminal/c/grid_ref.zig @@ -3,8 +3,10 @@ const testing = std.testing; const page = @import("../page.zig"); const PageList = @import("../PageList.zig"); const size = @import("../size.zig"); +const stylepkg = @import("../style.zig"); const cell_c = @import("cell.zig"); const row_c = @import("row.zig"); +const style_c = @import("style.zig"); const Result = @import("result.zig").Result; /// C: GhosttyGridRef @@ -53,6 +55,58 @@ pub fn grid_ref_row( return .success; } +pub fn grid_ref_graphemes( + ref: *const CGridRef, + out_buf: ?[*]u32, + buf_len: usize, + out_len: *usize, +) callconv(.c) Result { + const p = ref.toPin() orelse return .invalid_value; + const cell = p.rowAndCell().cell; + + if (!cell.hasText()) { + out_len.* = 0; + return .success; + } + + const cp = cell.codepoint(); + const extra = if (cell.hasGrapheme()) p.grapheme(cell) else null; + const total = 1 + if (extra) |e| e.len else 0; + + if (out_buf == null or buf_len < total) { + out_len.* = total; + return .out_of_space; + } + + const buf = out_buf.?[0..buf_len]; + buf[0] = cp; + if (extra) |e| for (e, 1..) |c, i| { + buf[i] = c; + }; + + out_len.* = total; + return .success; +} + +pub fn grid_ref_style( + ref: *const CGridRef, + out: ?*style_c.Style, +) callconv(.c) Result { + const p = ref.toPin() orelse return .invalid_value; + if (out) |o| { + const cell = p.rowAndCell().cell; + if (cell.style_id == stylepkg.default_id) { + o.* = .fromStyle(.{}); + } else { + o.* = .fromStyle(p.node.data.styles.get( + p.node.data.memory, + cell.style_id, + ).*); + } + } + return .success; +} + test "grid_ref_cell null node" { const ref = CGridRef{}; var out: cell_c.CCell = undefined; @@ -74,3 +128,28 @@ test "grid_ref_row null out" { const ref = CGridRef{}; try testing.expectEqual(Result.invalid_value, grid_ref_row(&ref, null)); } + +test "grid_ref_graphemes null node" { + const ref = CGridRef{}; + var len: usize = undefined; + try testing.expectEqual(Result.invalid_value, grid_ref_graphemes(&ref, null, 0, &len)); +} + +test "grid_ref_graphemes null buf returns out_of_space" { + const ref = CGridRef{}; + var len: usize = undefined; + // With null node this returns invalid_value before checking the buffer, + // so we can only test null node here. Full buffer tests require a real page. + try testing.expectEqual(Result.invalid_value, grid_ref_graphemes(&ref, null, 0, &len)); +} + +test "grid_ref_style null node" { + const ref = CGridRef{}; + var out: style_c.Style = undefined; + try testing.expectEqual(Result.invalid_value, grid_ref_style(&ref, &out)); +} + +test "grid_ref_style null out" { + const ref = CGridRef{}; + try testing.expectEqual(Result.invalid_value, grid_ref_style(&ref, null)); +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 962e616cc..8964610df 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -114,6 +114,8 @@ pub const terminal_grid_ref = terminal.grid_ref; const grid_ref = @import("grid_ref.zig"); pub const grid_ref_cell = grid_ref.grid_ref_cell; pub const grid_ref_row = grid_ref.grid_ref_row; +pub const grid_ref_graphemes = grid_ref.grid_ref_graphemes; +pub const grid_ref_style = grid_ref.grid_ref_style; test { _ = cell;