vterm.js 0.5.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);
@@ -303,6 +545,192 @@ function createScreen(options = {}) {
303
545
  highlightBgColor = cloneColor(snapshot.highlightBgColor);
304
546
  highlightFgColor = cloneColor(snapshot.highlightFgColor);
305
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
+ }
306
734
  let tabStops = defaultTabStops(cols);
307
735
  function defaultTabStops(c) {
308
736
  const s = /* @__PURE__ */ new Set();
@@ -337,6 +765,7 @@ function createScreen(options = {}) {
337
765
  let mainSoftWrapped = new Array(rows).fill(false);
338
766
  let altSoftWrapped = new Array(rows).fill(false);
339
767
  let softWrapped = mainSoftWrapped;
768
+ let scrollbackSoftWrapped = [];
340
769
  let lastChar = "";
341
770
  let pendingRegionalIndicator = null;
342
771
  let afterZWJ = false;
@@ -347,16 +776,122 @@ function createScreen(options = {}) {
347
776
  let dcsStartRow = 0;
348
777
  let dcsStartCol = 0;
349
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
+ }
350
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
+ }
351
876
  function makeGrid(c, r) {
352
877
  const g = [];
353
878
  for (let row = 0; row < r; row++) g.push(makeRow(c));
354
879
  return g;
355
880
  }
356
881
  function makeRow(c) {
357
- const row = [];
358
- for (let col = 0; col < c; col++) row.push(EMPTY_CELL);
359
- 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);
360
895
  }
361
896
  function resetAttrs() {
362
897
  return {
@@ -388,14 +923,35 @@ function createScreen(options = {}) {
388
923
  for (let r = top; r < bottom; r++) {
389
924
  const srcRow = grid[r + 1];
390
925
  const dstRow = grid[r];
391
- 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);
392
927
  }
393
928
  const bottomRow = grid[bottom];
394
- 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);
395
931
  } else {
396
- if (grid === mainGrid && top === 0) {
932
+ const enteredScrollback = grid === mainGrid && top === 0;
933
+ if (enteredScrollback) {
397
934
  scrollback.push(grid[0]);
398
- 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
+ }
399
955
  }
400
956
  for (let i = top; i < bottom; i++) {
401
957
  grid[i] = grid[i + 1];
@@ -403,6 +959,11 @@ function createScreen(options = {}) {
403
959
  }
404
960
  grid[bottom] = makeRow(cols);
405
961
  softWrapped[bottom] = false;
962
+ if (enteredScrollback) {
963
+ dirtyScrolled++;
964
+ if (sigDamageActive) sigDirtyScrolled++;
965
+ markScreenRowDirty(bottom);
966
+ } else markScreenRowsDirty(top, bottom);
406
967
  }
407
968
  }
408
969
  function scrollDown(top, bottom) {
@@ -412,10 +973,10 @@ function createScreen(options = {}) {
412
973
  for (let r = bottom; r > top; r--) {
413
974
  const srcRow = grid[r - 1];
414
975
  const dstRow = grid[r];
415
- 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);
416
977
  }
417
978
  const topRow = grid[top];
418
- 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);
419
980
  } else {
420
981
  for (let i = bottom; i > top; i--) {
421
982
  grid[i] = grid[i - 1];
@@ -424,11 +985,17 @@ function createScreen(options = {}) {
424
985
  grid[top] = makeRow(cols);
425
986
  softWrapped[top] = false;
426
987
  }
988
+ markScreenRowsDirty(top, bottom);
427
989
  }
428
990
  function scrollViewport(delta) {
429
991
  viewportOffset = Math.max(0, Math.min(scrollback.length, viewportOffset + delta));
430
992
  }
431
- /** Find the previous non-spacer cell (the cell before curX, skipping wide-char spacers) */
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
+ */
432
999
  function getPrevCell() {
433
1000
  if (curX === 0 && curY === 0) return null;
434
1001
  let prevCol = curX - 1;
@@ -439,42 +1006,17 @@ function createScreen(options = {}) {
439
1006
  prevCol = cols - 1;
440
1007
  }
441
1008
  const row = grid[prevRow];
442
- let cell = row[prevCol];
443
- if (cell !== EMPTY_CELL && cell.char === "" && prevCol > 0) {
444
- prevCol--;
445
- cell = row[prevCol];
446
- }
447
- 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;
448
1011
  return {
449
- cell,
450
- col: prevCol
1012
+ row,
1013
+ col: prevCol,
1014
+ rowIdx: prevRow
451
1015
  };
452
1016
  }
453
- /** Widen a cell to 2 columns, adding a spacer cell after it */
454
- function widenCell(row, col, cell) {
455
- cell.wide = true;
456
- if (col + 1 < cols) {
457
- let spacer = row[col + 1];
458
- if (spacer === EMPTY_CELL) {
459
- spacer = { ...EMPTY_CELL };
460
- row[col + 1] = spacer;
461
- }
462
- spacer.char = "";
463
- spacer.fg = null;
464
- spacer.bg = null;
465
- spacer.bold = false;
466
- spacer.faint = false;
467
- spacer.italic = false;
468
- spacer.underline = "none";
469
- spacer.underlineColor = null;
470
- spacer.overline = false;
471
- spacer.strikethrough = false;
472
- spacer.inverse = false;
473
- spacer.hidden = false;
474
- spacer.blink = false;
475
- spacer.wide = false;
476
- spacer.url = null;
477
- }
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);
478
1020
  }
