Ghostty 源码拆解

启动与运行时:从 main 到第一张 Surface

追踪进程初始化、CLI action、App、runtime 和线程启动顺序

启动与运行时:从 main 到第一张 Surface

Ghostty 的启动不是“main 里创建窗口”这么简单。src/main_ghostty.zig 同时服务于可执行程序和 lib 构建:它先初始化全局状态、判断有没有 +action,再根据编译时 runtime 决定是跑 GUI 还是只提供 helper CLI。

启动主线

main(minimal)
  → global.init(.main)
  → global.action()?
       ├─ yes: action.run(alloc) → exit
       └─ no
           → build_config.app_runtime == none?
                ├─ yes: print helper CLI usage → exit
                └─ no
                    → App.create
                    → apprt.App.init(app)
                    → startQuitTimer (可用时)
                    → apprt.App.run
                    → App.tick / mailbox / surface lifecycle

global.init 必须先于几乎所有核心对象,因为它提供 allocator、xev、临时目录、日志和崩溃状态。源码注释明确要求:C API 可以访问 global,但其他 Zig 代码不应该绕过对象边界直接依赖 global state。

CLI action 是一条旁路

ghostty +version+list-fonts+validate-config 为例,CLI action 在 GUI 创建前执行。这样做有三个效果:

  1. 不需要初始化窗口、字体、GPU 和 PTY;
  2. action 可以被 macOS Swift 宿主通过 ghostty_cli_try_action 触发;
  3. 多 action 和非法 action 能在统一入口失败,而不是让运行时半初始化。

src/cli/ghostty.zig 负责 action 注册和分发,具体实现位于 src/cli/ 下的独立文件。要新增 action,通常需要同时考虑解析、帮助、退出码、是否需要 GUI 和 C API 宿主的调用方式。

App 初始化不是第一张窗口

App.init 只建立 app 级状态和 SharedGridSet。它不会默认创建 terminal surface,因为平台 runtime 可能需要先决定窗口、tab、split 或恢复会话。App.addSurface 负责注册 surface;当 surface 数从 0 变为非 0 时,它会通知 runtime 取消 quit timer。

相反,deleteSurface 在最后一张 surface 消失时重新启动 quit timer。这个小机制解释了 Ghostty 为什么可以允许“暂时没有窗口但 app 仍在清理/等待”的状态,而不把窗口关闭粗暴等价成进程退出。

surface 生命周期

runtime requests new surface
  → Surface.init
      → derive config / size
      → create terminal
      → create renderer + renderer state
      → init Termio + backend
      → start IO thread
      → start renderer thread
  → App.addSurface
  → runtime presents window/view

close request
  → Surface request close / action
  → stop renderer + IO + search threads
  → join threads
  → App.deleteSurface
  → runtime frees native surface

这里的关键不是某个函数名,而是join 早于 deinit/freerenderer.Thread.deinittermio.Thread.deinit 的注释都要求调用者先等待线程结束;否则 mailbox、xev handle、terminal 指针和 GPU 状态都可能被并发访问。

none runtime 的意义

src/apprt/runtime.zig 把默认 runtime 作为编译时选择:Linux/FreeBSD 默认 GTK,其他目标默认 nonenone 并非“没有功能”,而是表示当前产物只构建共享库或 helper CLI;macOS 应用由 Xcode 链接 libghostty,再由 Swift 负责原生 UI。

这种选择把“核心能否编译”和“某平台 GUI 是否完整”分开。它也让 zig build -Demit-lib-vt-Demit-lib 等库目标不必携带完整窗口系统依赖。

初始化失败如何对用户可见

主入口在 global 初始化失败时会把错误翻译成明确的 stderr 文案;IO thread 启动失败时则会把错误文本写入 Terminal 网格、隐藏 cursor、让用户在窗口里看到“终端不可用”的原因,而不是只留下一张空白窗口。这个错误路径在 src/termio/Thread.zigthreadMain 中实现,是一个很有价值的产品化细节:底层故障仍然通过可见的终端状态交付。

On this page