mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-05 15:18:40 +00:00
libghostty-vt: add C API for snapshotting functions
Expose terminal snapshot through the libghostty-vt C API and add
a new C example that runs in CI to verify this stuff works!
## Example
```c
size_t continuation_limit = 1024;
assert(ghostty_terminal_set(
terminal,
GHOSTTY_TERMINAL_OPT_CONTINUATION_MAX_BYTES,
&continuation_limit) == GHOSTTY_SUCCESS);
uint8_t *bytes = NULL;
size_t len = 0;
assert(ghostty_snapshot_encode_alloc(
terminal, NULL, &bytes, &len) == GHOSTTY_SUCCESS);
GhosttySnapshotDecoder decoder = NULL;
assert(ghostty_snapshot_decoder_new_buf(
NULL, &decoder, bytes, len) == GHOSTTY_SUCCESS);
GhosttyTerminal restored = NULL;
assert(ghostty_snapshot_decoder_decode(
decoder, &restored) == GHOSTTY_SUCCESS);
ghostty_snapshot_decoder_free(decoder);
ghostty_free(NULL, bytes, len);
```
Streaming decode:
```c
GhosttyReader reader = {
.read = read_snapshot,
.userdata = source,
};
GhosttySnapshotDecoder decoder = NULL;
assert(ghostty_snapshot_decoder_new(
NULL, &decoder, reader) == GHOSTTY_SUCCESS);
GhosttyTerminal terminal = NULL;
assert(ghostty_snapshot_decoder_ready(
decoder, &terminal) == GHOSTTY_SUCCESS);
GhosttyResult result;
while ((result = ghostty_snapshot_decoder_next(decoder)) ==
GHOSTTY_SUCCESS) {
size_t rows = 0;
assert(ghostty_snapshot_decoder_get(
decoder,
GHOSTTY_SNAPSHOT_DECODER_DATA_PROGRESS_ROWS,
&rows) == GHOSTTY_SUCCESS);
render(terminal);
}
assert(result == GHOSTTY_NO_VALUE);
```
This commit is contained in:
15
example/c-vt-snapshot/README.md
Normal file
15
example/c-vt-snapshot/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Example: Terminal Snapshots in C
|
||||
|
||||
This example creates a terminal with continuation tracking, encodes its full
|
||||
state, and restores the snapshot using both the one-shot and incremental C
|
||||
decoder APIs. The incremental path uses a synchronous `GhosttyReader` callback
|
||||
and reports each restored history page. The standalone project links the static
|
||||
libghostty-vt artifact so it can run consistently on every supported host.
|
||||
|
||||
## Usage
|
||||
|
||||
Run the example:
|
||||
|
||||
```shell-session
|
||||
zig build run
|
||||
```
|
||||
33
example/c-vt-snapshot/build.zig
Normal file
33
example/c-vt-snapshot/build.zig
Normal file
@@ -0,0 +1,33 @@
|
||||
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"},
|
||||
});
|
||||
exe_mod.addCMacro("GHOSTTY_STATIC", "");
|
||||
|
||||
if (b.lazyDependency("ghostty", .{})) |dep| {
|
||||
exe_mod.linkLibrary(dep.artifact("ghostty-vt-static"));
|
||||
}
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "c_vt_snapshot",
|
||||
.root_module = exe_mod,
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
||||
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);
|
||||
}
|
||||
14
example/c-vt-snapshot/build.zig.zon
Normal file
14
example/c-vt-snapshot/build.zig.zon
Normal file
@@ -0,0 +1,14 @@
|
||||
.{
|
||||
.name = .c_vt_snapshot,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xff13ccc637fd5383,
|
||||
.minimum_zig_version = "0.15.1",
|
||||
.dependencies = .{
|
||||
.ghostty = .{ .path = "../../" },
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
162
example/c-vt-snapshot/src/main.c
Normal file
162
example/c-vt-snapshot/src/main.c
Normal file
@@ -0,0 +1,162 @@
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
//! [snapshot-buffer-reader]
|
||||
typedef struct {
|
||||
const uint8_t *data;
|
||||
size_t len;
|
||||
size_t offset;
|
||||
} BufferReader;
|
||||
|
||||
// GhosttyReader callbacks are synchronous. A successful zero-byte read is
|
||||
// permanent EOF; returning false would report an I/O error.
|
||||
static bool buffer_read(void *userdata,
|
||||
uint8_t *buffer,
|
||||
size_t capacity,
|
||||
size_t *out_read) {
|
||||
BufferReader *reader = userdata;
|
||||
size_t remaining = reader->len - reader->offset;
|
||||
size_t count = remaining < capacity ? remaining : capacity;
|
||||
|
||||
// Deliberately return short reads to demonstrate that the decoder retries.
|
||||
if (count > 64) count = 64;
|
||||
memcpy(buffer, reader->data + reader->offset, count);
|
||||
reader->offset += count;
|
||||
*out_read = count;
|
||||
return true;
|
||||
}
|
||||
//! [snapshot-buffer-reader]
|
||||
|
||||
int main(void) {
|
||||
GhosttyResult result;
|
||||
|
||||
//! [snapshot-encode]
|
||||
GhosttyTerminal source = NULL;
|
||||
// A wide, shallow screen fills backing pages quickly enough to leave older
|
||||
// PAGE records after READY for the incremental decoder to demonstrate.
|
||||
result = ghostty_terminal_new(NULL, &source, 215, 2);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
// Snapshot encoding requires continuation tracking to be enabled before
|
||||
// feeding input. The limit bounds unfinished VT sequence retention.
|
||||
const size_t continuation_limit = 1024;
|
||||
result = ghostty_terminal_set(
|
||||
source,
|
||||
GHOSTTY_TERMINAL_OPT_CONTINUATION_MAX_BYTES,
|
||||
&continuation_limit);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
// Keep enough scrollback to demonstrate incremental history restoration.
|
||||
result = ghostty_terminal_set(
|
||||
source, GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES, NULL);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
const char *line = "snapshot history line\r\n";
|
||||
for (size_t i = 0; i < 1000; i++) {
|
||||
ghostty_terminal_vt_write(
|
||||
source, (const uint8_t *)line, strlen(line));
|
||||
}
|
||||
|
||||
// Leave an SGR sequence unfinished so its continuation is snapshotted too.
|
||||
const char *unfinished = "\x1b[31";
|
||||
ghostty_terminal_vt_write(
|
||||
source, (const uint8_t *)unfinished, strlen(unfinished));
|
||||
|
||||
uint8_t *snapshot = NULL;
|
||||
size_t snapshot_len = 0;
|
||||
result = ghostty_snapshot_encode_alloc(
|
||||
source, NULL, &snapshot, &snapshot_len);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
printf("encoded %zu snapshot bytes\n", snapshot_len);
|
||||
//! [snapshot-encode]
|
||||
|
||||
//! [snapshot-decode]
|
||||
GhosttySnapshotDecoder full_decoder = NULL;
|
||||
result = ghostty_snapshot_decoder_new_buf(
|
||||
NULL, &full_decoder, snapshot, snapshot_len);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
GhosttyTerminal full_terminal = NULL;
|
||||
result = ghostty_snapshot_decoder_decode(full_decoder, &full_terminal);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
ghostty_snapshot_decoder_free(full_decoder);
|
||||
ghostty_terminal_free(full_terminal);
|
||||
//! [snapshot-decode]
|
||||
|
||||
//! [snapshot-incremental]
|
||||
BufferReader reader_state = {
|
||||
.data = snapshot,
|
||||
.len = snapshot_len,
|
||||
.offset = 0,
|
||||
};
|
||||
GhosttyReader reader = {
|
||||
.read = buffer_read,
|
||||
.userdata = &reader_state,
|
||||
};
|
||||
|
||||
GhosttySnapshotDecoder incremental_decoder = NULL;
|
||||
result = ghostty_snapshot_decoder_new(
|
||||
NULL, &incremental_decoder, reader);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
// READY authenticates and returns a renderable terminal before old history.
|
||||
GhosttyTerminal incremental_terminal = NULL;
|
||||
result = ghostty_snapshot_decoder_ready(
|
||||
incremental_decoder, &incremental_terminal);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
uint64_t history_rows = 0;
|
||||
result = ghostty_snapshot_decoder_get(
|
||||
incremental_decoder,
|
||||
GHOSTTY_SNAPSHOT_DECODER_DATA_HISTORY_ROWS_PRIMARY,
|
||||
&history_rows);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
printf("snapshot advertises %llu primary history rows\n",
|
||||
(unsigned long long)history_rows);
|
||||
|
||||
size_t page_count = 0;
|
||||
while ((result = ghostty_snapshot_decoder_next(incremental_decoder)) ==
|
||||
GHOSTTY_SUCCESS) {
|
||||
GhosttyTerminalScreen screen;
|
||||
size_t rows = 0;
|
||||
uint32_t remaining = 0;
|
||||
const GhosttySnapshotDecoderData keys[] = {
|
||||
GHOSTTY_SNAPSHOT_DECODER_DATA_PROGRESS_SCREEN,
|
||||
GHOSTTY_SNAPSHOT_DECODER_DATA_PROGRESS_ROWS,
|
||||
GHOSTTY_SNAPSHOT_DECODER_DATA_PROGRESS_REMAINING,
|
||||
};
|
||||
void *values[] = {&screen, &rows, &remaining};
|
||||
size_t written = 0;
|
||||
result = ghostty_snapshot_decoder_get_multi(
|
||||
incremental_decoder,
|
||||
sizeof(keys) / sizeof(keys[0]),
|
||||
keys,
|
||||
values,
|
||||
&written);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
assert(written == sizeof(keys) / sizeof(keys[0]));
|
||||
|
||||
printf("restored %zu rows to screen %d (%u pages remain)\n",
|
||||
rows, (int)screen, remaining);
|
||||
page_count++;
|
||||
}
|
||||
|
||||
// NO_VALUE means FINISH authenticated successfully and is idempotent.
|
||||
assert(result == GHOSTTY_NO_VALUE);
|
||||
assert(page_count > 0);
|
||||
assert(ghostty_snapshot_decoder_next(incremental_decoder) ==
|
||||
GHOSTTY_NO_VALUE);
|
||||
|
||||
ghostty_snapshot_decoder_free(incremental_decoder);
|
||||
ghostty_terminal_free(incremental_terminal);
|
||||
//! [snapshot-incremental]
|
||||
|
||||
ghostty_free(NULL, snapshot, snapshot_len);
|
||||
ghostty_terminal_free(source);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user