vt: add GhosttyCell and GhosttyRow C API with data getters

Add opaque GhosttyCell (uint64_t) and GhosttyRow (uint64_t) types that
bitcast to the internal packed Cell and Row structs from page.zig. Each
type has a corresponding data enum and getter function following the
same pattern as ghostty_terminal_get.

ghostty_cell_get supports extracting codepoint, content tag, wide
property, has_text, has_styling, style_id, has_hyperlink, protected,
and semantic_content. ghostty_row_get supports wrap, wrap_continuation,
grapheme, styled, hyperlink, semantic_prompt, kitty_virtual_placeholder,
and dirty.

The cell and row types and functions live in a new screen.h header,
separate from terminal.h, with terminal.h including screen.h for
convenience.
This commit is contained in:
Mitchell Hashimoto
2026-03-19 13:10:31 -07:00
parent d827225573
commit 5c8b9f3f43
7 changed files with 623 additions and 0 deletions

View File

@@ -103,6 +103,7 @@ extern "C" {
#include <ghostty/vt/modes.h>
#include <ghostty/vt/mouse.h>
#include <ghostty/vt/paste.h>
#include <ghostty/vt/screen.h>
#include <ghostty/vt/size_report.h>
#include <ghostty/vt/wasm.h>

323
include/ghostty/vt/screen.h Normal file
View File

@@ -0,0 +1,323 @@
/**
* @file screen.h
*
* Terminal screen cell and row types.
*/
#ifndef GHOSTTY_VT_SCREEN_H
#define GHOSTTY_VT_SCREEN_H
#include <stdbool.h>
#include <stdint.h>
#include <ghostty/vt/types.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup screen Screen
*
* Terminal screen cell and row types.
*
* These types represent the contents of a terminal screen. A GhosttyCell
* is a single grid cell and a GhosttyRow is a single row. Both are opaque
* values whose fields are accessed via ghostty_cell_get() and
* ghostty_row_get() respectively.
*
* @{
*/
/**
* Opaque cell value.
*
* Represents a single terminal cell. The internal layout is opaque and
* must be queried via ghostty_cell_get(). Obtain cell values from
* terminal query APIs.
*
* @ingroup screen
*/
typedef uint64_t GhosttyCell;
/**
* Opaque row value.
*
* Represents a single terminal row. The internal layout is opaque and
* must be queried via ghostty_row_get(). Obtain row values from
* terminal query APIs.
*
* @ingroup screen
*/
typedef uint64_t GhosttyRow;
/**
* Cell content tag.
*
* Describes what kind of content a cell holds.
*
* @ingroup screen
*/
typedef enum {
/** A single codepoint (may be zero for empty). */
GHOSTTY_CELL_CONTENT_CODEPOINT = 0,
/** A codepoint that is part of a multi-codepoint grapheme cluster. */
GHOSTTY_CELL_CONTENT_CODEPOINT_GRAPHEME = 1,
/** No text; background color from palette. */
GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE = 2,
/** No text; background color as RGB. */
GHOSTTY_CELL_CONTENT_BG_COLOR_RGB = 3,
} GhosttyCellContentTag;
/**
* Cell wide property.
*
* Describes the width behavior of a cell.
*
* @ingroup screen
*/
typedef enum {
/** Not a wide character, cell width 1. */
GHOSTTY_CELL_WIDE_NARROW = 0,
/** Wide character, cell width 2. */
GHOSTTY_CELL_WIDE_WIDE = 1,
/** Spacer after wide character. Do not render. */
GHOSTTY_CELL_WIDE_SPACER_TAIL = 2,
/** Spacer at end of soft-wrapped line for a wide character. */
GHOSTTY_CELL_WIDE_SPACER_HEAD = 3,
} GhosttyCellWide;
/**
* Semantic content type of a cell.
*
* Set by semantic prompt sequences (OSC 133) to distinguish between
* command output, user input, and shell prompt text.
*
* @ingroup screen
*/
typedef enum {
/** Regular output content, such as command output. */
GHOSTTY_CELL_SEMANTIC_OUTPUT = 0,
/** Content that is part of user input. */
GHOSTTY_CELL_SEMANTIC_INPUT = 1,
/** Content that is part of a shell prompt. */
GHOSTTY_CELL_SEMANTIC_PROMPT = 2,
} GhosttyCellSemanticContent;
/**
* Cell data types.
*
* These values specify what type of data to extract from a cell
* using `ghostty_cell_get`.
*
* @ingroup screen
*/
typedef enum {
/** Invalid data type. Never results in any data extraction. */
GHOSTTY_CELL_DATA_INVALID = 0,
/**
* The codepoint of the cell (0 if empty or bg-color-only).
*
* Output type: uint32_t *
*/
GHOSTTY_CELL_DATA_CODEPOINT = 1,
/**
* The content tag describing what kind of content is in the cell.
*
* Output type: GhosttyCellContentTag *
*/
GHOSTTY_CELL_DATA_CONTENT_TAG = 2,
/**
* The wide property of the cell.
*
* Output type: GhosttyCellWide *
*/
GHOSTTY_CELL_DATA_WIDE = 3,
/**
* Whether the cell has text to render.
*
* Output type: bool *
*/
GHOSTTY_CELL_DATA_HAS_TEXT = 4,
/**
* Whether the cell has non-default styling.
*
* Output type: bool *
*/
GHOSTTY_CELL_DATA_HAS_STYLING = 5,
/**
* The style ID for the cell (for use with style lookups).
*
* Output type: uint16_t *
*/
GHOSTTY_CELL_DATA_STYLE_ID = 6,
/**
* Whether the cell has a hyperlink.
*
* Output type: bool *
*/
GHOSTTY_CELL_DATA_HAS_HYPERLINK = 7,
/**
* Whether the cell is protected.
*
* Output type: bool *
*/
GHOSTTY_CELL_DATA_PROTECTED = 8,
/**
* The semantic content type of the cell (from OSC 133).
*
* Output type: GhosttyCellSemanticContent *
*/
GHOSTTY_CELL_DATA_SEMANTIC_CONTENT = 9,
} GhosttyCellData;
/**
* Row semantic prompt state.
*
* Indicates whether any cells in a row are part of a shell prompt,
* as reported by OSC 133 sequences.
*
* @ingroup screen
*/
typedef enum {
/** No prompt cells in this row. */
GHOSTTY_ROW_SEMANTIC_NONE = 0,
/** Prompt cells exist and this is a primary prompt line. */
GHOSTTY_ROW_SEMANTIC_PROMPT = 1,
/** Prompt cells exist and this is a continuation line. */
GHOSTTY_ROW_SEMANTIC_PROMPT_CONTINUATION = 2,
} GhosttyRowSemanticPrompt;
/**
* Row data types.
*
* These values specify what type of data to extract from a row
* using `ghostty_row_get`.
*
* @ingroup screen
*/
typedef enum {
/** Invalid data type. Never results in any data extraction. */
GHOSTTY_ROW_DATA_INVALID = 0,
/**
* Whether this row is soft-wrapped.
*
* Output type: bool *
*/
GHOSTTY_ROW_DATA_WRAP = 1,
/**
* Whether this row is a continuation of a soft-wrapped row.
*
* Output type: bool *
*/
GHOSTTY_ROW_DATA_WRAP_CONTINUATION = 2,
/**
* Whether any cells in this row have grapheme clusters.
*
* Output type: bool *
*/
GHOSTTY_ROW_DATA_GRAPHEME = 3,
/**
* Whether any cells in this row have styling (may have false positives).
*
* Output type: bool *
*/
GHOSTTY_ROW_DATA_STYLED = 4,
/**
* Whether any cells in this row have hyperlinks (may have false positives).
*
* Output type: bool *
*/
GHOSTTY_ROW_DATA_HYPERLINK = 5,
/**
* The semantic prompt state of this row.
*
* Output type: GhosttyRowSemanticPrompt *
*/
GHOSTTY_ROW_DATA_SEMANTIC_PROMPT = 6,
/**
* Whether this row contains a Kitty virtual placeholder.
*
* Output type: bool *
*/
GHOSTTY_ROW_DATA_KITTY_VIRTUAL_PLACEHOLDER = 7,
/**
* Whether this row is dirty and requires a redraw.
*
* Output type: bool *
*/
GHOSTTY_ROW_DATA_DIRTY = 8,
} GhosttyRowData;
/**
* Get data from a cell.
*
* Extracts typed data from the given cell based on the specified
* data type. The output pointer must be of the appropriate type for the
* requested data kind. Valid data types and output types are documented
* in the `GhosttyCellData` enum.
*
* @param cell The cell value
* @param data The type of data to extract
* @param out Pointer to store the extracted data (type depends on data parameter)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the
* data type is invalid
*
* @ingroup screen
*/
GhosttyResult ghostty_cell_get(GhosttyCell cell,
GhosttyCellData data,
void *out);
/**
* Get data from a row.
*
* Extracts typed data from the given row based on the specified
* data type. The output pointer must be of the appropriate type for the
* requested data kind. Valid data types and output types are documented
* in the `GhosttyRowData` enum.
*
* @param row The row value
* @param data The type of data to extract
* @param out Pointer to store the extracted data (type depends on data parameter)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the
* data type is invalid
*
* @ingroup screen
*/
GhosttyResult ghostty_row_get(GhosttyRow row,
GhosttyRowData data,
void *out);
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_SCREEN_H */

View File

@@ -13,6 +13,7 @@
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/modes.h>
#include <ghostty/vt/screen.h>
#include <ghostty/vt/style.h>
#ifdef __cplusplus

View File

@@ -171,6 +171,8 @@ comptime {
@export(&c.size_report_encode, .{ .name = "ghostty_size_report_encode" });
@export(&c.style_default, .{ .name = "ghostty_style_default" });
@export(&c.style_is_default, .{ .name = "ghostty_style_is_default" });
@export(&c.cell_get, .{ .name = "ghostty_cell_get" });
@export(&c.row_get, .{ .name = "ghostty_row_get" });
@export(&c.color_rgb_get, .{ .name = "ghostty_color_rgb_get" });
@export(&c.sgr_new, .{ .name = "ghostty_sgr_new" });
@export(&c.sgr_free, .{ .name = "ghostty_sgr_free" });

158
src/terminal/c/cell.zig Normal file
View File

@@ -0,0 +1,158 @@
const std = @import("std");
const testing = std.testing;
const page = @import("../page.zig");
const Cell = page.Cell;
const style_c = @import("style.zig");
const Result = @import("result.zig").Result;
/// C: GhosttyCell
pub const CCell = u64;
/// C: GhosttyCellContentTag
pub const ContentTag = enum(c_int) {
codepoint = 0,
codepoint_grapheme = 1,
bg_color_palette = 2,
bg_color_rgb = 3,
};
/// C: GhosttyCellWide
pub const Wide = enum(c_int) {
narrow = 0,
wide = 1,
spacer_tail = 2,
spacer_head = 3,
};
/// C: GhosttyCellSemanticContent
pub const SemanticContent = enum(c_int) {
output = 0,
input = 1,
prompt = 2,
};
/// C: GhosttyCellData
pub const CellData = enum(c_int) {
invalid = 0,
/// The codepoint of the cell (0 if empty or bg-color-only).
/// Output type: uint32_t * (stored as u21, zero-extended)
codepoint = 1,
/// The content tag describing what kind of content is in the cell.
/// Output type: GhosttyCellContentTag *
content_tag = 2,
/// The wide property of the cell.
/// Output type: GhosttyCellWide *
wide = 3,
/// Whether the cell has text to render.
/// Output type: bool *
has_text = 4,
/// Whether the cell has styling (non-default style).
/// Output type: bool *
has_styling = 5,
/// The style ID for the cell (for use with style lookups).
/// Output type: uint16_t *
style_id = 6,
/// Whether the cell has a hyperlink.
/// Output type: bool *
has_hyperlink = 7,
/// Whether the cell is protected.
/// Output type: bool *
protected = 8,
/// The semantic content type of the cell (from OSC 133).
/// Output type: GhosttyCellSemanticContent *
semantic_content = 9,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: CellData) type {
return switch (self) {
.invalid => void,
.codepoint => u32,
.content_tag => ContentTag,
.wide => Wide,
.has_text, .has_styling, .has_hyperlink, .protected => bool,
.style_id => u16,
.semantic_content => SemanticContent,
};
}
};
pub fn get(
cell_: CCell,
data: CellData,
out: ?*anyopaque,
) callconv(.c) Result {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(CellData, @intFromEnum(data)) catch {
return .invalid_value;
};
}
return switch (data) {
inline else => |comptime_data| getTyped(
cell_,
comptime_data,
@ptrCast(@alignCast(out)),
),
};
}
fn getTyped(
cell_: CCell,
comptime data: CellData,
out: *data.OutType(),
) Result {
const cell: Cell = @bitCast(cell_);
switch (data) {
.invalid => return .invalid_value,
.codepoint => out.* = @intCast(cell.codepoint()),
.content_tag => out.* = @enumFromInt(@intFromEnum(cell.content_tag)),
.wide => out.* = @enumFromInt(@intFromEnum(cell.wide)),
.has_text => out.* = cell.hasText(),
.has_styling => out.* = cell.hasStyling(),
.style_id => out.* = cell.style_id,
.has_hyperlink => out.* = cell.hyperlink,
.protected => out.* = cell.protected,
.semantic_content => out.* = @enumFromInt(@intFromEnum(cell.semantic_content)),
}
return .success;
}
test "get codepoint" {
const cell: CCell = @bitCast(Cell.init('A'));
var cp: u32 = 0;
try testing.expectEqual(Result.success, get(cell, .codepoint, @ptrCast(&cp)));
try testing.expectEqual(@as(u32, 'A'), cp);
}
test "get has_text" {
const cell: CCell = @bitCast(Cell.init('A'));
var has: bool = false;
try testing.expectEqual(Result.success, get(cell, .has_text, @ptrCast(&has)));
try testing.expect(has);
}
test "get empty cell" {
const cell: CCell = @bitCast(Cell.init(0));
var has: bool = true;
try testing.expectEqual(Result.success, get(cell, .has_text, @ptrCast(&has)));
try testing.expect(!has);
}
test "get wide" {
var zig_cell = Cell.init('A');
zig_cell.wide = .wide;
const cell: CCell = @bitCast(zig_cell);
var w: Wide = .narrow;
try testing.expectEqual(Result.success, get(cell, .wide, @ptrCast(&w)));
try testing.expectEqual(Wide.wide, w);
}

View File

@@ -1,3 +1,4 @@
pub const cell = @import("cell.zig");
pub const color = @import("color.zig");
pub const focus = @import("focus.zig");
pub const formatter = @import("formatter.zig");
@@ -8,6 +9,7 @@ pub const key_encode = @import("key_encode.zig");
pub const mouse_event = @import("mouse_event.zig");
pub const mouse_encode = @import("mouse_encode.zig");
pub const paste = @import("paste.zig");
pub const row = @import("row.zig");
pub const sgr = @import("sgr.zig");
pub const size_report = @import("size_report.zig");
pub const style = @import("style.zig");
@@ -91,6 +93,10 @@ pub const paste_is_safe = paste.is_safe;
pub const size_report_encode = size_report.encode;
pub const cell_get = cell.get;
pub const row_get = row.get;
pub const style_default = style.default_style;
pub const style_is_default = style.style_is_default;
@@ -105,7 +111,9 @@ pub const terminal_mode_set = terminal.mode_set;
pub const terminal_get = terminal.get;
test {
_ = cell;
_ = color;
_ = row;
_ = focus;
_ = formatter;
_ = modes;

130
src/terminal/c/row.zig Normal file
View File

@@ -0,0 +1,130 @@
const std = @import("std");
const testing = std.testing;
const page = @import("../page.zig");
const Row = page.Row;
const Result = @import("result.zig").Result;
/// C: GhosttyRow
pub const CRow = u64;
/// C: GhosttyRowSemanticPrompt
pub const SemanticPrompt = enum(c_int) {
none = 0,
prompt = 1,
prompt_continuation = 2,
};
/// C: GhosttyRowData
pub const RowData = enum(c_int) {
invalid = 0,
/// Whether this row is soft-wrapped.
/// Output type: bool *
wrap = 1,
/// Whether this row is a continuation of a soft-wrapped row.
/// Output type: bool *
wrap_continuation = 2,
/// Whether any cells in this row have grapheme clusters.
/// Output type: bool *
grapheme = 3,
/// Whether any cells in this row have styling (may have false positives).
/// Output type: bool *
styled = 4,
/// Whether any cells in this row have hyperlinks (may have false positives).
/// Output type: bool *
hyperlink = 5,
/// The semantic prompt state of this row.
/// Output type: GhosttyRowSemanticPrompt *
semantic_prompt = 6,
/// Whether this row contains a Kitty virtual placeholder.
/// Output type: bool *
kitty_virtual_placeholder = 7,
/// Whether this row is dirty and requires a redraw.
/// Output type: bool *
dirty = 8,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: RowData) type {
return switch (self) {
.invalid => void,
.wrap, .wrap_continuation, .grapheme, .styled, .hyperlink => bool,
.kitty_virtual_placeholder, .dirty => bool,
.semantic_prompt => SemanticPrompt,
};
}
};
pub fn get(
row_: CRow,
data: RowData,
out: ?*anyopaque,
) callconv(.c) Result {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(RowData, @intFromEnum(data)) catch {
return .invalid_value;
};
}
return switch (data) {
inline else => |comptime_data| getTyped(
row_,
comptime_data,
@ptrCast(@alignCast(out)),
),
};
}
fn getTyped(
row_: CRow,
comptime data: RowData,
out: *data.OutType(),
) Result {
const row: Row = @bitCast(row_);
switch (data) {
.invalid => return .invalid_value,
.wrap => out.* = row.wrap,
.wrap_continuation => out.* = row.wrap_continuation,
.grapheme => out.* = row.grapheme,
.styled => out.* = row.styled,
.hyperlink => out.* = row.hyperlink,
.semantic_prompt => out.* = @enumFromInt(@intFromEnum(row.semantic_prompt)),
.kitty_virtual_placeholder => out.* = row.kitty_virtual_placeholder,
.dirty => out.* = row.dirty,
}
return .success;
}
test "get wrap" {
var zig_row: Row = @bitCast(@as(u64, 0));
zig_row.wrap = true;
const row: CRow = @bitCast(zig_row);
var wrap: bool = false;
try testing.expectEqual(Result.success, get(row, .wrap, @ptrCast(&wrap)));
try testing.expect(wrap);
}
test "get semantic_prompt" {
var zig_row: Row = @bitCast(@as(u64, 0));
zig_row.semantic_prompt = .prompt;
const row: CRow = @bitCast(zig_row);
var sp: SemanticPrompt = .none;
try testing.expectEqual(Result.success, get(row, .semantic_prompt, @ptrCast(&sp)));
try testing.expectEqual(SemanticPrompt.prompt, sp);
}
test "get dirty" {
var zig_row: Row = @bitCast(@as(u64, 0));
zig_row.dirty = true;
const row: CRow = @bitCast(zig_row);
var dirty: bool = false;
try testing.expectEqual(Result.success, get(row, .dirty, @ptrCast(&dirty)));
try testing.expect(dirty);
}