libghostty: terminal data, grid point and cell inspection APIs (#11676)

This adds a complete set of APIs for inspecting individual cells and
rows in the terminal grid from C. Callers can now resolve any point in
the grid to a reference, then extract codepoints, grapheme clusters,
styles, wide-character state, semantic prompt tags, and row-level
metadata like wrap and dirty flags.

This also adds a robust `ghostty_terminal_get` API for extracting
information like rows, cols, active screen, cursor information, etc.
from the terminal.

## Example

```c
// Write bold red text via SGR sequences
const char *text = "\033[1;31mHello\033[0m";
ghostty_terminal_vt_write(terminal, (const uint8_t *)text, strlen(text));

// Resolve cell (0,0) to a grid reference
GhosttyGridRef ref = GHOSTTY_INIT_SIZED(GhosttyGridRef);
GhosttyPoint pt = {
  .tag = GHOSTTY_POINT_TAG_ACTIVE,
  .value = { .coordinate = { .x = 0, .y = 0 } },
};
ghostty_terminal_grid_ref(terminal, pt, &ref);

// Read the codepoint ('H')
GhosttyCell cell;
ghostty_grid_ref_cell(&ref, &cell);
uint32_t codepoint = 0;
ghostty_cell_get(cell, GHOSTTY_CELL_DATA_CODEPOINT, &codepoint);

// Read the resolved style (bold=true, fg=red)
GhosttyStyle style = GHOSTTY_INIT_SIZED(GhosttyStyle);
ghostty_grid_ref_style(&ref, &style);
assert(style.bold);
```

## API Changes

### New Types

| Type | Description |
|------|-------------|
| `GhosttyCell` | Opaque 64-bit cell value |
| `GhosttyRow` | Opaque 64-bit row value |
| `GhosttyCellData` | Enum for `ghostty_cell_get` data kinds (codepoint,
content tag, wide, has_text, etc.) |
| `GhosttyCellContentTag` | Cell content kind (codepoint, grapheme, bg
color palette/RGB) |
| `GhosttyCellWide` | Cell width (narrow, wide, spacer tail/head) |
| `GhosttyCellSemanticContent` | Semantic content type (output, input,
prompt) |
| `GhosttyRowData` | Enum for `ghostty_row_get` data kinds (wrap,
grapheme, styled, dirty, etc.) |
| `GhosttyRowSemanticPrompt` | Row-level semantic prompt state |
| `GhosttyGridRef` | Sized struct — resolved reference to a cell
position in the page structure |
| `GhosttyPoint` | Tagged union specifying a grid position in a given
coordinate system |
| `GhosttyPointTag` | Coordinate system tag: `ACTIVE`, `VIEWPORT`,
`SCREEN`, `HISTORY` |
| `GhosttyPointCoordinate` | x/y coordinate pair |
| `GhosttyStyleId` | Style identifier type (uint16) |

### New Functions

| Function | Description |
|----------|-------------|
| `ghostty_cell_get` | Extract typed data from a cell (codepoint, wide,
style ID, etc.) |
| `ghostty_row_get` | Extract typed data from a row (wrap, dirty,
semantic prompt, etc.) |
| `ghostty_terminal_grid_ref` | Resolve a `GhosttyPoint` to a
`GhosttyGridRef` |
| `ghostty_grid_ref_cell` | Extract the `GhosttyCell` from a grid ref |
| `ghostty_grid_ref_row` | Extract the `GhosttyRow` from a grid ref |
| `ghostty_grid_ref_graphemes` | Get the full grapheme cluster
(codepoints) for the cell |
| `ghostty_grid_ref_style` | Get the resolved `GhosttyStyle` for the
cell |
This commit is contained in:
Mitchell Hashimoto
2026-03-19 20:12:56 -07:00
committed by GitHub
19 changed files with 1999 additions and 42 deletions

View File

@@ -0,0 +1,19 @@
# Example: `ghostty-vt` Grid Traversal
This contains a simple example of how to use the `ghostty-vt` terminal and
grid reference APIs to create a terminal, write content into it, and then
traverse the entire grid cell-by-cell using grid refs to inspect codepoints,
row state, and styles.
This uses a `build.zig` and `Zig` to build the C program so that we
can reuse a lot of our build logic and depend directly on our source
tree, but Ghostty emits a standard C library that can be used with any
C tooling.
## Usage
Run the program:
```shell-session
zig build run
```

View File

@@ -0,0 +1,42 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const run_step = b.step("run", "Run the app");
const exe_mod = b.createModule(.{
.target = target,
.optimize = optimize,
});
exe_mod.addCSourceFiles(.{
.root = b.path("src"),
.files = &.{"main.c"},
});
// You'll want to use a lazy dependency here so that ghostty is only
// downloaded if you actually need it.
if (b.lazyDependency("ghostty", .{
// Setting simd to false will force a pure static build that
// doesn't even require libc, but it has a significant performance
// penalty. If your embedding app requires libc anyway, you should
// always keep simd enabled.
// .simd = false,
})) |dep| {
exe_mod.linkLibrary(dep.artifact("ghostty-vt"));
}
// Exe
const exe = b.addExecutable(.{
.name = "c_vt_grid_traverse",
.root_module = exe_mod,
});
b.installArtifact(exe);
// Run
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
run_step.dependOn(&run_cmd.step);
}

View File