479
1021
  function writeChar(ch) {
480
1022
  if (charsetG0 && ch.length === 1) {
@@ -484,10 +1026,10 @@ function createScreen(options = {}) {
484
1026
  const codePoint = ch.codePointAt(0) ?? 0;
485
1027
  if (isVS16(codePoint)) {
486
1028
  const prev = getPrevCell();
487
- if (prev && !prev.cell.wide) {
488
- prev.cell.char += ch;
489
- const row = grid[curY === 0 && curX === 0 ? 0 : curY];
490
- 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);
491
1033
  curX = prev.col + 2;
492
1034
  if (curX >= cols) curX = cols - 1;
493
1035
  }
@@ -495,18 +1037,25 @@ function createScreen(options = {}) {
495
1037
  }
496
1038
  if (isCombining(codePoint) && !isVS16(codePoint)) {
497
1039
  const prev = getPrevCell();
498
- if (prev) prev.cell.char += ch;
1040
+ if (prev) {
1041
+ prev.row.appendChar(prev.col, ch);
1042
+ markScreenRowDirty(prev.rowIdx);
1043
+ }
499
1044
  return;
500
1045
  }
501
1046
  if (isEmojiModifier(codePoint)) {
502
1047
  const prev = getPrevCell();
503
- if (prev) prev.cell.char += ch;
1048
+ if (prev) {
1049
+ prev.row.appendChar(prev.col, ch);
1050
+ markScreenRowDirty(prev.rowIdx);
1051
+ }
504
1052
  return;
505
1053
  }
506
1054
  if (isZWJ(codePoint)) {
507
1055
  const prev = getPrevCell();
508
1056
  if (prev) {
509
- prev.cell.char += ch;
1057
+ prev.row.appendChar(prev.col, ch);
1058
+ markScreenRowDirty(prev.rowIdx);
510
1059
  afterZWJ = true;
511
1060
  }
512
1061
  return;
@@ -515,7 +1064,8 @@ function createScreen(options = {}) {
515
1064
  afterZWJ = false;
516
1065
  const prev = getPrevCell();
517
1066
  if (prev) {
518
- prev.cell.char += ch;
1067
+ prev.row.appendChar(prev.col, ch);
1068
+ markScreenRowDirty(prev.rowIdx);
519
1069
  return;
520
1070
  }
521
1071
  }
@@ -551,35 +1101,18 @@ function createScreen(options = {}) {
551
1101
  }
552
1102
  } else curX = wrapBoundary - charWidth;
553
1103
  if (insertMode) {
554
- const row = grid[curY];
555
1104
  const insertEnd = leftRightMarginMode ? rightMargin + 1 : cols;
556
- for (let i = 0; i < charWidth; i++) {
557
- row.splice(insertEnd - 1, 1);
558
- row.splice(curX, 0, EMPTY_CELL);
559
- }
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
+ });
560
1111
  }
561
1112
  const row = grid[curY];
562
- let cell = row[curX];
563
- if (cell === EMPTY_CELL) {
564
- cell = { ...EMPTY_CELL };
565
- row[curX] = cell;
566
- }
567
- cell.char = ch;
568
- cell.fg = attrs.fg ? { ...attrs.fg } : null;
569
- cell.bg = attrs.bg ? { ...attrs.bg } : null;
570
- cell.bold = attrs.bold;
571
- cell.faint = attrs.faint;
572
- cell.italic = attrs.italic;
573
- cell.underline = attrs.underline;
574
- cell.underlineColor = attrs.underlineColor ? { ...attrs.underlineColor } : null;
575
- cell.overline = attrs.overline;
576
- cell.strikethrough = attrs.strikethrough;
577
- cell.inverse = attrs.inverse;
578
- cell.hidden = attrs.hidden;
579
- cell.blink = attrs.blink;
580
- cell.wide = wide;
581
- cell.url = attrs.url;
582
- if (wide) widenCell(row, curX, cell);
1113
+ row.writeFromAttrs(curX, ch, attrs, wide);
1114
+ if (wide) widenCell(row, curX);
1115
+ markScreenRowDirty(curY);
583
1116
  curX += charWidth;
584
1117
  lastChar = ch;
585
1118
  }
@@ -634,14 +1167,23 @@ function createScreen(options = {}) {
634
1167
  const charCode = parts[0] ?? 32;
635
1168
  if (charCode < 32 || charCode > 126) return;
636
1169
  const { t, l, b, r } = normalizeRect(parts[1] ?? 1, parts[2] ?? 1, parts[3] ?? rows, parts[4] ?? cols);
637
- for (let row = t; row <= b && row < rows; row++) for (let col = l; col <= r && col < cols; col++) {
638
- const cell = emptyCell();
639
- cell.char = String.fromCharCode(charCode);
640
- 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
+ }
641
1178
  }
