首页
/ Ghostty C API 实战:用 ghostty-vt 的 Tracked Grid Reference 实现跨滚动、跨重置的单元格长引用

Ghostty C API 实战:用 ghostty-vt 的 Tracked Grid Reference 实现跨滚动、跨重置的单元格长引用

2026-09-05 18:04:45作者:苗圣禹Peter

本文基于 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.hscreen.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()falsesnapshot() 返回 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_VALUEout_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.zigTrackedGridRef 结构只有五个字段:

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 转成普通 GhosttyGridRefpoint() 则走 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 时需要注意:

  1. 读取模式固定为“先快照再读”:从 tracked 引用取任何单元格数据,都必须先 snapshot() 得到短生命周期的 GhosttyGridRef,并在终端再次变更(vt_writeresizeresetfree 等)之前读完 ghostty_grid_ref_cell/row/graphemes/hyperlink_uri/style
  2. “无值”是状态不是错误:reset、回滚修剪等会合法地把引用打入无值态。健壮的做法是先用 has_value() 探测,或把 GHOSTTY_NO_VALUE 当作正常分支处理,必要时 set() 重定位;不要断言“创建后永远有值”。
  3. 坐标系选择要匹配用途:屏幕渲染定位用 ACTIVE/VIEWPORT,全局搜索/选区索引用 SCREEN/HISTORY;引用滚出可视区后,VIEWPORT 坐标会表示不出来(返回 GHOSTTY_NO_VALUE),这是 point.h 中各 tag 语义的直接推论。
  4. 性能预算:网格引用 API 明确不适合作为渲染循环核心(渲染请用 render state API);跟踪引用会随数量线性增加终端变更开销,适合少量长生命周期锚点,不适合给每个单元格建引用。
  5. 终端先释放是安全的:终端释放后句柄只会报告无值并允许释放,但此后不应再对它做任何有意义的使用,建议尽快 free

7. 小结

本示例以不到 90 行 C 代码串起了 ghostty-vt 跟踪网格引用的完整语义:创建(ghostty_terminal_grid_ref_track)→ 滚动跟随(has_value/snapshot)→ 坐标回转(point + 四种 GhosttyPointTag)→ 重置失效(GHOSTTY_NO_VALUE)→ 句柄复用(set)→ 释放(free)。配套文件可直接对照阅读:

如果你在基于 ghostty-vt 构建编辑器内嵌终端、搜索高亮或选区持久化,这套 tracked grid reference API 就是官方给出的“把语义锚点钉在内容上而不是钉在坐标上”的标准方案。

登录后查看全文
热门项目推荐
相关项目推荐