@@ -0,0 +1,24 @@
.{
.name = .c_vt_grid_traverse,
.version = "0.0.0",
.fingerprint = 0xf694dd12db9be040,
.minimum_zig_version = "0.15.1",
.dependencies = .{
// Ghostty dependency. In reality, you'd probably use a URL-based
// dependency like the one showed (and commented out) below this one.
// We use a path dependency here for simplicity and to ensure our
// examples always test against the source they're bundled with.
.ghostty = .{ .path = "../../" },
// Example of what a URL-based dependency looks like:
// .ghostty = .{
// .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz",
// .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s",
// },
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@@ -0,0 +1,85 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <ghostty/vt.h>
//! [grid-ref-traverse]
int main() {
// Create a small terminal
GhosttyTerminal terminal;
GhosttyTerminalOptions opts = {
.cols = 10,
.rows = 3,
.max_scrollback = 0,
};
GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts);
assert(result == GHOSTTY_SUCCESS);
// Write some content so the grid has interesting data
const char *text = "Hello!\r\n" // Row 0: H e l l o !
"World\r\n" // Row 1: W o r l d
"\033[1mBold"; // Row 2: B o l d (bold style)
ghostty_terminal_vt_write(
terminal, (const uint8_t *)text, strlen(text));
// Get terminal dimensions
uint16_t cols, rows;
ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_COLS, &cols);
ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_ROWS, &rows);
// Traverse the entire grid using grid refs
for (uint16_t row = 0; row < rows; row++) {
printf("Row %u: ", row);
for (uint16_t col = 0; col < cols; col++) {
// Resolve the point to a grid reference
GhosttyGridRef ref = GHOSTTY_INIT_SIZED(GhosttyGridRef);
GhosttyPoint pt = {
.tag = GHOSTTY_POINT_TAG_ACTIVE,
.value = { .coordinate = { .x = col, .y = row } },
};
result = ghostty_terminal_grid_ref(terminal, pt, &ref);
assert(result == GHOSTTY_SUCCESS);
// Read the cell from the grid ref
GhosttyCell cell;
result = ghostty_grid_ref_cell(&ref, &cell);
assert(result == GHOSTTY_SUCCESS);
// Check if the cell has text
bool has_text = false;
ghostty_cell_get(cell, GHOSTTY_CELL_DATA_HAS_TEXT, &has_text);
if (has_text) {
uint32_t codepoint = 0;
ghostty_cell_get(cell, GHOSTTY_CELL_DATA_CODEPOINT, &codepoint);
printf("%c", (char)codepoint);
} else {
printf(".");
}
}
// Also inspect the row for wrap state
GhosttyGridRef ref = GHOSTTY_INIT_SIZED(GhosttyGridRef);
GhosttyPoint pt = {
.tag = GHOSTTY_POINT_TAG_ACTIVE,
.value = { .coordinate = { .x = 0, .y = row } },
};
ghostty_terminal_grid_ref(terminal, pt, &ref);
GhosttyRow grid_row;
ghostty_grid_ref_row(&ref, &grid_row);
bool wrap = false;
ghostty_row_get(grid_row, GHOSTTY_ROW_DATA_WRAP, &wrap);
printf(" (wrap=%s", wrap ? "true" : "false");
// Check the style of the first cell with text
GhosttyStyle style = GHOSTTY_INIT_SIZED(GhosttyStyle);
ghostty_grid_ref_style(&ref, &style);
printf(", bold=%s)\n", style.bold ? "true" : "false");
}
ghostty_terminal_free(terminal);
return 0;
}
//! [grid-ref-traverse]

View File

