Ghostty libghostty-vt:C 语言实现 Scrollback 增量压缩——活动令牌与调用方驱动调度
本文围绕 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)点出了两条设计基线,这两条是整个模式的前提:
- 压缩是调用方驱动的(caller-driven):libghostty-vt 不会自己创建定时器,也不会起后台线程。何时压、压多少,完全由嵌入应用自己决定。
- 嵌入应用负责两件事:
- 调度(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.h 中 ghostty_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.zig、lz4.zig、lz4_differential.zig 三个文件。可以推断:scrollback 以"页(page)"为单位存储与压缩,压缩算法基于 LZ4 并带有差分(differential)压缩路径——这也解释了头文件中"pages may be unprofitable"的说法:压缩是逐页决策的,某页若压后不划算或资源不足,就跳过而不是报错。
两个值得留意的工程事实:
- 压缩不影响逻辑内容:C API 注释明确"Compression changes only the terminal's storage representation and never its logical contents or scrollback limit",且"Accessing compressed history restores it transparently"。也就是说嵌入方不需要感知解压缩——滚动查看、搜索历史时自动透明恢复,业务代码零改动。
- 性能验证入口:仓库自带 scrollback 压缩基准测试 src/benchmark/ScrollbackCompression.zig,可结合
src/benchmark/下的 CLI 选项运行,用于评估压缩路径的实际开销;C 侧 API 的封装实现在 src/terminal/c/terminal.zig 中,头文件注释中的@snippet标注(compression-activity、compression-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。
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 StartedRust0624
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