Rich text specification (OSC 7404)
Mirrored from Docs/rich-text.md at product commit c989e6125e.
A program places rich text into a rectangle of cells: text in more than one family, size, weight, slant and colour, wrapped or cut to the rectangle, and selected and copied as text. Before it places anything it can ask what fits: how many characters per line and how many lines a style gets in a rectangle, or whether a given body fits and what extent it needs.
The body uses the Rich Text Format subset specified below. The escape’s keys carry the rectangle, overflow policy, default alignment, and block identifier; character and paragraph styles belong to the RTF body.
The sizing protocol (OSC 66, Docs/proportional-text.md) sets one run of one
face inside a row; a block is the shape for anything with more than one style
or more than one line.
The sequence
Section titled “The sequence”OSC 7404 ; <keys> ; <rtf> STThe 75xx numbers belong to another terminal’s protocol that Ubiquitty honours; Ubiquitty allocates in 74xx.
<keys> is a colon-separated list of key=value pairs; <rtf> is everything
after the second semicolon, up to the terminator, and may itself contain
semicolons. BEL terminates the code as ST does.
| key | values | default | meaning |
|---|---|---|---|
a |
p, q, d, s |
p |
Action: place, measure, delete, or subscribe to typography changes. |
w |
0–1024 | — | Width in cells. Required by p and q. 0 leaves the width open and is accepted only by q. |
h |
0–1024 | — | Height in rows. Required by p and q. 0 leaves the height open and is accepted only by q. |
i |
0–65535 | 0 | The block’s id for p and d; the request id a reply echoes for q. |
o |
w, c, e |
w |
Overflow: wrap, clip, or wrap and end with an ellipsis. |
j |
l, c, r |
l |
Alignment of a paragraph that names none in the body. |
C |
0, 1 | 0 | 1 leaves the cursor where it was after a placement. |
g |
0, 1 | 0 | 1 uses the terminal row height and baseline; \fs24 follows the configured font size for its family, including zoom. Proportional families use the proportional size; \fmodern uses the grid font’s size. |
M |
0, 1 | 0 | 1 reports shaped character positions through OSC 7405; requires a nonzero id. |
P |
0, 1 | 0 | 1 retains an application overlay across ordinary cell writes. Full screen erase and leaving the alternate screen remove it. |
k |
0–4096 | absent | Logical scalar insertion offset of the native terminal caret inside the block. |
v |
0–4096 | 0 | Skip this many content rows before drawing, for a scrolled viewport. Requires g=1. |
p |
N or T,R,B,L, each 0–255 |
0 | Padding in points: the content insets of p and q, one value for every side or four in the order top, right, bottom, left. Any other count, or a value past 255, refuses the code. |
y |
t, c, b |
t |
Where content that needs fewer rows than a placement covers sits in its content box: top, centre or bottom. A query accepts it and answers as it would without it. |
r |
x,y,width,height,... |
full rectangle | Up to 32 disjoint visible rectangles relative to the placement, in cells. Placement only; each must be positive and inside w×h. |
G |
0, 1 | 0 | Query only: include native grapheme caret geometry in bounded reply chunks. |
N |
0, 1 | — | Subscription only: enable or disable font-change reports. Requires a nonzero i. |
e |
0, 1 | 0 | Request status replies, including refusals. Requires an explicit i. |
The keys follow the sizing protocol’s strictness: an unknown key, a pair
without =, an empty or out-of-range value, a repeated key, a missing
required key, a body over the caps, or a body that is not RTF refuses the
whole code. A refused code changes nothing. Without e=1 it answers nothing. With e=1
and an unambiguous i, it returns the status described below. A terminal that
does not implement the protocol remains silent.
There is no key-value form for styles. A plain block is {\rtf1 text}, which
costs eight bytes over the text; a second vocabulary for the common case would
be one more thing a program must learn and the terminal must keep in step
with the first.
a=p places a block whose top-left cell is the cursor, w cells wide and h
rows tall. The rectangle must fit the screen’s width from the cursor’s column,
and h must not exceed the screen’s rows. When the rectangle reaches past the
bottom row, the screen scrolls up by the excess first, as that many line feeds
at the bottom would; with a scroll region narrower than the screen the code is
refused instead. A row with a double-width or double-height line attribute
refuses the placement.
A nonzero i names the block. Placing a block with the id of a live block on
the same screen removes the old one first, so a program updates a block by
placing it again. Id 0 is anonymous: the block is placed and can be removed
only by what removes any block.
Unless C=1, the cursor then moves to the block’s left column on the row
below the block, scrolling as a line feed would.
r clips presentation without changing paragraph layout. The complete source
still shapes at width w, including text behind a menu. Only visible rectangles
cover cells, draw pixels, receive pointers or contribute to visible screen text.
Two blocks may share a bounding box when their visible rectangles do not overlap.
Clip edges are rounded once to device pixels; every fragment samples the same
full paragraph raster. Splitting source at a clip edge would break contextual
shaping, bidi ordering and ligatures, so callers must not do that.
Delete
Section titled “Delete”a=d:i=N removes the block named N on the active screen. a=d alone
removes every block on the active screen. The body is ignored. A removed
block’s cells stay as the blank cells they already are.
a=q measures the body in a w×h rectangle and replies; it places nothing
and moves nothing.
A query proposes the rectangle, the way a layout asks a view for its size.
w=0 leaves the width open: nothing wraps at an edge, and the reply’s w is
the ideal width, the widest paragraph. h=0 leaves the height open: the body
wraps at w, every line is shown, and the reply’s h is the rows those lines
need. Both may be open at once. An open dimension is measured up to the
largest rectangle a request can name, 1024 cells wide and 5120 rows tall, and
then resolves to the reply’s value, or to 1 for an empty body; f, n, c,
l and R describe that resolved rectangle. A placement must name both
dimensions, and a=p with w=0 or h=0 is refused. A query with an empty body measures nothing and replies
anyway, which makes it the feature test: a terminal that answers knows the
protocol.
OSC 7404 ; a=r : i=<id> : f=<0|1> : w=<cells> : h=<rows> : n=<chars> : c=<chars> : l=<lines> : m=<cells> : x=<0|1> : v=3 : W=<cell-width> : H=<cell-height> : B=<baseline> : S=<font-size> : E=<epoch> : L=<line-height> : A=<line-baseline> : Q=<0|1> : V=1 : N=<caret-count> : Z=<limits> : R=<line-ends> ST| key | meaning |
|---|---|
i |
The query’s i. |
f |
1 when the whole body is shown under the query’s overflow policy: no line is dropped, and no character is cut or replaced by an ellipsis. |
w |
The cells the widest laid-out line and the horizontal padding need, rounded up. Under o=c, the widest paragraph; under o=w and o=e, the widest line after wrapping at the query’s w. |
h |
The rows every line, its paragraphs’ space before and after, and the vertical padding need, rounded up, at the query’s w. |
n |
How many characters of the body are shown: all of them when f=1. |
c |
Characters per line in the style of the body’s first character: the width the first line’s indents leave in the rectangle, over that face’s average advance at that size, rounded down. |
l |
Lines that fit in the style of the body’s first character: the rectangle’s height over that face’s line height at that size in the first paragraph’s line spacing, rounded down. |
m |
The narrowest width in cells that breaks no word: the widest run of text between two break opportunities and the horizontal padding, rounded up. A break opportunity follows a space, a tab or a line break, and falls on either side of a wide character. It does not depend on the query’s w or h. |
x |
1 when every character was measured from a published table (exact); 0 when any character, or the whole answer, is an estimate. |
The version 3 fields W, H, B, S, L and A use 1/64 point.
E identifies the typography publication. Q=1 means a native shaper
measured the complete styled source. R lists exclusive Unicode scalar line
end offsets, separated by commas, including explicit line feeds. It is empty
for an empty feature probe. With g=1, each entry reserves exactly one row.
c remains an average-width estimate and is zero for native shaping; it must
not be used to wrap a particular string. Use R and h instead.
V=1 advertises placement clipping with r.
A character is a code point of the body’s text after the RTF is read, \par
and \line counting one each. A program sizes a block by asking with a
generous h, reading h back, and placing with that. It sizes a label from the measured w; the average c is not a string measurement.
Version 3 exposes limits in Z, as eight decimal values in this order: RTF body
bytes, document scalars, style runs, blocks per screen, total retained scalars
per terminal, pending measurements, visible rectangles, and caret spans per
reply chunk. Current values are 16384,4096,512,256,16384,32,32,128. Clients
bound their own work by the smaller of these and their implementation limits.
Caret geometry
Section titled “Caret geometry”A query with G=1 sends zero or more geometry packets before its final a=r:
OSC 7404 ; a=g:i=<request>:v=3:E=<epoch>:s=<span-start>:G=<first,end,row,leading,trailing,...> STEach packet holds at most 128 spans and starts at the next expected span index.
The final N is the complete span count; it is zero when geometry was not
requested or native shaping is unavailable. The entire result is queued
atomically. Each individual OSC is below 32 KiB, including the final line-end
list. Clients assemble at most 4096 spans for an outstanding request, require a
single epoch and contiguous chunk indices, then commit on the final result.
Offsets count Unicode scalars; [first,end) is one grapheme, never a UTF-8 byte
range or a UTF-16 range. row indexes R. Leading and trailing caret positions
are signed-direction edges expressed as nonnegative coordinates in 1/64 point
from the block’s left edge; leading can exceed trailing in RTL text. They include
alignment. A client maps validated grapheme boundaries to its own string indices
and preserves the visual x when navigating vertically. Measurement and pointer
reports come from the same shaped geometry.
Typography subscription
Section titled “Typography subscription”OSC 7404 ; a=s:i=<subscription>:N=1 STOSC 7404 ; a=n:i=<subscription>:v=3:E=<epoch>:W=<cell-width>:H=<cell-height>:B=<baseline>:S=<font-size>:Q=<native> STOSC 7404 ; a=s:i=<subscription>:N=0 STA nonzero subscription id replaces the terminal’s one active subscription. An
initial a=n reports the current metrics; subsequent publications report only
changed facts, even if the number of terminal rows and columns stays unchanged.
The application invalidates cached measurements on a changed E and requests
fresh layouts asynchronously. No keyboard reader waits for a measurement.
Screen switches and terminal reset cancel the subscription. Full screen erasure
does not. Clients unsubscribe on teardown. Host handover carries the subscription;
replicas never originate application reports. Every report checks TextMeasureRead.
Request status
Section titled “Request status”With e=1, a failed request returns:
OSC 7404 ; a=e:i=<request>:v=3:s=<status> STStatuses are invalid (malformed keys or body), unsupported (unknown key),
limit (document or retained-storage bound), geometry (placement cannot fit),
denied (rendering permission), busy (native measurement queue full), or
stale (font publication replaced an in-flight measurement). Successful place,
delete, and subscription requests return ok; a query’s final a=r is its
success response. Status reports themselves require TextMeasureRead, so a
denied read or a passive replica stays silent. Duplicate request ids within one
header or ambiguous error opt-ins stay silent. Clients release a refused query
rather than leaving it pending indefinitely.
The reply is a terminal report: it goes through the reply disposition every
report goes through and is gated by TextMeasureRead. A terminal that is a
replica of another answers nothing, because the host it mirrors answers.
The RTF subset
Section titled “The RTF subset”The body must be one group opening with {\rtf1. Everything after the
group’s close is ignored. Control words outside the subset are ignored, which
is what an RTF reader does by definition; a group whose first control word is
\* is skipped whole, as are the destinations below that carry no body text.
| Control words | Meaning |
|---|---|
\fonttbl with {\fN \fnil|\froman|\fswiss|\fmodern|\fscript|\fdecor|\ftech name;} |
Font entries: a number, a family class, and a family name. \fcharset and \fprq are read and ignored. |
\deffN |
The font of text that names none. |
\fN |
Select a font entry. An unknown number selects the default. |
\fsN |
Size in half-points, 2–1000. Default 24 (12 points). |
\b, \b0, \i, \i0 |
Bold and italic. |
\ul, \ul0, \ulnone, \strike, \strike0 |
Single underline and strikethrough. |
\colortbl with \redN\greenN\blueN; entries |
Colours. The first entry is conventionally empty: the automatic colour. |
\cfN |
Foreground colour by table index. 0, an empty entry, or an unknown index is the automatic colour. |
\highlightN |
Character background from the colour table; 0 is transparent. |
\plain |
Character formatting back to the defaults: default font, 12 points, regular, automatic colour. |
\pard, \ql, \qc, \qr, \qj |
Paragraph alignment; \qj is left-aligned. \pard also resets the paragraph’s spacing and indents. |
\slN, \slmultN |
Line spacing. With \slmult0, N in twips: N > 0 at least N, N < 0 exactly −N, \sl0 natural. With \slmult1, N/240 of the natural line, 0–2400. |
\sbN, \saN |
Space before and after the paragraph, 0–32767 twips. |
\liN, \riN, \fiN |
Left and right indents, 0–32767 twips, and the first line’s indent from the left one, −32767–32767 twips; a negative \fi hangs the first line. |
\par, \line |
A paragraph break and a line break. \par writes U+000A and \line U+2028 LINE SEPARATOR, which breaks the line inside its paragraph. A copy, a capture and accessibility read either as a line feed. |
\tab |
A tab: the pen advances to the next multiple of eight cell widths from the content box’s left edge; the first tab of a hanging first line stops at the left indent instead. |
\uN, \ucN |
A UTF-16 unit, signed as RTF writes it, and the count of fallback characters skipped after it. Surrogate pairs combine; an unpaired surrogate becomes U+FFFD. |
\'hh |
A byte in the document’s code page: Windows-1252, which \ansi and \ansicpg1252 name. Another \ansicpgN is read as Windows-1252 too. |
\\, \{, \} |
The literal characters. |
\~, \_, \- |
No-break space, non-breaking hyphen, and an optional hyphen (ignored). |
\emdash, \endash, \bullet, \lquote, \rquote, \ldblquote, \rdblquote |
The characters they name. |
\stylesheet, \info, \pict, \object, \header, \footer, \footnote, \fldinst |
Skipped with their group. A field’s \fldrslt text is kept. |
Bytes 0x20–0x7E outside control words are text. CR and LF are ignored, as RTF says. A byte at or above 0x80 starts a UTF-8 sequence, which the subset reads as text: RTF is seven-bit, and a program writing UTF-8 directly means the character, not its Windows-1252 bytes. A malformed sequence becomes U+FFFD. Text is grapheme-preserving: combining marks, variation selectors and ZWJ sequences keep their code points, and the layout keeps a cluster together.
A body is refused when it does not open with {\rtf1, when its groups nest
deeper than 32, when a paragraph’s spacing or indent is outside its range
(status invalid), or when it reaches a cap. A body whose final group is never
closed is read to its end: a truncated writer still places what it wrote.
The box model
Section titled “The box model”A block covers the w×h rectangle of cells it was placed in, on the screen
it was placed on. Each covered cell holds a written space in the pen the block
was placed with, marked as covered. The grid under a block is therefore
ordinary cells: the pen’s background paints the rectangle, a search finds
nothing there, a renderer that does not draw blocks shows a blank rectangle,
and the wire carries the cells as it carries every cell.
With the default P=0, anything that changes a covered cell removes its block: printing into it,
erasing it (ED, EL, ECH, DECERA, DECSERA), inserting or deleting characters
across it (ICH, DCH), inserting or deleting lines inside it (IL, DL), a scroll
that moves some of its rows and not others, and a reflow that separates its
cells. The
check runs once after each parse pass, against the covered mark: a block
whose rectangle no longer holds its own covered cells is removed, and the
covered mark is cleared from its cells that remain, which leaves them as
spaces in the block’s pen. The block’s text goes with it, as an image’s
pixels go when the cells under the image are erased.
A scroll that feeds the scrollback moves every block with its cells into it, and a block stays selectable and copyable there until its top row leaves the scrollback. A scroll or line edit that moves rows in place moves a block it moves whole and keeps inside the moved rows; it removes a block it moves in part or carries past the edge of the moved rows. A resize moves a block with its rows, and removes it when the reflow separates its cells. Switching screens keeps each screen’s blocks with that screen. RIS removes every block; DECSTR keeps them; ED 3 removes the blocks in the scrollback.
Alternate-screen application pointers
Section titled “Alternate-screen application pointers”M=1 replaces an otherwise eligible mouse report inside that block with:
OSC 7405 ; a=p:i=<block>:e=<p|r|m>:b=<button>:m=<modifiers>:x=<column>:y=<row>:c=<caret>:k=<character> STCoordinates are one-based physical terminal cells. c is the nearest logical
scalar insertion boundary and k is the grapheme under the pointer. They use
the original full document even with v. The modifier/button values use SGR
mouse conventions; e supplies press, release or motion separately. Motion
is not deduplicated by cell, since a cell may cover several shaped characters.
The ordinary mouse modes still control eligibility; Shift retains terminal
selection. M=0 retains ordinary cell reports.
Selection and copy
Section titled “Selection and copy”Selection inside a block is per character, never per cell. The contract:
- Pointer to character. Core maps a pointer position inside a block’s rectangle to a character index from the block’s laid-out advances: the line is the last line whose top is at or above the point, and the character is the first one on that line whose right edge is past the point. A point left of a line’s first character is on it; a point right of its last is after it. The caret a press opens stands on the nearer edge of that character. A caret never stands inside a grapheme: a combining mark, a variation selector, an emoji sequence or a wide glyph selects as one.
- One layout. The hit-test, the highlight and the drawn glyphs read the same shaped layout for that frontend. The renderer draws each complete line through the same typesetter used to measure it.
- Highlight shaped as a text view’s. Each line the selection reaches is
highlighted by one band. A band starts at the press, or at the block’s
leading edge on a line the selection came into from before, and ends at
the release, or at the block’s trailing edge on a line the selection goes
on past; a line crossed whole fills the block’s width. The bands tile the
block: each runs from its line’s top to the next line’s, the first from
the block’s shown top and the last to its bottom where the selection goes
on past them, so the block’s highlight meets the fill of the grid rows
around it with no gap. A line selected only between the press and the
release keeps one band per visual interval, which bidirectional text can
split. Covered cells are never highlighted as cells. Background edges are rounded to device pixels, including adjacent shared
edges, with fill antialiasing disabled. Glyph positions retain fractional
advances. This applies to terminal selection and RTF
\highlightin an application’s alternate screen. - Words. A double click selects the word around the character under the pointer, by the word rules a double click uses everywhere else, applied to the block’s text and bounded by the block.
- Reading order. In the grid’s reading order a block is read at its top-left cell, whole: the rows above it, then the cells left of it on its first row, then its text, then the cells right of it on its first row, then the cells outside it on the rows below. A drag that leaves a block continues into the surrounding grid in that order, and one that enters a block takes its characters up to the pointer.
- Copy. A copy yields the selected characters in reading order as plain
text.
\parand\linecopy as a line feed and\tabas a tab; a line the layout wrapped copies with no break. Styles are not copied. The copyable text runs through the block’s last visible line; an ellipsis is drawn, not copied.
The caret a selection holds inside a block is the block’s top-left cell and a character index into the block’s text, which is the sub-cell caret the selection model already speaks for proportional runs. Nothing else in the selection model gains a coordinate.
Layout
Section titled “Layout”Core owns the layout and hit testing. A frontend may install a native shaper that receives the complete styled source and returns lines and grapheme caret edges. Apple frontends use CoreText for fallback fonts, contextual shaping, ligatures, bidirectional order and language-aware line breaks. Unshaped frontends retain the table-derived fallback described below.
Units. Lengths are integers in layout units of 1/4096 point. A face’s advances are in 1/2048 em, and a size is in half-points, so an advance at a size is the product of the two, exactly. Sums of products are exact, on every arm and every compiler; nothing in the layout rounds until a frontend maps it to pixels.
Lines. The text breaks into lines at every \par and \line. Under o=w
and o=e, a line also breaks after the last space that keeps it within the
width its indents leave, and a word wider than that breaks between graphemes.
Spaces at a wrapped line’s end hang past the edge and take no width. Under
o=c a line breaks only where the body says, and what passes the right edge
is clipped there.
Height. A line’s natural height combines the largest ascent with the
largest descent plus nonnegative leading among its characters’ faces at their
sizes. Its baseline sits the largest ascent below its top, so faces with
different proportions fit above and below their shared baseline. The
paragraph’s line spacing sets the line box from that: at
least \sl twips, exactly -\sl twips, or the natural height times
\sl/240 with \slmult1; the baseline keeps the share of the box it has at
natural height. A paragraph’s \sb sits above its first line inside that
line’s box and its \sa below its last line inside that box, so the boxes tile
the block with no gap, the first paragraph’s and the last’s included. With
g=1 every line keeps one row and the spacing and paragraph spaces do not
apply. Lines stack from the block’s top. Only whole lines are shown; the first
line is shown even when it is taller than the block, clipped at the block’s
bottom.
Indents. Every line of a paragraph starts \li twips in from the content
box’s left edge, and its first line \fi further, which a negative \fi
turns into a hanging indent; no line starts left of the box. A line wraps at
the width that leaves less \ri, and aligns within it. A paragraph takes its
spacing and indents from its first character, as it takes its alignment.
Alignment. A line takes the alignment of its paragraph, which is the one
in force at the paragraph’s first character, or j when the body names none.
Centred and right-aligned lines are offset by the free width, rounded down to
a layout unit.
Padding. p insets a content box from the block’s rectangle, in points on
each side. The text flows into the content box: lines wrap at its width, are
shown while they fit its height, are clipped and cut for an ellipsis at its
edges, and align within it. Padding that leaves no width puts every grapheme
on a line of its own. The padding shows the covered cells’ background, as the
rest of the block does; a block has no background or border of its own. Every
size a fit reply names is the rectangle’s, its padding included, so a program
places what it was told, and caret geometry is measured from the rectangle’s
top-left. Without a typography table the layout counts in cells and padding
does not apply.
Vertical placement. When the lines and their paragraph spaces need less
height than the content box, y places them in the slack: t leaves it all
below, c splits it with the odd layout unit below, and b puts it all
above. Content that fills or overflows the box sits at its top whatever y
says, so clipping and the ellipsis do not change, and a block scrolled with
v is not placed. The text, its selection highlight and a pointer’s hit test
move with the placement; the content box and its padding do not.
Ellipsis. Under o=e, when text remains after the last line that fits,
that line is cut at the last grapheme that leaves room for U+2026 in the style
of the character before it, and the ellipsis is drawn there.
Fallback storage. A table-derived layout is one allocation sized at layout: a line array (first character, end, top, height, baseline, origin) and one array of the prefix-summed right edge of every laid-out character, relative to its line’s origin. The layout is derived state: Core builds it when the block is placed and again only when the typography table’s epoch moves — a resize of the cell, a change of font or size, a newly published face.
The measurement model
Section titled “The measurement model”The answer is Core’s and the metrics are the frontend’s. A program observes the reply and the selection, so Core decides both; only the frontend knows its fonts, so it publishes the facts Core decides from, as it publishes its window’s size.
A native shaped layout also keeps logical grapheme ranges and their two visual caret positions. Leading may be to the right of trailing in RTL text. The same source and line ranges are passed to CoreText for drawing; text is never rasterized one character at a time on the shaped path.
The typography facts table
Section titled “The typography facts table”| field | meaning |
|---|---|
| cell width, cell height | The grid’s cell, in 1/64 point, as the frontend draws it: its pixel cell over its pixels per point. |
| faces, at most 16 | One entry per face the frontend publishes. |
Each face entry:
| field | meaning |
|---|---|
| family | The name RTF’s font table uses for it, or empty for a class face. |
| class | Monospace, sans-serif or serif. |
| bold, italic | The style the entry measures. |
| ascent, descent, line height | In 1/2048 em. |
| advances | U+0020–U+024F, 560 advances in 1/2048 em, one u16 each: 1120 bytes. |
| average | The advance an estimate uses for a narrow character outside the range. |
A table is scale-free: an advance at a size is the em advance times the size. A face’s advances are not exactly linear in its size where it has optical sizes or hinting; the renderer places glyphs at Core’s advances anyway, so the layout, the hit-test and the pixels agree by construction, and an optical difference shows as spacing a glyph’s own design did not choose, never as disagreement.
The class-face table has three families in four styles each: the grid’s face
for monospace, its proportional face for sans-serif, and the platform’s serif:
the system face’s serif design on Apple (New York, reached through the system
descriptor, since its family name is hidden from a lookup by name) and
fontconfig’s serif on Linux.
Class faces share the em. A run is set at its own point size in whichever class face it resolves to: every class face takes the primary face’s em at that size and keeps its own ascent, descent, line height and advances. No class face is scaled to match another’s line box, cap height or x-height, so a face whose letters are drawn smaller within its em looks smaller, and the platform serif is chosen for proportions that match the sans-serif face’s. A line whose class face’s ascent and descent exceed the row it sits in takes the taller line box the “Height” rule gives it, and what passes the block’s bottom is clipped there, as any sized text is; it is never shrunk to fit. The table format also permits named families. When a frontend publishes only class faces, an RTF entry’s family name resolves through the fallback rules below; it does not cause an independent font lookup during drawing.
Resolving a font entry to a face
Section titled “Resolving a font entry to a face”An RTF font entry resolves to the published face whose family matches its
name (ASCII case-insensitively) in the requested bold and italic; failing
that, to the class face of its family class (\fmodern monospace, \froman
serif, every other class sans-serif) in that style; failing that, to the
sans-serif class face in that style; failing that, to the regular sans-serif
class face, then to the first published face. The same resolution serves the query, the layout and the
renderer, so a face the frontend never published is drawn in the face it was
measured in.
What the answer promises
Section titled “What the answer promises”With Q=1, the query uses the frontend’s native shaping service at the actual
font size. The full Unicode source participates in shaping, including text
outside the legacy dense advance table. Without a native shaper, Q=0 and
the table fallback promises the following:
A character in U+0020–U+024F measured in a published face is exact: the
layout places it where the renderer draws it. A character beyond the range is
an estimate: a narrow character takes the face’s average advance, a wide one
(UAX #11 W and F) one em, and a combining mark none. The renderer still draws
it at Core’s position, so its glyph may be wider or narrower than the room
the layout gave it. x=0 says an estimate is in the answer.
With no table published at all — a headless terminal with no renderer —
Core answers in cells: each character takes its cell width, every line takes
one row, and the reply says x=0. This is the sizing protocol’s answer
without a measurer.
Local and remote panes
Section titled “Local and remote panes”A local pane’s frontend publishes its table to the pane’s Core directly, once when its first frame is cut and again whenever its cell, font or size changes; macOS and iOS derive it in the shared surface factory, once.
A remote pane is answered by its host, which runs the program. The client
publishes its table to the host in a TypographyFacts frame (Docs/sync.md):
a fact the client tells the host about itself, beside Facts, in a frame of
its own because it is large and changes rarely — a focus change resends
Facts, never the table. The host answers a program in a pane with the table
of the attachment that published one most recently, so a program running on
the host and viewed on a phone measures in the phone’s metrics. The table is
capped in faces and bytes so that it travels in one frame body.
Each side derives its own layout from its own table. The host never lays out for a client’s pointer, and the client’s Core runs the pointer ladder on its own layout; selection never crosses the wire.
Native measurement across a connection
Section titled “Native measurement across a connection”The host forwards a nonempty query to a client whose published typography
matches the pane’s current table. RichTextMeasure (55) carries a pane-bound
request token, the shape request and the parsed document. RichTextMeasured
(76) returns structured, bounded geometry and line endings. The host verifies
the outstanding token and source extent, then constructs the OSC reply itself.
No client-supplied terminal bytes are forwarded to the program.
At most 32 queries are in flight per terminal and connection. Requests expire from the connection after two seconds and can be retried by the host. An alternate-screen exit drops the terminal’s pending queries. Replies to a cancelled token have no effect. An empty probe reports native capability from the published facts without a round trip. No UI-thread dispatch is allowed inside the shaper callback: the caller holds the terminal model lock.
Colour and SGR
Section titled “Colour and SGR”A block’s text takes the pen it was placed with. The automatic colour is the
pen’s foreground, which follows the theme as any cell’s does; \cfN overrides
it with the table’s colour, as a direct colour that the theme does not move.
The block’s background is the covered cells’ background: the pen’s. Bold and
italic in the body are the block’s own; the pen’s SGR bold and italic do not
apply inside a block, and neither do its underline and strikethrough, which
the body says itself. A selection highlight is drawn in the tone of a
selected cell beside the block: where the selection is filled behind the
text, the selection colour washed over the block’s background by the rule a
selected cell’s background takes, opaque, under the glyphs; where it is a
tint, the selection’s tint over them. Glyphs keep their colours.
Rendering
Section titled “Rendering”Core hands each visible block to the frontend through the frame capture: its rectangle in pixels, a content key that moves only when the block, its layout, the palette or the scale changes, and its draw list — runs of glyphs with a face, a size, a colour, decorations and each glyph’s position in layout units. Decorations come as stretches: the longest run of touching graphemes on one line that share a style. A frontend strokes one underline and one strikethrough across each stretch, never one per grapheme, so a rule is continuous across letter boundaries that fall inside a device pixel and breaks only where the style or the line does. Pango’s run-level decorations meet the same rule on Linux. The frontend rasterises the block once per key into a retained layer and composites it as it composites an image, over the covered cells; a frame whose blocks did not change does no block work. The layer is rasterised at the drawable’s device pixels and composited at exactly one texel per device pixel, never resampled: a change of backing scale or of the table’s epoch moves the key and rasterises it again. The layer is premultiplied, with a transparent ground, so the selection highlights — separate rectangles drawn beneath it — show through between and around the glyphs of every run, coloured or not.
- Swift and Metal (macOS and iOS). CoreText draws complete shaped lines
using the same faces and ranges used for measurement, into a
linear half-float raster with premultiplied alpha, drawn over the blitted
grid every frame and redrawn only when the content key moves. The table is
measured from the same faces, in the one derivation both shells share
(
RichTextFaces). On the bitmap tier the blocks are still CoreText’s. - Linux (GTK, CPU and Vulkan). Pango lays out complete paragraphs through HarfBuzz, FriBidi and fontconfig fallback. Its line boundaries and grapheme carets feed the same Core layout, OSC replies, pointer reports and agent projection. PangoCairo paints retained transparent paragraph rasters onto the existing RGBA presentation path for both grid renderers. Native chrome clips the rich layer; character backgrounds, selection and the caret share rounded device-pixel boundaries. GTK zoom and DPI changes republish font metrics.
- Backend fallback. A backend without block rasterization shows the covered cells in the placing pen’s background. Core still owns selection and copy. A backend that rasterizes blocks must use the faces represented by its published table and place their glyphs at Core’s positions.
Performance
Section titled “Performance”Layouts and rasters are retained until source, typography or placement changes. Rows are located by binary search; shaped grapheme spans on that row determine pointer hits and selections. Backgrounds use shared rounded device-pixel edges. The original document, rather than raster data or per-frame glyph streams, crosses Sync. Queries may require one round trip to the font-owning client; applications should cache results by source, width and typography revision and keep the previous proportional frame visible while a new measurement arrives.
Synchronization
Section titled “Synchronization”| State | Carriage |
|---|---|
| The blocks’ placements: screen, serial, id, rectangle, visible rectangles, overflow, alignment, padding, vertical placement | Digested, in the RichText state block (Sync wire 25) |
| The blocks’ documents: text, runs, font table, colour table | Defined once per attachment by a DefineRichText op naming the block’s serial |
| A block’s layout and drawing | Derived |
| The typography table | Host-only: it arrives from the client in TypographyFacts and never travels in a checkpoint |
The covered cells travel as cells. A serial names one placement for the life of the host terminal — a hard reset continues the count — so an update that moves or removes blocks carries their placements and no document, and a checkpoint defines every document it places. The digest covers the placements; the loopback oracle compares every block, placement and document, between host and client after every frame.
Capabilities
Section titled “Capabilities”Placing and deleting blocks is grid content, like an image: RichTextRender,
in the Rendering tier, allowed for every origin. The query is
TextMeasureRead, in the Benign tier: the reply reveals metrics of the
viewer’s fonts and nothing beyond the screen. Both are checked at the
operation, before the code touches anything.
| What | Cap |
|---|---|
| RTF body | 16,384 bytes |
| Text per block | 4,096 code points |
| Runs per block | 512 |
| Font table entries | 16 |
| Colour table entries | 16 |
| Group depth | 32 |
| Blocks per screen | 256 |
| Visible rectangles per block | 32 |
| Text per pane, every block of both screens and the scrollback | 16,384 code points |
| Faces per typography table | 16 |
Every cap is checked before anything is mutated: a code over a cap is refused whole. A placement that would exceed the blocks per screen or the text per pane is refused rather than evicting an older block.
Font family names are stored in at most 47 bytes; a longer name is truncated. This storage bound does not reject the body.
A worked example
Section titled “A worked example”A program shows a heading and a paragraph in a 40×6 block. It asks first:
printf '\033]7404;a=q:i=1:w=40:h=6;{\\rtf1{\\fonttbl{\\f0\\fswiss Helvetica;}}{\\colortbl;\\red0\\green102\\blue204;}\\f0\\fs36\\b\\cf1 Release notes\\par\\fs24\\b0\\cf0 The build now signs every artefact and verifies the chain before upload.}\033\\'and reads the reply:
ESC ] 7404 ; a=r:i=1:f=1:w=39:h=5:n=88:c=60:l=4:m=9:x=1 ESC \The body fits (f=1) in 39 columns and 5 rows, and no word is wider than 9
columns (m=9). A program that does not know the width it wants asks with
w=0 for the ideal width, or with h=0 for the rows a fixed width needs.
The program places it:
printf '\033]7404;a=p:i=1:w=40:h=5;{\\rtf1{\\fonttbl{\\f0\\fswiss Helvetica;}}{\\colortbl;\\red0\\green102\\blue204;}\\f0\\fs36\\b\\cf1 Release notes\\par\\fs24\\b0\\cf0 The build now signs every artefact and verifies the chain before upload.}\033\\'The cursor lands on the row below the block. To update the paragraph, the
program places i=1 again; to remove it, it sends printf '\033]7404;a=d:i=1\033\\'.
The numbers in the reply depend on the viewer’s fonts; that is the point of asking.
- Native shaping covers wrap and clip. Ellipsis currently uses the table fallback (
Q=0). - An unpublished family resolves through the table’s class-face fallback.
\qjis left-aligned; tabs stop every eight cells, and only the hanging first line has a custom stop; underline is single.\highlightis supported;\cb, superscript, subscript, tables, lists, links and pictures are ignored.- Styles are not copied.
- A double-width or double-height row refuses a block.
Agent access
Section titled “Agent access”pane.capture with source=screen returns the same visible text projection as
accessibility: grid labels and visible rich-text ranges, in screen-row order.
The richText member carries the original UTF-8 text of each intersecting block,
style runs (including named and generic font families), and the visible source ranges. Hidden text remains in the block’s
source for shaping context; only visible ranges contribute to the flat text.
All source offsets count Unicode scalar values, with half-open [first,end)
ranges. They never count glyphs, UTF-8 bytes, or UTF-16 code units.
Blocks are ordered by top row then left column. id is the application’s OSC
identifier; serial identifies this placement. The pane, serial and
typographyEpoch together identify the geometry. Replacement or font changes
invalidate that identity. Cell rectangles are zero-based. Native lines and
graphemes use points relative to the unscrolled block content, with a top-left
origin. Subtract viewportTop * cellHeight from a line’s top to locate it inside
the visible rectangle; add the block’s cell origin to locate it in the pane.
visibleRects carries the optional placement clips. Each visible source range
also names its screen column, so grid labels inside clip holes retain their
correct position in the flat text projection.
Line baselines are relative to the line top. Leading and trailing carets may
run right-to-left. A grapheme range can contain several source scalars.
On a capture request, the font-owning attachment supplies the host’s geometry over Sync. Requests
are bounded to 32 pending measurements; replies are assembled from ordered
chunks, at most 512 KiB per request, and expire with the request’s two-second
deadline. Block replacement and typography changes prevent stale adoption.
Until native geometry arrives, richText.layoutPending is true and complete
is false. AgentSession retries within the original capture deadline. A host
without an attached shaper cannot invent native geometry. Table fallback
layouts report shaped=false and omit native lines and grapheme edges.
Architecture choices
Section titled “Architecture choices”The terminal retains styled source and placement, not a stream of individually positioned characters. Core owns lifetime, bounds, permission checks, the wire, and validated layout geometry. The font-owning frontend owns paragraph shaping and rasterization: CoreText on Apple platforms and Pango on Linux. Measurement, hit testing and paint must use the same complete source, font configuration and line ranges. A scalar width table is an explicitly approximate fallback, never an exact paragraph typesetter.
This division follows CoreText’s typesetter/line model and Pango’s paragraph layout model. HarfBuzz alone supplies glyph shaping, but does not provide the paragraph breaking, bidi resolution or font fallback that this protocol needs. The shaping callback never dispatches synchronously to a UI thread while Core holds its model lock. Native font state is owned per renderer and synchronized at that boundary.
Placement clips (r) and a scrolled content origin (v) are presentation
operations. They must not split the source into fragments before shaping.
Application menus can cover part of a paragraph without changing its wrapping
or exposing the monospace backing grid. Colour changes invalidate painted
content; unchanged geometry and unrelated typing do not require measurement.
OSC 66 remains the bounded grid-run protocol; OSC 7404 is the retained paragraph
protocol, and OSC 7405 reports logical character positions without assuming
that a glyph occupies a terminal cell.
Native ellipsis (o=e) shapes a grapheme-safe source prefix with U+2026 attached.
Core uses a bounded search of native layouts rather than summing character widths;
the complete styled prefix participates in bidi resolution and contextual shaping.
Paint, hit testing and selection use that presentation; the synthetic ellipsis is
not copied. Query extents, R, and optional G describe the complete unelided
source, while n reports the visible source prefix. Remote measurement carries
both geometries, so the host and font-owning frontend use the same cut.
References: CoreText typesetters, Pango layout, and HarfBuzz’s scope.
ubq read --json exposes this snapshot under screen. MCP callers can request
terminal_screen with format="structured" for the capture JSON, or omit the
format for visible text. Both use the existing region-read permission. Capturing
text grants no input or selection authority. Screenshots remain useful for
visual verification.
Ubiquitty and this site are copyright © 2026 Stanislav Yaglo. Ubiquitty is licensed under the Apache License 2.0.
Apple, iPad, iPadOS, iPhone, Keychain, Mac, macOS, Metal, Swift and Xcode are trademarks of Apple Inc., registered in the U.S. and other countries and regions.