@@ -50,6 +50,7 @@
* - @ref c-vt-paste/src/main.c - Paste safety check example
* - @ref c-vt-sgr/src/main.c - SGR parser example
* - @ref c-vt-formatter/src/main.c - Terminal formatter example
* - @ref c-vt-grid-traverse/src/main.c - Grid traversal example using grid refs
*
*/
@@ -84,6 +85,11 @@
* contents as plain text.
*/
/** @example c-vt-grid-traverse/src/main.c
* This example demonstrates how to traverse the entire terminal grid using
* grid refs to inspect cell codepoints, row wrap state, and cell styles.
*/
#ifndef GHOSTTY_VT_H
#define GHOSTTY_VT_H
@@ -96,12 +102,15 @@ extern "C" {
#include <ghostty/vt/focus.h>
#include <ghostty/vt/formatter.h>
#include <ghostty/vt/terminal.h>
#include <ghostty/vt/grid_ref.h>
#include <ghostty/vt/osc.h>
#include <ghostty/vt/sgr.h>
#include <ghostty/vt/style.h>
#include <ghostty/vt/key.h>
#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>

View File

@@ -0,0 +1,131 @@
/**
* @file grid_ref.h
*
* Terminal grid reference type for referencing a resolved position in the
* terminal grid.
*/
#ifndef GHOSTTY_VT_GRID_REF_H
#define GHOSTTY_VT_GRID_REF_H
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/screen.h>
#include <ghostty/vt/style.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup grid_ref Grid Reference
*
* A grid reference is a resolved reference to a specific cell position in the
* terminal's internal page structure. Obtain a grid reference from
* ghostty_terminal_grid_ref(), then extract the cell or row via
* ghostty_grid_ref_cell() and ghostty_grid_ref_row().
*
* A grid reference is only valid until the next update to the terminal
* instance. There is no guarantee that a grid reference will remain
* valid after ANY operation, even if a seemingly unrelated part of
* the grid is changed, so any information related to the grid reference
* should be read and cached immediately after obtaining the grid reference.
*
* This API is not meant to be used as the core of render loop. It isn't
* built to sustain the framerates needed for rendering large screens.
* Use the render state API for that.
*
* ## Example
*
* @snippet c-vt-grid-traverse/src/main.c grid-ref-traverse
*
* @{
*/
/**
* A resolved reference to a terminal cell position.
*
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
*
* @ingroup grid_ref
*/
typedef struct {
size_t size;
void *node;
uint16_t x;
uint16_t y;
} GhosttyGridRef;
/**
* Get the cell from a grid reference.
*
* @param ref Pointer to the grid reference
* @param[out] out_cell On success, set to the cell at the ref's position (may be NULL)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's
* node is NULL
*
* @ingroup grid_ref
*/
GhosttyResult ghostty_grid_ref_cell(const GhosttyGridRef *ref,
GhosttyCell *out_cell);
/**
* Get the row from a grid reference.
*
* @param ref Pointer to the grid reference
* @param[out] out_row On success, set to the row at the ref's position (may be NULL)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's
* node is NULL
*
* @ingroup grid_ref
*/
GhosttyResult ghostty_grid_ref_row(const GhosttyGridRef *ref,
GhosttyRow *out_row);
/**
* Get the grapheme cluster codepoints for the cell at the grid reference's
* position.
*
* Writes the full grapheme cluster (the cell's primary codepoint followed by
* any combining codepoints) into the provided buffer. If the cell has no text,
* out_len is set to 0 and GHOSTTY_SUCCESS is returned.
*
* If the buffer is too small (or NULL), the function returns
* GHOSTTY_OUT_OF_SPACE and writes the required number of codepoints to
* out_len. The caller can then retry with a sufficiently sized buffer.
*
* @param ref Pointer to the grid reference
* @param buf Output buffer of uint32_t codepoints (may be NULL)
* @param buf_len Number of uint32_t elements in the buffer
* @param[out] out_len On success, the number of codepoints written. On
* GHOSTTY_OUT_OF_SPACE, the required buffer size in codepoints.
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's
* node is NULL, GHOSTTY_OUT_OF_SPACE if the buffer is too small
*
* @ingroup grid_ref
*/
GhosttyResult ghostty_grid_ref_graphemes(const GhosttyGridRef *ref,
uint32_t *buf,
size_t buf_len,
size_t *out_len);
/**
* Get the style of the cell at the grid reference's position.
*
* @param ref Pointer to the grid reference
* @param[out] out_style On success, set to the cell's style (may be NULL)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's
* node is NULL
*
* @ingroup grid_ref
*/
GhosttyResult ghostty_grid_ref_style(const GhosttyGridRef *ref,
GhosttyStyle *out_style);
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_GRID_REF_H */

View File

@@ -0,0 +1,88 @@
/**
* @file point.h
*
* Terminal point types for referencing locations in the terminal grid.
*/
#ifndef GHOSTTY_VT_POINT_H
#define GHOSTTY_VT_POINT_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup point Point
*
* Types for referencing x/y positions in the terminal grid under
* different coordinate systems (active area, viewport, full screen,
* scrollback history).
*
* @{
*/
/**
* A coordinate in the terminal grid.
*
* @ingroup point
*/
typedef struct {
/** Column (0-indexed). */
uint16_t x;
/** Row (0-indexed). May exceed page size for screen/history tags. */
uint32_t y;
} GhosttyPointCoordinate;
/**
* Point reference tag.
*
* Determines which coordinate system a point uses.
*
* @ingroup point
*/
typedef enum {
/** Active area where the cursor can move. */
GHOSTTY_POINT_TAG_ACTIVE = 0,
/** Visible viewport (changes when scrolled). */
GHOSTTY_POINT_TAG_VIEWPORT = 1,
/** Full screen including scrollback. */
GHOSTTY_POINT_TAG_SCREEN = 2,
/** Scrollback history only (before active area). */
GHOSTTY_POINT_TAG_HISTORY = 3,
} GhosttyPointTag;
/**
* Point value union.
*
* @ingroup point
*/
typedef union {
/** Coordinate (used for all tag variants). */
GhosttyPointCoordinate coordinate;
/** Padding for ABI compatibility. Do not use. */
uint64_t _padding[2];
} GhosttyPointValue;
/**
* Tagged union for a point in the terminal grid.
*
* @ingroup point
*/
typedef struct {
GhosttyPointTag tag;
GhosttyPointValue value;
} GhosttyPoint;
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_POINT_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 */

138
include/ghostty/vt/style.h Normal file
View File

@@ -0,0 +1,138 @@
/**
* @file style.h
*
* Terminal cell style types.
*/
#ifndef GHOSTTY_VT_STYLE_H
#define GHOSTTY_VT_STYLE_H
#include <ghostty/vt/color.h>
#include <ghostty/vt/types.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup style Style
*
* Terminal cell style attributes.
*
* A style describes the visual attributes of a terminal cell, including
* foreground, background, and underline colors, as well as flags for
* bold, italic, underline, and other text decorations.
*
* @{
*/
/**
* Style identifier type.
*
* Used to look up the full style from a grid reference.
* Obtain this from a cell via GHOSTTY_CELL_DATA_STYLE_ID.
*
* @ingroup style
*/
typedef uint16_t GhosttyStyleId;
/**
* Style color tags.
*
* These values identify the type of color in a style color.
* Use the tag to determine which field in the color value union to access.
*
* @ingroup style
*/
typedef enum {
GHOSTTY_STYLE_COLOR_NONE = 0,
GHOSTTY_STYLE_COLOR_PALETTE = 1,
GHOSTTY_STYLE_COLOR_RGB = 2,
} GhosttyStyleColorTag;
/**
* Style color value union.
*
* Use the tag to determine which field is active.
*
* @ingroup style
*/
typedef union {
GhosttyColorPaletteIndex palette;
GhosttyColorRgb rgb;
uint64_t _padding;
} GhosttyStyleColorValue;
/**
* Style color (tagged union).
*
* A color used in a style attribute. Can be unset (none), a palette
* index, or a direct RGB value.
*
* @ingroup style
*/
typedef struct {
GhosttyStyleColorTag tag;
GhosttyStyleColorValue value;
} GhosttyStyleColor;
/**
* Terminal cell style.
*
* Describes the complete visual style for a terminal cell, including
* foreground, background, and underline colors, as well as text
* decoration flags. The underline field uses the same values as
* GhosttySgrUnderline.
*
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
*
* @ingroup style
*/
typedef struct {
size_t size;
GhosttyStyleColor fg_color;
GhosttyStyleColor bg_color;
GhosttyStyleColor underline_color;
bool bold;
bool italic;
bool faint;
bool blink;
bool inverse;
bool invisible;
bool strikethrough;
bool overline;
int underline; /**< One of GHOSTTY_SGR_UNDERLINE_* values */
} GhosttyStyle;
/**
* Get the default style.
*
* Initializes the style to the default values (no colors, no flags).
*
* @param style Pointer to the style to initialize
*
* @ingroup style
*/
void ghostty_style_default(GhosttyStyle* style);
/**
* Check if a style is the default style.
*
* Returns true if all colors are unset and all flags are off.
*
* @param style Pointer to the style to check
* @return true if the style is the default style
*
* @ingroup style
*/
bool ghostty_style_is_default(const GhosttyStyle* style);
#ifdef __cplusplus
}
#endif
/** @} */
#endif /* GHOSTTY_VT_STYLE_H */

View File

@@ -13,6 +13,10 @@
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/modes.h>
#include <ghostty/vt/grid_ref.h>
#include <ghostty/vt/screen.h>
#include <ghostty/vt/point.h>
#include <ghostty/vt/style.h>
#ifdef __cplusplus
extern "C" {
@@ -97,6 +101,128 @@ typedef struct {
GhosttyTerminalScrollViewportValue value;
} GhosttyTerminalScrollViewport;
/**
* Terminal screen identifier.
*
* Identifies which screen buffer is active in the terminal.
*
* @ingroup terminal
*/
typedef enum {
/** The primary (normal) screen. */
GHOSTTY_TERMINAL_SCREEN_PRIMARY = 0,
/** The alternate screen. */
GHOSTTY_TERMINAL_SCREEN_ALTERNATE = 1,
} GhosttyTerminalScreen;
/**
* Scrollbar state for the terminal viewport.
*
* Represents the scrollable area dimensions needed to render a scrollbar.
*
* @ingroup terminal
*/
typedef struct {
/** Total size of the scrollable area in rows. */
uint64_t total;
/** Offset into the total area that the viewport is at. */
uint64_t offset;
/** Length of the visible area in rows. */
uint64_t len;
} GhosttyTerminalScrollbar;
/**
* Terminal data types.
*
* These values specify what type of data to extract from a terminal
* using `ghostty_terminal_get`.
*
* @ingroup terminal
*/
typedef enum {
/** Invalid data type. Never results in any data extraction. */
GHOSTTY_TERMINAL_DATA_INVALID = 0,
/**
* Terminal width in cells.
*
* Output type: uint16_t *
*/
GHOSTTY_TERMINAL_DATA_COLS = 1,
/**
* Terminal height in cells.
*
* Output type: uint16_t *
*/
GHOSTTY_TERMINAL_DATA_ROWS = 2,
/**
* Cursor column position (0-indexed).
*
* Output type: uint16_t *
*/
GHOSTTY_TERMINAL_DATA_CURSOR_X = 3,
/**
* Cursor row position within the active area (0-indexed).
*
* Output type: uint16_t *
*/
GHOSTTY_TERMINAL_DATA_CURSOR_Y = 4,
/**
* Whether the cursor has a pending wrap (next print will soft-wrap).
*
* Output type: bool *
*/
GHOSTTY_TERMINAL_DATA_CURSOR_PENDING_WRAP = 5,
/**
* The currently active screen.
*
* Output type: GhosttyTerminalScreen *
*/
GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN = 6,
/**
* Whether the cursor is visible (DEC mode 25).
*
* Output type: bool *
*/
GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE = 7,
/**
* Current Kitty keyboard protocol flags.
*
* Output type: GhosttyKittyKeyFlags * (uint8_t *)
*/
GHOSTTY_TERMINAL_DATA_KITTY_KEYBOARD_FLAGS = 8,
/**
* Scrollbar state for the terminal viewport.
*
* This may be expensive to calculate depending on where the viewport
* is (arbitrary pins are expensive). The caller should take care to only
* call this as needed and not too frequently.
*
* Output type: GhosttyTerminalScrollbar *
*/
GHOSTTY_TERMINAL_DATA_SCROLLBAR = 9,
/**
* The current SGR style of the cursor.
*
* This is the style that will be applied to newly printed characters.
*
* Output type: GhosttyStyle *
*/
GHOSTTY_TERMINAL_DATA_CURSOR_STYLE = 10,
} GhosttyTerminalData;
/**
* Create a new terminal instance.
*
@@ -228,8 +354,58 @@ GhosttyResult ghostty_terminal_mode_get(GhosttyTerminal terminal,
* @ingroup terminal
*/
GhosttyResult ghostty_terminal_mode_set(GhosttyTerminal terminal,
GhosttyMode mode,
bool value);
GhosttyMode mode,
bool value);
/**
* Get data from a terminal instance.
*
* Extracts typed data from the given terminal 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 `GhosttyTerminalData` enum.
*
* @param terminal The terminal handle (may be NULL)
* @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 terminal
* is NULL or the data type is invalid
*
* @ingroup terminal
*/
GhosttyResult ghostty_terminal_get(GhosttyTerminal terminal,
GhosttyTerminalData data,
void *out);
/**
* Resolve a point in the terminal grid to a grid reference.
*
* Resolves the given point (which can be in active, viewport, screen,
* or history coordinates) to a grid reference for that location. Use
* ghostty_grid_ref_cell() and ghostty_grid_ref_row() to extract the cell
* and row.
*
* Lookups using the `active` and `viewport` tags are fast. The `screen`
* and `history` tags may require traversing the full scrollback page list
* to resolve the y coordinate, so they can be expensive for large
* scrollback buffers.
*
* This function isn't meant to be used as the core of render loop. It
* isn't built to sustain the framerates needed for rendering large screens.
* Use the render state API for that. This API is instead meant for less
* strictly performance-sensitive use cases.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param point The point specifying which cell to look up
* @param[out] out_ref On success, set to the grid reference at the given point (may be NULL)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal
* is NULL or the point is out of bounds
*
* @ingroup terminal
*/
GhosttyResult ghostty_terminal_grid_ref(GhosttyTerminal terminal,
GhosttyPoint point,
GhosttyGridRef *out_ref);
/** @} */

View File

@@ -169,6 +169,10 @@ comptime {
@export(&c.mode_report_encode, .{ .name = "ghostty_mode_report_encode" });
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" });
@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" });
@@ -191,6 +195,12 @@ comptime {
@export(&c.terminal_scroll_viewport, .{ .name = "ghostty_terminal_scroll_viewport" });
@export(&c.terminal_mode_get, .{ .name = "ghostty_terminal_mode_get" });
@export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" });
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
@export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" });
@export(&c.grid_ref_cell, .{ .name = "ghostty_grid_ref_cell" });
@export(&c.grid_ref_row, .{ .name = "ghostty_grid_ref_row" });
@export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" });
@export(&c.grid_ref_style, .{ .name = "ghostty_grid_ref_style" });
// On Wasm we need to export our allocator convenience functions.
if (builtin.target.cpu.arch.isWasm()) {

View File

@@ -9,15 +9,19 @@ const ScreenSet = @This();
const std = @import("std");
const assert = @import("../quirks.zig").inlineAssert;
const build_options = @import("terminal_options");
const lib = @import("../lib/main.zig");
const testing = std.testing;
const Allocator = std.mem.Allocator;
const Screen = @import("Screen.zig");
const lib_target: lib.Target = if (build_options.c_abi) .c else .zig;
/// The possible keys for screens in the screen set.
pub const Key = enum(u1) {
primary,
alternate,
};
pub const Key = lib.Enum(lib_target, &.{
"primary",
"alternate",
});
/// The key value of the currently active screen. Useful for simple
/// comparisons, e.g. "is this screen the primary screen".

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);
}

155
src/terminal/c/grid_ref.zig Normal file
View File

@@ -0,0 +1,155 @@
const std = @import("std");
const testing = std.testing;
const page = @import("../page.zig");
const PageList = @import("../PageList.zig");
const size = @import("../size.zig");
const stylepkg = @import("../style.zig");
const cell_c = @import("cell.zig");
const row_c = @import("row.zig");
const style_c = @import("style.zig");
const Result = @import("result.zig").Result;
/// C: GhosttyGridRef
///
/// A sized struct that holds a reference to a position in the terminal grid.
/// The ref points to a specific cell position within the terminal's
/// internal page structure.
pub const CGridRef = extern struct {
size: usize = @sizeOf(CGridRef),
node: ?*PageList.List.Node = null,
x: size.CellCountInt = 0,
y: size.CellCountInt = 0,
pub fn fromPin(pin: PageList.Pin) CGridRef {
return .{
.node = pin.node,
.x = pin.x,
.y = pin.y,
};
}
fn toPin(self: CGridRef) ?PageList.Pin {
return .{
.node = self.node orelse return null,
.x = self.x,
.y = self.y,
};
}
};
pub fn grid_ref_cell(
ref: *const CGridRef,
out: ?*cell_c.CCell,
) callconv(.c) Result {
const p = ref.toPin() orelse return .invalid_value;
if (out) |o| o.* = @bitCast(p.rowAndCell().cell.*);
return .success;
}
pub fn grid_ref_row(
ref: *const CGridRef,
out: ?*row_c.CRow,
) callconv(.c) Result {
const p = ref.toPin() orelse return .invalid_value;
if (out) |o| o.* = @bitCast(p.rowAndCell().row.*);
return .success;
}
pub fn grid_ref_graphemes(
ref: *const CGridRef,
out_buf: ?[*]u32,
buf_len: usize,
out_len: *usize,
) callconv(.c) Result {
const p = ref.toPin() orelse return .invalid_value;
const cell = p.rowAndCell().cell;
if (!cell.hasText()) {
out_len.* = 0;
return .success;
}
const cp = cell.codepoint();
const extra = if (cell.hasGrapheme()) p.grapheme(cell) else null;
const total = 1 + if (extra) |e| e.len else 0;
if (out_buf == null or buf_len < total) {
out_len.* = total;
return .out_of_space;
}
const buf = out_buf.?[0..buf_len];
buf[0] = cp;
if (extra) |e| for (e, 1..) |c, i| {
buf[i] = c;
};
out_len.* = total;
return .success;
}
pub fn grid_ref_style(
ref: *const CGridRef,
out: ?*style_c.Style,
) callconv(.c) Result {
const p = ref.toPin() orelse return .invalid_value;
if (out) |o| {
const cell = p.rowAndCell().cell;
if (cell.style_id == stylepkg.default_id) {
o.* = .fromStyle(.{});
} else {
o.* = .fromStyle(p.node.data.styles.get(
p.node.data.memory,
cell.style_id,
).*);
}
}
return .success;
}
test "grid_ref_cell null node" {
const ref = CGridRef{};
var out: cell_c.CCell = undefined;
try testing.expectEqual(Result.invalid_value, grid_ref_cell(&ref, &out));
}
test "grid_ref_row null node" {
const ref = CGridRef{};
var out: row_c.CRow = undefined;
try testing.expectEqual(Result.invalid_value, grid_ref_row(&ref, &out));
}
test "grid_ref_cell null out" {
const ref = CGridRef{};
try testing.expectEqual(Result.invalid_value, grid_ref_cell(&ref, null));
}
test "grid_ref_row null out" {
const ref = CGridRef{};
try testing.expectEqual(Result.invalid_value, grid_ref_row(&ref, null));
}
test "grid_ref_graphemes null node" {
const ref = CGridRef{};
var len: usize = undefined;
try testing.expectEqual(Result.invalid_value, grid_ref_graphemes(&ref, null, 0, &len));
}
test "grid_ref_graphemes null buf returns out_of_space" {
const ref = CGridRef{};
var len: usize = undefined;
// With null node this returns invalid_value before checking the buffer,
// so we can only test null node here. Full buffer tests require a real page.
try testing.expectEqual(Result.invalid_value, grid_ref_graphemes(&ref, null, 0, &len));
}
test "grid_ref_style null node" {
const ref = CGridRef{};
var out: style_c.Style = undefined;
try testing.expectEqual(Result.invalid_value, grid_ref_style(&ref, &out));
}
test "grid_ref_style null out" {
const ref = CGridRef{};
try testing.expectEqual(Result.invalid_value, grid_ref_style(&ref, null));
}

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,8 +9,10 @@ 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");
pub const terminal = @import("terminal.zig");
// The full C API, unexported.
@@ -90,6 +93,13 @@ 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;
pub const terminal_new = terminal.new;
pub const terminal_free = terminal.free;
pub const terminal_reset = terminal.reset;
@@ -98,9 +108,20 @@ pub const terminal_vt_write = terminal.vt_write;
pub const terminal_scroll_viewport = terminal.scroll_viewport;
pub const terminal_mode_get = terminal.mode_get;
pub const terminal_mode_set = terminal.mode_set;
pub const terminal_get = terminal.get;
pub const terminal_grid_ref = terminal.grid_ref;
const grid_ref = @import("grid_ref.zig");
pub const grid_ref_cell = grid_ref.grid_ref_cell;
pub const grid_ref_row = grid_ref.grid_ref_row;
pub const grid_ref_graphemes = grid_ref.grid_ref_graphemes;
pub const grid_ref_style = grid_ref.grid_ref_style;
test {
_ = cell;
_ = color;
_ = grid_ref;
_ = row;
_ = focus;
_ = formatter;
_ = modes;
@@ -112,6 +133,7 @@ test {
_ = paste;
_ = sgr;
_ = size_report;
_ = style;
_ = terminal;
// We want to make sure we run the tests for the C allocator interface.

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);
}

133
src/terminal/c/style.zig Normal file
View File

@@ -0,0 +1,133 @@
const std = @import("std");
const assert = std.debug.assert;
const testing = std.testing;
const style = @import("../style.zig");
const color = @import("../color.zig");
const sgr = @import("../sgr.zig");
/// C: GhosttyStyleColorTag
pub const ColorTag = enum(c_int) {
none = 0,
palette = 1,
rgb = 2,
};
/// C: GhosttyStyleColorValue
pub const ColorValue = extern union {
palette: u8,
rgb: color.RGB.C,
_padding: u64,
};
/// C: GhosttyStyleColor
pub const Color = extern struct {
tag: ColorTag,
value: ColorValue,
pub fn fromColor(c: style.Style.Color) Color {
return switch (c) {
.none => .{
.tag = .none,
.value = .{ ._padding = 0 },
},
.palette => |idx| .{
.tag = .palette,
.value = .{ .palette = idx },
},
.rgb => |rgb| .{
.tag = .rgb,
.value = .{ .rgb = rgb.cval() },
},
};
}
};
/// C: GhosttyStyle
pub const Style = extern struct {
size: usize = @sizeOf(Style),
fg_color: Color,
bg_color: Color,
underline_color: Color,
bold: bool,
italic: bool,
faint: bool,
blink: bool,
inverse: bool,
invisible: bool,
strikethrough: bool,
overline: bool,
underline: c_int,
pub fn fromStyle(s: style.Style) Style {
return .{
.fg_color = .fromColor(s.fg_color),
.bg_color = .fromColor(s.bg_color),
.underline_color = .fromColor(s.underline_color),
.bold = s.flags.bold,
.italic = s.flags.italic,
.faint = s.flags.faint,
.blink = s.flags.blink,
.inverse = s.flags.inverse,
.invisible = s.flags.invisible,
.strikethrough = s.flags.strikethrough,
.overline = s.flags.overline,
.underline = @intFromEnum(s.flags.underline),
};
}
};
/// Returns the default style.
pub fn default_style(result: *Style) callconv(.c) void {
result.* = .fromStyle(.{});
assert(result.size == @sizeOf(Style));
}
/// Returns true if the style is the default style.
pub fn style_is_default(s: *const Style) callconv(.c) bool {
assert(s.size == @sizeOf(Style));
return s.fg_color.tag == .none and
s.bg_color.tag == .none and
s.underline_color.tag == .none and
s.bold == false and
s.italic == false and
s.faint == false and
s.blink == false and
s.inverse == false and
s.invisible == false and
s.strikethrough == false and
s.overline == false and
s.underline == 0;
}
test "default style" {
var s: Style = undefined;
default_style(&s);
try testing.expect(style_is_default(&s));
try testing.expectEqual(ColorTag.none, s.fg_color.tag);
try testing.expectEqual(ColorTag.none, s.bg_color.tag);
try testing.expectEqual(ColorTag.none, s.underline_color.tag);
try testing.expect(!s.bold);
try testing.expect(!s.italic);
try testing.expectEqual(@as(c_int, 0), s.underline);
}
test "convert style with colors" {
const zig_style: style.Style = .{
.fg_color = .{ .palette = 42 },
.bg_color = .{ .rgb = .{ .r = 255, .g = 128, .b = 64 } },
.underline_color = .none,
.flags = .{ .bold = true, .underline = .curly },
};
const c_style: Style = .fromStyle(zig_style);
try testing.expectEqual(ColorTag.palette, c_style.fg_color.tag);
try testing.expectEqual(@as(u8, 42), c_style.fg_color.value.palette);
try testing.expectEqual(ColorTag.rgb, c_style.bg_color.tag);
try testing.expectEqual(@as(u8, 255), c_style.bg_color.value.rgb.r);
try testing.expectEqual(@as(u8, 128), c_style.bg_color.value.rgb.g);
try testing.expectEqual(@as(u8, 64), c_style.bg_color.value.rgb.b);
try testing.expectEqual(ColorTag.none, c_style.underline_color.tag);
try testing.expect(c_style.bold);
try testing.expectEqual(@as(c_int, 3), c_style.underline);
try testing.expect(!style_is_default(&c_style));
}

View File

@@ -3,10 +3,20 @@ const testing = std.testing;
const lib_alloc = @import("../../lib/allocator.zig");
const CAllocator = lib_alloc.Allocator;
const ZigTerminal = @import("../Terminal.zig");
const ScreenSet = @import("../ScreenSet.zig");
const PageList = @import("../PageList.zig");
const kitty = @import("../kitty/key.zig");
const modes = @import("../modes.zig");
const point = @import("../point.zig");
const size = @import("../size.zig");
const cell_c = @import("cell.zig");
const row_c = @import("row.zig");
const grid_ref_c = @import("grid_ref.zig");
const style_c = @import("style.zig");
const Result = @import("result.zig").Result;
const log = std.log.scoped(.terminal_c);
/// C: GhosttyTerminal
pub const Terminal = ?*ZigTerminal;
@@ -123,6 +133,102 @@ pub fn mode_set(
return .success;
}
/// C: GhosttyTerminalScreen
pub const TerminalScreen = ScreenSet.Key;
/// C: GhosttyTerminalScrollbar
pub const TerminalScrollbar = PageList.Scrollbar.C;
/// C: GhosttyTerminalData
pub const TerminalData = enum(c_int) {
invalid = 0,
cols = 1,
rows = 2,
cursor_x = 3,
cursor_y = 4,
cursor_pending_wrap = 5,
active_screen = 6,
cursor_visible = 7,
kitty_keyboard_flags = 8,
scrollbar = 9,
cursor_style = 10,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: TerminalData) type {
return switch (self) {
.invalid => void,
.cols, .rows, .cursor_x, .cursor_y => size.CellCountInt,
.cursor_pending_wrap, .cursor_visible => bool,
.active_screen => TerminalScreen,
.kitty_keyboard_flags => u8,
.scrollbar => TerminalScrollbar,
.cursor_style => style_c.Style,
};
}
};
pub fn get(
terminal_: Terminal,
data: TerminalData,
out: ?*anyopaque,
) callconv(.c) Result {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(TerminalData, @intFromEnum(data)) catch {
log.warn("terminal_get invalid data value={d}", .{@intFromEnum(data)});
return .invalid_value;
};
}
return switch (data) {
inline else => |comptime_data| getTyped(
terminal_,
comptime_data,
@ptrCast(@alignCast(out)),
),
};
}
fn getTyped(
terminal_: Terminal,
comptime data: TerminalData,
out: *data.OutType(),
) Result {
const t = terminal_ orelse return .invalid_value;
switch (data) {
.invalid => return .invalid_value,
.cols => out.* = t.cols,
.rows => out.* = t.rows,
.cursor_x => out.* = t.screens.active.cursor.x,
.cursor_y => out.* = t.screens.active.cursor.y,
.cursor_pending_wrap => out.* = t.screens.active.cursor.pending_wrap,
.active_screen => out.* = t.screens.active_key,
.cursor_visible => out.* = t.modes.get(.cursor_visible),
.kitty_keyboard_flags => out.* = @as(u8, t.screens.active.kitty_keyboard.current().int()),
.scrollbar => out.* = t.screens.active.pages.scrollbar().cval(),
.cursor_style => out.* = .fromStyle(t.screens.active.cursor.style),
}
return .success;
}
pub fn grid_ref(
terminal_: Terminal,
pt: point.Point.C,
out_ref: ?*grid_ref_c.CGridRef,
) callconv(.c) Result {
const t = terminal_ orelse return .invalid_value;
const zig_pt: point.Point = switch (pt.tag) {
.active => .{ .active = pt.value.active },
.viewport => .{ .viewport = pt.value.viewport },
.screen => .{ .screen = pt.value.screen },
.history => .{ .history = pt.value.history },
};
const p = t.screens.active.pages.pin(zig_pt) orelse
return .invalid_value;
if (out_ref) |out| out.* = grid_ref_c.CGridRef.fromPin(p);
return .success;
}
pub fn free(terminal_: Terminal) callconv(.c) void {
const t = terminal_ orelse return;
@@ -397,3 +503,192 @@ test "vt_write" {
defer testing.allocator.free(str);
try testing.expectEqualStrings("Hello", str);
}
test "get cols and rows" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
var cols: size.CellCountInt = undefined;
var rows: size.CellCountInt = undefined;
try testing.expectEqual(Result.success, get(t, .cols, @ptrCast(&cols)));
try testing.expectEqual(Result.success, get(t, .rows, @ptrCast(&rows)));
try testing.expectEqual(80, cols);
try testing.expectEqual(24, rows);
}
test "get cursor position" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
vt_write(t, "Hello", 5);
var x: size.CellCountInt = undefined;
var y: size.CellCountInt = undefined;
try testing.expectEqual(Result.success, get(t, .cursor_x, @ptrCast(&x)));
try testing.expectEqual(Result.success, get(t, .cursor_y, @ptrCast(&y)));
try testing.expectEqual(5, x);
try testing.expectEqual(0, y);
}
test "get null" {
var cols: size.CellCountInt = undefined;
try testing.expectEqual(Result.invalid_value, get(null, .cols, @ptrCast(&cols)));
}
test "get cursor_visible" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
var visible: bool = undefined;
try testing.expectEqual(Result.success, get(t, .cursor_visible, @ptrCast(&visible)));
try testing.expect(visible);
// DEC mode 25 controls cursor visibility
const cursor_visible_mode: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 25, .ansi = false });
try testing.expectEqual(Result.success, mode_set(t, cursor_visible_mode, false));
try testing.expectEqual(Result.success, get(t, .cursor_visible, @ptrCast(&visible)));
try testing.expect(!visible);
}
test "get active_screen" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
var screen: TerminalScreen = undefined;
try testing.expectEqual(Result.success, get(t, .active_screen, @ptrCast(&screen)));
try testing.expectEqual(.primary, screen);
}
test "get kitty_keyboard_flags" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
var flags: u8 = undefined;
try testing.expectEqual(Result.success, get(t, .kitty_keyboard_flags, @ptrCast(&flags)));
try testing.expectEqual(0, flags);
// Push kitty flags via VT sequence: CSI > 3 u (push disambiguate | report_events)
vt_write(t, "\x1b[>3u", 5);
try testing.expectEqual(Result.success, get(t, .kitty_keyboard_flags, @ptrCast(&flags)));
try testing.expectEqual(3, flags);
}
test "get invalid" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
try testing.expectEqual(Result.invalid_value, get(t, .invalid, null));
}
test "grid_ref" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
vt_write(t, "Hello", 5);
var out_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 0, .y = 0 } },
}, &out_ref));
// Extract cell from grid ref and verify it contains 'H'
var out_cell: cell_c.CCell = undefined;
try testing.expectEqual(Result.success, grid_ref_c.grid_ref_cell(&out_ref, &out_cell));
var cp: u32 = 0;
try testing.expectEqual(Result.success, cell_c.get(out_cell, .codepoint, @ptrCast(&cp)));
try testing.expectEqual(@as(u32, 'H'), cp);
}
test "grid_ref null terminal" {
var out_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.invalid_value, grid_ref(null, .{
.tag = .active,
.value = .{ .active = .{ .x = 0, .y = 0 } },
}, &out_ref));
}
test "grid_ref out of bounds" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
var out_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.invalid_value, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 100, .y = 0 } },
}, &out_ref));
}

