# Glyph Protocol (APC 25a1)

> A proposal for a program to send its terminal vector glyphs for Private Use Area code points, so that icons render without a patched font.

- **Sequence:** `APC 25a1 ; verb ; key=value … ; payload ST`
- **Specification:** [Glyph Protocol, v1.11](https://github.com/raphamorim/rio/blob/1eb41306831c8a02b26c2b901fac82b8874eb3c6/specs/glyph-protocol.md)

The *Glyph Protocol* lets a program send its terminal the outlines of the
icons it wants to draw, instead of relying on the user to have installed a
patched font, such as one of the Nerd Fonts, that happens to put those icons
at the code points the program uses. The program picks a code point in one
of Unicode's Private Use Areas, registers a vector glyph for it, and from
then on that code point is drawn with the registered glyph in that terminal
session. The program can also ask whether a code point would render at all
before it uses one. Glyphs are vector, in the outline formats OpenType fonts
already use, so they scale to any cell size, and the terminal draws them
with the rasterizer it already has for text.

This summarizes version 1.11 of the specification, dated 2026-08-17, as it
stood at
[commit `1eb4130`](https://github.com/raphamorim/rio/tree/1eb41306831c8a02b26c2b901fac82b8874eb3c6/specs)
of the Rio repository. Its author is Raphael Amorim, and
[Rio](https://raphamorim.io/rio) is its reference implementation. Its
change log runs from v1 of 2026-04-17; the two latest revisions add a limit
on how large an outline may become once decoded, asked for by Mitchell
Hashimoto while implementing the protocol in Ghostty, and make `width` a
matter of drawing only, as described below.

libghostty-vt implements it as of
[0f171f6](https://github.com/ghostty-org/ghostty/tree/0f171f650a1cbbd6df4a2573b6b0c7de57cc6d90),
in `src/terminal/apc/glyph.zig` and the files beside it. It is compiled in
and turned on by default, and a host can switch it off with the
`GHOSTTY_TERMINAL_OPT_GLYPH_PROTOCOL` option, which also empties the
glossary. It accepts `glyf` only, and its answer to `q` reports only its own
registrations, since it has no fonts; a host that has them is expected to
add `system` itself. A full reset empties the glossary. Its doc comment
still describes `width` as setting the code point's cell width, the v1.7
wording, and cites an earlier revision of the specification.

## The sequence

```
APC 25a1 ; verb [ ; key=value ]… [ ; payload ] ST
```

The protocol travels in an [APC](https://control-codes.page/esc/sos/index.html.md), `ESC _`, rather than an OSC,
on the grounds that a terminal must already ignore an APC it does not
implement. Every message begins with `25a1`, the hexadecimal for U+25A1
WHITE SQUARE, the box a missing glyph is often drawn as, and a terminal
ignores an APC whose content does not begin with it. Keys are lower-case
ASCII. Code points are written in lower-case hexadecimal, integers in
decimal, binary payloads in base64, and choices as lower-case names,
several separated by commas. The answers come back in the same framing.

| Verb | Asks the terminal to |
| --- | --- |
| `s` | list the payload formats it accepts; any answer at all shows the protocol is supported |
| `q` | say what would draw a code point |
| `r` | register a glyph for a Private Use Area code point |
| `c` | clear one registration, or all of them |

A program finds out whether the protocol is there by sending `s` and
waiting; no answer within its own timeout means it is not.

```
APC 25a1 ; s ST                               asked by the program
APC 25a1 ; s ; fmt=glyf,colrv0,colrv1 ST      the answer
```

`fmt` is a set, in no particular order, and a program ignores names it
does not know. An empty `fmt=` means the terminal knows the protocol but
will currently refuse every registration.

## The glossary

Each terminal session, a tab, a pane or a pseudo-terminal, has its own
*glossary* of registrations, and none of them may be seen from another
session. Registrations may only be made in the three Private Use Areas,
which no one types and no existing text contains:

| Range | Plane | Commonly used by |
| --- | --- | --- |
| U+E000 to U+F8FF | 0 | Nerd Fonts, Powerline, Font Awesome |
| U+F0000 to U+FFFFD | 15 | Nerd Fonts' Material Design icons |
| U+100000 to U+10FFFD | 16 | nothing in common use |

A glossary holds at most 1024 registrations, one per code point, however
many outlines a color glyph carries. When it is full, a registration for a
new code point evicts the oldest one, silently, so a program that cannot
afford to lose one should check with `q` before using it. Registering a
code point again replaces its glyph, which is how a program follows a
change of theme. Registrations last as long as the session, must not
survive a restart of the terminal, and may be cleared by
[RIS](https://control-codes.page/esc/ris/index.html.md). Whenever a registration goes, by `c`, replacement or
eviction, the terminal must forget anything it had drawn for it.

## Query

```
APC 25a1 ; q ; cp=hex ST
APC 25a1 ; q ; cp=hex ; status=list ST      the answer
```

Any code point may be asked about, inside the Private Use Areas or not.
`status` is a set of what can draw it:

| `status` | Meaning |
| --- | --- |
| empty | Nothing: no font has it and nothing is registered, so it would be drawn as a missing glyph |
| `system` | A font in the terminal's fallback chain has it, and nothing is registered for it |
| `glossary` | A registration in this session draws it, and no font has it |
| `system,glossary` | Both; the registration is what is drawn |

For a code point outside the Private Use Areas only an empty answer and
`system` are possible.

## Register

```
APC 25a1 ; r ; cp=hex [ ; key=value ]… ; payload ST
```

The payload is the base64 of the glyph, at most 64 KiB once decoded, and
the keys before it say what it is and how to fit it to a cell:

| Key | Value | Meaning |
| --- | --- | --- |
| `cp` | hex, required | The code point, which must be in a Private Use Area |
| `fmt` | `glyf`, `colrv0` or `colrv1` | The payload's format; default `glyf` |
| `reply` | `0`, `1` or `2` | `1`, the default, answers success and failure; `2` only failure; `0` nothing at all. Any other value counts as `1` |
| `upm` | integer | Units per em, the coordinate space the outline is drawn in; default `1000` |
| `aw` | integer | The intended advance width in those units, not the outline's bounding box; default `upm` |
| `lh` | integer | The intended height from descender to ascender, in those units; default `upm` |
| `width` | `1` or `2` | How many cells the glyph is drawn across; default `1` |
| `size` | `height`, `advance`, `contain`, `cover` or `stretch` | How it is scaled to the cell; default `height` |
| `align` | `h,v` | Where it sits once scaled: `h` is `start`, `center` or `end`, `v` is those or `baseline`; default `center,center` |
| `pad` | `t,r,b,l` | Insets from the edges as fractions of the cell; default `0,0,0,0` |

A terminal ignores keys it does not know. The answer is

```
APC 25a1 ; r ; cp=hex ; status=0 ST
APC 25a1 ; r ; cp=hex ; status=n ; reason=code ST
```

with `n` nonzero and `reason` one of these:

| `reason` | Meaning |
| --- | --- |
| `out_of_namespace` | `cp` is not in a Private Use Area |
| `composite_unsupported` | The outline is a composite glyph |
| `hinting_unsupported` | The outline carries hinting instructions |
| `malformed_payload` | The payload does not parse, or is in a format the terminal did not list for `s` |
| `payload_too_large` | The payload is over 64 KiB decoded from base64 |
| `outline_too_large` | The outlines would be over 64 KiB once their points are decoded |

`reply=0` exists for a program registering many glyphs as it starts, and
the specification warns of the alternative: an answer that arrives after
the program has stopped reading, typically to a `c` sent as it exits, is
read by the shell that takes over and shows up as text. Registrations end
with the session anyway, so a program is advised not to clear them on its
way out.

### Width

`width` affects only how the glyph is drawn. The code point still takes
the one cell that `wcwidth` gives every Private Use Area code point, so the
cursor, wrapping and selection agree with shells, line editors and
multiplexers that count columns for themselves. A `width=2` glyph is drawn
across its own cell and the one after it, and the program must leave that
next cell blank, by writing a space after the code point, for it to land
on. Version 1.7 had made `width` decide the cell width as well; 1.11 took
that back because it put the terminal's idea of the cursor position out of
step with the program's.

### Fitting the glyph to the cell

An outline is drawn in a space `upm` units to the em, with Y upward and
`y=0` on the baseline, and its intended extent is `aw` wide by `lh` high.
The terminal fits it to the cell in three steps each time it draws it.
First `pad` shrinks the span of `width` cells, unless the insets on either
axis add up to 1 or more, in which case they are ignored. Then `size`
scales the extent to the span:

| `size` | Scaled so that | Aspect ratio |
| --- | --- | --- |
| `height` | `lh` fills the span's height | kept; the width may overflow |
| `advance` | `aw` fills the span's width | kept; the height may overflow |
| `contain` | the whole extent fits inside the span | kept |
| `cover` | the extent covers the whole span | kept; one axis overflows |
| `stretch` | each axis fills the span independently | not kept |

Last, `align` places the result in the span: `start` is the left or bottom
edge, `end` the right or top, and `baseline` puts `y=0` on the terminal's
own baseline, for glyphs that should sit on a line of text. A program must
not assume a cell size or register its glyphs again when the font size
changes, since all of this is worked out again at drawing time. There is
no way to group glyphs that must line up, such as the frames of a spinner:
they line up if they are registered with the same `aw`, `lh`, `size`,
`align` and `pad` and drawn to match.

## Payloads

| `fmt` | What it is | Required |
| --- | --- | --- |
| `glyf` | One TrueType simple glyph, drawn in the foreground color | yes |
| `colrv0` | Layers of flat color: OpenType `COLR` version 0 and `CPAL`, with the outlines they use | no |
| `colrv1` | OpenType `COLR` version 1, a graph of paints with gradients, transforms and compositing, with the outlines it uses | no |

A `glyf` payload is a single glyph record from an OpenType `glyf` table,
restricted to the part of it that is plain data: a simple glyph, never a
composite of others, with no hinting instructions, which are the only
executable part of TrueType. Its points are counted before any room is
made for them: at 12 bytes a point, an outline over 64 KiB, or 5,461
points, is refused. Without that limit a 64 KiB payload made almost
entirely of repeat flags would decode to about 768 KiB, in every one of a
session's 1024 slots. The figure comes from Mitchell Hashimoto's survey
for Ghostty of Apple Symbols, Noto and the Nerd Fonts, whose largest glyph
decodes to about 40 KiB.

The two color formats share one container, with every integer big-endian:

```
u16  n_glyphs                 1 to 1024
     n_glyphs times:
u16    glyf_len
       glyf_len bytes         a simple glyph, as for fmt=glyf
u16  colr_len                 more than 0
     colr_len bytes           the COLR table
u16  cpal_len                 0 is allowed only for colrv1
     cpal_len bytes           the CPAL table
```

Glyph IDs in the `COLR` table index the outlines in the container, and
glyph 0 is the one drawn for the code point. Palette index `0xFFFF` is the
cell's foreground color, so a terminal that caches the drawn glyph must
draw it again when the foreground changes. Every outline must keep to the
`glyf` rules above, and all of them together to the same 64 KiB. A
terminal implementing `colrv1` should support every kind of paint, and may
draw one it does not support, such as a sweep gradient, with a reasonable
substitute, typically its first solid color, rather than refusing the
glyph.

## Clear

```
APC 25a1 ; c [ ; cp=hex ] ST
APC 25a1 ; c ; status=0 ST                              the answer
APC 25a1 ; c ; status=1 ; reason=out_of_namespace ST
```

With `cp` it clears that one registration, and without it the whole
glossary. Clearing a code point that has no registration still succeeds,
and a `cp` outside the Private Use Areas is refused.

## Security

The property the protocol sets out to keep is that **a program cannot
change how existing text looks**: it cannot register a glyph for `a` that
looks like `o`. That is why only the Private Use Areas may be registered.
Nothing a user types or reads elsewhere, a file name, a URL, a command,
contains those code points, so the worst a program can do is draw an odd
symbol at a code point it, or a program following the same convention,
chose to write. The cell keeps the code point that was written, and
selection, copying, searching, hyperlinks and anything else that reads the
text back must give that code point, never the glyph drawn for it.

Messages must not refer to files or load anything from disk. A session's
glossary is bounded by its 1024 slots and the two 64 KiB limits, and the
specification counts the limit on decoded outlines as the one that
matters: the `glyf` encoding is compressed, so the limit on the payload
alone bounds nothing worth bounding.

## What it leaves out

Version 1 leaves out, on purpose: any code point outside the Private Use
Areas; ligatures, or any glyph keyed by more than one code point; keeping
glyphs from one session to the next, or sharing them between programs;
bitmaps, including the bitmap and SVG color formats of OpenType, so that
every glyph stays resolution independent; and control over positioning
within a cell finer than `align` and `pad`. Composite glyphs and hinting
may be allowed by a later version.

---

This is the Markdown version of <https://control-codes.page/proposals/glyph/>. On that page every validation case runs live in libghostty-vt, the terminal emulation core of Ghostty, compiled to WebAssembly.
