终端渲染 是做什么的?
一个终端屏幕是一个 cols × rows 的单元格网格。正确性源于 VT;
“好看” 则来源于度量和绘制。相同的模拟器 ≠ 相同的外观。
四个层次
| 层次 | 职责 | 如果出错 |
|---|---|---|
| 1. VT | 字节 → 单元格网格(光标、模式、回滚、alt 屏幕) | 乱码 / 尺寸错误 / 光标卡死 |
| 2. 度量 | 根据字体确定单元格宽度/高度;根据行列 和 单元格像素调整 PTY 大小 | 列抖动、边缘模糊 |
| 3. 绘制 | 绘制单元格:背景区域、字形、精灵图、对比度 | 间隙、文字变细、TUI 边框断裂 |
| 4. 会话(可选) | 持久化/远程:守护进程拥有 VT,客户端镜像帧 | 窗口关闭时回滚丢失 |
一句话总结: VT 保证正确性;字体 + 度量 + 逐单元格放置 + 几何框线绘制保证美观;脏行缓存保证速度。
1. VT(网格必须正确)
- 对于 Agent TUI,优先使用真实的模拟器(例如 libghostty-vt),而不是手写的 ANSI 子集。
- 每个单元格:文本(字素簇)、前景色/背景色、粗体/斜体/细体、下划线/删除线、宽字符标志。
- 处理:宽字符/emoji 宽度、alt 屏幕、同步输出(模式 2026)、在下次 PTY 读取前写回查询响应。
- UI 读取网格的 快照;不要在绘制路径中重新解析字节流。
2. 度量
- 列宽 ≈ 字符
'M'的步进宽度(可选CellWidth乘数)。 - 行高 ≈ 上升量 + 下降量,然后乘以约 1.15–1.2;对齐到设备像素。
- 保持宽度为字体步进宽度;将 框/背景边缘 对齐到像素,使相邻单元格无缝连接(不要以导致字形抖动的方式舍入单元格宽度)。
- 调整大小时,向 PTY 发送
cols、rows、cell_width_px、cell_height_px(程序使用像素尺寸进行鼠标/图像操作)。
3. 绘制(美观成败的关键)
改进现有终端时的优先级顺序:
- 提供一套固定的等宽 + Nerd 符号字体(例如 JetBrains Mono + Symbols Nerd Font)。系统默认等宽字体/缺少 Powerline 图标会破坏 Agent TUI。
- 将框/块/盲文/Powerline 作为几何图形绘制,而不是使用字体字形 —— 字体在行之间留有间隙;全屏边框必须无缝连接。
- 在网格上放置字形:使用强制宽度塑形的相同样式区域,或逐单元格居中。缩小溢出的后备字形(emoji),使其适合狭窄的单元格。
- 将缺失的 PUA / Nerd 图标路由到符号字体(或将它们按轮廓适配到单元格中)。
- 可选优化:
minimum-contrast(跳过精灵图/有意图形)、macOS 字体加粗、多样式下划线、填充颜色扩展。
绘制管线(每帧)
- 用终端背景色填充面板。
- 原点 = 内边距 + 垂直剩余空间/2;将原点对齐到设备像素。
- 仅当行的
Arc/id 发生变化(或字体/单元格大小改变/整屏脏)时,重建行操作。 - 先绘制所有 背景范围,然后是 精灵图,接着是 文本运行,最后是光标 / IME / 覆盖层。
行操作
- 合并连续具有相同背景色的单元格为一个四边形。
- 一个精灵图单元格 → 几何绘制(参见 herdr / Ghostty 中精灵图的方法)。
- 否则,创建一个相同样式的文本运行;对于窄单元格,强制步进宽度为
cell_w,以使列保持锁定。
不要归咎于 VT 的情况
如果边框有间隙、图标显示为空格(tofu)或文本看起来变细/不均匀,但网格内容正确 —— 修复 绘制/字体,不要更换模拟器。
4. 会话拓扑
进程内(简单): PTY → VT → 快照 → UI 绘制。
守护进程 + 客户端(kero 风格): 守护进程无头 VT + PTY;Mirror 编码当前屏幕的 VT 帧;客户端使用 Conn 的 VT 逐单元格绘制;回滚在窗口关闭 / 远程连接后依然存在。
参考实现
| 项目 | 技术栈 | 备注 |
|---|---|---|
| Ghostty | Zig + libghostty-vt | VT + 精灵图语义的来源 |
| herdr-app | GPUI + libghostty-vt | 单元格绘制、精灵图、对比度、打包字体 |
| kero | MyGo + libghostty-vt | 镜像帧、Ghostty 主题、GPU UI |
| kumu | GPUI-kit + libghostty-vt | 相同的绘制思路:精灵图 + JetBrains + Nerd |
检查清单
添加或修复终端渲染时:
- VT 拥有网格;UI 仅绘制快照
- 从等宽字体计算单元格度量;调整大小时包含像素单元格尺寸
- 打包(或保证)等宽 + Nerd 符号字体
- 框 / Powerline / 盲文绘制为形状
- 脏行绘制缓存以行标识为键
- 光标、IME 和选择区域位于单元格坐标上
- 对照 Ghostty 比较已知的 TUI(htop、Claude Code、状态行)
附加细节
- 精灵图 / 几何范围及对比度规则:paint.md