Ghostty 源码拆解

PTY 与 Termio:字节流的并发边界

shell、PTY、xev、IO thread 和 renderer mailbox 如何协作

PTY 与 Termio:字节流的并发边界

终端模拟器最容易被低估的部分是 IO:shell 既可能持续吐出大量字节,也可能要求 emulator 立即回答 device status、window size 或 clipboard 查询。Ghostty 把这件事集中在 src/termio/,并把“读取热路径”和“写入/控制消息”分开。

Termio 负责什么

termio.Termio 持有:

  • 一个 termio.Backend,可连接 PTY、手动输入或嵌入环境;
  • 一个 renderer-agnostic 的 terminal.Terminal
  • StreamHandler.Stream,把字节流转成 parser action 和副作用;
  • renderer/surface mailbox 和 wakeup handle;
  • size、DerivedConfig、初始输入和线程入口状态。

它是“终端模型和 OS IO 之间的适配器”,但不是平台窗口层。PTY 创建、子进程启动和前台进程信息由 pty.zigtermio/Exec.zig 等协同完成。

一次 shell 输出

child writes bytes
  → PTY master readable
  → Termio read callback
  → StreamHandler.Stream.feed
  → UTF8Decoder + Parser.next
  → TerminalStream dispatch
  → Terminal state mutation
  → renderer wakeup

parser 不直接操作屏幕,它只产生 print, execute, csi_dispatch, esc_dispatch, osc_dispatch, dcs_*, apc_* 等 action。stream_terminal.zig 再把这些 action 绑定到 Terminal 的操作和外部事件。

这个分层使 parser 能独立测试,也让 libghostty-vt 不必依赖 PTY 或 GPU。

为什么单独有 writer thread

termio/Thread.zig 的注释给出答案:reader 是解析 VT 的热路径,writer thread 负责 PTY 写入、synchronized output、linefeed、resize coalescing、selection scroll 等控制操作。把这些操作移走,可以避免读路径被锁、系统调用或较慢的事件处理拖住。

线程本身用 xev loop,带有:

  • Async stop:下一轮停止;
  • Async wakeup:从其他线程唤醒;
  • resize timer:25ms coalesce;
  • selection scroll timer:15ms tick;
  • sync reset timer:防止坏程序让 synchronized output 永久冻结;
  • SPSC mailbox:接收来自 surface/renderer 的消息。

mailbox 是协议,不是随手的队列

termio/message.zigtermio/mailbox.zig 把跨线程信息建模成带所有权的消息。写入请求可能带有分配的 byte buffer,resize、focus、mouse、paste、action 等则是不同的 union 分支。生产者必须清楚消息何时被消费,消费者必须在异常退出时 drain 或释放。

IO thread 发生异常时,源码不会简单退出:它先把可见错误写到 Terminal,隐藏 cursor,然后在 loop 未停止时进入 drain 模式,继续排空 mailbox,保证其他线程不会永远阻塞在一个已失效的通道上。

PTY 与子进程

termio/Exec.zig 把命令、环境、工作目录、shell integration 和初始 input 组合成 subprocess。Termio.ThreadEnterState 使用 arena 保存启动前的重复输入;一旦进入 IO thread,状态就可以整体销毁。这个设计避免把一批零散的临时字符串分散到多个生命周期里。

PTY 的关键约束是 size 必须在 child 启动前或启动早期可用,终端请求 window size 时还要通过 renderer/surface 回写 pixel/cell 尺寸。Resize 事件因此被 coalesce,而不是每一次鼠标拖动都立即触发完整终端重排。

初始输入与错误路径

配置中的 input 可以是 raw string 或 file path。prepareInput 会复制 raw 数据或打开文件;文件不存在、不可读、过大等错误都会在 child 进入可用状态前被发现。因为初始化输入的语义是“必须送到终端”,Ghostty 宁可把 terminal 标成不可用,也不假装启动成功后悄悄丢掉输入。

设计取舍

  • 读路径只做必要的解析和状态更新,副作用通过 mailbox 发送;
  • 写入、resize、selection scroll 和 sync reset 由独立 loop/timer 驱动;
  • 线程异常仍然产生可见终端错误,并继续 drain;
  • backend 抽象允许 libghostty-vt 或测试使用没有真实 PTY 的字节源。

On this page