vt: add cell-level iteration and data access to render state row cells

Add next, select, and get functions to the render state row cells
API, mirroring the row iterator pattern. row_cells_next advances to
the next cell sequentially, row_cells_select jumps to a specific
column index with bounds validation, and row_cells_get queries data
for the current cell position.

The get function supports querying raw cell values (GhosttyCell),
resolved styles (GhosttyStyle), grapheme codepoint counts, and
writing grapheme codepoints into a caller-provided buffer.

Also add Cell.C and Cell.cval() to page.zig, matching the existing
Row.C/Row.cval() pattern, so the render state can convert cells to
the C ABI type without a raw bitCast.
This commit is contained in:
Mitchell Hashimoto
2026-03-20 08:47:16 -07:00
parent ecd1d0d1e1
commit 6ae17a02af
5 changed files with 184 additions and 0 deletions

View File

@@ -411,6 +411,84 @@ GhosttyResult ghostty_render_state_row_cells_new(
const GhosttyAllocator* allocator,
GhosttyRenderStateRowCells* out_cells);
/**
* Queryable data kinds for ghostty_render_state_row_cells_get().
*
* @ingroup render
*/
typedef enum {
/** Invalid / sentinel value. */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_INVALID = 0,
/** The raw cell value (GhosttyCell). */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW = 1,
/** The style for the current cell (GhosttyStyle). */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE = 2,
/** The total number of grapheme codepoints including the base codepoint
* (uint32_t). Returns 0 if the cell has no text. */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN = 3,
/** Write grapheme codepoints into a caller-provided buffer (uint32_t*).
* The buffer must be at least graphemes_len elements. The base codepoint
* is written first, followed by any extra codepoints. */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF = 4,
} GhosttyRenderStateRowCellsData;
/**
* Move a render-state row cells iterator to the next cell.
*
* Returns true if the iterator moved successfully and cell data is
* available to read at the new position.
*
* @param cells The row cells handle to advance (may be NULL)
* @return true if advanced to the next cell, false if `cells` is
* NULL or if the iterator has reached the end
*
* @ingroup render
*/
bool ghostty_render_state_row_cells_next(GhosttyRenderStateRowCells cells);
/**
* Move a render-state row cells iterator to a specific column.
*
* Positions the iterator at the given x (column) index so that
* subsequent reads return data for that cell.
*
* @param cells The row cells handle to reposition (NULL returns
* GHOSTTY_INVALID_VALUE)
* @param x The zero-based column index to select
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `cells`
* is NULL or `x` is out of range
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_row_cells_select(
GhosttyRenderStateRowCells cells, uint16_t x);
/**
* Get a value from the current cell in a render-state row cells iterator.
*
* The `out` pointer must point to a value of the type corresponding to the
* requested data kind (see GhosttyRenderStateRowCellsData).
* Call ghostty_render_state_row_cells_next() or
* ghostty_render_state_row_cells_select() at least once before
* calling this function.
*
* @param cells The row cells 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
* `cells` is NULL or the iterator is not positioned on a cell
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_row_cells_get(
GhosttyRenderStateRowCells cells,
GhosttyRenderStateRowCellsData data,
void* out);
/**
* Free a row cells instance.
*

View File

@@ -198,6 +198,9 @@ comptime {
@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_row_cells_new, .{ .name = "ghostty_render_state_row_cells_new" });
@export(&c.render_state_row_cells_next, .{ .name = "ghostty_render_state_row_cells_next" });
@export(&c.render_state_row_cells_select, .{ .name = "ghostty_render_state_row_cells_select" });
@export(&c.render_state_row_cells_get, .{ .name = "ghostty_render_state_row_cells_get" });
@export(&c.render_state_row_cells_free, .{ .name = "ghostty_render_state_row_cells_free" });
@export(&c.render_state_free, .{ .name = "ghostty_render_state_free" });
@export(&c.terminal_new, .{ .name = "ghostty_terminal_new" });

View File

@@ -48,6 +48,9 @@ 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 render_state_row_cells_new = render.row_cells_new;
pub const render_state_row_cells_next = render.row_cells_next;
pub const render_state_row_cells_select = render.row_cells_select;
pub const render_state_row_cells_get = render.row_cells_get;
pub const render_state_row_cells_free = render.row_cells_free;
pub const sgr_new = sgr.new;

View File

@@ -12,6 +12,7 @@ const terminal_c = @import("terminal.zig");
const renderpkg = @import("../render.zig");
const Result = @import("result.zig").Result;
const row = @import("row.zig");
const style_c = @import("style.zig");
const log = std.log.scoped(.render_state_c);
@@ -331,12 +332,103 @@ pub fn row_cells_new(
return .success;
}
pub fn row_cells_next(cells_: RowCells) callconv(.c) bool {
const cells = cells_ orelse return false;
const next_x: size.CellCountInt = if (cells.x) |x| x + 1 else 0;
if (next_x >= cells.raws.len) return false;
cells.x = next_x;
return true;
}
pub fn row_cells_select(cells_: RowCells, x: size.CellCountInt) callconv(.c) Result {
const cells = cells_ orelse return .invalid_value;
if (x >= cells.raws.len) return .invalid_value;
cells.x = x;
return .success;
}
pub fn row_cells_free(cells_: RowCells) callconv(.c) void {
const cells = cells_ orelse return;
const alloc = cells.alloc;
alloc.destroy(cells);
}
/// C: GhosttyRenderStateRowCellsData
pub const RowCellsData = enum(c_int) {
invalid = 0,
raw = 1,
style = 2,
graphemes_len = 3,
graphemes_buf = 4,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: RowCellsData) type {
return switch (self) {
.invalid => void,
.raw => page.Cell.C,
.style => style_c.Style,
.graphemes_len => u32,
.graphemes_buf => [*]u32,
};
}
};
pub fn row_cells_get(
cells_: RowCells,
data: RowCellsData,
out: ?*anyopaque,
) callconv(.c) Result {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(RowCellsData, @intFromEnum(data)) catch {
log.warn("render_state_row_cells_get invalid data value={d}", .{@intFromEnum(data)});
return .invalid_value;
};
}
return switch (data) {
inline else => |comptime_data| rowCellsGetTyped(
cells_,
comptime_data,
@ptrCast(@alignCast(out)),
),
};
}
fn rowCellsGetTyped(
cells_: RowCells,
comptime data: RowCellsData,
out: *data.OutType(),
) Result {
const cells = cells_ orelse return .invalid_value;
const x = cells.x orelse return .invalid_value;
const cell = cells.raws[x];
switch (data) {
.invalid => return .invalid_value,
.raw => out.* = cell.cval(),
.style => out.* = style_c.Style.fromStyle(cells.styles[x]),
.graphemes_len => {
if (!cell.hasText()) {
out.* = 0;
return .success;
}
const extra = if (cell.hasGrapheme()) cells.graphemes[x] else &[_]u21{};
out.* = @intCast(1 + extra.len);
},
.graphemes_buf => {
if (!cell.hasText()) return .success;
const extra = if (cell.hasGrapheme()) cells.graphemes[x] else &[_]u21{};
const total = 1 + extra.len;
const out_slice = out.*[0..total];
out_slice[0] = cell.codepoint();
for (extra, 1..) |cp, i| {
out_slice[i] = cp;
}
},
}
return .success;
}
/// C: GhosttyRenderStateRowData
pub const RowData = enum(c_int) {
invalid = 0,

View File

@@ -2059,6 +2059,14 @@ pub const Cell = packed struct(u64) {
prompt = 2,
};
/// C ABI type.
pub const C = u64;
/// Returns this cell as a C ABI value.
pub fn cval(self: Cell) C {
return @bitCast(self);
}
/// Helper to make a cell that just has a codepoint.
pub fn init(cp: u21) Cell {
// We have to use this bitCast here to ensure that our memory is