mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-19 14:00:29 +00:00
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:
@@ -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
158
src/terminal/c/cell.zig
Normal 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);
|
||||
}
|
||||
@@ -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
130
src/terminal/c/row.zig
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user