每日壁纸

一句话项目 slogan:「原生」不是一套外观,是两套实现。

这是「一天一个开源项目」系列的一篇。今天讲 Ghostty。

终端是那种每天开八小时、却很少有人认真挑的工具。真去挑会发现选项被卡成三选二:要快(Alacritty)就没什么窗口和标签页;要功能全(iTerm2、Terminal.app)就得接受它的渲染路径;要跨平台,往往换来的是一套在哪都不太像的 UI。

还有一个更隐蔽的需求:终端能不能被复用。你在写编辑器、CI 面板或者 IDE 插件,想要一块能跑 shell 的地方,最后通常只能塞个 xterm.js,或者干脆调外部程序。

Ghostty 的答案是两头都不妥协:界面用平台原生的(macOS 上 SwiftUI + Metal,Linux 上 GTK4 + OpenGL),核心用 Zig 写成共享的一份,再把这份核心拆出一个 C 库 libghostty-vt,给别人嵌。

项目速览

⭐ 62,487 Stars | 🍴 3,684 Forks | 📄 MIT 协议 | 🔧 18,384 次提交 | 🐛 254 个 Open Issues

截至 2026-10-22。创建于 2022-03-29,1.0 发布于 2024-12-26。18,384 次提交里有 12,089 次来自 mitchellh(Mitchell Hashimoto,HashiCorp 创始人),约占 66%,贡献者总数 422。代码是 Zig 13.6 MB 加 Swift 1.46 MB——这个比例本身就是这篇文章的主题。

阅读价值与前置说明

读完你应该能回答:

  • 一个跨平台应用,怎么做到「每个平台都像是为本平台写的」?代价是什么?
  • 为什么 macOS 上的 Ghostty 界面不是 zig build 产出的?那它怎么跑起来的?
  • 终端核心怎么变成别人能嵌的库?C ABI、WASM 和「零依赖」这三件事怎么凑到一起?
  • 终端热路径上的 SIMD 为什么用 C++ 写、Zig 再调?为什么连一个空 assert 都要自己实现?
  • 一个 6 万 Star 的项目为什么把「只有 Ghostty 支持的控制序列」明确标成不做?

前置知识:

  • 用过终端,知道 TTY / PTY、ANSI 转义序列大概是什么
  • 了解 GUI 应用的窗口和事件循环概念
  • 可选:见过 Zig 的 comptime,或者写过 C ABI 绑定,读「深度设计剖析」会更顺

项目背景与完整档案

官方定位(README):"Fast, native, feature-rich terminal emulator pushing modern features." 再加一句更关键的:"A native GUI or embeddable library via libghostty."

基础信息:

  • 协议:MIT
  • 当前稳定版:v1.3.1(2026-03-13);v1.0.0 发布于 2024-12-26,v1.1.0 在 2025-01,v1.2.0 在 2025-09,v1.3.0 在 2026-03
  • 构建:Zig 构建系统,build.zig.zon 要求 Zig 0.17.0;macOS 侧另需 Xcode 26 和 macOS 26 SDK
  • 平台:macOS(SwiftUI 应用)与 Linux(GTK4);FreeBSD 走 GTK 那条线
  • 代码构成:Zig 13.6 MB / Swift 1.46 MB / C 1.21 MB / C++ 735 KB,另有 GLSL 与 Metal 着色器
  • 规模:603 个 .zig 文件;src/terminal/Terminal.zig 17,518 行,src/config/Config.zig 11,375 行

项目数据(截至 2026-10-22):

  • ⭐ Stars 62,487 | 🍴 Forks 3,684 | 🐛 Open Issues 254
  • 🔧 提交 18,384 | 👥 贡献者 422
  • 🗓️ 创建于 2022-03-29,最近推送 2026-10-21
  • 🏷️ 最新 tag v1.3.1;主干仍然每天在推进(GitHub Releases 页面只有一个早年的 tip 预发布,版本按 tag 走)

时间线上还有个值得注意的点:仓库 2022 年 3 月创建,v1.0.0 到 2024 年 12 月才发,中间近三年没给过正式版本号,roadmap 是拆成六步推的。现在 README 的自述是「已经稳定,每天跑在数以百万计的人和机器上」。

