@termwright/vt
v0.7.7
Published
termwright VT core: one terminal factory and one terminal profile, shared by driver, trace, screenshot and ui
Readme
@termwright/vt
The VT core every termwright package shares: one terminal factory, one terminal profile.
A live session, a replay of that session, a screenshot of that replay and the
runner pane showing it are four terminals. They only agree about what is on the
screen if they count characters the same way — and a terminal built by hand
somewhere else will not. This package exists so there is exactly one place where
a @xterm/headless terminal comes into being.
Install
pnpm add @termwright/vtUsage
import { createTerminal, loadSerializeAddon, TERMINAL_PROFILES } from '@termwright/vt';
const { terminal, profile } = createTerminal({
columns: 100,
rows: 30,
scrollback: 2_000,
profile: 'cjk-wide', // an id, a profile object, or nothing
});
const serialize = loadSerializeAddon(terminal);
terminal.write('│ boxes line up only if both ends agree │');
console.log(profile.id); // record this with the session
console.log(terminal.unicode.activeVersion); // the same id: ask a terminal what it is
console.log(Object.keys(TERMINAL_PROFILES));In a browser
The main entry builds a @xterm/headless terminal, which pulls Node into a
bundle. A browser consumer imports the other half instead — profiles, width
tables and the provider that applies them, with no headless in the runtime path:
import { applyProfile, resolveProfileId, DEFAULT_PROFILE } from '@termwright/vt/unicode';
const profile = resolveProfileId(recording.terminalProfile) ?? DEFAULT_PROFILE;
applyProfile(term.unicode, profile);applyProfile registers the profile's provider AND activates it. Registering
alone changes nothing — that is the half of the trap that produced the bug this
package exists to prevent.
What a profile is
Terminals disagree about a handful of things that decide whether a bordered box lines up. A profile is a named set of answers to exactly those questions:
| Switch | What it decides |
| -------------------------- | ------------------------------------------------------------------------------------- |
| ambiguousWidth | whether East Asian Ambiguous characters take one column or two |
| reflowCursorLineOnResize | whether the cursor's line reflows when the terminal resizes (wrapped lines always do) |
Every profile uses Termwright's Unicode 15 extended-grapheme model. Two policy profiles ship:
default— East Asian Ambiguous characters are narrow.cjk-wide— East Asian Ambiguous characters are wide.
A profile is a set of switches that reproduces how terminals differ; it is not an emulation of any particular one, and it is not trying to become one.
reflowCursorLineOnResize is named for exactly what it reaches. xterm.js always
reflows wrapped lines; the only choice it offers is the cursor's line, and the
field says so rather than promising reflow control it does not have.
Why the factory, and not just a shared config
The driver and replay once activated different Unicode providers, so the same bytes produced different geometry. A shared config would not have fixed that; only a shared factory makes it impossible to forget.
The factory also absorbs one upstream trap: @xterm/headless and its addons are
CJS-only despite shipping .mjs builds, so they must be imported through their
default export. That workaround now lives in one file instead of four.