View File

@@ -1,52 +1,56 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_options = @import("terminal_options");
const lib = @import("../lib/main.zig");
const size = @import("size.zig");
const lib_target: lib.Target = if (build_options.c_abi) .c else .zig;
/// The possible reference locations for a point. When someone says "(42, 80)"
/// in the context of a terminal, that could mean multiple things: it is in the
/// current visible viewport? the current active area of the screen where the
/// cursor is? the entire scrollback history? etc.
///
/// This tag is used to differentiate those cases.
pub const Tag = enum {
/// Top-left is part of the active area where a running program can
/// jump the cursor and make changes. The active area is the "editable"
/// part of the screen.
///
/// The bottom-right of the active tag differs from all other tags
/// because it includes the full height (rows) of the screen, including
/// rows that may not be written yet. This is required because the active
/// area is fully "addressable" by the running program (see below) whereas
/// the other tags are used primarily for reading/modifying past-written
/// data so they can't address unwritten rows.
///
/// Note for those less familiar with terminal functionality: there
/// are escape sequences to move the cursor to any position on
/// the screen, but it is limited to the size of the viewport and
/// the bottommost part of the screen. Terminal programs can't --
/// with sequences at the time of writing this comment -- modify
/// anything in the scrollback, visible viewport (if it differs
/// from the active area), etc.
active,
pub const Tag = lib.Enum(lib_target, &.{
// Top-left is part of the active area where a running program can
// jump the cursor and make changes. The active area is the "editable"
// part of the screen.
//
// The bottom-right of the active tag differs from all other tags
// because it includes the full height (rows) of the screen, including
// rows that may not be written yet. This is required because the active
// area is fully "addressable" by the running program (see below) whereas
// the other tags are used primarily for reading/modifying past-written
// data so they can't address unwritten rows.
//
// Note for those less familiar with terminal functionality: there
// are escape sequences to move the cursor to any position on
// the screen, but it is limited to the size of the viewport and
// the bottommost part of the screen. Terminal programs can't --
// with sequences at the time of writing this comment -- modify
// anything in the scrollback, visible viewport (if it differs
// from the active area), etc.
"active",
/// Top-left is the visible viewport. This means that if the user
/// has scrolled in any direction, top-left changes. The bottom-right
/// is the last written row from the top-left.
viewport,
// Top-left is the visible viewport. This means that if the user
// has scrolled in any direction, top-left changes. The bottom-right
// is the last written row from the top-left.
"viewport",
/// Top-left is the furthest back in the scrollback history
/// supported by the screen and the bottom-right is the bottom-right
/// of the last written row. Note this last point is important: the
/// bottom right is NOT necessarily the same as "active" because
/// "active" always allows referencing the full rows tall of the
/// screen whereas "screen" only contains written rows.
screen,
// Top-left is the furthest back in the scrollback history
// supported by the screen and the bottom-right is the bottom-right
// of the last written row. Note this last point is important: the
// bottom right is NOT necessarily the same as "active" because
// "active" always allows referencing the full rows tall of the
// screen whereas "screen" only contains written rows.
"screen",
/// The top-left is the same as "screen" but the bottom-right is
/// the line just before the top of "active". This contains only
/// the scrollback history.
history,
};
// The top-left is the same as "screen" but the bottom-right is
// the line just before the top of "active". This contains only
// the scrollback history.
"history",
});
/// An x/y point in the terminal for some definition of location (tag).
pub const Point = union(Tag) {
@@ -64,6 +68,17 @@ pub const Point = union(Tag) {
=> |v| v,
};
}
const c_union = lib.TaggedUnion(
lib_target,
@This(),
// Padding: largest variant is Coordinate (u16 + u32 = 6 bytes).
// Use [2]u64 (16 bytes) for future expansion.
[2]u64,
);
pub const C = c_union.C;
pub const CValue = c_union.CValue;
pub const cval = c_union.cval;
};
pub const Coordinate = extern struct {