mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-06-07 20:34:29 +00:00
vt: add c render state api and example
Introduce the first public C render-state surface for libghostty-vt. Before this change, the render-state path was only available in Zig, so C embedders had no direct way to create and update that cache. Add an opaque GhosttyRenderState type with new, update, and free entry points, then wire those symbols through the C API bridge and library exports. Keep the surface intentionally minimal for now so ownership and update behavior are established before adding read accessors.
This commit is contained in:
17
example/c-vt-render/README.md
Normal file
17
example/c-vt-render/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Example: `ghostty-vt` Render State
|
||||
|
||||
This contains a simple example of how to use the `ghostty-vt` render-state API
|
||||
to create a render state, update it from terminal content, and clean it up.
|
||||
|
||||
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-render/build.zig
Normal file
42
example/c-vt-render/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_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);
|
||||
}
|
||||
24
example/c-vt-render/build.zig.zon
Normal file
24
example/c-vt-render/build.zig.zon
Normal 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",
|
||||
},
|
||||
}
|
||||
45
example/c-vt-render/src/main.c
Normal file
45
example/c-vt-render/src/main.c
Normal file
@@ -0,0 +1,45 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
//! [render-state-update]
|
||||
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;
|
||||
}
|
||||
//! [render-state-update]
|
||||
@@ -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
|
||||
@@ -101,6 +102,7 @@ extern "C" {
|
||||
#include <ghostty/vt/allocator.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>
|
||||
|
||||
100
include/ghostty/vt/render.h
Normal file
100
include/ghostty/vt/render.h
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @file render.h
|
||||
*
|
||||
* Render state for creating high performance renderers.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_RENDER_H
|
||||
#define GHOSTTY_VT_RENDER_H
|
||||
|
||||
#include <ghostty/vt/allocator.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.
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* @snippet c-vt-render/src/main.c render-state-update
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Opaque handle to a render state instance.
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
typedef struct GhosttyRenderState* GhosttyRenderState;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/** @} */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* GHOSTTY_VT_RENDER_H */
|
||||
@@ -187,6 +187,9 @@ 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_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" });
|
||||
|
||||
@@ -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,10 @@ 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 sgr_new = sgr.new;
|
||||
pub const sgr_free = sgr.free;
|
||||
pub const sgr_reset = sgr.reset;
|
||||
@@ -126,6 +131,7 @@ test {
|
||||
_ = formatter;
|
||||
_ = modes;
|
||||
_ = osc;
|
||||
_ = render;
|
||||
_ = key_event;
|
||||
_ = key_encode;
|
||||
_ = mouse_event;
|
||||
|
||||
107
src/terminal/c/render.zig
Normal file
107
src/terminal/c/render.zig
Normal file
@@ -0,0 +1,107 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const lib_alloc = @import("../../lib/allocator.zig");
|
||||
const CAllocator = lib_alloc.Allocator;
|
||||
const terminal_c = @import("terminal.zig");
|
||||
const renderpkg = @import("../render.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
const RenderStateWrapper = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
state: renderpkg.RenderState = .empty,
|
||||
};
|
||||
|
||||
/// C: GhosttyRenderState
|
||||
pub const RenderState = ?*RenderStateWrapper;
|
||||
|
||||
pub fn new(
|
||||
alloc_: ?*const CAllocator,
|
||||
result: *RenderState,
|
||||
) callconv(.c) Result {
|
||||
result.* = new_(alloc_) catch |err| {
|
||||
result.* = null;
|
||||
return switch (err) {
|
||||
error.OutOfMemory => .out_of_memory,
|
||||
};
|
||||
};
|
||||
|
||||
return .success;
|
||||
}
|
||||
|
||||
fn new_(alloc_: ?*const CAllocator) error{OutOfMemory}!*RenderStateWrapper {
|
||||
const alloc = lib_alloc.default(alloc_);
|
||||
const ptr = alloc.create(RenderStateWrapper) catch
|
||||
return error.OutOfMemory;
|
||||
ptr.* = .{ .alloc = alloc };
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
state_: RenderState,
|
||||
terminal_: terminal_c.Terminal,
|
||||
) callconv(.c) Result {
|
||||
const state = state_ orelse return .invalid_value;
|
||||
const t = terminal_ orelse return .invalid_value;
|
||||
|
||||
state.state.update(state.alloc, t) catch return .out_of_memory;
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn free(state_: RenderState) callconv(.c) void {
|
||||
const state = state_ orelse return;
|
||||
const alloc = state.alloc;
|
||||
state.state.deinit(alloc);
|
||||
alloc.destroy(state);
|
||||
}
|
||||
|
||||
test "render: new/free" {
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&state,
|
||||
));
|
||||
try testing.expect(state != null);
|
||||
free(state);
|
||||
}
|
||||
|
||||
test "render: free null" {
|
||||
free(null);
|
||||
}
|
||||
|
||||
test "render: update invalid value" {
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&state,
|
||||
));
|
||||
defer free(state);
|
||||
|
||||
try testing.expectEqual(Result.invalid_value, update(null, null));
|
||||
try testing.expectEqual(Result.invalid_value, update(state, null));
|
||||
}
|
||||
|
||||
test "render: update" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib_alloc.test_allocator,
|
||||
&terminal,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 10_000,
|
||||
},
|
||||
));
|
||||
defer terminal_c.free(terminal);
|
||||
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&state,
|
||||
));
|
||||
defer free(state);
|
||||
|
||||
try testing.expectEqual(Result.success, update(state, terminal));
|
||||
|
||||
terminal_c.vt_write(terminal, "hello", 5);
|
||||
try testing.expectEqual(Result.success, update(state, terminal));
|
||||
}
|
||||
Reference in New Issue
Block a user