Ghostty C API 实战:用 ghostty-vt 的 Tracked Grid Reference 实现跨滚动、跨重置的单元格长引用
本文基于 Ghostty 仓库中的官方示例 c-vt-grid-ref-tracked,完整讲解如何通过 ghostty-vt 的 C API 创建一个“跟踪式网格引用”(tracked grid reference):它能在终端滚动、回滚区修剪、重置等变化时持续跟随同一个单元格,能在引用丢失语义位置时给出明确信号(GHOSTTY_NO_VALUE),并支持把同一个句柄“搬运”到新位置。读完后,你将掌握这套 API 的完整生命周期(创建、读取、坐标转换、失效检测、重定位、释放)、配套的 Zig 构建方式,以及其底层基于 PageList 引脚(pin)的实现原理,可直接套用于在嵌入式终端组件中实现选区、搜索状态、书签等长生命周期锚点。
1. 为什么需要“跟踪式”网格引用
Ghostty 的 ghostty-vt 库把终端网格中的单元格位置抽象为“网格引用”(grid reference)。官方头文件 grid_ref.h 中明确区分了两类引用,这是理解本示例的前提:
非跟踪引用(Untracked)——值类型快照,由 ghostty_terminal_grid_ref() 获得。它“只在终端下一次更新之前有效”,之后任何操作(哪怕是看似无关的网格变化)都可能使其失效。它的定位是“读出来立刻缓存”,一次查找有性能开销,但不会持续拖累终端性能。
跟踪引用(Tracked)——由 ghostty_terminal_grid_ref_track() 创建的、调用方持有的(owned)引用。它会在常规的屏幕操作中自动跟随其单元格:滚动、回滚区修剪(scrollback pruning)、窗口 resize/reflow 等终端变更都会自动更新这个引用。但它仍可能“丢失语义位置”——当底层网格被重置(reset)、修剪或整体丢弃,且无法映射到某个有意义的新单元格时:
ghostty_tracked_grid_ref_has_value()返回false;ghostty_tracked_grid_ref_snapshot()/ghostty_tracked_grid_ref_point()返回GHOSTTY_NO_VALUE;- 句柄本身仍然有效,可以用
ghostty_tracked_grid_ref_set()把它移到新位置继续用。
头文件同时给出了使用约束:跟踪引用“为每次终端变更增加簿记开销,应谨慎使用,只用于选区、搜索状态、标记或应用侧书签这类长生命周期锚点”;并且网格引用 API 不是为渲染循环设计的,渲染请用 render state API。
本示例 c-vt-grid-ref-tracked 正是针对这些行为的官方演示:保持一个长生命周期引用指向某个单元格、检测它何时失去有意义的位置、再把同一个句柄移到新点。
2. 构建方式:用 Zig 构建 C 示例程序,但 API 与任何 C 工具链兼容
示例自带的 README 说明了构建思路:示例用 build.zig + Zig 来编译 C 程序,目的是复用 Ghostty 的构建逻辑并直接依赖其源码树;而 Ghostty 本身对外发布的是一个标准 C 库,任何 C 工具链(CMake、Make、MSVC 等)都可以链接使用。
运行方式(来自 README):
zig build run
构建脚本 build.zig 的关键点:
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", .{
// 设置 .simd = false 可强制纯静态构建(不依赖 libc),
// 但有明显性能损失;若你的宿主应用本来就要 libc,应保持 simd 开启
// .simd = false,
})) |dep| {
exe_mod.linkLibrary(dep.artifact("ghostty-vt"));
}
lazyDependency保证只在真正构建本示例时才拉取 ghostty 依赖;- 链接的是
ghostty-vt这个产物,即纯终端/网格核心库(不含 GUI 部分)。
依赖声明在 build.zig.zon 中,要求 minimum_zig_version = "0.15.1",并用 path 依赖指向仓库根目录(示例在仓库内测试时要保证与实际源码同步),同时注释中给出了实际项目更常用的 URL 式依赖写法(下载指定 commit 的源码 tarball 并校验 hash):
.dependencies = .{
// 示例仓库内使用 path 依赖以便始终测试所捆绑的源码
.ghostty = .{ .path = "../../" },
// 实际项目中推荐 URL 依赖:
// .ghostty = .{
// .url = ".../archive/COMMIT.tar.gz",
// .hash = "...",
// },
},
也就是说,非 Zig 项目的集成方式就是:拿到 Ghostty 的 C 库产物,把 include/ghostty/vt.h 作为头文件、链接库即可,示例中 C 代码只依赖标准库和 ghostty/vt.h。
3. 示例源码逐段解析
完整源码见 src/main.c(全文如下,注意该文件同时被 grid_ref.h 中的 @snippet c-vt-grid-ref-tracked/src/main.c grid-ref-tracked 引用,作为 Doxygen 文档中的官方代码示例):
#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;
GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 8, 3);
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]
按执行顺序拆解这段代码,它恰好覆盖了 tracked grid reference 的完整生命周期:
第 1 步:创建终端与内容。 ghostty_terminal_new(NULL, &terminal, 8, 3) 创建 8 列 × 3 行的终端(allocator 传 NULL 使用默认分配器;终端创建时已带合理的默认配置,如回滚区上限,可在用前用 ghostty_terminal_set() 调整)。随后 ghostty_terminal_vt_write() 写入 alpha / bravo / charlie 三行,填满整个活动区,此时第 (0,0) 行即 alpha。
第 2 步:创建跟踪引用。 构造一个 GhosttyPoint:tag 为 GHOSTTY_POINT_TAG_ACTIVE(活动区坐标系),坐标 (0, 0),调用 ghostty_terminal_grid_ref_track() 得到句柄。头文件 terminal.h 说明:引用绑定到“创建时活动的那个终端屏幕/page-list”;若点越界则返回 GHOSTTY_INVALID_VALUE 并写出 NULL;句柄必须用 ghostty_tracked_grid_ref_free() 释放,即使终端先被释放,句柄对 tracked-grid-ref API 仍然有效(报告无值且可释放)。
第 3 步:滚动后引用自动跟随。 再写入 "\r\ndelta",alpha 行被顶入回滚区。关键断言是 ghostty_tracked_grid_ref_has_value(tracked) 仍为真,且通过 codepoint_at_tracked_ref() 读出的仍是 a——这正是“跟踪”的语义:引用跟着单元格走,而不是钉死在屏幕坐标上。注意读取单元格数据的标准姿势:先 ghostty_tracked_grid_ref_snapshot() 快照成普通 GhosttyGridRef(这是一个短生命周期的非跟踪引用,必须在下次终端更新前读完),再 ghostty_grid_ref_cell() 取 GhosttyCell,最后用 ghostty_cell_get() 按数据类型提取字段——GHOSTTY_CELL_DATA_HAS_TEXT(输出 bool*)与 GHOSTTY_CELL_DATA_CODEPOINT(输出 uint32_t*,见 screen.h 与 screen.h)。
第 4 步:把引用转回坐标。 ghostty_tracked_grid_ref_point(tracked, GHOSTTY_POINT_TAG_SCREEN, &screen) 把引用转换为“整个屏幕(含回滚区)”坐标系下的 (x, y)。此时 alpha 已滚入历史区,因此它在 screen 坐标系中的 y 会超出可视区域——这正体现了 tracked API 与非跟踪版本的差异:非跟踪版的 ghostty_terminal_point_from_grid_ref()(见 terminal.h)要求引用来自同一终端且只活到下次变更,而 tracked 版本不暴露中间的非跟踪 GhosttyGridRef,且按“当前持有该引用的屏幕/page-list”解析,即使终端后来在主屏/备用屏之间切换过。
四种坐标系 tag 定义于 point.h:
| Tag | 含义 |
|---|---|
GHOSTTY_POINT_TAG_ACTIVE |
光标可移动的活动区 |
GHOSTTY_POINT_TAG_VIEWPORT |
当前可见视口(随滚动变化) |
GHOSTTY_POINT_TAG_SCREEN |
整个屏幕(含回滚区),y 可超过页尺寸 |
GHOSTTY_POINT_TAG_HISTORY |
仅回滚历史(活动区之前) |
第 5 步:终端重置 → 失去有意义的位置。 ghostty_terminal_reset() 执行 RIS 全量重置,丢弃旧网格内容。此时三个断言演示了“失效但句柄存活”的契约:has_value() 为 false;snapshot() 返回 GHOSTTY_NO_VALUE;句柄本身没有崩溃或悬空,仍可以继续使用。
第 6 步:重定位同一句柄。 写入 echo 后,用 ghostty_tracked_grid_ref_set(tracked, terminal, echo) 把同一个句柄移到新的活动区 (0,0)。文档(grid_ref_tracked.h)约定:成功后引用开始跟踪新点并清除“无值”状态;GHOSTTY_OUT_OF_MEMORY 时原引用保持不变;terminal 参数必须是创建该引用的同一个终端实例。
第 7 步:释放。 ghostty_tracked_grid_ref_free()(传 NULL 安全无副作用)后释放终端。
4. API 速查
tracked 引用的四个核心函数全部定义在 grid_ref_tracked.h,创建入口在 terminal.h:
| 函数 | 作用 | 关键返回/语义 |
|---|---|---|
ghostty_terminal_grid_ref_track(terminal, point, &out_ref) |
为某点创建跟踪引用 | GHOSTTY_SUCCESS / GHOSTTY_INVALID_VALUE(点越界时写出 NULL)/ GHOSTTY_OUT_OF_MEMORY |
ghostty_tracked_grid_ref_free(ref) |
释放句柄;NULL 安全;终端先释放后仍可安全调用 |
无返回值 |
ghostty_tracked_grid_ref_has_value(ref) |
当前是否仍有有意义的位置 | 终端已释放时返回 false |
ghostty_tracked_grid_ref_point(ref, tag, &coord) |
转换为指定坐标系下的点 | 无值(含终端已释放、无法表示)时 GHOSTTY_NO_VALUE;out_point 可为 NULL |
ghostty_tracked_grid_ref_set(ref, terminal, point) |
把同一句柄移到新点 | 成功后清除“无值”状态;OOM 时原引用不变 |
ghostty_tracked_grid_ref_snapshot(ref, &grid_ref) |
快照为非跟踪 GhosttyGridRef 以便读单元格数据 |
返回的 GhosttyGridRef 只活到下次终端更新;无值时 GHOSTTY_NO_VALUE |
返回值枚举 GHOSTTY_SUCCESS(0)、GHOSTTY_NO_VALUE(-4)等定义在 types.h。带 size 字段的 C ABI 结构(如 GhosttyGridRef)需用 GHOSTTY_INIT_SIZED() 宏初始化(见 types.h),示例中的 GhosttyGridRef snapshot = GHOSTTY_INIT_SIZED(GhosttyGridRef); 就是这一约定。
5. 底层实现:Pin + 屏幕代数校验
从源码结构看,C 层实现在 src/terminal/c/grid_ref_tracked.zig。TrackedGridRef 结构只有五个字段:
pub const TrackedGridRef = struct {
alloc: std.mem.Allocator,
terminal: terminal_c.Terminal,
screen_key: terminal_c.TerminalScreen,
screen_generation: usize,
pin: *PageList.Pin,
};
它不持有单元格数据,而是持有对终端 PageList(页列表,即主屏+回滚区的统一结构)的一个 pin(引脚)。头文件注释直白地解释了“跟踪”的机制:“underlying PageList pin is automatically updated as the PageList changes”——页列表在滚动、trim、reflow 时会自动平移 pin,这就是 has_value() 在滚动后仍为真的原因。
pageList() 辅助函数则解释了“失去价值”的两个来源:
fn pageList(ref: *const TrackedGridRef) ?*PageList {
const wrapper = ref.terminal orelse return null;
const t = wrapper.terminal;
if (t.screens.generation(ref.screen_key) != ref.screen_generation) return null;
const screen = t.screens.get(ref.screen_key) orelse return null;
return &screen.pages;
}
即:终端指针为空(终端已释放),或该屏幕的**代数(generation)**发生了变化(终端 reset 会重新初始化屏幕,代数不匹配)——两种情况都返回 null,进而使 has_value()/snapshot()/point() 报告无值,而结构体本身不依赖任何已释放内存,所以“句柄继续有效、可安全释放”这一契约在实现层面成立。snapshot() 内部调用 CGridRef.fromPin(ref.pin.*) 把 pin 转成普通 GhosttyGridRef;point() 则走 list.pointFromPin(tag, pin) 在持有者页列表中做坐标换算;set() 会先校验 ref.terminal == terminal_(跨终端重定位直接 GHOSTTY_INVALID_VALUE),与头文件承诺一致。
释放路径也值得注意:tracked_grid_ref_free() 除了 list.untrackPin(ref.pin) 外,还会把该引用从终端的 tracked_grid_refs 容器中 swapRemove——也就是说终端对每个跟踪引用都持有反向登记,这正是头文件所说“每个跟踪引用为终端变更增加簿记开销”的实现来源,也印证了“ sparingly(节制使用)”的告诫。
6. 工程使用建议与边界
结合头文件契约与实现,使用这套 API 时需要注意:
- 读取模式固定为“先快照再读”:从 tracked 引用取任何单元格数据,都必须先
snapshot()得到短生命周期的GhosttyGridRef,并在终端再次变更(vt_write、resize、reset、free等)之前读完ghostty_grid_ref_cell/row/graphemes/hyperlink_uri/style。 - “无值”是状态不是错误:reset、回滚修剪等会合法地把引用打入无值态。健壮的做法是先用
has_value()探测,或把GHOSTTY_NO_VALUE当作正常分支处理,必要时set()重定位;不要断言“创建后永远有值”。 - 坐标系选择要匹配用途:屏幕渲染定位用
ACTIVE/VIEWPORT,全局搜索/选区索引用SCREEN/HISTORY;引用滚出可视区后,VIEWPORT坐标会表示不出来(返回GHOSTTY_NO_VALUE),这是 point.h 中各 tag 语义的直接推论。 - 性能预算:网格引用 API 明确不适合作为渲染循环核心(渲染请用 render state API);跟踪引用会随数量线性增加终端变更开销,适合少量长生命周期锚点,不适合给每个单元格建引用。
- 终端先释放是安全的:终端释放后句柄只会报告无值并允许释放,但此后不应再对它做任何有意义的使用,建议尽快
free。
7. 小结
本示例以不到 90 行 C 代码串起了 ghostty-vt 跟踪网格引用的完整语义:创建(ghostty_terminal_grid_ref_track)→ 滚动跟随(has_value/snapshot)→ 坐标回转(point + 四种 GhosttyPointTag)→ 重置失效(GHOSTTY_NO_VALUE)→ 句柄复用(set)→ 释放(free)。配套文件可直接对照阅读:
- 示例入口与构建:src/main.c、build.zig、build.zig.zon
- C API 契约:grid_ref.h、grid_ref_tracked.h、point.h、terminal.h、screen.h
- 参考实现:src/terminal/c/grid_ref_tracked.zig
如果你在基于 ghostty-vt 构建编辑器内嵌终端、搜索高亮或选区持久化,这套 tracked grid reference API 就是官方给出的“把语义锚点钉在内容上而不是钉在坐标上”的标准方案。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00