1179
+ markScreenRowsDirty(t, Math.min(b, rows - 1));
642
1180
  } else if (finalByte === "z" || finalByte === "{") {
643
1181
  const { t, l, b, r } = normalizeRect(parts[0] ?? 1, parts[1] ?? 1, parts[2] ?? rows, parts[3] ?? cols);
644
- 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));
645
1187
  } else if (finalByte === "v") {
646
1188
  const src = normalizeRect(parts[0] ?? 1, parts[1] ?? 1, parts[2] ?? rows, parts[3] ?? cols);
647
1189
  const dstTop = Math.max(1, parts[5] ?? 1) - 1;
@@ -650,34 +1192,35 @@ function createScreen(options = {}) {
650
1192
  const w = src.r - src.l + 1;
651
1193
  const snapshot = [];
652
1194
  for (let row = 0; row < h; row++) {
1195
+ const srcRow = grid[src.t + row];
653
1196
  const line = [];
654
- for (let col = 0; col < w; col++) {
655
- const srcCell = grid[src.t + row]?.[src.l + col] ?? EMPTY_CELL;
656
- line.push(srcCell === EMPTY_CELL ? EMPTY_CELL : { ...srcCell });
657
- }
1197
+ for (let col = 0; col < w; col++) line.push(srcRow ? srcRow.getCellRaw(src.l + col) : emptyCell());
658
1198
  snapshot.push(line);
659
1199
  }
660
1200
  for (let row = 0; row < h; row++) {
661
1201
  const dr = dstTop + row;
662
1202
  if (dr < 0 || dr >= rows) continue;
1203
+ const dstRow = grid[dr];
663
1204
  for (let col = 0; col < w; col++) {
664
1205
  const dc = dstLeft + col;
665
1206
  if (dc < 0 || dc >= cols) continue;
666
- grid[dr][dc] = snapshot[row][col];
1207
+ dstRow.setCellRaw(dc, snapshot[row][col]);
667
1208
  }
668
1209
  }
1210
+ markScreenRowsDirty(Math.max(0, dstTop), Math.min(dstTop + h - 1, rows - 1));
669
1211
  } else if (finalByte === "r" || finalByte === "t") {
670
1212
  const { t, l, b, r } = normalizeRect(parts[0] ?? 1, parts[1] ?? 1, parts[2] ?? rows, parts[3] ?? cols);
671
1213
  const sgrParts = parts.slice(4);
672
1214
  const reverse = finalByte === "t";
673
- for (let row = t; row <= b && row < rows; row++) for (let col = l; col <= r && col < cols; col++) {
674
- let cell = grid[row][col];
675
- if (cell === EMPTY_CELL) {
676
- cell = { ...EMPTY_CELL };
677
- 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);
678
1221
  }
679
- applyRectAttrs(cell, sgrParts, reverse);
680
1222
  }
1223
+ markScreenRowsDirty(t, Math.min(b, rows - 1));
681
1224
  }
682
1225
  return;
683
1226
  }
@@ -693,11 +1236,15 @@ function createScreen(options = {}) {
693
1236
  const b = Math.min(bArg, rows) - 1;
694
1237
  const r = Math.min(rArg, cols) - 1;
695
1238
  let sum = 0;
696
- for (let row = t; row <= b && row < rows; row++) for (let col = l; col <= r && col < cols; col++) {
697
- const cell = grid[row]?.[col];
698
- if (cell?.char) {
699
- const cp = cell.char.codePointAt(0) ?? 0;
700
- 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
+ }
701
1248
  }
702
1249
  }
703
1250
  const hex = sum.toString(16).toUpperCase().padStart(4, "0");
@@ -916,6 +1463,13 @@ function createScreen(options = {}) {
916
1463
  if (onResponse) {
917
1464
  if (params === "" || params === "0") onResponse("\x1BP!|00000000\x1B\\");
918
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;
919
1473
  }
920
1474
  }
921
1475
  function handleCSIPrivate(params, intermediates, finalByte) {
@@ -1037,10 +1591,12 @@ function createScreen(options = {}) {
1037
1591
  useAltScreen = true;
1038
1592
  grid = altGrid;
1039
1593
  softWrapped = altSoftWrapped;
1594
+ markAllDirty();
1040
1595
  } else if (!set && useAltScreen) {
1041
1596
  useAltScreen = false;
1042
1597
  grid = mainGrid;
1043
1598
  softWrapped = mainSoftWrapped;
1599
+ markAllDirty();
1044
1600
  }
1045
1601
  break;
1046
1602
  case 66:
@@ -1072,6 +1628,7 @@ function createScreen(options = {}) {
1072
1628
  softWrapped = altSoftWrapped;
1073
1629
  curX = 0;
1074
1630
  curY = 0;
1631
+ markAllDirty();
1075
1632
  } else if (!set && useAltScreen) {
1076
1633
  useAltScreen = false;
1077
1634
  grid = mainGrid;
@@ -1079,6 +1636,7 @@ function createScreen(options = {}) {
1079
1636
  curX = savedCurX;
1080
1637
  curY = savedCurY;
1081
1638
  clampCursor();
1639
+ markAllDirty();
1082
1640
  }
1083
1641
  break;
1084
1642
  case 69:
@@ -1135,6 +1693,7 @@ function createScreen(options = {}) {
1135
1693
  case 3:
1136
1694
  for (let row = 0; row < rows; row++) eraseCells(row, 0, row, cols - 1);
1137
1695
  if (mode === 3) scrollback.length = 0;
1696
+ markAllDirty();
1138
1697
  break;
1139
1698
  }
1140
1699
  }
@@ -1156,11 +1715,8 @@ function createScreen(options = {}) {
1156
1715
  function eraseCells(row, startCol, _endRow, endCol) {
1157
1716
  const r = grid[row];
1158
1717
  if (!r) return;
1159
- for (let col = startCol; col <= endCol && col < cols; col++) {
1160
- const cell = emptyCell();
1161
- if (attrs.bg) cell.bg = { ...attrs.bg };
1162
- r[col] = cell;
1163
- }
1718
+ for (let col = startCol; col <= endCol && col < cols; col++) r.eraseWithBg(col, attrs.bg);
1719
+ markScreenRowDirty(row);
1164
1720
  }
1165
1721
  function handleInsertLines(count) {
1166
1722
  if (curY < scrollTop || curY > scrollBottom) return;
@@ -1173,32 +1729,36 @@ function createScreen(options = {}) {
1173
1729
  function handleDeleteChars(count) {
1174
1730
  const row = grid[curY];
1175
1731
  if (!row) return;
1176
- if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
1177
- for (let i = 0; i < count; i++) if (curX <= rightMargin) {
1178
- row.splice(curX, 1);
1179
- 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());
1180
1741
  }
1181
- } else for (let i = 0; i < count; i++) if (curX < cols) {
1182
- row.splice(curX, 1);
1183
- row.push(emptyCell());
1184
- }
1742
+ });
1743
+ markScreenRowDirty(curY);
1185
1744
  }