它解决什么问题

终端用户的痛点:

  • 速度与功能互斥:快的那几个通常没有标签页、分屏、设置 GUI;功能全的那几个渲染路径老旧
  • 「跨平台」的体验代价:一套自绘 UI 在三个系统上都像外来户,菜单、快捷键、输入法、无障碍全是模仿出来的
  • 现代协议支持不全:Kitty 图形协议、剪贴板序列、同步渲染、亮暗色通知,各家支持度参差
  • 复用不了:终端模拟器基本都长成 app,想把「解析转义序列 + 维护屏幕状态」这块拿进自己项目,只能 fork 或者退到浏览器里的模拟器

Ghostty 的解法分三层:

第一层,共享核心、分开的壳。VT 解析、屏幕状态、滚动缓冲、字体度量都在 Zig 里写一份;窗口、菜单、输入、剪贴板这些交给平台。

第二层,把壳做到像原生的。macOS 那份是真正的 SwiftUI 应用,Metal 渲染、CoreText 找字体,还接了 AppleScript 和 AppIntents;Linux 那份是 GTK4,并且和 systemd 集成,能单实例开新窗口、能做 cgroup 隔离。

第三层,把核心拆出来卖。libghostty-vt 是独立的 C 与 Zig 库,只负责解析序列和维护状态,能编到 macOS、Linux、Windows 和 WebAssembly 上。

使用场景:

  • 日常主力终端:多窗口、标签、分屏,配置只有一个文件 ~/.config/ghostty/config
  • 在别人应用里嵌终端:编辑器、IDE 插件、CI 面板,用 libghostty-vt 而不是 xterm.js
  • 看一个「原生优先」的跨平台工程怎么组织:两套壳、一份核心,边界划在哪
  • 学 Zig 的工程实践:comptime 生成 CLI 与文档、C ABI 绑定、多目标 SIMD

快速上手

macOS,官方只发布预编译包,社区维护的 cask 只是重新打包官方 .dmg:

brew install --cask ghostty

Linux 走各发行版仓库(官方不直接发 Linux 二进制):

# Arch
pacman -S ghostty
# Ubuntu 26.04 起
apt install ghostty
# Nix
nix run nixpkgs#ghostty

从源码构建,需要 Zig 0.17;macOS 上(主干分支)还要求 Xcode 26 与 macOS 26 SDK:

git clone https://github.com/ghostty-org/ghostty
cd ghostty
zig build run     # 直接跑
zig build test    # 单元测试

配置文件按 XDG 解析:XDG_CONFIG_HOME 优先,否则 ~/.config,路径是 ghostty/config。多窗口、分屏这些用默认快捷键就行,ghostty +list-keybinds 之类的 CLI 子命令可以看当前生效的绑定。

核心特性拆解

原生窗口能力——多窗口、标签页、分屏,两边都有;标签能重命名、上色。

现代终端协议——Kitty 图形协议(图能直接显示在终端里)、剪贴板序列、同步渲染、亮暗色模式通知、内联图片。README 的说法是支持度「几乎超过所有其它终端」。

兼容性的定义方式——明确写了顺序:先用标准(ECMA-48),标准没有就用 xterm,再没有就看其它流行终端。为了不拍脑袋,他们做了一次逐条对照 xterm 的审计,并沉淀成 conformance 测试。

性能——官方口径是与 Alacritty 在各种 benchmark 上差几个百分点,比 Terminal.app 和 iTerm 快约两个数量级。架构上的解释是:每个终端一个读线程、一个写线程、一个渲染线程;读线程上的解析器用了 CPU 相关的 SIMD。

macOS 原生集成——SwiftUI 应用、Metal 渲染、CoreText 字体发现、AppleScript、Apple Shortcuts(AppIntents)。

Linux 原生集成——GTK4;和 systemd 深度集成,做 always-on、单实例开新窗、cgroup 隔离。

可嵌入——libghostty-vt 已可用,覆盖 Zig 与 C,兼容 macOS / Linux / Windows / WebAssembly。GitHub 上有最小示例项目 Ghostling。

配置与主题——一个配置文件加一组 CLI flag,字段和参数是同一份定义;ghostty +show-config 能把当前生效的配置展开、+list-themes / +list-fonts / +list-keybinds 各自列清单,排查「我到底配上了没」不用猜。

