vterm.js 0.6.0 → 0.7.0

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 CHANGED
@@ -435,6 +435,67 @@ The **reactive** counterpart is `signals.damage$` (see [Signals](#signals--the-r
435
435
 
436
436
  **Use vterm.js** when you need everything — 100% coverage of the [terminfo.dev](https://terminfo.dev) feature matrix.
437
437
 
438
+ ## vterm.js and xterm.js
439
+
440
+ [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`.
441
+
442
+ 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.
443
+
444
+ 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).
445
+
446
+ ## Deliberate divergences
447
+
448
+ A handful of behaviours where vterm.js knowingly differs from neovim's
449
+ libvterm, the reference implementation the conformance corpus is mined from.
450
+ Each was ruled deliberately rather than left as a gap, so **100% conformance
451
+ for vterm.js means 126/128 plus these documented divergences** — not 128.
452
+
453
+ Each entry names the _override word_ that would reverse the ruling. If you need
454
+ the other behaviour, open an issue with that word and the reason; it is the
455
+ argument, not the preference, that would move it.
456
+
457
+ ### Cursor follows reflow when a resize shrinks below it
458
+
459
+ Shrinking the terminal past the cursor's column carries the cursor with its
460
+ line through the rewrap. libvterm, xterm.js and vt100.js instead clamp it to
461
+ the last column of the row it was on.
462
+
463
+ Following the line is what a reflowing terminal does — the cursor stays with
464
+ the text a user was typing, rather than jumping to wherever that text used to
465
+ end. Clamping is the pre-reflow behaviour the other three inherit from a
466
+ shared ancestry, and three engines agreeing about their own history is not an
467
+ argument about which is correct. Both ends of our own record/replay path read
468
+ vterm.js, so there is no internal disagreement to resolve.
469
+
470
+ **Override word: `clamp`.**
471
+
472
+ ### RIS clears the scrollback
473
+
474
+ A full reset (`ESC c`) discards scrollback along with the screen. libvterm
475
+ preserves it, so history banked before a reset can still be scrolled back to —
476
+ and can even be popped back onto the screen by a later resize.
477
+
478
+ Every other engine in the corpus — xterm.js, Ghostty and vt100.js — clears on
479
+ RIS, as does xterm itself. A reset that leaves history behind surprises the
480
+ common case, where `reset` is reached for precisely to get a clean terminal.
481
+
482
+ **Override word: `match-ris`.**
483
+
484
+ ### Related: the deferred-wrap cursor
485
+
486
+ Not an excluded case, but the same family and worth knowing if you read the
487
+ cursor. When a glyph fills the last column with autowrap on, the terminal owes
488
+ a wrap it has not performed. vterm.js represents that as the cursor sitting at
489
+ `col == cols` — one past the last column — as xterm.js and vt100.js do.
490
+ libvterm and Ghostty instead keep the cursor _on_ the last column and carry a
491
+ separate pending-wrap flag. The corpus normalizes this rather than excluding
492
+ it, so those cases pass; consumers reading `cursor.col` should simply expect
493
+ the `col == cols` form.
494
+
495
+ These entries mirror the corpus-side table in the suite's own README and the
496
+ machine-readable reasons in `known-gaps.json`. A new ruling lands in all three
497
+ at once.
498
+
438
499
  ## See also
439
500
 
440
501
  - [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;
@@ -193,6 +217,8 @@ interface Snapshot {
193
217
  focusTracking: boolean;
194
218
  origin: boolean;
195
219
  insert: boolean;
220
+ /** LNM — LF also returns the carriage. Absent in older snapshots. */
221
+ newLine: boolean;
196
222
  reverseVideo: boolean;
197
223
  syncOutput: boolean;
198
224
  kittyKeyboardFlags: number;
@@ -402,6 +428,8 @@ interface TerminalModes {
402
428
  focusTracking: boolean;
403
429
  originMode: boolean;
404
430
  insertMode: boolean;
431
+ /** LNM — LF also returns the carriage (ECMA-48 mode 20). */
432
+ newLineMode: boolean;
405
433
  reverseVideo: boolean;
406
434
  syncOutput: boolean;
407
435
  leftRightMargin: boolean;
package/dist/index.mjs CHANGED
@@ -17,6 +17,7 @@ const EMPTY_CELL = Object.freeze({
17
17
  hidden: false,
18
18
  blink: false,
19
19
  wide: false,
20
+ protected: false,
20
21
  url: null
21
22
  });
22
23
  function emptyCell() {
@@ -31,7 +32,7 @@ function emptyCell() {
31
32
  * case), so the read path allocates nothing there.
32
33
  */
33
34
  function stripColorIndex(c) {
34
- if (c === null || c.index === void 0) return c;
35
+ if (c?.index === void 0) return c;
35
36
  return {
36
37
  r: c.r,
37
38
  g: c.g,
@@ -62,6 +63,7 @@ const F_HAS_FG = 4096;
62
63
  const F_HAS_BG = 8192;
63
64
  const F_HAS_UL = 16384;
64
65
  const F_HAS_URL = 32768;
66
+ const F_PROTECTED$1 = 65536;
65
67
  const UL_STYLES = [
66
68
  "none",
67
69
  "single",
@@ -147,6 +149,7 @@ function makePackedRow(width) {
147
149
  if (a.inverse) m |= F_INVERSE$1;
148
150
  if (a.hidden) m |= F_HIDDEN$1;
149
151
  if (a.blink) m |= F_BLINK$1;
152
+ if (a.protected) m |= F_PROTECTED$1;
150
153
  if (wide) m |= F_WIDE$1;
151
154
  return m | underlineToBits(a.underline);
152
155
  }
@@ -234,6 +237,7 @@ function makePackedRow(width) {
234
237
  inverse: (m & F_INVERSE$1) !== 0,
235
238
  hidden: (m & F_HIDDEN$1) !== 0,
236
239
  blink: (m & F_BLINK$1) !== 0,
240
+ protected: (m & F_PROTECTED$1) !== 0,
237
241
  wide: (m & F_WIDE$1) !== 0,
238
242
  url: m & F_HAS_URL ? urlMap.get(col) ?? null : null
239
243
  };
@@ -497,6 +501,7 @@ function createScreen(options = {}) {
497
501
  let focusTracking = false;
498
502
  let originMode = false;
499
503
  let insertMode = false;
504
+ let newLineMode = false;
500
505
  let reverseVideo = false;
501
506
  let syncOutput = false;
502
507
  let kittyKeyboardFlags = 0;
@@ -778,7 +783,68 @@ function createScreen(options = {}) {
778
783
  let mainSoftWrapped = new Array(rows).fill(false);
779
784
  let altSoftWrapped = new Array(rows).fill(false);
780
785
  let softWrapped = mainSoftWrapped;
786
+ let mainLineAttrs = new Array(rows).fill(0);
787
+ let altLineAttrs = new Array(rows).fill(0);
788
+ let lineAttrs = mainLineAttrs;
781
789
  let scrollbackSoftWrapped = [];
790
+ let scrollbackLineAttrs = [];
791
+ /**
792
+ * Drop every banked scrollback row AND its soft-wrap flag.
793
+ *
794
+ * These two arrays are one data structure wearing two names, and nothing in
795
+ * the type system says so. `ESC [ 3 J` (erase display + scrollback) once
796
+ * emptied the rows and left the flags, which diverged the pair permanently
797
+ * by however many rows were banked at that instant. Every later push
798
+ * extended both and preserved the gap, so the snapshot codec refused to
799
+ * write a frame — `soft-wrap length 3580 != rowCount 2103` — and the seat
800
+ * kept running with its recording silently ended
801
+ * (@i/5-agent-loop/vterm-codec-ends-recording).
802
+ *
803
+ * `ESC [ 3 J` is not exotic: `clear` emits it on a modern terminal. Every
804
+ * clear goes through here so the pairing is structural rather than
805
+ * remembered at each call site.
806
+ */
807
+ /** Dense per-row attributes -> sparse `[row, attr]` pairs, skipping normal rows. */
808
+ function toSparseAttrs(dense) {
809
+ const out = [];
810
+ for (let r = 0; r < dense.length; r++) {
811
+ const a = dense[r] ?? 0;
812
+ if (a !== 0) out.push([r, a]);
813
+ }
814
+ return out;
815
+ }
816
+ /** Sparse pairs -> a dense array of `length`; out-of-range rows are dropped. */
817
+ function fromSparseAttrs(sparse, length) {
818
+ const dense = new Array(length).fill(0);
819
+ for (const pair of sparse ?? []) {
820
+ const [r, a] = pair;
821
+ if (Number.isInteger(r) && r >= 0 && r < length) dense[r] = a;
822
+ }
823
+ return dense;
824
+ }
825
+ function clearScrollback() {
826
+ scrollback.length = 0;
827
+ scrollbackSoftWrapped.length = 0;
828
+ scrollbackLineAttrs.length = 0;
829
+ }
830
+ /**
831
+ * Bank one row into scrollback, trimming to the limit. Returns how many rows
832
+ * were trimmed off the front so a caller tracking absolute rows can rebase.
833
+ *
834
+ * Every scrollback mutation goes through a helper that moves BOTH arrays —
835
+ * see `clearScrollback` for what one unpaired mutation cost.
836
+ */
837
+ function bankScrollbackRow(row, wrapped, lineAttr) {
838
+ scrollback.push(row);
839
+ scrollbackSoftWrapped.push(wrapped);
840
+ scrollbackLineAttrs.push(lineAttr);
841
+ if (scrollback.length <= scrollbackLimit * 2) return 0;
842
+ const over = scrollback.length - scrollbackLimit;
843
+ scrollback.splice(0, over);
844
+ scrollbackSoftWrapped.splice(0, over);
845
+ scrollbackLineAttrs.splice(0, over);
846
+ return over;
847
+ }
782
848
  let lastChar = "";
783
849
  let pendingRegionalIndicator = null;
784
850
  let afterZWJ = false;
@@ -912,6 +978,16 @@ function createScreen(options = {}) {
912
978
  fn(cells);
913
979
  row.replaceAllFromCells(cells);
914
980
  }
981
+ /**
982
+ * SGR 0 resets the pen but NOT DECSCA: character protection is independent
983
+ * of SGR, and is cleared only by DECSTR/RIS. Using the full `resetAttrs()`
984
+ * here would let any `ESC [ 0 m` silently unprotect subsequent writes.
985
+ */
986
+ function resetSgrAttrs() {
987
+ const next = resetAttrs();
988
+ next.protected = attrs.protected;
989
+ return next;
990
+ }
915
991
  function resetAttrs() {
916
992
  return {
917
993
  fg: null,
@@ -926,14 +1002,49 @@ function createScreen(options = {}) {
926
1002
  inverse: false,
927
1003
  hidden: false,
928
1004
  blink: false,
929
- url: null
1005
+ url: null,
1006
+ protected: false
930
1007
  };
931
1008
  }
1009
+ /**
1010
+ * Usable columns on a row. A DEC double-width line (DECDWL/DECDHL) renders
1011
+ * each cell twice as wide, so the row addresses and wraps at half the
1012
+ * terminal's columns — an 80-column screen wraps a double-width line at 40.
1013
+ */
1014
+ function rowCols(row) {
1015
+ return (lineAttrs[row] ?? 0) === 0 ? cols : cols >> 1;
1016
+ }
932
1017
  function clampCursor() {
933
1018
  if (curX < 0) curX = 0;
934
- if (curX >= cols) curX = cols - 1;
935
1019
  if (curY < 0) curY = 0;
936
1020
  if (curY >= rows) curY = rows - 1;
1021
+ const usable = rowCols(curY);
1022
+ if (curX >= usable) curX = usable - 1;
1023
+ if (originMode) {
1024
+ if (curY < scrollTop) curY = scrollTop;
1025
+ if (curY > scrollBottom) curY = scrollBottom;
1026
+ }
1027
+ }
1028
+ /**
1029
+ * Move the cursor down one line, scrolling ONLY at the region's bottom edge.
1030
+ *
1031
+ * `curY++; if (curY > scrollBottom) { curY = scrollBottom; scrollUp() }`
1032
+ * looks equivalent and is not. From a row BELOW the scrolling region it
1033
+ * clamps the cursor BACKWARDS into the region and scrolls a region the
1034
+ * cursor was never in — with DECSTBM at 1;10, a linefeed from row 19 landed
1035
+ * on row 9 instead of row 20.
1036
+ *
1037
+ * DECSTBM confines SCROLLING, not movement: a cursor outside the region
1038
+ * simply walks the screen.
1039
+ */
1040
+ function lineFeedDown() {
1041
+ if (curY === scrollBottom) scrollUp(scrollTop, scrollBottom);
1042
+ else if (curY < rows - 1) curY++;
1043
+ }
1044
+ /** RI's mirror: scroll only at the region's top edge, else walk up. */
1045
+ function reverseIndexUp() {
1046
+ if (curY === scrollTop) scrollDown(scrollTop, scrollBottom);
1047
+ else if (curY > 0) curY--;
937
1048
  }
938
1049
  function scrollUp(top, bottom) {
939
1050
  if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
@@ -950,13 +1061,10 @@ function createScreen(options = {}) {
950
1061
  } else {
951
1062
  const enteredScrollback = grid === mainGrid && top === 0;
952
1063
  if (enteredScrollback) {
953
- scrollback.push(grid[0]);
954
- scrollbackSoftWrapped.push(softWrapped[0] ?? false);
955
- markAbsoluteRowDirty(scrollback.length - 1);
956
- if (scrollback.length > scrollbackLimit * 2) {
957
- const over = scrollback.length - scrollbackLimit;
958
- scrollback.splice(0, over);
959
- scrollbackSoftWrapped.splice(0, over);
1064
+ const banked = grid[0];
1065
+ const over = banked === void 0 ? 0 : bankScrollbackRow(banked, softWrapped[0] ?? false, lineAttrs[0] ?? 0);
1066
+ markAbsoluteRowDirty(scrollback.length - 1 + over);
1067
+ if (over > 0) {
960
1068
  if (!dirtyAll && dirtyRows.size > 0) {
961
1069
  const shifted = /* @__PURE__ */ new Set();
962
1070
  for (const r of dirtyRows) if (r >= over) shifted.add(r - over);
@@ -1108,7 +1216,7 @@ function createScreen(options = {}) {
1108
1216
  function writeCharCore(ch, wide) {
1109
1217
  if (cols <= 0 || rows <= 0) return;
1110
1218
  const charWidth = wide ? 2 : 1;
1111
- const wrapBoundary = leftRightMarginMode ? rightMargin + 1 : cols;
1219
+ const wrapBoundary = leftRightMarginMode ? rightMargin + 1 : rowCols(curY);
1112
1220
  const wrapReturn = leftRightMarginMode ? leftMargin : 0;
1113
1221
  if (curX + charWidth > wrapBoundary) if (autoWrap) {
1114
1222
  softWrapped[curY] = true;
@@ -1133,6 +1241,7 @@ function createScreen(options = {}) {
1133
1241
  if (wide) widenCell(row, curX);
1134
1242
  markScreenRowDirty(curY);
1135
1243
  curX += charWidth;
1244
+ if (!autoWrap && curX >= wrapBoundary) curX = wrapBoundary - 1;
1136
1245
  lastChar = ch;
1137
1246
  }
1138
1247
  function handleCSI(params, intermediates, finalByte) {
@@ -1169,6 +1278,10 @@ function createScreen(options = {}) {
1169
1278
  else if (finalByte === "A") handleShiftRight(Math.max(parts[0] ?? 1, 1));
1170
1279
  return;
1171
1280
  }
1281
+ if (intermediates === "\"" && finalByte === "q") {
1282
+ attrs.protected = (parts[0] ?? 0) === 1;
1283
+ return;
1284
+ }
1172
1285
  if (intermediates === "!") {
1173
1286
  if (finalByte === "p") softReset();
1174
1287
  return;
@@ -1278,18 +1391,22 @@ function createScreen(options = {}) {
1278
1391
  }
1279
1392
  switch (finalByte) {
1280
1393
  case "A":
1394
+ case "k":
1281
1395
  curY -= Math.max(parts[0] ?? 1, 1);
1282
1396
  clampCursor();
1283
1397
  break;
1284
1398
  case "B":
1399
+ case "e":
1285
1400
  curY += Math.max(parts[0] ?? 1, 1);
1286
1401
  clampCursor();
1287
1402
  break;
1288
1403
  case "C":
1404
+ case "a":
1289
1405
  curX += Math.max(parts[0] ?? 1, 1);
1290
1406
  clampCursor();
1291
1407
  break;
1292
1408
  case "D":
1409
+ case "j":
1293
1410
  curX -= Math.max(parts[0] ?? 1, 1);
1294
1411
  clampCursor();
1295
1412
  break;
@@ -1351,12 +1468,14 @@ function createScreen(options = {}) {
1351
1468
  case "H":
1352
1469
  case "f":
1353
1470
  if (originMode) {
1471
+ const originX = leftRightMarginMode ? leftMargin : 0;
1472
+ const limitX = leftRightMarginMode ? rightMargin : cols - 1;
1354
1473
  curY = scrollTop + (parts[0] ?? 1) - 1;
1355
- curX = (parts[1] ?? 1) - 1;
1474
+ curX = originX + (parts[1] ?? 1) - 1;
1356
1475
  if (curY < scrollTop) curY = scrollTop;
1357
1476
  if (curY > scrollBottom) curY = scrollBottom;
1358
- if (curX < 0) curX = 0;
1359
- if (curX >= cols) curX = cols - 1;
1477
+ if (curX < originX) curX = originX;
1478
+ if (curX > limitX) curX = limitX;
1360
1479
  } else {
1361
1480
  curY = (parts[0] ?? 1) - 1;
1362
1481
  curX = (parts[1] ?? 1) - 1;
@@ -1427,10 +1546,16 @@ function createScreen(options = {}) {
1427
1546
  }
1428
1547
  break;
1429
1548
  case "h":
1430
- for (const code of parts) if (code === 4) insertMode = true;
1549
+ for (const code of parts) {
1550
+ if (code === 4) insertMode = true;
1551
+ if (code === 20) newLineMode = true;
1552
+ }
1431
1553
  break;
1432
1554
  case "l":
1433
- for (const code of parts) if (code === 4) insertMode = false;
1555
+ for (const code of parts) {
1556
+ if (code === 4) insertMode = false;
1557
+ if (code === 20) newLineMode = false;
1558
+ }
1434
1559
  break;
1435
1560
  case "s":
1436
1561
  if (leftRightMarginMode && intermediates === "") {
@@ -1551,6 +1676,9 @@ function createScreen(options = {}) {
1551
1676
  case 4:
1552
1677
  value = insertMode ? 1 : 2;
1553
1678
  break;
1679
+ case 20:
1680
+ value = newLineMode ? 1 : 2;
1681
+ break;
1554
1682
  case 3:
1555
1683
  value = decColumnMode ? 1 : 2;
1556
1684
  break;
@@ -1577,11 +1705,11 @@ function createScreen(options = {}) {
1577
1705
  return;
1578
1706
  }
1579
1707
  if (finalByte === "J") {
1580
- handleEraseDisplay(parts[0] ?? 0);
1708
+ handleEraseDisplay(parts[0] ?? 0, true);
1581
1709
  return;
1582
1710
  }
1583
1711
  if (finalByte === "K") {
1584
- handleEraseLine(parts[0] ?? 0);
1712
+ handleEraseLine(parts[0] ?? 0, true);
1585
1713
  return;
1586
1714
  }
1587
1715
  const set = finalByte === "h";
@@ -1610,11 +1738,13 @@ function createScreen(options = {}) {
1610
1738
  useAltScreen = true;
1611
1739
  grid = altGrid;
1612
1740
  softWrapped = altSoftWrapped;
1741
+ lineAttrs = altLineAttrs;
1613
1742
  markAllDirty();
1614
1743
  } else if (!set && useAltScreen) {
1615
1744
  useAltScreen = false;
1616
1745
  grid = mainGrid;
1617
1746
  softWrapped = mainSoftWrapped;
1747
+ lineAttrs = mainLineAttrs;
1618
1748
  markAllDirty();
1619
1749
  }
1620
1750
  break;
@@ -1643,8 +1773,10 @@ function createScreen(options = {}) {
1643
1773
  useAltScreen = true;
1644
1774
  altGrid = makeGrid(cols, rows);
1645
1775
  altSoftWrapped = new Array(rows).fill(false);
1776
+ altLineAttrs = new Array(rows).fill(0);
1646
1777
  grid = altGrid;
1647
1778
  softWrapped = altSoftWrapped;
1779
+ lineAttrs = altLineAttrs;
1648
1780
  curX = 0;
1649
1781
  curY = 0;
1650
1782
  markAllDirty();
@@ -1652,6 +1784,7 @@ function createScreen(options = {}) {
1652
1784
  useAltScreen = false;
1653
1785
  grid = mainGrid;
1654
1786
  softWrapped = mainSoftWrapped;
1787
+ lineAttrs = mainLineAttrs;
1655
1788
  curX = savedCurX;
1656
1789
  curY = savedCurY;
1657
1790
  clampCursor();
@@ -1698,43 +1831,46 @@ function createScreen(options = {}) {
1698
1831
  break;
1699
1832
  }
1700
1833
  }
1701
- function handleEraseDisplay(mode) {
1834
+ function handleEraseDisplay(mode, selective = false) {
1702
1835
  switch (mode) {
1703
1836
  case 0:
1704
- eraseCells(curY, curX, curY, cols - 1);
1705
- for (let row = curY + 1; row < rows; row++) eraseCells(row, 0, row, cols - 1);
1837
+ eraseCells(curY, curX, curY, cols - 1, selective);
1838
+ for (let row = curY + 1; row < rows; row++) eraseCells(row, 0, row, cols - 1, selective);
1706
1839
  break;
1707
1840
  case 1:
1708
- for (let row = 0; row < curY; row++) eraseCells(row, 0, row, cols - 1);
1709
- eraseCells(curY, 0, curY, curX);
1841
+ for (let row = 0; row < curY; row++) eraseCells(row, 0, row, cols - 1, selective);
1842
+ eraseCells(curY, 0, curY, curX, selective);
1710
1843
  break;
1711
1844
  case 2:
1712
1845
  case 3:
1713
- for (let row = 0; row < rows; row++) eraseCells(row, 0, row, cols - 1);
1714
- if (mode === 3) scrollback.length = 0;
1846
+ for (let row = 0; row < rows; row++) eraseCells(row, 0, row, cols - 1, selective);
1847
+ if (mode === 3) clearScrollback();
1715
1848
  markAllDirty();
1716
1849
  break;
1717
1850
  }
1718
1851
  }
1719
- function handleEraseLine(mode) {
1852
+ function handleEraseLine(mode, selective = false) {
1720
1853
  const eraseRight = leftRightMarginMode ? rightMargin : cols - 1;
1721
1854
  const eraseLeft = leftRightMarginMode ? leftMargin : 0;
1722
1855
  switch (mode) {
1723
1856
  case 0:
1724
- eraseCells(curY, curX, curY, eraseRight);
1857
+ eraseCells(curY, curX, curY, eraseRight, selective);
1725
1858
  break;
1726
1859
  case 1:
1727
- eraseCells(curY, eraseLeft, curY, curX);
1860
+ eraseCells(curY, eraseLeft, curY, curX, selective);
1728
1861
  break;
1729
1862
  case 2:
1730
- eraseCells(curY, eraseLeft, curY, eraseRight);
1863
+ eraseCells(curY, eraseLeft, curY, eraseRight, selective);
1731
1864
  break;
1732
1865
  }
1733
1866
  }
1734
- function eraseCells(row, startCol, _endRow, endCol) {
1867
+ function eraseCells(row, startCol, _endRow, endCol, selective = false) {
1735
1868
  const r = grid[row];
1736
1869
  if (!r) return;
1737
- for (let col = startCol; col <= endCol && col < cols; col++) r.eraseWithBg(col, attrs.bg);
1870
+ for (let col = startCol; col <= endCol && col < cols; col++) {
1871
+ if (selective && r.getCellRaw(col).protected) continue;
1872
+ r.eraseWithBg(col, attrs.bg);
1873
+ }
1738
1874
  markScreenRowDirty(row);
1739
1875
  }
1740
1876
  function handleInsertLines(count) {
@@ -1893,7 +2029,7 @@ function createScreen(options = {}) {
1893
2029
  params.push(subs[0]);
1894
2030
  } else params.push(seg === "" ? 0 : parseInt(seg, 10));
1895
2031
  if (params.length === 0 || params.length === 1 && params[0] === 0) {
1896
- attrs = resetAttrs();
2032
+ attrs = resetSgrAttrs();
1897
2033
  return;
1898
2034
  }
1899
2035
  let i = 0;
@@ -1901,7 +2037,7 @@ function createScreen(options = {}) {
1901
2037
  const code = params[i];
1902
2038
  switch (code) {
1903
2039
  case 0:
1904
- attrs = resetAttrs();
2040
+ attrs = resetSgrAttrs();
1905
2041
  break;
1906
2042
  case 1:
1907
2043
  attrs.bold = true;
@@ -2602,6 +2738,7 @@ function createScreen(options = {}) {
2602
2738
  }
2603
2739
  function softReset() {
2604
2740
  insertMode = false;
2741
+ newLineMode = false;
2605
2742
  originMode = false;
2606
2743
  autoWrap = true;
2607
2744
  curVisible = true;
@@ -2659,6 +2796,7 @@ function createScreen(options = {}) {
2659
2796
  focusTracking = false;
2660
2797
  originMode = false;
2661
2798
  insertMode = false;
2799
+ newLineMode = false;
2662
2800
  reverseVideo = false;
2663
2801
  syncOutput = false;
2664
2802
  kittyKeyboardFlags = 0;
@@ -2717,6 +2855,9 @@ function createScreen(options = {}) {
2717
2855
  mainSoftWrapped = new Array(rows).fill(false);
2718
2856
  altSoftWrapped = new Array(rows).fill(false);
2719
2857
  softWrapped = mainSoftWrapped;
2858
+ mainLineAttrs = new Array(rows).fill(0);
2859
+ altLineAttrs = new Array(rows).fill(0);
2860
+ lineAttrs = mainLineAttrs;
2720
2861
  markAllDirty();
2721
2862
  dirtyScrolled = 0;
2722
2863
  if (sigDamageActive) sigDirtyScrolled = 0;
@@ -2868,11 +3009,8 @@ function createScreen(options = {}) {
2868
3009
  emitExecute(9);
2869
3010
  } else if (code === 10 || code === 11 || code === 12) {
2870
3011
  softWrapped[curY] = false;
2871
- curY++;
2872
- if (curY > scrollBottom) {
2873
- curY = scrollBottom;
2874
- scrollUp(scrollTop, scrollBottom);
2875
- }
3012
+ lineFeedDown();
3013
+ if (newLineMode) curX = 0;
2876
3014
  emitExecute(code);
2877
3015
  } else if (code === 13) {
2878
3016
  curX = 0;
@@ -2910,19 +3048,11 @@ function createScreen(options = {}) {
2910
3048
  parserState = "ground";
2911
3049
  emitEsc("c", "");
2912
3050
  } else if (ch === "D") {
2913
- curY++;
2914
- if (curY > scrollBottom) {
2915
- curY = scrollBottom;
2916
- scrollUp(scrollTop, scrollBottom);
2917
- }
3051
+ lineFeedDown();
2918
3052
  parserState = "ground";
2919
3053
  emitEsc("D", "");
2920
3054
  } else if (ch === "M") {
2921
- curY--;
2922
- if (curY < scrollTop) {
2923
- curY = scrollTop;
2924
- scrollDown(scrollTop, scrollBottom);
2925
- }
3055
+ reverseIndexUp();
2926
3056
  parserState = "ground";
2927
3057
  emitEsc("M", "");
2928
3058
  } else if (ch === "7") {
@@ -3012,7 +3142,10 @@ function createScreen(options = {}) {
3012
3142
  }
3013
3143
  curX = 0;
3014
3144
  curY = 0;
3015
- }
3145
+ } else if (ch === "3") lineAttrs[curY] = 2;
3146
+ else if (ch === "4") lineAttrs[curY] = 3;
3147
+ else if (ch === "5") lineAttrs[curY] = 0;
3148
+ else if (ch === "6") lineAttrs[curY] = 1;
3016
3149
  parserState = "ground";
3017
3150
  emitEsc(ch, escIntermediate);
3018
3151
  break;
@@ -3129,9 +3262,25 @@ function createScreen(options = {}) {
3129
3262
  * trailing blanks or one past the last cell) lands after the last emitted
3130
3263
  * cell, advanced by the leftover distance, clamped to the row.
3131
3264
  */
3265
+ /**
3266
+ * One line attribute per LOGICAL line, taken from the line's FIRST physical
3267
+ * row — the grouping `getLogicalLines` uses, so the two arrays line up.
3268
+ * A rewrap then re-applies the attribute to every row the line becomes.
3269
+ */
3270
+ function logicalLineAttrs(srcGrid, srcSoftWrapped, srcLineAttrs, srcRows) {
3271
+ const out = [];
3272
+ let starting = true;
3273
+ for (let r = 0; r < srcRows; r++) {
3274
+ if (!srcGrid[r]) continue;
3275
+ if (starting) out.push(srcLineAttrs[r] ?? 0);
3276
+ starting = !srcSoftWrapped[r];
3277
+ }
3278
+ return out;
3279
+ }
3132
3280
  function rewrapLines(logicalLines, newCols, track) {
3133
3281
  const outRows = [];
3134
3282
  const outWrapped = [];
3283
+ const outLine = [];
3135
3284
  let tracked = null;
3136
3285
  for (let li = 0; li < logicalLines.length; li++) {
3137
3286
  const line = logicalLines[li];
@@ -3144,7 +3293,8 @@ function createScreen(options = {}) {
3144
3293
  if (lineLen === 0) {
3145
3294
  outRows.push(makeRow(newCols));
3146
3295
  outWrapped.push(false);
3147
- if (track !== void 0 && track.line === li) tracked = {
3296
+ outLine.push(li);
3297
+ if (track?.line === li) tracked = {
3148
3298
  row: outRows.length - 1,
3149
3299
  col: Math.min(track.offset, newCols - 1)
3150
3300
  };
@@ -3157,7 +3307,7 @@ function createScreen(options = {}) {
3157
3307
  while (col < newCols && pos < lineLen) {
3158
3308
  const cell = line[pos];
3159
3309
  if (cell.wide && col + 2 > newCols) break;
3160
- if (track !== void 0 && track.line === li && pos === track.offset) tracked = {
3310
+ if (track?.line === li && pos === track.offset) tracked = {
3161
3311
  row: outRows.length,
3162
3312
  col
3163
3313
  };
@@ -3168,7 +3318,8 @@ function createScreen(options = {}) {
3168
3318
  const moreContent = pos < lineLen;
3169
3319
  outRows.push(row);
3170
3320
  outWrapped.push(moreContent);
3171
- if (!moreContent && track !== void 0 && track.line === li && tracked === null) {
3321
+ outLine.push(li);
3322
+ if (!moreContent && track?.line === li && tracked === null) {
3172
3323
  const extra = track.offset - lineLen;
3173
3324
  tracked = {
3174
3325
  row: outRows.length - 1,
@@ -3180,6 +3331,7 @@ function createScreen(options = {}) {
3180
3331
  return {
3181
3332
  rows: outRows,
3182
3333
  wrapped: outWrapped,
3334
+ lineIndex: outLine,
3183
3335
  tracked
3184
3336
  };
3185
3337
  }
@@ -3191,13 +3343,20 @@ function createScreen(options = {}) {
3191
3343
  /**
3192
3344
  * Trim trailing empty rows from reflowed result, so they don't push content off the top
3193
3345
  * when we take the last newRows rows.
3346
+ *
3347
+ * Never trims the row the CURSOR is on. A shell that has just emitted its
3348
+ * final newline sits on a blank last row, and dropping it makes the content
3349
+ * measure one row shorter than it is — so a shrink banks one row too few and
3350
+ * every surviving line lands one row low, taking the cursor with it.
3194
3351
  */
3195
3352
  function trimTrailingEmptyRows(result) {
3196
- while (result.rows.length > 1) {
3353
+ const floor = result.tracked == null ? 1 : Math.max(1, result.tracked.row + 1);
3354
+ while (result.rows.length > floor) {
3197
3355
  const lastRow = result.rows[result.rows.length - 1];
3198
3356
  if (packedRowIsBlank(lastRow) && !result.wrapped[result.rows.length - 2]) {
3199
3357
  result.rows.pop();
3200
3358
  result.wrapped.pop();
3359
+ result.lineIndex?.pop();
3201
3360
  } else break;
3202
3361
  }
3203
3362
  }
@@ -3213,23 +3372,56 @@ function createScreen(options = {}) {
3213
3372
  line: cursorLineIdx,
3214
3373
  offset: (cursorRowPre - cursorLineStart) * cols + curX
3215
3374
  };
3216
- const mainResult = rewrapLines(getLogicalLines(mainGrid, mainSoftWrapped, rows), newCols, useAltScreen ? void 0 : cursorTrack);
3375
+ let restoredRows = [];
3376
+ let restoredWrapped = [];
3377
+ let restoredAttrs = [];
3378
+ if (!useAltScreen && newRows > rows && scrollback.length > 0) {
3379
+ const probe = rewrapLines(getLogicalLines(mainGrid, mainSoftWrapped, rows), newCols, void 0);
3380
+ trimTrailingEmptyRows(probe);
3381
+ const take = Math.min(Math.max(0, newRows - probe.rows.length), scrollback.length);
3382
+ if (take > 0) {
3383
+ restoredRows = scrollback.splice(scrollback.length - take, take);
3384
+ restoredWrapped = scrollbackSoftWrapped.splice(scrollbackSoftWrapped.length - take, take);
3385
+ restoredAttrs = scrollbackLineAttrs.splice(scrollbackLineAttrs.length - take, take);
3386
+ }
3387
+ }
3388
+ if (restoredRows.length > 0) {
3389
+ let completed = 0;
3390
+ for (const wrapped of restoredWrapped) if (!wrapped) completed++;
3391
+ if (cursorTrack.line === 0 && restoredWrapped[restoredWrapped.length - 1] === true) for (let i = restoredWrapped.length - 1; i >= 0 && restoredWrapped[i]; i--) cursorTrack.offset += restoredRows[i].length;
3392
+ cursorTrack.line += completed;
3393
+ }
3394
+ const mainSrc = restoredRows.length > 0 ? [...restoredRows, ...mainGrid] : mainGrid;
3395
+ const mainSrcWrapped = restoredRows.length > 0 ? [...restoredWrapped, ...mainSoftWrapped] : mainSoftWrapped;
3396
+ const mainSrcAttrs = restoredRows.length > 0 ? [...restoredAttrs, ...mainLineAttrs] : mainLineAttrs;
3397
+ const mainSrcRows = restoredRows.length + rows;
3398
+ const mainLogicalAttrs = logicalLineAttrs(mainSrc, mainSrcWrapped, mainSrcAttrs, mainSrcRows);
3399
+ const mainResult = rewrapLines(getLogicalLines(mainSrc, mainSrcWrapped, mainSrcRows), newCols, useAltScreen ? void 0 : cursorTrack);
3217
3400
  trimTrailingEmptyRows(mainResult);
3401
+ const altLogicalAttrs = logicalLineAttrs(altGrid, altSoftWrapped, altLineAttrs, rows);
3218
3402
  const altResult = rewrapLines(getLogicalLines(altGrid, altSoftWrapped, rows), newCols, useAltScreen ? cursorTrack : void 0);
3219
3403
  trimTrailingEmptyRows(altResult);
3220
3404
  const newMain = makeGrid(newCols, newRows);
3221
3405
  const newMainWrapped = new Array(newRows).fill(false);
3406
+ const newMainAttrs = new Array(newRows).fill(0);
3222
3407
  const mainStartRow = Math.max(0, mainResult.rows.length - newRows);
3408
+ for (let r = 0; r < mainStartRow; r++) {
3409
+ const overflow = mainResult.rows[r];
3410
+ if (overflow !== void 0) bankScrollbackRow(overflow, mainResult.wrapped[r] ?? false, mainLogicalAttrs[mainResult.lineIndex[r] ?? 0] ?? 0);
3411
+ }
3223
3412
  for (let r = 0; r < newRows && mainStartRow + r < mainResult.rows.length; r++) {
3224
3413
  newMain[r] = mainResult.rows[mainStartRow + r];
3225
3414
  newMainWrapped[r] = mainResult.wrapped[mainStartRow + r];
3415
+ newMainAttrs[r] = mainLogicalAttrs[mainResult.lineIndex[mainStartRow + r] ?? 0] ?? 0;
3226
3416
  }
3227
3417
  const newAlt = makeGrid(newCols, newRows);
3228
3418
  const newAltWrapped = new Array(newRows).fill(false);
3419
+ const newAltAttrs = new Array(newRows).fill(0);
3229
3420
  const altStartRow = Math.max(0, altResult.rows.length - newRows);
3230
3421
  for (let r = 0; r < newRows && altStartRow + r < altResult.rows.length; r++) {
3231
3422
  newAlt[r] = altResult.rows[altStartRow + r];
3232
3423
  newAltWrapped[r] = altResult.wrapped[altStartRow + r];
3424
+ newAltAttrs[r] = altLogicalAttrs[altResult.lineIndex[altStartRow + r] ?? 0] ?? 0;
3233
3425
  }
3234
3426
  const activeResult = useAltScreen ? altResult : mainResult;
3235
3427
  const activeStartRow = useAltScreen ? altStartRow : mainStartRow;
@@ -3241,8 +3433,11 @@ function createScreen(options = {}) {
3241
3433
  altGrid = newAlt;
3242
3434
  mainSoftWrapped = newMainWrapped;
3243
3435
  altSoftWrapped = newAltWrapped;
3436
+ mainLineAttrs = newMainAttrs;
3437
+ altLineAttrs = newAltAttrs;
3244
3438
  grid = useAltScreen ? altGrid : mainGrid;
3245
3439
  softWrapped = useAltScreen ? altSoftWrapped : mainSoftWrapped;
3440
+ lineAttrs = useAltScreen ? altLineAttrs : mainLineAttrs;
3246
3441
  const oldCols = cols;
3247
3442
  cols = newCols;
3248
3443
  rows = newRows;
@@ -3277,6 +3472,11 @@ function createScreen(options = {}) {
3277
3472
  },
3278
3473
  scrollback: cloneGridSnapshot(scrollback),
3279
3474
  scrollbackSoftWrapped: [...scrollbackSoftWrapped],
3475
+ lineAttrs: {
3476
+ main: toSparseAttrs(mainLineAttrs),
3477
+ alt: toSparseAttrs(altLineAttrs),
3478
+ scrollback: toSparseAttrs(scrollbackLineAttrs)
3479
+ },
3280
3480
  cursor: {
3281
3481
  x: curX,
3282
3482
  y: curY,
@@ -3306,6 +3506,7 @@ function createScreen(options = {}) {
3306
3506
  focusTracking,
3307
3507
  origin: originMode,
3308
3508
  insert: insertMode,
3509
+ newLine: newLineMode,
3309
3510
  reverseVideo,
3310
3511
  syncOutput,
3311
3512
  kittyKeyboardFlags,
@@ -3372,9 +3573,13 @@ function createScreen(options = {}) {
3372
3573
  scrollbackSoftWrapped = snapshotValue.scrollbackSoftWrapped ? [...snapshotValue.scrollbackSoftWrapped] : new Array(scrollback.length).fill(false);
3373
3574
  mainSoftWrapped = [...snapshotValue.main.softWrapped];
3374
3575
  altSoftWrapped = [...snapshotValue.alt.softWrapped];
3576
+ mainLineAttrs = fromSparseAttrs(snapshotValue.lineAttrs?.main, mainSoftWrapped.length);
3577
+ altLineAttrs = fromSparseAttrs(snapshotValue.lineAttrs?.alt, altSoftWrapped.length);
3578
+ scrollbackLineAttrs = fromSparseAttrs(snapshotValue.lineAttrs?.scrollback, scrollback.length);
3375
3579
  useAltScreen = snapshotValue.activeBuffer === "alt";
3376
3580
  grid = useAltScreen ? altGrid : mainGrid;
3377
3581
  softWrapped = useAltScreen ? altSoftWrapped : mainSoftWrapped;
3582
+ lineAttrs = useAltScreen ? altLineAttrs : mainLineAttrs;
3378
3583
  curX = snapshotValue.cursor.x;
3379
3584
  curY = snapshotValue.cursor.y;
3380
3585
  curVisible = snapshotValue.cursor.visible;
@@ -3401,6 +3606,7 @@ function createScreen(options = {}) {
3401
3606
  focusTracking = snapshotValue.modes.focusTracking;
3402
3607
  originMode = snapshotValue.modes.origin;
3403
3608
  insertMode = snapshotValue.modes.insert;
3609
+ newLineMode = snapshotValue.modes.newLine ?? false;
3404
3610
  reverseVideo = snapshotValue.modes.reverseVideo;
3405
3611
  syncOutput = snapshotValue.modes.syncOutput;
3406
3612
  kittyKeyboardFlags = snapshotValue.modes.kittyKeyboardFlags;
@@ -3558,6 +3764,7 @@ function createScreen(options = {}) {
3558
3764
  case "focusTracking": return focusTracking;
3559
3765
  case "originMode": return originMode;
3560
3766
  case "insertMode": return insertMode;
3767
+ case "newLineMode": return newLineMode;
3561
3768
  case "reverseVideo": return reverseVideo;
3562
3769
  case "syncOutput": return syncOutput;
3563
3770
  case "sgrMouse": return sgrMouse;
@@ -3637,6 +3844,7 @@ function createScreen(options = {}) {
3637
3844
  focusTracking,
3638
3845
  originMode,
3639
3846
  insertMode,
3847
+ newLineMode,
3640
3848
  reverseVideo,
3641
3849
  syncOutput,
3642
3850
  leftRightMargin: leftRightMarginMode,
@@ -3647,7 +3855,7 @@ function createScreen(options = {}) {
3647
3855
  };
3648
3856
  }
3649
3857
  function modesEq(a, b) {
3650
- return a.altScreen === b.altScreen && a.cursorVisible === b.cursorVisible && a.bracketedPaste === b.bracketedPaste && a.applicationCursor === b.applicationCursor && a.applicationKeypad === b.applicationKeypad && a.autoWrap === b.autoWrap && a.mouseTracking === b.mouseTracking && a.mouseTrackingMode === b.mouseTrackingMode && a.sgrMouse === b.sgrMouse && a.utf8Mouse === b.utf8Mouse && a.focusTracking === b.focusTracking && a.originMode === b.originMode && a.insertMode === b.insertMode && a.reverseVideo === b.reverseVideo && a.syncOutput === b.syncOutput && a.leftRightMargin === b.leftRightMargin && a.colorSchemeReporting === b.colorSchemeReporting && a.kittyKeyboard === b.kittyKeyboard && a.kittyGraphics === b.kittyGraphics && a.sixel === b.sixel;
3858
+ return a.altScreen === b.altScreen && a.cursorVisible === b.cursorVisible && a.bracketedPaste === b.bracketedPaste && a.applicationCursor === b.applicationCursor && a.applicationKeypad === b.applicationKeypad && a.autoWrap === b.autoWrap && a.mouseTracking === b.mouseTracking && a.mouseTrackingMode === b.mouseTrackingMode && a.sgrMouse === b.sgrMouse && a.utf8Mouse === b.utf8Mouse && a.focusTracking === b.focusTracking && a.originMode === b.originMode && a.insertMode === b.insertMode && a.newLineMode === b.newLineMode && a.reverseVideo === b.reverseVideo && a.syncOutput === b.syncOutput && a.leftRightMargin === b.leftRightMargin && a.colorSchemeReporting === b.colorSchemeReporting && a.kittyKeyboard === b.kittyKeyboard && a.kittyGraphics === b.kittyGraphics && a.sixel === b.sixel;
3651
3859
  }
3652
3860
  const damageListeners = /* @__PURE__ */ new Set();
3653
3861
  let damageLast = {
@@ -4112,10 +4320,12 @@ function serializeSnapshot(snapshot, options = {}) {
4112
4320
  if (hyperlinks && snapshot.attrs.url !== null) out.push(`\x1b]8;;${snapshot.attrs.url}\x1b\\`);
4113
4321
  out.push(snapshot.cursor.visible ? "\x1B[?25h" : "\x1B[?25l");
4114
4322
  const origin = m.origin && !excluded.has("origin");
4115
- const inRegion = snapshot.cursor.y >= snapshot.margins.scrollTop && snapshot.cursor.y <= snapshot.margins.scrollBottom;
4323
+ const originX = snapshot.margins.leftRight ? snapshot.margins.left : 0;
4324
+ const limitX = snapshot.margins.leftRight ? snapshot.margins.right : snapshot.cols - 1;
4325
+ const inRegion = snapshot.cursor.y >= snapshot.margins.scrollTop && snapshot.cursor.y <= snapshot.margins.scrollBottom && snapshot.cursor.x >= originX && snapshot.cursor.x <= limitX;
4116
4326
  if (origin && inRegion) {
4117
4327
  out.push("\x1B[?6h");
4118
- out.push(`\x1b[${String(snapshot.cursor.y - snapshot.margins.scrollTop + 1)};${String(snapshot.cursor.x + 1)}H`);
4328
+ out.push(`\x1b[${String(snapshot.cursor.y - snapshot.margins.scrollTop + 1)};${String(snapshot.cursor.x - originX + 1)}H`);
4119
4329
  } else {
4120
4330
  out.push(`\x1b[${String(snapshot.cursor.y + 1)};${String(snapshot.cursor.x + 1)}H`);
4121
4331
  if (origin) out.push("\x1B[?6h");
@@ -4138,6 +4348,7 @@ const F_BLINK = 128;
4138
4348
  const F_WIDE = 256;
4139
4349
  const UL_SHIFT = 9;
4140
4350
  const UL_MASK = 7;
4351
+ const F_PROTECTED = 65536;
4141
4352
  const UNDERLINE_STYLES = [
4142
4353
  "none",
4143
4354
  "single",
@@ -4368,6 +4579,7 @@ function createInterner() {
4368
4579
  if (cell.inverse) flags |= F_INVERSE;
4369
4580
  if (cell.hidden) flags |= F_HIDDEN;
4370
4581
  if (cell.blink) flags |= F_BLINK;
4582
+ if (cell.protected) flags |= F_PROTECTED;
4371
4583
  if (cell.wide) flags |= F_WIDE;
4372
4584
  flags |= ulIdx << UL_SHIFT;
4373
4585
  const key = `${flags}|${fgRef}|${bgRef}|${ulRef}|${urlRef}`;
@@ -4536,6 +4748,7 @@ function makeCell(charIdx, styleIdx, strings, colors, styles) {
4536
4748
  inverse: (style.flags & F_INVERSE) !== 0,
4537
4749
  hidden: (style.flags & F_HIDDEN) !== 0,
4538
4750
  blink: (style.flags & F_BLINK) !== 0,
4751
+ protected: (style.flags & F_PROTECTED) !== 0,
4539
4752
  wide: (style.flags & F_WIDE) !== 0,
4540
4753
  url
4541
4754
  };
@@ -4589,6 +4802,7 @@ function encodeScreenSnapshotBinary(snapshot) {
4589
4802
  savedState: snapshot.savedState,
4590
4803
  attrs: snapshot.attrs,
4591
4804
  modes: snapshot.modes,
4805
+ lineAttrs: snapshot.lineAttrs,
4592
4806
  margins: snapshot.margins,
4593
4807
  colors: snapshot.colors,
4594
4808
  tabStops: snapshot.tabStops,
@@ -4656,6 +4870,7 @@ function decodeScreenSnapshotBinary(bytes) {
4656
4870
  savedState: header.savedState,
4657
4871
  attrs: header.attrs,
4658
4872
  modes: header.modes,
4873
+ lineAttrs: header.lineAttrs,
4659
4874
  margins: header.margins,
4660
4875
  colors: header.colors,
4661
4876
  tabStops: header.tabStops,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vterm.js",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Modern terminal emulator — full VT/ECMA-48/xterm standards coverage. Pure TypeScript, zero dependencies.",
5
5
  "keywords": [
6
6
  "ansi",