首页
/ Ghostty libghostty-vt:C 语言实现 Scrollback 增量压缩——活动令牌与调用方驱动调度

Ghostty libghostty-vt:C 语言实现 Scrollback 增量压缩——活动令牌与调用方驱动调度

2026-09-05 09:02:18作者:瞿蔚英Wynne

本文围绕 Ghostty 官方示例 c-vt-compression 展开,讲解嵌入应用(embedding application)如何基于 libghostty-vt 的 C API 实现 Scrollback 增量压缩:通过缓存 ghostty_terminal_compression_activity 返回的压缩活动令牌来检测终端变化、用自己的空闲定时器(idle timer)调度 ghostty_terminal_compress 的增量压缩步骤,并正确处理 PENDING / COMPLETE / UNSUPPORTED 三种调度结果。读完后你能在自研终端或终端组件中落地“空闲期后台压缩 scrollback”的完整模式,并理解其源码层面的设计约束。

1. 示例定位:libghostty-vt 不做调度,调度权在嵌入方

example/c-vt-compression 这个示例演示的核心问题非常明确:当一个应用把 libghostty-vt 作为库嵌入时,如何在不阻塞前台 I/O 的前提下,对终端 scrollback(回滚缓冲)做压缩

官方 README(example/c-vt-compression/README.md)点出了两条设计基线,这两条是整个模式的前提:

  1. 压缩是调用方驱动的(caller-driven):libghostty-vt 不会自己创建定时器,也不会起后台线程。何时压、压多少,完全由嵌入应用自己决定。
  2. 嵌入应用负责两件事
    • 调度(scheduling):用自己的空闲定时器决定何时发起压缩;
    • 串行化(serialization):压缩调用必须与终端的其他访问(写入、渲染、搜索等)串行执行,因为 ghostty_terminal_compress 对同一终端的其他操作不是线程安全的(见 include/ghostty/vt/terminal.h 中的函数注释)。

运行示例本身很简单,进入示例目录后执行 zig build run 即可,完整流程(建终端 → 写入可压缩历史 → 追踪活动令牌 → 模拟空闲压缩循环)都由单文件 main.c 演示。

2. C API 全景:压缩相关的三个核心接口

Scrollback 压缩在 C 头文件 include/ghostty/vt/terminal.h 中由三块构成,先建立这张全景表,后面逐节对照源码。

2.1 压缩模式 GhosttyTerminalCompressionMode

枚举值 含义
GHOSTTY_TERMINAL_COMPRESSION_MODE_INCREMENTAL 执行一步有界(bounded)的压缩工作,适合空闲回调调度;这是示例采用的模式
GHOSTTY_TERMINAL_COMPRESSION_MODE_FULL 同步地扫描当前所有符合条件的页;在大规模 scrollback 上可能造成卡顿,头文件明确提示 "can stall on large scrollback buffers"

2.2 调度结果 GhosttyTerminalCompressionResult

枚举值 语义 嵌入方应对
GHOSTTY_TERMINAL_COMPRESSION_RESULT_PENDING 仍有增量压缩工作未完成 保持终端空闲时再发起下一步
GHOSTTY_TERMINAL_COMPRESSION_RESULT_COMPLETE 本轮 pass 已无后续可调度 停止压缩,回到等待“活动令牌变化”
GHOSTTY_TERMINAL_COMPRESSION_RESULT_UNSUPPORTED 当前目标平台不可用(头文件注释:"Retained-mapping reclamation is unavailable on this target") 视为终态,不再调度

2.3 两个函数

// 读取压缩活动令牌(只观察,不执行任何压缩)
GhosttyResult ghostty_terminal_compression_activity(
    GhosttyTerminal terminal,
    uint64_t* out_activity);

// 压缩符合条件的 scrollback
GhosttyResult ghostty_terminal_compress(
    GhosttyTerminal terminal,
    GhosttyTerminalCompressionMode mode,
    GhosttyTerminalCompressionResult* out_result);