深度设计剖析

设计亮点一:跨平台但不做「最低共同标准」——apprt 是编译期单态

常规方案:

跨平台 GUI 有两条常见路。要么让一个跨平台框架把界面也接管(Electron 用网页画,GTK/Qt 用自绘控件),代码写一次,但菜单、标题栏、输入法、无障碍都得靠框架去模仿;要么干脆每个平台各写一套原生实现,工作量和维护成本按平台数翻倍。Ghostty 选的是第三条:核心共享,壳各写。

Ghostty 的做法:

它给自己加了一层叫 apprt(application runtime) 的抽象,注释写得很直白:

//! "apprt" is the "application runtime" package. This abstracts the
//! application runtime and lifecycle management such as creating windows,
//! getting user input (mouse/keyboard), etc.
//!
//! ... The goal is to have different implementations share as much of
//! the core logic as possible, and to only reach out to platform-specific
//! implementation code when absolutely necessary.

关键在于这个抽象不是运行时多态,而是编译期选一次:

// src/apprt.zig
pub const runtime = switch (build_config.artifact) {
    .exe => switch (build_config.app_runtime) {
        .none => none,
        .gtk => gtk,
    },
    .lib => embedded,
    .wasm_module => browser,
};

一个构建产物里永远只有一个 app runtime 实现。更意外的是默认值:

// src/apprt/runtime.zig
pub fn default(target: std.Target) Runtime {
    return switch (target.os.tag) {
        .linux, .freebsd => .gtk,
        else => .none,
    };
}

注释解释了 .none 的含义:"On macOS, Xcode is used to build the app that links to libghostty." 也就是说,macOS 上 zig build 根本不生成那个 app——它只构建 libghostty,然后由 Xcode 那边的 SwiftUI 工程把它链接进去。build.zig 里也确实调用了 GhosttyXcodebuild、有 emit_macos_app 和 emit_xcframework 这两条路。

为什么这么做:

因为「原生」不是一个可以抽象出来的东西。macOS 菜单栏、快捷键表、输入法候选框、AppleScript 字典,都是 AppKit/SwiftUI 给的能力;GTK 那边对应的是另一套。把这两套塞进一个统一接口,最后得到的通常是两边的交集,也就是谁都不像。

工程价值与代价:

价值是用户拿到的就是平台体验。代价也很实在:

  • 两套壳:Swift 那部分 1.46 MB,GTK 那部分要另写,功能必然会有时间差
  • 平台怪癖集中管理:src/quirks.zig 的文件头写着它「集中了所有 sad 的环境适配」,灵感来自 WebKit 的 Quirks.cpp,并声明这是「最后手段」
  • 编译期单态的代价:一个二进制只有一个 runtime,换来零开销和编译期检查,但也意味着你没法在同一个构建里切两个后端做对比测试

设计亮点二:把终端核心做成零依赖的 C 库(libghostty-vt)

常规方案:

终端模拟器基本都是一个 app:VT 解析、状态机、渲染、窗口缠在一起。想复用这块能力,只能 fork,然后把别人的窗口代码一路删掉。

Ghostty 的做法:

把「解析序列 + 维护状态」抽成独立库,并且同时给三条出口:

  • C ABI:include/ghostty.h 是头文件,src/terminal/c/ 是一整层 C 适配(30 多个文件,从 key_encode 到 kitty_graphics 都在)
  • Zig 模块:src/lib_vt.zig 是 Zig 消费者的入口
  • WASM:src/main_wasm.zig 让同一套核心编成 WASM 模块

lib_vt.zig 的文件头就把边界写清楚了:

//! WARNING: The API is not guaranteed to be stable.
//! The functionality is extremely stable ... However, the API itself
//! (functions, types, etc.) may change without warning.

零依赖是硬约束,实现方式是让宿主注入能力:

/// This module provides runtime-swappable function pointers ...
/// This lets libghostty-vt have no runtime dependencies on external
/// libraries, while still allowing rich functionality that may require
/// external libraries (e.g. image decoding or regular expresssions).
pub const sys = terminal.sys;

