首页
/ Ghostty C API 实战:用 ghostty-vt 的 Focus 编码接口把焦点事件编码为终端转义序列

Ghostty C API 实战:用 ghostty-vt 的 Focus 编码接口把焦点事件编码为终端转义序列

2026-09-05 15:46:40作者:滑思眉Philip

Ghostty 除了作为完整终端仿真器之外,还对外发布了名为 ghostty-vt 的标准 C 库(libghostty),其中包含一组轻量的“编码”辅助接口。example/c-vt-encode-focus 示例演示了其中最简单的一类用法:调用 ghostty_focus_encode() 把“窗口获得焦点 / 失去焦点”事件编码为终端转义序列(CSI I / CSI O)。读完本文,你将掌握如何编写并构建一个链接 ghostty-vt 的 C 程序,理解该接口的函数签名、返回约定与缓冲区语义,并能从源码层面确认它实际输出的是哪几个字节。

示例的定位与运行方式

该示例位于仓库的 example/c-vt-encode-focus 目录,其 README 说明:这是一个展示如何使用 ghostty-vt focus 编码 API 把 focus gained/lost 事件编码为转义序列的简单示例。示例本身是 C 程序,但通过 build.zig 和 Zig 构建系统来编译——这样做可以直接复用 Ghostty 仓库的构建逻辑并依赖源码树本身,而 Ghostty 实际发布的是标准 C 库,任何 C 工具链都可以链接使用。

按照 example/README.md 的统一约定,所有示例(包括以 c- 开头的 C API 示例)都可以进入目录后执行以下命令构建并运行:

cd example/c-vt-encode-focus
zig build run

其中 zig build runbuild.zig 中注册的 run 步骤,它依赖 install 步骤先编译产物再执行。

完整的 C 示例代码

示例的全部 C 源码只有 src/main.c 一个文件:

#include <stdio.h>
#include <ghostty/vt.h>

//! [focus-encode]
int main() {
  char buf[8];
  size_t written = 0;

  GhosttyResult result = ghostty_focus_encode(
      GHOSTTY_FOCUS_GAINED, buf, sizeof(buf), &written);

  if (result == GHOSTTY_SUCCESS) {
    printf("Encoded %zu bytes: ", written);
    fwrite(buf, 1, written, stdout);
    printf("\n");
  }

  return 0;
}
//! [focus-encode]

代码要点:

  • 调用 ghostty_focus_encode() 时传入事件 GHOSTTY_FOCUS_GAINED(获得焦点),一个 8 字节的输出缓冲区 buf,缓冲区长度 sizeof(buf),以及输出参数 &written 用于接收实际写入的字节数;
  • 只有当返回值等于 GHOSTTY_SUCCESS 时,才把 written 字节写入 stdout;
  • 源码中的 //! [focus-encode] 标记不是注释的普通内容,而是 Doxygen 的 snippet 边界标记(见下文“与文档系统联动”一节)。

示例选用 8 字节缓冲区并非偶然设计,而是出于健壮性考虑:底层实现保证一次编码最多只写 3 个字节(见下节),8 字节足以容纳,同时演示了“调用方提供缓冲区”这一 API 约定。

接口定义:include/ghostty/vt/focus.h

该接口的权威定义在头文件 include/ghostty/vt/focus.h 中,头部注释说明这是 “focus encoding” 模块——把 focus in/out 事件编码为终端转义序列(CSI I / CSI O),服务于焦点报告模式(focus reporting mode,即 mode 1004)。

焦点事件由一个枚举表示:

/**
 * Focus event types for focus reporting mode (mode 1004).
 */
