ターミナル向け Glyph Protocol の紹介
(rapha.land)- ターミナルアプリケーションでカスタムアイコンをレンダリングするには パッチ済みフォント(Nerd Font など)のインストール が必要だった従来の問題を解決するため、新しいターミナルプロトコルが登場
- Glyph Protocol は、アプリケーションが 実行時にベクターグリフをターミナルへ直接登録 し、特定コードポイントのレンダリング可否を問い合わせられる仕組み
- グリフデータは TrueType の
glyfフォーマット を使用し、ターミナルがすでに持つラスタライザをそのまま活用できるため、新たな依存関係なしで実装可能 - 登録対象コードポイントを Unicode Private Use Area(PUA) に限定し、フィッシングや視覚的な偽装攻撃を根本的に防止
- Rio ターミナル で最初の実装が進行中で、Bubble Tea・Ratatui・Ink など主要 TUI フレームワーク向けのサンプルコードも公開済み
従来の問題: パッチフォント依存
- ターミナルエディタ、プロンプト、TUI でアイコンを正しく表示するには、ユーザーが Nerd Font や Powerline のようなパッチフォントを自分でインストール しなければならない
- フォントをインストールしていない場合、アイコンの位置には tofu(□) が表示され、パッチフォントは 1 つあたり 6〜12MB 程度と大容量
- JetBrainsMono Nerd Font Regular は約 7.8MB、FiraCode Nerd Font Regular は約 10.4MB、シンボル全体のアーカイブは約 60MB
- アプリケーション開発者は必要なグリフを自ら配布する手段がなく、ユーザーが 正しいフォント・バージョン・コードポイント対応 を揃えていることを期待するしかない構造
Glyph Protocol の主要機能
- 2 つの中核機能をサポート
- カスタムグリフ登録: アプリケーションが Unicode PUA コードポイントを選び、ベクターアウトラインをターミナルへ直接送信して実行時に登録
- コードポイント問い合わせ: 特定のコードポイントがシステムフォントでカバーされているか、セッション内登録でカバーされているか、両方か、どちらでもないかを問い合わせ
- ユーザーが Nerd Font をすでにインストールしていれば、問い合わせによって グリフ送信を省略 でき、未インストールでもアプリケーションがアウトラインを直接送って アイコンを正しく表示 できる
プロトコル構造
転送方式(Transport)
- OSC ではなく APC(Application Program Command) を使用
- APC はアプリケーション定義コマンド向けに設計されており、未実装のターミナルはそのシーケンスを 安全に無視 できる
- OSC は単一の 10 進整数をコマンド識別子とする グローバル名前空間 を共有するため衝突リスクがあるが、APC には独自の識別構造がありこの問題がない
識別子(Identifier)
- すべての Glyph Protocol メッセージには
25a1(U+25A1, WHITE SQUARE) コードポイントが接頭辞として付く- この文字は、グリフがないときにターミナルが描画する tofu の標準シンボル
- フレーミング形式:
ESC _ 25a1 ; <verb> [ ; key=value ]* [ ; <payload> ] ESC \\ - 4 つの verb:
s(support),q(query),r(register),c(clear)
Support(s): ターミナル対応可否の確認
- ターミナルがどのペイロード形式とプロトコルバージョンをサポートしているか確認する用途
- Glyph Protocol の存在自体を検出 する標準的な方法でもある
- 応答の
fmtは ビットフィールド で、各ビットが 1 つのペイロード形式を意味する1=glyf: TrueType 単純グリフ、v1 で必須2=colrv0: レイヤー化されたフラットカラーグリフ(OpenType COLR v0)、v1.2 で追加4=colrv1: グラデーションや変形を含む完全なペイントグラフ(OpenType COLR v1)、v1.2 で追加
- 応答があればプロトコル対応、タイムアウトなら未対応、
fmt=0ならプロトコル自体は実装されているがフォーマット未対応(完全性のための定義)
Query(q): コードポイントのレンダリング可否を問い合わせ
- 特定コードポイントがレンダリング可能かを問い合わせ、
status値で応答0(free): 何もレンダリングせず、tofu を表示1(system): システムフォントがカバー2(glossary): セッション内登録がカバー3(both): 両方がカバーし、レンダリング時は登録がシステムフォントを上書き
- TUI はシステムにすでにそのアイコンがあれば 登録をスキップ し、なければカスタムコードポイントを登録して 自然にフォールバック できる
Register(r): グリフ登録
- アプリケーションが PUA コードポイントを選び、base64 エンコードされた
glyfアウトライン を送信して登録 - 主なパラメータ
cp: 対象コードポイント(hex)。必ず 3 つの Unicode PUA 範囲(U+E000–U+F8FF,U+F0000–U+FFFFD,U+100000–U+10FFFD)内でなければならず、範囲外はreason=out_of_namespaceで拒否fmt: ペイロード形式。v1 ではglyfのみ定義されておりデフォルト値のため、通常は省略可能upm: units per em。アウトラインの座標空間を定義し、デフォルトは 1000
- 同じ
cpに 2 回目のrを送ると 以前の登録を上書き する - エラー時(非 PUA コードポイント、不正ペイロード、コンポジットグリフなど)は
status=<nonzero>; reason=<code>で応答
glyf フォーマットを選んだ理由
ベクターである理由
- グリフは写真ではないため 固定解像度を持たない: 同じアイコンを 12px の高密度 TUI と 24px の HiDPI ディスプレイの両方で描画する必要がある
- ラスターグリフは特定解像度に固定されるため、HiDPI ではぼやけたり、小さなセルでは判読できなくなったり する
glyf を具体的に選んだ理由
- テキストをレンダリングするすべてのターミナルには、すでに
glyfラスタライザ がリンクされている(FreeType, swash, ttf-parser, fontdue, allsorts など) - Glyph Protocol を採用しても、ターミナル側に 新たな依存関係は一切追加されない
- SVG を採用すると
resvgを取り込むか、新たに XML+path パーサを実装する必要がある - 転送サイズも小さい: 一般的なアイコンは 150〜400 バイトの
glyfデータで済み、同等の SVG と比べて 2〜3 倍小さい(base64 のオーバーヘッド込み)- 50 個のアイコンを登録する場合、約 13KB vs 35KB の差になり、tmux パイプやモバイル SSH 回線では体感できる
glyf の簡単な説明
glyfレコードはグリフを 閉じた輪郭線(contour)の集合 として保存する- 各点は on-curve または off-curve の 1 ビットのメタデータを持つ
- on-curve の 2 点が連続 → 直線
- on-curve の間に off-curve 点 → 2 次ベジェ曲線
- off-curve の 2 点が連続 → 中点に暗黙の on-curve 点が存在(圧縮トリック)
- 座標は EM スクエア内の整数グリッド位置で、
upm=1000のとき(500, 900)は半分の幅・90% の高さを意味する - 閉じた三角形は約 30 バイト、30 点のアイコンは約 200 バイト
プロトコルが定義する glyf サブセット
- 単純グリフのみ許可: コンポジットグリフ、他グリフ参照、フォントレベルのコンテキストは不可
- OpenType 仕様で定義された 標準フラグエンコーディング を使用
- ヒンティング命令なし: ヒンティングはフォント全体の制御値セットを前提とし、ここには該当しない
- 座標空間は
upmで定義され、デフォルト値は 1000、登録ごとに上書き可能
カラー・スケーリング・作成
glyfアウトラインには色情報がなく、現在の前景色 でレンダリングされる → Nerd Font の継承ケースと同じ- カラーグリフは別のペイロード形式
fmt=colrv0/fmt=colrv1でサポート upm値がグリフの座標空間を定義し、ターミナルが描画時にセルへマッピングするため、フォントサイズ変更時も再登録は不要- ほとんどの開発者は
glyfバイト列を手書きせず、SVG からビルド時に変換 する:fonttoolsのttx/pensインターフェースが使え、svg2glyfヘルパーも Rio のリファレンス実装とともに配布予定
寿命と容量
- 各ターミナルセッションは、3 つの PUA 範囲内のコードポイントをキーとする 最大 1024 個の同時登録 を保持する glossary を持つ
- 登録はセッション継続中有効
- 1025 個目のグリフ登録時には FIFO 順で最も古い登録を追い出す → 「glossary full」エラーはない
- サイレントな追い出しを許容できないアプリケーションは、出力前にそのコードポイントを問い合わせる必要がある
実例: 空き PUA にアイコンを登録
U+100000(Supplementary PUA-B の先頭コードポイント)にスタイライズされたアウトラインを登録する一連のパイプライン例fontToolsを SVG→glyf変換器として使用TTGlyphPenでアウトラインを描いてからbase64エンコードし、APC シーケンスとして送信した後、そのコードポイントを出力- 一般的な 20 ポイントアイコンの
glyfペイロードは約 150 バイト、APC ラップとbase64を含めても約 250 バイト - SVG アセットを持つ開発者向けに
svg2glyfヘルパーを提供予定 → 2 行で登録完了
大量登録向けオプション: reply=
- デフォルトではターミナルはすべての
rに対して ACK 応答を返すが、100 個のグリフを登録する起動フックでは 100 個のキューされた ACK が PTY に流れ込み、シェル上でゴミとして表示される 問題が発生 - 3 段階の制御
reply=1(デフォルト): 成功・失敗の両方で応答。対話的な単発登録向けreply=2: 失敗時のみ応答、成功時は無言。大量登録でエラーだけ検出 したいときに使うreply=0: 一切応答なし。fire-and-forget。起動フックのように応答を読む主体がいない場合向け
- 不明な値は
reply=1に自動フォールバックするため、将来拡張しても下位互換性を保てる
Clear(c): 登録解除
- エディタ終了時のターミナル既定値復元、TUI テーマ切り替え、デバッグ時に利用
- 単一スロット解除:
cpパラメータで特定コードポイントを指定 - glossary 全体を解除:
cpを省略 - 空スロットの解除はエラーではなく no-op で、
status=0を返す cpは PUA 範囲内でなければならず、範囲外ならreason=out_of_namespaceを返す
v1 に意図的に含めなかった機能
- 非 PUA コードポイントの登録不可: 3 つの Unicode PUA 範囲に限定
- リガチャなし: 登録は単一コードポイントにのみ適用され、シーケンスキー置換は v1 の範囲外。プログラミング用リガチャ(
->→⟶)は既存の OpenType フォントが処理 - セッション間の永続性なし: 実行ごとにグリフを新たに送信し、ターミナルがフォントキャッシュ化するのを防ぐ
- アプリケーション間共有なし: 各ターミナルセッションが独自の glossary を持ち、IPC やデーモンはない
- v1 の
glyfペイロードにカラーグリフなし: 前景色でレンダリングし、カラーは v1.2 のcolrv0/colrv1に分離 - これらの機能は必要なら後から追加できるが、一度追加すると簡単には削除できないため 意図的に除外
PUA 制限のセキュリティ上の根拠
- PUA 制限は API の美学ではなく、プロトコルをデフォルト有効でも安全にするための性質
- 任意コードポイントの登録を許すと、
U+0061(a)にo形のグリフを登録してbad.comをbod.comに見せかけられる- セルバッファ自体は依然として
bad.comのままなので、コピー&ペースト時のバイト列は正しいが、ユーザーが読む内容は偽りになる - すべてのターミナルプログラムに フィッシングのプリミティブ が生まれ、同じセッション内で後から実行されるプログラムにも影響が残る
- セルバッファ自体は依然として
- PUA に限定すれば、この攻撃タイプは 機械的に不可能 になる: ユーザーは PUA コードポイントを入力せず、ファイル名・URL・コマンド・変数名・ログにも PUA コードポイントは含まれない
- Nerd Font が慣例として築いてきた 信頼モデル(カスタムグリフは予約範囲にしか存在せず、実際のテキスト上には置けない)を、プロトコルレベルで強制
- 追加のセキュリティ特性
- セルバッファが権威を持つ: 選択・コピー・検索・ハイパーリンク検出・シェル履歴などは、アプリケーションが出力したコードポイントを返さなければならず、「見えるものとコピーされるものが違う」罠を作れない
- セッション分離: 2 つのタブが
U+E0A0にそれぞれ異なる branch アイコンを独立して登録でき、一方のタブの登録が他方のレンダリングに影響しない
既存方式との比較
Kitty Image Protocol(KIP)+ Unicode Placeholders
- KIP の Unicode placeholder を使えば Glyph Protocol を近似的に実装できるが、統合は難しく、placeholder を実装しているターミナルは Kitty、Ghostty、Rio だけ
- KIP は 画像プロトコル であり、グリフは画像ではない
- 利用ごとのコスト: 画面上で 200 回再利用されるグリフ(表の罫線、箇条書きマーカーなど)ごとに 200 個の画像参照を配置する必要があり、レイアウトと合成コストが発生する。Glyph Protocol はコードポイントを登録した後、フォント並みの速度でレンダリング できる
- ネイティブ解像度がない:
glyfアウトラインはピクセルサイズを持たないため、フォントサイズ変更時に自動適用される。KIP は特定サイズのビットマップを送るので、サイズ変更時に ぼやけるか再アップロードが必要 になり、フォントサイズ変更を検知する手段もない - 前景色の継承: 単色の
glyfアウトラインはセルの現在の前景色で描画されるため、テーマが自動で適用 される。画像は独自のピクセルを持つため、テキストカラーリングには参加しない
DEC DECDLD / DRCS
- VT220 が 1983 年に導入した Dynamically Redefinable Character Sets で、形の上では Glyph Protocol に似ている
- 2 つの重大な問題
- ビットマップ方式: ターミナルの現在のセルサイズに合わせたピクセルグリッドをアップロードするため、フォントサイズ変更・HiDPI・4K モニタへの切り替え時に ブロック状のピクセルが拡大縮小される。固定 10×20 CRT 時代の方式であり、現代の多様なセルサイズには不向き
- 名前空間制限がない: DECDLD は GL 範囲(a, b, c がある領域)にマッピングされる文字セットを上書きできるため、信頼できないプログラムが
aのレンダリングを再定義 できてしまう → これが現代のターミナルが DECDLD を有効化したがらない最大の理由
Rio ターミナルでの実装状況
- Glyph Protocol は Rio ターミナル の main ブランチですでに利用可能で、5 月中に正式導入予定 → 最初の実装
- 仕様全体はリリースとともに公開され、グリフ登録とターミナル問い合わせのサンプルコードも含まれる
- 動作サンプルは raphamorim/glyph-protocol-examples リポジトリで確認でき、Bubble Tea、Ratatui、Ink 向けの統合例も含まれる
- プロトコルは今後も更新される可能性があり、より多くのアプリケーションやターミナルが参加するにつれてメッセージ形式・問い合わせ応答・エッジケースが変わるかもしれない → 現時点でビルドする場合は動くターゲットとして扱い、実装バージョンの固定を推奨
- 他のターミナルエミュレータでの採用にも期待しており、エコシステム全体の利点が大きく、実装範囲は意図的に小さく保たれている
コミュニティへの未解決の問い
- フォントサイズ変更通知をプロトコル範囲に含めるべきか?: Glyph Protocol 自体はアウトラインが解像度非依存なのでこの問題を回避できるが、画像とグリフを組み合わせる TUI は セルメトリクス変更をポーリング以外で知る方法がない →
resizeまたはmetrics-changed通知が範囲内か範囲外かについて議論中 - 非 PUA 登録を責任を持って許可する方法はあるか?: PUA 専用ルールはデフォルトの安全性を保証する一方、CJK 入力メソッドが未収録漢字向けグリフを送る、あるいは言語固有ツールがグリフを上書きするようなユースケースは阻まれる → 明示的なユーザーレベルのオプトイン、署名付き機能、信頼済みソースフラグなどにより、フィッシングを再導入せずにこれらのケースを開ける形 について意見を募っている
まだコメントはありません。