Glyph Protocol for Terminals 介绍
摘要
# Glyph Protocol for Terminals 介绍 - Raphael Amorim 来源: [https://rapha.land/introducing-glyph-protocol-for-terminals/](https://rapha.land/introducing-glyph-protocol-for-terminals/) 终端有一个一直让我感到困扰的问题:为了让你喜爱的编辑器、提示符或 TUI 能够正常渲染,你几乎总是被迫安装一个修补过的字体。你知道这个过程。你打开一个新终端,启动你的编辑器,结果 UI 的一半被小方块取代了——th
<p><a href="https://lobste.rs/s/uevqfc/introducing_glyph_protocol_for">评论</a></p>
查看缓存全文
缓存时间:
2026/04/20 14:51
# 为终端引入字形协议 - Raphael Amorim 来源: https://rapha.land/introducing-glyph-protocol-for-terminals/
一直以来,终端有一个让我感到困扰的地方:为了让你最喜欢的编辑器、提示符或 TUI 渲染得漂亮,你几乎总是被迫安装打补丁的字体。你知道这个流程。你打开一个新的终端,启动你的编辑器,一半的 UI 被小矩形替换掉了——臭名昭著的[豆腐字](https://fonts.google.com/knowledge/glossary/tofu)。解决方法是下载一个 Nerd Font 或 Powerline 或其他打补丁的字体集合,然后切换你的终端字体。一个通常超过 10MB 的字体[^1]。所有这些——仅仅是为了渲染一个图标,或者几个图标。
一个终端提示符中的多个代码点显示为空矩形。这是豆腐字的例子——系统字体没有字形的代码点。
这很糟糕。包体积巨大,工作流复杂,应用作者无法传送他们真正想要的字形。他们只能希望用户安装了正确的字体、正确的版本、映射了正确的代码点。
所以我决定做点什么。
## 字形协议
字形协议是一个终端协议,让应用程序可以做两件事:
1. **在运行时直接向终端注册自定义字形**。应用程序选择 Unicode 私有使用区的一个代码点(这正是 Nerd Fonts、Powerline 和其他所有图标约定已经使用的地方),提供矢量轮廓,并在想要渲染字形时发出该代码点。
2. **查询终端**询问给定代码点是否由系统字体覆盖、由本会话中的注册覆盖、两者都覆盖,或两者都不覆盖。
与其要求每个用户安装打补丁的字体让你的 TUI 看起来正确,你的应用程序可以直接提供字形,并复用一个 Nerd Font 代码点——或任何你喜欢的 PUA 代码点——来渲染它。如果用户已经安装了 Nerd Fonts,查询让你完全跳过提供自己的字形;如果没有,你提供轮廓,图标仍然会显示出来。
应用程序通过字形协议注册字形后,相同的终端提示符正确渲染图标。通过字形协议加载图标后的终端——无需安装字体。
## 为什么这很重要
字体是一个伪装成渲染问题的分发问题。Nerd Font 模型可行,但有代价:用户携带他们永远看不到的数百万字形字节,应用作者被锁定到私有使用区中的固定代码点集合,任何不在字体中的图标都无法渲染。如果你想提供新的图标,你需要整个生态系统更新。
字形协议反转了这一点。应用程序提供字形。终端渲染它。用户什么都不安装。这也意味着 TUI 可以坦诚地说明他们需要什么。现在,使用 Nerd Font 图标表示"git branch"的编辑器无法知道用户是否真的安装了 Nerd Font——它只是绘制代码点并希望它能工作。通过查询,应用程序可以先问后再优雅地回退。
## 协议的形状
**传输。**该协议使用 [APC](https://en.wikipedia.org/wiki/C0_and_C1_control_codes#C1_controls)(应用程序命令)而不是 OSC。APC 就是为这种情况设计的:应用程序定义的命令,不实现该协议的终端可以安全地忽略,无需争夺 OSC 的共享数字命名空间[^2]。
**标识符。**每条字形协议消息以代码点 `25a1`(U+25A1,[白色方块](https://www.unicode.org/charts/PDF/U25A0.pdf))为前缀——当终端没有字形时绘制的字符,豆腐字的规范符号。不识别这个前缀的终端会丢弃该消息。框架看起来像:
```
ESC _ 25a1 ; [ ; key=value ]* [ ; ] ESC \
```
四个动词开始:`s` 表示支持,`q` 表示查询,`r` 表示注册,`c` 表示清除。
### 支持:终端实现什么?
在注册任何东西之前,应用程序需要知道终端支持什么——它可以栅格化哪些有效负载格式,它说什么协议版本。这也是完全检测字形协议的规范方法:该动词不带参数,仅需一次往返。
客户端发送:
```
ESC _ 25a1 ; s ; fmt=1 ESC \
```
终端回复:
`fmt` 是一个位字段,其中每一位标记一个受支持的有效负载格式。该集合随时间增长;客户端将未知位视为保留位并忽略它们。
| 值 | 格式 | 含义 |
|---|---|---|
| `1` | `glyf` | TrueType 简单字形。v1 中必需。 |
| `2` | `colrv0` | 分层平面色字形(OpenType COLR v0)。在 v1.2 中添加——参见[颜色后续文章](https://rapha.land/adding-color-glyphs-to-glyph-protocol/)。 |
| `4` | `colrv1` | 包含渐变和变换的完整绘制图(OpenType COLR v1)。在 v1.2 中添加。 |
| | | 进一步的位保留用于未来格式。 |
任何回复都确认终端实现了字形协议;如果在短超时内没有返回任何东西,它不会。`fmt=0` 的回复意味着终端说了协议但不宣传任何格式——为完整性定义,实际中不应该出现。需要 `glyf`(v1 唯一定义的有效负载)的客户端在发送任何 `r` 请求之前检查位 0 是否已设置。
### 查询:谁能渲染这个代码点?
应用程序想知道当前字体——无论是系统安装的字体还是本会话中的注册——是否可以渲染 `U+E0A0`(Powerline 分支图标)。
客户端发送:
```
ESC _ 25a1 ; q ; cp=E0A0 ESC \
```
终端回复:
```
ESC _ 25a1 ; q ; cp=E0A0 ; status=1 ESC \
```
`status` 是一个十进制 `u8`,编码两位字段——位 0 表示"系统字体覆盖它",位 1 表示"词汇表注册覆盖它":
| 值 | 状态 | 含义 |
|---|---|---|
| `0` | `free` | 没有任何东西渲染这个代码点。该单元格将显示豆腐字。 |
| `1` | `system` | 系统字体覆盖它。 |
| `2` | `glossary` | 本会话中的词汇表注册覆盖它。 |
| `3` | `both` | 两者都覆盖它;注册在渲染时遮蔽系统字体。 |
有了这个,TUI 可以先问后再优雅地回退——当系统已经有分支图标时跳过注册自己的,当系统没有时注册并发出自定义代码点。协议检测本身由上面的 `s` 动词处理。
### 注册:提供你自己的字形
应用程序想提供自己的分支图标。它选择一个 PUA 代码点——这里是 `U+E0A0`,Powerline 约定——并发送 `glyf` 轮廓([字体已使用四十年的相同 TrueType 矢量格式](https://learn.microsoft.com/en-us/typography/opentype/spec/glyf))base64 编码:
```
ESC _ 25a1 ; r ; cp=E0A0 ; upm=1000 ; ESC \
```
参数:
- `cp` — 十六进制目标代码点。**必须在三个 Unicode 私有使用区范围之一内**(`U+E000`–`U+F8FF`、`U+F0000`–`U+FFFFD` 或 `U+100000`–`U+10FFFD`)。任何其他内容都会被拒绝,返回 `reason=out_of_namespace`。参见下面的"为什么终端限制在 PUA"。
- `fmt` — 有效负载格式。可选;`glyf` 是 v1 中唯一定义的值,也是默认值,所以大多数注册可以完全省略它。
- `upm` — 每个 em 的单位,轮廓所在的坐标空间。可选;默认 `1000`。
- 有效负载 — base64 编码的 `glyf` 简单字形记录。
终端确认:
```
ESC _ 25a1 ; r ; cp=E0A0 ; status=0 ESC \
```
从这一点开始,每当应用程序发出 `U+E0A0` 时,其注册的字形在该单元格处渲染。在同一 `cp` 上的第二个 `r` 会覆盖第一个。发生错误时(非 PUA 代码点、格式错误的有效负载、复合字形等),回复会带有 `status=; reason=`。
为什么是矢量?因为字形不是照片。它没有固定大小:同一个图标需要在密集 TUI 中以 12px 渲染,在 HiDPI 显示屏上以 24px 渲染,任何烘焙分辨率的东西都在至少其中一个上做错了决定。128px 的光栅字形在你的笔记本电脑上看起来清晰,在外接显示器上会变得模糊,在 9px 的状态栏中将无法辨认。
为什么特别是 `glyf`?因为每个已经渲染文本的终端都已经链接了 `glyf` 栅格化程序。[FreeType](https://freetype.org/)、[swash](https://github.com/dfrg/swash)、[ttf-parser](https://github.com/RazrFalcon/ttf-parser)、[fontdue](https://github.com/mooman219/fontdue)、[allsorts](https://github.com/yeslogic/allsorts)——渲染程序已经在终端编写的每种语言中了。采用字形协议不会在终端方添加零个新的依赖项。相比之下,采用 SVG 会意味着拉入 `resvg` 或编写新的 XML+路径解析器。`glyf` 在线路上也很小。一个典型的图标大约 150–400 字节的 `glyf` 数据——相当于 SVG,base64 开销包括在内,小 2–3 倍。对于在启动时注册五十个图标的应用程序,这是 13KB 和 35KB APC 流量突发之间的区别。在饱和的 tmux 管道或移动 SSH 链接上,你会感受到这一点。
**`glyf` 的快速入门。**如果你从未打开过 TrueType 规范,这是三十秒版本。`glyf` 记录将字形存储为一组闭合轮廓。每个轮廓是一系列点,每个点都有一个元数据位:*曲线上*或*曲线外*。遍历轮廓的规则很简单:
- 两个连续的曲线上点 → 它们之间的直线。
- 一个曲线外点位于两个曲线上点之间 → 二次贝塞尔曲线,曲线外点作为控制点。
- 两个连续的曲线外点 → 在它们的中点有*隐含*的曲线上点。
这是一个压缩技巧:曲线外点链使用大约显式形式的一半顶点来编码平滑曲线。坐标是 EM 方块中的整数网格位置。在 `upm=1000` 时,`(500, 900)` 处的点位于半宽、九十%处向上。线路格式紧密打包点:每个点一个标志字节(带有压缩相同标志运行的重复位),后跟 delta 编码的 x 和 y 坐标,当它们适合有符号字节时存储为短(1 字节),当不适合时存储为长(2 字节)。一个闭合三角形约三十字节。一个三十点图标约两百字节。这就是整个格式。权威参考是 [OpenType `glyf` 规范(Microsoft)](https://learn.microsoft.com/en-us/typography/opentype/spec/glyf)和 [Apple TrueType 参考手册,第 6 章](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6glyf.html)——两者都可在下午阅读,两者在现有库中被正确实现了一百多次。
**子集。**字形协议不要求终端实现完整的 `glyf` 表。规范定义了一个受限的子集:
- **仅简单字形。**没有复合字形,没有对其他字形的引用,没有字体级上下文。
- **标准标志编码**如 OpenType 规范所定义(曲线上、曲线外、x 短、y 短、重复)。
- **没有提示指令。**关于提示的所有有趣的地方都假设不适用于这里的字体级控制值集。
- **坐标空间**由 `upm` 定义——默认为 1000,可以按注册覆盖。终端在渲染时将此空间映射到其单元格。
简单字形是 `glyf` 的子集,任何 `ttf-parser` 风格的库已经以大约三百行代码阅读。复合字形和提示是 TrueType 变复杂的地方;两者都被排除。
**颜色行为。**`glyf` 轮廓没有颜色。终端以当前前景色渲染它们——这*是* Nerd Font 继承情况,该协议的主要用例。彩色字形(状态徽章、多色徽标)作为单独的有效负载格式提供,`fmt=colrv0`/`fmt=colrv1`,在[后续文章](https://rapha.land/adding-color-glyphs-to-glyph-protocol/)中介绍。
**缩放和单元格指标。**`upm` 值定义字形的坐标空间;终端在渲染时将该空间映射到其单元格。在 `upm=1000` 处创作的图标将干净地缩放到 8×16 单元格和 32×64 单元格。应用程序不需要知道终端的单元格大小,在字体大小改变时也不必重新注册。
**创作。**大多数应用程序作者不会手工编写 `glyf` 字节。他们会从 SVG(他们的设计师交给他们的、每个图标库提供的)开始,在构建时转换。[`fonttools`](https://fonttools.readthedocs.io/) 已经通过其 `ttx`/`pens` 接口做到了这一点,我会随着 Rio 的参考实现一起提供一个小的 `svg2glyf` 助手,以便转换是一行代码。运行时注册就像加载字节和发送它们一样简单。
**生命周期和容量。**每个终端会话都有一个*词汇表*,最多同时包含 1024 个注册,由三个 PUA 范围中任何地方的代码点键入。注册在会话期间存在。如果应用程序注册第 1025 个字形,终端以 FIFO 顺序逐出最旧的注册——没有"词汇表已满"错误要处理。无法容忍静默逐出的应用程序应在发出之前查询其代码点。
### 注册:发送你自己的字形
应用程序想提供自己的分支图标。它选择一个 PUA 代码点——这里是 `U+E0A0`,Powerline 约定——并发送 `glyf` 轮廓([字体已使用四十年的相同 TrueType 矢量格式](https://learn.microsoft.com/en-us/typography/opentype/spec/glyf))base64 编码:
```
ESC _ 25a1 ; r ; cp=E0A0 ; upm=1000 ; ESC \
```
### 一个完整的例子:空 PUA 中的图标
为了使其具体,这是注册风格化轮廓和渲染它的完整管道。示例中的代码点是 `U+100000`——补充 PUA-B 的第一个代码点,没有已知字体覆盖它。这使演示明确:你看到的是你提供的轮廓,仅此而已。我们将使用 [`fontTools`](https://fonttools.readthedocs.io/) 作为 SVG 到 `glyf` 转换器——OpenType 工作的*事实上的* Python 工具包。
```python
# register_icon.py
import base64, sys
from fontTools.pens.ttGlyphPen import TTGlyphPen
# 在 glyf 坐标空间中绘制轮廓 (upm=1000, Y-up)。
pen = TTGlyphPen(None)
# ... pen 命令 ...
pen.closePath()
payload = base64.b64encode(pen.glyph().compile(None)).decode("ascii")
# 在 U+100000 处注册——空 PUA,没有系统字体声称它。
sys.stdout.write(f"\x1b_25a1;r;cp=100000;upm=1000;{payload}\x1b\\")
sys.stdout.flush()
# 发出代码点。单词"icon: "原样通过;
# 最后一个单元格渲染我们的轮廓。
sys.stdout.write(f"icon: {chr(0x100000)}\n")
```
应用程序不需要在打印之前读取回复——它选择了代码点,所以它已经知道要发出什么。一个典型的 20 点图标的 `glyf` 有效负载大约 150 字节;base64 编码并包装在 APC 中,线路上不到 250 字节。对于已经有 SVG 资产的应用程序作者,像 `svg2glyf` 这样的助手(随着 Rio 的参考实现提供)将整个东西折叠为两行:
```python
from glyph_protocol import register_from_svg
register_from_svg(cp=0x100000, svg_path="icon.svg")
print(f"icon: {chr(0x100000)}")
```
### 批量注册的旋钮:`reply=`
默认情况下,终端使用 `status=0` ACK 每个 `r`,错误回复带有 `reason=` 代码。非常适合一次性交互注册。不适合在启动钩子中注册 100 个字形然后退出——100 个排队的 ACK 从 PTY 流出进入继承它的任何 shell,作为用户下一个提示符的可见垃圾。
三个级别:
| `reply=` | 含义 |
|---|---|
| `1` | 默认。发出成功(`status=0`)和失败回复。用于一次性交互注册。 |
| `2` | 仅发出失败回复;成功是无声的。用于批量注册,其中你仍然想了解损坏的那些。 |
| `0` | 发出任何东西。一劳永逸。用于不会在身边读取回复的启动钩子。 |
```
ESC _ 25a1 ; r ; cp=E0A0 ; reply=0 ; upm=1000 ; ESC \
```
未知值静默回退到 `reply=1`,所以未来级别扩展(比如 `reply=3` 表示"仅成功")可以发布而不会破坏旧客户端。
### 清除:释放一个槽位
有时你想撤销注册——当编辑器退出并想返回终端到其默认值时,当 TUI 交换主题时,或当你调试时。`c` 动词:
```
ESC _ 25a1 ; c ; cp=E0A0 ESC \
```
终端回复 `status=0`(成功)或 `status=; reason=` 代码的任何地方。
---
[^1]: Nerd Font 包(FiraCode Nerd Font、JetBrains Mono Nerd Font 等)是整个字体范围分布的完整副本,包括每个 PUA 范围。对于 FiraCode 来说,这有大约 3500 个额外的字形,与基础字体大小相当。
[^2]: OSC 使用格式为 `ESC ] <number> ; ... ESC \` 的命令。OSC 序列数字被许多终端扩展使用:如果添加新序列,很可能与现有的东西冲突。APC 没有广泛使用,为新的东西提供了安全的命名空间。
相似文章
X AI KOLs Timeline
精选终端组合(Neovim、Herd、Gitu、Ghostty、Keeby)展示,将开发环境美学化的潮流。
Hacker News Top
1980 年代苹果 Macintosh 团队苦于缺乏美术功底,请来 Susan Kare 手绘经典 UI 图标,诞生了 Cairo 字体和传奇的 dogcow 图标,用于页面方向设置对话框。
OpenAI Blog
美甲技师Tabytha Scott利用ChatGPT作为创意合作伙伴,帮助设计定制美甲艺术。她利用AI来探索色彩搭配和设计理念,然后结合自己的艺术专长进行完善,最终在客户的指甲上呈现出完美作品。
GitHub Trending (daily)
FinceptTerminal 是一个开源金融智能平台,采用 C++20 和 Qt6 构建,提供 CFA 级别的分析工具、AI 自动化和全面的数据连接功能,适用于股票研究、投资组合管理和交易。
Google DeepMind Blog
Google 向所有开发者开放 Gemini 2.0 Flash 原生图像生成功能,支持多模态文本和图像输出,可用于故事创作、对话式图像编辑以及需要世界理解和文本渲染的应用。