libghostty: starting render state API in C (#11664)

This adds the `terminal.RenderState` API for C. 

The render state API is the API that should be used to create a high
performance renderer. It limits access to a terminal instance to a very
optimized `update` call so that terminal IO is blocked for a tiny amount
of time. After that, all read access on the RenderState is safe to build
frame data.

## Example

```c
int main(void) {
  GhosttyResult result;

  GhosttyTerminal terminal = NULL;
  GhosttyTerminalOptions terminal_opts = {
      .cols = 80,
      .rows = 24,
      .max_scrollback = 10000,
  };
  result = ghostty_terminal_new(NULL, &terminal, terminal_opts);
  assert(result == GHOSTTY_SUCCESS);

  GhosttyRenderState render_state = NULL;
  result = ghostty_render_state_new(NULL, &render_state);
  assert(result == GHOSTTY_SUCCESS);

  const char* first_frame = "first frame\r\n";
  ghostty_terminal_vt_write(
      terminal,
      (const uint8_t*)first_frame,
      strlen(first_frame));
  result = ghostty_render_state_update(render_state, terminal);
  assert(result == GHOSTTY_SUCCESS);

  const char* second_frame = "second frame\r\n";
  ghostty_terminal_vt_write(
      terminal,
      (const uint8_t*)second_frame,
      strlen(second_frame));
  result = ghostty_render_state_update(render_state, terminal);
  assert(result == GHOSTTY_SUCCESS);

  printf("Render state was updated successfully.\n");

  ghostty_render_state_free(render_state);
  ghostty_terminal_free(terminal);
  return 0;
}
```

## API Changes

New `GhosttyRenderState` C API (`include/ghostty/vt/render.h`):

| Function | Description |
|---|---|
| `ghostty_render_state_new` | Allocate an empty render state. |
| `ghostty_render_state_free` | Destroy a render state. |
| `ghostty_render_state_update` | Snapshot a terminal instance into the
render state. |
| `ghostty_render_state_get` | Type-tagged read of dimensions, dirty
state, colors, cursor, palette. |
| `ghostty_render_state_set` | Type-tagged write (currently: dirty
state). |
| `ghostty_render_state_colors_get` | Bulk color read via sized-struct
for forward compatibility. |
| `ghostty_render_state_row_iterator_new` | Allocate a reusable row
iterator. |
| `ghostty_render_state_row_iterator_next` | Advance the row iterator. |
| `ghostty_render_state_row_iterator_free` | Destroy a row iterator. |
| `ghostty_render_state_row_get` | Read per-row data (dirty flag, raw
row, cells). |
| `ghostty_render_state_row_set` | Write per-row data (dirty flag). |
| `ghostty_render_state_row_cells_new` | Allocate a reusable cell
iterator. |
| `ghostty_render_state_row_cells_next` | Advance the cell iterator. |
| `ghostty_render_state_row_cells_select` | Jump the cell iterator to a
specific column. |
| `ghostty_render_state_row_cells_get` | Read per-cell data (raw cell,
style, graphemes). |
| `ghostty_render_state_row_cells_free` | Destroy a cell iterator. |

`GhosttyRenderStateData` keys (for `_get`):

| Key | Type | Description |
|---|---|---|
| `COLS` | `uint16_t` | Viewport width in cells. |
| `ROWS` | `uint16_t` | Viewport height in cells. |
| `DIRTY` | `GhosttyRenderStateDirty` | Global dirty state. |
| `ROW_ITERATOR` | `GhosttyRenderStateRowIterator` | Populate a
pre-allocated row iterator. |
| `COLOR_BACKGROUND` | `GhosttyColorRgb` | Default background color. |
| `COLOR_FOREGROUND` | `GhosttyColorRgb` | Default foreground color. |
| `COLOR_CURSOR` | `GhosttyColorRgb` | Explicit cursor color (invalid if
not set). |
| `COLOR_CURSOR_HAS_VALUE` | `bool` | Whether an explicit cursor color
is set. |
| `COLOR_PALETTE` | `GhosttyColorRgb[256]` | Active 256-color palette. |
| `CURSOR_VISUAL_STYLE` | `GhosttyRenderStateCursorVisualStyle` | Bar,
block, underline, or hollow block. |
| `CURSOR_VISIBLE` | `bool` | Cursor visibility from terminal modes. |
| `CURSOR_BLINKING` | `bool` | Cursor blink state from terminal modes. |
| `CURSOR_PASSWORD_INPUT` | `bool` | Whether cursor is at a password
field. |
| `CURSOR_VIEWPORT_HAS_VALUE` | `bool` | Whether cursor is in the
viewport. |
| `CURSOR_VIEWPORT_X` | `uint16_t` | Cursor viewport column. |
| `CURSOR_VIEWPORT_Y` | `uint16_t` | Cursor viewport row. |
| `CURSOR_VIEWPORT_WIDE_TAIL` | `bool` | Cursor on wide-char tail cell.
|

`GhosttyRenderStateOption` keys (for `_set`):

| Key | Type | Description |
|---|---|---|
| `DIRTY` | `GhosttyRenderStateDirty` | Reset global dirty state. |

`GhosttyRenderStateRowData` keys (for `_row_get`):

| Key | Type | Description |
|---|---|---|
| `DIRTY` | `bool` | Whether this row is dirty. |
| `RAW` | `GhosttyRow` | Raw row value. |
| `CELLS` | `GhosttyRenderStateRowCells` | Populate a pre-allocated cell
iterator. |

`GhosttyRenderStateRowOption` keys (for `_row_set`):

| Key | Type | Description |
|---|---|---|
| `DIRTY` | `bool` | Clear/set dirty flag for this row. |

`GhosttyRenderStateRowCellsData` keys (for `_row_cells_get`):

| Key | Type | Description |
|---|---|---|
| `RAW` | `GhosttyCell` | Raw cell value. |
| `STYLE` | `GhosttyStyle` | Resolved style for this cell. |
| `GRAPHEMES_LEN` | `uint32_t` | Total codepoints including base (0 if
empty). |
| `GRAPHEMES_BUF` | `uint32_t*` | Write codepoints into caller buffer. |
This commit is contained in:
Mitchell Hashimoto
2026-03-20 09:34:52 -07:00
committed by GitHub
14 changed files with 2129 additions and 14 deletions

View File

@@ -0,0 +1,19 @@
# Example: `ghostty-vt` Render State
This contains an example of how to use the `ghostty-vt` render-state API
to create a render state, update it from terminal content, iterate rows
and cells, read styles and colors, inspect cursor state, and manage dirty
tracking.
This uses a `build.zig` and `Zig` to build the C program so that we
can reuse a lot of our build logic and depend directly on our source
tree, but Ghostty emits a standard C library that can be used with any
C tooling.
## Usage
Run the program:
```shell-session
zig build run
```

View File

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

View File

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

View File

@@ -0,0 +1,234 @@
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <ghostty/vt.h>
/// Helper: resolve a style color to an RGB value using the palette.
static GhosttyColorRgb resolve_color(GhosttyStyleColor color,
const GhosttyRenderStateColors* colors,
GhosttyColorRgb fallback) {
switch (color.tag) {
case GHOSTTY_STYLE_COLOR_RGB:
return color.value.rgb;
case GHOSTTY_STYLE_COLOR_PALETTE:
return colors->palette[color.value.palette];
default:
return fallback;
}
}
int main(void) {
GhosttyResult result;
//! [render-state-update]
// Create a terminal and render state, then update the render state
// from the terminal. The render state captures a snapshot of everything
// needed to draw a frame.
GhosttyTerminal terminal = NULL;
GhosttyTerminalOptions terminal_opts = {
.cols = 40,
.rows = 5,
.max_scrollback = 10000,
};
result = ghostty_terminal_new(NULL, &terminal, terminal_opts);
assert(result == GHOSTTY_SUCCESS);
GhosttyRenderState render_state = NULL;
result = ghostty_render_state_new(NULL, &render_state);
assert(result == GHOSTTY_SUCCESS);
// Feed some styled content into the terminal.
const char* content =
"Hello, \033[1;32mworld\033[0m!\r\n" // bold green "world"
"\033[4munderlined\033[0m text\r\n" // underlined text
"\033[38;2;255;128;0morange\033[0m\r\n"; // 24-bit orange fg
ghostty_terminal_vt_write(
terminal, (const uint8_t*)content, strlen(content));
result = ghostty_render_state_update(render_state, terminal);
assert(result == GHOSTTY_SUCCESS);
//! [render-state-update]
//! [render-dirty-check]
// Check the global dirty state to decide how much work the renderer
// needs to do. After rendering, reset it to false.
GhosttyRenderStateDirty dirty;
result = ghostty_render_state_get(
render_state, GHOSTTY_RENDER_STATE_DATA_DIRTY, &dirty);
assert(result == GHOSTTY_SUCCESS);
switch (dirty) {
case GHOSTTY_RENDER_STATE_DIRTY_FALSE:
printf("Frame is clean, nothing to draw.\n");
break;
case GHOSTTY_RENDER_STATE_DIRTY_PARTIAL:
printf("Partial redraw needed.\n");
break;
case GHOSTTY_RENDER_STATE_DIRTY_FULL:
printf("Full redraw needed.\n");
break;
}
//! [render-dirty-check]
//! [render-colors]
// Retrieve colors (background, foreground, palette) from the render
// state. These are needed to resolve palette-indexed cell colors.
GhosttyRenderStateColors colors =
GHOSTTY_INIT_SIZED(GhosttyRenderStateColors);
result = ghostty_render_state_colors_get(render_state, &colors);
assert(result == GHOSTTY_SUCCESS);
printf("Background: #%02x%02x%02x\n",
colors.background.r, colors.background.g, colors.background.b);
printf("Foreground: #%02x%02x%02x\n",
colors.foreground.r, colors.foreground.g, colors.foreground.b);
//! [render-colors]
//! [render-cursor]
// Read cursor position and visual style from the render state.
bool cursor_visible = false;
ghostty_render_state_get(
render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE,
&cursor_visible);
bool cursor_in_viewport = false;
ghostty_render_state_get(
render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE,
&cursor_in_viewport);
if (cursor_visible && cursor_in_viewport) {
uint16_t cx, cy;
ghostty_render_state_get(
render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X, &cx);
ghostty_render_state_get(
render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y, &cy);
GhosttyRenderStateCursorVisualStyle style;
ghostty_render_state_get(
render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE,
&style);
const char* style_name = "unknown";
switch (style) {
case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR:
style_name = "bar";
break;
case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK:
style_name = "block";
break;
case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE:
style_name = "underline";
break;
case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW:
style_name = "hollow";
break;
}
printf("Cursor at (%u, %u), style: %s\n", cx, cy, style_name);
}
//! [render-cursor]
//! [render-row-iterate]
// Iterate rows via the row iterator. For each dirty row, iterate its
// cells, read codepoints/graphemes and styles, and emit ANSI-colored
// output as a simple "renderer".
GhosttyRenderStateRowIterator row_iter = NULL;
result = ghostty_render_state_row_iterator_new(NULL, &row_iter);
assert(result == GHOSTTY_SUCCESS);
result = ghostty_render_state_get(
render_state, GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, &row_iter);
assert(result == GHOSTTY_SUCCESS);
GhosttyRenderStateRowCells cells = NULL;
result = ghostty_render_state_row_cells_new(NULL, &cells);
assert(result == GHOSTTY_SUCCESS);
int row_index = 0;
while (ghostty_render_state_row_iterator_next(row_iter)) {
// Check per-row dirty state; a real renderer would skip clean rows.
bool row_dirty = false;
ghostty_render_state_row_get(
row_iter, GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY, &row_dirty);
printf("Row %2d [%s]: ", row_index,
row_dirty ? "dirty" : "clean");
// Get cells for this row (reuses the same cells handle).
result = ghostty_render_state_row_get(
row_iter, GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, &cells);
assert(result == GHOSTTY_SUCCESS);
while (ghostty_render_state_row_cells_next(cells)) {
// Get the grapheme length; 0 means the cell is empty.
uint32_t grapheme_len = 0;
ghostty_render_state_row_cells_get(
cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN,
&grapheme_len);
if (grapheme_len == 0) {
putchar(' ');
continue;
}
// Read the style for this cell. Returns the default style for
// cells that have no explicit styling.
GhosttyStyle style = GHOSTTY_INIT_SIZED(GhosttyStyle);
ghostty_render_state_row_cells_get(
cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE, &style);
// Resolve foreground color for this cell.
GhosttyColorRgb fg =
resolve_color(style.fg_color, &colors, colors.foreground);
// Emit ANSI true-color escape for the foreground.
printf("\033[38;2;%u;%u;%um", fg.r, fg.g, fg.b);
if (style.bold) printf("\033[1m");
if (style.underline) printf("\033[4m");
// Read grapheme codepoints into a buffer and print them.
// The buffer must be at least grapheme_len elements.
uint32_t codepoints[16];
uint32_t len = grapheme_len < 16 ? grapheme_len : 16;
ghostty_render_state_row_cells_get(
cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF,
codepoints);
for (uint32_t i = 0; i < len; i++) {
// Simple ASCII print; a real renderer would handle UTF-8.
if (codepoints[i] < 128)
putchar((char)codepoints[i]);
else
printf("U+%04X", codepoints[i]);
}
printf("\033[0m"); // Reset style after each cell.
}
printf("\n");
// Clear per-row dirty flag after "rendering" it.
bool clean = false;
ghostty_render_state_row_set(
row_iter, GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY, &clean);
row_index++;
}
//! [render-row-iterate]
//! [render-dirty-reset]
// After finishing the frame, reset the global dirty state so the next
// update can report changes accurately.
GhosttyRenderStateDirty clean_state = GHOSTTY_RENDER_STATE_DIRTY_FALSE;
result = ghostty_render_state_set(
render_state, GHOSTTY_RENDER_STATE_OPTION_DIRTY, &clean_state);
assert(result == GHOSTTY_SUCCESS);
//! [render-dirty-reset]
// Cleanup
ghostty_render_state_row_cells_free(cells);
ghostty_render_state_row_iterator_free(row_iter);
ghostty_render_state_free(render_state);
ghostty_terminal_free(terminal);
return 0;
}

View File

@@ -29,6 +29,7 @@
*
* The API is organized into the following groups:
* - @ref terminal "Terminal" - Complete terminal emulator state and rendering
* - @ref render "Render State" - Incremental render state updates for custom renderers
* - @ref formatter "Formatter" - Format terminal content as plain text, VT sequences, or HTML
* - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
* - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences
@@ -99,8 +100,10 @@ extern "C" {
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/color.h>
#include <ghostty/vt/focus.h>
#include <ghostty/vt/formatter.h>
#include <ghostty/vt/render.h>
#include <ghostty/vt/terminal.h>
#include <ghostty/vt/grid_ref.h>
#include <ghostty/vt/osc.h>

587
include/ghostty/vt/render.h Normal file
View File

@@ -0,0 +1,587 @@
/**
* @file render.h
*
* Render state for creating high performance renderers.
*/
#ifndef GHOSTTY_VT_RENDER_H
#define GHOSTTY_VT_RENDER_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/color.h>
#include <ghostty/vt/terminal.h>
#include <ghostty/vt/types.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup render Render State
*
* Represents the state required to render a visible screen (a viewport)
* of a terminal instance. This is stateful and optimized for repeated
* updates from a single terminal instance and only updating dirty regions
* of the screen.
*
* The key design principle of this API is that it only needs read/write
* access to the terminal instance during the update call. This allows
* the render state to minimally impact terminal IO performance and also
* allows the renderer to be safely multi-threaded (as long as a lock is
* held during the update call to ensure exclusive access to the terminal
* instance).
*
* The basic usage of this API is:
*
* 1. Create an empty render state
* 2. Update it from a terminal instance whenever you need.
* 3. Read from the render state to get the data needed to draw your frame.
*
* ## Dirty Tracking
*
* Dirty tracking is a key feature of the render state that allows renderers
* to efficiently determine what parts of the screen have changed and only
* redraw changed regions.
*
* The render state API keeps track of dirty state at two independent layers:
* a global dirty state that indicates whether the entire frame is clean,
* partially dirty, or fully dirty, and a per-row dirty state that allows
* tracking which rows in a partially dirty frame have changed.
*
* The user of the render state API is expected to unset both of these.
* The `update` call does not unset dirty state, it only updates it.
*
* An extremely important detail: setting one dirty state doesn't unset
* the other. For example, setting the global dirty state to false does not
* reset the row-level dirty flags. So, the caller of the render state API must
* be careful to manage both layers of dirty state correctly.
*
* ## Examples
*
* ### Creating and updating render state
* @snippet c-vt-render/src/main.c render-state-update
*
* ### Checking dirty state
* @snippet c-vt-render/src/main.c render-dirty-check
*
* ### Reading colors
* @snippet c-vt-render/src/main.c render-colors
*
* ### Reading cursor state
* @snippet c-vt-render/src/main.c render-cursor
*
* ### Iterating rows and cells
* @snippet c-vt-render/src/main.c render-row-iterate
*
* ### Resetting dirty state after rendering
* @snippet c-vt-render/src/main.c render-dirty-reset
*
* @{
*/
/**
* Opaque handle to a render state instance.
*
* @ingroup render
*/
typedef struct GhosttyRenderState* GhosttyRenderState;
/**
* Opaque handle to a render-state row iterator.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateRowIterator* GhosttyRenderStateRowIterator;
/**
* Opaque handle to render-state row cells.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateRowCells* GhosttyRenderStateRowCells;
/**
* Dirty state of a render state after update.
*
* @ingroup render
*/
typedef enum {
/** Not dirty at all; rendering can be skipped. */
GHOSTTY_RENDER_STATE_DIRTY_FALSE = 0,
/** Some rows changed; renderer can redraw incrementally. */
GHOSTTY_RENDER_STATE_DIRTY_PARTIAL = 1,
/** Global state changed; renderer should redraw everything. */
GHOSTTY_RENDER_STATE_DIRTY_FULL = 2,
} GhosttyRenderStateDirty;
/**
* Visual style of the cursor.
*
* @ingroup render
*/
typedef enum {
/** Bar cursor (DECSCUSR 5, 6). */
GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR = 0,
/** Block cursor (DECSCUSR 1, 2). */
GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK = 1,
/** Underline cursor (DECSCUSR 3, 4). */
GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE = 2,
/** Hollow block cursor. */
GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW = 3,
} GhosttyRenderStateCursorVisualStyle;
/**
* Queryable data kinds for ghostty_render_state_get().
*
* @ingroup render
*/
typedef enum {
/** Invalid / sentinel value. */
GHOSTTY_RENDER_STATE_DATA_INVALID = 0,
/** Viewport width in cells (uint16_t). */
GHOSTTY_RENDER_STATE_DATA_COLS = 1,
/** Viewport height in cells (uint16_t). */
GHOSTTY_RENDER_STATE_DATA_ROWS = 2,
/** Current dirty state (GhosttyRenderStateDirty). */
GHOSTTY_RENDER_STATE_DATA_DIRTY = 3,
/** Populate a pre-allocated GhosttyRenderStateRowIterator with row data
* from the render state (GhosttyRenderStateRowIterator). Row data is
* only valid as long as the underlying render state is not updated.
* It is unsafe to use row data after updating the render state.
* */
GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR = 4,
/** Default/current background color (GhosttyColorRgb). */
GHOSTTY_RENDER_STATE_DATA_COLOR_BACKGROUND = 5,
/** Default/current foreground color (GhosttyColorRgb). */
GHOSTTY_RENDER_STATE_DATA_COLOR_FOREGROUND = 6,
/** Cursor color when explicitly set by terminal state (GhosttyColorRgb).
* Returns GHOSTTY_INVALID_VALUE if no explicit cursor color is set;
* use COLOR_CURSOR_HAS_VALUE to check first. */
GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR = 7,
/** Whether an explicit cursor color is set (bool). */
GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR_HAS_VALUE = 8,
/** The active 256-color palette (GhosttyColorRgb[256]). */
GHOSTTY_RENDER_STATE_DATA_COLOR_PALETTE = 9,
/** The visual style of the cursor (GhosttyRenderStateCursorVisualStyle). */
GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE = 10,
/** Whether the cursor is visible based on terminal modes (bool). */
GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE = 11,
/** Whether the cursor should blink based on terminal modes (bool). */
GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING = 12,
/** Whether the cursor is at a password input field (bool). */
GHOSTTY_RENDER_STATE_DATA_CURSOR_PASSWORD_INPUT = 13,
/** Whether the cursor is visible within the viewport (bool).
* If false, the cursor viewport position values are undefined. */
GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE = 14,
/** Cursor viewport x position in cells (uint16_t).
* Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */
GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X = 15,
/** Cursor viewport y position in cells (uint16_t).
* Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */
GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y = 16,
/** Whether the cursor is on the tail of a wide character (bool).
* Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */
GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL = 17,
} GhosttyRenderStateData;
/**
* Settable options for ghostty_render_state_set().
*
* @ingroup render
*/
typedef enum {
/** Set dirty state (GhosttyRenderStateDirty). */
GHOSTTY_RENDER_STATE_OPTION_DIRTY = 0,
} GhosttyRenderStateOption;
/**
* Queryable data kinds for ghostty_render_state_row_get().
*
* @ingroup render
*/
typedef enum {
/** Invalid / sentinel value. */
GHOSTTY_RENDER_STATE_ROW_DATA_INVALID = 0,
/** Whether the current row is dirty (bool). */
GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY = 1,
/** The raw row value (GhosttyRow). */
GHOSTTY_RENDER_STATE_ROW_DATA_RAW = 2,
/** Populate a pre-allocated GhosttyRenderStateRowCells with cell data for
* the current row (GhosttyRenderStateRowCells). Cell data is only
* valid as long as the underlying render state is not updated.
* It is unsafe to use cell data after updating the render state. */
GHOSTTY_RENDER_STATE_ROW_DATA_CELLS = 3,
} GhosttyRenderStateRowData;
/**
* Settable options for ghostty_render_state_row_set().
*
* @ingroup render
*/
typedef enum {
/** Set dirty state for the current row (bool). */
GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY = 0,
} GhosttyRenderStateRowOption;
/**
* Render-state color information.
*
* This struct uses the sized-struct ABI pattern. Initialize with
* GHOSTTY_INIT_SIZED(GhosttyRenderStateColors) before calling
* ghostty_render_state_colors_get().
*
* Example:
* @code
* GhosttyRenderStateColors colors = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors);
* GhosttyResult result = ghostty_render_state_colors_get(state, &colors);
* @endcode
*
* @ingroup render
*/
typedef struct {
/** Size of this struct in bytes. Must be set to sizeof(GhosttyRenderStateColors). */
size_t size;
/** The default/current background color for the render state. */
GhosttyColorRgb background;
/** The default/current foreground color for the render state. */
GhosttyColorRgb foreground;
/** The cursor color when explicitly set by terminal state. */
GhosttyColorRgb cursor;
/**
* True when cursor contains a valid explicit cursor color value.
* If this is false, the cursor color should be ignored; it will
* contain undefined data.
* */
bool cursor_has_value;
/** The active 256-color palette for this render state. */
GhosttyColorRgb palette[256];
} GhosttyRenderStateColors;
/**
* Create a new render state instance.
*
* @param allocator Pointer to allocator, or NULL to use the default allocator
* @param state Pointer to store the created render state handle
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation
* failure
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_new(const GhosttyAllocator* allocator,
GhosttyRenderState* state);
/**
* Free a render state instance.
*
* Releases all resources associated with the render state. After this call,
* the render state handle becomes invalid.
*
* @param state The render state handle to free (may be NULL)
*
* @ingroup render
*/
void ghostty_render_state_free(GhosttyRenderState state);
/**
* Update a render state instance from a terminal.
*
* This consumes terminal/screen dirty state in the same way as the internal
* render state update path.
*
* @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param terminal The terminal handle to read from (NULL returns GHOSTTY_INVALID_VALUE)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or
* `terminal` is NULL, GHOSTTY_OUT_OF_MEMORY if updating the state requires
* allocation and that allocation fails
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_update(GhosttyRenderState state,
GhosttyTerminal terminal);
/**
* Get a value from a render state.
*
* The `out` pointer must point to a value of the type corresponding to the
* requested data kind (see GhosttyRenderStateData).
*
* @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param data The data kind to query
* @param[out] out Pointer to receive the queried value
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` is
* NULL or `data` is not a recognized enum value
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_get(GhosttyRenderState state,
GhosttyRenderStateData data,
void* out);
/**
* Set an option on a render state.
*
* The `value` pointer must point to a value of the type corresponding to the
* requested option kind (see GhosttyRenderStateOption).
*
* @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param option The option to set
* @param[in] value Pointer to the value to set (NULL returns
* GHOSTTY_INVALID_VALUE)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or
* `value` is NULL
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_set(GhosttyRenderState state,
GhosttyRenderStateOption option,
const void* value);
/**
* Get the current color information from a render state.
*
* This writes as many fields as fit in the caller-provided sized struct.
* `out_colors->size` must be set by the caller (typically via
* GHOSTTY_INIT_SIZED(GhosttyRenderStateColors)).
*
* @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param[out] out_colors Sized output struct to receive render-state colors
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or
* `out_colors` is NULL, or if `out_colors->size` is smaller than
* `sizeof(size_t)`
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_colors_get(GhosttyRenderState state,
GhosttyRenderStateColors* out_colors);
/**
* Create a new row iterator instance.
*
* All fields except the allocator are left undefined until populated
* via ghostty_render_state_get() with
* GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR.
*
* @param allocator Pointer to allocator, or NULL to use the default allocator
* @param[out] out_iterator On success, receives the created iterator handle
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation
* failure
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_row_iterator_new(
const GhosttyAllocator* allocator,
GhosttyRenderStateRowIterator* out_iterator);
/**
* Free a render-state row iterator.
*
* @param iterator The iterator handle to free (may be NULL)
*
* @ingroup render
*/
void ghostty_render_state_row_iterator_free(GhosttyRenderStateRowIterator iterator);
/**
* Move a render-state row iterator to the next row.
*
* Returns true if the iterator moved successfully and row data is
* available to read at the new position.
*
* @param iterator The iterator handle to advance (may be NULL)
* @return true if advanced to the next row, false if `iterator` is
* NULL or if the iterator has reached the end
*
* @ingroup render
*/
bool ghostty_render_state_row_iterator_next(GhosttyRenderStateRowIterator iterator);
/**
* Get a value from the current row in a render-state row iterator.
*
* The `out` pointer must point to a value of the type corresponding to the
* requested data kind (see GhosttyRenderStateRowData).
* Call ghostty_render_state_row_iterator_next() at least once before
* calling this function.
*
* @param iterator The iterator handle to query (NULL returns GHOSTTY_INVALID_VALUE)
* @param data The data kind to query
* @param[out] out Pointer to receive the queried value
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if
* `iterator` is NULL or the iterator is not positioned on a row
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_row_get(
GhosttyRenderStateRowIterator iterator,
GhosttyRenderStateRowData data,
void* out);
/**
* Set an option on the current row in a render-state row iterator.
*
* The `value` pointer must point to a value of the type corresponding to the
* requested option kind (see GhosttyRenderStateRowOption).
* Call ghostty_render_state_row_iterator_next() at least once before
* calling this function.
*
* @param iterator The iterator handle to update (NULL returns GHOSTTY_INVALID_VALUE)
* @param option The option to set
* @param[in] value Pointer to the value to set (NULL returns
* GHOSTTY_INVALID_VALUE)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if
* `iterator` is NULL or the iterator is not positioned on a row
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_row_set(
GhosttyRenderStateRowIterator iterator,
GhosttyRenderStateRowOption option,
const void* value);
/**
* Create a new row cells instance.
*
* All fields except the allocator are left undefined until populated
* via ghostty_render_state_row_get() with
* GHOSTTY_RENDER_STATE_ROW_DATA_CELLS.
*
* You can reuse this value repeatedly with ghostty_render_state_row_get() to
* avoid allocating a new cells container for every row.
*
* @param allocator Pointer to allocator, or NULL to use the default allocator
* @param[out] out_cells On success, receives the created row cells handle
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation
* failure
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_row_cells_new(
const GhosttyAllocator* allocator,
GhosttyRenderStateRowCells* out_cells);
/**
* Queryable data kinds for ghostty_render_state_row_cells_get().
*
* @ingroup render
*/
typedef enum {
/** Invalid / sentinel value. */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_INVALID = 0,
/** The raw cell value (GhosttyCell). */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW = 1,
/** The style for the current cell (GhosttyStyle). */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE = 2,
/** The total number of grapheme codepoints including the base codepoint
* (uint32_t). Returns 0 if the cell has no text. */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN = 3,
/** Write grapheme codepoints into a caller-provided buffer (uint32_t*).
* The buffer must be at least graphemes_len elements. The base codepoint
* is written first, followed by any extra codepoints. */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF = 4,
} GhosttyRenderStateRowCellsData;
/**
* Move a render-state row cells iterator to the next cell.
*
* Returns true if the iterator moved successfully and cell data is
* available to read at the new position.
*
* @param cells The row cells handle to advance (may be NULL)
* @return true if advanced to the next cell, false if `cells` is
* NULL or if the iterator has reached the end
*
* @ingroup render
*/
bool ghostty_render_state_row_cells_next(GhosttyRenderStateRowCells cells);
/**
* Move a render-state row cells iterator to a specific column.
*
* Positions the iterator at the given x (column) index so that
* subsequent reads return data for that cell.
*
* @param cells The row cells handle to reposition (NULL returns
* GHOSTTY_INVALID_VALUE)
* @param x The zero-based column index to select
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `cells`
* is NULL or `x` is out of range
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_row_cells_select(
GhosttyRenderStateRowCells cells, uint16_t x);
/**
* Get a value from the current cell in a render-state row cells iterator.
*
* The `out` pointer must point to a value of the type corresponding to the
* requested data kind (see GhosttyRenderStateRowCellsData).
* Call ghostty_render_state_row_cells_next() or
* ghostty_render_state_row_cells_select() at least once before
* calling this function.
*
* @param cells The row cells handle to query (NULL returns GHOSTTY_INVALID_VALUE)
* @param data The data kind to query
* @param[out] out Pointer to receive the queried value
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if
* `cells` is NULL or the iterator is not positioned on a cell
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_row_cells_get(
GhosttyRenderStateRowCells cells,
GhosttyRenderStateRowCellsData data,
void* out);
/**
* Free a row cells instance.
*
* @param cells The row cells handle to free (may be NULL)
*
* @ingroup render
*/
void ghostty_render_state_row_cells_free(GhosttyRenderStateRowCells cells);
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_RENDER_H */

View File

@@ -1,5 +1,6 @@
const std = @import("std");
const enumpkg = @import("enum.zig");
const structpkg = @import("struct.zig");
const types = @import("types.zig");
const unionpkg = @import("union.zig");
@@ -7,7 +8,8 @@ pub const allocator = @import("allocator.zig");
pub const Enum = enumpkg.Enum;
pub const checkGhosttyHEnum = enumpkg.checkGhosttyHEnum;
pub const String = types.String;
pub const Struct = @import("struct.zig").Struct;
pub const Struct = structpkg.Struct;
pub const structSizedFieldFits = structpkg.sizedFieldFits;
pub const Target = @import("target.zig").Target;
pub const TaggedUnion = unionpkg.TaggedUnion;
pub const cutPrefix = @import("string.zig").cutPrefix;

View File

@@ -41,6 +41,52 @@ pub fn Struct(
};
}
/// Returns true if a struct of type `T` with size `size` can set
/// field `field` (if it fits within the size). This is used for ABI
/// compatibility for structs that have an explicit size field.
pub fn sizedFieldFits(
comptime T: type,
size: usize,
comptime field: []const u8,
) bool {
const offset = @offsetOf(T, field);
const field_size = @sizeOf(@FieldType(T, field));
return size >= offset + field_size;
}
test "sizedFieldFits boundary checks" {
const Sized = extern struct {
size: usize,
a: u8,
b: u32,
};
const size_required = @offsetOf(Sized, "size") + @sizeOf(@FieldType(Sized, "size"));
const a_required = @offsetOf(Sized, "a") + @sizeOf(@FieldType(Sized, "a"));
const b_required = @offsetOf(Sized, "b") + @sizeOf(@FieldType(Sized, "b"));
try testing.expect(sizedFieldFits(Sized, size_required, "size"));
try testing.expect(!sizedFieldFits(Sized, size_required - 1, "size"));
try testing.expect(sizedFieldFits(Sized, a_required, "a"));
try testing.expect(!sizedFieldFits(Sized, a_required - 1, "a"));
try testing.expect(sizedFieldFits(Sized, b_required, "b"));
try testing.expect(!sizedFieldFits(Sized, b_required - 1, "b"));
}
test "sizedFieldFits respects alignment padding" {
const Sized = extern struct {
size: usize,
a: u8,
b: u32,
};
const up_to_padding = @offsetOf(Sized, "b");
try testing.expect(sizedFieldFits(Sized, up_to_padding, "a"));
try testing.expect(!sizedFieldFits(Sized, up_to_padding, "b"));
}
test "packed struct converts to extern with full-size bools" {
const Packed = packed struct {
flag1: bool,

View File

@@ -187,6 +187,22 @@ comptime {
@export(&c.formatter_format_buf, .{ .name = "ghostty_formatter_format_buf" });
@export(&c.formatter_format_alloc, .{ .name = "ghostty_formatter_format_alloc" });
@export(&c.formatter_free, .{ .name = "ghostty_formatter_free" });
@export(&c.render_state_new, .{ .name = "ghostty_render_state_new" });
@export(&c.render_state_update, .{ .name = "ghostty_render_state_update" });
@export(&c.render_state_get, .{ .name = "ghostty_render_state_get" });
@export(&c.render_state_set, .{ .name = "ghostty_render_state_set" });
@export(&c.render_state_colors_get, .{ .name = "ghostty_render_state_colors_get" });
@export(&c.render_state_row_iterator_new, .{ .name = "ghostty_render_state_row_iterator_new" });
@export(&c.render_state_row_iterator_next, .{ .name = "ghostty_render_state_row_iterator_next" });
@export(&c.render_state_row_get, .{ .name = "ghostty_render_state_row_get" });
@export(&c.render_state_row_set, .{ .name = "ghostty_render_state_row_set" });
@export(&c.render_state_row_iterator_free, .{ .name = "ghostty_render_state_row_iterator_free" });
@export(&c.render_state_row_cells_new, .{ .name = "ghostty_render_state_row_cells_new" });
@export(&c.render_state_row_cells_next, .{ .name = "ghostty_render_state_row_cells_next" });
@export(&c.render_state_row_cells_select, .{ .name = "ghostty_render_state_row_cells_select" });
@export(&c.render_state_row_cells_get, .{ .name = "ghostty_render_state_row_cells_get" });
@export(&c.render_state_row_cells_free, .{ .name = "ghostty_render_state_row_cells_free" });
@export(&c.render_state_free, .{ .name = "ghostty_render_state_free" });
@export(&c.terminal_new, .{ .name = "ghostty_terminal_new" });
@export(&c.terminal_free, .{ .name = "ghostty_terminal_free" });
@export(&c.terminal_reset, .{ .name = "ghostty_terminal_reset" });

View File

@@ -4,6 +4,7 @@ pub const focus = @import("focus.zig");
pub const formatter = @import("formatter.zig");
pub const modes = @import("modes.zig");
pub const osc = @import("osc.zig");
pub const render = @import("render.zig");
pub const key_event = @import("key_event.zig");
pub const key_encode = @import("key_encode.zig");
pub const mouse_event = @import("mouse_event.zig");
@@ -35,6 +36,23 @@ pub const formatter_format_buf = formatter.format_buf;
pub const formatter_format_alloc = formatter.format_alloc;
pub const formatter_free = formatter.free;
pub const render_state_new = render.new;
pub const render_state_free = render.free;
pub const render_state_update = render.update;
pub const render_state_get = render.get;
pub const render_state_set = render.set;
pub const render_state_colors_get = render.colors_get;
pub const render_state_row_iterator_new = render.row_iterator_new;
pub const render_state_row_iterator_next = render.row_iterator_next;
pub const render_state_row_get = render.row_get;
pub const render_state_row_set = render.row_set;
pub const render_state_row_iterator_free = render.row_iterator_free;
pub const render_state_row_cells_new = render.row_cells_new;
pub const render_state_row_cells_next = render.row_cells_next;
pub const render_state_row_cells_select = render.row_cells_select;
pub const render_state_row_cells_get = render.row_cells_get;
pub const render_state_row_cells_free = render.row_cells_free;
pub const sgr_new = sgr.new;
pub const sgr_free = sgr.free;
pub const sgr_reset = sgr.reset;
@@ -126,6 +144,7 @@ test {
_ = formatter;
_ = modes;
_ = osc;
_ = render;
_ = key_event;
_ = key_encode;
_ = mouse_event;

1104
src/terminal/c/render.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ const Row = page.Row;
const Result = @import("result.zig").Result;
/// C: GhosttyRow
pub const CRow = u64;
pub const CRow = Row.C;
/// C: GhosttyRowSemanticPrompt
pub const SemanticPrompt = enum(c_int) {

View File

@@ -1947,6 +1947,14 @@ pub const Row = packed struct(u64) {
prompt_continuation = 2,
};
/// C ABI type.
pub const C = u64;
/// Returns this row as a C ABI value.
pub fn cval(self: Row) C {
return @bitCast(self);
}
/// Returns true if this row has any managed memory outside of the
/// row structure (graphemes, styles, etc.)
pub inline fn managedMemory(self: Row) bool {
@@ -2051,6 +2059,14 @@ pub const Cell = packed struct(u64) {
prompt = 2,
};
/// C ABI type.
pub const C = u64;
/// Returns this cell as a C ABI value.
pub fn cval(self: Cell) C {
return @bitCast(self);
}
/// Helper to make a cell that just has a codepoint.
pub fn init(cp: u21) Cell {
// We have to use this bitCast here to ensure that our memory is

View File

@@ -1,8 +1,11 @@
const std = @import("std");
const build_options = @import("terminal_options");
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const fastmem = @import("../fastmem.zig");
const lib = @import("../lib/main.zig");
const lib_target: lib.Target = if (build_options.c_abi) .c else .zig;
const color = @import("color.zig");
const cursor = @import("cursor.zig");
const highlight = @import("highlight.zig");
@@ -222,20 +225,20 @@ pub const RenderState = struct {
style: Style,
};
// Dirty state
pub const Dirty = enum {
/// Not dirty at all. Can skip rendering if prior state was
/// already rendered.
false,
// Dirty state.
pub const Dirty = lib.Enum(lib_target, &.{
// Not dirty at all. Can skip rendering if prior state was
// already rendered.
"false",
/// Partially dirty. Some rows changed but not all. None of the
/// global state changed such as colors.
partial,
// Some rows changed but not all. None of the global state
// changed such as colors.
"partial",
/// Fully dirty. Global state changed or dimensions changed. All rows
/// should be redrawn.
full,
};
// Global state changed or dimensions changed. All rows should
// be redrawn.
"full",
});
const SelectionCache = struct {
selection: Selection,