启动与运行时:从 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 lifecycleglobal.init 必须先于几乎所有核心对象,因为它提供 allocator、xev、临时目录、日志和崩溃状态。源码注释明确要求:C API 可以访问 global,但其他 Zig 代码不应该绕过对象边界直接依赖 global state。
CLI action 是一条旁路
以 ghostty +version、+list-fonts、+validate-config 为例,CLI action 在 GUI 创建前执行。这样做有三个效果:
- 不需要初始化窗口、字体、GPU 和 PTY;
- action 可以被 macOS Swift 宿主通过
ghostty_cli_try_action触发; - 多 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/free。renderer.Thread.deinit 和 termio.Thread.deinit 的注释都要求调用者先等待线程结束;否则 mailbox、xev handle、terminal 指针和 GPU 状态都可能被并发访问。
none runtime 的意义
src/apprt/runtime.zig 把默认 runtime 作为编译时选择:Linux/FreeBSD 默认 GTK,其他目标默认 none。none 并非“没有功能”,而是表示当前产物只构建共享库或 helper CLI;macOS 应用由 Xcode 链接 libghostty,再由 Swift 负责原生 UI。
这种选择把“核心能否编译”和“某平台 GUI 是否完整”分开。它也让 zig build -Demit-lib-vt、-Demit-lib 等库目标不必携带完整窗口系统依赖。
初始化失败如何对用户可见
主入口在 global 初始化失败时会把错误翻译成明确的 stderr 文案;IO thread 启动失败时则会把错误文本写入 Terminal 网格、隐藏 cursor、让用户在窗口里看到“终端不可用”的原因,而不是只留下一张空白窗口。这个错误路径在 src/termio/Thread.zig 的 threadMain 中实现,是一个很有价值的产品化细节:底层故障仍然通过可见的终端状态交付。