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.
pull/11676/head
Mitchell Hashimoto 2026-03-19 19:45:19 -07:00
parent df8813bf1b
commit 549824842d
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
5 changed files with 134 additions and 0 deletions

View File

@ -12,6 +12,7 @@
#include <stdint.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/screen.h>
#include <ghostty/vt/style.h>
#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

View File

@ -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.
*

View File

@ -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()) {

View File

@ -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));
}

View File

@ -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;