^[control-codes
live in libghostty-vt

Proposals

APC 25a1 Glyph Protocol

View as Markdown

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.

APC 25a1 ; verb ; key=value … ; payload ST
Specification
Glyph Protocol, v1.11

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 of the Rio repository. Its author is Raphael Amorim, and 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, 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, 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.

VerbAsks the terminal to
slist the payload formats it accepts; any answer at all shows the protocol is supported
qsay what would draw a code point
rregister a glyph for a Private Use Area code point
cclear 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:

RangePlaneCommonly used by
U+E000 to U+F8FF0Nerd Fonts, Powerline, Font Awesome
U+F0000 to U+FFFFD15Nerd Fonts’ Material Design icons
U+100000 to U+10FFFD16nothing 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. 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:

statusMeaning
emptyNothing: no font has it and nothing is registered, so it would be drawn as a missing glyph
systemA font in the terminal’s fallback chain has it, and nothing is registered for it
glossaryA registration in this session draws it, and no font has it
system,glossaryBoth; 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:

KeyValueMeaning
cphex, requiredThe code point, which must be in a Private Use Area
fmtglyf, colrv0 or colrv1The payload’s format; default glyf
reply0, 1 or 21, the default, answers success and failure; 2 only failure; 0 nothing at all. Any other value counts as 1
upmintegerUnits per em, the coordinate space the outline is drawn in; default 1000
awintegerThe intended advance width in those units, not the outline’s bounding box; default upm
lhintegerThe intended height from descender to ascender, in those units; default upm
width1 or 2How many cells the glyph is drawn across; default 1
sizeheight, advance, contain, cover or stretchHow it is scaled to the cell; default height
alignh,vWhere it sits once scaled: h is start, center or end, v is those or baseline; default center,center
padt,r,b,lInsets 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:

reasonMeaning
out_of_namespacecp is not in a Private Use Area
composite_unsupportedThe outline is a composite glyph
hinting_unsupportedThe outline carries hinting instructions
malformed_payloadThe payload does not parse, or is in a format the terminal did not list for s
payload_too_largeThe payload is over 64 KiB decoded from base64
outline_too_largeThe 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:

sizeScaled so thatAspect ratio
heightlh fills the span’s heightkept; the width may overflow
advanceaw fills the span’s widthkept; the height may overflow
containthe whole extent fits inside the spankept
coverthe extent covers the whole spankept; one axis overflows
stretcheach axis fills the span independentlynot 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

fmtWhat it isRequired
glyfOne TrueType simple glyph, drawn in the foreground coloryes
colrv0Layers of flat color: OpenType COLR version 0 and CPAL, with the outlines they useno
colrv1OpenType COLR version 1, a graph of paints with gradients, transforms and compositing, with the outlines it usesno

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.

Every example on these pages runs in your browser, in libghostty-vt compiled to WebAssembly.