PNG 解码、正则这些能力由嵌入方挂上去;不挂,库照样能跑,只是 Kitty 图形协议里的 PNG 就不支持。连 std.Io 都准备了一个极简实现,注释里把账算得很清楚:用 TinyIo 代替 std.Io.Threaded,在 macOS 上省约 110KB、Windows 上省约 370KB 二进制体积。

为什么这么做:

一是「终端能力应该能被任何程序嵌入」,二是拆库会强迫你把边界划干净。为了给 C 用,你不能把 Zig 的分配器、异步 IO、错误联合类型直接暴露出去;这些约束最后都变成了核心层的设计纪律。有 example/ 下的 C 与 Zig 示例,也有完整的最小项目 Ghostling。

工程价值与代价:

代价很具体。C ABI 是最难维护的那种接口,所以仓库里直接放了 types.schema.json 和一个 types-schema-verify.py,build.zig 里还有一步专门的 "Validate the libghostty-vt ABI type manifest"——用 schema 校验手写的绑定和 Zig 里的类型是否漂移。

另外 src/main_c.zig 的注释很坦诚:这套 C API 目前「不是通用 embedding API」,主要用途是让 macOS 那个 app 嵌进来,未来才可能泛化。所以现在对外推的是 libghostty-vt(只做 VT 解析和状态),而不是整个 GUI——先把最难稳定、最有复用价值的那块交出去。

设计亮点三:热路径只让一个线程碰,其它都靠邮箱排队

常规方案:

终端程序最常见的写法是一个事件循环:读 pty、解析、更新状态、渲染都在一个线程里转。实现简单,但一端会拖另一端——渲染一帧要几毫秒,这期间内核缓冲区里可能已经堆了几百 KB 要解析的输出;反过来,超高吞吐的输出也会把界面拖住。

Ghostty 的做法:

每个终端三组线程:主线程(apprt 的事件循环)、IO 线程(termio)、渲染线程;IO 线程里再分读和写。src/termio/Thread.zig 开头把取舍写明了:

//! The writer thread does handle writing bytes to the pty but also handles
//! different events such as starting synchronized output, changing some
//! modes (like linefeed), etc. The goal is to offload as much from the
//! reader thread as possible since it is the hot path in parsing VT
//! sequences and updating terminal state.

线程之间尽量不共享状态,用定长阻塞队列当邮箱:

// 渲染线程收消息
pub const Mailbox = BlockingQueue(rendererpkg.Message, 64);
// IO 线程收写相关消息
const Queue = BlockingQueue(termio.Message, 64);

容量 64 是拍出来的,注释也承认了:"hardcoded to a value that empirically has made sense for Ghostty usage but I'm open to changing it with good arguments."

还有几个刻意做的时间常量:resize 消息在 25ms 内合并(Coalesce.min_ms = 25),拖窗口时不会刷出一堆重排;如果程序起了同步输出却没结束,1000ms 后兜底重置(sync_reset_ms = 1000),否则整个终端会停在「不刷新」的状态。

为什么这么做:

终端的形状就是「一端字节流、一端像素」,两边速率天然不匹配。塞进一个线程,等于让快的那个被慢的限速。拆开之后解析可以一直在后台跑,渲染按自己的节奏取最新状态,中间那层定长队列负责吸收抖动。

工程价值与代价:

价值是输入不会因为渲染丢掉,长时间滚动输出也不会把界面卡死。代价是跨线程的状态交互都得变成消息:「立刻读一下对方现在的状态」这种需求要么绕一圈,要么专门设计快照机制;队列满之后的策略也得逐场景想清楚。

项目细节补充

配置、CLI、文档同源。 Config.zig 里的字段名直接映射 CLI flag(所以代码里到处是 @"" 语法来支持连字符),字段的 doc comment 必须写成 Pandoc 风格的 Markdown。然后 src/helpgen.zig 把 Config.zig 当 Zig 源码解析(std.zig.Ast.parse(@embedFile("config/Config.zig"))),自动生成帮助字符串,再往手册页、网站文档走。一份定义长出四样东西:配置项、命令行参数、帮助文本、文档页面。