活动令牌的使用约束(来自头文件注释,include/ghostty/vt/terminal.hghostty_terminal_compression_activity 的文档):

  • 令牌是不透明的,只有相等性比较有含义;
  • 嵌入方应缓存它,值变化时重启自己的压缩空闲延迟(而不是在输出路径上直接压缩);
  • 令牌可以回绕(wrap),变大变小含义相同;
  • 该函数只观察终端状态,不执行也不调度压缩;终端句柄为 NULL 时返回 GHOSTTY_INVALID_VALUE

ghostty_terminal_compress 的注释补充了两条重要的语义边界

  • 压缩是**机会主义(opportunistic)**的:COMPLETE 表示"pass 结束了",不保证每一页都被压缩——某些页可能压缩收益不足(unprofitable),也可能遇到分配或回收失败;
  • 压缩只改变终端的存储表示,绝不改变逻辑内容,也不影响 scrollback 上限;访问已压缩的历史会被透明地恢复。

3. 示例源码逐段精读:完整的压缩调度模式

下面按 main.c 的实际结构走一遍。

3.1 创建终端并设置 scrollback 上限

GhosttyTerminal terminal;
GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 80, 24);
assert(result == GHOSTTY_SUCCESS);

size_t max_scrollback_bytes = 10 * 1024 * 1024;
result = ghostty_terminal_set(
    terminal,
    GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES,
    &max_scrollback_bytes);
assert(result == GHOSTTY_SUCCESS);

示例以 80×24 创建终端,并通过 ghostty_terminal_set 把 scrollback 上限设为 10 MiB(GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES)。这个上限设定了对压缩"有意义"的前提:只有存在受上限约束的大缓冲,压缩省下的存储才有实际价值。

3.2 追踪压缩活动:token 比较而非直接压缩

uint64_t compression_activity;
result = ghostty_terminal_compression_activity(terminal, &compression_activity);
assert(result == GHOSTTY_SUCCESS);

// 终端变化可能使 token 改变。改变时重启应用自己的空闲定时器,
// 而不是在输出路径上直接压缩。
const char *line = "repeated and compressible terminal history\r\n";
for (size_t i = 0; i < 4000; i++) {
  ghostty_terminal_vt_write(terminal, (const uint8_t *)line, strlen(line));
}

uint64_t new_activity;
result = ghostty_terminal_compression_activity(terminal, &new_activity);
assert(result == GHOSTTY_SUCCESS);
if (new_activity != compression_activity) {
  compression_activity = new_activity;
  // 在这里重启应用的压缩空闲定时器。
}

这段是 README 强调的模式核心:写入前后各取一次活动令牌做相等比较,变了就重置空闲定时器。示例特意写入了 4000 行重复文本("repeated and compressible terminal history"),制造大量可压缩的 scrollback 历史。源码注释还点明了一个工程细节:比较发生在输出路径之外——真实应用中通常是在处理完一批 PTY 输出后顺带检查一次 token,而不是每写一个字节就压缩。

3.3 空闲步骤:一次有界的增量压缩

//! [compression-idle-step]
// 在应用的空闲定时器触发后执行一步。返回 true 表示
// 只要终端仍空闲,应用应继续调度下一步。
static bool compression_idle_step(GhosttyTerminal terminal) {
  GhosttyTerminalCompressionResult compression_result;
  GhosttyResult result = ghostty_terminal_compress(
      terminal,
      GHOSTTY_TERMINAL_COMPRESSION_MODE_INCREMENTAL,
      &compression_result);
  assert(result == GHOSTTY_SUCCESS);

  switch (compression_result) {
    case GHOSTTY_TERMINAL_COMPRESSION_RESULT_PENDING:
      return true;
    case GHOSTTY_TERMINAL_COMPRESSION_RESULT_COMPLETE:
    case GHOSTTY_TERMINAL_COMPRESSION_RESULT_UNSUPPORTED:
      return false;
    default:
      assert(false);
      return false;
  }
}
//! [compression-idle-step]