1186
1745
  function handleInsertChars(count) {
1187
1746
  const row = grid[curY];
1188
1747
  if (!row) return;
1189
- if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) for (let i = 0; i < count; i++) {
1190
- row.splice(rightMargin, 1);
1191
- row.splice(curX, 0, emptyCell());
1192
- }
1193
- else for (let i = 0; i < count; i++) {
1194
- row.splice(curX, 0, emptyCell());
1195
- row.pop();
1196
- }
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);
1197
1759
  }
1198
1760
  function handleEraseChars(count) {
1199
- const row = grid[curY];
1200
- if (!row) return;
1201
- 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));
1202
1762
  }
1203
1763
  /** Bounds of the active scroll/margin rectangle (inclusive). */
1204
1764
  function activeRect() {
@@ -1216,9 +1776,11 @@ function createScreen(options = {}) {
1216
1776
  if (!r) continue;
1217
1777
  for (let col = left; col <= right; col++) {
1218
1778
  const src = col + count;
1219
- r[col] = src <= right ? r[src] : emptyCell();
1779
+ if (src <= right) r.copyCellFrom(col, r, src);
1780
+ else r.setEmpty(col);
1220
1781
  }
1221
1782
  }
1783
+ markScreenRowsDirty(top, bottom);
1222
1784
  }
1223
1785
  function handleShiftRight(count) {
1224
1786
  const { top, left, bottom, right } = activeRect();
@@ -1227,9 +1789,11 @@ function createScreen(options = {}) {
1227
1789
  if (!r) continue;
1228
1790
  for (let col = right; col >= left; col--) {
1229
1791
  const src = col - count;
1230
- r[col] = src >= left ? r[src] : emptyCell();
1792
+ if (src >= left) r.copyCellFrom(col, r, src);
1793
+ else r.setEmpty(col);
1231
1794
  }
1232
1795
  }
1796
+ markScreenRowsDirty(top, bottom);
1233
1797
  }
1234
1798
  function handleInsertColumn(count) {
1235
1799
  const { top, left, bottom, right } = activeRect();
@@ -1237,9 +1801,10 @@ function createScreen(options = {}) {
1237
1801
  for (let row = top; row <= bottom; row++) {
1238
1802
  const r = grid[row];
1239
1803
  if (!r) continue;
1240
- for (let col = right; col >= curX + count; col--) r[col] = r[col - count];
1241
- 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);
1242
1806
  }
1807
+ markScreenRowsDirty(top, bottom);
1243
1808
  }
1244
1809
  function handleDeleteColumn(count) {
1245
1810
  const { top, left, bottom, right } = activeRect();
@@ -1247,9 +1812,10 @@ function createScreen(options = {}) {
1247
1812
  for (let row = top; row <= bottom; row++) {
1248
1813
  const r = grid[row];
1249
1814
  if (!r) continue;
1250
- for (let col = curX; col + count <= right; col++) r[col] = r[col + count];
1251
- 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);
1252
1817
  }
1818
+ markScreenRowsDirty(top, bottom);
1253
1819
  }
