diff --git a/example/c-vt-grid-traverse/README.md b/example/c-vt-grid-traverse/README.md new file mode 100644 index 000000000..f9a15851a --- /dev/null +++ b/example/c-vt-grid-traverse/README.md @@ -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 +``` diff --git a/example/c-vt-grid-traverse/build.zig b/example/c-vt-grid-traverse/build.zig new file mode 100644 index 000000000..caf174028 --- /dev/null +++ b/example/c-vt-grid-traverse/build.zig @@ -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); +} diff --git a/example/c-vt-grid-traverse/build.zig.zon b/example/c-vt-grid-traverse/build.zig.zon new file mode 100644 index 000000000..21b6cea18 --- /dev/null +++ b/example/c-vt-grid-traverse/build.zig.zon @@ -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", + }, +} diff --git a/example/c-vt-grid-traverse/src/main.c b/example/c-vt-grid-traverse/src/main.c new file mode 100644 index 000000000..f07169eb6 --- /dev/null +++ b/example/c-vt-grid-traverse/src/main.c @@ -0,0 +1,85 @@ +#include +#include +#include +#include + +//! [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] diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index c6ab35fa3..a3d0ec57d 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -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 #include #include +#include #include #include +#include #include #include #include #include +#include #include #include diff --git a/include/ghostty/vt/grid_ref.h b/include/ghostty/vt/grid_ref.h new file mode 100644 index 000000000..29ecda7b5 --- /dev/null +++ b/include/ghostty/vt/grid_ref.h @@ -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 +#include +#include +#include +#include + +#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 */ diff --git a/include/ghostty/vt/point.h b/include/ghostty/vt/point.h new file mode 100644 index 000000000..f152a5c46 --- /dev/null +++ b/include/ghostty/vt/point.h @@ -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 + +#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 */ 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/style.h b/include/ghostty/vt/style.h new file mode 100644 index 000000000..ac5cd2ad6 --- /dev/null +++ b/include/ghostty/vt/style.h @@ -0,0 +1,138 @@ +/** + * @file style.h + * + * Terminal cell style types. + */ + +#ifndef GHOSTTY_VT_STYLE_H +#define GHOSTTY_VT_STYLE_H + +#include +#include +#include +#include +#include + +#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 */ diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 5be6db001..042d3ba93 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -13,6 +13,10 @@ #include #include #include +#include +#include +#include +#include #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); /** @} */ diff --git a/src/lib_vt.zig b/src/lib_vt.zig index aabd78592..d6cfe49ea 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -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()) { diff --git a/src/terminal/ScreenSet.zig b/src/terminal/ScreenSet.zig index cbaa03f47..6fe866f70 100644 --- a/src/terminal/ScreenSet.zig +++ b/src/terminal/ScreenSet.zig @@ -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". 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/grid_ref.zig b/src/terminal/c/grid_ref.zig new file mode 100644 index 000000000..d6afb0c45 --- /dev/null +++ b/src/terminal/c/grid_ref.zig @@ -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)); +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 72fa74345..8964610df 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,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. 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); +} diff --git a/src/terminal/c/style.zig b/src/terminal/c/style.zig new file mode 100644 index 000000000..99f591c10 --- /dev/null +++ b/src/terminal/c/style.zig @@ -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)); +} diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index c3c084c52..ad560dc1a 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -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)); +} diff --git a/src/terminal/point.zig b/src/terminal/point.zig index c5983fcbc..db834732c 100644 --- a/src/terminal/point.zig +++ b/src/terminal/point.zig @@ -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 {