Attyx:小巧且快速的 GPU 加速终端模拟器
Source: Dev.to
Neovim、tmux、git、SSH——这就是我整天的工作。我用过市面上所有的终端模拟器:iTerm2、Alacritty、Kitty、Ghostty。都很棒。
但我从未真正弄清楚内部到底发生了什么。字节从 shell 输出,转义序列被解析,字符显示在屏幕上。ESC[38;2;255;100;0m 到底对内部状态做了什么?一次按键是如何通过伪终端传递并最终以文本形式返回的?我毫无头绪。
我唯一知道的学习方式就是 自己动手实现。
于是我用 Zig 从零开始构建了 Attyx ——一个 GPU 加速的终端模拟器。周六动手,五天后我已经在日常使用它了,而且至今仍在日常使用。
为什么会这样
“我们不需要另一个终端模拟器。”
当然。但我并不是因为世界需要它才去构建它的。我出于两个自私的理由才创建它。
1. 我想学习 Zig
不是通过文档和教程——而是通过一个真正的项目,让我在系统编程问题上“当头棒喝”。终端模拟器涉及方方面面:解析、GPU 渲染、字体光栅化、Unicode、PTY 管理、平台 API。完美契合。
2. 我需要一个可测试的终端核心
我编写 TUI 应用。我做了 Glyph ——一个面向终端的 React 渲染器。Flexbox、组件、hooks,完整的生态,只是渲染到终端而不是浏览器。在此之上我又做了 Aion(日历 TUI)和 Epist(带 vim 键绑定的邮件客户端)。这些是真正每天都在使用的应用。
问题: 如何测试一个 TUI 应用实际的 外观?你可以测试组件状态、可以测试逻辑,但最终的输出——终端在解释你的转义序列后得到的字符网格和样式——却是个黑洞。截图对比?脆弱。Asciinema 录制?无法自动化。单元测试原始转义码?捕捉不到解释层的错误。
我想要的东西非常简单——把字节喂给终端引擎,得到一个确定性的网格,然后对其进行断言。没有任何终端应用能够满足我的需求。所以我从头实现了一个核心完全纯粹的终端。
var engine = try Engine.init(allocator, 24, 80);
engine.feed(my_app_output);
const cell = engine.state.grid.getCell(0, 0);
try expectEqual('A', cell.char);
try expect(cell.style.bold);
进入全屏模式
退出全屏模式
没有 GPU。没有窗口。没有 PTY。只有字节输入,状态输出。这就是我多年来一直想要的测试原语。
您将获得的功能
- GPU 加速渲染 — macOS 上使用 Metal,Linux 上使用 OpenGL 3.3。
- 完整的 VT100/xterm 兼容性。真彩色、256 色,功能齐全。
- 鼠标跟踪、Kitty 图形协议支持内联图像、超链接、备用屏幕缓冲区、滚动区域、光标形状。
- 20 000 行滚动缓冲区,并在窗口大小改变时自动重新换行。
- 可在会话上方浮动的弹出终端。
- 搜索功能。
- 使用 TOML 配置文件并支持热重载。
全部体积 5 MB 以下。
整个项目——GPU 渲染、VT 解析器、字体处理、平台代码——都凝聚在一个小巧的二进制文件中。这正是使用 Zig 编写、尽量少依赖、采用原生平台框架而非捆绑 GUI 工具包,并让编译器剔除死代码的结果。没有运行时,没有垃圾回收,没有 Electron。只有一个 Zig 二进制文件直接与操作系统交互。
深入内部(不让你无聊至死)
我在 Semos 博客上写了一篇深度技术解析,如果想了解所有细节可以去看看。但这里是要点。
所有操作都通过一个管道流动:
Raw bytes → Parser → Actions → State → Grid
进入全屏模式
退出全屏模式
-
Parser – 增量状态机。一次处理一个字节,输出诸如 “打印 H”、 “将光标移动到第 3 行第 5 列”、 “将前景色设为红色” 等动作。使用固定大小缓冲区,零堆分配,能够跨
read()边界处理不完整的序列。整个解析器结构体大小仅占栈空间。 -
Grid – 扁平的单元格数组。每个单元格存储一个 Unicode 码点、两个组合记号槽(用于变音符号)、样式以及一个超链接 ID。初始化时只进行一次分配。滚动区域通过在连续内存上
memcpy实现。没有链表,没有间接引用。 -
Damage tracking – 保持渲染高速。256 位脏位集合(四个
u64)。状态机在触及某行时翻转对应位。渲染器只重绘脏行。大多数帧只需要重绘一两行。 -
Two threads, no locks – PTY 线程读取字节并填充共享单元格缓冲区。主线程在垂直同步时渲染。使用 seqlock(仅一个原子代数计数器)防止撕裂读取。如果渲染器在 PTY 写入过程中捕获到数据,它会跳过该帧。你永远不会注意到丢帧,只会注意到互斥锁竞争。
-
GPU rendering – 将每个字符转换为纹理化的四边形。字形按需光栅化到存放于 GPU 内存的图集。绘制 10 000 个单元格的开销几乎与绘制 100 个相同——这正是把工作交给 GPU 的意义所在。CPU 负责解析和状态管理;GPU 负责像素绘制。各司其职,发挥所长。
Semos 堆栈
- Glyph – React 终端渲染器。使用 JSX、flexbox、hooks 编写终端应用——与网页上相同的开发体验,但渲染到终端。
- Aion 和 Epist – 基于 Glyph 构建的应用(日历和邮件)。
- Attyx – 完成闭环。Glyph 应用需要一个可测试的终端。Attyx 需要真实世界的应用来进行压力测试。它们相互推动前进。
试一试
brew install semos-labs/tap/attyx
进入全屏模式
退出全屏模式
或从源码构建:
git clone https://github.com/semos-labs/attyx
cd attyx
zig build -Doptimize=ReleaseFast
进入全屏模式
退出全屏模式
或者如果你使用的是 Mac,可以从官网下载安装。这样还能获得自动更新。
配置文件位于 ~/.config/attyx/attyx.toml —— 包括字体、颜色、快捷键、透明度、模糊等。修改后,按 Ctrl+Shift+R 即可生效,无需重启。
开源,采用 MIT 许可证。
它像 Ghostty 或 Kitty 那样成熟吗?还没有。不过我了解其中的每一行代码,天天使用,而且整体体积不足 5 MB。这也算是个优势。