1254
1820
  /**
1255
1821
  * Apply (or reverse-toggle) a list of SGR attribute codes to a single cell.
@@ -1403,7 +1969,10 @@ function createScreen(options = {}) {
1403
1969
  case 35:
1404
1970
  case 36:
1405
1971
  case 37:
1406
- attrs.fg = { ...palette256[code - 30] };
1972
+ attrs.fg = {
1973
+ ...palette256[code - 30],
1974
+ index: code - 30
1975
+ };
1407
1976
  break;
1408
1977
  case 38: {
1409
1978
  const subs = subParams.get(i);
@@ -1430,7 +1999,10 @@ function createScreen(options = {}) {
1430
1999
  case 45:
1431
2000
  case 46:
1432
2001
  case 47:
1433
- attrs.bg = { ...palette256[code - 40] };
2002
+ attrs.bg = {
2003
+ ...palette256[code - 40],
2004
+ index: code - 40
2005
+ };
1434
2006
  break;
1435
2007
  case 48: {
1436
2008
  const subs = subParams.get(i);
@@ -1480,7 +2052,10 @@ function createScreen(options = {}) {
1480
2052
  case 95:
1481
2053
  case 96:
1482
2054
  case 97:
1483
- attrs.fg = { ...palette256[code - 90 + 8] };
2055
+ attrs.fg = {
2056
+ ...palette256[code - 90 + 8],
2057
+ index: code - 90 + 8
2058
+ };
1484
2059
  break;
1485
2060
  case 100:
1486
2061
  case 101:
@@ -1490,7 +2065,10 @@ function createScreen(options = {}) {
1490
2065
  case 105:
1491
2066
  case 106:
1492
2067
  case 107:
1493
- attrs.bg = { ...palette256[code - 100 + 8] };
2068
+ attrs.bg = {
2069
+ ...palette256[code - 100 + 8],
2070
+ index: code - 100 + 8
2071
+ };
1494
2072
  break;
1495
2073
  }
1496
2074
  i++;
@@ -1501,12 +2079,16 @@ function createScreen(options = {}) {
1501
2079
  const type = params[startIndex + 1];
1502
2080
  if (type === 5 && startIndex + 2 < params.length) {
1503
2081
  const idx = params[startIndex + 2];
2082
+ const entry = palette256[idx];
1504
2083
  return {
1505
- color: { ...palette256[idx] ?? {
2084
+ color: entry ? {
2085
+ ...entry,
2086
+ index: idx
2087
+ } : {
1506
2088
  r: 0,
1507
2089
  g: 0,
1508
2090
  b: 0
1509
- } },
2091
+ },
1510
2092
  nextIndex: startIndex + 3
1511
2093
  };
1512
2094
  } else if (type === 2 && startIndex + 4 < params.length) return {
@@ -1525,11 +2107,15 @@ function createScreen(options = {}) {
1525
2107
  const type = subs[1];
1526
2108
  if (type === 5 && subs.length >= 3) {
1527
2109
  const idx = subs[2];
1528
- return { ...palette256[idx] ?? {
2110
+ const entry = palette256[idx];
2111
+ return entry ? {
2112
+ ...entry,
2113
+ index: idx
2114
+ } : {
1529
2115
  r: 0,
1530
2116
  g: 0,
1531
2117
  b: 0
1532
- } };
2118
+ };
1533
2119
  } else if (type === 2) {
1534
2120
  if (subs.length >= 5) {
1535
2121
  if (subs.length >= 6) return {
@@ -1622,7 +2208,10 @@ function createScreen(options = {}) {
1622
2208
  if (isNaN(idx) || idx < 0 || idx > 4) continue;
1623
2209
  if (spec === "?") {
1624
2210
  const c = specialColors.get(idx);
1625
- 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
+ }
1626
2215
  } else {
1627
2216
  const c = parseColorSpec(spec);
1628
2217
  if (c) specialColors.set(idx, c);
@@ -1836,7 +2425,10 @@ function createScreen(options = {}) {
1836
2425
  if (clipSemi !== -1) {
1837
2426
  const data = value.substring(clipSemi + 1);
1838
2427
  if (data === "?") {
1839
- 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
+ }
1840
2432
  } else {
1841
2433
  try {
1842
2434
  clipboard = atob(data);
@@ -1938,8 +2530,10 @@ function createScreen(options = {}) {
1938
2530
  break;
1939
2531
  }
1940
2532
  case 5522:
1941
- if (value === "?" && onResponse) onResponse(`\x1b]5522;${btoa(advancedClipboard)}\x1b\\`);
1942
- else try {
2533
+ if (value === "?" && onResponse) {
2534
+ const encoded = btoa(advancedClipboard);
2535
+ onResponse(`\x1b]5522;${encoded}\x1b\\`);
2536
+ } else try {
1943
2537
  advancedClipboard = atob(value);
1944
2538
  } catch {
1945
2539
  advancedClipboard = "";
@@ -1968,8 +2562,10 @@ function createScreen(options = {}) {
1968
2562
  return;
1969
2563
  }
1970
2564
  if (data.startsWith("+q") && onResponse) {
1971
- if (data.substring(2) === "544e") onResponse(`\x1bP1+r544e=${Array.from(new TextEncoder().encode("vterm")).map((b) => b.toString(16).padStart(2, "0")).join("")}\x1b\\`);
1972
- 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\\");
1973
2569
  return;
1974
2570
  }
1975
2571
  }
@@ -2015,6 +2611,7 @@ function createScreen(options = {}) {
2015
2611
  altGrid = makeGrid(cols, rows);
2016
2612
  grid = mainGrid;
2017
2613
  scrollback = [];
2614
+ scrollbackSoftWrapped = [];
2018
2615
  curX = 0;
2019
2616
  curY = 0;
2020
2617
  curVisible = true;
@@ -2086,14 +2683,87 @@ function createScreen(options = {}) {
2086
2683
  parserState = "ground";
2087
2684
  escBuf = "";
2088
2685
  oscBuf = "";
2686
+ dcsBuf = "";
2687
+ dcsStartRow = 0;
2688
+ dcsStartCol = 0;
2089
2689
  apcBuf = "";
2690
+ utf8PendingBytes = [];
2090
2691
  semanticZones = [];
2091
2692
  mainSoftWrapped = new Array(rows).fill(false);
2092
2693
  altSoftWrapped = new Array(rows).fill(false);
2093
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);
2094
2764
  }
2095
2765
  function process(data) {
2096
- const text = decoder.decode(data, { stream: true });
2766
+ const text = decodeInput(data);
2097
2767
  for (let i = 0; i < text.length; i++) {
2098
2768
  const ch = text[i];
2099
2769
  const code = text.charCodeAt(i);
@@ -2102,18 +2772,25 @@ function createScreen(options = {}) {
2102
2772
  if (code === 27) {
2103
2773
  parserState = "escape";
2104
2774
  escBuf = "";
2105
- } else if (code === 7) {} else if (code === 8) {
2775
+ } else if (code === 7) emitExecute(7);
2776
+ else if (code === 8) {
2106
2777
  if (curX > 0) curX--;
2107
- } else if (code === 9) curX = nextTabStop(curX);
2108
- 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) {
2109
2783
  softWrapped[curY] = false;
2110
2784
  curY++;
2111
2785
  if (curY > scrollBottom) {
2112
2786
  curY = scrollBottom;
2113
2787
  scrollUp(scrollTop, scrollBottom);
2114
2788
  }
2115
- } else if (code === 13) curX = 0;
2116
- else if (code >= 32) {
2789
+ emitExecute(code);
2790
+ } else if (code === 13) {
2791
+ curX = 0;
2792
+ emitExecute(13);
2793
+ } else if (code >= 32) {
2117
2794
  let char = ch;
2118
2795
  if (code >= 55296 && code <= 56319 && i + 1 < text.length) {
2119
2796
  const nextCode = text.charCodeAt(i + 1);
@@ -2123,6 +2800,7 @@ function createScreen(options = {}) {
2123
2800
  }
2124
2801
  }
2125
2802
  writeChar(char);
2803
+ if (parserListeners.size > 0) printRunParts.push(char);
2126
2804
  }
2127
2805
  break;
2128
2806
  case "escape":
@@ -2141,6 +2819,7 @@ function createScreen(options = {}) {
2141
2819
  } else if (ch === "c") {
2142
2820
  fullReset();
2143
2821
  parserState = "ground";
2822
+ emitEsc("c", "");
2144
2823
  } else if (ch === "D") {
2145
2824
  curY++;
2146
2825
  if (curY > scrollBottom) {
@@ -2148,6 +2827,7 @@ function createScreen(options = {}) {
2148
2827
  scrollUp(scrollTop, scrollBottom);
2149
2828
  }
2150
2829
  parserState = "ground";
2830
+ emitEsc("D", "");
2151
2831
  } else if (ch === "M") {
2152
2832
  curY--;
2153
2833
  if (curY < scrollTop) {
@@ -2155,6 +2835,7 @@ function createScreen(options = {}) {
2155
2835
  scrollDown(scrollTop, scrollBottom);
2156
2836
  }
2157
2837
  parserState = "ground";
2838
+ emitEsc("M", "");
2158
2839
  } else if (ch === "7") {
2159
2840
  savedState = {
2160
2841
  curX,
@@ -2170,6 +2851,7 @@ function createScreen(options = {}) {
2170
2851
  charsetG0
2171
2852
  };
2172
2853
  parserState = "ground";
2854
+ emitEsc("7", "");
2173
2855
  } else if (ch === "8") {
2174
2856
  curX = savedState.curX;
2175
2857
  curY = savedState.curY;
@@ -2184,6 +2866,7 @@ function createScreen(options = {}) {
2184
2866
  charsetG0 = savedState.charsetG0;
2185
2867
  clampCursor();
2186
2868
  parserState = "ground";
2869
+ emitEsc("8", "");
2187
2870
  } else if (ch === "E") {
2188
2871
  curX = 0;
2189
2872
  curY++;
@@ -2192,18 +2875,28 @@ function createScreen(options = {}) {
2192
2875
  scrollUp(scrollTop, scrollBottom);
2193
2876
  }
2194
2877
  parserState = "ground";
2878
+ emitEsc("E", "");
2195
2879
  } else if (ch === "H") {
2196
2880
  tabStops.add(curX);
2197
2881
  parserState = "ground";
2198
- } else if (ch === "#") parserState = "escape_hash";
2199
- else if (ch === "(") parserState = "escape_charset";
2200
- else if (ch === ")") parserState = "escape_charset";
2201
- 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 === "=") {
2202
2893
  applicationKeypad = true;
2203
2894
  parserState = "ground";
2895
+ emitEsc("=", "");
2204
2896
  } else if (ch === ">") {
2205
2897
  applicationKeypad = false;
2206
2898
  parserState = "ground";
2899
+ emitEsc(">", "");
2207
2900
  } else if (ch === "_") {
2208
2901
  parserState = "apc";
2209
2902
  apcBuf = "";
@@ -2213,22 +2906,22 @@ function createScreen(options = {}) {
2213
2906
  if (ch === "0") charsetG0 = true;
2214
2907
  else charsetG0 = false;
2215
2908
  parserState = "ground";
2909
+ emitEsc(ch, escIntermediate);
2216
2910
  break;
2217
2911
  case "escape_hash":
2218
2912
  if (ch === "8") {
2913
+ const alignCell = emptyCell();
2914
+ alignCell.char = "E";
2219
2915
  for (let r = 0; r < rows; r++) {
2220
2916
  const row = grid[r];
2221
- for (let c = 0; c < cols; c++) {
2222
- const cell = emptyCell();
2223
- cell.char = "E";
2224
- row[c] = cell;
2225
- }
2917
+ for (let c = 0; c < cols; c++) row.setCellRaw(c, alignCell);
2226
2918
  softWrapped[r] = false;
2227
2919
  }
2228
2920
  curX = 0;
2229
2921
  curY = 0;
2230
2922
  }
2231
2923
  parserState = "ground";
2924
+ emitEsc(ch, escIntermediate);
2232
2925
  break;
2233
2926
  case "csi":
2234
2927
  if (code >= 64 && code <= 126) {
@@ -2250,6 +2943,7 @@ function createScreen(options = {}) {
2250
2943
  else if (paramPart.startsWith("=")) handleCSIEq(paramPart.substring(1), intermediatePart, ch);
2251
2944
  else handleCSI(paramPart, intermediatePart, ch);
2252
2945
  parserState = "ground";
2946
+ emitCsiEvent(paramPart, intermediatePart, ch);
2253
2947
  } else if (escBuf.length >= 256) parserState = "ground";
2254
2948
  else escBuf += ch;
2255
2949
  break;
@@ -2257,14 +2951,18 @@ function createScreen(options = {}) {
2257
2951
  if (code === 7) {
2258
2952
  handleOSC(oscBuf);
2259
2953
  parserState = "ground";
2954
+ emitOscEvent(oscBuf);
2260
2955
  } else if (code === 27) parserState = "osc_st";
2261
2956
  else if (oscBuf.length >= 4096) parserState = "ground";
2262
2957
  else oscBuf += ch;
2263
2958
  break;
2264
- case "osc_st":
2265
- if (ch === "\\") handleOSC(oscBuf);
2959
+ case "osc_st": {
2960
+ const oscComplete = ch === "\\";
2961
+ if (oscComplete) handleOSC(oscBuf);
2266
2962
  parserState = "ground";
2963
+ if (oscComplete) emitOscEvent(oscBuf);
2267
2964
  break;
2965
+ }
2268
2966
  case "dcs":
2269
2967
  if (code === 27) parserState = "dcs_st";
2270
2968
  else if (code === 7) {
@@ -2292,6 +2990,12 @@ function createScreen(options = {}) {
2292
2990
  break;
2293
2991
  }
2294
2992
  }
2993
+ flushPrintRun();
2994
+ if (opListeners.size > 0) emitOp({
2995
+ type: "output",
2996
+ data
2997
+ });
2998
+ flushSignals();
2295
2999
  }
2296
3000
  /**
2297
3001
  * Reconstruct logical lines from a grid, joining rows that were soft-wrapped.
@@ -2303,7 +3007,7 @@ function createScreen(options = {}) {
2303
3007
  for (let r = 0; r < srcRows; r++) {
2304
3008
  const row = srcGrid[r];
2305
3009
  if (!row) continue;
2306
- 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));
2307
3011
  if (srcSoftWrapped[r]) continue;
2308
3012
  logical.push(currentLine);
2309
3013
  currentLine = [];
@@ -2313,20 +3017,34 @@ function createScreen(options = {}) {
2313
3017
  }
2314
3018
  /**
2315
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.
2316
3028
  */
