From 867e1b5207a88505da66d650610bc4a56bd16411 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Mon, 20 Apr 2026 15:34:35 +0200 Subject: [PATCH 1/7] add glyph protocol spec --- specs/glyph-protocol.md | 655 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 655 insertions(+) create mode 100644 specs/glyph-protocol.md diff --git a/specs/glyph-protocol.md b/specs/glyph-protocol.md new file mode 100644 index 0000000000..e838893dc1 --- /dev/null +++ b/specs/glyph-protocol.md @@ -0,0 +1,655 @@ +# Glyph Protocol + +**Author:** Raphael Amorim +**Year:** 2026 +**Last updated:** 2026-04-20 + +**See also:** +- Blog post introducing the protocol and its rationale: + +- Reference implementation: [Rio terminal](https://raphamorim.io/rio) +- Example apps (ratatui, bubbletea v2, ink) registering real Nerd Font + outlines at empty PUA-B slots: + [glyph-protocol-examples](https://github.com/raphamorim/glyph-protocol-examples) + +--- + +## Abstract + +Glyph Protocol is a terminal protocol that lets applications ship +custom vector glyphs to the terminal at runtime without requiring +the user to install a patched font (Nerd Fonts, Powerline, etc.). +Registrations are restricted to the Unicode Private Use Areas — +ranges the user never types and existing text never contains — so +the protocol cannot be used to modify the appearance of real text. + +The protocol is transported over APC (Application Program Command) +sequences. The default payload is the OpenType `glyf` simple-glyph +record for monochrome icons; colour icons ride OpenType `COLR` v0 +(layered flat colour) or `COLR` v1 (full paint graph). Four verbs +are defined: support-negotiation (`s`), query (`q`), register +(`r`), and clear (`c`). + +## 1. Motivation + +Terminal applications today rely on out-of-band font distribution +to render non-ASCII iconography. The dominant workflow is: + +1. Application author picks codepoints in the Unicode Private Use + Area. +2. User installs a multi-megabyte patched font that maps those + codepoints to glyphs. +3. User switches their terminal's font to the patched font. +4. Application emits the codepoint and hopes the mapping is correct. + +This workflow has three structural problems: + +- **Distribution cost.** Users carry megabytes of glyphs they + never see. +- **Coupling.** Adding a new icon requires the entire font + ecosystem to update. Application authors are locked into a fixed + PUA allocation. +- **Invisible failure.** An application cannot tell whether a + given codepoint will render; it can only emit it and accept the + result. + +Glyph Protocol moves glyph ownership from the font file to the +application, and gives applications a way to ask the terminal what +it can render before it renders it. + +## 2. Design goals + +- **Small surface.** Four verbs, three payload formats (one + required, two optional), no daemons, no caches, no cross-session + state. +- **Zero new terminal dependencies.** Every terminal that renders + text already links a `glyf` rasterizer. +- **Resolution independent.** Glyphs are vector and scale to any + cell size. +- **Graceful degradation.** Terminals that do not implement the + protocol ignore the APC message. Applications detect support by + sending a query and watching for a reply. +- **No override of user text.** Registrations are confined to PUA + codepoints — ranges no user types and no pre-existing text + contains. The rendered appearance of `a`, `ssh`, or any URL + cannot be changed by any program that writes to the terminal. + See §9. +- **Small on the wire.** Typical icons are 150–400 bytes of + `glyf`, 2–3× smaller than the equivalent SVG. + +## 3. Transport + +Glyph Protocol uses APC (Application Program Command, +`ESC _ ... ESC \`). APC is specified for application-defined +commands; terminals that do not implement a particular APC command +are required to ignore it, making APC safer than OSC for +introducing new protocols. + +### 3.1 Identifier + +Every Glyph Protocol message begins with the Unicode codepoint +**U+25A1** (WHITE SQUARE), written in the message as the +lowercase hex string `25a1`. Terminals MUST ignore any APC +message whose body does not begin with this identifier. + +### 3.2 Framing + +The general form of a Glyph Protocol message is: + +``` +ESC _ 25a1 ; [ ; key=value ]* [ ; ] ESC \ +``` + +Parameter keys use lowercase ASCII. Values are lowercase hex for +codepoints, decimal for integers, base64 for binary payloads, and +decimal u8 values for the `status` field of every response. + +### 3.3 Verbs + +| Verb | Meaning | +|------|---------| +| `s` | Advertise supported payload formats. Doubles as a protocol-detection ping — any reply confirms Glyph Protocol; a timeout means unsupported. | +| `q` | Query the state of a codepoint. | +| `r` | Register a glyph for a PUA codepoint. | +| `c` | Clear one slot or every slot in this session's glossary. | + +The `s` verb takes no parameters and returns a decimal `u8` +bitfield under the `fmt` key: + +| Bit | `fmt=` value | Format name | +|-----|--------------|-------------| +| 0 | 1 | `glyf` (monochrome simple-glyph; §8). | +| 1 | 2 | `colrv0` (layered flat colour; §8.6). | +| 2 | 4 | `colrv1` (OpenType paint graph; §8.7). | + +Further bits are reserved. Clients treat unknown bits as +unsupported and ignore them. A terminal that advertises only +`fmt=1` (monochrome) and receives an `r` with `fmt=colrv0` / +`fmt=colrv1` MUST reject the registration (`reason=malformed_payload` +is acceptable for parser-level rejection). Clients SHOULD check +the `s` reply before emitting colour registrations so they can +fall back to a monochrome `fmt=glyf` without making a doomed +round-trip. + +## 4. Glossary namespace + +Registrations target codepoints the application picks, constrained +to the three Unicode Private Use Areas: + +| Range | Plane | Common use | +|------------------------------|-------|------------| +| `U+E000`–`U+F8FF` | BMP | Basic PUA. Nerd Fonts, Powerline, Font Awesome all live here. | +| `U+F0000`–`U+FFFFD` | 15 | Supplementary PUA-A. Nerd Fonts v3 Material icons live here. | +| `U+100000`–`U+10FFFD` | 16 | Supplementary PUA-B. No common convention — clean space for apps that want it. | + +Any other codepoint — ASCII, Latin-1, CJK, emoji, control chars — +is rejected by `r` and `c` with `reason=out_of_namespace`. + +Each terminal session holds at most **1024 simultaneous +registrations**. One codepoint = one slot regardless of payload +format: a `fmt=colrv1` registration with 500 inner outlines still +consumes exactly one glossary slot. When the glossary is full and +a new `r` arrives, the terminal evicts the oldest registration in +FIFO order; the new registration succeeds. Applications that +cannot tolerate silent eviction SHOULD query their codepoint with +`q` before emitting. + +Each terminal session (tab, pane, PTY) owns its own glossary. Two +sessions can independently register `U+E0A0`, each pointing at a +different glyph. Registrations MUST NOT leak between sessions. + +## 5. Query (`q`) + +### 5.1 Request + +``` +ESC _ 25a1 ; q ; cp= ESC \ +``` + +Parameters: + +- `cp` — codepoint in hex. Any valid Unicode scalar value (not a + surrogate). May be inside or outside PUA. + +### 5.2 Response + +``` +ESC _ 25a1 ; q ; cp= ; status= ESC \ +``` + +`status` is a decimal u8 encoding a two-bit field: + +| Value | State | Meaning | +|-------|-------------|---------| +| `0` | `free` | No font in the fallback chain renders `cp`, and the glossary has no registration for it. The cell will render as tofu. | +| `1` | `system` | Some font in the fallback chain renders `cp`. No glossary registration (or `cp` is outside PUA). | +| `2` | `glossary` | `cp` is in PUA and has a live registration in this session. No system font covers it. | +| `3` | `both` | `cp` is in PUA, has a live registration, AND a system font also covers it. The registration shadows the system font at render time. | + +Bit 0 = system coverage, bit 1 = glossary coverage. + +For non-PUA codepoints only `0` and `1` are possible. + +## 6. Register (`r`) + +### 6.1 Request + +``` +ESC _ 25a1 ; r ; cp= ; fmt=glyf ; reply=<0|1|2> ; upm= ; ESC \ +``` + +Parameters: + +- `cp` — target codepoint in hex. MUST be in one of the PUA ranges + defined in §4. Otherwise the request is rejected with + `reason=out_of_namespace`. +- `fmt` — payload format. One of `glyf`, `colrv0`, `colrv1`. + Optional; `glyf` is the default. See §8 for each format's wire + layout. +- `reply` — reply-level control. Optional; default `1`. + - `reply=0` — the terminal MUST NOT emit any reply for this + registration (neither success nor failure). Intended for bulk + fire-and-forget startup registrations that won't be read back. + - `reply=1` — the terminal emits both success and failure replies + (the default; equivalent to omitting the parameter). + - `reply=2` — the terminal emits failure replies only; success + registrations are silent. Useful for bulk registrations that + still want to learn about the broken ones without a success + ACK for every glyph. + Unknown values fall back to `reply=1`. +- `upm` — units per em, the coordinate space the outline is + authored in. Optional; default `1000`. +- payload — base64-encoded payload for the declared `fmt`. + +### 6.2 Response + +Replies are gated by the request's `reply` parameter (§6.1). +For `reply=1` (the default), successful registrations emit: + +``` +ESC _ 25a1 ; r ; cp= ; status=0 ESC \ +``` + +`cp` is echoed from the request. + +Failures, when not suppressed by `reply=0`, emit: + +``` +ESC _ 25a1 ; r ; cp= ; status= ; reason= ESC \ +``` + +At `reply=2`, successful registrations are silent; failures still +emit the error reply above. At `reply=0`, neither success nor +failure produces any output — the registration is fire-and-forget. + +Defined error codes: + +| Code | Meaning | +|-------------------------|---------| +| `out_of_namespace` | `cp` is not in any PUA range. | +| `composite_unsupported` | Payload contains composite glyphs. | +| `hinting_unsupported` | Payload contains hinting instructions. | +| `malformed_payload` | Payload failed to parse as `glyf`. | +| `payload_too_large` | Payload exceeds 64 KiB post-base64-decode. | + +### 6.3 Overwrite and eviction + +A second `r` on the same `cp` overwrites the first. This is how +applications update a glyph or react to theme changes. + +When the glossary already holds 1024 registrations and the new `r` +is for a `cp` that is NOT already registered, the terminal evicts +the oldest registration (FIFO) to make room. Eviction silently +invalidates the evicted codepoint: subsequent emissions fall +through to the system font (or tofu). Applications SHOULD query +before emitting if they cannot tolerate silent eviction. + +### 6.4 Lifetime + +Registrations live for the duration of the terminal session. A +terminal reset (e.g. `ESC c`) MAY clear the entire glossary. +Registrations MUST NOT persist across terminal restarts. + +## 7. Clear (`c`) + +### 7.1 Request + +``` +ESC _ 25a1 ; c [ ; cp= ] ESC \ +``` + +If `cp` is omitted, every slot in the session's glossary is +cleared. Otherwise the slot corresponding to `cp` is cleared. `cp` +MUST be in a PUA range; otherwise the request is rejected with +`reason=out_of_namespace`. + +### 7.2 Response + +Success: + +``` +ESC _ 25a1 ; c ; status=0 ESC \ +``` + +Clearing an empty slot is a no-op and MUST return `status=0`. + +Failure: + +``` +ESC _ 25a1 ; c ; status=1 ; reason=out_of_namespace ESC \ +``` + +### 7.3 Cache invalidation + +When a slot is cleared (explicitly, via overwrite, or via +eviction), the terminal MUST invalidate any rasterization cached +for that codepoint. A subsequent `r` that reuses the codepoint +MUST rasterize the new outline fresh, not serve stale pixels. + +## 8. Payload format: `glyf` + +### 8.1 Scope + +Glyph Protocol reuses the OpenType `glyf` table's simple-glyph +record as its wire format. Authoritative references: + +- OpenType `glyf` specification (Microsoft Typography). +- Apple TrueType Reference Manual, Chapter 6. + +### 8.2 Constraints + +Terminals implementing Glyph Protocol MUST accept the following +subset of `glyf` and MAY reject anything else with +`reason=composite_unsupported` or `reason=hinting_unsupported`: + +- **Simple glyphs only.** No composite glyphs, no references to + other glyphs. +- **Standard flag encoding** as defined by the OpenType spec + (on-curve, off-curve, x-short, y-short, repeat). +- **No hinting instructions.** The `instructionLength` field MUST + be zero. +- **Coordinate space** defined by `upm`. The terminal maps this + space onto its cell at render time. + +### 8.3 Contour semantics + +A `glyf` record stores a glyph as a set of closed contours. Each +contour is a sequence of points; each point carries a single +on-curve/off-curve flag bit. Contour walking follows standard +TrueType semantics: + +- Two on-curve points in a row → straight line. +- An off-curve point between two on-curve points → quadratic + Bézier with the off-curve point as the control point. +- Two off-curve points in a row → an implied on-curve point at + their midpoint. + +### 8.4 Color + +`glyf` outlines carry no color. Terminals MUST render them in the +current foreground color. For colored icons see the `colrv0` and +`colrv1` formats in §8.6 / §8.7. + +### 8.5 Scaling + +The `upm` value defines the glyph's authoring coordinate space. +The terminal maps that space onto its cell at render time. +Applications MUST NOT assume a particular cell size and MUST NOT +re-register glyphs on font size change. + +### 8.6 Payload format: `colrv0` + +`fmt=colrv0` carries a layered flat-colour glyph using the +OpenType `COLR` v0 and `CPAL` tables verbatim. The protocol wraps +those tables in a small container that also ships the simple-glyph +outlines each layer references, so a colour glyph is self- +contained: no external font needed. + +**Container layout** (all integers big-endian, post-base64-decode): + +``` +u16 n_glyphs # 1..=1024 +repeat n_glyphs: + u16 glyf_len + glyf_len bytes # simple-glyph record, §8.2 subset +u16 colr_len # > 0 +colr_len bytes # OpenType COLR v0 table +u16 cpal_len # may be 0 (see below) +cpal_len bytes # OpenType CPAL table (required for v0) +``` + +`GlyphId` values in the `COLR` table resolve to indices into the +outline array (glyph 0 is the base glyph rendered when the +terminal emits `cp`). `paletteIndex` values in the `COLR` layer +records resolve to entries in the CPAL colour records array, in +standard OpenType order (one record = one BGRA quadruple). +`paletteIndex = 0xFFFF` MUST be rendered as the current foreground +colour, per the OpenType spec. + +**Rendering rules.** + +- Layers composite in painter order (first layer painted first). +- Per-layer colours come from CPAL; `0xFFFF` means foreground. +- `COLR` v0 defines no transforms or compositing modes beyond + `src-over`, so terminals MAY implement v0 in one pass with no + graphics-state stack. + +**Validation.** Terminals SHOULD validate the wrapped `COLR` and +`CPAL` tables using an OpenType parser (e.g. `ttf-parser`); a +`COLR` table that fails to parse SHOULD be rejected with +`reason=malformed_payload`. Every carried outline MUST satisfy the +`glyf` simple-glyph subset of §8.2; violations use the same error +codes as `fmt=glyf`. + +### 8.7 Payload format: `colrv1` + +`fmt=colrv1` shares the container layout of §8.6 but ships an +OpenType `COLR` v1 table, which adds a full paint graph: linear, +radial, and sweep gradients, affine transforms, clip boxes, and +per-layer compositing modes. `CPAL` remains valid but is optional +— v1 paints may carry sRGBA directly — so `cpal_len = 0` is +permitted and means "the COLR references no palette index." + +**Paint types.** Terminals implementing `colrv1` SHOULD support +the full OpenType paint-graph vocabulary for maximum interop. A +conforming subset for low-overhead implementations is: + +- Solid (direct sRGBA or palette index). +- Linear gradient. +- Radial gradient. +- Affine transforms on paint subtrees. +- `src-over` layer composite. + +Terminals MAY render unsupported paint nodes (sweep gradients, +blend modes beyond `src-over`, variations) using a reasonable +fallback — typically the paint subtree's first solid colour — +rather than rejecting the registration. + +**Foreground inheritance.** CPAL palette index `0xFFFF` and +v1's `PaintSolid` with the foreground sentinel MUST resolve to +the cell's current foreground colour at rasterisation time. +Terminals that cache rasterised colour glyphs MUST re-rasterise +on foreground change for any glyph whose paint graph references +`0xFFFF`. + +**Security.** The colour formats add no new attack surface beyond +§9: `cp` is still PUA-only, the cell buffer is still authoritative +for copy/selection, and registrations are still session-scoped. +A malformed `COLR` is a rendering error, not an injection vector +— the rendered pixels can only affect cells the client itself +emits at a PUA codepoint. + +### 8.8 Authoring + +Most applications will not hand-author `COLR` bytes either. +Typical flows: + +- **From an existing colour font.** Use `fontTools` to extract the + `COLR`/`CPAL` tables for the glyphs of interest, then pack them + with the referenced outlines into the container above. +- **From SVG.** The Skia team publishes `nanoemoji` / `maximum-color`, + which compiles a directory of SVGs into a `COLR` v1 font; feed + its output into the packer. + +Rio ships an `svg2colr` helper alongside `svg2glyf` for this flow. + +## 9. Security considerations + +The core property Glyph Protocol must preserve is that **an +application cannot change how existing text looks**. Enforcement +is structural: + +- Register accepts a `cp` parameter, but `cp` MUST be in one of the + three Unicode Private Use Areas (§4). Any non-PUA codepoint is + rejected with `reason=out_of_namespace`. +- Users never type PUA codepoints. No pre-existing text — + filenames, URLs, commands, variable names, log lines — contains + them. A program that registers a glyph can only affect how PUA + codepoints render, and PUA codepoints only appear in text the + same application (or another one opting into the same + convention) has deliberately emitted. +- The cell buffer is authoritative. Selection, copy, search, + hyperlinks, shell history, and any other text extraction MUST + return the codepoint the application emitted, never the + rendered glyph. + +Without these properties, a program writing to the PTY could +register a glyph for `a` that looks like `o` and mislead the +reader. With them, the worst a program can do is render a +weird-looking character at a PUA codepoint the user never types +and the cell buffer honestly reports. + +Other considerations: + +- **Resource bounds.** The 1024-slot cap and 64 KiB per-payload cap + give a hard upper bound of 64 MiB on the glossary's memory + footprint per session. +- **No code execution.** The `glyf` subset defined in §8.2 + excludes hinting instructions, which is the only part of + TrueType that is executable. Glyph Protocol is purely + declarative. +- **No filesystem access.** Glyph Protocol messages do not + reference files and MUST NOT be used to load data from disk. +- **Session isolation.** Glossaries MUST NOT leak between terminal + tabs, windows, multiplexer panes, or PTY sessions. + +## 10. Non-goals (v1) + +- **No non-PUA codepoints.** Registration is restricted to the + three PUA ranges — see §4. +- **No ligatures.** Registration applies to a single codepoint. + Sequence-keyed substitution is out of scope; programming + ligatures are already handled by OpenType fonts. +- **No persistence across sessions.** Glyphs are shipped fresh on + each run. +- **No cross-application sharing.** Each terminal session owns its + glossary. No IPC, no daemon. +- **No bitmap colour glyphs.** Colour is delivered via `colrv0` + and `colrv1` (§8.6 / §8.7), which are vector. `CBDT`/`sbix`/ + `SVG ` tables are explicitly out of scope so resolution + independence is preserved. +- **No subpixel positioning control.** The terminal's normal cell + positioning applies. +- **No bitmap payloads.** Vector only, to preserve resolution + independence. + +## 11. Conformance + +A terminal emulator is Glyph Protocol v1 conformant if it: + +1. Recognizes the `25a1` identifier in APC sequences. +2. Implements the `s`, `q`, `r`, and `c` verbs with the semantics + defined in this specification, and advertises every accepted + payload format via the `fmt=` bitfield in the `s` reply. +3. Restricts register/clear `cp` to the three PUA ranges; rejects + anything else with `reason=out_of_namespace`. +4. Holds at most 1024 simultaneous registrations per session and + evicts in FIFO order when full. +5. Accepts the `glyf` simple-glyph subset defined in §8.2. The + `colrv0` and `colrv1` formats are OPTIONAL; terminals that + accept them MUST set the corresponding bit in the `s` reply. +6. Renders registered `glyf` glyphs in the current foreground + color; renders `colrv0`/`colrv1` glyphs using the COLR paint + graph, resolving palette index `0xFFFF` to the current + foreground color. +7. Scales glyphs according to `upm` and the current cell size. +8. Enforces the cell-buffer authority invariant in §9: selection, + copy, and search return the raw codepoint. +9. Ignores unrecognized parameters rather than failing the + request. + +A client (application) is Glyph Protocol v1 conformant if it: + +1. Emits `cp` only from the three PUA ranges. +2. Treats query timeout as "terminal does not implement Glyph + Protocol." +3. Emits only the `glyf` subset defined in §8.2. +4. Handles all `reason=*` error codes without crashing. + +## 12. Reference implementation + +The reference implementation ships in Rio terminal. A companion +helper, `svg2glyf`, ships alongside to convert existing SVG assets +to the accepted `glyf` subset at build time. + +## Appendix A. Worked example: register an icon in empty PUA + +```python +import base64, sys +from fontTools.pens.ttGlyphPen import TTGlyphPen + +# A stylised outline in glyf coordinate space (upm=1000, Y-up). +pen = TTGlyphPen(None) +# ... draw commands ... +pen.closePath() + +payload = base64.b64encode(pen.glyph().compile(None)).decode("ascii") + +# Register at U+100000 — the first codepoint of Supplementary PUA-B. +# No known font covers this range, so the registration is the sole +# source of the rendered glyph and the demo is unambiguous. +sys.stdout.write( + f"\x1b_25a1;r;cp=100000;upm=1000;{payload}\x1b\\" +) +sys.stdout.flush() + +# From now on, U+100000 renders our outline. +sys.stdout.write(f"icon: {chr(0x100000)}\n") +``` + +## Appendix B. Worked example: query before registering + +```python +import sys + +def q(cp: int) -> None: + sys.stdout.write(f"\x1b_25a1;q;cp={cp:x}\x1b\\") + sys.stdout.flush() + +# Does the user already have Nerd Fonts installed? +q(0xE0A0) +# Expected reply parsed from the PTY: +# status=1 → system font covers it; don't register, just emit cp +# status=0 → nothing covers it; register and emit +``` + +## Appendix C. Implementation notes + +These are not normative but reflect lessons from the first +implementations. + +**Response draining.** By default `r` and `c` produce an APC reply +on the PTY. Client applications that register at startup and do not +care about the reply have three options, in order of preference: + +1. Use `reply=0` on the register request (§6.1). The terminal emits + nothing, so there's nothing to drain and nothing to leak. Best + for bulk startup registrations. +2. Use `reply=2` to keep failure replies but drop success ACKs. + Retains debuggability (you still learn about malformed payloads) + without the success-reply noise of a 100-glyph registration. +3. Let the framework's input reader swallow the reply — safe only + while that reader is alive. + +The failure mode to watch for is sending `r` or `c` with `reply=1` +AFTER the framework has torn down its input reader — typically on +exit — at which point the reply arrives in the PTY but nobody reads +it, and the shell that takes over the PTY after the app exits emits +the queued bytes as visible text (`.25a1;c;status=0` or +`.25a1;r;cp=…;status=0`). For exit-time cleanup, prefer skipping +the `c` altogether (registrations expire with the session anyway). + +**Practical source of `glyf` data.** Most apps will not hand-author +`glyf` bytes. The typical pipeline is: + +1. Open a Nerd Font or similar icon TTF with `fontTools`. +2. For each codepoint of interest, pull the glyph record. +3. If composite, flatten via `fontTools.pens.ttGlyphPen.TTGlyphPen`. +4. Strip hinting instructions (`instructionLength := 0`). +5. Compile to bytes; base64-encode; register at a codepoint of the + app's choosing. + +Because the source codepoint in the font is irrelevant to the +protocol, applications commonly pull outlines from a Nerd Font's +basic-PUA codepoints (`U+E0A0`, `U+F07B`, …) and register them at +Supplementary PUA-B slots (`U+100000`+) — that way the rendered glyph +is unambiguously from the registration, not from a system font that +happens to cover the same codepoint. + +**Atlas cache invalidation.** The cache-invalidation rule in §7.3 +applies to overwrite and eviction too, not only explicit clear. +Implementations that key their glyph atlas on some stable slot id +(rather than `cp`) must ensure the slot id is either released on +clear/evict or paired with a per-registration invalidation tag, so +that a subsequent register reusing the id rasterizes fresh bytes +rather than serving a stale bitmap. + +## Appendix D. Change log + +| Date | Version | Notes | +|------------|---------|-------| +| 2026-04-17 | v1 | Initial release. Register accepts a client-picked `cp` restricted to PUA; 256-slot glossary with FIFO eviction; numeric `status` field; ligatures out of scope. | +| 2026-04-19 | v1.1 | Added `s` verb (support advertisement / protocol ping). | +| 2026-04-19 | v1.2 | Added `fmt=colrv0` and `fmt=colrv1` payload formats wrapping OpenType `COLR` / `CPAL` tables with sidecar `glyf` outlines. Both advertised via bits 1 and 2 of the `s` reply's `fmt=` bitfield. | +| 2026-04-19 | v1.3 | Added `reply=0|1|2` parameter to the `r` verb so bulk registrations can suppress success ACKs (`reply=2`) or go fully fire-and-forget (`reply=0`). Default `reply=1` preserves v1.0 behaviour. | +| 2026-04-19 | v1.4 | Raised the glossary capacity from 256 to 1024 simultaneous registrations per session, and raised the `n_glyphs` cap in `fmt=colrv0`/`colrv1` containers from 256 to 1024 outlines. Both bumps quadruple the worst-case memory footprint; the 64 KiB per-payload cap is unchanged. | From 09aba1b0c80bc31e8d24bcf516b75f16bd4b679c Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Mon, 20 Apr 2026 15:38:09 +0200 Subject: [PATCH 2/7] update with colr v0 and v1 --- specs/glyph-protocol.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/specs/glyph-protocol.md b/specs/glyph-protocol.md index e838893dc1..1c6b81d85e 100644 --- a/specs/glyph-protocol.md +++ b/specs/glyph-protocol.md @@ -9,7 +9,7 @@ - Reference implementation: [Rio terminal](https://raphamorim.io/rio) - Example apps (ratatui, bubbletea v2, ink) registering real Nerd Font - outlines at empty PUA-B slots: + outlines and colrv1/colrv0 emojis at empty PUA-B slots: [glyph-protocol-examples](https://github.com/raphamorim/glyph-protocol-examples) --- @@ -17,11 +17,14 @@ ## Abstract Glyph Protocol is a terminal protocol that lets applications ship -custom vector glyphs to the terminal at runtime without requiring -the user to install a patched font (Nerd Fonts, Powerline, etc.). -Registrations are restricted to the Unicode Private Use Areas — -ranges the user never types and existing text never contains — so -the protocol cannot be used to modify the appearance of real text. +custom vector glyphs to the terminal at runtime — monochrome via +OpenType `glyf`, or full-colour via OpenType `COLR` v0 (flat layered +colour) and `COLR` v1 (paint graph with gradients, transforms, and +composites) — without requiring the user to install a patched font +(Nerd Fonts, Powerline, etc.). Registrations are restricted to the +Unicode Private Use Areas — ranges the user never types and existing +text never contains — so the protocol cannot be used to modify the +appearance of real text. The protocol is transported over APC (Application Program Command) sequences. The default payload is the OpenType `glyf` simple-glyph From 55b92796a0c4c75667ce0b842a948b45c59fdf96 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Mon, 20 Apr 2026 15:48:45 +0200 Subject: [PATCH 3/7] small updates --- specs/glyph-protocol.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/specs/glyph-protocol.md b/specs/glyph-protocol.md index 1c6b81d85e..675d34ecc1 100644 --- a/specs/glyph-protocol.md +++ b/specs/glyph-protocol.md @@ -62,11 +62,14 @@ it can render before it renders it. ## 2. Design goals -- **Small surface.** Four verbs, three payload formats (one +- **Small surface.** Few verbs, three payload formats (one required, two optional), no daemons, no caches, no cross-session state. - **Zero new terminal dependencies.** Every terminal that renders - text already links a `glyf` rasterizer. + OpenType text already links a `glyf` rasterizer; terminals that + render Apple / Google colour emoji also already parse `COLR` + + `CPAL`. No new format support is required — the protocol reuses + the tables the font stack already decodes. - **Resolution independent.** Glyphs are vector and scale to any cell size. - **Graceful degradation.** Terminals that do not implement the @@ -77,8 +80,8 @@ it can render before it renders it. contains. The rendered appearance of `a`, `ssh`, or any URL cannot be changed by any program that writes to the terminal. See §9. -- **Small on the wire.** Typical icons are 150–400 bytes of - `glyf`, 2–3× smaller than the equivalent SVG. +- **Small on the wire.** Typical icons are 50–400 bytes of + `glyf`, 2–4× smaller than the equivalent SVG. ## 3. Transport @@ -325,12 +328,15 @@ Terminals implementing Glyph Protocol MUST accept the following subset of `glyf` and MAY reject anything else with `reason=composite_unsupported` or `reason=hinting_unsupported`: -- **Simple glyphs only.** No composite glyphs, no references to - other glyphs. +- **Simple glyphs only** (in v1). No composite glyphs, no references + to other glyphs. Composite support may land in a future version + once the simple-glyph baseline is settled in the field. - **Standard flag encoding** as defined by the OpenType spec (on-curve, off-curve, x-short, y-short, repeat). -- **No hinting instructions.** The `instructionLength` field MUST - be zero. +- **No hinting instructions** (in v1). The `instructionLength` + field MUST be zero. Bytecode hinting may be allowed in a future + version once the unhinted baseline is settled; for now the + terminal's own anti-aliasing is what users see. - **Coordinate space** defined by `upm`. The terminal maps this space onto its cell at render time. @@ -454,8 +460,6 @@ Typical flows: which compiles a directory of SVGs into a `COLR` v1 font; feed its output into the packer. -Rio ships an `svg2colr` helper alongside `svg2glyf` for this flow. - ## 9. Security considerations The core property Glyph Protocol must preserve is that **an From c6575da72845df9dc6ef9b03797c2e1766df5fd6 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Tue, 21 Apr 2026 22:01:22 +0200 Subject: [PATCH 4/7] auto allocation --- specs/glyph-protocol.md | 78 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/specs/glyph-protocol.md b/specs/glyph-protocol.md index 675d34ecc1..f0f16f5275 100644 --- a/specs/glyph-protocol.md +++ b/specs/glyph-protocol.md @@ -2,7 +2,7 @@ **Author:** Raphael Amorim **Year:** 2026 -**Last updated:** 2026-04-20 +**Last updated:** 2026-04-21 **See also:** - Blog post introducing the protocol and its rationale: @@ -201,14 +201,18 @@ For non-PUA codepoints only `0` and `1` are possible. ### 6.1 Request ``` -ESC _ 25a1 ; r ; cp= ; fmt=glyf ; reply=<0|1|2> ; upm= ; ESC \ +ESC _ 25a1 ; r ; cp= ; fmt=glyf ; reply=<0|1|2> ; upm= ; ESC \ ``` Parameters: -- `cp` — target codepoint in hex. MUST be in one of the PUA ranges - defined in §4. Otherwise the request is rejected with - `reason=out_of_namespace`. +- `cp` — target codepoint in hex, OR the literal string `auto` to + request terminal-side auto-allocation. + - When hex: MUST be in one of the PUA ranges defined in §4. + Otherwise the request is rejected with `reason=out_of_namespace`. + - When `auto`: the terminal picks a free PUA codepoint, registers + the payload there, and echoes the allocated codepoint in the + reply's `cp` field. See §6.5. - `fmt` — payload format. One of `glyf`, `colrv0`, `colrv1`. Optional; `glyf` is the default. See §8 for each format's wire layout. @@ -236,7 +240,9 @@ For `reply=1` (the default), successful registrations emit: ESC _ 25a1 ; r ; cp= ; status=0 ESC \ ``` -`cp` is echoed from the request. +`cp` is echoed from the request. When the request used `cp=auto`, +the reply's `cp` is the hex codepoint the terminal allocated (never +the literal `auto`), so the client can emit it. Failures, when not suppressed by `reply=0`, emit: @@ -257,6 +263,8 @@ Defined error codes: | `hinting_unsupported` | Payload contains hinting instructions. | | `malformed_payload` | Payload failed to parse as `glyf`. | | `payload_too_large` | Payload exceeds 64 KiB post-base64-decode. | +| `auto_unsupported` | `cp=auto` was requested but this terminal does not implement auto-allocation. | +| `glossary_exhausted` | `cp=auto` was requested and no free PUA codepoint was available (see §6.5). | ### 6.3 Overwrite and eviction @@ -276,6 +284,59 @@ Registrations live for the duration of the terminal session. A terminal reset (e.g. `ESC c`) MAY clear the entire glossary. Registrations MUST NOT persist across terminal restarts. +### 6.5 Auto-allocation (`cp=auto`) + +`cp=auto` asks the terminal to pick the codepoint on the client's +behalf. It exists so applications that do not care *which* PUA +slot their icon lands at — the common case — do not have to hand- +pick a codepoint, coordinate with other apps, or hardcode a range. + +**Allocation.** On `cp=auto` the terminal MUST allocate a codepoint +that: + +1. Is in one of the three PUA ranges defined in §4. +2. Is not currently registered in this session's glossary. + +Terminals SHOULD allocate from Supplementary PUA-B +(`U+100000`–`U+10FFFD`) by default, because that range has no +existing convention — no Nerd Font, Powerline, or Font Awesome +mapping — so the allocated codepoint is unambiguously the +registration's, not a system font's. The exact allocation strategy +(sequential, random, recycled on clear) is implementation-defined; +clients MUST NOT assume any particular ordering. + +**Reply.** The allocated codepoint is communicated to the client +via the `cp=` field of the success reply (§6.2). Because the +client has no other way to learn which codepoint was allocated, +`cp=auto` MUST produce a reply regardless of the `reply` parameter: + +- `reply=1` (default) — normal success/failure replies. No change. +- `reply=2` — the terminal MUST still emit the success reply for + `cp=auto` requests, because the allocated codepoint is carried + in it. For non-`auto` registrations in the same stream `reply=2` + still suppresses success ACKs as specified in §6.1. +- `reply=0` — the terminal MUST still emit the success reply for + `cp=auto` requests for the same reason. A client that genuinely + wants fire-and-forget registration cannot use `cp=auto`; it must + pick its own codepoint. + +**Allocation failure.** If every PUA codepoint in the terminal's +chosen allocation pool is already registered, the terminal MAY +either evict the oldest registration in FIFO order (as with +explicit `cp`, §6.3) and reuse that slot, or reject the request +with `reason=glossary_exhausted`. With the default 1024-slot cap +and ~64K codepoints in PUA-B alone, exhaustion is not reachable +in practice; the error code exists so non-default allocation +strategies (e.g. a small reserved window) remain conformant. + +**Unsupported.** Terminals that implement Glyph Protocol v1.4 or +earlier will not recognise `cp=auto` and will most likely reject +the request with `reason=malformed_payload` (the value fails hex +parsing). Terminals that understand `cp=auto` but choose not to +implement it SHOULD reject with `reason=auto_unsupported` so +clients can distinguish a protocol-level "no" from a payload +error and fall back to hand-picking a codepoint. + ## 7. Clear (`c`) ### 7.1 Request @@ -529,7 +590,9 @@ A terminal emulator is Glyph Protocol v1 conformant if it: defined in this specification, and advertises every accepted payload format via the `fmt=` bitfield in the `s` reply. 3. Restricts register/clear `cp` to the three PUA ranges; rejects - anything else with `reason=out_of_namespace`. + anything else with `reason=out_of_namespace`. Accepts `cp=auto` + on `r` and allocates a free PUA codepoint per §6.5, OR rejects + with `reason=auto_unsupported`. 4. Holds at most 1024 simultaneous registrations per session and evicts in FIFO order when full. 5. Accepts the `glyf` simple-glyph subset defined in §8.2. The @@ -660,3 +723,4 @@ rather than serving a stale bitmap. | 2026-04-19 | v1.2 | Added `fmt=colrv0` and `fmt=colrv1` payload formats wrapping OpenType `COLR` / `CPAL` tables with sidecar `glyf` outlines. Both advertised via bits 1 and 2 of the `s` reply's `fmt=` bitfield. | | 2026-04-19 | v1.3 | Added `reply=0|1|2` parameter to the `r` verb so bulk registrations can suppress success ACKs (`reply=2`) or go fully fire-and-forget (`reply=0`). Default `reply=1` preserves v1.0 behaviour. | | 2026-04-19 | v1.4 | Raised the glossary capacity from 256 to 1024 simultaneous registrations per session, and raised the `n_glyphs` cap in `fmt=colrv0`/`colrv1` containers from 256 to 1024 outlines. Both bumps quadruple the worst-case memory footprint; the 64 KiB per-payload cap is unchanged. | +| 2026-04-21 | v1.5 | Added `cp=auto` to the `r` verb: the terminal allocates a free PUA codepoint (SHOULD come from PUA-B) and echoes it in the success reply so the client can emit it. Added `reason=auto_unsupported` and `reason=glossary_exhausted` error codes. `cp=auto` forces a success reply regardless of `reply=0|2` because the allocated codepoint is only carried in the reply. | From 0cc3e5e8632bdb9eefc7dcd2cf4b325fe7e1051a Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Thu, 23 Apr 2026 23:26:20 +0200 Subject: [PATCH 5/7] v1.6 rollback on auto --- specs/glyph-protocol.md | 78 +++++------------------------------------ 1 file changed, 8 insertions(+), 70 deletions(-) diff --git a/specs/glyph-protocol.md b/specs/glyph-protocol.md index f0f16f5275..50b2dd5547 100644 --- a/specs/glyph-protocol.md +++ b/specs/glyph-protocol.md @@ -2,7 +2,7 @@ **Author:** Raphael Amorim **Year:** 2026 -**Last updated:** 2026-04-21 +**Last updated:** 2026-04-23 **See also:** - Blog post introducing the protocol and its rationale: @@ -201,18 +201,14 @@ For non-PUA codepoints only `0` and `1` are possible. ### 6.1 Request ``` -ESC _ 25a1 ; r ; cp= ; fmt=glyf ; reply=<0|1|2> ; upm= ; ESC \ +ESC _ 25a1 ; r ; cp= ; fmt=glyf ; reply=<0|1|2> ; upm= ; ESC \ ``` Parameters: -- `cp` — target codepoint in hex, OR the literal string `auto` to - request terminal-side auto-allocation. - - When hex: MUST be in one of the PUA ranges defined in §4. - Otherwise the request is rejected with `reason=out_of_namespace`. - - When `auto`: the terminal picks a free PUA codepoint, registers - the payload there, and echoes the allocated codepoint in the - reply's `cp` field. See §6.5. +- `cp` — target codepoint in hex. MUST be in one of the PUA ranges + defined in §4. Otherwise the request is rejected with + `reason=out_of_namespace`. - `fmt` — payload format. One of `glyf`, `colrv0`, `colrv1`. Optional; `glyf` is the default. See §8 for each format's wire layout. @@ -240,9 +236,7 @@ For `reply=1` (the default), successful registrations emit: ESC _ 25a1 ; r ; cp= ; status=0 ESC \ ``` -`cp` is echoed from the request. When the request used `cp=auto`, -the reply's `cp` is the hex codepoint the terminal allocated (never -the literal `auto`), so the client can emit it. +`cp` is echoed from the request. Failures, when not suppressed by `reply=0`, emit: @@ -263,8 +257,6 @@ Defined error codes: | `hinting_unsupported` | Payload contains hinting instructions. | | `malformed_payload` | Payload failed to parse as `glyf`. | | `payload_too_large` | Payload exceeds 64 KiB post-base64-decode. | -| `auto_unsupported` | `cp=auto` was requested but this terminal does not implement auto-allocation. | -| `glossary_exhausted` | `cp=auto` was requested and no free PUA codepoint was available (see §6.5). | ### 6.3 Overwrite and eviction @@ -284,59 +276,6 @@ Registrations live for the duration of the terminal session. A terminal reset (e.g. `ESC c`) MAY clear the entire glossary. Registrations MUST NOT persist across terminal restarts. -### 6.5 Auto-allocation (`cp=auto`) - -`cp=auto` asks the terminal to pick the codepoint on the client's -behalf. It exists so applications that do not care *which* PUA -slot their icon lands at — the common case — do not have to hand- -pick a codepoint, coordinate with other apps, or hardcode a range. - -**Allocation.** On `cp=auto` the terminal MUST allocate a codepoint -that: - -1. Is in one of the three PUA ranges defined in §4. -2. Is not currently registered in this session's glossary. - -Terminals SHOULD allocate from Supplementary PUA-B -(`U+100000`–`U+10FFFD`) by default, because that range has no -existing convention — no Nerd Font, Powerline, or Font Awesome -mapping — so the allocated codepoint is unambiguously the -registration's, not a system font's. The exact allocation strategy -(sequential, random, recycled on clear) is implementation-defined; -clients MUST NOT assume any particular ordering. - -**Reply.** The allocated codepoint is communicated to the client -via the `cp=` field of the success reply (§6.2). Because the -client has no other way to learn which codepoint was allocated, -`cp=auto` MUST produce a reply regardless of the `reply` parameter: - -- `reply=1` (default) — normal success/failure replies. No change. -- `reply=2` — the terminal MUST still emit the success reply for - `cp=auto` requests, because the allocated codepoint is carried - in it. For non-`auto` registrations in the same stream `reply=2` - still suppresses success ACKs as specified in §6.1. -- `reply=0` — the terminal MUST still emit the success reply for - `cp=auto` requests for the same reason. A client that genuinely - wants fire-and-forget registration cannot use `cp=auto`; it must - pick its own codepoint. - -**Allocation failure.** If every PUA codepoint in the terminal's -chosen allocation pool is already registered, the terminal MAY -either evict the oldest registration in FIFO order (as with -explicit `cp`, §6.3) and reuse that slot, or reject the request -with `reason=glossary_exhausted`. With the default 1024-slot cap -and ~64K codepoints in PUA-B alone, exhaustion is not reachable -in practice; the error code exists so non-default allocation -strategies (e.g. a small reserved window) remain conformant. - -**Unsupported.** Terminals that implement Glyph Protocol v1.4 or -earlier will not recognise `cp=auto` and will most likely reject -the request with `reason=malformed_payload` (the value fails hex -parsing). Terminals that understand `cp=auto` but choose not to -implement it SHOULD reject with `reason=auto_unsupported` so -clients can distinguish a protocol-level "no" from a payload -error and fall back to hand-picking a codepoint. - ## 7. Clear (`c`) ### 7.1 Request @@ -590,9 +529,7 @@ A terminal emulator is Glyph Protocol v1 conformant if it: defined in this specification, and advertises every accepted payload format via the `fmt=` bitfield in the `s` reply. 3. Restricts register/clear `cp` to the three PUA ranges; rejects - anything else with `reason=out_of_namespace`. Accepts `cp=auto` - on `r` and allocates a free PUA codepoint per §6.5, OR rejects - with `reason=auto_unsupported`. + anything else with `reason=out_of_namespace`. 4. Holds at most 1024 simultaneous registrations per session and evicts in FIFO order when full. 5. Accepts the `glyf` simple-glyph subset defined in §8.2. The @@ -724,3 +661,4 @@ rather than serving a stale bitmap. | 2026-04-19 | v1.3 | Added `reply=0|1|2` parameter to the `r` verb so bulk registrations can suppress success ACKs (`reply=2`) or go fully fire-and-forget (`reply=0`). Default `reply=1` preserves v1.0 behaviour. | | 2026-04-19 | v1.4 | Raised the glossary capacity from 256 to 1024 simultaneous registrations per session, and raised the `n_glyphs` cap in `fmt=colrv0`/`colrv1` containers from 256 to 1024 outlines. Both bumps quadruple the worst-case memory footprint; the 64 KiB per-payload cap is unchanged. | | 2026-04-21 | v1.5 | Added `cp=auto` to the `r` verb: the terminal allocates a free PUA codepoint (SHOULD come from PUA-B) and echoes it in the success reply so the client can emit it. Added `reason=auto_unsupported` and `reason=glossary_exhausted` error codes. `cp=auto` forces a success reply regardless of `reply=0|2` because the allocated codepoint is only carried in the reply. | +| 2026-04-23 | v1.6 | Removed `cp=auto` from the `r` verb (introduced in v1.5). Auto-allocation forced a stateful round-trip reply the client depended on to learn its codepoint, which recording tools like `asciinema` and `tee` cannot capture or replay — making `cp=auto` output impossible to reproduce from a transcript. Clients must pick their own PUA codepoint. The `auto_unsupported` and `glossary_exhausted` error codes are withdrawn. | From 7c34d58a4dbee891c97482b2ca10f926e0bd6d49 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Fri, 1 May 2026 19:30:21 +0200 Subject: [PATCH 6/7] placement support (#1554) --- specs/glyph-protocol.md | 142 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 6 deletions(-) diff --git a/specs/glyph-protocol.md b/specs/glyph-protocol.md index 50b2dd5547..db02de4486 100644 --- a/specs/glyph-protocol.md +++ b/specs/glyph-protocol.md @@ -201,7 +201,7 @@ For non-PUA codepoints only `0` and `1` are possible. ### 6.1 Request ``` -ESC _ 25a1 ; r ; cp= ; fmt=glyf ; reply=<0|1|2> ; upm= ; ESC \ +ESC _ 25a1 ; r ; cp= ; fmt=glyf ; reply=<0|1|2> ; upm= ; aw= ; lh= ; width=<1|2> ; size= ; align=, ; pad=,,, ; ESC \ ``` Parameters: @@ -225,6 +225,28 @@ Parameters: Unknown values fall back to `reply=1`. - `upm` — units per em, the coordinate space the outline is authored in. Optional; default `1000`. +- `aw` — authored advance width, in upm units. The intended + horizontal extent of the glyph, NOT the outline's bounding box. + Optional; default `upm`. +- `lh` — authored line height, in upm units. The intended vertical + extent (descender-to-ascender), NOT the outline's bounding box. + Optional; default `upm`. +- `width` — the codepoint's Unicode width, in the `wcwidth` / + UAX #11 sense. One of `1` (narrow) or `2` (wide). Optional; + default `1`. Authoritative for all terminal layout decisions + (cursor advance, wrapping, selection geometry), overriding the + codepoint's UAX #11 East Asian Width (all PUA ranges are + Ambiguous by default). +- `size` — scale policy. One of `height`, `advance`, `contain`, + `cover`, `stretch`. Optional; default `height`. See §8.5. +- `align` — placement of the scaled outline within the render span, + as a comma-separated pair `,`. `` is one of `start`, + `center`, `end`; `` is one of `start`, `center`, `end`, + `baseline`. Optional; default `center,center`. See §8.5. +- `pad` — insets from the render span edges, as comma-separated + fractions `,,,` (each `0.0`–`1.0`; + top/bottom are fractions of cell height, left/right of render + span width). Optional; default `0,0,0,0`. See §8.5. - payload — base64-encoded payload for the declared `fmt`. ### 6.2 Response @@ -359,12 +381,115 @@ TrueType semantics: current foreground color. For colored icons see the `colrv0` and `colrv1` formats in §8.6 / §8.7. -### 8.5 Scaling +### 8.5 Sizing, placement, and coordinate convention + +Every registered outline passes through three transforms at render +time, in order: **pad** (compute the effective render span), +**size** (pick scale factors), **align** (position the scaled +outline within the span). + +#### 8.5.1 Coordinate convention + +Outlines are authored in `upm`-unit space, Y-up, with `y=0` at the +baseline. The authored extent is the rectangle `[0, aw] × [y_min, +y_max]` where `lh = y_max − y_min` (matching OpenType line height: +descender to ascender, with descender ≤ 0). Points outside this +rectangle are allowed — they clip or overflow per the `size`/align` +rules below. + +#### 8.5.2 Render span and padding + +The render span is the rectangle of pixels the outline is scaled +into. Before scaling, the span is: + +``` +W = width × cell_width_px (cell_width_px from the terminal) +H = cell_height_px +``` + +`pad=,,,` shrinks the span: + +``` +W' = W × (1 − l − r) +H' = H × (1 − t − b) +``` + +Padding values are fractions. If `l + r ≥ 1` or `t + b ≥ 1` the +terminal MUST treat the request as if `pad=0,0,0,0` (no padding) +— a degenerate span is not useful and suggests a client bug. The +effective span `W' × H'` is what `size` and `align` operate on. + +#### 8.5.3 Size modes + +Given the authored extent `aw × lh` and effective span `W' × H'`: + +| `size` | `sx` | `sy` | Aspect ratio | Notes | +|------------|-------------------|-------------------|--------------|-------| +| `height` | `H' / lh` | `H' / lh` | Preserved | Default. Line-height drives. Horizontal extent is whatever `aw` scales to; may overflow the span. | +| `advance` | `W' / aw` | `W' / aw` | Preserved | Advance drives. Vertical extent is whatever `lh` scales to; may overflow. | +| `contain` | `min(W'/aw, H'/lh)` | same | Preserved | Outline fits entirely inside the span on both axes. May leave empty space on one axis. | +| `cover` | `max(W'/aw, H'/lh)` | same | Preserved | Outline fills the span on both axes. May overflow on one axis. | +| `stretch` | `W' / aw` | `H' / lh` | Not preserved | Each axis independent. Useful for box-drawing. | + +`height` is the default because it matches how characters behave +(the terminal's line-height maps to the cell's vertical pixels, +the horizontal footprint is what the glyph's own advance dictates). +For icons that must stay inside the cell regardless of aspect, +prefer `contain`. + +#### 8.5.4 Alignment + +After scaling, the outline has a scaled authored extent `(aw×sx) +× (lh×sy)` positioned somewhere within the effective span. `align` +picks where. + +Horizontal: + +- `start` — outline's `x=0` aligns with the span's left edge + (`pad_left`). +- `center` — outline's horizontal midpoint aligns with the span's + horizontal midpoint. +- `end` — outline's `x=aw` aligns with the span's right edge + (`W − pad_right`). + +Vertical (Y-up): + +- `start` — outline's `y=y_min` aligns with the span's bottom edge + (`pad_bottom`). +- `center` — outline's vertical midpoint aligns with the span's + vertical midpoint. +- `end` — outline's `y=y_max` aligns with the span's top edge + (`H − pad_top`). +- `baseline` — outline's `y=0` aligns with the terminal's text + baseline within the cell. Preferred for character-like glyphs + that must sit on the same baseline as surrounding text; + descenders extend below the baseline naturally. + +When `size=stretch`, the scaled extent exactly matches the span on +both axes, so `align` has no visible effect. When `size=cover`, +the scaled extent overflows on one axis; `align` picks which edge +to anchor (and therefore which overflow is visible vs. clipped). +When `size=contain`/`height`/`advance`, the scaled extent may be +smaller than the span on at least one axis; `align` picks where +the empty space goes. + +#### 8.5.5 Resolution independence -The `upm` value defines the glyph's authoring coordinate space. -The terminal maps that space onto its cell at render time. Applications MUST NOT assume a particular cell size and MUST NOT -re-register glyphs on font size change. +re-register glyphs on font size change. All scaling is computed +at render time from the parameters above and the terminal's +current cell metrics. + +#### 8.5.6 Coordinated sets (no scale groups) + +There is no `group` parameter. A set of glyphs that must visually +align — spinner frames, progress-bar steps, a multi-glyph logo — +aligns automatically if authored with identical `aw`, `lh`, `size`, +`align`, and `pad`, and if their outline geometry is coordinated +(e.g. all spinner frames sized inside a common bounding circle). +Scale-group semantics can be added later if authoring experience +shows they are genuinely needed; for v1 the burden sits with +the author, not the protocol. ### 8.6 Payload format: `colrv0` @@ -539,7 +664,11 @@ A terminal emulator is Glyph Protocol v1 conformant if it: color; renders `colrv0`/`colrv1` glyphs using the COLR paint graph, resolving palette index `0xFFFF` to the current foreground color. -7. Scales glyphs according to `upm` and the current cell size. +7. Scales and positions glyphs according to `upm`, `aw`, `lh`, + `width`, `size`, `align`, and `pad` as specified in §8.5, and + treats the registered codepoint as having the declared `width` + (`1` or `2`) for every layout decision, overriding the + codepoint's UAX #11 East Asian Width. 8. Enforces the cell-buffer authority invariant in §9: selection, copy, and search return the raw codepoint. 9. Ignores unrecognized parameters rather than failing the @@ -662,3 +791,4 @@ rather than serving a stale bitmap. | 2026-04-19 | v1.4 | Raised the glossary capacity from 256 to 1024 simultaneous registrations per session, and raised the `n_glyphs` cap in `fmt=colrv0`/`colrv1` containers from 256 to 1024 outlines. Both bumps quadruple the worst-case memory footprint; the 64 KiB per-payload cap is unchanged. | | 2026-04-21 | v1.5 | Added `cp=auto` to the `r` verb: the terminal allocates a free PUA codepoint (SHOULD come from PUA-B) and echoes it in the success reply so the client can emit it. Added `reason=auto_unsupported` and `reason=glossary_exhausted` error codes. `cp=auto` forces a success reply regardless of `reply=0|2` because the allocated codepoint is only carried in the reply. | | 2026-04-23 | v1.6 | Removed `cp=auto` from the `r` verb (introduced in v1.5). Auto-allocation forced a stateful round-trip reply the client depended on to learn its codepoint, which recording tools like `asciinema` and `tee` cannot capture or replay — making `cp=auto` output impossible to reproduce from a transcript. Clients must pick their own PUA codepoint. The `auto_unsupported` and `glossary_exhausted` error codes are withdrawn. | +| 2026-04-23 | v1.7 | Added a sizing and placement model to the `r` verb: `aw` / `lh` (authored extent in upm units), `width` (Unicode/wcwidth width, `1` or `2`, authoritative), `size` (`height`/`advance`/`contain`/`cover`/`stretch`), `align` (`,` positioning after scale, with `v=baseline` for character-like glyphs), and `pad` (fractional insets from the render span). Pinned the coordinate convention: Y-up, `y=0` at baseline, `lh` measured descender-to-ascender (OpenType). Scale groups are intentionally omitted — coordinated sets align via matching parameters and outline geometry. | From 97f12ab2950ca9fd16ad1630bd66339be6117a2f Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Sun, 3 May 2026 21:47:02 +0200 Subject: [PATCH 7/7] update protocol for more declarative --- specs/glyph-protocol.md | 83 +++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/specs/glyph-protocol.md b/specs/glyph-protocol.md index db02de4486..a582360688 100644 --- a/specs/glyph-protocol.md +++ b/specs/glyph-protocol.md @@ -2,7 +2,7 @@ **Author:** Raphael Amorim **Year:** 2026 -**Last updated:** 2026-04-23 +**Last updated:** 2026-05-03 **See also:** - Blog post introducing the protocol and its rationale: @@ -108,7 +108,10 @@ ESC _ 25a1 ; [ ; key=value ]* [ ; ] ESC \ Parameter keys use lowercase ASCII. Values are lowercase hex for codepoints, decimal for integers, base64 for binary payloads, and -decimal u8 values for the `status` field of every response. +lowercase ASCII names for enums (single value, or comma-separated +for lists). The `status` field's shape is verb-specific: `r` and +`c` return a `u8` (`0` = success, nonzero paired with `reason=`); +`q` returns a comma-separated coverage list (§5.2). ### 3.3 Verbs @@ -119,23 +122,33 @@ decimal u8 values for the `status` field of every response. | `r` | Register a glyph for a PUA codepoint. | | `c` | Clear one slot or every slot in this session's glossary. | -The `s` verb takes no parameters and returns a decimal `u8` -bitfield under the `fmt` key: +The `s` verb takes no parameters. The reply lists the terminal's +supported payload formats in the `fmt` key as a comma-separated +list of lowercase ASCII names, with no surrounding whitespace: -| Bit | `fmt=` value | Format name | -|-----|--------------|-------------| -| 0 | 1 | `glyf` (monochrome simple-glyph; §8). | -| 1 | 2 | `colrv0` (layered flat colour; §8.6). | -| 2 | 4 | `colrv1` (OpenType paint graph; §8.7). | +``` +ESC _ 25a1 ; s ; fmt=glyf,colrv0,colrv1 ESC \ +``` -Further bits are reserved. Clients treat unknown bits as -unsupported and ignore them. A terminal that advertises only -`fmt=1` (monochrome) and receives an `r` with `fmt=colrv0` / -`fmt=colrv1` MUST reject the registration (`reason=malformed_payload` -is acceptable for parser-level rejection). Clients SHOULD check -the `s` reply before emitting colour registrations so they can -fall back to a monochrome `fmt=glyf` without making a doomed -round-trip. +Format names defined in v1.8: + +| Name | Format | +|----------|----------------------------------------------------------| +| `glyf` | Monochrome simple-glyph (§8). | +| `colrv0` | Layered flat colour, OpenType `COLR` v0 + `CPAL` (§8.6). | +| `colrv1` | OpenType paint graph, `COLR` v1 (§8.7). | + +Order is not significant; clients MUST treat the value as a set. +An empty `fmt=` value means the terminal recognises Glyph Protocol +but currently advertises no payload formats — every `r` will be +rejected. Clients MUST ignore names they do not recognise rather +than failing the reply, so future format names are forward- +compatible. A terminal that advertises only `fmt=glyf` and +receives an `r` with `fmt=colrv0` / `fmt=colrv1` MUST reject the +registration (`reason=malformed_payload` is acceptable for +parser-level rejection). Clients SHOULD check the `s` reply +before emitting colour registrations so they can fall back to a +monochrome `fmt=glyf` without making a doomed round-trip. ## 4. Glossary namespace @@ -180,21 +193,23 @@ Parameters: ### 5.2 Response ``` -ESC _ 25a1 ; q ; cp= ; status= ESC \ +ESC _ 25a1 ; q ; cp= ; status= ESC \ ``` -`status` is a decimal u8 encoding a two-bit field: - -| Value | State | Meaning | -|-------|-------------|---------| -| `0` | `free` | No font in the fallback chain renders `cp`, and the glossary has no registration for it. The cell will render as tofu. | -| `1` | `system` | Some font in the fallback chain renders `cp`. No glossary registration (or `cp` is outside PUA). | -| `2` | `glossary` | `cp` is in PUA and has a live registration in this session. No system font covers it. | -| `3` | `both` | `cp` is in PUA, has a live registration, AND a system font also covers it. The registration shadows the system font at render time. | +`status` is a comma-separated list of coverage names — the set of +sources that can render `cp` in this session. Order is not +significant; clients MUST treat the value as a set. -Bit 0 = system coverage, bit 1 = glossary coverage. +| `status=` value | Meaning | +|---------------------|---------| +| (empty) | No font in the fallback chain renders `cp`, and the glossary has no registration for it. The cell will render as tofu. | +| `system` | Some font in the fallback chain renders `cp`. No glossary registration (or `cp` is outside PUA). | +| `glossary` | `cp` is in PUA and has a live registration in this session. No system font covers it. | +| `system,glossary` | `cp` is in PUA, has a live registration, AND a system font also covers it. The registration shadows the system font at render time. | -For non-PUA codepoints only `0` and `1` are possible. +For non-PUA codepoints only the empty value and `status=system` +are possible. Clients MUST ignore unknown coverage names so +future sources are forward-compatible. ## 6. Register (`r`) @@ -651,15 +666,15 @@ A terminal emulator is Glyph Protocol v1 conformant if it: 1. Recognizes the `25a1` identifier in APC sequences. 2. Implements the `s`, `q`, `r`, and `c` verbs with the semantics - defined in this specification, and advertises every accepted - payload format via the `fmt=` bitfield in the `s` reply. + defined in this specification, and lists every accepted payload + format by name in the `fmt=` value of the `s` reply. 3. Restricts register/clear `cp` to the three PUA ranges; rejects anything else with `reason=out_of_namespace`. 4. Holds at most 1024 simultaneous registrations per session and evicts in FIFO order when full. 5. Accepts the `glyf` simple-glyph subset defined in §8.2. The `colrv0` and `colrv1` formats are OPTIONAL; terminals that - accept them MUST set the corresponding bit in the `s` reply. + accept them MUST list the corresponding name in the `s` reply. 6. Renders registered `glyf` glyphs in the current foreground color; renders `colrv0`/`colrv1` glyphs using the COLR paint graph, resolving palette index `0xFFFF` to the current @@ -725,8 +740,8 @@ def q(cp: int) -> None: # Does the user already have Nerd Fonts installed? q(0xE0A0) # Expected reply parsed from the PTY: -# status=1 → system font covers it; don't register, just emit cp -# status=0 → nothing covers it; register and emit +# status=system → system font covers it; don't register, just emit cp +# status= → empty list (nothing covers it); register and emit ``` ## Appendix C. Implementation notes @@ -792,3 +807,5 @@ rather than serving a stale bitmap. | 2026-04-21 | v1.5 | Added `cp=auto` to the `r` verb: the terminal allocates a free PUA codepoint (SHOULD come from PUA-B) and echoes it in the success reply so the client can emit it. Added `reason=auto_unsupported` and `reason=glossary_exhausted` error codes. `cp=auto` forces a success reply regardless of `reply=0|2` because the allocated codepoint is only carried in the reply. | | 2026-04-23 | v1.6 | Removed `cp=auto` from the `r` verb (introduced in v1.5). Auto-allocation forced a stateful round-trip reply the client depended on to learn its codepoint, which recording tools like `asciinema` and `tee` cannot capture or replay — making `cp=auto` output impossible to reproduce from a transcript. Clients must pick their own PUA codepoint. The `auto_unsupported` and `glossary_exhausted` error codes are withdrawn. | | 2026-04-23 | v1.7 | Added a sizing and placement model to the `r` verb: `aw` / `lh` (authored extent in upm units), `width` (Unicode/wcwidth width, `1` or `2`, authoritative), `size` (`height`/`advance`/`contain`/`cover`/`stretch`), `align` (`,` positioning after scale, with `v=baseline` for character-like glyphs), and `pad` (fractional insets from the render span). Pinned the coordinate convention: Y-up, `y=0` at baseline, `lh` measured descender-to-ascender (OpenType). Scale groups are intentionally omitted — coordinated sets align via matching parameters and outline geometry. | +| 2026-05-03 | v1.8 | Replaced the `s` reply's `u8` bitfield with a comma-separated list of format names (e.g. `fmt=glyf,colrv0,colrv1`). Names extend without bit-collision worries and stay readable in transcripts; an empty `fmt=` means the terminal advertises no payload formats. Unknown names MUST be ignored by clients, so future formats are forward-compatible. | +| 2026-05-03 | v1.9 | Replaced the `q` reply's `u8` two-bit `status` field with a comma-separated list of coverage names: `status=system`, `status=glossary`, `status=system,glossary`, or empty for "free". Same motivation as v1.8 for the `s` reply. The `r` and `c` replies still use `status=` for success/failure since that's a closed boolean, not an extensible set. |