vterm.js 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/index.d.mts +36 -0
- package/dist/index.mjs +520 -277
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ Part of the [vterm](https://github.com/beorn/vterm) monorepo.
|
|
|
35
35
|
|
|
36
36
|
## Install
|
|
37
37
|
|
|
38
|
+
> Install from npm; a git install resolves the TypeScript source and runs only under Bun.
|
|
39
|
+
|
|
38
40
|
```bash
|
|
39
41
|
npm install vterm.js
|
|
40
42
|
```
|
|
@@ -435,6 +437,67 @@ The **reactive** counterpart is `signals.damage$` (see [Signals](#signals--the-r
|
|
|
435
437
|
|
|
436
438
|
**Use vterm.js** when you need everything — 100% coverage of the [terminfo.dev](https://terminfo.dev) feature matrix.
|
|
437
439
|
|
|
440
|
+
## vterm.js and xterm.js
|
|
441
|
+
|
|
442
|
+
[xterm.js](https://www.npmjs.com/package/@xterm/xterm) is the standard for putting a terminal on a web page — DOM, canvas, and WebGL renderers, a large addon ecosystem, and the terminal inside VS Code. When the job is rendering a terminal to a human, reach for xterm.js. We do: [Termless](https://termless.dev)'s own `@termless/web-player`, which plays back recorded terminal sessions in the browser, is built directly on `@xterm/xterm`.
|
|
443
|
+
|
|
444
|
+
vterm.js is for the opposite case: nothing ever gets painted. It's headless-first — the terminal as a **readable data structure**, not a rendered surface. Zero dependencies, pure TypeScript, full VT/ECMA-48/xterm standards coverage, a binary snapshot codec for exact state capture, a reactive read plane (`screen.signals`), absolute-row scrollback addressing, and semantic-zone detection (OSC 133 / OSC 633 prompt/command/output). That combination is aimed at testing, session recording, audit trails, and AI-agent infrastructure — consumers that need to know exactly what a terminal contains without ever drawing it.
|
|
445
|
+
|
|
446
|
+
Because vterm.js makes no display claims, it's held to a stricter standard instead: engine conformance, graded publicly and continuously in [Termless](https://termless.dev)'s differential conformance corpus against xterm.js, Ghostty, and neovim's libvterm. Current per-engine results and every known gap live in the ratchet ledger (`known-gaps.json`) — it's two-way: an unlisted failure fails the build, and a listed gap that starts passing fails too. Full results are public at [termless.dev/advanced/conformance-corpus](https://termless.dev/advanced/conformance-corpus).
|
|
447
|
+
|
|
448
|
+
## Deliberate divergences
|
|
449
|
+
|
|
450
|
+
A handful of behaviours where vterm.js knowingly differs from neovim's
|
|
451
|
+
libvterm, the reference implementation the conformance corpus is mined from.
|
|
452
|
+
Each was ruled deliberately rather than left as a gap, so **100% conformance
|
|
453
|
+
for vterm.js means 126/128 plus these documented divergences** — not 128.
|
|
454
|
+
|
|
455
|
+
Each entry names the _override word_ that would reverse the ruling. If you need
|
|
456
|
+
the other behaviour, open an issue with that word and the reason; it is the
|
|
457
|
+
argument, not the preference, that would move it.
|
|
458
|
+
|
|
459
|
+
### Cursor follows reflow when a resize shrinks below it
|
|
460
|
+
|
|
461
|
+
Shrinking the terminal past the cursor's column carries the cursor with its
|
|
462
|
+
line through the rewrap. libvterm, xterm.js and vt100.js instead clamp it to
|
|
463
|
+
the last column of the row it was on.
|
|
464
|
+
|
|
465
|
+
Following the line is what a reflowing terminal does — the cursor stays with
|
|
466
|
+
the text a user was typing, rather than jumping to wherever that text used to
|
|
467
|
+
end. Clamping is the pre-reflow behaviour the other three inherit from a
|
|
468
|
+
shared ancestry, and three engines agreeing about their own history is not an
|
|
469
|
+
argument about which is correct. Both ends of our own record/replay path read
|
|
470
|
+
vterm.js, so there is no internal disagreement to resolve.
|
|
471
|
+
|
|
472
|
+
**Override word: `clamp`.**
|
|
473
|
+
|
|
474
|
+
### RIS clears the scrollback
|
|
475
|
+
|
|
476
|
+
A full reset (`ESC c`) discards scrollback along with the screen. libvterm
|
|
477
|
+
preserves it, so history banked before a reset can still be scrolled back to —
|
|
478
|
+
and can even be popped back onto the screen by a later resize.
|
|
479
|
+
|
|
480
|
+
Every other engine in the corpus — xterm.js, Ghostty and vt100.js — clears on
|
|
481
|
+
RIS, as does xterm itself. A reset that leaves history behind surprises the
|
|
482
|
+
common case, where `reset` is reached for precisely to get a clean terminal.
|
|
483
|
+
|
|
484
|
+
**Override word: `match-ris`.**
|
|
485
|
+
|
|
486
|
+
### Related: the deferred-wrap cursor
|
|
487
|
+
|
|
488
|
+
Not an excluded case, but the same family and worth knowing if you read the
|
|
489
|
+
cursor. When a glyph fills the last column with autowrap on, the terminal owes
|
|
490
|
+
a wrap it has not performed. vterm.js represents that as the cursor sitting at
|
|
491
|
+
`col == cols` — one past the last column — as xterm.js and vt100.js do.
|
|
492
|
+
libvterm and Ghostty instead keep the cursor _on_ the last column and carry a
|
|
493
|
+
separate pending-wrap flag. The corpus normalizes this rather than excluding
|
|
494
|
+
it, so those cases pass; consumers reading `cursor.col` should simply expect
|
|
495
|
+
the `col == cols` form.
|
|
496
|
+
|
|
497
|
+
These entries mirror the corpus-side table in the suite's own README and the
|
|
498
|
+
machine-readable reasons in `known-gaps.json`. A new ruling lands in all three
|
|
499
|
+
at once.
|
|
500
|
+
|
|
438
501
|
## See also
|
|
439
502
|
|
|
440
503
|
- [vt100.js](../vt100/) — VT100-era emulator (smaller, focused)
|
package/dist/index.d.mts
CHANGED
|
@@ -77,6 +77,8 @@ interface ScreenCell {
|
|
|
77
77
|
hidden: boolean;
|
|
78
78
|
blink: boolean;
|
|
79
79
|
wide: boolean;
|
|
80
|
+
/** DECSCA: a selective erase (DECSED/DECSEL) leaves this cell alone. */
|
|
81
|
+
protected: boolean;
|
|
80
82
|
url: string | null;
|
|
81
83
|
}
|
|
82
84
|
interface ScreenOptions {
|
|
@@ -118,6 +120,8 @@ interface ScreenAttrsSnapshot {
|
|
|
118
120
|
inverse: boolean;
|
|
119
121
|
hidden: boolean;
|
|
120
122
|
blink: boolean;
|
|
123
|
+
/** DECSCA — cells written under it resist selective erase. */
|
|
124
|
+
protected: boolean;
|
|
121
125
|
url: string | null;
|
|
122
126
|
}
|
|
123
127
|
interface ScreenColorStateSnapshot {
|
|
@@ -164,6 +168,26 @@ interface Snapshot {
|
|
|
164
168
|
* `restore()` treats missing as all-false (hard-wrapped).
|
|
165
169
|
*/
|
|
166
170
|
scrollbackSoftWrapped: boolean[];
|
|
171
|
+
/**
|
|
172
|
+
* DEC line attributes (DECDWL/DECDHL), SPARSE: only rows that are not normal
|
|
173
|
+
* appear, as `[rowIndex, attr]` pairs — 1 double-width, 2 double-height top,
|
|
174
|
+
* 3 double-height bottom.
|
|
175
|
+
*
|
|
176
|
+
* Sparse and header-resident on purpose. Double-width lines are a banner
|
|
177
|
+
* feature that almost no session emits, so a dense array per grid would be
|
|
178
|
+
* thousands of zeros; and the binary grid sections have no skippable framing,
|
|
179
|
+
* so a new array THERE would desynchronise an older reader rather than be
|
|
180
|
+
* ignored by it.
|
|
181
|
+
*
|
|
182
|
+
* Absent in snapshots taken before this field existed — `restore()` treats
|
|
183
|
+
* missing as all-normal. **Readers older than this field render DWL/DHL lines
|
|
184
|
+
* single-width**: a visual downgrade, never corruption.
|
|
185
|
+
*/
|
|
186
|
+
lineAttrs?: {
|
|
187
|
+
main: [number, number][];
|
|
188
|
+
alt: [number, number][];
|
|
189
|
+
scrollback: [number, number][];
|
|
190
|
+
};
|
|
167
191
|
cursor: {
|
|
168
192
|
x: number;
|
|
169
193
|
y: number;
|
|
@@ -180,6 +204,10 @@ interface Snapshot {
|
|
|
180
204
|
originMode: boolean;
|
|
181
205
|
autoWrap: boolean;
|
|
182
206
|
charsetG0: boolean;
|
|
207
|
+
/** True when DEC Special Graphics is designated as G1. Absent in older snapshots: ASCII. */
|
|
208
|
+
charsetG1?: boolean;
|
|
209
|
+
/** True when SO has invoked G1 into GL. Absent in older snapshots: SI/G0. */
|
|
210
|
+
activeG1?: boolean;
|
|
183
211
|
};
|
|
184
212
|
attrs: ScreenAttrsSnapshot;
|
|
185
213
|
modes: {
|
|
@@ -193,6 +221,8 @@ interface Snapshot {
|
|
|
193
221
|
focusTracking: boolean;
|
|
194
222
|
origin: boolean;
|
|
195
223
|
insert: boolean;
|
|
224
|
+
/** LNM — LF also returns the carriage. Absent in older snapshots. */
|
|
225
|
+
newLine: boolean;
|
|
196
226
|
reverseVideo: boolean;
|
|
197
227
|
syncOutput: boolean;
|
|
198
228
|
kittyKeyboardFlags: number;
|
|
@@ -242,6 +272,10 @@ interface Snapshot {
|
|
|
242
272
|
};
|
|
243
273
|
unicode: {
|
|
244
274
|
charsetG0: boolean;
|
|
275
|
+
/** True when DEC Special Graphics is designated as G1. Absent in older snapshots: ASCII. */
|
|
276
|
+
charsetG1?: boolean;
|
|
277
|
+
/** True when SO has invoked G1 into GL. Absent in older snapshots: SI/G0. */
|
|
278
|
+
activeG1?: boolean;
|
|
245
279
|
lastChar: string;
|
|
246
280
|
pendingRegionalIndicator: string | null;
|
|
247
281
|
afterZWJ: boolean;
|
|
@@ -402,6 +436,8 @@ interface TerminalModes {
|
|
|
402
436
|
focusTracking: boolean;
|
|
403
437
|
originMode: boolean;
|
|
404
438
|
insertMode: boolean;
|
|
439
|
+
/** LNM — LF also returns the carriage (ECMA-48 mode 20). */
|
|
440
|
+
newLineMode: boolean;
|
|
405
441
|
reverseVideo: boolean;
|
|
406
442
|
syncOutput: boolean;
|
|
407
443
|
leftRightMargin: boolean;
|