2317
- function rewrapLines(logicalLines, newCols) {
3029
+ function rewrapLines(logicalLines, newCols, track) {
2318
3030
  const outRows = [];
2319
3031
  const outWrapped = [];
2320
- for (const line of logicalLines) {
3032
+ let tracked = null;
3033
+ for (let li = 0; li < logicalLines.length; li++) {
3034
+ const line = logicalLines[li];
2321
3035
  let lineLen = line.length;
2322
3036
  while (lineLen > 0) {
2323
3037
  const cell = line[lineLen - 1];
2324
- if (cell === EMPTY_CELL || cell.char === "" && !cell.wide) lineLen--;
3038
+ if (cell.char === "" && !cell.wide) lineLen--;
2325
3039
  else break;
2326
3040
  }
2327
3041
  if (lineLen === 0) {
2328
3042
  outRows.push(makeRow(newCols));
2329
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
+ };
2330
3048
  continue;
2331
3049
  }
2332
3050
  let pos = 0;
@@ -2336,34 +3054,65 @@ function createScreen(options = {}) {
2336
3054
  while (col < newCols && pos < lineLen) {
2337
3055
  const cell = line[pos];
2338
3056
  if (cell.wide && col + 2 > newCols) break;
2339
- 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);
2340
3062
  col++;
2341
3063
  pos++;
2342
3064
  }
2343
3065
  const moreContent = pos < lineLen;
2344
3066
  outRows.push(row);
2345
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
+ }
2346
3075
  }
2347
3076
  }
