vterm.js 0.4.0 → 0.5.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/dist/index.mjs CHANGED
@@ -20,6 +20,248 @@ const EMPTY_CELL = Object.freeze({
20
20
  function emptyCell() {
21
21
  return { ...EMPTY_CELL };
22
22
  }
23
+ /**
24
+ * Drop the palette-origin {@link CellColor.index} for the public per-cell read
25
+ * boundary. `getCell`/`getLine` return the RESOLVED RGB — their long-standing
26
+ * contract — while the origin index is serialization-only provenance that rides
27
+ * {@link ScreenSnapshot} (and thus `serialize()`), not the inspection API.
28
+ * Returns the SAME reference when there is no index (the common truecolor/null
29
+ * case), so the read path allocates nothing there.
30
+ */
31
+ function stripColorIndex(c) {
32
+ if (c === null || c.index === void 0) return c;
33
+ return {
34
+ r: c.r,
35
+ g: c.g,
36
+ b: c.b
37
+ };
38
+ }
39
+ /** Shallow cell copy with each color stripped of its palette-origin index. */
40
+ function stripCellColorIndex(cell) {
41
+ return {
42
+ ...cell,
43
+ fg: stripColorIndex(cell.fg),
44
+ bg: stripColorIndex(cell.bg),
45
+ underlineColor: stripColorIndex(cell.underlineColor)
46
+ };
47
+ }
48
+ const F_BOLD$1 = 1;
49
+ const F_FAINT$1 = 2;
50
+ const F_ITALIC$1 = 4;
51
+ const F_OVERLINE$1 = 8;
52
+ const F_STRIKE$1 = 16;
53
+ const F_INVERSE$1 = 32;
54
+ const F_HIDDEN$1 = 64;
55
+ const F_BLINK$1 = 128;
56
+ const F_WIDE$1 = 256;
57
+ const UL_SHIFT$1 = 9;
58
+ const UL_MASK$1 = 7 << UL_SHIFT$1;
59
+ const F_HAS_FG = 4096;
60
+ const F_HAS_BG = 8192;
61
+ const F_HAS_UL = 16384;
62
+ const F_HAS_URL = 32768;
63
+ const UL_STYLES = [
64
+ "none",
65
+ "single",
66
+ "double",
67
+ "curly",
68
+ "dotted",
69
+ "dashed"
70
+ ];
71
+ function underlineToBits(u) {
72
+ const i = UL_STYLES.indexOf(u);
73
+ return (i < 0 ? 0 : i) << UL_SHIFT$1;
74
+ }
75
+ function bitsToUnderline(meta) {
76
+ return UL_STYLES[(meta & UL_MASK$1) >> UL_SHIFT$1] ?? "none";
77
+ }
78
+ /** Pack a color's 8-bit channels into a 24-bit `0xRRGGBB` word. */
79
+ function packRgb(c) {
80
+ return ((c.r & 255) << 16 | (c.g & 255) << 8 | c.b & 255) >>> 0;
81
+ }
82
+ /** Materialize a {@link CellColor} from a packed RGB word + index (`-1` = no index). */
83
+ function unpackColor(rgb, idx) {
84
+ const r = rgb >> 16 & 255;
85
+ const g = rgb >> 8 & 255;
86
+ const b = rgb & 255;
87
+ return idx >= 0 ? {
88
+ r,
89
+ g,
90
+ b,
91
+ index: idx
92
+ } : {
93
+ r,
94
+ g,
95
+ b
96
+ };
97
+ }
98
+ function makePackedRow(width) {
99
+ const meta = new Uint32Array(width);
100
+ const chars = new Array(width).fill("");
101
+ let fgRgb = null;
102
+ let fgIdx = null;
103
+ let bgRgb = null;
104
+ let bgIdx = null;
105
+ let ulRgb = null;
106
+ let ulIdx = null;
107
+ let urlMap = null;
108
+ function setFg(col, c) {
109
+ if (fgRgb === null) {
110
+ fgRgb = new Uint32Array(width);
111
+ fgIdx = new Int16Array(width);
112
+ }
113
+ fgRgb[col] = packRgb(c);
114
+ fgIdx[col] = c.index ?? -1;
115
+ }
116
+ function setBg(col, c) {
117
+ if (bgRgb === null) {
118
+ bgRgb = new Uint32Array(width);
119
+ bgIdx = new Int16Array(width);
120
+ }
121
+ bgRgb[col] = packRgb(c);
122
+ bgIdx[col] = c.index ?? -1;
123
+ }
124
+ function setUl(col, c) {
125
+ if (ulRgb === null) {
126
+ ulRgb = new Uint32Array(width);
127
+ ulIdx = new Int16Array(width);
128
+ }
129
+ ulRgb[col] = packRgb(c);
130
+ ulIdx[col] = c.index ?? -1;
131
+ }
132
+ function setUrl(col, u) {
133
+ (urlMap ??= /* @__PURE__ */ new Map()).set(col, u);
134
+ }
135
+ function clearUrl(col) {
136
+ if (urlMap !== null && urlMap.size > 0) urlMap.delete(col);
137
+ }
138
+ function packFlags(a, wide) {
139
+ let m = 0;
140
+ if (a.bold) m |= F_BOLD$1;
141
+ if (a.faint) m |= F_FAINT$1;
142
+ if (a.italic) m |= F_ITALIC$1;
143
+ if (a.overline) m |= F_OVERLINE$1;
144
+ if (a.strikethrough) m |= F_STRIKE$1;
145
+ if (a.inverse) m |= F_INVERSE$1;
146
+ if (a.hidden) m |= F_HIDDEN$1;
147
+ if (a.blink) m |= F_BLINK$1;
148
+ if (wide) m |= F_WIDE$1;
149
+ return m | underlineToBits(a.underline);
150
+ }
151
+ const row = {
152
+ length: width,
153
+ writeFromAttrs(col, ch, a, wide) {
154
+ let m = packFlags(a, wide);
155
+ chars[col] = ch;
156
+ if (a.fg) {
157
+ setFg(col, a.fg);
158
+ m |= F_HAS_FG;
159
+ }
160
+ if (a.bg) {
161
+ setBg(col, a.bg);
162
+ m |= F_HAS_BG;
163
+ }
164
+ if (a.underlineColor) {
165
+ setUl(col, a.underlineColor);
166
+ m |= F_HAS_UL;
167
+ }
168
+ if (a.url !== null) {
169
+ setUrl(col, a.url);
170
+ m |= F_HAS_URL;
171
+ } else clearUrl(col);
172
+ meta[col] = m;
173
+ },
174
+ setCellRaw(col, cell) {
175
+ let m = packFlags(cell, cell.wide);
176
+ chars[col] = cell.char;
177
+ if (cell.fg) {
178
+ setFg(col, cell.fg);
179
+ m |= F_HAS_FG;
180
+ }
181
+ if (cell.bg) {
182
+ setBg(col, cell.bg);
183
+ m |= F_HAS_BG;
184
+ }
185
+ if (cell.underlineColor) {
186
+ setUl(col, cell.underlineColor);
187
+ m |= F_HAS_UL;
188
+ }
189
+ if (cell.url !== null) {
190
+ setUrl(col, cell.url);
191
+ m |= F_HAS_URL;
192
+ } else clearUrl(col);
193
+ meta[col] = m;
194
+ },
195
+ setEmpty(col) {
196
+ meta[col] = 0;
197
+ chars[col] = "";
198
+ clearUrl(col);
199
+ },
200
+ eraseWithBg(col, bg) {
201
+ chars[col] = "";
202
+ clearUrl(col);
203
+ if (bg) {
204
+ setBg(col, bg);
205
+ meta[col] = F_HAS_BG;
206
+ } else meta[col] = 0;
207
+ },
208
+ widen(col) {
209
+ meta[col] = (meta[col] | F_WIDE$1) >>> 0;
210
+ if (col + 1 < width) row.setEmpty(col + 1);
211
+ },
212
+ appendChar(col, ch) {
213
+ chars[col] = chars[col] + ch;
214
+ },
215
+ copyCellFrom(col, src, srcCol) {
216
+ row.setCellRaw(col, src.getCellRaw(srcCol));
217
+ },
218
+ getCellRaw(col) {
219
+ const m = meta[col];
220
+ if (m === 0 && chars[col] === "") return emptyCell();
221
+ return {
222
+ char: chars[col],
223
+ fg: m & F_HAS_FG ? unpackColor(fgRgb[col], fgIdx[col]) : null,
224
+ bg: m & F_HAS_BG ? unpackColor(bgRgb[col], bgIdx[col]) : null,
225
+ bold: (m & F_BOLD$1) !== 0,
226
+ faint: (m & F_FAINT$1) !== 0,
227
+ italic: (m & F_ITALIC$1) !== 0,
228
+ underline: bitsToUnderline(m),
229
+ underlineColor: m & F_HAS_UL ? unpackColor(ulRgb[col], ulIdx[col]) : null,
230
+ overline: (m & F_OVERLINE$1) !== 0,
231
+ strikethrough: (m & F_STRIKE$1) !== 0,
232
+ inverse: (m & F_INVERSE$1) !== 0,
233
+ hidden: (m & F_HIDDEN$1) !== 0,
234
+ blink: (m & F_BLINK$1) !== 0,
235
+ wide: (m & F_WIDE$1) !== 0,
236
+ url: m & F_HAS_URL ? urlMap.get(col) ?? null : null
237
+ };
238
+ },
239
+ toCells() {
240
+ const out = new Array(width);
241
+ for (let c = 0; c < width; c++) out[c] = row.getCellRaw(c);
242
+ return out;
243
+ },
244
+ replaceAllFromCells(cells) {
245
+ for (let c = 0; c < width; c++) row.setCellRaw(c, cells[c] ?? EMPTY_CELL);
246
+ },
247
+ isEmpty(col) {
248
+ return meta[col] === 0 && chars[col] === "";
249
+ },
250
+ getChar(col) {
251
+ return chars[col];
252
+ },
253
+ isWide(col) {
254
+ return (meta[col] & F_WIDE$1) !== 0;
255
+ }
256
+ };
257
+ return row;
258
+ }
259
+ /** Build a packed row from a materialized cell array (reflow / restore output). */
260
+ function packedRowFromCells(cells, width) {
261
+ const r = makePackedRow(width);
262
+ for (let c = 0; c < width; c++) r.setCellRaw(c, cells[c] ?? EMPTY_CELL);
263
+ return r;
264
+ }
23
265
  const ANSI_16 = [
24
266
  {
25
267
  r: 0,
@@ -217,7 +459,7 @@ function isEmojiModifier(cp) {
217
459
  function createScreen(options = {}) {
218
460
  let cols = options.cols ?? 80;
219
461
  let rows = options.rows ?? 24;
220
- const scrollbackLimit = options.scrollbackLimit ?? 1e3;
462
+ let scrollbackLimit = options.scrollbackLimit ?? 1e3;
221
463
  const onResponse = options.onResponse;
222
464
  let mainGrid = makeGrid(cols, rows);
223
465
  let altGrid = makeGrid(cols, rows);
@@ -270,8 +512,225 @@ function createScreen(options = {}) {
270
512
  let defaultBgColor = null;
271
513
  let cursorColor = null;
272
514
  const specialColors = /* @__PURE__ */ new Map();
515
+ let pointerFgColor = null;
516
+ let pointerBgColor = null;
273
517
  let highlightBgColor = null;
274
518
  let highlightFgColor = null;
519
+ const colorStack = [];
520
+ function cloneColor(c) {
521
+ return c ? { ...c } : null;
522
+ }
523
+ function snapshotColorState() {
524
+ return {
525
+ palette256: palette256.map((c) => ({ ...c })),
526
+ defaultFgColor: cloneColor(defaultFgColor),
527
+ defaultBgColor: cloneColor(defaultBgColor),
528
+ cursorColor: cloneColor(cursorColor),
529
+ specialColors: [...specialColors].map(([idx, color]) => [idx, { ...color }]),
530
+ pointerFgColor: cloneColor(pointerFgColor),
531
+ pointerBgColor: cloneColor(pointerBgColor),
532
+ highlightBgColor: cloneColor(highlightBgColor),
533
+ highlightFgColor: cloneColor(highlightFgColor)
534
+ };
535
+ }
536
+ function restoreColorState(snapshot) {
537
+ palette256 = snapshot.palette256.map((c) => ({ ...c }));
538
+ defaultFgColor = cloneColor(snapshot.defaultFgColor);
539
+ defaultBgColor = cloneColor(snapshot.defaultBgColor);
540
+ cursorColor = cloneColor(snapshot.cursorColor);
541
+ specialColors.clear();
542
+ for (const [idx, color] of snapshot.specialColors) specialColors.set(idx, { ...color });
543
+ pointerFgColor = cloneColor(snapshot.pointerFgColor);
544
+ pointerBgColor = cloneColor(snapshot.pointerBgColor);
545
+ highlightBgColor = cloneColor(snapshot.highlightBgColor);
546
+ highlightFgColor = cloneColor(snapshot.highlightFgColor);
547
+ }
548
+ function cloneColorStateSnapshot(snapshot) {
549
+ return {
550
+ palette256: snapshot.palette256.map((c) => ({ ...c })),
551
+ defaultFgColor: cloneColor(snapshot.defaultFgColor),
552
+ defaultBgColor: cloneColor(snapshot.defaultBgColor),
553
+ cursorColor: cloneColor(snapshot.cursorColor),
554
+ specialColors: snapshot.specialColors.map(([idx, color]) => [idx, { ...color }]),
555
+ pointerFgColor: cloneColor(snapshot.pointerFgColor),
556
+ pointerBgColor: cloneColor(snapshot.pointerBgColor),
557
+ highlightBgColor: cloneColor(snapshot.highlightBgColor),
558
+ highlightFgColor: cloneColor(snapshot.highlightFgColor)
559
+ };
560
+ }
561
+ function cloneAttrsSnapshot(source) {
562
+ return {
563
+ ...source,
564
+ fg: cloneColor(source.fg),
565
+ bg: cloneColor(source.bg),
566
+ underlineColor: cloneColor(source.underlineColor)
567
+ };
568
+ }
569
+ function cloneCellSnapshot(source) {
570
+ return {
571
+ ...source,
572
+ fg: cloneColor(source.fg),
573
+ bg: cloneColor(source.bg),
574
+ underlineColor: cloneColor(source.underlineColor)
575
+ };
576
+ }
577
+ function isDefaultEmptyCell(cell) {
578
+ return cell.char === "" && cell.fg === null && cell.bg === null && cell.bold === false && cell.faint === false && cell.italic === false && cell.underline === "none" && cell.underlineColor === null && cell.overline === false && cell.strikethrough === false && cell.inverse === false && cell.hidden === false && cell.blink === false && cell.wide === false && cell.url === null;
579
+ }
580
+ function restoreCellSnapshot(source) {
581
+ const cell = cloneCellSnapshot(source);
582
+ return isDefaultEmptyCell(cell) ? EMPTY_CELL : cell;
583
+ }
584
+ /** Materialize a packed grid to plain, deep-cloned Snapshot cells (index preserved). */
585
+ function cloneGridSnapshot(source) {
586
+ return source.map((row) => row.toCells().map(cloneCellSnapshot));
587
+ }
588
+ function restoreGridSnapshot(source, expectedRows, expectedCols) {
589
+ const out = [];
590
+ for (let row = 0; row < expectedRows; row++) {
591
+ const cells = (source[row] ?? []).slice(0, expectedCols).map(restoreCellSnapshot);
592
+ while (cells.length < expectedCols) cells.push(EMPTY_CELL);
593
+ out.push(packedRowFromCells(cells, expectedCols));
594
+ }
595
+ return out;
596
+ }
597
+ function restoreScrollbackSnapshot(source) {
598
+ return source.map((row) => packedRowFromCells(row.map(restoreCellSnapshot), row.length));
599
+ }
600
+ function isRecord(value) {
601
+ return typeof value === "object" && value !== null;
602
+ }
603
+ function assertInteger(name, value, min = 0) {
604
+ if (!Number.isInteger(value) || value < min) throw new TypeError(`Invalid vterm snapshot ${name}`);
605
+ }
606
+ function assertString(name, value) {
607
+ if (typeof value !== "string") throw new TypeError(`Invalid vterm snapshot ${name}`);
608
+ }
609
+ function assertBoolean(name, value) {
610
+ if (typeof value !== "boolean") throw new TypeError(`Invalid vterm snapshot ${name}`);
611
+ }
612
+ function assertRecord(name, value) {
613
+ if (!isRecord(value)) throw new TypeError(`Invalid vterm snapshot ${name}`);
614
+ }
615
+ function assertGrid(name, value, expectedRows, expectedCols) {
616
+ if (!Array.isArray(value) || value.length !== expectedRows) throw new TypeError(`Invalid vterm snapshot ${name}`);
617
+ for (const row of value) if (!Array.isArray(row) || row.length !== expectedCols) throw new TypeError(`Invalid vterm snapshot ${name}`);
618
+ }
619
+ function assertSoftWraps(name, value, expectedRows) {
620
+ if (!Array.isArray(value) || value.length !== expectedRows || value.some((entry) => typeof entry !== "boolean")) throw new TypeError(`Invalid vterm snapshot ${name}`);
621
+ }
622
+ function assertSnapshot(snapshot) {
623
+ const value = snapshot;
624
+ if (!isRecord(value)) throw new TypeError("Invalid vterm snapshot");
625
+ if (value.version !== 1) throw new Error(`Unsupported vterm snapshot version: ${String(value.version)}`);
626
+ assertInteger("cols", value.cols, 1);
627
+ assertInteger("rows", value.rows, 1);
628
+ assertInteger("scrollbackLimit", value.scrollbackLimit, 0);
629
+ if (value.activeBuffer !== "main" && value.activeBuffer !== "alt") throw new TypeError("Invalid vterm snapshot activeBuffer");
630
+ const main = value.main;
631
+ const alt = value.alt;
632
+ assertRecord("main", main);
633
+ assertRecord("alt", alt);
634
+ assertGrid("main.grid", main.grid, value.rows, value.cols);
635
+ assertGrid("alt.grid", alt.grid, value.rows, value.cols);
636
+ assertSoftWraps("main.softWrapped", main.softWrapped, value.rows);
637
+ assertSoftWraps("alt.softWrapped", alt.softWrapped, value.rows);
638
+ if (!Array.isArray(value.scrollback)) throw new TypeError("Invalid vterm snapshot scrollback");
639
+ if (value.scrollbackSoftWrapped !== void 0) assertSoftWraps("scrollbackSoftWrapped", value.scrollbackSoftWrapped, value.scrollback.length);
640
+ const cursor = value.cursor;
641
+ assertRecord("cursor", cursor);
642
+ assertInteger("cursor.x", cursor.x);
643
+ assertInteger("cursor.y", cursor.y);
644
+ assertBoolean("cursor.visible", cursor.visible);
645
+ if (cursor.shape !== "block" && cursor.shape !== "underline" && cursor.shape !== "bar") throw new TypeError("Invalid vterm snapshot cursor.shape");
646
+ assertBoolean("cursor.blinking", cursor.blinking);
647
+ assertInteger("cursor.savedX", cursor.savedX);
648
+ assertInteger("cursor.savedY", cursor.savedY);
649
+ const saved = value.savedState;
650
+ assertRecord("savedState", saved);
651
+ assertInteger("savedState.x", saved.x);
652
+ assertInteger("savedState.y", saved.y);
653
+ assertRecord("savedState.attrs", saved.attrs);
654
+ assertBoolean("savedState.originMode", saved.originMode);
655
+ assertBoolean("savedState.autoWrap", saved.autoWrap);
656
+ assertBoolean("savedState.charsetG0", saved.charsetG0);
657
+ assertRecord("attrs", value.attrs);
658
+ const modes = value.modes;
659
+ assertRecord("modes", modes);
660
+ for (const name of [
661
+ "bracketedPaste",
662
+ "applicationCursor",
663
+ "applicationKeypad",
664
+ "autoWrap",
665
+ "mouseTracking",
666
+ "sgrMouse",
667
+ "focusTracking",
668
+ "origin",
669
+ "insert",
670
+ "reverseVideo",
671
+ "syncOutput",
672
+ "kittyGraphics",
673
+ "colorSchemeReporting",
674
+ "decColumn",
675
+ "altScroll",
676
+ "utf8Mouse"
677
+ ]) assertBoolean(`modes.${name}`, modes[name]);
678
+ assertInteger("modes.mouseTrackingMode", modes.mouseTrackingMode);
679
+ assertInteger("modes.kittyKeyboardFlags", modes.kittyKeyboardFlags);
680
+ if (!Array.isArray(modes.kittyKeyboardStack)) throw new TypeError("Invalid vterm snapshot modes.kittyKeyboardStack");
681
+ const margins = value.margins;
682
+ assertRecord("margins", margins);
683
+ assertInteger("margins.scrollTop", margins.scrollTop);
684
+ assertInteger("margins.scrollBottom", margins.scrollBottom);
685
+ assertBoolean("margins.leftRight", margins.leftRight);
686
+ assertInteger("margins.left", margins.left);
687
+ assertInteger("margins.right", margins.right);
688
+ const colors = value.colors;
689
+ assertRecord("colors", colors);
690
+ assertRecord("colors.current", colors.current);
691
+ if (!Array.isArray(colors.stack)) throw new TypeError("Invalid vterm snapshot colors.stack");
692
+ if (!Array.isArray(value.tabStops)) throw new TypeError("Invalid vterm snapshot tabStops");
693
+ for (const stop of value.tabStops) assertInteger("tabStops", stop);
694
+ assertString("title", value.title);
695
+ assertString("clipboard", value.clipboard);
696
+ assertString("cwd", value.cwd);
697
+ if (!Array.isArray(value.notifications) || value.notifications.some((entry) => typeof entry !== "string")) throw new TypeError("Invalid vterm snapshot notifications");
698
+ assertInteger("viewportOffset", value.viewportOffset);
699
+ const parser = value.parser;
700
+ assertRecord("parser", parser);
701
+ if (![
702
+ "ground",
703
+ "escape",
704
+ "escape_charset",
705
+ "escape_hash",
706
+ "csi",
707
+ "osc",
708
+ "dcs",
709
+ "dcs_passthrough",
710
+ "osc_st",
711
+ "dcs_st",
712
+ "apc",
713
+ "apc_st"
714
+ ].includes(parser.state)) throw new TypeError("Invalid vterm snapshot parser.state");
715
+ assertString("parser.esc", parser.esc);
716
+ assertString("parser.osc", parser.osc);
717
+ assertString("parser.dcs", parser.dcs);
718
+ assertRecord("parser.dcsStart", parser.dcsStart);
719
+ assertInteger("parser.dcsStart.row", parser.dcsStart.row);
720
+ assertInteger("parser.dcsStart.col", parser.dcsStart.col);
721
+ assertString("parser.apc", parser.apc);
722
+ if (!Array.isArray(parser.utf8PendingBytes)) throw new TypeError("Invalid vterm snapshot parser.utf8PendingBytes");
723
+ for (const byte of parser.utf8PendingBytes) {
724
+ assertInteger("parser.utf8PendingBytes", byte, 0);
725
+ if (byte > 255) throw new TypeError("Invalid vterm snapshot parser.utf8PendingBytes");
726
+ }
727
+ const unicode = value.unicode;
728
+ assertRecord("unicode", unicode);
729
+ assertBoolean("unicode.charsetG0", unicode.charsetG0);
730
+ assertString("unicode.lastChar", unicode.lastChar);
731
+ if (unicode.pendingRegionalIndicator !== null) assertString("unicode.pendingRegionalIndicator", unicode.pendingRegionalIndicator);
732
+ assertBoolean("unicode.afterZWJ", unicode.afterZWJ);
733
+ }
275
734
  let tabStops = defaultTabStops(cols);
276
735
  function defaultTabStops(c) {
277
736
  const s = /* @__PURE__ */ new Set();
@@ -288,6 +747,12 @@ function createScreen(options = {}) {
288
747
  return 0;
289
748
  }
290
749
  let textScale = 1;
750
+ const CELL_W_PX = 8;
751
+ const CELL_H_PX = 17;
752
+ const FONT_ASCENT_PX = 14;
753
+ let fontSize = 12;
754
+ let fontWindowSize = 12;
755
+ let locale = "en_US.UTF-8";
291
756
  let advancedClipboard = "";
292
757
  let viewportOffset = 0;
293
758
  let charsetG0 = false;
@@ -300,6 +765,7 @@ function createScreen(options = {}) {
300
765
  let mainSoftWrapped = new Array(rows).fill(false);
301
766
  let altSoftWrapped = new Array(rows).fill(false);
302
767
  let softWrapped = mainSoftWrapped;
768
+ let scrollbackSoftWrapped = [];
303
769
  let lastChar = "";
304
770
  let pendingRegionalIndicator = null;
305
771
  let afterZWJ = false;
@@ -310,16 +776,122 @@ function createScreen(options = {}) {
310
776
  let dcsStartRow = 0;
311
777
  let dcsStartCol = 0;
312
778
  let apcBuf = "";
779
+ const opListeners = /* @__PURE__ */ new Set();
780
+ const parserListeners = /* @__PURE__ */ new Set();
781
+ let printRunParts = [];
782
+ let escIntermediate = "";
783
+ let dirtyAll = false;
784
+ let dirtyRows = /* @__PURE__ */ new Set();
785
+ let dirtyScrolled = 0;
786
+ let dirtyLastAbs = -1;
787
+ let trimmedRowCount = 0;
788
+ let dirtyCursor = {
789
+ x: 0,
790
+ y: 0,
791
+ visible: true,
792
+ shape: "block",
793
+ blinking: true
794
+ };
795
+ let sigDamageActive = false;
796
+ let sigDirtyAll = false;
797
+ let sigDirtyRows = /* @__PURE__ */ new Set();
798
+ let sigDirtyScrolled = 0;
799
+ let sigDirtyLastAbs = -1;
800
+ let sigDirtyCursor = {
801
+ x: 0,
802
+ y: 0,
803
+ visible: true,
804
+ shape: "block",
805
+ blinking: true
806
+ };
807
+ function markScreenRowDirty(screenRow) {
808
+ const abs = scrollback.length + screenRow;
809
+ if (!dirtyAll && abs !== dirtyLastAbs) {
810
+ dirtyLastAbs = abs;
811
+ dirtyRows.add(abs);
812
+ }
813
+ if (sigDamageActive && !sigDirtyAll && abs !== sigDirtyLastAbs) {
814
+ sigDirtyLastAbs = abs;
815
+ sigDirtyRows.add(abs);
816
+ }
817
+ }
818
+ function markAbsoluteRowDirty(abs) {
819
+ if (!dirtyAll && abs !== dirtyLastAbs) {
820
+ dirtyLastAbs = abs;
821
+ dirtyRows.add(abs);
822
+ }
823
+ if (sigDamageActive && !sigDirtyAll && abs !== sigDirtyLastAbs) {
824
+ sigDirtyLastAbs = abs;
825
+ sigDirtyRows.add(abs);
826
+ }
827
+ }
828
+ function markScreenRowsDirty(top, bottom) {
829
+ if (dirtyAll && (!sigDamageActive || sigDirtyAll)) return;
830
+ for (let r = top; r <= bottom; r++) markScreenRowDirty(r);
831
+ }
832
+ /** Structural change — the whole visible buffer is damaged. Drops the per-row set (both planes). */
833
+ function markAllDirty() {
834
+ dirtyAll = true;
835
+ dirtyRows.clear();
836
+ dirtyLastAbs = -1;
837
+ if (sigDamageActive) {
838
+ sigDirtyAll = true;
839
+ sigDirtyRows.clear();
840
+ sigDirtyLastAbs = -1;
841
+ }
842
+ }
313
843
  const decoder = new TextDecoder();
844
+ const encoder = new TextEncoder();
845
+ let utf8PendingBytes = [];
846
+ function utf8SequenceLength(lead) {
847
+ if (lead <= 127) return 1;
848
+ if (lead >= 194 && lead <= 223) return 2;
849
+ if (lead >= 224 && lead <= 239) return 3;
850
+ if (lead >= 240 && lead <= 244) return 4;
851
+ return 0;
852
+ }
853
+ function utf8CompletePrefixLength(bytes) {
854
+ if (bytes.length === 0) return 0;
855
+ let continuationCount = 0;
856
+ let leadIndex = bytes.length - 1;
857
+ while (leadIndex >= 0 && (bytes[leadIndex] & 192) === 128 && continuationCount < 3) {
858
+ continuationCount++;
859
+ leadIndex--;
860
+ }
861
+ if (leadIndex < 0) return bytes.length;
862
+ const lead = bytes[leadIndex];
863
+ const expectedLength = utf8SequenceLength(lead);
864
+ if (expectedLength === 0) return bytes.length;
865
+ return expectedLength > bytes.length - leadIndex ? leadIndex : bytes.length;
866
+ }
867
+ function decodeInput(data) {
868
+ const bytes = new Uint8Array(utf8PendingBytes.length + data.length);
869
+ bytes.set(utf8PendingBytes, 0);
870
+ bytes.set(data, utf8PendingBytes.length);
871
+ const completeLength = utf8CompletePrefixLength(bytes);
872
+ utf8PendingBytes = Array.from(bytes.slice(completeLength));
873
+ if (completeLength === 0) return "";
874
+ return decoder.decode(bytes.slice(0, completeLength));
875
+ }
314
876
  function makeGrid(c, r) {
315
877
  const g = [];
316
878
  for (let row = 0; row < r; row++) g.push(makeRow(c));
317
879
  return g;
318
880
  }
319
881
  function makeRow(c) {
320
- const row = [];
321
- for (let col = 0; col < c; col++) row.push(EMPTY_CELL);
322
- return row;
882
+ return makePackedRow(c);
883
+ }
884
+ /**
885
+ * Edit-path escape hatch for the rare column-splice operations (insert/delete
886
+ * chars & columns, rect fill/copy/attr): materialize the row to a fixed-length
887
+ * cell array, run the ordinary array edit, then repack. Length-preserving edits
888
+ * (splice+push/pop pairs) keep the row exactly `cols` wide. Not the hot path —
889
+ * plain printing goes straight through {@link PackedRow.writeFromAttrs}.
890
+ */
891
+ function mutateRowAsCells(row, fn) {
892
+ const cells = row.toCells();
893
+ fn(cells);
894
+ row.replaceAllFromCells(cells);
323
895
  }
324
896
  function resetAttrs() {
325
897
  return {
@@ -351,14 +923,35 @@ function createScreen(options = {}) {
351
923
  for (let r = top; r < bottom; r++) {
352
924
  const srcRow = grid[r + 1];
353
925
  const dstRow = grid[r];
354
- for (let c = lm; c <= rm && c < cols; c++) dstRow[c] = srcRow[c];
926
+ for (let c = lm; c <= rm && c < cols; c++) dstRow.copyCellFrom(c, srcRow, c);
355
927
  }
356
928
  const bottomRow = grid[bottom];
357
- for (let c = lm; c <= rm && c < cols; c++) bottomRow[c] = EMPTY_CELL;
929
+ for (let c = lm; c <= rm && c < cols; c++) bottomRow.setEmpty(c);
930
+ markScreenRowsDirty(top, bottom);
358
931
  } else {
359
- if (grid === mainGrid && top === 0) {
932
+ const enteredScrollback = grid === mainGrid && top === 0;
933
+ if (enteredScrollback) {
360
934
  scrollback.push(grid[0]);
361
- if (scrollback.length > scrollbackLimit * 2) scrollback.splice(0, scrollback.length - scrollbackLimit);
935
+ scrollbackSoftWrapped.push(softWrapped[0] ?? false);
936
+ markAbsoluteRowDirty(scrollback.length - 1);
937
+ if (scrollback.length > scrollbackLimit * 2) {
938
+ const over = scrollback.length - scrollbackLimit;
939
+ scrollback.splice(0, over);
940
+ scrollbackSoftWrapped.splice(0, over);
941
+ if (!dirtyAll && dirtyRows.size > 0) {
942
+ const shifted = /* @__PURE__ */ new Set();
943
+ for (const r of dirtyRows) if (r >= over) shifted.add(r - over);
944
+ dirtyRows = shifted;
945
+ dirtyLastAbs = -1;
946
+ }
947
+ if (sigDamageActive && !sigDirtyAll && sigDirtyRows.size > 0) {
948
+ const shifted = /* @__PURE__ */ new Set();
949
+ for (const r of sigDirtyRows) if (r >= over) shifted.add(r - over);
950
+ sigDirtyRows = shifted;
951
+ sigDirtyLastAbs = -1;
952
+ }
953
+ trimmedRowCount += over;
954
+ }
362
955
  }
363
956
  for (let i = top; i < bottom; i++) {
364
957
  grid[i] = grid[i + 1];
@@ -366,6 +959,11 @@ function createScreen(options = {}) {
366
959
  }
367
960
  grid[bottom] = makeRow(cols);
368
961
  softWrapped[bottom] = false;
962
+ if (enteredScrollback) {
963
+ dirtyScrolled++;
964
+ if (sigDamageActive) sigDirtyScrolled++;
965
+ markScreenRowDirty(bottom);
966
+ } else markScreenRowsDirty(top, bottom);
369
967
  }
370
968
  }
371
969
  function scrollDown(top, bottom) {
@@ -375,10 +973,10 @@ function createScreen(options = {}) {
375
973
  for (let r = bottom; r > top; r--) {
376
974
  const srcRow = grid[r - 1];
377
975
  const dstRow = grid[r];
378
- for (let c = lm; c <= rm && c < cols; c++) dstRow[c] = srcRow[c];
976
+ for (let c = lm; c <= rm && c < cols; c++) dstRow.copyCellFrom(c, srcRow, c);
379
977
  }
380
978
  const topRow = grid[top];
381
- for (let c = lm; c <= rm && c < cols; c++) topRow[c] = EMPTY_CELL;
979
+ for (let c = lm; c <= rm && c < cols; c++) topRow.setEmpty(c);
382
980
  } else {
383
981
  for (let i = bottom; i > top; i--) {
384
982
  grid[i] = grid[i - 1];
@@ -387,8 +985,17 @@ function createScreen(options = {}) {
387
985
  grid[top] = makeRow(cols);
388
986
  softWrapped[top] = false;
389
987
  }
988
+ markScreenRowsDirty(top, bottom);
390
989
  }
391
- /** Find the previous non-spacer cell (the cell before curX, skipping wide-char spacers) */
990
+ function scrollViewport(delta) {
991
+ viewportOffset = Math.max(0, Math.min(scrollback.length, viewportOffset + delta));
992
+ }
993
+ /**
994
+ * Locate the previous non-spacer cell position (before curX, skipping a wide-char
995
+ * spacer). Returns the packed row and column so callers mutate through the row's API
996
+ * (combining marks append, VS-16 widens). The spacer is detected structurally — a
997
+ * blank char whose left neighbour is wide — rather than by object identity.
998
+ */
392
999
  function getPrevCell() {
393
1000
  if (curX === 0 && curY === 0) return null;
394
1001
  let prevCol = curX - 1;
@@ -399,42 +1006,17 @@ function createScreen(options = {}) {
399
1006
  prevCol = cols - 1;
400
1007
  }
401
1008
  const row = grid[prevRow];
402
- let cell = row[prevCol];
403
- if (cell !== EMPTY_CELL && cell.char === "" && prevCol > 0) {
404
- prevCol--;
405
- cell = row[prevCol];
406
- }
407
- if (cell === EMPTY_CELL) return null;
1009
+ if (prevCol > 0 && row.getChar(prevCol) === "" && row.isWide(prevCol - 1)) prevCol--;
1010
+ if (row.isEmpty(prevCol)) return null;
408
1011
  return {
409
- cell,
410
- col: prevCol
1012
+ row,
1013
+ col: prevCol,
1014
+ rowIdx: prevRow
411
1015
  };
412
1016
  }
413
- /** Widen a cell to 2 columns, adding a spacer cell after it */
414
- function widenCell(row, col, cell) {
415
- cell.wide = true;
416
- if (col + 1 < cols) {
417
- let spacer = row[col + 1];
418
- if (spacer === EMPTY_CELL) {
419
- spacer = { ...EMPTY_CELL };
420
- row[col + 1] = spacer;
421
- }
422
- spacer.char = "";
423
- spacer.fg = null;
424
- spacer.bg = null;
425
- spacer.bold = false;
426
- spacer.faint = false;
427
- spacer.italic = false;
428
- spacer.underline = "none";
429
- spacer.underlineColor = null;
430
- spacer.overline = false;
431
- spacer.strikethrough = false;
432
- spacer.inverse = false;
433
- spacer.hidden = false;
434
- spacer.blink = false;
435
- spacer.wide = false;
436
- spacer.url = null;
437
- }
1017
+ /** Widen the cell at `col` to 2 columns, blanking the trailing spacer at col+1. */
1018
+ function widenCell(row, col) {
1019
+ row.widen(col);
438
1020
  }
439
1021
  function writeChar(ch) {
440
1022
  if (charsetG0 && ch.length === 1) {
@@ -444,10 +1026,10 @@ function createScreen(options = {}) {
444
1026
  const codePoint = ch.codePointAt(0) ?? 0;
445
1027
  if (isVS16(codePoint)) {
446
1028
  const prev = getPrevCell();
447
- if (prev && !prev.cell.wide) {
448
- prev.cell.char += ch;
449
- const row = grid[curY === 0 && curX === 0 ? 0 : curY];
450
- widenCell(row, prev.col, prev.cell);
1029
+ if (prev && !prev.row.isWide(prev.col)) {
1030
+ prev.row.appendChar(prev.col, ch);
1031
+ widenCell(prev.row, prev.col);
1032
+ markScreenRowDirty(prev.rowIdx);
451
1033
  curX = prev.col + 2;
452
1034
  if (curX >= cols) curX = cols - 1;
453
1035
  }
@@ -455,18 +1037,25 @@ function createScreen(options = {}) {
455
1037
  }
456
1038
  if (isCombining(codePoint) && !isVS16(codePoint)) {
457
1039
  const prev = getPrevCell();
458
- if (prev) prev.cell.char += ch;
1040
+ if (prev) {
1041
+ prev.row.appendChar(prev.col, ch);
1042
+ markScreenRowDirty(prev.rowIdx);
1043
+ }
459
1044
  return;
460
1045
  }
461
1046
  if (isEmojiModifier(codePoint)) {
462
1047
  const prev = getPrevCell();
463
- if (prev) prev.cell.char += ch;
1048
+ if (prev) {
1049
+ prev.row.appendChar(prev.col, ch);
1050
+ markScreenRowDirty(prev.rowIdx);
1051
+ }
464
1052
  return;
465
1053
  }
466
1054
  if (isZWJ(codePoint)) {
467
1055
  const prev = getPrevCell();
468
1056
  if (prev) {
469
- prev.cell.char += ch;
1057
+ prev.row.appendChar(prev.col, ch);
1058
+ markScreenRowDirty(prev.rowIdx);
470
1059
  afterZWJ = true;
471
1060
  }
472
1061
  return;
@@ -475,7 +1064,8 @@ function createScreen(options = {}) {
475
1064
  afterZWJ = false;
476
1065
  const prev = getPrevCell();
477
1066
  if (prev) {
478
- prev.cell.char += ch;
1067
+ prev.row.appendChar(prev.col, ch);
1068
+ markScreenRowDirty(prev.rowIdx);
479
1069
  return;
480
1070
  }
481
1071
  }
@@ -511,35 +1101,18 @@ function createScreen(options = {}) {
511
1101
  }
512
1102
  } else curX = wrapBoundary - charWidth;
513
1103
  if (insertMode) {
514
- const row = grid[curY];
515
1104
  const insertEnd = leftRightMarginMode ? rightMargin + 1 : cols;
516
- for (let i = 0; i < charWidth; i++) {
517
- row.splice(insertEnd - 1, 1);
518
- row.splice(curX, 0, EMPTY_CELL);
519
- }
1105
+ mutateRowAsCells(grid[curY], (cells) => {
1106
+ for (let i = 0; i < charWidth; i++) {
1107
+ cells.splice(insertEnd - 1, 1);
1108
+ cells.splice(curX, 0, emptyCell());
1109
+ }
1110
+ });
520
1111
  }
521
1112
  const row = grid[curY];
522
- let cell = row[curX];
523
- if (cell === EMPTY_CELL) {
524
- cell = { ...EMPTY_CELL };
525
- row[curX] = cell;
526
- }
527
- cell.char = ch;
528
- cell.fg = attrs.fg ? { ...attrs.fg } : null;
529
- cell.bg = attrs.bg ? { ...attrs.bg } : null;
530
- cell.bold = attrs.bold;
531
- cell.faint = attrs.faint;
532
- cell.italic = attrs.italic;
533
- cell.underline = attrs.underline;
534
- cell.underlineColor = attrs.underlineColor ? { ...attrs.underlineColor } : null;
535
- cell.overline = attrs.overline;
536
- cell.strikethrough = attrs.strikethrough;
537
- cell.inverse = attrs.inverse;
538
- cell.hidden = attrs.hidden;
539
- cell.blink = attrs.blink;
540
- cell.wide = wide;
541
- cell.url = attrs.url;
542
- if (wide) widenCell(row, curX, cell);
1113
+ row.writeFromAttrs(curX, ch, attrs, wide);
1114
+ if (wide) widenCell(row, curX);
1115
+ markScreenRowDirty(curY);
543
1116
  curX += charWidth;
544
1117
  lastChar = ch;
545
1118
  }
@@ -594,14 +1167,23 @@ function createScreen(options = {}) {
594
1167
  const charCode = parts[0] ?? 32;
595
1168
  if (charCode < 32 || charCode > 126) return;
596
1169
  const { t, l, b, r } = normalizeRect(parts[1] ?? 1, parts[2] ?? 1, parts[3] ?? rows, parts[4] ?? cols);
597
- for (let row = t; row <= b && row < rows; row++) for (let col = l; col <= r && col < cols; col++) {
598
- const cell = emptyCell();
599
- cell.char = String.fromCharCode(charCode);
600
- grid[row][col] = cell;
1170
+ const fillChar = String.fromCharCode(charCode);
1171
+ for (let row = t; row <= b && row < rows; row++) {
1172
+ const gr = grid[row];
1173
+ for (let col = l; col <= r && col < cols; col++) {
1174
+ const cell = emptyCell();
1175
+ cell.char = fillChar;
1176
+ gr.setCellRaw(col, cell);
1177
+ }
601
1178
  }
1179
+ markScreenRowsDirty(t, Math.min(b, rows - 1));
602
1180
  } else if (finalByte === "z" || finalByte === "{") {
603
1181
  const { t, l, b, r } = normalizeRect(parts[0] ?? 1, parts[1] ?? 1, parts[2] ?? rows, parts[3] ?? cols);
604
- for (let row = t; row <= b && row < rows; row++) for (let col = l; col <= r && col < cols; col++) grid[row][col] = emptyCell();
1182
+ for (let row = t; row <= b && row < rows; row++) {
1183
+ const gr = grid[row];
1184
+ for (let col = l; col <= r && col < cols; col++) gr.setEmpty(col);
1185
+ }
1186
+ markScreenRowsDirty(t, Math.min(b, rows - 1));
605
1187
  } else if (finalByte === "v") {
606
1188
  const src = normalizeRect(parts[0] ?? 1, parts[1] ?? 1, parts[2] ?? rows, parts[3] ?? cols);
607
1189
  const dstTop = Math.max(1, parts[5] ?? 1) - 1;
@@ -610,34 +1192,35 @@ function createScreen(options = {}) {
610
1192
  const w = src.r - src.l + 1;
611
1193
  const snapshot = [];
612
1194
  for (let row = 0; row < h; row++) {
1195
+ const srcRow = grid[src.t + row];
613
1196
  const line = [];
614
- for (let col = 0; col < w; col++) {
615
- const srcCell = grid[src.t + row]?.[src.l + col] ?? EMPTY_CELL;
616
- line.push(srcCell === EMPTY_CELL ? EMPTY_CELL : { ...srcCell });
617
- }
1197
+ for (let col = 0; col < w; col++) line.push(srcRow ? srcRow.getCellRaw(src.l + col) : emptyCell());
618
1198
  snapshot.push(line);
619
1199
  }
620
1200
  for (let row = 0; row < h; row++) {
621
1201
  const dr = dstTop + row;
622
1202
  if (dr < 0 || dr >= rows) continue;
1203
+ const dstRow = grid[dr];
623
1204
  for (let col = 0; col < w; col++) {
624
1205
  const dc = dstLeft + col;
625
1206
  if (dc < 0 || dc >= cols) continue;
626
- grid[dr][dc] = snapshot[row][col];
1207
+ dstRow.setCellRaw(dc, snapshot[row][col]);
627
1208
  }
628
1209
  }
1210
+ markScreenRowsDirty(Math.max(0, dstTop), Math.min(dstTop + h - 1, rows - 1));
629
1211
  } else if (finalByte === "r" || finalByte === "t") {
630
1212
  const { t, l, b, r } = normalizeRect(parts[0] ?? 1, parts[1] ?? 1, parts[2] ?? rows, parts[3] ?? cols);
631
1213
  const sgrParts = parts.slice(4);
632
1214
  const reverse = finalByte === "t";
633
- for (let row = t; row <= b && row < rows; row++) for (let col = l; col <= r && col < cols; col++) {
634
- let cell = grid[row][col];
635
- if (cell === EMPTY_CELL) {
636
- cell = { ...EMPTY_CELL };
637
- grid[row][col] = cell;
1215
+ for (let row = t; row <= b && row < rows; row++) {
1216
+ const gr = grid[row];
1217
+ for (let col = l; col <= r && col < cols; col++) {
1218
+ const cell = gr.getCellRaw(col);
1219
+ applyRectAttrs(cell, sgrParts, reverse);
1220
+ gr.setCellRaw(col, cell);
638
1221
  }
639
- applyRectAttrs(cell, sgrParts, reverse);
640
1222
  }
1223
+ markScreenRowsDirty(t, Math.min(b, rows - 1));
641
1224
  }
642
1225
  return;
643
1226
  }
@@ -653,11 +1236,15 @@ function createScreen(options = {}) {
653
1236
  const b = Math.min(bArg, rows) - 1;
654
1237
  const r = Math.min(rArg, cols) - 1;
655
1238
  let sum = 0;
656
- for (let row = t; row <= b && row < rows; row++) for (let col = l; col <= r && col < cols; col++) {
657
- const cell = grid[row]?.[col];
658
- if (cell?.char) {
659
- const cp = cell.char.codePointAt(0) ?? 0;
660
- sum = sum + cp & 65535;
1239
+ for (let row = t; row <= b && row < rows; row++) {
1240
+ const gr = grid[row];
1241
+ if (!gr) continue;
1242
+ for (let col = l; col <= r && col < cols; col++) {
1243
+ const ch = gr.getChar(col);
1244
+ if (ch) {
1245
+ const cp = ch.codePointAt(0) ?? 0;
1246
+ sum = sum + cp & 65535;
1247
+ }
661
1248
  }
662
1249
  }
663
1250
  const hex = sum.toString(16).toUpperCase().padStart(4, "0");
@@ -876,6 +1463,13 @@ function createScreen(options = {}) {
876
1463
  if (onResponse) {
877
1464
  if (params === "" || params === "0") onResponse("\x1BP!|00000000\x1B\\");
878
1465
  }
1466
+ } else if (finalByte === "u") {
1467
+ const parts = params.split(";").map((s) => s === "" ? 0 : parseInt(s, 10));
1468
+ const flags = parts[0] ?? 0;
1469
+ const mode = parts[1] ?? 1;
1470
+ if (mode === 2) kittyKeyboardFlags |= flags;
1471
+ else if (mode === 3) kittyKeyboardFlags &= ~flags;
1472
+ else kittyKeyboardFlags = flags;
879
1473
  }
880
1474
  }
881
1475
  function handleCSIPrivate(params, intermediates, finalByte) {
@@ -997,10 +1591,12 @@ function createScreen(options = {}) {
997
1591
  useAltScreen = true;
998
1592
  grid = altGrid;
999
1593
  softWrapped = altSoftWrapped;
1594
+ markAllDirty();
1000
1595
  } else if (!set && useAltScreen) {
1001
1596
  useAltScreen = false;
1002
1597
  grid = mainGrid;
1003
1598
  softWrapped = mainSoftWrapped;
1599
+ markAllDirty();
1004
1600
  }
1005
1601
  break;
1006
1602
  case 66:
@@ -1032,6 +1628,7 @@ function createScreen(options = {}) {
1032
1628
  softWrapped = altSoftWrapped;
1033
1629
  curX = 0;
1034
1630
  curY = 0;
1631
+ markAllDirty();
1035
1632
  } else if (!set && useAltScreen) {
1036
1633
  useAltScreen = false;
1037
1634
  grid = mainGrid;
@@ -1039,6 +1636,7 @@ function createScreen(options = {}) {
1039
1636
  curX = savedCurX;
1040
1637
  curY = savedCurY;
1041
1638
  clampCursor();
1639
+ markAllDirty();
1042
1640
  }
1043
1641
  break;
1044
1642
  case 69:
@@ -1095,6 +1693,7 @@ function createScreen(options = {}) {
1095
1693
  case 3:
1096
1694
  for (let row = 0; row < rows; row++) eraseCells(row, 0, row, cols - 1);
1097
1695
  if (mode === 3) scrollback.length = 0;
1696
+ markAllDirty();
1098
1697
  break;
1099
1698
  }
1100
1699
  }
@@ -1116,11 +1715,8 @@ function createScreen(options = {}) {
1116
1715
  function eraseCells(row, startCol, _endRow, endCol) {
1117
1716
  const r = grid[row];
1118
1717
  if (!r) return;
1119
- for (let col = startCol; col <= endCol && col < cols; col++) {
1120
- const cell = emptyCell();
1121
- if (attrs.bg) cell.bg = { ...attrs.bg };
1122
- r[col] = cell;
1123
- }
1718
+ for (let col = startCol; col <= endCol && col < cols; col++) r.eraseWithBg(col, attrs.bg);
1719
+ markScreenRowDirty(row);
1124
1720
  }
1125
1721
  function handleInsertLines(count) {
1126
1722
  if (curY < scrollTop || curY > scrollBottom) return;
@@ -1133,32 +1729,36 @@ function createScreen(options = {}) {
1133
1729
  function handleDeleteChars(count) {
1134
1730
  const row = grid[curY];
1135
1731
  if (!row) return;
1136
- if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
1137
- for (let i = 0; i < count; i++) if (curX <= rightMargin) {
1138
- row.splice(curX, 1);
1139
- row.splice(rightMargin, 0, emptyCell());
1732
+ mutateRowAsCells(row, (cells) => {
1733
+ if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
1734
+ for (let i = 0; i < count; i++) if (curX <= rightMargin) {
1735
+ cells.splice(curX, 1);
1736
+ cells.splice(rightMargin, 0, emptyCell());
1737
+ }
1738
+ } else for (let i = 0; i < count; i++) if (curX < cols) {
1739
+ cells.splice(curX, 1);
1740
+ cells.push(emptyCell());
1140
1741
  }
1141
- } else for (let i = 0; i < count; i++) if (curX < cols) {
1142
- row.splice(curX, 1);
1143
- row.push(emptyCell());
1144
- }
1742
+ });
1743
+ markScreenRowDirty(curY);
1145
1744
  }
1146
1745
  function handleInsertChars(count) {
1147
1746
  const row = grid[curY];
1148
1747
  if (!row) return;
1149
- if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) for (let i = 0; i < count; i++) {
1150
- row.splice(rightMargin, 1);
1151
- row.splice(curX, 0, emptyCell());
1152
- }
1153
- else for (let i = 0; i < count; i++) {
1154
- row.splice(curX, 0, emptyCell());
1155
- row.pop();
1156
- }
1748
+ mutateRowAsCells(row, (cells) => {
1749
+ if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) for (let i = 0; i < count; i++) {
1750
+ cells.splice(rightMargin, 1);
1751
+ cells.splice(curX, 0, emptyCell());
1752
+ }
1753
+ else for (let i = 0; i < count; i++) {
1754
+ cells.splice(curX, 0, emptyCell());
1755
+ cells.pop();
1756
+ }
1757
+ });
1758
+ markScreenRowDirty(curY);
1157
1759
  }
1158
1760
  function handleEraseChars(count) {
1159
- const row = grid[curY];
1160
- if (!row) return;
1161
- for (let i = 0; i < count && curX + i < cols; i++) row[curX + i] = emptyCell();
1761
+ eraseCells(curY, curX, curY, Math.min(curX + count - 1, cols - 1));
1162
1762
  }
1163
1763
  /** Bounds of the active scroll/margin rectangle (inclusive). */
1164
1764
  function activeRect() {
@@ -1176,9 +1776,11 @@ function createScreen(options = {}) {
1176
1776
  if (!r) continue;
1177
1777
  for (let col = left; col <= right; col++) {
1178
1778
  const src = col + count;
1179
- r[col] = src <= right ? r[src] : emptyCell();
1779
+ if (src <= right) r.copyCellFrom(col, r, src);
1780
+ else r.setEmpty(col);
1180
1781
  }
1181
1782
  }
1783
+ markScreenRowsDirty(top, bottom);
1182
1784
  }
1183
1785
  function handleShiftRight(count) {
1184
1786
  const { top, left, bottom, right } = activeRect();
@@ -1187,9 +1789,11 @@ function createScreen(options = {}) {
1187
1789
  if (!r) continue;
1188
1790
  for (let col = right; col >= left; col--) {
1189
1791
  const src = col - count;
1190
- r[col] = src >= left ? r[src] : emptyCell();
1792
+ if (src >= left) r.copyCellFrom(col, r, src);
1793
+ else r.setEmpty(col);
1191
1794
  }
1192
1795
  }
1796
+ markScreenRowsDirty(top, bottom);
1193
1797
  }
1194
1798
  function handleInsertColumn(count) {
1195
1799
  const { top, left, bottom, right } = activeRect();
@@ -1197,9 +1801,10 @@ function createScreen(options = {}) {
1197
1801
  for (let row = top; row <= bottom; row++) {
1198
1802
  const r = grid[row];
1199
1803
  if (!r) continue;
1200
- for (let col = right; col >= curX + count; col--) r[col] = r[col - count];
1201
- for (let col = curX; col < curX + count && col <= right; col++) r[col] = emptyCell();
1804
+ for (let col = right; col >= curX + count; col--) r.copyCellFrom(col, r, col - count);
1805
+ for (let col = curX; col < curX + count && col <= right; col++) r.setEmpty(col);
1202
1806
  }
1807
+ markScreenRowsDirty(top, bottom);
1203
1808
  }
1204
1809
  function handleDeleteColumn(count) {
1205
1810
  const { top, left, bottom, right } = activeRect();
@@ -1207,9 +1812,10 @@ function createScreen(options = {}) {
1207
1812
  for (let row = top; row <= bottom; row++) {
1208
1813
  const r = grid[row];
1209
1814
  if (!r) continue;
1210
- for (let col = curX; col + count <= right; col++) r[col] = r[col + count];
1211
- for (let col = right - count + 1; col <= right && col >= 0; col++) r[col] = emptyCell();
1815
+ for (let col = curX; col + count <= right; col++) r.copyCellFrom(col, r, col + count);
1816
+ for (let col = right - count + 1; col <= right && col >= 0; col++) r.setEmpty(col);
1212
1817
  }
1818
+ markScreenRowsDirty(top, bottom);
1213
1819
  }
1214
1820
  /**
1215
1821
  * Apply (or reverse-toggle) a list of SGR attribute codes to a single cell.
@@ -1363,7 +1969,10 @@ function createScreen(options = {}) {
1363
1969
  case 35:
1364
1970
  case 36:
1365
1971
  case 37:
1366
- attrs.fg = { ...palette256[code - 30] };
1972
+ attrs.fg = {
1973
+ ...palette256[code - 30],
1974
+ index: code - 30
1975
+ };
1367
1976
  break;
1368
1977
  case 38: {
1369
1978
  const subs = subParams.get(i);
@@ -1390,7 +1999,10 @@ function createScreen(options = {}) {
1390
1999
  case 45:
1391
2000
  case 46:
1392
2001
  case 47:
1393
- attrs.bg = { ...palette256[code - 40] };
2002
+ attrs.bg = {
2003
+ ...palette256[code - 40],
2004
+ index: code - 40
2005
+ };
1394
2006
  break;
1395
2007
  case 48: {
1396
2008
  const subs = subParams.get(i);
@@ -1440,7 +2052,10 @@ function createScreen(options = {}) {
1440
2052
  case 95:
1441
2053
  case 96:
1442
2054
  case 97:
1443
- attrs.fg = { ...palette256[code - 90 + 8] };
2055
+ attrs.fg = {
2056
+ ...palette256[code - 90 + 8],
2057
+ index: code - 90 + 8
2058
+ };
1444
2059
  break;
1445
2060
  case 100:
1446
2061
  case 101:
@@ -1450,7 +2065,10 @@ function createScreen(options = {}) {
1450
2065
  case 105:
1451
2066
  case 106:
1452
2067
  case 107:
1453
- attrs.bg = { ...palette256[code - 100 + 8] };
2068
+ attrs.bg = {
2069
+ ...palette256[code - 100 + 8],
2070
+ index: code - 100 + 8
2071
+ };
1454
2072
  break;
1455
2073
  }
1456
2074
  i++;
@@ -1461,12 +2079,16 @@ function createScreen(options = {}) {
1461
2079
  const type = params[startIndex + 1];
1462
2080
  if (type === 5 && startIndex + 2 < params.length) {
1463
2081
  const idx = params[startIndex + 2];
2082
+ const entry = palette256[idx];
1464
2083
  return {
1465
- color: { ...palette256[idx] ?? {
2084
+ color: entry ? {
2085
+ ...entry,
2086
+ index: idx
2087
+ } : {
1466
2088
  r: 0,
1467
2089
  g: 0,
1468
2090
  b: 0
1469
- } },
2091
+ },
1470
2092
  nextIndex: startIndex + 3
1471
2093
  };
1472
2094
  } else if (type === 2 && startIndex + 4 < params.length) return {
@@ -1485,11 +2107,15 @@ function createScreen(options = {}) {
1485
2107
  const type = subs[1];
1486
2108
  if (type === 5 && subs.length >= 3) {
1487
2109
  const idx = subs[2];
1488
- return { ...palette256[idx] ?? {
2110
+ const entry = palette256[idx];
2111
+ return entry ? {
2112
+ ...entry,
2113
+ index: idx
2114
+ } : {
1489
2115
  r: 0,
1490
2116
  g: 0,
1491
2117
  b: 0
1492
- } };
2118
+ };
1493
2119
  } else if (type === 2) {
1494
2120
  if (subs.length >= 5) {
1495
2121
  if (subs.length >= 6) return {
@@ -1582,7 +2208,10 @@ function createScreen(options = {}) {
1582
2208
  if (isNaN(idx) || idx < 0 || idx > 4) continue;
1583
2209
  if (spec === "?") {
1584
2210
  const c = specialColors.get(idx);
1585
- if (onResponse) onResponse(`\x1b]5;${idx};${c ? formatColorResponse(c) : "rgb:0000/0000/0000"}\x1b\\`);
2211
+ if (onResponse) {
2212
+ const payload = c ? formatColorResponse(c) : "rgb:0000/0000/0000";
2213
+ onResponse(`\x1b]5;${idx};${payload}\x1b\\`);
2214
+ }
1586
2215
  } else {
1587
2216
  const c = parseColorSpec(spec);
1588
2217
  if (c) specialColors.set(idx, c);
@@ -1626,6 +2255,30 @@ function createScreen(options = {}) {
1626
2255
  if (c) cursorColor = c;
1627
2256
  }
1628
2257
  break;
2258
+ case 13:
2259
+ if (value === "?") {
2260
+ if (onResponse) onResponse(`\x1b]13;${formatColorResponse(pointerFgColor ?? defaultFgColor ?? {
2261
+ r: 255,
2262
+ g: 255,
2263
+ b: 255
2264
+ })}\x1b\\`);
2265
+ } else {
2266
+ const c = parseColorSpec(value);
2267
+ if (c) pointerFgColor = c;
2268
+ }
2269
+ break;
2270
+ case 14:
2271
+ if (value === "?") {
2272
+ if (onResponse) onResponse(`\x1b]14;${formatColorResponse(pointerBgColor ?? defaultBgColor ?? {
2273
+ r: 0,
2274
+ g: 0,
2275
+ b: 0
2276
+ })}\x1b\\`);
2277
+ } else {
2278
+ const c = parseColorSpec(value);
2279
+ if (c) pointerBgColor = c;
2280
+ }
2281
+ break;
1629
2282
  case 17:
1630
2283
  if (value === "?") {
1631
2284
  if (onResponse) onResponse(`\x1b]17;${formatColorResponse(highlightBgColor ?? defaultFgColor ?? {
@@ -1755,6 +2408,12 @@ function createScreen(options = {}) {
1755
2408
  case 112:
1756
2409
  cursorColor = null;
1757
2410
  break;
2411
+ case 113:
2412
+ pointerFgColor = null;
2413
+ break;
2414
+ case 114:
2415
+ pointerBgColor = null;
2416
+ break;
1758
2417
  case 117:
1759
2418
  highlightBgColor = null;
1760
2419
  break;
@@ -1766,7 +2425,10 @@ function createScreen(options = {}) {
1766
2425
  if (clipSemi !== -1) {
1767
2426
  const data = value.substring(clipSemi + 1);
1768
2427
  if (data === "?") {
1769
- if (onResponse) onResponse(`\x1b]52;c;${btoa(clipboard)}\x1b\\`);
2428
+ if (onResponse) {
2429
+ const encoded = btoa(clipboard);
2430
+ onResponse(`\x1b]52;c;${encoded}\x1b\\`);
2431
+ }
1770
2432
  } else {
1771
2433
  try {
1772
2434
  clipboard = atob(data);
@@ -1820,9 +2482,58 @@ function createScreen(options = {}) {
1820
2482
  }
1821
2483
  }
1822
2484
  break;
2485
+ case 701:
2486
+ if (value === "?") {
2487
+ if (onResponse) onResponse(`\x1b]701;${locale}\x1b\\`);
2488
+ } else if (value !== "") locale = value;
2489
+ break;
2490
+ case 702:
2491
+ if (onResponse) onResponse("\x1B]702;vterm.js;vterm;0;2\x1B\\");
2492
+ break;
2493
+ case 720: {
2494
+ const n = parseInt(value, 10);
2495
+ scrollViewport(!isNaN(n) && n > 0 ? n : 1);
2496
+ break;
2497
+ }
2498
+ case 721: {
2499
+ const n = parseInt(value, 10);
2500
+ scrollViewport(-(!isNaN(n) && n > 0 ? n : 1));
2501
+ break;
2502
+ }
2503
+ case 776:
2504
+ if (onResponse) onResponse(`\x1b]776;${CELL_W_PX};${CELL_H_PX};${FONT_ASCENT_PX}\x1b\\`);
2505
+ break;
2506
+ case 7770:
2507
+ if (value === "?") {
2508
+ if (onResponse) onResponse(`\x1b]7770;${fontSize}\x1b\\`);
2509
+ } else if (value === "") fontSize = 12;
2510
+ else {
2511
+ const n = parseInt(value, 10);
2512
+ if (!isNaN(n) && n > 0) fontSize = n;
2513
+ }
2514
+ break;
2515
+ case 7777:
2516
+ if (value === "?") {
2517
+ if (onResponse) onResponse(`\x1b]7777;${fontWindowSize}\x1b\\`);
2518
+ } else if (value === "") fontWindowSize = 12;
2519
+ else {
2520
+ const n = parseInt(value, 10);
2521
+ if (!isNaN(n) && n > 0) fontWindowSize = n;
2522
+ }
2523
+ break;
2524
+ case 30001:
2525
+ colorStack.push(snapshotColorState());
2526
+ break;
2527
+ case 30101: {
2528
+ const snapshot = colorStack.pop();
2529
+ if (snapshot) restoreColorState(snapshot);
2530
+ break;
2531
+ }
1823
2532
  case 5522:
1824
- if (value === "?" && onResponse) onResponse(`\x1b]5522;${btoa(advancedClipboard)}\x1b\\`);
1825
- else try {
2533
+ if (value === "?" && onResponse) {
2534
+ const encoded = btoa(advancedClipboard);
2535
+ onResponse(`\x1b]5522;${encoded}\x1b\\`);
2536
+ } else try {
1826
2537
  advancedClipboard = atob(value);
1827
2538
  } catch {
1828
2539
  advancedClipboard = "";
@@ -1851,8 +2562,10 @@ function createScreen(options = {}) {
1851
2562
  return;
1852
2563
  }
1853
2564
  if (data.startsWith("+q") && onResponse) {
1854
- if (data.substring(2) === "544e") onResponse(`\x1bP1+r544e=${Array.from(new TextEncoder().encode("vterm")).map((b) => b.toString(16).padStart(2, "0")).join("")}\x1b\\`);
1855
- else onResponse("\x1BP0+r\x1B\\");
2565
+ if (data.substring(2) === "544e") {
2566
+ const hexValue = Array.from(new TextEncoder().encode("vterm")).map((b) => b.toString(16).padStart(2, "0")).join("");
2567
+ onResponse(`\x1bP1+r544e=${hexValue}\x1b\\`);
2568
+ } else onResponse("\x1BP0+r\x1B\\");
1856
2569
  return;
1857
2570
  }
1858
2571
  }
@@ -1898,6 +2611,7 @@ function createScreen(options = {}) {
1898
2611
  altGrid = makeGrid(cols, rows);
1899
2612
  grid = mainGrid;
1900
2613
  scrollback = [];
2614
+ scrollbackSoftWrapped = [];
1901
2615
  curX = 0;
1902
2616
  curY = 0;
1903
2617
  curVisible = true;
@@ -1943,6 +2657,9 @@ function createScreen(options = {}) {
1943
2657
  altScrollMode = false;
1944
2658
  utf8MouseMode = false;
1945
2659
  textScale = 1;
2660
+ fontSize = 12;
2661
+ fontWindowSize = 12;
2662
+ locale = "en_US.UTF-8";
1946
2663
  advancedClipboard = "";
1947
2664
  viewportOffset = 0;
1948
2665
  charsetG0 = false;
@@ -1954,8 +2671,11 @@ function createScreen(options = {}) {
1954
2671
  defaultBgColor = null;
1955
2672
  cursorColor = null;
1956
2673
  specialColors.clear();
2674
+ pointerFgColor = null;
2675
+ pointerBgColor = null;
1957
2676
  highlightBgColor = null;
1958
2677
  highlightFgColor = null;
2678
+ colorStack.length = 0;
1959
2679
  tabStops = defaultTabStops(cols);
1960
2680
  lastChar = "";
1961
2681
  pendingRegionalIndicator = null;
@@ -1963,14 +2683,87 @@ function createScreen(options = {}) {
1963
2683
  parserState = "ground";
1964
2684
  escBuf = "";
1965
2685
  oscBuf = "";
2686
+ dcsBuf = "";
2687
+ dcsStartRow = 0;
2688
+ dcsStartCol = 0;
1966
2689
  apcBuf = "";
2690
+ utf8PendingBytes = [];
1967
2691
  semanticZones = [];
1968
2692
  mainSoftWrapped = new Array(rows).fill(false);
1969
2693
  altSoftWrapped = new Array(rows).fill(false);
1970
2694
  softWrapped = mainSoftWrapped;
2695
+ markAllDirty();
2696
+ dirtyScrolled = 0;
2697
+ if (sigDamageActive) sigDirtyScrolled = 0;
2698
+ trimmedRowCount = 0;
2699
+ flushSignals();
2700
+ }
2701
+ function emitParserEvent(ev) {
2702
+ for (const listener of parserListeners) listener(ev);
2703
+ }
2704
+ function flushPrintRun() {
2705
+ if (printRunParts.length === 0) return;
2706
+ const text = printRunParts.join("");
2707
+ printRunParts = [];
2708
+ emitParserEvent({
2709
+ kind: "print",
2710
+ text
2711
+ });
2712
+ }
2713
+ function emitExecute(code) {
2714
+ if (parserListeners.size === 0) return;
2715
+ flushPrintRun();
2716
+ emitParserEvent({
2717
+ kind: "execute",
2718
+ code
2719
+ });
2720
+ }
2721
+ function emitCsiEvent(rawParams, intermediates, final) {
2722
+ if (parserListeners.size === 0) return;
2723
+ flushPrintRun();
2724
+ let prefix;
2725
+ let body = rawParams;
2726
+ const marker = body.charCodeAt(0);
2727
+ if (marker === 63 || marker === 62 || marker === 60 || marker === 61) {
2728
+ prefix = body[0];
2729
+ body = body.slice(1);
2730
+ }
2731
+ const ev = {
2732
+ kind: "csi",
2733
+ final,
2734
+ params: body.split(";").map((s) => s === "" ? 0 : parseInt(s, 10))
2735
+ };
2736
+ if (prefix !== void 0) ev.prefix = prefix;
2737
+ if (intermediates !== "") ev.intermediates = intermediates;
2738
+ emitParserEvent(ev);
2739
+ }
2740
+ function emitOscEvent(osc) {
2741
+ if (parserListeners.size === 0) return;
2742
+ const semi = osc.indexOf(";");
2743
+ const code = semi === -1 ? parseInt(osc, 10) : parseInt(osc.slice(0, semi), 10);
2744
+ if (Number.isNaN(code)) return;
2745
+ flushPrintRun();
2746
+ emitParserEvent({
2747
+ kind: "osc",
2748
+ code,
2749
+ data: semi === -1 ? "" : osc.slice(semi + 1)
2750
+ });
2751
+ }
2752
+ function emitEsc(final, intermediates) {
2753
+ if (parserListeners.size === 0) return;
2754
+ flushPrintRun();
2755
+ const ev = {
2756
+ kind: "esc",
2757
+ final
2758
+ };
2759
+ if (intermediates !== "") ev.intermediates = intermediates;
2760
+ emitParserEvent(ev);
2761
+ }
2762
+ function emitOp(op) {
2763
+ for (const listener of opListeners) listener(op);
1971
2764
  }
1972
2765
  function process(data) {
1973
- const text = decoder.decode(data, { stream: true });
2766
+ const text = decodeInput(data);
1974
2767
  for (let i = 0; i < text.length; i++) {
1975
2768
  const ch = text[i];
1976
2769
  const code = text.charCodeAt(i);
@@ -1979,18 +2772,25 @@ function createScreen(options = {}) {
1979
2772
  if (code === 27) {
1980
2773
  parserState = "escape";
1981
2774
  escBuf = "";
1982
- } else if (code === 7) {} else if (code === 8) {
2775
+ } else if (code === 7) emitExecute(7);
2776
+ else if (code === 8) {
1983
2777
  if (curX > 0) curX--;
1984
- } else if (code === 9) curX = nextTabStop(curX);
1985
- else if (code === 10 || code === 11 || code === 12) {
2778
+ emitExecute(8);
2779
+ } else if (code === 9) {
2780
+ curX = nextTabStop(curX);
2781
+ emitExecute(9);
2782
+ } else if (code === 10 || code === 11 || code === 12) {
1986
2783
  softWrapped[curY] = false;
1987
2784
  curY++;
1988
2785
  if (curY > scrollBottom) {
1989
2786
  curY = scrollBottom;
1990
2787
  scrollUp(scrollTop, scrollBottom);
1991
2788
  }
1992
- } else if (code === 13) curX = 0;
1993
- else if (code >= 32) {
2789
+ emitExecute(code);
2790
+ } else if (code === 13) {
2791
+ curX = 0;
2792
+ emitExecute(13);
2793
+ } else if (code >= 32) {
1994
2794
  let char = ch;
1995
2795
  if (code >= 55296 && code <= 56319 && i + 1 < text.length) {
1996
2796
  const nextCode = text.charCodeAt(i + 1);
@@ -2000,6 +2800,7 @@ function createScreen(options = {}) {
2000
2800
  }
2001
2801
  }
2002
2802
  writeChar(char);
2803
+ if (parserListeners.size > 0) printRunParts.push(char);
2003
2804
  }
2004
2805
  break;
2005
2806
  case "escape":
@@ -2018,6 +2819,7 @@ function createScreen(options = {}) {
2018
2819
  } else if (ch === "c") {
2019
2820
  fullReset();
2020
2821
  parserState = "ground";
2822
+ emitEsc("c", "");
2021
2823
  } else if (ch === "D") {
2022
2824
  curY++;
2023
2825
  if (curY > scrollBottom) {
@@ -2025,6 +2827,7 @@ function createScreen(options = {}) {
2025
2827
  scrollUp(scrollTop, scrollBottom);
2026
2828
  }
2027
2829
  parserState = "ground";
2830
+ emitEsc("D", "");
2028
2831
  } else if (ch === "M") {
2029
2832
  curY--;
2030
2833
  if (curY < scrollTop) {
@@ -2032,6 +2835,7 @@ function createScreen(options = {}) {
2032
2835
  scrollDown(scrollTop, scrollBottom);
2033
2836
  }
2034
2837
  parserState = "ground";
2838
+ emitEsc("M", "");
2035
2839
  } else if (ch === "7") {
2036
2840
  savedState = {
2037
2841
  curX,
@@ -2047,6 +2851,7 @@ function createScreen(options = {}) {
2047
2851
  charsetG0
2048
2852
  };
2049
2853
  parserState = "ground";
2854
+ emitEsc("7", "");
2050
2855
  } else if (ch === "8") {
2051
2856
  curX = savedState.curX;
2052
2857
  curY = savedState.curY;
@@ -2061,6 +2866,7 @@ function createScreen(options = {}) {
2061
2866
  charsetG0 = savedState.charsetG0;
2062
2867
  clampCursor();
2063
2868
  parserState = "ground";
2869
+ emitEsc("8", "");
2064
2870
  } else if (ch === "E") {
2065
2871
  curX = 0;
2066
2872
  curY++;
@@ -2069,18 +2875,28 @@ function createScreen(options = {}) {
2069
2875
  scrollUp(scrollTop, scrollBottom);
2070
2876
  }
2071
2877
  parserState = "ground";
2878
+ emitEsc("E", "");
2072
2879
  } else if (ch === "H") {
2073
2880
  tabStops.add(curX);
2074
2881
  parserState = "ground";
2075
- } else if (ch === "#") parserState = "escape_hash";
2076
- else if (ch === "(") parserState = "escape_charset";
2077
- else if (ch === ")") parserState = "escape_charset";
2078
- else if (ch === "=") {
2882
+ emitEsc("H", "");
2883
+ } else if (ch === "#") {
2884
+ escIntermediate = "#";
2885
+ parserState = "escape_hash";
2886
+ } else if (ch === "(") {
2887
+ escIntermediate = "(";
2888
+ parserState = "escape_charset";
2889
+ } else if (ch === ")") {
2890
+ escIntermediate = ")";
2891
+ parserState = "escape_charset";
2892
+ } else if (ch === "=") {
2079
2893
  applicationKeypad = true;
2080
2894
  parserState = "ground";
2895
+ emitEsc("=", "");
2081
2896
  } else if (ch === ">") {
2082
2897
  applicationKeypad = false;
2083
2898
  parserState = "ground";
2899
+ emitEsc(">", "");
2084
2900
  } else if (ch === "_") {
2085
2901
  parserState = "apc";
2086
2902
  apcBuf = "";
@@ -2090,22 +2906,22 @@ function createScreen(options = {}) {
2090
2906
  if (ch === "0") charsetG0 = true;
2091
2907
  else charsetG0 = false;
2092
2908
  parserState = "ground";
2909
+ emitEsc(ch, escIntermediate);
2093
2910
  break;
2094
2911
  case "escape_hash":
2095
2912
  if (ch === "8") {
2913
+ const alignCell = emptyCell();
2914
+ alignCell.char = "E";
2096
2915
  for (let r = 0; r < rows; r++) {
2097
2916
  const row = grid[r];
2098
- for (let c = 0; c < cols; c++) {
2099
- const cell = emptyCell();
2100
- cell.char = "E";
2101
- row[c] = cell;
2102
- }
2917
+ for (let c = 0; c < cols; c++) row.setCellRaw(c, alignCell);
2103
2918
  softWrapped[r] = false;
2104
2919
  }
2105
2920
  curX = 0;
2106
2921
  curY = 0;
2107
2922
  }
2108
2923
  parserState = "ground";
2924
+ emitEsc(ch, escIntermediate);
2109
2925
  break;
2110
2926
  case "csi":
2111
2927
  if (code >= 64 && code <= 126) {
@@ -2127,6 +2943,7 @@ function createScreen(options = {}) {
2127
2943
  else if (paramPart.startsWith("=")) handleCSIEq(paramPart.substring(1), intermediatePart, ch);
2128
2944
  else handleCSI(paramPart, intermediatePart, ch);
2129
2945
  parserState = "ground";
2946
+ emitCsiEvent(paramPart, intermediatePart, ch);
2130
2947
  } else if (escBuf.length >= 256) parserState = "ground";
2131
2948
  else escBuf += ch;
2132
2949
  break;
@@ -2134,14 +2951,18 @@ function createScreen(options = {}) {
2134
2951
  if (code === 7) {
2135
2952
  handleOSC(oscBuf);
2136
2953
  parserState = "ground";
2954
+ emitOscEvent(oscBuf);
2137
2955
  } else if (code === 27) parserState = "osc_st";
2138
2956
  else if (oscBuf.length >= 4096) parserState = "ground";
2139
2957
  else oscBuf += ch;
2140
2958
  break;
2141
- case "osc_st":
2142
- if (ch === "\\") handleOSC(oscBuf);
2959
+ case "osc_st": {
2960
+ const oscComplete = ch === "\\";
2961
+ if (oscComplete) handleOSC(oscBuf);
2143
2962
  parserState = "ground";
2963
+ if (oscComplete) emitOscEvent(oscBuf);
2144
2964
  break;
2965
+ }
2145
2966
  case "dcs":
2146
2967
  if (code === 27) parserState = "dcs_st";
2147
2968
  else if (code === 7) {
@@ -2169,6 +2990,12 @@ function createScreen(options = {}) {
2169
2990
  break;
2170
2991
  }
2171
2992
  }
2993
+ flushPrintRun();
2994
+ if (opListeners.size > 0) emitOp({
2995
+ type: "output",
2996
+ data
2997
+ });
2998
+ flushSignals();
2172
2999
  }
2173
3000
  /**
2174
3001
  * Reconstruct logical lines from a grid, joining rows that were soft-wrapped.
@@ -2180,7 +3007,7 @@ function createScreen(options = {}) {
2180
3007
  for (let r = 0; r < srcRows; r++) {
2181
3008
  const row = srcGrid[r];
2182
3009
  if (!row) continue;
2183
- for (let c = 0; c < row.length; c++) currentLine.push(row[c]);
3010
+ for (let c = 0; c < row.length; c++) currentLine.push(row.getCellRaw(c));
2184
3011
  if (srcSoftWrapped[r]) continue;
2185
3012
  logical.push(currentLine);
2186
3013
  currentLine = [];
@@ -2190,20 +3017,34 @@ function createScreen(options = {}) {
2190
3017
  }
2191
3018
  /**
2192
3019
  * Re-wrap logical lines to a new column width, producing grid rows and soft-wrap flags.
3020
+ *
3021
+ * `track` names one position — a logical line index plus a cell offset
3022
+ * within that line (the cursor, expressed content-relative) — and the
3023
+ * returned `tracked` reports the output row/col it lands on, so `resize()`
3024
+ * can move the cursor WITH its logical line instead of leaving it at its
3025
+ * old absolute row. An offset at/beyond the trimmed line end (cursor in
3026
+ * trailing blanks or one past the last cell) lands after the last emitted
3027
+ * cell, advanced by the leftover distance, clamped to the row.
2193
3028
  */
2194
- function rewrapLines(logicalLines, newCols) {
3029
+ function rewrapLines(logicalLines, newCols, track) {
2195
3030
  const outRows = [];
2196
3031
  const outWrapped = [];
2197
- for (const line of logicalLines) {
3032
+ let tracked = null;
3033
+ for (let li = 0; li < logicalLines.length; li++) {
3034
+ const line = logicalLines[li];
2198
3035
  let lineLen = line.length;
2199
3036
  while (lineLen > 0) {
2200
3037
  const cell = line[lineLen - 1];
2201
- if (cell === EMPTY_CELL || cell.char === "" && !cell.wide) lineLen--;
3038
+ if (cell.char === "" && !cell.wide) lineLen--;
2202
3039
  else break;
2203
3040
  }
2204
3041
  if (lineLen === 0) {
2205
3042
  outRows.push(makeRow(newCols));
2206
3043
  outWrapped.push(false);
3044
+ if (track !== void 0 && track.line === li) tracked = {
3045
+ row: outRows.length - 1,
3046
+ col: Math.min(track.offset, newCols - 1)
3047
+ };
2207
3048
  continue;
2208
3049
  }
2209
3050
  let pos = 0;
@@ -2213,34 +3054,65 @@ function createScreen(options = {}) {
2213
3054
  while (col < newCols && pos < lineLen) {
2214
3055
  const cell = line[pos];
2215
3056
  if (cell.wide && col + 2 > newCols) break;
2216
- row[col] = cell === EMPTY_CELL ? EMPTY_CELL : { ...cell };
3057
+ if (track !== void 0 && track.line === li && pos === track.offset) tracked = {
3058
+ row: outRows.length,
3059
+ col
3060
+ };
3061
+ row.setCellRaw(col, cell);
2217
3062
  col++;
2218
3063
  pos++;
2219
3064
  }
2220
3065
  const moreContent = pos < lineLen;
2221
3066
  outRows.push(row);
2222
3067
  outWrapped.push(moreContent);
3068
+ if (!moreContent && track !== void 0 && track.line === li && tracked === null) {
3069
+ const extra = track.offset - lineLen;
3070
+ tracked = {
3071
+ row: outRows.length - 1,
3072
+ col: Math.min(newCols - 1, col + extra)
3073
+ };
3074
+ }
2223
3075
  }
2224
3076
  }
2225
3077
  return {
2226
3078
  rows: outRows,
2227
- wrapped: outWrapped
3079
+ wrapped: outWrapped,
3080
+ tracked
2228
3081
  };
2229
3082
  }
3083
+ /** True when a packed row has no printable content (all columns blank). */
3084
+ function packedRowIsBlank(row) {
3085
+ for (let c = 0; c < row.length; c++) if (!(row.getChar(c) === "" && !row.isWide(c))) return false;
3086
+ return true;
3087
+ }
2230
3088
  /**
2231
3089
  * Trim trailing empty rows from reflowed result, so they don't push content off the top
2232
3090
  * when we take the last newRows rows.
2233
3091
  */
2234
3092
  function trimTrailingEmptyRows(result) {
2235
- while (result.rows.length > 1) if (result.rows[result.rows.length - 1].every((cell) => cell === EMPTY_CELL || cell.char === "" && !cell.wide) && !result.wrapped[result.rows.length - 2]) {
2236
- result.rows.pop();
2237
- result.wrapped.pop();
2238
- } else break;
3093
+ while (result.rows.length > 1) {
3094
+ const lastRow = result.rows[result.rows.length - 1];
3095
+ if (packedRowIsBlank(lastRow) && !result.wrapped[result.rows.length - 2]) {
3096
+ result.rows.pop();
3097
+ result.wrapped.pop();
3098
+ } else break;
3099
+ }
2239
3100
  }
2240
3101
  function resize(newCols, newRows) {
2241
- const mainResult = rewrapLines(getLogicalLines(mainGrid, mainSoftWrapped, rows), newCols);
3102
+ if (newCols === cols && newRows === rows) return;
3103
+ const activeWrappedPre = useAltScreen ? altSoftWrapped : mainSoftWrapped;
3104
+ const cursorRowPre = Math.min(Math.max(curY, 0), rows - 1);
3105
+ let cursorLineStart = cursorRowPre;
3106
+ while (cursorLineStart > 0 && activeWrappedPre[cursorLineStart - 1]) cursorLineStart--;
3107
+ let cursorLineIdx = 0;
3108
+ for (let r = 0; r < cursorLineStart; r++) if (!activeWrappedPre[r]) cursorLineIdx++;
3109
+ const cursorTrack = {
3110
+ line: cursorLineIdx,
3111
+ offset: (cursorRowPre - cursorLineStart) * cols + curX
3112
+ };
3113
+ const mainResult = rewrapLines(getLogicalLines(mainGrid, mainSoftWrapped, rows), newCols, useAltScreen ? void 0 : cursorTrack);
2242
3114
  trimTrailingEmptyRows(mainResult);
2243
- const altResult = rewrapLines(getLogicalLines(altGrid, altSoftWrapped, rows), newCols);
3115
+ const altResult = rewrapLines(getLogicalLines(altGrid, altSoftWrapped, rows), newCols, useAltScreen ? cursorTrack : void 0);
2244
3116
  trimTrailingEmptyRows(altResult);
2245
3117
  const newMain = makeGrid(newCols, newRows);
2246
3118
  const newMainWrapped = new Array(newRows).fill(false);
@@ -2256,6 +3128,12 @@ function createScreen(options = {}) {
2256
3128
  newAlt[r] = altResult.rows[altStartRow + r];
2257
3129
  newAltWrapped[r] = altResult.wrapped[altStartRow + r];
2258
3130
  }
3131
+ const activeResult = useAltScreen ? altResult : mainResult;
3132
+ const activeStartRow = useAltScreen ? altStartRow : mainStartRow;
3133
+ if (activeResult.tracked !== null) {
3134
+ curY = Math.min(activeResult.tracked.row, activeResult.rows.length) - activeStartRow;
3135
+ curX = activeResult.tracked.col;
3136
+ }
2259
3137
  mainGrid = newMain;
2260
3138
  altGrid = newAlt;
2261
3139
  mainSoftWrapped = newMainWrapped;
@@ -2271,31 +3149,265 @@ function createScreen(options = {}) {
2271
3149
  rightMargin = cols - 1;
2272
3150
  if (newCols > oldCols) for (let i = Math.max(8, Math.ceil(oldCols / 8) * 8); i < newCols; i += 8) tabStops.add(i);
2273
3151
  clampCursor();
3152
+ markAllDirty();
3153
+ if (opListeners.size > 0) emitOp({
3154
+ type: "resize",
3155
+ cols: newCols,
3156
+ rows: newRows
3157
+ });
3158
+ flushSignals();
3159
+ }
3160
+ function snapshot() {
3161
+ return {
3162
+ version: 1,
3163
+ cols,
3164
+ rows,
3165
+ scrollbackLimit,
3166
+ activeBuffer: useAltScreen ? "alt" : "main",
3167
+ main: {
3168
+ grid: cloneGridSnapshot(mainGrid),
3169
+ softWrapped: [...mainSoftWrapped]
3170
+ },
3171
+ alt: {
3172
+ grid: cloneGridSnapshot(altGrid),
3173
+ softWrapped: [...altSoftWrapped]
3174
+ },
3175
+ scrollback: cloneGridSnapshot(scrollback),
3176
+ scrollbackSoftWrapped: [...scrollbackSoftWrapped],
3177
+ cursor: {
3178
+ x: curX,
3179
+ y: curY,
3180
+ visible: curVisible,
3181
+ shape: cursorShape,
3182
+ blinking: cursorBlinking,
3183
+ savedX: savedCurX,
3184
+ savedY: savedCurY
3185
+ },
3186
+ savedState: {
3187
+ x: savedState.curX,
3188
+ y: savedState.curY,
3189
+ attrs: cloneAttrsSnapshot(savedState.attrs),
3190
+ originMode: savedState.originMode,
3191
+ autoWrap: savedState.autoWrap,
3192
+ charsetG0: savedState.charsetG0
3193
+ },
3194
+ attrs: cloneAttrsSnapshot(attrs),
3195
+ modes: {
3196
+ bracketedPaste,
3197
+ applicationCursor,
3198
+ applicationKeypad,
3199
+ autoWrap,
3200
+ mouseTracking,
3201
+ mouseTrackingMode,
3202
+ sgrMouse,
3203
+ focusTracking,
3204
+ origin: originMode,
3205
+ insert: insertMode,
3206
+ reverseVideo,
3207
+ syncOutput,
3208
+ kittyKeyboardFlags,
3209
+ kittyKeyboardStack: [...kittyKeyboardStack],
3210
+ kittyGraphics: hasKittyGraphics,
3211
+ colorSchemeReporting,
3212
+ decColumn: decColumnMode,
3213
+ altScroll: altScrollMode,
3214
+ utf8Mouse: utf8MouseMode
3215
+ },
3216
+ margins: {
3217
+ scrollTop,
3218
+ scrollBottom,
3219
+ leftRight: leftRightMarginMode,
3220
+ left: leftMargin,
3221
+ right: rightMargin
3222
+ },
3223
+ colors: {
3224
+ current: snapshotColorState(),
3225
+ stack: colorStack.map(cloneColorStateSnapshot)
3226
+ },
3227
+ tabStops: [...tabStops].sort((a, b) => a - b),
3228
+ title,
3229
+ clipboard,
3230
+ cwd,
3231
+ notifications: [...notifications],
3232
+ viewportOffset,
3233
+ parser: {
3234
+ state: parserState,
3235
+ esc: escBuf,
3236
+ osc: oscBuf,
3237
+ dcs: dcsBuf,
3238
+ dcsStart: {
3239
+ row: dcsStartRow,
3240
+ col: dcsStartCol
3241
+ },
3242
+ apc: apcBuf,
3243
+ utf8PendingBytes: [...utf8PendingBytes]
3244
+ },
3245
+ unicode: {
3246
+ charsetG0,
3247
+ lastChar,
3248
+ pendingRegionalIndicator,
3249
+ afterZWJ
3250
+ }
3251
+ };
3252
+ }
3253
+ function restore(snapshotValue) {
3254
+ assertSnapshot(snapshotValue);
3255
+ cols = snapshotValue.cols;
3256
+ rows = snapshotValue.rows;
3257
+ scrollbackLimit = snapshotValue.scrollbackLimit;
3258
+ mainGrid = restoreGridSnapshot(snapshotValue.main.grid, rows, cols);
3259
+ altGrid = restoreGridSnapshot(snapshotValue.alt.grid, rows, cols);
3260
+ scrollback = restoreScrollbackSnapshot(snapshotValue.scrollback);
3261
+ scrollbackSoftWrapped = snapshotValue.scrollbackSoftWrapped ? [...snapshotValue.scrollbackSoftWrapped] : new Array(scrollback.length).fill(false);
3262
+ mainSoftWrapped = [...snapshotValue.main.softWrapped];
3263
+ altSoftWrapped = [...snapshotValue.alt.softWrapped];
3264
+ useAltScreen = snapshotValue.activeBuffer === "alt";
3265
+ grid = useAltScreen ? altGrid : mainGrid;
3266
+ softWrapped = useAltScreen ? altSoftWrapped : mainSoftWrapped;
3267
+ curX = snapshotValue.cursor.x;
3268
+ curY = snapshotValue.cursor.y;
3269
+ curVisible = snapshotValue.cursor.visible;
3270
+ cursorShape = snapshotValue.cursor.shape;
3271
+ cursorBlinking = snapshotValue.cursor.blinking;
3272
+ savedCurX = snapshotValue.cursor.savedX;
3273
+ savedCurY = snapshotValue.cursor.savedY;
3274
+ savedState = {
3275
+ curX: snapshotValue.savedState.x,
3276
+ curY: snapshotValue.savedState.y,
3277
+ attrs: cloneAttrsSnapshot(snapshotValue.savedState.attrs),
3278
+ originMode: snapshotValue.savedState.originMode,
3279
+ autoWrap: snapshotValue.savedState.autoWrap,
3280
+ charsetG0: snapshotValue.savedState.charsetG0
3281
+ };
3282
+ attrs = cloneAttrsSnapshot(snapshotValue.attrs);
3283
+ bracketedPaste = snapshotValue.modes.bracketedPaste;
3284
+ applicationCursor = snapshotValue.modes.applicationCursor;
3285
+ applicationKeypad = snapshotValue.modes.applicationKeypad;
3286
+ autoWrap = snapshotValue.modes.autoWrap;
3287
+ mouseTracking = snapshotValue.modes.mouseTracking;
3288
+ mouseTrackingMode = snapshotValue.modes.mouseTrackingMode;
3289
+ sgrMouse = snapshotValue.modes.sgrMouse;
3290
+ focusTracking = snapshotValue.modes.focusTracking;
3291
+ originMode = snapshotValue.modes.origin;
3292
+ insertMode = snapshotValue.modes.insert;
3293
+ reverseVideo = snapshotValue.modes.reverseVideo;
3294
+ syncOutput = snapshotValue.modes.syncOutput;
3295
+ kittyKeyboardFlags = snapshotValue.modes.kittyKeyboardFlags;
3296
+ kittyKeyboardStack = [...snapshotValue.modes.kittyKeyboardStack];
3297
+ hasKittyGraphics = snapshotValue.modes.kittyGraphics;
3298
+ colorSchemeReporting = snapshotValue.modes.colorSchemeReporting;
3299
+ decColumnMode = snapshotValue.modes.decColumn;
3300
+ altScrollMode = snapshotValue.modes.altScroll;
3301
+ utf8MouseMode = snapshotValue.modes.utf8Mouse;
3302
+ scrollTop = snapshotValue.margins.scrollTop;
3303
+ scrollBottom = snapshotValue.margins.scrollBottom;
3304
+ leftRightMarginMode = snapshotValue.margins.leftRight;
3305
+ leftMargin = snapshotValue.margins.left;
3306
+ rightMargin = snapshotValue.margins.right;
3307
+ restoreColorState(snapshotValue.colors.current);
3308
+ colorStack.length = 0;
3309
+ colorStack.push(...snapshotValue.colors.stack.map(cloneColorStateSnapshot));
3310
+ tabStops = new Set(snapshotValue.tabStops);
3311
+ title = snapshotValue.title;
3312
+ clipboard = snapshotValue.clipboard;
3313
+ cwd = snapshotValue.cwd;
3314
+ notifications = [...snapshotValue.notifications];
3315
+ viewportOffset = Math.max(0, Math.min(scrollback.length, snapshotValue.viewportOffset));
3316
+ parserState = snapshotValue.parser.state;
3317
+ escBuf = snapshotValue.parser.esc;
3318
+ oscBuf = snapshotValue.parser.osc;
3319
+ dcsBuf = snapshotValue.parser.dcs;
3320
+ dcsStartRow = snapshotValue.parser.dcsStart.row;
3321
+ dcsStartCol = snapshotValue.parser.dcsStart.col;
3322
+ apcBuf = snapshotValue.parser.apc;
3323
+ utf8PendingBytes = [...snapshotValue.parser.utf8PendingBytes];
3324
+ charsetG0 = snapshotValue.unicode.charsetG0;
3325
+ lastChar = snapshotValue.unicode.lastChar;
3326
+ pendingRegionalIndicator = snapshotValue.unicode.pendingRegionalIndicator;
3327
+ afterZWJ = snapshotValue.unicode.afterZWJ;
3328
+ textScale = 1;
3329
+ fontSize = 12;
3330
+ fontWindowSize = 12;
3331
+ locale = "en_US.UTF-8";
3332
+ advancedClipboard = "";
3333
+ semanticZones = [];
3334
+ hasSixel = false;
3335
+ sixelImages = [];
3336
+ if (curX < 0) curX = 0;
3337
+ if (curX > cols) curX = cols;
3338
+ if (curY < 0) curY = 0;
3339
+ if (curY >= rows) curY = rows - 1;
3340
+ markAllDirty();
3341
+ dirtyScrolled = 0;
3342
+ if (sigDamageActive) sigDirtyScrolled = 0;
3343
+ trimmedRowCount = 0;
3344
+ flushSignals();
2274
3345
  }
2275
3346
  function getCell(row, col) {
2276
3347
  const r = grid[row];
2277
3348
  if (!r || col >= cols) return emptyCell();
2278
- return { ...r[col] };
3349
+ return stripCellColorIndex(r.getCellRaw(col));
2279
3350
  }
2280
3351
  function getLine(row) {
2281
3352
  const r = grid[row];
2282
- if (!r) return makeRow(cols);
2283
- return r.map((cell) => ({ ...cell }));
3353
+ if (!r) return makeRow(cols).toCells();
3354
+ return r.toCells().map(stripCellColorIndex);
2284
3355
  }
2285
3356
  function getText() {
2286
3357
  const lines = [];
2287
3358
  for (let r = 0; r < rows; r++) lines.push(rowToString(grid[r]));
2288
3359
  return lines.join("\n");
2289
3360
  }
3361
+ function getScrollbackText() {
3362
+ const lines = [];
3363
+ for (const row of scrollback) lines.push(rowToString(row));
3364
+ return lines.join("\n");
3365
+ }
3366
+ function totalRows() {
3367
+ return scrollback.length + rows;
3368
+ }
3369
+ function screenRows() {
3370
+ return rows;
3371
+ }
3372
+ function viewportTop() {
3373
+ return scrollback.length - viewportOffset;
3374
+ }
3375
+ function getRowAbsolute(row) {
3376
+ if (row < 0 || row >= scrollback.length + rows) return makeRow(cols).toCells();
3377
+ return (row < scrollback.length ? scrollback[row] : grid[row - scrollback.length]).toCells().map(stripCellColorIndex);
3378
+ }
3379
+ function firstRetainedRow() {
3380
+ return trimmedRowCount;
3381
+ }
3382
+ function takeDirty() {
3383
+ const cursorChanged = curX !== dirtyCursor.x || curY !== dirtyCursor.y || curVisible !== dirtyCursor.visible || cursorShape !== dirtyCursor.shape || cursorBlinking !== dirtyCursor.blinking;
3384
+ const region = {
3385
+ rows: dirtyAll ? "all" : dirtyRows,
3386
+ cursor: cursorChanged,
3387
+ scrolled: dirtyScrolled
3388
+ };
3389
+ dirtyRows = /* @__PURE__ */ new Set();
3390
+ dirtyAll = false;
3391
+ dirtyScrolled = 0;
3392
+ dirtyLastAbs = -1;
3393
+ dirtyCursor = {
3394
+ x: curX,
3395
+ y: curY,
3396
+ visible: curVisible,
3397
+ shape: cursorShape,
3398
+ blinking: cursorBlinking
3399
+ };
3400
+ return region;
3401
+ }
2290
3402
  function rowToString(row) {
2291
3403
  let line = "";
2292
3404
  for (let i = 0; i < row.length; i++) {
2293
- const cell = row[i];
2294
- if (cell.wide) line += cell.char;
2295
- else if (cell.char === "") {
2296
- if (i > 0 && row[i - 1]?.wide) continue;
3405
+ const ch = row.getChar(i);
3406
+ if (row.isWide(i)) line += ch;
3407
+ else if (ch === "") {
3408
+ if (i > 0 && row.isWide(i - 1)) continue;
2297
3409
  line += " ";
2298
- } else line += cell.char;
3410
+ } else line += ch;
2299
3411
  }
2300
3412
  return line.replace(/\s+$/, "");
2301
3413
  }
@@ -2308,10 +3420,10 @@ function createScreen(options = {}) {
2308
3420
  const colEnd = row === endRow ? endCol : cols;
2309
3421
  let line = "";
2310
3422
  for (let col = colStart; col < colEnd; col++) {
2311
- const cell = r[col];
2312
- if (!cell) continue;
2313
- if (cell.char === "" && col > 0 && r[col - 1]?.wide) continue;
2314
- line += cell.char || " ";
3423
+ if (col < 0 || col >= r.length) continue;
3424
+ const ch = r.getChar(col);
3425
+ if (ch === "" && col > 0 && r.isWide(col - 1)) continue;
3426
+ line += ch || " ";
2315
3427
  }
2316
3428
  parts.push(line.replace(/\s+$/, ""));
2317
3429
  }
@@ -2341,6 +3453,176 @@ function createScreen(options = {}) {
2341
3453
  default: return false;
2342
3454
  }
2343
3455
  }
3456
+ function apply(op) {
3457
+ switch (op.type) {
3458
+ case "output":
3459
+ process(typeof op.data === "string" ? encoder.encode(op.data) : op.data);
3460
+ return;
3461
+ case "resize":
3462
+ resize(op.cols, op.rows);
3463
+ return;
3464
+ default: throw new Error(`vterm.apply: unhandled op ${JSON.stringify(op)}`);
3465
+ }
3466
+ }
3467
+ function tapOps(listener) {
3468
+ opListeners.add(listener);
3469
+ return () => {
3470
+ opListeners.delete(listener);
3471
+ };
3472
+ }
3473
+ function tapParser(listener) {
3474
+ parserListeners.add(listener);
3475
+ return () => {
3476
+ parserListeners.delete(listener);
3477
+ };
3478
+ }
3479
+ const stateFlushSignals = [];
3480
+ /**
3481
+ * A state signal (title/modes/cursor/size): `get()` reads live; `subscribe` captures the current
3482
+ * value as its baseline (so only later changes deliver) and `flush()` delivers once per flush
3483
+ * boundary iff the value changed. Registered in `stateFlushSignals` so `flushSignals` visits it.
3484
+ */
3485
+ function makeStateSignal(read, eq) {
3486
+ const listeners = /* @__PURE__ */ new Set();
3487
+ let last = read();
3488
+ const signal = {
3489
+ get: read,
3490
+ subscribe(listener) {
3491
+ if (listeners.size === 0) last = read();
3492
+ listeners.add(listener);
3493
+ return () => {
3494
+ listeners.delete(listener);
3495
+ };
3496
+ },
3497
+ flush() {
3498
+ if (listeners.size === 0) return;
3499
+ const current = read();
3500
+ if (eq(current, last)) return;
3501
+ last = current;
3502
+ for (const listener of listeners) listener(current);
3503
+ }
3504
+ };
3505
+ stateFlushSignals.push(signal);
3506
+ return signal;
3507
+ }
3508
+ function modesNow() {
3509
+ return {
3510
+ altScreen: useAltScreen,
3511
+ cursorVisible: curVisible,
3512
+ bracketedPaste,
3513
+ applicationCursor,
3514
+ applicationKeypad,
3515
+ autoWrap,
3516
+ mouseTracking,
3517
+ mouseTrackingMode,
3518
+ sgrMouse,
3519
+ utf8Mouse: utf8MouseMode,
3520
+ focusTracking,
3521
+ originMode,
3522
+ insertMode,
3523
+ reverseVideo,
3524
+ syncOutput,
3525
+ leftRightMargin: leftRightMarginMode,
3526
+ colorSchemeReporting,
3527
+ kittyKeyboard: kittyKeyboardFlags > 0,
3528
+ kittyGraphics: hasKittyGraphics,
3529
+ sixel: hasSixel
3530
+ };
3531
+ }
3532
+ function modesEq(a, b) {
3533
+ 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;
3534
+ }
3535
+ const damageListeners = /* @__PURE__ */ new Set();
3536
+ let damageLast = {
3537
+ rows: /* @__PURE__ */ new Set(),
3538
+ cursor: false,
3539
+ scrolled: 0
3540
+ };
3541
+ let damageSignal;
3542
+ function getDamageSignal() {
3543
+ return damageSignal ??= {
3544
+ get: () => damageLast,
3545
+ subscribe(listener) {
3546
+ if (damageListeners.size === 0) {
3547
+ sigDamageActive = true;
3548
+ sigDirtyAll = false;
3549
+ sigDirtyRows = /* @__PURE__ */ new Set();
3550
+ sigDirtyScrolled = 0;
3551
+ sigDirtyLastAbs = -1;
3552
+ sigDirtyCursor = {
3553
+ x: curX,
3554
+ y: curY,
3555
+ visible: curVisible,
3556
+ shape: cursorShape,
3557
+ blinking: cursorBlinking
3558
+ };
3559
+ }
3560
+ damageListeners.add(listener);
3561
+ return () => {
3562
+ damageListeners.delete(listener);
3563
+ if (damageListeners.size === 0) sigDamageActive = false;
3564
+ };
3565
+ }
3566
+ };
3567
+ }
3568
+ function emitDamageBatch() {
3569
+ const cursorChanged = curX !== sigDirtyCursor.x || curY !== sigDirtyCursor.y || curVisible !== sigDirtyCursor.visible || cursorShape !== sigDirtyCursor.shape || cursorBlinking !== sigDirtyCursor.blinking;
3570
+ if (!sigDirtyAll && sigDirtyRows.size === 0 && sigDirtyScrolled === 0 && !cursorChanged) return;
3571
+ const region = {
3572
+ rows: sigDirtyAll ? "all" : sigDirtyRows,
3573
+ cursor: cursorChanged,
3574
+ scrolled: sigDirtyScrolled
3575
+ };
3576
+ sigDirtyRows = /* @__PURE__ */ new Set();
3577
+ sigDirtyAll = false;
3578
+ sigDirtyScrolled = 0;
3579
+ sigDirtyLastAbs = -1;
3580
+ sigDirtyCursor = {
3581
+ x: curX,
3582
+ y: curY,
3583
+ visible: curVisible,
3584
+ shape: cursorShape,
3585
+ blinking: cursorBlinking
3586
+ };
3587
+ damageLast = region;
3588
+ for (const listener of damageListeners) listener(region);
3589
+ }
3590
+ let signalsFacade;
3591
+ let titleSignal;
3592
+ let modesSignal;
3593
+ let cursorSignal;
3594
+ let sizeSignal;
3595
+ function getSignals() {
3596
+ return signalsFacade ??= {
3597
+ get title$() {
3598
+ return titleSignal ??= makeStateSignal(() => title, (a, b) => a === b);
3599
+ },
3600
+ get modes$() {
3601
+ return modesSignal ??= makeStateSignal(modesNow, modesEq);
3602
+ },
3603
+ get cursor$() {
3604
+ return cursorSignal ??= makeStateSignal(() => ({
3605
+ col: curX,
3606
+ row: curY
3607
+ }), (a, b) => a.col === b.col && a.row === b.row);
3608
+ },
3609
+ get size$() {
3610
+ return sizeSignal ??= makeStateSignal(() => ({
3611
+ cols,
3612
+ rows
3613
+ }), (a, b) => a.cols === b.cols && a.rows === b.rows);
3614
+ },
3615
+ get damage$() {
3616
+ return getDamageSignal();
3617
+ }
3618
+ };
3619
+ }
3620
+ /** Coalesced end-of-call emission for every accessed signal — the flush boundary (§4). */
3621
+ function flushSignals() {
3622
+ if (signalsFacade === void 0) return;
3623
+ for (const signal of stateFlushSignals) signal.flush();
3624
+ if (sigDamageActive) emitDamageBatch();
3625
+ }
2344
3626
  return {
2345
3627
  get cols() {
2346
3628
  return cols;
@@ -2348,13 +3630,33 @@ function createScreen(options = {}) {
2348
3630
  get rows() {
2349
3631
  return rows;
2350
3632
  },
3633
+ get signals() {
3634
+ return getSignals();
3635
+ },
2351
3636
  process,
2352
3637
  resize,
3638
+ apply,
3639
+ tapOps,
3640
+ tapParser,
2353
3641
  reset: fullReset,
3642
+ snapshot,
3643
+ restore,
2354
3644
  getCell,
3645
+ getRow: getLine,
2355
3646
  getLine,
2356
3647
  getText,
3648
+ getScrollbackText,
2357
3649
  getTextRange,
3650
+ totalRows,
3651
+ screenRows,
3652
+ viewportTop,
3653
+ getRowAbsolute,
3654
+ firstRetainedRow,
3655
+ takeDirty,
3656
+ getCursor: () => ({
3657
+ col: curX,
3658
+ row: curY
3659
+ }),
2358
3660
  getCursorPosition: () => ({
2359
3661
  x: curX,
2360
3662
  y: curY
@@ -2369,12 +3671,890 @@ function createScreen(options = {}) {
2369
3671
  getNotifications: () => [...notifications],
2370
3672
  getScrollbackLength: () => scrollback.length,
2371
3673
  getViewportOffset: () => viewportOffset,
2372
- scrollViewport: (delta) => {
2373
- viewportOffset = Math.max(0, Math.min(scrollback.length, viewportOffset + delta));
2374
- },
3674
+ scrollViewport,
2375
3675
  getSemanticZones: () => semanticZones.map((z) => ({ ...z })),
2376
- getSixelImages: () => sixelImages.map((img) => ({ ...img }))
3676
+ getSixelImages: () => sixelImages.map((img) => ({ ...img })),
3677
+ serialize: (options) => serializeSnapshot(snapshot(), options)
3678
+ };
3679
+ }
3680
+ const SERIALIZE_DEFAULT_PEN = Object.freeze({
3681
+ fg: null,
3682
+ bg: null,
3683
+ bold: false,
3684
+ faint: false,
3685
+ italic: false,
3686
+ underline: "none",
3687
+ underlineColor: null,
3688
+ overline: false,
3689
+ strikethrough: false,
3690
+ inverse: false,
3691
+ hidden: false,
3692
+ blink: false
3693
+ });
3694
+ const SERIALIZE_BOOL_CODES = [
3695
+ ["bold", "1"],
3696
+ ["faint", "2"],
3697
+ ["italic", "3"],
3698
+ ["blink", "5"],
3699
+ ["inverse", "7"],
3700
+ ["hidden", "8"],
3701
+ ["strikethrough", "9"],
3702
+ ["overline", "53"]
3703
+ ];
3704
+ /** Colon-subparam underline forms — the lossless spellings vterm itself parses. */
3705
+ function serializeUnderlineParam(style) {
3706
+ switch (style) {
3707
+ case "single": return "4";
3708
+ case "double": return "4:2";
3709
+ case "curly": return "4:3";
3710
+ case "dotted": return "4:4";
3711
+ case "dashed": return "4:5";
3712
+ case "none": return "24";
3713
+ }
3714
+ }
3715
+ function serializeColorEq(a, b) {
3716
+ if (a === null || b === null) return a === b;
3717
+ return a.r === b.r && a.g === b.g && a.b === b.b && a.index === b.index;
3718
+ }
3719
+ function serializePenOf(source) {
3720
+ return {
3721
+ fg: source.fg,
3722
+ bg: source.bg,
3723
+ bold: source.bold,
3724
+ faint: source.faint,
3725
+ italic: source.italic,
3726
+ underline: source.underline,
3727
+ underlineColor: source.underlineColor,
3728
+ overline: source.overline,
3729
+ strikethrough: source.strikethrough,
3730
+ inverse: source.inverse,
3731
+ hidden: source.hidden,
3732
+ blink: source.blink
3733
+ };
3734
+ }
3735
+ function serializePenEq(a, b) {
3736
+ return a.bold === b.bold && a.faint === b.faint && a.italic === b.italic && a.blink === b.blink && a.inverse === b.inverse && a.hidden === b.hidden && a.strikethrough === b.strikethrough && a.overline === b.overline && a.underline === b.underline && serializeColorEq(a.fg, b.fg) && serializeColorEq(a.bg, b.bg) && serializeColorEq(a.underlineColor, b.underlineColor);
3737
+ }
3738
+ function serializePenIsDefault(pen) {
3739
+ return serializePenEq(pen, SERIALIZE_DEFAULT_PEN);
3740
+ }
3741
+ function serializeColorParams(prefix, defaultCode, color) {
3742
+ if (color === null) return defaultCode;
3743
+ const idx = color.index;
3744
+ if (idx !== void 0 && idx >= 0 && idx <= 255) {
3745
+ if (prefix === "38") {
3746
+ if (idx <= 7) return String(30 + idx);
3747
+ if (idx <= 15) return String(90 + (idx - 8));
3748
+ return `38;5;${String(idx)}`;
3749
+ }
3750
+ if (prefix === "48") {
3751
+ if (idx <= 7) return String(40 + idx);
3752
+ if (idx <= 15) return String(100 + (idx - 8));
3753
+ return `48;5;${String(idx)}`;
3754
+ }
3755
+ return `${prefix};5;${String(idx)}`;
3756
+ }
3757
+ return `${prefix};2;${String(color.r)};${String(color.g)};${String(color.b)}`;
3758
+ }
3759
+ /**
3760
+ * Reset-on-removal pen diff (the tmux `grid_string_cells` model): emit `SGR 0`
3761
+ * iff a boolean attribute turns off or the underline changes away from a
3762
+ * non-none style, then re-add deltas from the reset base. This makes the
3763
+ * `SGR 22` bold/faint coupling and the underline-off classes structurally
3764
+ * impossible, at a few bytes' cost. The leading `0` is emitted only on an
3765
+ * actual reset (not whenever the previous pen happens to be default) so
3766
+ * output stays byte-deterministic and idempotence-friendly.
3767
+ */
3768
+ function serializeDiffPen(prev, cur) {
3769
+ let boolOff = false;
3770
+ for (const [key] of SERIALIZE_BOOL_CODES) if (prev[key] === true && cur[key] === false) {
3771
+ boolOff = true;
3772
+ break;
3773
+ }
3774
+ const underlineChanged = prev.underline !== cur.underline && prev.underline !== "none";
3775
+ const reset = boolOff || underlineChanged;
3776
+ const base = reset ? SERIALIZE_DEFAULT_PEN : prev;
3777
+ const out = reset ? ["0"] : [];
3778
+ for (const [key, code] of SERIALIZE_BOOL_CODES) if (cur[key] === true && base[key] === false) out.push(code);
3779
+ if (cur.underline !== base.underline) out.push(serializeUnderlineParam(cur.underline));
3780
+ if (!serializeColorEq(cur.fg, base.fg)) out.push(serializeColorParams("38", "39", cur.fg));
3781
+ if (!serializeColorEq(cur.bg, base.bg)) out.push(serializeColorParams("48", "49", cur.bg));
3782
+ if (!serializeColorEq(cur.underlineColor, base.underlineColor)) out.push(serializeColorParams("58", "59", cur.underlineColor));
3783
+ return out;
3784
+ }
3785
+ /**
3786
+ * Last cell worth emitting: trailing UNWRITTEN cells (`char === ""`, default
3787
+ * pen, no url) are trimmed — the receiver's cleared screen already has them.
3788
+ * Erased-with-bg trailing cells (BCE) are kept and reproduced via ECH.
3789
+ */
3790
+ function serializeRowEnd(row) {
3791
+ let end = row.length;
3792
+ while (end > 0) {
3793
+ const cell = row[end - 1];
3794
+ if (cell === void 0) {
3795
+ end--;
3796
+ continue;
3797
+ }
3798
+ if (cell.char.length > 0 || cell.wide || cell.url !== null) break;
3799
+ if (!serializePenIsDefault(serializePenOf(cell))) break;
3800
+ end--;
3801
+ }
3802
+ return end;
3803
+ }
3804
+ /**
3805
+ * Encode one row with per-run pen diffing. Invariants:
3806
+ * - the terminal pen is DEFAULT at entry (rows end with `SGR 0`);
3807
+ * - wide-cell spacers (char `""` following a `wide` cell) emit nothing;
3808
+ * - runs of unwritten cells are SKIPPED via CHA (the sink's cells stay
3809
+ * genuinely empty — not painted as spaces), and erased-with-bg runs are
3810
+ * reproduced via SGR+ECH, so the sink grid equals the source grid natively;
3811
+ * - an open OSC 8 hyperlink is always closed by row end.
3812
+ *
3813
+ * `literalWidth > 0` switches to LITERAL mode for soft-wrap-linked history
3814
+ * rows: every one of `literalWidth` columns is WRITTEN (unwritten cells as
3815
+ * pen-carrying spaces — no CHA skips, no ECH, no trailing trim), so the last
3816
+ * column write arms the receiver's pending-wrap and the next row's first
3817
+ * character re-links the logical line. Cell-level cost: the receiver's
3818
+ * scrollback holds written spaces where the source had unwritten cells —
3819
+ * invisible to the text + wrap-bit projections that scrollback serves.
3820
+ */
3821
+ function serializeEncodeRow(row, hyperlinks, literalWidth = 0) {
3822
+ const literal = literalWidth > 0;
3823
+ const end = literal ? literalWidth : serializeRowEnd(row);
3824
+ let out = "";
3825
+ let pen = SERIALIZE_DEFAULT_PEN;
3826
+ let openUrl = null;
3827
+ let col = 0;
3828
+ while (col < end) {
3829
+ const cell = row[col];
3830
+ if (cell === void 0) {
3831
+ if (!literal) {
3832
+ col++;
3833
+ continue;
3834
+ }
3835
+ const diff = serializeDiffPen(pen, SERIALIZE_DEFAULT_PEN);
3836
+ if (diff.length > 0) {
3837
+ out += `\x1b[${diff.join(";")}m`;
3838
+ pen = SERIALIZE_DEFAULT_PEN;
3839
+ if (diff[0] === "0") openUrl = null;
3840
+ }
3841
+ if (openUrl !== null) {
3842
+ out += "\x1B]8;;\x1B\\";
3843
+ openUrl = null;
3844
+ }
3845
+ out += " ";
3846
+ col++;
3847
+ continue;
3848
+ }
3849
+ if (cell.char.length === 0 && col > 0 && row[col - 1]?.wide === true) {
3850
+ col++;
3851
+ continue;
3852
+ }
3853
+ if (cell.char.length === 0 && cell.url === null && !literal) {
3854
+ const runPen = serializePenOf(cell);
3855
+ const runStart = col;
3856
+ while (col < end) {
3857
+ const next = row[col];
3858
+ if (next === void 0 || next.char.length > 0 || next.url !== null) break;
3859
+ if (!serializePenEq(serializePenOf(next), runPen)) break;
3860
+ col++;
3861
+ }
3862
+ const runLength = col - runStart;
3863
+ if (serializePenIsDefault(runPen)) out += `\x1b[${String(col + 1)}G`;
3864
+ else {
3865
+ const diff = serializeDiffPen(pen, runPen);
3866
+ if (diff.length > 0) {
3867
+ out += `\x1b[${diff.join(";")}m`;
3868
+ pen = runPen;
3869
+ if (diff[0] === "0") openUrl = null;
3870
+ }
3871
+ out += `\x1b[${String(runLength)}X\x1b[${String(col + 1)}G`;
3872
+ }
3873
+ continue;
3874
+ }
3875
+ const cellPen = serializePenOf(cell);
3876
+ const diff = serializeDiffPen(pen, cellPen);
3877
+ if (diff.length > 0) {
3878
+ out += `\x1b[${diff.join(";")}m`;
3879
+ pen = cellPen;
3880
+ if (diff[0] === "0") openUrl = null;
3881
+ }
3882
+ const url = hyperlinks ? cell.url : null;
3883
+ if (url !== openUrl) {
3884
+ out += url === null ? "\x1B]8;;\x1B\\" : `\x1b]8;;${url}\x1b\\`;
3885
+ openUrl = url;
3886
+ }
3887
+ out += cell.char.length > 0 ? cell.char : " ";
3888
+ col++;
3889
+ }
3890
+ if (openUrl !== null) out += "\x1B]8;;\x1B\\";
3891
+ return out;
3892
+ }
3893
+ /**
3894
+ * Paint-safe DECSET boolean flags emitted in phase 3 — set-forms only, emitted
3895
+ * only when true (the receiver is fresh: every flag defaults to false). Origin
3896
+ * and insert are deliberately NOT here — they corrupt a positioned paint and
3897
+ * are restored in finalize instead.
3898
+ */
3899
+ const SERIALIZE_DECSET_FLAGS = [
3900
+ ["applicationCursor", 1],
3901
+ ["reverseVideo", 5],
3902
+ ["focusTracking", 1004],
3903
+ ["utf8Mouse", 1005],
3904
+ ["sgrMouse", 1006],
3905
+ ["altScroll", 1007],
3906
+ ["bracketedPaste", 2004],
3907
+ ["colorSchemeReporting", 2031]
3908
+ ];
3909
+ /** Default palette shared by every serialize call (read-only baseline for OSC 4 diffs). */
3910
+ const SERIALIZE_DEFAULT_PALETTE = buildPalette256();
3911
+ /** `rgb:RR/GG/BB` — the XParseColor spelling vterm's own OSC parser round-trips exactly. */
3912
+ function serializeColorSpec(color) {
3913
+ const hex = (v) => v.toString(16).padStart(2, "0");
3914
+ return `rgb:${hex(color.r)}/${hex(color.g)}/${hex(color.b)}`;
3915
+ }
3916
+ /** DECSCUSR code for (shape, blinking); 1 (blinking block) is the fresh default. */
3917
+ function serializeCursorStyleCode(shape, blinking) {
3918
+ const base = shape === "block" ? 1 : shape === "underline" ? 3 : 5;
3919
+ return blinking ? base : base + 1;
3920
+ }
3921
+ /** True when `stops` (sorted) is exactly the fresh default — a stop every 8 columns. */
3922
+ function serializeTabStopsAreDefault(stops, cols) {
3923
+ let expected = 8;
3924
+ for (const stop of stops) {
3925
+ if (stop !== expected) return false;
3926
+ expected += 8;
3927
+ }
3928
+ return expected >= cols;
3929
+ }
3930
+ /**
3931
+ * Serialize a snapshot to minimal ANSI a FRESH same-size terminal can replay.
3932
+ * Phase order + the hard exclusion set are documented on the section header
3933
+ * above; every ordering constraint is pinned by the mode round-trip matrix.
3934
+ *
3935
+ * Intended divergences (documented, not silent): syncOutput/decColumn are
3936
+ * never emitted; the inactive buffer, SCP saved cursor, DECSC saved state,
3937
+ * color stack, and parser/pending-wrap/mid-parse state are unserializable to
3938
+ * a VT byte stream by design — the binary snapshot carries them.
3939
+ */
3940
+ function serializeSnapshot(snapshot, options = {}) {
3941
+ const hyperlinks = options.hyperlinks !== false;
3942
+ const excluded = new Set(options.excludeModes ?? []);
3943
+ const rows = snapshot.rows;
3944
+ const m = snapshot.modes;
3945
+ const out = [];
3946
+ if (options.includeScrollback !== false && snapshot.scrollback.length > 0) {
3947
+ const wraps = snapshot.scrollbackSoftWrapped;
3948
+ const last = snapshot.scrollback.length - 1;
3949
+ for (let i = 0; i < snapshot.scrollback.length; i++) {
3950
+ const row = snapshot.scrollback[i];
3951
+ if (wraps?.[i] === true && i < last) out.push(serializeEncodeRow(row, hyperlinks, snapshot.cols), "\x1B[0m");
3952
+ else out.push(serializeEncodeRow(row, hyperlinks), "\x1B[0m\r\n");
3953
+ }
3954
+ if (rows > 1) out.push("\n".repeat(rows - 1));
3955
+ }
3956
+ if (snapshot.activeBuffer === "alt") out.push("\x1B[?1049h");
3957
+ if (snapshot.margins.scrollTop !== 0 || snapshot.margins.scrollBottom !== rows - 1) out.push(`\x1b[${String(snapshot.margins.scrollTop + 1)};${String(snapshot.margins.scrollBottom + 1)}r`);
3958
+ for (const [key, code] of SERIALIZE_DECSET_FLAGS) if (m[key] === true && !excluded.has(key)) out.push(`\x1b[?${String(code)}h`);
3959
+ if (m.mouseTracking && m.mouseTrackingMode > 0 && !excluded.has("mouseTracking")) out.push(`\x1b[?${String(m.mouseTrackingMode)}h`);
3960
+ if (m.applicationKeypad && !excluded.has("applicationKeypad")) out.push("\x1B=");
3961
+ if (!excluded.has("kittyKeyboardFlags") && (m.kittyKeyboardFlags !== 0 || m.kittyKeyboardStack.length > 0)) {
3962
+ const stack = m.kittyKeyboardStack;
3963
+ if (stack.length === 0) out.push(`\x1b[=${String(m.kittyKeyboardFlags)}u`);
3964
+ else {
3965
+ out.push(`\x1b[=${String(stack[0])}u`);
3966
+ for (let i = 1; i < stack.length; i++) out.push(`\x1b[>${String(stack[i])}u`);
3967
+ out.push(`\x1b[>${String(m.kittyKeyboardFlags)}u`);
3968
+ }
3969
+ }
3970
+ const colors = snapshot.colors.current;
3971
+ if (colors.defaultFgColor) out.push(`\x1b]10;${serializeColorSpec(colors.defaultFgColor)}\x1b\\`);
3972
+ if (colors.defaultBgColor) out.push(`\x1b]11;${serializeColorSpec(colors.defaultBgColor)}\x1b\\`);
3973
+ for (let i = 0; i < colors.palette256.length; i++) {
3974
+ const cur = colors.palette256[i];
3975
+ const def = SERIALIZE_DEFAULT_PALETTE[i];
3976
+ if (cur !== void 0 && def !== void 0 && !serializeColorEq(cur, def)) out.push(`\x1b]4;${String(i)};${serializeColorSpec(cur)}\x1b\\`);
3977
+ }
3978
+ if (options.includeTitle === true && snapshot.title.length > 0) out.push(`\x1b]0;${snapshot.title}\x1b\\`);
3979
+ out.push("\x1B[?7l", "\x1B[H\x1B[2J");
3980
+ const grid = snapshot.activeBuffer === "alt" ? snapshot.alt.grid : snapshot.main.grid;
3981
+ for (let r = 0; r < rows; r++) {
3982
+ const line = serializeEncodeRow(grid[r] ?? [], hyperlinks);
3983
+ if (line.length === 0) continue;
3984
+ out.push(`\x1b[${String(r + 1)};1H`, line, "\x1B[0m");
3985
+ }
3986
+ out.push(m.autoWrap ? "\x1B[?7h" : "\x1B[?7l");
3987
+ if (snapshot.cursor.shape !== "block" || !snapshot.cursor.blinking) out.push(`\x1b[${String(serializeCursorStyleCode(snapshot.cursor.shape, snapshot.cursor.blinking))} q`);
3988
+ if (snapshot.unicode.charsetG0) out.push("\x1B(0");
3989
+ if (m.insert && !excluded.has("insert")) out.push("\x1B[4h");
3990
+ if (!serializeTabStopsAreDefault(snapshot.tabStops, snapshot.cols)) {
3991
+ out.push("\x1B[3g");
3992
+ for (const stop of snapshot.tabStops) if (stop < snapshot.cols) out.push(`\x1b[${String(stop + 1)}G\x1bH`);
3993
+ }
3994
+ if (snapshot.margins.leftRight) {
3995
+ out.push("\x1B[?69h");
3996
+ if (snapshot.margins.left !== 0 || snapshot.margins.right !== snapshot.cols - 1) out.push(`\x1b[${String(snapshot.margins.left + 1)};${String(snapshot.margins.right + 1)}s`);
3997
+ }
3998
+ const penParams = serializeDiffPen(SERIALIZE_DEFAULT_PEN, serializePenOf(snapshot.attrs));
3999
+ if (penParams.length > 0) out.push(`\x1b[${penParams.join(";")}m`);
4000
+ if (hyperlinks && snapshot.attrs.url !== null) out.push(`\x1b]8;;${snapshot.attrs.url}\x1b\\`);
4001
+ out.push(snapshot.cursor.visible ? "\x1B[?25h" : "\x1B[?25l");
4002
+ const origin = m.origin && !excluded.has("origin");
4003
+ const inRegion = snapshot.cursor.y >= snapshot.margins.scrollTop && snapshot.cursor.y <= snapshot.margins.scrollBottom;
4004
+ if (origin && inRegion) {
4005
+ out.push("\x1B[?6h");
4006
+ out.push(`\x1b[${String(snapshot.cursor.y - snapshot.margins.scrollTop + 1)};${String(snapshot.cursor.x + 1)}H`);
4007
+ } else {
4008
+ out.push(`\x1b[${String(snapshot.cursor.y + 1)};${String(snapshot.cursor.x + 1)}H`);
4009
+ if (origin) out.push("\x1B[?6h");
4010
+ }
4011
+ return out.join("");
4012
+ }
4013
+ //#endregion
4014
+ //#region src/snapshot-codec.ts
4015
+ const VERSION = 1;
4016
+ const encoder = new TextEncoder();
4017
+ const decoder = new TextDecoder();
4018
+ const F_BOLD = 1;
4019
+ const F_FAINT = 2;
4020
+ const F_ITALIC = 4;
4021
+ const F_OVERLINE = 8;
4022
+ const F_STRIKE = 16;
4023
+ const F_INVERSE = 32;
4024
+ const F_HIDDEN = 64;
4025
+ const F_BLINK = 128;
4026
+ const F_WIDE = 256;
4027
+ const UL_SHIFT = 9;
4028
+ const UL_MASK = 7;
4029
+ const UNDERLINE_STYLES = [
4030
+ "none",
4031
+ "single",
4032
+ "double",
4033
+ "curly",
4034
+ "dotted",
4035
+ "dashed"
4036
+ ];
4037
+ const UNDERLINE_INDEX = {
4038
+ none: 0,
4039
+ single: 1,
4040
+ double: 2,
4041
+ curly: 3,
4042
+ dotted: 4,
4043
+ dashed: 5
4044
+ };
4045
+ function createWriter() {
4046
+ let buf = /* @__PURE__ */ new Uint8Array(1024);
4047
+ let len = 0;
4048
+ function ensure(extra) {
4049
+ if (len + extra <= buf.length) return;
4050
+ let cap = buf.length;
4051
+ while (cap < len + extra) cap *= 2;
4052
+ const next = new Uint8Array(cap);
4053
+ next.set(buf.subarray(0, len));
4054
+ buf = next;
4055
+ }
4056
+ return {
4057
+ u8(byte) {
4058
+ ensure(1);
4059
+ buf[len++] = byte & 255;
4060
+ },
4061
+ varint(value) {
4062
+ ensure(10);
4063
+ let v = value;
4064
+ while (v >= 128) {
4065
+ buf[len++] = v % 128 + 128;
4066
+ v = Math.floor(v / 128);
4067
+ }
4068
+ buf[len++] = v;
4069
+ },
4070
+ bytes(src) {
4071
+ ensure(src.length);
4072
+ buf.set(src, len);
4073
+ len += src.length;
4074
+ },
4075
+ finish() {
4076
+ return buf.slice(0, len);
4077
+ }
4078
+ };
4079
+ }
4080
+ function createReader(bytes) {
4081
+ let pos = 0;
4082
+ function u8() {
4083
+ if (pos >= bytes.length) throw new Error("vterm snapshot codec: unexpected end of input");
4084
+ return bytes[pos++];
4085
+ }
4086
+ return {
4087
+ u8,
4088
+ varint() {
4089
+ let result = 0;
4090
+ let shift = 1;
4091
+ for (;;) {
4092
+ const byte = u8();
4093
+ result += (byte & 127) * shift;
4094
+ if ((byte & 128) === 0) break;
4095
+ shift *= 128;
4096
+ if (shift > 2 ** 53) throw new Error("vterm snapshot codec: varint too large");
4097
+ }
4098
+ return result;
4099
+ },
4100
+ take(n) {
4101
+ if (pos + n > bytes.length) throw new Error("vterm snapshot codec: unexpected end of input");
4102
+ const slice = bytes.subarray(pos, pos + n);
4103
+ pos += n;
4104
+ return slice;
4105
+ }
4106
+ };
4107
+ }
4108
+ function createRunAccumulator() {
4109
+ const runs = [];
4110
+ let hasLast = false;
4111
+ let lastValue = 0;
4112
+ return {
4113
+ runs,
4114
+ push(value) {
4115
+ if (hasLast && value === lastValue) runs[runs.length - 2] += 1;
4116
+ else {
4117
+ runs.push(1, value);
4118
+ lastValue = value;
4119
+ hasLast = true;
4120
+ }
4121
+ }
4122
+ };
4123
+ }
4124
+ function writeRuns(w, runs) {
4125
+ w.varint(runs.length / 2);
4126
+ for (let i = 0; i < runs.length; i += 2) {
4127
+ w.varint(runs[i]);
4128
+ w.varint(runs[i + 1]);
4129
+ }
4130
+ }
4131
+ function readRuns(r) {
4132
+ const runCount = r.varint();
4133
+ const runs = [];
4134
+ for (let i = 0; i < runCount; i++) {
4135
+ const count = r.varint();
4136
+ const value = r.varint();
4137
+ runs.push(count, value);
4138
+ }
4139
+ return runs;
4140
+ }
4141
+ function createRunCursor(runs) {
4142
+ let total = 0;
4143
+ for (let i = 0; i < runs.length; i += 2) total += runs[i];
4144
+ let idx = 0;
4145
+ let remaining = runs.length > 0 ? runs[0] : 0;
4146
+ let value = runs.length > 1 ? runs[1] : 0;
4147
+ return {
4148
+ total,
4149
+ next() {
4150
+ while (remaining === 0) {
4151
+ idx += 2;
4152
+ if (idx >= runs.length) throw new Error("vterm snapshot codec: run-length underflow");
4153
+ remaining = runs[idx];
4154
+ value = runs[idx + 1];
4155
+ }
4156
+ remaining -= 1;
4157
+ return value;
4158
+ }
4159
+ };
4160
+ }
4161
+ function expandLengths(runs) {
4162
+ const out = [];
4163
+ for (let i = 0; i < runs.length; i += 2) {
4164
+ const count = runs[i];
4165
+ const value = runs[i + 1];
4166
+ for (let k = 0; k < count; k++) out.push(value);
4167
+ }
4168
+ return out;
4169
+ }
4170
+ function writeBits(w, bits) {
4171
+ let acc = 0;
4172
+ let n = 0;
4173
+ for (const bit of bits) {
4174
+ if (bit) acc |= 1 << n;
4175
+ n++;
4176
+ if (n === 8) {
4177
+ w.u8(acc);
4178
+ acc = 0;
4179
+ n = 0;
4180
+ }
4181
+ }
4182
+ if (n > 0) w.u8(acc);
4183
+ }
4184
+ function readBits(r, count) {
4185
+ const out = new Array(count);
4186
+ let acc = 0;
4187
+ let have = 0;
4188
+ for (let i = 0; i < count; i++) {
4189
+ if (have === 0) {
4190
+ acc = r.u8();
4191
+ have = 8;
4192
+ }
4193
+ out[i] = (acc & 1) === 1;
4194
+ acc >>= 1;
4195
+ have--;
4196
+ }
4197
+ return out;
4198
+ }
4199
+ function checkByte(name, value) {
4200
+ if (!Number.isInteger(value) || value < 0 || value > 255) throw new Error(`vterm snapshot codec: ${name} out of 0-255 range: ${String(value)}`);
4201
+ }
4202
+ function createInterner() {
4203
+ const stringList = [""];
4204
+ const stringMap = /* @__PURE__ */ new Map([["", 0]]);
4205
+ const colorList = [];
4206
+ const colorMap = /* @__PURE__ */ new Map();
4207
+ const styleList = [];
4208
+ const styleMap = /* @__PURE__ */ new Map();
4209
+ function internString(s) {
4210
+ let idx = stringMap.get(s);
4211
+ if (idx === void 0) {
4212
+ idx = stringList.length;
4213
+ stringList.push(s);
4214
+ stringMap.set(s, idx);
4215
+ }
4216
+ return idx;
4217
+ }
4218
+ /** null → 0; otherwise color-table index + 1. */
4219
+ function colorRef(c) {
4220
+ if (c === null) return 0;
4221
+ checkByte("color.r", c.r);
4222
+ checkByte("color.g", c.g);
4223
+ checkByte("color.b", c.b);
4224
+ if (c.index !== void 0) checkByte("color.index", c.index);
4225
+ const key = c.index === void 0 ? `${c.r},${c.g},${c.b}` : `${c.r},${c.g},${c.b},${c.index}`;
4226
+ let idx = colorMap.get(key);
4227
+ if (idx === void 0) {
4228
+ idx = colorList.length;
4229
+ colorList.push(c.index === void 0 ? {
4230
+ r: c.r,
4231
+ g: c.g,
4232
+ b: c.b
4233
+ } : {
4234
+ r: c.r,
4235
+ g: c.g,
4236
+ b: c.b,
4237
+ index: c.index
4238
+ });
4239
+ colorMap.set(key, idx);
4240
+ }
4241
+ return idx + 1;
4242
+ }
4243
+ function internStyle(cell) {
4244
+ const fgRef = colorRef(cell.fg);
4245
+ const bgRef = colorRef(cell.bg);
4246
+ const ulRef = colorRef(cell.underlineColor);
4247
+ const urlRef = cell.url === null ? 0 : internString(cell.url) + 1;
4248
+ const ulIdx = UNDERLINE_INDEX[cell.underline];
4249
+ if (ulIdx === void 0) throw new Error(`vterm snapshot codec: unknown underline style ${String(cell.underline)}`);
4250
+ let flags = 0;
4251
+ if (cell.bold) flags |= F_BOLD;
4252
+ if (cell.faint) flags |= F_FAINT;
4253
+ if (cell.italic) flags |= F_ITALIC;
4254
+ if (cell.overline) flags |= F_OVERLINE;
4255
+ if (cell.strikethrough) flags |= F_STRIKE;
4256
+ if (cell.inverse) flags |= F_INVERSE;
4257
+ if (cell.hidden) flags |= F_HIDDEN;
4258
+ if (cell.blink) flags |= F_BLINK;
4259
+ if (cell.wide) flags |= F_WIDE;
4260
+ flags |= ulIdx << UL_SHIFT;
4261
+ const key = `${flags}|${fgRef}|${bgRef}|${ulRef}|${urlRef}`;
4262
+ let idx = styleMap.get(key);
4263
+ if (idx === void 0) {
4264
+ idx = styleList.length;
4265
+ styleList.push({
4266
+ flags,
4267
+ fgRef,
4268
+ bgRef,
4269
+ ulRef,
4270
+ urlRef
4271
+ });
4272
+ styleMap.set(key, idx);
4273
+ }
4274
+ return idx;
4275
+ }
4276
+ return {
4277
+ stringList,
4278
+ colorList,
4279
+ styleList,
4280
+ internString,
4281
+ internStyle
4282
+ };
4283
+ }
4284
+ function buildGridColumns(grid, interner) {
4285
+ const charAcc = createRunAccumulator();
4286
+ const styleAcc = createRunAccumulator();
4287
+ const lenAcc = createRunAccumulator();
4288
+ for (const row of grid) {
4289
+ lenAcc.push(row.length);
4290
+ for (const cell of row) {
4291
+ charAcc.push(interner.internString(cell.char));
4292
+ styleAcc.push(interner.internStyle(cell));
4293
+ }
4294
+ }
4295
+ return {
4296
+ rowCount: grid.length,
4297
+ lenRuns: lenAcc.runs,
4298
+ charRuns: charAcc.runs,
4299
+ styleRuns: styleAcc.runs
4300
+ };
4301
+ }
4302
+ function writeGridSection(w, columns, softWrapped, softWrapNullable) {
4303
+ w.varint(columns.rowCount);
4304
+ if (softWrapNullable && softWrapped === void 0) w.u8(0);
4305
+ else {
4306
+ if (softWrapNullable) w.u8(1);
4307
+ if (softWrapped === void 0) throw new Error("vterm snapshot codec: missing soft-wrap array for a required grid");
4308
+ if (softWrapped.length !== columns.rowCount) throw new Error(`vterm snapshot codec: soft-wrap length ${softWrapped.length} != rowCount ${columns.rowCount}`);
4309
+ writeBits(w, softWrapped);
4310
+ }
4311
+ writeRuns(w, columns.lenRuns);
4312
+ writeRuns(w, columns.charRuns);
4313
+ writeRuns(w, columns.styleRuns);
4314
+ }
4315
+ function writeStringTable(w, list) {
4316
+ w.varint(list.length);
4317
+ for (const s of list) {
4318
+ const b = encoder.encode(s);
4319
+ w.varint(b.length);
4320
+ w.bytes(b);
4321
+ }
4322
+ }
4323
+ function writeColorTable(w, list) {
4324
+ w.varint(list.length);
4325
+ for (const c of list) {
4326
+ w.u8(c.r);
4327
+ w.u8(c.g);
4328
+ w.u8(c.b);
4329
+ w.varint(c.index === void 0 ? 0 : c.index + 1);
4330
+ }
4331
+ }
4332
+ function writeStyleTable(w, list) {
4333
+ w.varint(list.length);
4334
+ for (const st of list) {
4335
+ w.varint(st.flags);
4336
+ w.varint(st.fgRef);
4337
+ w.varint(st.bgRef);
4338
+ w.varint(st.ulRef);
4339
+ w.varint(st.urlRef);
4340
+ }
4341
+ }
4342
+ function readStringTable(r) {
4343
+ const count = r.varint();
4344
+ const list = new Array(count);
4345
+ for (let i = 0; i < count; i++) {
4346
+ const len = r.varint();
4347
+ list[i] = decoder.decode(r.take(len));
4348
+ }
4349
+ return list;
4350
+ }
4351
+ function readColorTable(r) {
4352
+ const count = r.varint();
4353
+ const list = new Array(count);
4354
+ for (let i = 0; i < count; i++) {
4355
+ const cr = r.u8();
4356
+ const cg = r.u8();
4357
+ const cb = r.u8();
4358
+ const idxPlus = r.varint();
4359
+ list[i] = idxPlus === 0 ? {
4360
+ r: cr,
4361
+ g: cg,
4362
+ b: cb
4363
+ } : {
4364
+ r: cr,
4365
+ g: cg,
4366
+ b: cb,
4367
+ index: idxPlus - 1
4368
+ };
4369
+ }
4370
+ return list;
4371
+ }
4372
+ function readStyleTable(r) {
4373
+ const count = r.varint();
4374
+ const list = new Array(count);
4375
+ for (let i = 0; i < count; i++) list[i] = {
4376
+ flags: r.varint(),
4377
+ fgRef: r.varint(),
4378
+ bgRef: r.varint(),
4379
+ ulRef: r.varint(),
4380
+ urlRef: r.varint()
4381
+ };
4382
+ return list;
4383
+ }
4384
+ function colorFromRef(ref, table) {
4385
+ if (ref === 0) return null;
4386
+ const c = table[ref - 1];
4387
+ if (c === void 0) throw new Error(`vterm snapshot codec: color ref ${ref} out of range (table size ${table.length})`);
4388
+ return c.index === void 0 ? {
4389
+ r: c.r,
4390
+ g: c.g,
4391
+ b: c.b
4392
+ } : {
4393
+ r: c.r,
4394
+ g: c.g,
4395
+ b: c.b,
4396
+ index: c.index
4397
+ };
4398
+ }
4399
+ function makeCell(charIdx, styleIdx, strings, colors, styles) {
4400
+ const char = strings[charIdx];
4401
+ if (char === void 0) throw new Error(`vterm snapshot codec: char ref ${charIdx} out of range (table size ${strings.length})`);
4402
+ const style = styles[styleIdx];
4403
+ if (style === void 0) throw new Error(`vterm snapshot codec: style ref ${styleIdx} out of range (table size ${styles.length})`);
4404
+ const underline = UNDERLINE_STYLES[style.flags >> UL_SHIFT & UL_MASK];
4405
+ if (underline === void 0) throw new Error(`vterm snapshot codec: invalid underline bits in style flags ${style.flags}`);
4406
+ let url;
4407
+ if (style.urlRef === 0) url = null;
4408
+ else {
4409
+ const u = strings[style.urlRef - 1];
4410
+ if (u === void 0) throw new Error(`vterm snapshot codec: url ref ${style.urlRef} out of range (table size ${strings.length})`);
4411
+ url = u;
4412
+ }
4413
+ return {
4414
+ char,
4415
+ fg: colorFromRef(style.fgRef, colors),
4416
+ bg: colorFromRef(style.bgRef, colors),
4417
+ bold: (style.flags & F_BOLD) !== 0,
4418
+ faint: (style.flags & F_FAINT) !== 0,
4419
+ italic: (style.flags & F_ITALIC) !== 0,
4420
+ underline,
4421
+ underlineColor: colorFromRef(style.ulRef, colors),
4422
+ overline: (style.flags & F_OVERLINE) !== 0,
4423
+ strikethrough: (style.flags & F_STRIKE) !== 0,
4424
+ inverse: (style.flags & F_INVERSE) !== 0,
4425
+ hidden: (style.flags & F_HIDDEN) !== 0,
4426
+ blink: (style.flags & F_BLINK) !== 0,
4427
+ wide: (style.flags & F_WIDE) !== 0,
4428
+ url
4429
+ };
4430
+ }
4431
+ function readGridSection(r, softWrapNullable, expected, strings, colors, styles) {
4432
+ const rowCount = r.varint();
4433
+ let softWrapped;
4434
+ if (softWrapNullable) {
4435
+ const present = r.u8();
4436
+ if (present === 0) softWrapped = void 0;
4437
+ else if (present === 1) softWrapped = readBits(r, rowCount);
4438
+ else throw new Error(`vterm snapshot codec: invalid soft-wrap presence byte ${present}`);
4439
+ } else softWrapped = readBits(r, rowCount);
4440
+ const rowLengths = expandLengths(readRuns(r));
4441
+ if (rowLengths.length !== rowCount) throw new Error(`vterm snapshot codec: decoded ${rowLengths.length} row lengths but rowCount is ${rowCount}`);
4442
+ if (expected) {
4443
+ if (rowCount !== expected.rows) throw new Error(`vterm snapshot codec: grid rowCount ${rowCount} != expected ${expected.rows}`);
4444
+ for (const len of rowLengths) if (len !== expected.cols) throw new Error(`vterm snapshot codec: grid row length ${len} != expected ${expected.cols}`);
4445
+ }
4446
+ let totalCells = 0;
4447
+ for (const len of rowLengths) totalCells += len;
4448
+ const charCursor = createRunCursor(readRuns(r));
4449
+ const styleCursor = createRunCursor(readRuns(r));
4450
+ if (charCursor.total !== totalCells) throw new Error(`vterm snapshot codec: char column has ${charCursor.total} cells, grid needs ${totalCells}`);
4451
+ if (styleCursor.total !== totalCells) throw new Error(`vterm snapshot codec: style column has ${styleCursor.total} cells, grid needs ${totalCells}`);
4452
+ const grid = new Array(rowCount);
4453
+ for (let row = 0; row < rowCount; row++) {
4454
+ const len = rowLengths[row];
4455
+ const cells = new Array(len);
4456
+ for (let col = 0; col < len; col++) cells[col] = makeCell(charCursor.next(), styleCursor.next(), strings, colors, styles);
4457
+ grid[row] = cells;
4458
+ }
4459
+ return {
4460
+ grid,
4461
+ softWrapped
4462
+ };
4463
+ }
4464
+ /**
4465
+ * Encode a {@link ScreenSnapshot} to a compact binary buffer. Output is
4466
+ * deterministic for identical input and decodes back to a deep-equal snapshot.
4467
+ */
4468
+ function encodeScreenSnapshotBinary(snapshot) {
4469
+ const w = createWriter();
4470
+ w.u8(VERSION);
4471
+ const header = {
4472
+ cols: snapshot.cols,
4473
+ rows: snapshot.rows,
4474
+ scrollbackLimit: snapshot.scrollbackLimit,
4475
+ activeBuffer: snapshot.activeBuffer,
4476
+ cursor: snapshot.cursor,
4477
+ savedState: snapshot.savedState,
4478
+ attrs: snapshot.attrs,
4479
+ modes: snapshot.modes,
4480
+ margins: snapshot.margins,
4481
+ colors: snapshot.colors,
4482
+ tabStops: snapshot.tabStops,
4483
+ title: snapshot.title,
4484
+ clipboard: snapshot.clipboard,
4485
+ cwd: snapshot.cwd,
4486
+ notifications: snapshot.notifications,
4487
+ viewportOffset: snapshot.viewportOffset,
4488
+ parser: snapshot.parser,
4489
+ unicode: snapshot.unicode
4490
+ };
4491
+ const headerBytes = encoder.encode(JSON.stringify(header));
4492
+ w.varint(headerBytes.length);
4493
+ w.bytes(headerBytes);
4494
+ const interner = createInterner();
4495
+ const mainColumns = buildGridColumns(snapshot.main.grid, interner);
4496
+ const altColumns = buildGridColumns(snapshot.alt.grid, interner);
4497
+ const scrollbackColumns = buildGridColumns(snapshot.scrollback, interner);
4498
+ writeStringTable(w, interner.stringList);
4499
+ writeColorTable(w, interner.colorList);
4500
+ writeStyleTable(w, interner.styleList);
4501
+ writeGridSection(w, mainColumns, snapshot.main.softWrapped, false);
4502
+ writeGridSection(w, altColumns, snapshot.alt.softWrapped, false);
4503
+ writeGridSection(w, scrollbackColumns, snapshot.scrollbackSoftWrapped, true);
4504
+ return w.finish();
4505
+ }
4506
+ /**
4507
+ * Decode a buffer produced by {@link encodeScreenSnapshotBinary} back into a
4508
+ * {@link ScreenSnapshot}. Throws loudly on an unknown version byte, truncated
4509
+ * input, or any internal inconsistency — never returns a partial/normalized result.
4510
+ */
4511
+ function decodeScreenSnapshotBinary(bytes) {
4512
+ const r = createReader(bytes);
4513
+ const version = r.u8();
4514
+ if (version !== VERSION) throw new Error(`vterm snapshot codec: unsupported version byte ${version} (expected ${VERSION})`);
4515
+ const headerLen = r.varint();
4516
+ const header = JSON.parse(decoder.decode(r.take(headerLen)));
4517
+ const strings = readStringTable(r);
4518
+ const colors = readColorTable(r);
4519
+ const styles = readStyleTable(r);
4520
+ const geom = {
4521
+ rows: header.rows,
4522
+ cols: header.cols
4523
+ };
4524
+ const main = readGridSection(r, false, geom, strings, colors, styles);
4525
+ const alt = readGridSection(r, false, geom, strings, colors, styles);
4526
+ const scrollback = readGridSection(r, true, null, strings, colors, styles);
4527
+ return {
4528
+ version: 1,
4529
+ cols: header.cols,
4530
+ rows: header.rows,
4531
+ scrollbackLimit: header.scrollbackLimit,
4532
+ activeBuffer: header.activeBuffer,
4533
+ main: {
4534
+ grid: main.grid,
4535
+ softWrapped: main.softWrapped
4536
+ },
4537
+ alt: {
4538
+ grid: alt.grid,
4539
+ softWrapped: alt.softWrapped
4540
+ },
4541
+ scrollback: scrollback.grid,
4542
+ scrollbackSoftWrapped: scrollback.softWrapped,
4543
+ cursor: header.cursor,
4544
+ savedState: header.savedState,
4545
+ attrs: header.attrs,
4546
+ modes: header.modes,
4547
+ margins: header.margins,
4548
+ colors: header.colors,
4549
+ tabStops: header.tabStops,
4550
+ title: header.title,
4551
+ clipboard: header.clipboard,
4552
+ cwd: header.cwd,
4553
+ notifications: header.notifications,
4554
+ viewportOffset: header.viewportOffset,
4555
+ parser: header.parser,
4556
+ unicode: header.unicode
2377
4557
  };
2378
4558
  }
2379
4559
  //#endregion
2380
- export { createScreen as createVtermScreen };
4560
+ export { createScreen as createVtermScreen, decodeScreenSnapshotBinary, encodeScreenSnapshotBinary, serializeSnapshot };