配置与输入:文本配置如何变成 Action
拆解配置加载、条件配置、keybind 解析和跨平台输入边界
配置与输入:文本配置如何变成 Action
Ghostty 的配置系统并不是启动时读一次 key-value 就结束。它要处理默认文件、用户文件、CLI 覆盖、条件配置、主题、keybind、动态更新、C API 和 WASM。输入系统也不是“收到字符就写 PTY”,而是先判断键盘/鼠标是否被 Ghostty 自己的 binding 或终端模式消费。
配置的四个阶段
可以把 src/config/ 看成一条管道:
Config.init
→ load default files
→ load explicit / recursive files
→ load CLI args
→ apply conditional state
→ finalize / validate / diagnostics
→ derive per-surface runtime configsrc/config/Config.zig 聚合字段和解析器;file_load.zig、io.zig、conditional.zig、formatter.zig、path.zig 等把 IO、条件表达式、格式化和路径语义分开。src/config/CApi.zig 再把同一套对象变成 ghostty_config_* API,避免 Swift 或其他宿主各写一套配置解释器。
为什么有 DerivedConfig
Termio、Surface、renderer 不长期持有一个共享 Config*,而是把自己需要的字段复制/派生到 arena 管理的 DerivedConfig。这样做牺牲少量复制,换取两个不变量:
- 配置热更新时,旧 surface 不会突然读取到已经释放或半更新的字符串;
- 每个线程拥有稳定的、只读倾向的配置快照,减少锁和生命周期耦合。
例如 Termio.DerivedConfig.init 会计算 256 色 palette、复制 enquiry-response、保存 clipboard 权限、cursor、foreground/background 和 conditional state。它是“配置文本”到“终端运行时语义”的转换点。
条件配置与热更新
配置可以受系统 light/dark、焦点或其他环境状态影响。App.updateConfig 先把配置消息发给已有 surface,再尝试用 config_conditional_state 应用 app 级条件状态,最后通知 runtime。Surface 自己维护 surface 级 conditional state。
这不是单纯的全量替换:源码明确区分 app-level 和 surface-level config,并允许应用失败时保留旧 config 继续运行。正确的阅读方式是把配置更新看作一个可失败的事务边界:输入是一份新配置,输出是各 surface 的可用派生快照和 runtime action。
keybind 解析
src/input/Binding.zig 把人类可读的 binding 语法解析为结构化 trigger/action:修饰键、物理/逻辑 key、按下/释放/repeat、参数化 action、key table 都会在这里归一化。src/input/key_mods.zig 专门处理 modifier 组合和平台侧差异。
action 的参数不直接塞进字符串执行,而是先解析成类型安全的 union/enum,再在 Surface 或 App 上执行。这样可以让 split, resize_split, write_screen_file, goto_tab 等动作拥有不同参数类型,并能在配置加载时尽早报错。
输入决策树
native key event
→ Surface.key
→ focus / readonly / app focus checks
→ binding match?
├─ yes: perform action; mark consumed
└─ no
→ terminal keyboard mode encoding
→ enqueue write request
→ pty鼠标路径类似,但要额外判断 terminal 的 mouse reporting mode、selection gesture、链接 hover、scrollback 和 clipboard。InputEffect 明确区分 ignored、consumed、closed,尤其 closed 之后 surface 指针可能已不安全,调用方必须立即退出当前事件处理。
设计取舍
- 解析时失败:配置和 binding 尽量在 load/finalize 阶段产生 diagnostics,避免运行到热路径才发现拼写错误。
- 输入在 core 判断:平台只提供原始事件和 callback,是否消费由 core 统一决定,防止 macOS/GTK 的快捷键行为漂移。
- 写入异步化:事件处理不直接阻塞写 PTY,转为 mailbox 请求交给 writer thread。
- 只读不是禁用所有操作:
Surface.readonly只阻止 PTY 输入,选择、滚动、复制等 terminal-level 操作仍然可用。