2348
3077
  return {
2349
3078
  rows: outRows,
2350
- wrapped: outWrapped
3079
+ wrapped: outWrapped,
3080
+ tracked
2351
3081
  };
2352
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
+ }
2353
3088
  /**
2354
3089
  * Trim trailing empty rows from reflowed result, so they don't push content off the top
2355
3090
  * when we take the last newRows rows.
2356
3091
  */
2357
3092
  function trimTrailingEmptyRows(result) {
2358
- 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]) {
2359
- result.rows.pop();
2360
- result.wrapped.pop();
2361
- } 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
+ }
2362
3100
  }
2363
3101
  function resize(newCols, newRows) {
2364
- 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);
2365
3114
  trimTrailingEmptyRows(mainResult);
2366
- const altResult = rewrapLines(getLogicalLines(altGrid, altSoftWrapped, rows), newCols);
3115
+ const altResult = rewrapLines(getLogicalLines(altGrid, altSoftWrapped, rows), newCols, useAltScreen ? cursorTrack : void 0);
2367
3116
  trimTrailingEmptyRows(altResult);
2368
3117
  const newMain = makeGrid(newCols, newRows);
2369
3118
  const newMainWrapped = new Array(newRows).fill(false);
@@ -2379,6 +3128,12 @@ function createScreen(options = {}) {
2379
3128
  newAlt[r] = altResult.rows[altStartRow + r];
2380
3129
  newAltWrapped[r] = altResult.wrapped[altStartRow + r];
2381
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
+ }
2382
3137
  mainGrid = newMain;
