diff --git a/include/ghostty/vt/render.h b/include/ghostty/vt/render.h index 4081a9d15..e268c54eb 100644 --- a/include/ghostty/vt/render.h +++ b/include/ghostty/vt/render.h @@ -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. * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index c6f019f76..6b6d0ad77 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -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" }); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index ba456e13d..d58a81378 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -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; diff --git a/src/terminal/c/render.zig b/src/terminal/c/render.zig index f39cb6188..f097eba28 100644 --- a/src/terminal/c/render.zig +++ b/src/terminal/c/render.zig @@ -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, diff --git a/src/terminal/page.zig b/src/terminal/page.zig index d013d1590..6364b823e 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -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