From 5c8b9f3f434abee1e70f454ec00301010ea01edf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Mar 2026 13:10:31 -0700 Subject: [PATCH] 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. --- include/ghostty/vt.h | 1 + include/ghostty/vt/screen.h | 323 ++++++++++++++++++++++++++++++++++ include/ghostty/vt/terminal.h | 1 + src/lib_vt.zig | 2 + src/terminal/c/cell.zig | 158 +++++++++++++++++ src/terminal/c/main.zig | 8 + src/terminal/c/row.zig | 130 ++++++++++++++ 7 files changed, 623 insertions(+) create mode 100644 include/ghostty/vt/screen.h create mode 100644 src/terminal/c/cell.zig create mode 100644 src/terminal/c/row.zig diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index effafd0f0..de690577d 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -103,6 +103,7 @@ extern "C" { #include #include #include +#include #include #include diff --git a/include/ghostty/vt/screen.h b/include/ghostty/vt/screen.h new file mode 100644 index 000000000..64207ce13 --- /dev/null +++ b/include/ghostty/vt/screen.h @@ -0,0 +1,323 @@ +/** + * @file screen.h + * + * Terminal screen cell and row types. + */ + +#ifndef GHOSTTY_VT_SCREEN_H +#define GHOSTTY_VT_SCREEN_H + +#include +#include +#include + +#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 */ diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 8ebd4e7dd..d0b14977f 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #ifdef __cplusplus diff --git a/src/lib_vt.zig b/src/lib_vt.zig index bf59d646b..5494019c4 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -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" }); diff --git a/src/terminal/c/cell.zig b/src/terminal/c/cell.zig new file mode 100644 index 000000000..493d89082 --- /dev/null +++ b/src/terminal/c/cell.zig @@ -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); +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index b195b4d4c..584f04f43 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -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; diff --git a/src/terminal/c/row.zig b/src/terminal/c/row.zig new file mode 100644 index 000000000..b67c98b3c --- /dev/null +++ b/src/terminal/c/row.zig @@ -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); +}