热路径的 SIMD 是 C++ 写的。 src/simd/ 里有 vt.cpp、index_of.cpp、base64.cpp,用 Google Highway 做多目标代码生成(HWY_TARGET_INCLUDE 那套),再叠一个 simdutf。Zig 侧从 stream.zig 调 simd.vt.utf8DecodeUntilControlSeq——这是每个字节都要过的函数。仓库自带 15 个基准模块(解析器、流处理、grapheme 断字、滚动缓冲压缩……),性能在这个项目里是有量具的,不是口号。

滚动缓冲按页管,压缩和策略分开。 PageList.zig 维护一条页的双向链表;compress.zig 只提供 LZ4 编解码,文件头明确写着:哪些页该压、什么时候把常驻内存 decommit、什么时候恢复,「策略属于 PageList」。压缩页还保留原来的虚拟映射,恢复时不用重新分配。

两件为了「少踩坑」而写的小工具。 quirks.zig 除了收平台怪癖,还自己实现了一个 inlineAssert:注释说 std 的 assert 在 ReleaseFast 下并不总被优化掉,而它在热路径上会带来 15–20% 的开销。tripwire.zig 则是错误注入库——Zig 里 errdefer 写错是 bug 大户,又很难在单测里触发,所以它让你在指定点强制失败,把错误分支跑一遍。

AI 政策和准入制度。 AI_POLICY.md 要求所有 AI 使用必须披露工具和程度,人类必须能完全理解代码,不允许 AI 生成的美术素材;对「明显 AI 生成的低质贡献」有一个公开的 denouncement 名单,进了名单就永久拒绝。同时它专门写了一节 "AI is Welcome Here",说明这不是反 AI,而且维护者自己也在用。配套还有 vouch 制度:首次贡献者要先在 discussion 里申请,被 !vouch 之后 PR 才不会被自动关闭。

路线图里有一条是「不做」。 六步路线图前五步全绿,第六步「Ghostty 独有控制序列」标着 ❌,理由写得很克制:不想因为在终端生态里造只有 Ghostty 认的序列而加剧碎片化。一个 6 万 Star 的项目主动把「扩展标准」这件事标成未做,这种克制比堆功能更少见。

已知的局限(都写在官方文档里,不用猜)。

  • 两套壳必然不对齐:AppleScript、AppIntents 这类能力只有 macOS 那份有,Linux 侧没有对应物;同一件事要写两遍,两边进度自然分先后
  • Zig 版本锁定有真实代价:Zig 几乎每个版本都有破坏性变更,所以 Ghostty 得绑一个特定版本。官方安装文档记录过一次后果——openSUSE 曾把它移出官方源,理由正是发行版不允许包依赖特定 Zig 版本(文档注明 2026 年 7 月时应已回到官方源)
  • libghostty-vt 的 API 还没稳定:功能稳定,但函数和类型明确写着「可能在无警告的情况下变化」,也还没打版本号
  • 稳定版和主干有距离:最新稳定 tag 是 2026-03-13 的 v1.3.1,主干同期一直在推进;等新特性的要么装 tip 构建(AUR 的 ghostty-git、官方 Nix flake 都能钉到开发分支),要么自己编

项目资源汇总

官方资源

相关资源

媒体报道与延伸阅读

核心要点回顾

  • 跨平台的代价可以不由用户体验来付:共用一份 Zig 核心,壳用 SwiftUI 和 GTK4 各写一份
  • apprt 是编译期单态而不是运行时多态——Linux 默认 GTK,macOS 默认 .none,那个 app 由 Xcode 链接 libghostty 构建
  • 终端核心被拆成零依赖的 C/Zig 库:能力靠宿主注入,C ABI 用手写绑定加 schema 校验兜住漂移
  • 性能不是形容词:SIMD 内核在 src/simd/*.cpp,15 个基准模块常驻仓库

适合谁

  • 每天在终端里待很久、想换一个既快又有窗口管理的人
  • 需要在自家应用里嵌一块真终端的开发者(libghostty-vt)
  • 想研究「同一个产品怎么做出两套原生实现」的跨平台工程师
  • 学 Zig、想看一个大型项目的 comptime 与 C ABI 实践的人

一句话评价

它证明「原生」不是一种风格,而是愿意为每个平台各付一次代价。

Next project →Pi