2383
3138
  altGrid = newAlt;
2384
3139
  mainSoftWrapped = newMainWrapped;
@@ -2394,31 +3149,265 @@ function createScreen(options = {}) {
2394
3149
  rightMargin = cols - 1;
2395
3150
  if (newCols > oldCols) for (let i = Math.max(8, Math.ceil(oldCols / 8) * 8); i < newCols; i += 8) tabStops.add(i);
2396
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();
2397
3345
  }
2398
3346
  function getCell(row, col) {
2399
3347
  const r = grid[row];
2400
3348
  if (!r || col >= cols) return emptyCell();
2401
- return { ...r[col] };
3349
+ return stripCellColorIndex(r.getCellRaw(col));
2402
3350
  }
2403
3351
  function getLine(row) {
2404
3352
  const r = grid[row];
2405
- if (!r) return makeRow(cols);
2406
- return r.map((cell) => ({ ...cell }));
3353
+ if (!r) return makeRow(cols).toCells();
3354
+ return r.toCells().map(stripCellColorIndex);
2407
3355
  }
2408
3356
  function getText() {
2409
3357
  const lines = [];
2410
3358
  for (let r = 0; r < rows; r++) lines.push(rowToString(grid[r]));
2411
3359
  return lines.join("\n");
2412
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
+ }
2413
3402
  function rowToString(row) {
2414
3403
  let line = "";
2415
3404
  for (let i = 0; i < row.length; i++) {
2416
- const cell = row[i];
2417
- if (cell.wide) line += cell.char;
2418
- else if (cell.char === "") {
2419
- 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;
2420
3409
  line += " ";
2421
- } else line += cell.char;
3410
+ } else line += ch;
2422
3411
  }
2423
3412
  return line.replace(/\s+$/, "");
2424
3413
  }
@@ -2431,10 +3420,10 @@ function createScreen(options = {}) {
2431
3420
  const colEnd = row === endRow ? endCol : cols;
2432
3421
  let line = "";
2433
3422
  for (let col = colStart; col < colEnd; col++) {
2434
- const cell = r[col];
2435
- if (!cell) continue;
2436
- if (cell.char === "" && col > 0 && r[col - 1]?.wide) continue;
2437
- 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 || " ";
2438
3427
  }
2439
3428
  parts.push(line.replace(/\s+$/, ""));
2440
3429
  }
@@ -2464,6 +3453,176 @@ function createScreen(options = {}) {
2464
3453
  default: return false;
2465
3454
  }
2466
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
+ }
2467
3626
  return {
2468
3627
  get cols() {
2469
3628
  return cols;
@@ -2471,13 +3630,33 @@ function createScreen(options = {}) {
2471
3630
  get rows() {
2472
3631
  return rows;
2473
3632
  },
3633
+ get signals() {
3634
+ return getSignals();
3635
+ },
2474
3636
  process,
2475
3637
  resize,
3638
+ apply,
3639
+ tapOps,
3640
+ tapParser,
2476
3641
  reset: fullReset,
3642
+ snapshot,
3643
+ restore,
2477
3644
  getCell,
3645
+ getRow: getLine,
2478
3646
  getLine,
2479
3647
  getText,
3648
+ getScrollbackText,
2480
3649
  getTextRange,
3650
+ totalRows,
3651
+ screenRows,
3652
+ viewportTop,
3653
+ getRowAbsolute,
3654
+ firstRetainedRow,
3655
+ takeDirty,
3656
+ getCursor: () => ({
3657
+ col: curX,
3658
+ row: curY
3659
+ }),
2481
3660
  getCursorPosition: () => ({
2482
3661
  x: curX,
2483
3662
  y: curY
@@ -2494,8 +3673,888 @@ function createScreen(options = {}) {
2494
3673
  getViewportOffset: () => viewportOffset,
2495
3674
  scrollViewport,
2496
3675
  getSemanticZones: () => semanticZones.map((z) => ({ ...z })),
2497
- 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
2498
4557
  };
2499
4558
  }
2500
4559
  //#endregion
2501
- export { createScreen as createVtermScreen };
4560
+ export { createScreen as createVtermScreen, decodeScreenSnapshotBinary, encodeScreenSnapshotBinary, serializeSnapshot };