libghostty: add tracked grid ref API

Add a C API for tracked pins, known as a tracked grid ref in C.

The new API can create tracked refs from terminal points, snapshot them
back to regular grid refs for cell access, convert them to coordinates,
move them to a new point, report when their semantic location was lost,
and free the tracked pin bookkeeping. This is backed by PageList tracked
pins and exposed through the libghostty-vt export layer and headers.
This commit is contained in:
Mitchell Hashimoto
2026-05-23 13:56:12 -07:00
parent 10c6121458
commit 2355550a94
15 changed files with 715 additions and 19 deletions

View File

@@ -0,0 +1,19 @@
# Example: `ghostty-vt` Tracked Grid References
This contains a simple example of how to use the `ghostty-vt` terminal and
tracked grid reference APIs to keep a long-lived reference to a cell as the
terminal scrolls, detect when that reference loses its meaningful location,
and move the same tracked handle to a new point.
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_grid_ref_tracked",
.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_grid_ref_tracked,
.version = "0.0.0",
.fingerprint = 0x64bd14b59e76c294,
.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,94 @@
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <ghostty/vt.h>
//! [grid-ref-tracked]
static uint32_t codepoint_at_tracked_ref(GhosttyTrackedGridRef tracked) {
GhosttyGridRef snapshot = GHOSTTY_INIT_SIZED(GhosttyGridRef);
GhosttyResult result = ghostty_tracked_grid_ref_snapshot(tracked, &snapshot);
assert(result == GHOSTTY_SUCCESS);
GhosttyCell cell;
result = ghostty_grid_ref_cell(&snapshot, &cell);
assert(result == GHOSTTY_SUCCESS);
bool has_text = false;
ghostty_cell_get(cell, GHOSTTY_CELL_DATA_HAS_TEXT, &has_text);
assert(has_text);
uint32_t codepoint = 0;
ghostty_cell_get(cell, GHOSTTY_CELL_DATA_CODEPOINT, &codepoint);
return codepoint;
}
int main() {
GhosttyTerminal terminal;
GhosttyTerminalOptions opts = {
.cols = 8,
.rows = 3,
.max_scrollback = 100,
};
GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts);
assert(result == GHOSTTY_SUCCESS);
const char *text = "alpha\r\n"
"bravo\r\n"
"charlie";
ghostty_terminal_vt_write(
terminal, (const uint8_t *)text, strlen(text));
GhosttyTrackedGridRef tracked = NULL;
GhosttyPoint alpha = {
.tag = GHOSTTY_POINT_TAG_ACTIVE,
.value = { .coordinate = { .x = 0, .y = 0 } },
};
result = ghostty_terminal_grid_ref_track(terminal, alpha, &tracked);
assert(result == GHOSTTY_SUCCESS);
// Writing another line scrolls the original "alpha" row into scrollback.
// The tracked ref still follows the same cell.
const char *more = "\r\ndelta";
ghostty_terminal_vt_write(
terminal, (const uint8_t *)more, strlen(more));
assert(ghostty_tracked_grid_ref_has_value(tracked));
printf("tracked codepoint after scroll: %c\n",
(char)codepoint_at_tracked_ref(tracked));
GhosttyPointCoordinate screen = {0};
result = ghostty_tracked_grid_ref_point(
tracked, GHOSTTY_POINT_TAG_SCREEN, &screen);
assert(result == GHOSTTY_SUCCESS);
printf("tracked screen point: %u,%u\n", screen.x, screen.y);
// Resetting the terminal discards the old grid contents. The tracked
// handle remains valid, but no longer has a meaningful location.
ghostty_terminal_reset(terminal);
assert(!ghostty_tracked_grid_ref_has_value(tracked));
GhosttyGridRef discarded = GHOSTTY_INIT_SIZED(GhosttyGridRef);
result = ghostty_tracked_grid_ref_snapshot(tracked, &discarded);
assert(result == GHOSTTY_NO_VALUE);
// The same handle can be moved to a new point after it loses its value.
const char *replacement = "echo";
ghostty_terminal_vt_write(
terminal, (const uint8_t *)replacement, strlen(replacement));
GhosttyPoint echo = {
.tag = GHOSTTY_POINT_TAG_ACTIVE,
.value = { .coordinate = { .x = 0, .y = 0 } },
};
result = ghostty_tracked_grid_ref_set(tracked, terminal, echo);
assert(result == GHOSTTY_SUCCESS);
assert(ghostty_tracked_grid_ref_has_value(tracked));
printf("tracked codepoint after reset/set: %c\n",
(char)codepoint_at_tracked_ref(tracked));
ghostty_tracked_grid_ref_free(tracked);
ghostty_terminal_free(terminal);
return 0;
}
//! [grid-ref-tracked]