Attyx:小巧且快速的 GPU 加速终端模拟器

发布: (2026年3月1日 GMT+8 03:37)
9 分钟阅读
原文: Dev.to

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 编写终端应用——与网页上相同的开发体验,但渲染到终端。
  • AionEpist – 基于 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。这也算是个优势。

0 浏览
Back to Blog

相关文章

阅读更多 »

不糟糕的语义失效

缓存问题 如果你在 Web 应用上工作了一段时间,你就会了解缓存的情况。你加入缓存,一切都变快了,然后有人……