typedef enum GHOSTTY_ENUM_TYPED {
    /** Terminal window gained focus */
    GHOSTTY_FOCUS_GAINED = 0,
    /** Terminal window lost focus */
    GHOSTTY_FOCUS_LOST = 1,
    GHOSTTY_FOCUS_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyFocusEvent;

编码函数原型为:

GHOSTTY_API GhosttyResult ghostty_focus_encode(
    GhosttyFocusEvent event,
    char* buf,
    size_t buf_len,
    size_t* out_written);

参数语义(来自头文件注释):

参数 说明
event 要编码的焦点事件(GHOSTTY_FOCUS_GAINEDGHOSTTY_FOCUS_LOST
buf 输出缓冲区,写入编码后的转义序列;可以为 NULL
buf_len 输出缓冲区的字节容量
out_written 成功时写入实际写入的字节数;缓冲区不足时写入所需缓冲区大小

返回值约定是该 API 值得注意的设计:成功返回 GHOSTTY_SUCCESS;若缓冲区太小则返回 GHOSTTY_OUT_OF_SPACE,并把所需大小写回 out_written,调用方据此用足够大的缓冲区重试。也就是说,buf 允许为 NULL 时可以用一次调用探测所需长度,这是嵌入式场景下典型的“先量后写”契约。

底层实现:实际输出的是哪几个字节

ghostty_focus_encode 是 C 导出符号,真正的实现在 Zig 侧。src/lib_vt.zig 中有显式导出:

@export(&c.focus_encode, .{ .name = "ghostty_focus_encode" });

该符号指向 src/terminal/focus.zig 中的 encode 函数:

/// Maximum number of bytes that `encode` will write. Any users of this
/// should be resilient to this changing, so this is always a specific
/// value (e.g. we don't add unnecessary padding).
pub const max_encode_size = 3;

/// Encode a focus in/out report (CSI I / CSI O).
pub fn encode(
    writer: *std.Io.Writer,
    event: Event,
) std.Io.Writer.Error!void {
    try writer.writeAll(switch (event) {
        .gained => "\x1B[I",
        .lost => "\x1B[O",
    });
}

由此可以确认几个实现事实:

  • 获得焦点输出 3 个字节 ESC [ I(即 \x1B[I,CSI I),失去焦点输出 ESC [ O(即 \x1B[O,CSI O);
  • 编码结果的上限是常量 max_encode_size = 3,因此示例中 8 字节的栈缓冲区必然足够,GHOSTTY_OUT_OF_SPACE 分支在该场景下不会触发;
  • 同一文件内还附带了两个单元测试(test "encode focus gained" / test "encode focus lost"),用固定缓冲区 writer 断言两种事件分别编码为 \x1B[I\x1B[O],是验证该接口行为的最直接依据。

从源码结构看,focus.zig 是终端内部实现,而 C 库通过 src/terminal/c/focus.zig 中的 encode 包装(经由 src/terminal/c/main.zigpub const focus_encode = focus.encode;)对外暴露,形成“C 头文件声明 → 导出符号 → Zig 编码函数”的调用链。

构建系统:build.zig 与 build.zig.zon

这个示例也完整展示了第三方项目如何依赖 ghostty-vtbuild.zig 的核心逻辑:

const exe_mod = b.createModule(.{ .target = target, .optimize = optimize });
exe_mod.addCSourceFiles(.{ .root = b.path("src"), .files = &.{"main.c"} });

// 使用 lazy dependency:只有真正需要时才解析 ghostty 依赖
if (b.lazyDependency("ghostty", .{ /* .simd = false 可做纯静态构建(无 libc),但有明显性能损耗 */ })) |dep| {
    exe_mod.linkLibrary(dep.artifact("ghostty-vt"));
}

const exe = b.addExecutable(.{ .name = "c_vt_encode_focus", .root_module = exe_mod });
b.installArtifact(exe);

关键约定:

  • 可执行目标名使用下划线:c_vt_encode_focus(对应目录名中的连字符),这是 example 目录的统一规范;
  • 通过 lazyDependency("ghostty", ...) 链接 ghostty-vt artifact,注释同时说明:设置 .simd = false 会强制得到不依赖 libc 的纯静态构建,但性能代价显著——如果宿主应用本就依赖 libc,应保持 simd 启用;
  • 依赖声明在 build.zig.zon 中。仓库内的示例使用路径依赖 .ghostty = .{ .path = "../../" },以便始终对照随示例捆绑的源码树进行测试;zon 文件里保留了注释掉的 URL 依赖写法示例,说明真实外部项目通常用带 hash 的 URL 归档依赖指向某个固定提交;minimum_zig_version0.15.1

与文档系统联动:snippet 标记的由来

回到 src/main.c 里那两行 //! [focus-encode]example/AGENTS.md 解释了这一约定:示例源码使用 Doxygen snippet 标记,让 include/ghostty/vt/focus.h 等头文件通过 @snippet c-vt-encode-focus/src/main.c focus-encode 引用同一份代码,而不是在头文件里重复内联代码块。这正是头文件中“Basic Usage / Example”一节直接指向本示例的原因——修改示例代码时需要保持 snippet 标记与头文件引用同步。

另外,example 目录约定所有新示例会被 CI 通过 example/*/build.zig.zon 通配自动发现,因此该示例同时充当了仓库自身的构建与文档集成样例。

小结

c-vt-encode-focus 用不到 20 行 C 代码展示了嵌入 ghostty-vt 的最小路径:包含 ghostty/vt.h → 调用 ghostty_focus_encode(GHOSTTY_FOCUS_GAINED, buf, len, &written) → 检查 GHOSTTY_SUCCESS 并消费 out_written。底层实现确认其输出固定为 3 字节的 CSI I(src/terminal/focus.zig 中的 max_encode_size),且配套单元测试覆盖了 gained/lost 两种事件。若你的终端嵌入场景需要向应用转发窗口焦点变化(配合 mode 1004 焦点报告),可以直接以 example/c-vt-encode-focus 为模板,替换依赖声明为指向发布归档的 URL 依赖,即可脱离 Ghostty 源码树独立构建。

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