# Glyph Protocol

**Author:** Raphael Amorim
**Year:** 2026
**Last updated:** 2026-05-03

**See also:**
- Blog post introducing the protocol and its rationale:
  <https://rapha.land/introducing-glyph-protocol-for-terminals/>
- Reference implementation: [Rio terminal](https://raphamorim.io/rio)
- Example apps (ratatui, bubbletea v2, ink) registering real Nerd Font
  outlines and colrv1/colrv0 emojis 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 — 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
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.** 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
  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
  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 50–400 bytes of
  `glyf`, 2–4× 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 ; <verb> [ ; key=value ]* [ ; <payload> ] ESC \
```

Parameter keys use lowercase ASCII. Values are lowercase hex for
codepoints, decimal for integers, base64 for binary payloads, and
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

| 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. 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:

```
ESC _ 25a1 ; s ; fmt=glyf,colrv0,colrv1 ESC \
```

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

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=<hex> 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=<hex> ; status=<list> ESC \
```

`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.

| `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 the empty value and `status=system`
are possible. Clients MUST ignore unknown coverage names so
future sources are forward-compatible.

## 6. Register (`r`)

### 6.1 Request

```
ESC _ 25a1 ; r ; cp=<hex> ; fmt=glyf ; reply=<0|1|2> ; upm=<int> ; aw=<int> ; lh=<int> ; width=<1|2> ; size=<mode> ; align=<h>,<v> ; pad=<t>,<r>,<b>,<l> ; <base64-payload> 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`.
- `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 `<h>,<v>`. `<h>` is one of `start`,
  `center`, `end`; `<v>` 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 `<top>,<right>,<bottom>,<left>` (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

Replies are gated by the request's `reply` parameter (§6.1).
For `reply=1` (the default), successful registrations emit:

```
ESC _ 25a1 ; r ; cp=<hex> ; status=0 ESC \
```

`cp` is echoed from the request.

Failures, when not suppressed by `reply=0`, emit:

```
ESC _ 25a1 ; r ; cp=<hex> ; status=<nonzero u8> ; reason=<code> 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=<hex> ] 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** (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** (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.

### 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 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=<t>,<r>,<b>,<l>` 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

Applications MUST NOT assume a particular cell size and MUST NOT
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`

`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.

## 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 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 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
   foreground color.
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
   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=system → system font covers it; don't register, just emit cp
#   status=       → empty list (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. |
| 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` (`<h>,<v>` 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=<u8>` for success/failure since that's a closed boolean, not an extensible set. |
