Text Sizing Protocol (OSC 66)
The text sizing protocol (OSC 66) lets the application tell the terminal exactly how many cells a character should occupy. This solves measurement/rendering mismatches for two categories of characters:
- Private Use Area (PUA) — nerdfont icons and powerline symbols that
string-widthreports as 1-cell but terminals render as 2-cell - Text-presentation emoji — characters like warning sign, checkmark, airplane that have ambiguous width across terminals
The Problem
PUA Characters
Nerdfont icons (U+E000-U+F8FF) cause layout misalignment:
string-widthsays the icon is 1 cell wide (per Unicode EAW tables)- The terminal renders the icon as 2 cells wide (because the font's glyph is double-width)
- Text after the icon is placed at the wrong column, causing truncation
Text-Presentation Emoji
Characters like \u26A0 (warning sign), \u2611 (checkmark), \u2708 (airplane) are Extended_Pictographic but do NOT have the Emoji_Presentation property. Terminals render them as 2-wide emoji glyphs, but string-width reports them as 1 cell.
The Solution
With OSC 66, the app wraps ambiguous-width characters in a sequence that specifies the exact width:
ESC ] 66 ; w=2 ; <character> BELWhen both the layout engine and the terminal agree on 2 cells, alignment is correct.
Enabling Text Sizing
Via run() (recommended)
import { run } from "@silvery/term/runtime"
// Auto-detect: enable if terminal supports it (Kitty 0.40+, Ghostty)
await run(<App />, { textSizing: "auto" })
// Force enable
await run(<App />, { textSizing: true })Programmatic Control
import { createMeasurer, runWithMeasurer, isTextSizingEnabled } from "@silvery/term"
// Create a measurer with text sizing enabled
const measurer = createMeasurer({ textSizingEnabled: true })
// Use the measurer for width calculations
measurer.graphemeWidth("\uE0B0") // 2 (PUA treated as double-width)
measurer.displayWidth("icon\uE0B0text") // accounts for PUA width
// Scope a measurer for pipeline operations (output phase, etc.)
runWithMeasurer(measurer, () => {
// All module-level functions (graphemeWidth, displayWidth, etc.)
// use this measurer within the callback
})
// Check current state (module-level default)
console.log(isTextSizingEnabled()) // false (default)Terminal Support
| Terminal | Version | Status |
|---|---|---|
| Kitty | v0.40+ | Full support |
| Ghostty | all | Full support |
| WezTerm | -- | Not yet |
| iTerm2 | -- | Not yet |
| Alacritty | -- | Not yet |
Use isTextSizingLikelySupported() for a fast synchronous env-var check, or detectTextSizingSupport() for definitive cursor-position-based detection.
API Reference
textSized(text: string, width: number): string
Wrap text in an OSC 66 sequence that tells the terminal to render it in exactly width cells.
import { textSized } from "@silvery/term"
textSized("\uE0B0", 2) // "\x1b]66;w=2;\uE0B0\x07"isPrivateUseArea(cp: number): boolean
Check if a code point is in the Private Use Area. Covers BMP PUA (U+E000-U+F8FF) and Supplementary PUA-A/B.
import { isPrivateUseArea } from "@silvery/term"
isPrivateUseArea(0xe0b0) // true (Powerline separator)
isPrivateUseArea(0x41) // false (ASCII 'A')isTextSizingLikelySupported(): boolean
Fast synchronous check based on TERM_PROGRAM and TERM_PROGRAM_VERSION environment variables. Returns true for Kitty v0.40+ and Ghostty.
detectTextSizingSupport(write, read, timeout?): Promise<{ supported, widthOnly }>
Definitive detection using cursor position reports. Sends an OSC 66 test sequence and checks if the cursor advanced by the expected amount.
createMeasurer(opts: { textSizingEnabled: boolean }): Measurer
Create a measurer instance with the given text sizing configuration. When textSizingEnabled is true:
measurer.graphemeWidth()returns 2 for PUA charactersmeasurer.displayWidth()accounts for PUA width- Used with
runWithMeasurer()to scope the output phase for OSC 66 wrapping
Each measurer has its own independent width cache.
runWithMeasurer(measurer: Measurer, fn: () => T): T
Run a function with a scoped measurer. All module-level width functions (graphemeWidth, displayWidth, etc.) use the provided measurer within the callback. This is how the output phase and other pipeline stages get text sizing awareness without threading measurer arguments.
isTextSizingEnabled(): boolean
Check if text sizing mode is currently active at the module level (default measurer).
How It Works Internally
When text sizing is enabled:
Measurement:
graphemeWidth()returns 2 for PUA characters (normally 1). This flows throughdisplayWidth(),wrapText(),truncateText(), and all layout calculations.Buffer: PUA characters are stored as wide characters in the terminal buffer (like CJK) -- the character occupies cells [x] and [x+1] with a continuation marker.
Output: When generating ANSI output, PUA characters in wide cells are wrapped in
ESC]66;w=2;...BELso the terminal renders them at the correct width.
run() auto-enables text sizing on supported terminals (Kitty 0.40+, Ghostty). Unsupported terminals ignore the sequences harmlessly.