mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
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:
19
example/c-vt-grid-traverse/README.md
Normal file
19
example/c-vt-grid-traverse/README.md
Normal 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
|
||||
```
|
||||
42
example/c-vt-grid-traverse/build.zig
Normal file
42
example/c-vt-grid-traverse/build.zig
Normal 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);
|
||||
}
|
||||
24
example/c-vt-grid-traverse/build.zig.zon
Normal file
24
example/c-vt-grid-traverse/build.zig.zon
Normal 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",
|
||||
},
|
||||
}
|
||||
85
example/c-vt-grid-traverse/src/main.c
Normal file
85
example/c-vt-grid-traverse/src/main.c
Normal 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]
|
||||
@@ -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>
|
||||
|
||||
|
||||
131
include/ghostty/vt/grid_ref.h
Normal file
131
include/ghostty/vt/grid_ref.h
Normal 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 */
|
||||
88
include/ghostty/vt/point.h
Normal file
88
include/ghostty/vt/point.h
Normal 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
323
include/ghostty/vt/screen.h
Normal 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
138
include/ghostty/vt/style.h
Normal 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 */
|
||||
@@ -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);
|
||||
|
||||
/** @} */
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
158
src/terminal/c/cell.zig
Normal file
@@ -0,0 +1,158 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const page = @import("../page.zig");
|
||||
const Cell = page.Cell;
|
||||
const style_c = @import("style.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
/// C: GhosttyCell
|
||||
pub const CCell = u64;
|
||||
|
||||
/// C: GhosttyCellContentTag
|
||||
pub const ContentTag = enum(c_int) {
|
||||
codepoint = 0,
|
||||
codepoint_grapheme = 1,
|
||||
bg_color_palette = 2,
|
||||
bg_color_rgb = 3,
|
||||
};
|
||||
|
||||
/// C: GhosttyCellWide
|
||||
pub const Wide = enum(c_int) {
|
||||
narrow = 0,
|
||||
wide = 1,
|
||||
spacer_tail = 2,
|
||||
spacer_head = 3,
|
||||
};
|
||||
|
||||
/// C: GhosttyCellSemanticContent
|
||||
pub const SemanticContent = enum(c_int) {
|
||||
output = 0,
|
||||
input = 1,
|
||||
prompt = 2,
|
||||
};
|
||||
|
||||
/// C: GhosttyCellData
|
||||
pub const CellData = enum(c_int) {
|
||||
invalid = 0,
|
||||
|
||||
/// The codepoint of the cell (0 if empty or bg-color-only).
|
||||
/// Output type: uint32_t * (stored as u21, zero-extended)
|
||||
codepoint = 1,
|
||||
|
||||
/// The content tag describing what kind of content is in the cell.
|
||||
/// Output type: GhosttyCellContentTag *
|
||||
content_tag = 2,
|
||||
|
||||
/// The wide property of the cell.
|
||||
/// Output type: GhosttyCellWide *
|
||||
wide = 3,
|
||||
|
||||
/// Whether the cell has text to render.
|
||||
/// Output type: bool *
|
||||
has_text = 4,
|
||||
|
||||
/// Whether the cell has styling (non-default style).
|
||||
/// Output type: bool *
|
||||
has_styling = 5,
|
||||
|
||||
/// The style ID for the cell (for use with style lookups).
|
||||
/// Output type: uint16_t *
|
||||
style_id = 6,
|
||||
|
||||
/// Whether the cell has a hyperlink.
|
||||
/// Output type: bool *
|
||||
has_hyperlink = 7,
|
||||
|
||||
/// Whether the cell is protected.
|
||||
/// Output type: bool *
|
||||
protected = 8,
|
||||
|
||||
/// The semantic content type of the cell (from OSC 133).
|
||||
/// Output type: GhosttyCellSemanticContent *
|
||||
semantic_content = 9,
|
||||
|
||||
/// Output type expected for querying the data of the given kind.
|
||||
pub fn OutType(comptime self: CellData) type {
|
||||
return switch (self) {
|
||||
.invalid => void,
|
||||
.codepoint => u32,
|
||||
.content_tag => ContentTag,
|
||||
.wide => Wide,
|
||||
.has_text, .has_styling, .has_hyperlink, .protected => bool,
|
||||
.style_id => u16,
|
||||
.semantic_content => SemanticContent,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn get(
|
||||
cell_: CCell,
|
||||
data: CellData,
|
||||
out: ?*anyopaque,
|
||||
) callconv(.c) Result {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(CellData, @intFromEnum(data)) catch {
|
||||
return .invalid_value;
|
||||
};
|
||||
}
|
||||
|
||||
return switch (data) {
|
||||
inline else => |comptime_data| getTyped(
|
||||
cell_,
|
||||
comptime_data,
|
||||
@ptrCast(@alignCast(out)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn getTyped(
|
||||
cell_: CCell,
|
||||
comptime data: CellData,
|
||||
out: *data.OutType(),
|
||||
) Result {
|
||||
const cell: Cell = @bitCast(cell_);
|
||||
switch (data) {
|
||||
.invalid => return .invalid_value,
|
||||
.codepoint => out.* = @intCast(cell.codepoint()),
|
||||
.content_tag => out.* = @enumFromInt(@intFromEnum(cell.content_tag)),
|
||||
.wide => out.* = @enumFromInt(@intFromEnum(cell.wide)),
|
||||
.has_text => out.* = cell.hasText(),
|
||||
.has_styling => out.* = cell.hasStyling(),
|
||||
.style_id => out.* = cell.style_id,
|
||||
.has_hyperlink => out.* = cell.hyperlink,
|
||||
.protected => out.* = cell.protected,
|
||||
.semantic_content => out.* = @enumFromInt(@intFromEnum(cell.semantic_content)),
|
||||
}
|
||||
|
||||
return .success;
|
||||
}
|
||||
|
||||
test "get codepoint" {
|
||||
const cell: CCell = @bitCast(Cell.init('A'));
|
||||
var cp: u32 = 0;
|
||||
try testing.expectEqual(Result.success, get(cell, .codepoint, @ptrCast(&cp)));
|
||||
try testing.expectEqual(@as(u32, 'A'), cp);
|
||||
}
|
||||
|
||||
test "get has_text" {
|
||||
const cell: CCell = @bitCast(Cell.init('A'));
|
||||
var has: bool = false;
|
||||
try testing.expectEqual(Result.success, get(cell, .has_text, @ptrCast(&has)));
|
||||
try testing.expect(has);
|
||||
}
|
||||
|
||||
test "get empty cell" {
|
||||
const cell: CCell = @bitCast(Cell.init(0));
|
||||
var has: bool = true;
|
||||
try testing.expectEqual(Result.success, get(cell, .has_text, @ptrCast(&has)));
|
||||
try testing.expect(!has);
|
||||
}
|
||||
|
||||
test "get wide" {
|
||||
var zig_cell = Cell.init('A');
|
||||
zig_cell.wide = .wide;
|
||||
const cell: CCell = @bitCast(zig_cell);
|
||||
var w: Wide = .narrow;
|
||||
try testing.expectEqual(Result.success, get(cell, .wide, @ptrCast(&w)));
|
||||
try testing.expectEqual(Wide.wide, w);
|
||||
}
|
||||
155
src/terminal/c/grid_ref.zig
Normal file
155
src/terminal/c/grid_ref.zig
Normal 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));
|
||||
}
|
||||
@@ -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
130
src/terminal/c/row.zig
Normal file
@@ -0,0 +1,130 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const page = @import("../page.zig");
|
||||
const Row = page.Row;
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
/// C: GhosttyRow
|
||||
pub const CRow = u64;
|
||||
|
||||
/// C: GhosttyRowSemanticPrompt
|
||||
pub const SemanticPrompt = enum(c_int) {
|
||||
none = 0,
|
||||
prompt = 1,
|
||||
prompt_continuation = 2,
|
||||
};
|
||||
|
||||
/// C: GhosttyRowData
|
||||
pub const RowData = enum(c_int) {
|
||||
invalid = 0,
|
||||
|
||||
/// Whether this row is soft-wrapped.
|
||||
/// Output type: bool *
|
||||
wrap = 1,
|
||||
|
||||
/// Whether this row is a continuation of a soft-wrapped row.
|
||||
/// Output type: bool *
|
||||
wrap_continuation = 2,
|
||||
|
||||
/// Whether any cells in this row have grapheme clusters.
|
||||
/// Output type: bool *
|
||||
grapheme = 3,
|
||||
|
||||
/// Whether any cells in this row have styling (may have false positives).
|
||||
/// Output type: bool *
|
||||
styled = 4,
|
||||
|
||||
/// Whether any cells in this row have hyperlinks (may have false positives).
|
||||
/// Output type: bool *
|
||||
hyperlink = 5,
|
||||
|
||||
/// The semantic prompt state of this row.
|
||||
/// Output type: GhosttyRowSemanticPrompt *
|
||||
semantic_prompt = 6,
|
||||
|
||||
/// Whether this row contains a Kitty virtual placeholder.
|
||||
/// Output type: bool *
|
||||
kitty_virtual_placeholder = 7,
|
||||
|
||||
/// Whether this row is dirty and requires a redraw.
|
||||
/// Output type: bool *
|
||||
dirty = 8,
|
||||
|
||||
/// Output type expected for querying the data of the given kind.
|
||||
pub fn OutType(comptime self: RowData) type {
|
||||
return switch (self) {
|
||||
.invalid => void,
|
||||
.wrap, .wrap_continuation, .grapheme, .styled, .hyperlink => bool,
|
||||
.kitty_virtual_placeholder, .dirty => bool,
|
||||
.semantic_prompt => SemanticPrompt,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn get(
|
||||
row_: CRow,
|
||||
data: RowData,
|
||||
out: ?*anyopaque,
|
||||
) callconv(.c) Result {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(RowData, @intFromEnum(data)) catch {
|
||||
return .invalid_value;
|
||||
};
|
||||
}
|
||||
|
||||
return switch (data) {
|
||||
inline else => |comptime_data| getTyped(
|
||||
row_,
|
||||
comptime_data,
|
||||
@ptrCast(@alignCast(out)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn getTyped(
|
||||
row_: CRow,
|
||||
comptime data: RowData,
|
||||
out: *data.OutType(),
|
||||
) Result {
|
||||
const row: Row = @bitCast(row_);
|
||||
switch (data) {
|
||||
.invalid => return .invalid_value,
|
||||
.wrap => out.* = row.wrap,
|
||||
.wrap_continuation => out.* = row.wrap_continuation,
|
||||
.grapheme => out.* = row.grapheme,
|
||||
.styled => out.* = row.styled,
|
||||
.hyperlink => out.* = row.hyperlink,
|
||||
.semantic_prompt => out.* = @enumFromInt(@intFromEnum(row.semantic_prompt)),
|
||||
.kitty_virtual_placeholder => out.* = row.kitty_virtual_placeholder,
|
||||
.dirty => out.* = row.dirty,
|
||||
}
|
||||
|
||||
return .success;
|
||||
}
|
||||
|
||||
test "get wrap" {
|
||||
var zig_row: Row = @bitCast(@as(u64, 0));
|
||||
zig_row.wrap = true;
|
||||
const row: CRow = @bitCast(zig_row);
|
||||
var wrap: bool = false;
|
||||
try testing.expectEqual(Result.success, get(row, .wrap, @ptrCast(&wrap)));
|
||||
try testing.expect(wrap);
|
||||
}
|
||||
|
||||
test "get semantic_prompt" {
|
||||
var zig_row: Row = @bitCast(@as(u64, 0));
|
||||
zig_row.semantic_prompt = .prompt;
|
||||
const row: CRow = @bitCast(zig_row);
|
||||
var sp: SemanticPrompt = .none;
|
||||
try testing.expectEqual(Result.success, get(row, .semantic_prompt, @ptrCast(&sp)));
|
||||
try testing.expectEqual(SemanticPrompt.prompt, sp);
|
||||
}
|
||||
|
||||
test "get dirty" {
|
||||
var zig_row: Row = @bitCast(@as(u64, 0));
|
||||
zig_row.dirty = true;
|
||||
const row: CRow = @bitCast(zig_row);
|
||||
var dirty: bool = false;
|
||||
try testing.expectEqual(Result.success, get(row, .dirty, @ptrCast(&dirty)));
|
||||
try testing.expect(dirty);
|
||||
}
|
||||
133
src/terminal/c/style.zig
Normal file
133
src/terminal/c/style.zig
Normal 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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user