这个函数把 2.2 节的结果表落成了代码:

  • PENDING → 返回 true,请空闲调度器再来一步;
  • COMPLETE / UNSUPPORTED → 返回 false,本轮结束;
  • 其他值 → 断言失败(防御未知枚举值)。

注意断言的是 GhosttyResult(调用是否成功),而业务分支走的是 GhosttyTerminalCompressionResult(调度语义)——两层返回值各司其职,这在 C API 中是通用约定。

3.4 主循环:模拟空闲定时器

// 模拟空闲定时器及其短暂的 pending 工作续延。
while (compression_idle_step(terminal)) {}

ghostty_terminal_free(terminal);
return 0;

while 循环就是在模拟"空闲定时器反复触发":只要还 PENDING 就继续压,直到 COMPLETE。真实应用中这个循环体不会在一个函数里转完,而是由定时器每次触发执行一步,与前台 I/O 交替进行。

4. 源码纵深:压缩落在终端页层

从源码结构看,压缩的实体实现位于 src/terminal/compress/ 目录,包含 Page.ziglz4.ziglz4_differential.zig 三个文件。可以推断:scrollback 以"页(page)"为单位存储与压缩,压缩算法基于 LZ4 并带有差分(differential)压缩路径——这也解释了头文件中"pages may be unprofitable"的说法:压缩是逐页决策的,某页若压后不划算或资源不足,就跳过而不是报错。

两个值得留意的工程事实:

  1. 压缩不影响逻辑内容:C API 注释明确"Compression changes only the terminal's storage representation and never its logical contents or scrollback limit",且"Accessing compressed history restores it transparently"。也就是说嵌入方不需要感知解压缩——滚动查看、搜索历史时自动透明恢复,业务代码零改动。
  2. 性能验证入口:仓库自带 scrollback 压缩基准测试 src/benchmark/ScrollbackCompression.zig,可结合 src/benchmark/ 下的 CLI 选项运行,用于评估压缩路径的实际开销;C 侧 API 的封装实现在 src/terminal/c/terminal.zig 中,头文件注释中的 @snippet 标注(compression-activitycompression-idle-step)说明 Doxygen 文档与示例源码是联动维护的。

5. 嵌入实践清单:把示例搬进真实应用

结合示例与头文件约束,一个生产级的调用方应当做到:

要点 依据 做法
空闲才压缩 README / 头文件 用应用自己的 idle timer,输出活跃期间不触发 ghostty_terminal_compress
token 驱动重启定时器 main.c 批处理输出后比较 compression_activity,变化则重置空闲延迟;只比较相等性
增量模式优先 terminal.h INCREMENTAL 有界、可续延;FULL 是同步全扫描,大缓冲会卡,仅在可控场景(如退出前、空闲窗口充裕)使用
串行化访问 头文件 "not thread-safe" 注释 压缩与 vt_write、渲染、搜索等在同一执行域内互斥,切勿另开后台线程直接调用
容忍"压不完全" opportunistic 语义 不要把 COMPLETE 当"全部压缩成功";UNSUPPORTED 是平台能力问题,静默降级即可
先设 scrollback 上限 示例的 GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES ghostty_terminal_set 在会话早期配置,10 MiB 可作为参考量级

6. 小结

c-vt-compression 示例的价值在于它给出了一套最小但完整的 caller-driven 压缩调度模式:用不透明活动令牌做"是否该重启空闲计时"的判断,用有界的 INCREMENTAL 步骤做"每次只做一点"的执行,用 PENDING/COMPLETE/UNSUPPORTED 三态闭环做"是否继续"的决策。libghostty-vt 刻意不内置定时器与后台线程,把调度主权留给嵌入应用——这既保证了库在无 UI 事件循环环境(C/C++/WASM 嵌入)下的可移植性,也要求嵌入方理解并接受"压缩是机会主义的、访问历史是透明的"这两条语义。配套入口:示例与构建脚本见 example/c-vt-compression/,API 声明见 include/ghostty/vt/terminal.h,压缩实现见 src/terminal/compress/,性能基准见 src/benchmark/ScrollbackCompression.zig

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