vterm.js 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  //#region src/screen.ts
2
+ /** Default retained payload bound for one APC/DCS sequence (16 MiB of ASCII/base64). */
3
+ const DEFAULT_MAX_STRING_SEQUENCE_LENGTH = 16 * 1024 * 1024;
2
4
  /** Frozen sentinel for unwritten cells — never mutate, copy-on-write in writeChar(). */
3
5
  const EMPTY_CELL = Object.freeze({
4
6
  char: "",
@@ -20,6 +22,248 @@ const EMPTY_CELL = Object.freeze({
20
22
  function emptyCell() {
21
23
  return { ...EMPTY_CELL };
22
24
  }
25
+ /**
26
+ * Drop the palette-origin {@link Color.index} for the public per-cell read
27
+ * boundary. `getCell`/`getRow` return the RESOLVED RGB — their long-standing
28
+ * contract — while the origin index is serialization-only provenance that rides
29
+ * {@link Snapshot} (and thus `serialize()`), not the inspection API.
30
+ * Returns the SAME reference when there is no index (the common truecolor/null
31
+ * case), so the read path allocates nothing there.
32
+ */
33
+ function stripColorIndex(c) {
34
+ if (c === null || c.index === void 0) return c;
35
+ return {
36
+ r: c.r,
37
+ g: c.g,
38
+ b: c.b
39
+ };
40
+ }
41
+ /** Shallow cell copy with each color stripped of its palette-origin index. */
42
+ function stripCellColorIndex(cell) {
43
+ return {
44
+ ...cell,
45
+ fg: stripColorIndex(cell.fg),
46
+ bg: stripColorIndex(cell.bg),
47
+ underlineColor: stripColorIndex(cell.underlineColor)
48
+ };
49
+ }
50
+ const F_BOLD$1 = 1;
51
+ const F_FAINT$1 = 2;
52
+ const F_ITALIC$1 = 4;
53
+ const F_OVERLINE$1 = 8;
54
+ const F_STRIKE$1 = 16;
55
+ const F_INVERSE$1 = 32;
56
+ const F_HIDDEN$1 = 64;
57
+ const F_BLINK$1 = 128;
58
+ const F_WIDE$1 = 256;
59
+ const UL_SHIFT$1 = 9;
60
+ const UL_MASK$1 = 7 << UL_SHIFT$1;
61
+ const F_HAS_FG = 4096;
62
+ const F_HAS_BG = 8192;
63
+ const F_HAS_UL = 16384;
64
+ const F_HAS_URL = 32768;
65
+ const UL_STYLES = [
66
+ "none",
67
+ "single",
68
+ "double",
69
+ "curly",
70
+ "dotted",
71
+ "dashed"
72
+ ];
73
+ function underlineToBits(u) {
74
+ const i = UL_STYLES.indexOf(u);
75
+ return (i < 0 ? 0 : i) << UL_SHIFT$1;
76
+ }
77
+ function bitsToUnderline(meta) {
78
+ return UL_STYLES[(meta & UL_MASK$1) >> UL_SHIFT$1] ?? "none";
79
+ }
80
+ /** Pack a color's 8-bit channels into a 24-bit `0xRRGGBB` word. */
81
+ function packRgb(c) {
82
+ return ((c.r & 255) << 16 | (c.g & 255) << 8 | c.b & 255) >>> 0;
83
+ }
84
+ /** Materialize a {@link Color} from a packed RGB word + index (`-1` = no index). */
85
+ function unpackColor(rgb, idx) {
86
+ const r = rgb >> 16 & 255;
87
+ const g = rgb >> 8 & 255;
88
+ const b = rgb & 255;
89
+ return idx >= 0 ? {
90
+ r,
91
+ g,
92
+ b,
93
+ index: idx
94
+ } : {
95
+ r,
96
+ g,
97
+ b
98
+ };
99
+ }
100
+ function makePackedRow(width) {
101
+ const meta = new Uint32Array(width);
102
+ const chars = new Array(width).fill("");
103
+ let fgRgb = null;
104
+ let fgIdx = null;
105
+ let bgRgb = null;
106
+ let bgIdx = null;
107
+ let ulRgb = null;
108
+ let ulIdx = null;
109
+ let urlMap = null;
110
+ function setFg(col, c) {
111
+ if (fgRgb === null) {
112
+ fgRgb = new Uint32Array(width);
113
+ fgIdx = new Int16Array(width);
114
+ }
115
+ fgRgb[col] = packRgb(c);
116
+ fgIdx[col] = c.index ?? -1;
117
+ }
118
+ function setBg(col, c) {
119
+ if (bgRgb === null) {
120
+ bgRgb = new Uint32Array(width);
121
+ bgIdx = new Int16Array(width);
122
+ }
123
+ bgRgb[col] = packRgb(c);
124
+ bgIdx[col] = c.index ?? -1;
125
+ }
126
+ function setUl(col, c) {
127
+ if (ulRgb === null) {
128
+ ulRgb = new Uint32Array(width);
129
+ ulIdx = new Int16Array(width);
130
+ }
131
+ ulRgb[col] = packRgb(c);
132
+ ulIdx[col] = c.index ?? -1;
133
+ }
134
+ function setUrl(col, u) {
135
+ (urlMap ??= /* @__PURE__ */ new Map()).set(col, u);
136
+ }
137
+ function clearUrl(col) {
138
+ if (urlMap !== null && urlMap.size > 0) urlMap.delete(col);
139
+ }
140
+ function packFlags(a, wide) {
141
+ let m = 0;
142
+ if (a.bold) m |= F_BOLD$1;
143
+ if (a.faint) m |= F_FAINT$1;
144
+ if (a.italic) m |= F_ITALIC$1;
145
+ if (a.overline) m |= F_OVERLINE$1;
146
+ if (a.strikethrough) m |= F_STRIKE$1;
147
+ if (a.inverse) m |= F_INVERSE$1;
148
+ if (a.hidden) m |= F_HIDDEN$1;
149
+ if (a.blink) m |= F_BLINK$1;
150
+ if (wide) m |= F_WIDE$1;
151
+ return m | underlineToBits(a.underline);
152
+ }
153
+ const row = {
154
+ length: width,
155
+ writeFromAttrs(col, ch, a, wide) {
156
+ let m = packFlags(a, wide);
157
+ chars[col] = ch;
158
+ if (a.fg) {
159
+ setFg(col, a.fg);
160
+ m |= F_HAS_FG;
161
+ }
162
+ if (a.bg) {
163
+ setBg(col, a.bg);
164
+ m |= F_HAS_BG;
165
+ }
166
+ if (a.underlineColor) {
167
+ setUl(col, a.underlineColor);
168
+ m |= F_HAS_UL;
169
+ }
170
+ if (a.url !== null) {
171
+ setUrl(col, a.url);
172
+ m |= F_HAS_URL;
173
+ } else clearUrl(col);
174
+ meta[col] = m;
175
+ },
176
+ setCellRaw(col, cell) {
177
+ let m = packFlags(cell, cell.wide);
178
+ chars[col] = cell.char;
179
+ if (cell.fg) {
180
+ setFg(col, cell.fg);
181
+ m |= F_HAS_FG;
182
+ }
183
+ if (cell.bg) {
184
+ setBg(col, cell.bg);
185
+ m |= F_HAS_BG;
186
+ }
187
+ if (cell.underlineColor) {
188
+ setUl(col, cell.underlineColor);
189
+ m |= F_HAS_UL;
190
+ }
191
+ if (cell.url !== null) {
192
+ setUrl(col, cell.url);
193
+ m |= F_HAS_URL;
194
+ } else clearUrl(col);
195
+ meta[col] = m;
196
+ },
197
+ setEmpty(col) {
198
+ meta[col] = 0;
199
+ chars[col] = "";
200
+ clearUrl(col);
201
+ },
202
+ eraseWithBg(col, bg) {
203
+ chars[col] = "";
204
+ clearUrl(col);
205
+ if (bg) {
206
+ setBg(col, bg);
207
+ meta[col] = F_HAS_BG;
208
+ } else meta[col] = 0;
209
+ },
210
+ widen(col) {
211
+ meta[col] = (meta[col] | F_WIDE$1) >>> 0;
212
+ if (col + 1 < width) row.setEmpty(col + 1);
213
+ },
214
+ appendChar(col, ch) {
215
+ chars[col] = chars[col] + ch;
216
+ },
217
+ copyCellFrom(col, src, srcCol) {
218
+ row.setCellRaw(col, src.getCellRaw(srcCol));
219
+ },
220
+ getCellRaw(col) {
221
+ const m = meta[col];
222
+ if (m === 0 && chars[col] === "") return emptyCell();
223
+ return {
224
+ char: chars[col],
225
+ fg: m & F_HAS_FG ? unpackColor(fgRgb[col], fgIdx[col]) : null,
226
+ bg: m & F_HAS_BG ? unpackColor(bgRgb[col], bgIdx[col]) : null,
227
+ bold: (m & F_BOLD$1) !== 0,
228
+ faint: (m & F_FAINT$1) !== 0,
229
+ italic: (m & F_ITALIC$1) !== 0,
230
+ underline: bitsToUnderline(m),
231
+ underlineColor: m & F_HAS_UL ? unpackColor(ulRgb[col], ulIdx[col]) : null,
232
+ overline: (m & F_OVERLINE$1) !== 0,
233
+ strikethrough: (m & F_STRIKE$1) !== 0,
234
+ inverse: (m & F_INVERSE$1) !== 0,
235
+ hidden: (m & F_HIDDEN$1) !== 0,
236
+ blink: (m & F_BLINK$1) !== 0,
237
+ wide: (m & F_WIDE$1) !== 0,
238
+ url: m & F_HAS_URL ? urlMap.get(col) ?? null : null
239
+ };
240
+ },
241
+ toCells() {
242
+ const out = new Array(width);
243
+ for (let c = 0; c < width; c++) out[c] = row.getCellRaw(c);
244
+ return out;
245
+ },
246
+ replaceAllFromCells(cells) {
247
+ for (let c = 0; c < width; c++) row.setCellRaw(c, cells[c] ?? EMPTY_CELL);
248
+ },
249
+ isEmpty(col) {
250
+ return meta[col] === 0 && chars[col] === "";
251
+ },
252
+ getChar(col) {
253
+ return chars[col];
254
+ },
255
+ isWide(col) {
256
+ return (meta[col] & F_WIDE$1) !== 0;
257
+ }
258
+ };
259
+ return row;
260
+ }
261
+ /** Build a packed row from a materialized cell array (reflow / restore output). */
262
+ function packedRowFromCells(cells, width) {
263
+ const r = makePackedRow(width);
264
+ for (let c = 0; c < width; c++) r.setCellRaw(c, cells[c] ?? EMPTY_CELL);
265
+ return r;
266
+ }
23
267
  const ANSI_16 = [
24
268
  {
25
269
  r: 0,
@@ -217,8 +461,10 @@ function isEmojiModifier(cp) {
217
461
  function createScreen(options = {}) {
218
462
  let cols = options.cols ?? 80;
219
463
  let rows = options.rows ?? 24;
220
- const scrollbackLimit = options.scrollbackLimit ?? 1e3;
464
+ let scrollbackLimit = options.scrollbackLimit ?? 1e3;
221
465
  const onResponse = options.onResponse;
466
+ const maxStringSequenceLength = options.maxStringSequenceLength ?? 16777216;
467
+ if (!Number.isSafeInteger(maxStringSequenceLength) || maxStringSequenceLength < 0) throw new RangeError("maxStringSequenceLength must be a non-negative safe integer");
222
468
  let mainGrid = makeGrid(cols, rows);
223
469
  let altGrid = makeGrid(cols, rows);
224
470
  let grid = mainGrid;
@@ -303,6 +549,201 @@ function createScreen(options = {}) {
303
549
  highlightBgColor = cloneColor(snapshot.highlightBgColor);
304
550
  highlightFgColor = cloneColor(snapshot.highlightFgColor);
305
551
  }
552
+ function cloneColorStateSnapshot(snapshot) {
553
+ return {
554
+ palette256: snapshot.palette256.map((c) => ({ ...c })),
555
+ defaultFgColor: cloneColor(snapshot.defaultFgColor),
556
+ defaultBgColor: cloneColor(snapshot.defaultBgColor),
557
+ cursorColor: cloneColor(snapshot.cursorColor),
558
+ specialColors: snapshot.specialColors.map(([idx, color]) => [idx, { ...color }]),
559
+ pointerFgColor: cloneColor(snapshot.pointerFgColor),
560
+ pointerBgColor: cloneColor(snapshot.pointerBgColor),
561
+ highlightBgColor: cloneColor(snapshot.highlightBgColor),
562
+ highlightFgColor: cloneColor(snapshot.highlightFgColor)
563
+ };
564
+ }
565
+ function cloneAttrsSnapshot(source) {
566
+ return {
567
+ ...source,
568
+ fg: cloneColor(source.fg),
569
+ bg: cloneColor(source.bg),
570
+ underlineColor: cloneColor(source.underlineColor)
571
+ };
572
+ }
573
+ function cloneCellSnapshot(source) {
574
+ return {
575
+ ...source,
576
+ fg: cloneColor(source.fg),
577
+ bg: cloneColor(source.bg),
578
+ underlineColor: cloneColor(source.underlineColor)
579
+ };
580
+ }
581
+ function isDefaultEmptyCell(cell) {
582
+ 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;
583
+ }
584
+ function restoreCellSnapshot(source) {
585
+ const cell = cloneCellSnapshot(source);
586
+ return isDefaultEmptyCell(cell) ? EMPTY_CELL : cell;
587
+ }
588
+ /** Materialize a packed grid to plain, deep-cloned Snapshot cells (index preserved). */
589
+ function cloneGridSnapshot(source) {
590
+ return source.map((row) => row.toCells().map(cloneCellSnapshot));
591
+ }
592
+ function restoreGridSnapshot(source, expectedRows, expectedCols) {
593
+ const out = [];
594
+ for (let row = 0; row < expectedRows; row++) {
595
+ const cells = (source[row] ?? []).slice(0, expectedCols).map(restoreCellSnapshot);
596
+ while (cells.length < expectedCols) cells.push(EMPTY_CELL);
597
+ out.push(packedRowFromCells(cells, expectedCols));
598
+ }
599
+ return out;
600
+ }
601
+ function restoreScrollbackSnapshot(source) {
602
+ return source.map((row) => packedRowFromCells(row.map(restoreCellSnapshot), row.length));
603
+ }
604
+ function isRecord(value) {
605
+ return typeof value === "object" && value !== null;
606
+ }
607
+ function assertInteger(name, value, min = 0) {
608
+ if (!Number.isInteger(value) || value < min) throw new TypeError(`Invalid vterm snapshot ${name}`);
609
+ }
610
+ function assertString(name, value) {
611
+ if (typeof value !== "string") throw new TypeError(`Invalid vterm snapshot ${name}`);
612
+ }
613
+ function assertBoolean(name, value) {
614
+ if (typeof value !== "boolean") throw new TypeError(`Invalid vterm snapshot ${name}`);
615
+ }
616
+ function assertRecord(name, value) {
617
+ if (!isRecord(value)) throw new TypeError(`Invalid vterm snapshot ${name}`);
618
+ }
619
+ function assertGrid(name, value, expectedRows, expectedCols) {
620
+ if (!Array.isArray(value) || value.length !== expectedRows) throw new TypeError(`Invalid vterm snapshot ${name}`);
621
+ for (const row of value) if (!Array.isArray(row) || row.length !== expectedCols) throw new TypeError(`Invalid vterm snapshot ${name}`);
622
+ }
623
+ function assertSoftWraps(name, value, expectedRows) {
624
+ if (!Array.isArray(value) || value.length !== expectedRows || value.some((entry) => typeof entry !== "boolean")) throw new TypeError(`Invalid vterm snapshot ${name}`);
625
+ }
626
+ function assertSnapshot(snapshot) {
627
+ const value = snapshot;
628
+ if (!isRecord(value)) throw new TypeError("Invalid vterm snapshot");
629
+ if (value.version !== 1) throw new Error(`Unsupported vterm snapshot version: ${String(value.version)}`);
630
+ assertInteger("cols", value.cols, 1);
631
+ assertInteger("rows", value.rows, 1);
632
+ assertInteger("scrollbackLimit", value.scrollbackLimit, 0);
633
+ if (value.activeBuffer !== "main" && value.activeBuffer !== "alt") throw new TypeError("Invalid vterm snapshot activeBuffer");
634
+ const main = value.main;
635
+ const alt = value.alt;
636
+ assertRecord("main", main);
637
+ assertRecord("alt", alt);
638
+ assertGrid("main.grid", main.grid, value.rows, value.cols);
639
+ assertGrid("alt.grid", alt.grid, value.rows, value.cols);
640
+ assertSoftWraps("main.softWrapped", main.softWrapped, value.rows);
641
+ assertSoftWraps("alt.softWrapped", alt.softWrapped, value.rows);
642
+ if (!Array.isArray(value.scrollback)) throw new TypeError("Invalid vterm snapshot scrollback");
643
+ if (value.scrollbackSoftWrapped !== void 0) assertSoftWraps("scrollbackSoftWrapped", value.scrollbackSoftWrapped, value.scrollback.length);
644
+ const cursor = value.cursor;
645
+ assertRecord("cursor", cursor);
646
+ assertInteger("cursor.x", cursor.x);
647
+ assertInteger("cursor.y", cursor.y);
648
+ assertBoolean("cursor.visible", cursor.visible);
649
+ if (cursor.shape !== "block" && cursor.shape !== "underline" && cursor.shape !== "bar") throw new TypeError("Invalid vterm snapshot cursor.shape");
650
+ assertBoolean("cursor.blinking", cursor.blinking);
651
+ assertInteger("cursor.savedX", cursor.savedX);
652
+ assertInteger("cursor.savedY", cursor.savedY);
653
+ const saved = value.savedState;
654
+ assertRecord("savedState", saved);
655
+ assertInteger("savedState.x", saved.x);
656
+ assertInteger("savedState.y", saved.y);
657
+ assertRecord("savedState.attrs", saved.attrs);
658
+ assertBoolean("savedState.originMode", saved.originMode);
659
+ assertBoolean("savedState.autoWrap", saved.autoWrap);
660
+ assertBoolean("savedState.charsetG0", saved.charsetG0);
661
+ assertRecord("attrs", value.attrs);
662
+ const modes = value.modes;
663
+ assertRecord("modes", modes);
664
+ for (const name of [
665
+ "bracketedPaste",
666
+ "applicationCursor",
667
+ "applicationKeypad",
668
+ "autoWrap",
669
+ "mouseTracking",
670
+ "sgrMouse",
671
+ "focusTracking",
672
+ "origin",
673
+ "insert",
674
+ "reverseVideo",
675
+ "syncOutput",
676
+ "kittyGraphics",
677
+ "colorSchemeReporting",
678
+ "decColumn",
679
+ "altScroll",
680
+ "utf8Mouse"
681
+ ]) assertBoolean(`modes.${name}`, modes[name]);
682
+ assertInteger("modes.mouseTrackingMode", modes.mouseTrackingMode);
683
+ assertInteger("modes.kittyKeyboardFlags", modes.kittyKeyboardFlags);
684
+ if (!Array.isArray(modes.kittyKeyboardStack)) throw new TypeError("Invalid vterm snapshot modes.kittyKeyboardStack");
685
+ const margins = value.margins;
686
+ assertRecord("margins", margins);
687
+ assertInteger("margins.scrollTop", margins.scrollTop);
688
+ assertInteger("margins.scrollBottom", margins.scrollBottom);
689
+ assertBoolean("margins.leftRight", margins.leftRight);
690
+ assertInteger("margins.left", margins.left);
691
+ assertInteger("margins.right", margins.right);
692
+ const colors = value.colors;
693
+ assertRecord("colors", colors);
694
+ assertRecord("colors.current", colors.current);
695
+ if (!Array.isArray(colors.stack)) throw new TypeError("Invalid vterm snapshot colors.stack");
696
+ if (!Array.isArray(value.tabStops)) throw new TypeError("Invalid vterm snapshot tabStops");
697
+ for (const stop of value.tabStops) assertInteger("tabStops", stop);
698
+ assertString("title", value.title);
699
+ assertString("clipboard", value.clipboard);
700
+ assertString("cwd", value.cwd);
701
+ if (!Array.isArray(value.notifications) || value.notifications.some((entry) => typeof entry !== "string")) throw new TypeError("Invalid vterm snapshot notifications");
702
+ assertInteger("viewportOffset", value.viewportOffset);
703
+ const parser = value.parser;
704
+ assertRecord("parser", parser);
705
+ if (![
706
+ "ground",
707
+ "escape",
708
+ "escape_charset",
709
+ "escape_hash",
710
+ "csi",
711
+ "osc",
712
+ "dcs",
713
+ "dcs_passthrough",
714
+ "osc_st",
715
+ "dcs_st",
716
+ "apc",
717
+ "apc_st"
718
+ ].includes(parser.state)) throw new TypeError("Invalid vterm snapshot parser.state");
719
+ assertString("parser.esc", parser.esc);
720
+ assertString("parser.osc", parser.osc);
721
+ assertString("parser.dcs", parser.dcs);
722
+ assertRecord("parser.dcsStart", parser.dcsStart);
723
+ assertInteger("parser.dcsStart.row", parser.dcsStart.row);
724
+ assertInteger("parser.dcsStart.col", parser.dcsStart.col);
725
+ if (parser.dcsReceivedLength !== void 0) assertInteger("parser.dcsReceivedLength", parser.dcsReceivedLength, 0);
726
+ if (parser.dcsOverflow !== void 0) assertBoolean("parser.dcsOverflow", parser.dcsOverflow);
727
+ assertString("parser.apc", parser.apc);
728
+ if (parser.apcStart !== void 0) {
729
+ assertRecord("parser.apcStart", parser.apcStart);
730
+ assertInteger("parser.apcStart.row", parser.apcStart.row);
731
+ assertInteger("parser.apcStart.col", parser.apcStart.col);
732
+ }
733
+ if (parser.apcReceivedLength !== void 0) assertInteger("parser.apcReceivedLength", parser.apcReceivedLength, 0);
734
+ if (parser.apcOverflow !== void 0) assertBoolean("parser.apcOverflow", parser.apcOverflow);
735
+ if (!Array.isArray(parser.utf8PendingBytes)) throw new TypeError("Invalid vterm snapshot parser.utf8PendingBytes");
736
+ for (const byte of parser.utf8PendingBytes) {
737
+ assertInteger("parser.utf8PendingBytes", byte, 0);
738
+ if (byte > 255) throw new TypeError("Invalid vterm snapshot parser.utf8PendingBytes");
739
+ }
740
+ const unicode = value.unicode;
741
+ assertRecord("unicode", unicode);
742
+ assertBoolean("unicode.charsetG0", unicode.charsetG0);
743
+ assertString("unicode.lastChar", unicode.lastChar);
744
+ if (unicode.pendingRegionalIndicator !== null) assertString("unicode.pendingRegionalIndicator", unicode.pendingRegionalIndicator);
745
+ assertBoolean("unicode.afterZWJ", unicode.afterZWJ);
746
+ }
306
747
  let tabStops = defaultTabStops(cols);
307
748
  function defaultTabStops(c) {
308
749
  const s = /* @__PURE__ */ new Set();
@@ -337,6 +778,7 @@ function createScreen(options = {}) {
337
778
  let mainSoftWrapped = new Array(rows).fill(false);
338
779
  let altSoftWrapped = new Array(rows).fill(false);
339
780
  let softWrapped = mainSoftWrapped;
781
+ let scrollbackSoftWrapped = [];
340
782
  let lastChar = "";
341
783
  let pendingRegionalIndicator = null;
342
784
  let afterZWJ = false;
@@ -346,17 +788,129 @@ function createScreen(options = {}) {
346
788
  let dcsBuf = "";
347
789
  let dcsStartRow = 0;
348
790
  let dcsStartCol = 0;
791
+ let dcsReceivedLength = 0;
792
+ let dcsOverflow = false;
349
793
  let apcBuf = "";
794
+ let apcStartRow = 0;
795
+ let apcStartCol = 0;
796
+ let apcReceivedLength = 0;
797
+ let apcOverflow = false;
798
+ const opListeners = /* @__PURE__ */ new Set();
799
+ const parserListeners = /* @__PURE__ */ new Set();
800
+ let printRunParts = [];
801
+ let escIntermediate = "";
802
+ let dirtyAll = false;
803
+ let dirtyRows = /* @__PURE__ */ new Set();
804
+ let dirtyScrolled = 0;
805
+ let dirtyLastAbs = -1;
806
+ let trimmedRowCount = 0;
807
+ let dirtyCursor = {
808
+ x: 0,
809
+ y: 0,
810
+ visible: true,
811
+ shape: "block",
812
+ blinking: true
813
+ };
814
+ let sigDamageActive = false;
815
+ let sigDirtyAll = false;
816
+ let sigDirtyRows = /* @__PURE__ */ new Set();
817
+ let sigDirtyScrolled = 0;
818
+ let sigDirtyLastAbs = -1;
819
+ let sigDirtyCursor = {
820
+ x: 0,
821
+ y: 0,
822
+ visible: true,
823
+ shape: "block",
824
+ blinking: true
825
+ };
826
+ function markScreenRowDirty(screenRow) {
827
+ const abs = scrollback.length + screenRow;
828
+ if (!dirtyAll && abs !== dirtyLastAbs) {
829
+ dirtyLastAbs = abs;
830
+ dirtyRows.add(abs);
831
+ }
832
+ if (sigDamageActive && !sigDirtyAll && abs !== sigDirtyLastAbs) {
833
+ sigDirtyLastAbs = abs;
834
+ sigDirtyRows.add(abs);
835
+ }
836
+ }
837
+ function markAbsoluteRowDirty(abs) {
838
+ if (!dirtyAll && abs !== dirtyLastAbs) {
839
+ dirtyLastAbs = abs;
840
+ dirtyRows.add(abs);
841
+ }
842
+ if (sigDamageActive && !sigDirtyAll && abs !== sigDirtyLastAbs) {
843
+ sigDirtyLastAbs = abs;
844
+ sigDirtyRows.add(abs);
845
+ }
846
+ }
847
+ function markScreenRowsDirty(top, bottom) {
848
+ if (dirtyAll && (!sigDamageActive || sigDirtyAll)) return;
849
+ for (let r = top; r <= bottom; r++) markScreenRowDirty(r);
850
+ }
851
+ /** Structural change — the whole visible buffer is damaged. Drops the per-row set (both planes). */
852
+ function markAllDirty() {
853
+ dirtyAll = true;
854
+ dirtyRows.clear();
855
+ dirtyLastAbs = -1;
856
+ if (sigDamageActive) {
857
+ sigDirtyAll = true;
858
+ sigDirtyRows.clear();
859
+ sigDirtyLastAbs = -1;
860
+ }
861
+ }
350
862
  const decoder = new TextDecoder();
863
+ const encoder = new TextEncoder();
864
+ let utf8PendingBytes = [];
865
+ function utf8SequenceLength(lead) {
866
+ if (lead <= 127) return 1;
867
+ if (lead >= 194 && lead <= 223) return 2;
868
+ if (lead >= 224 && lead <= 239) return 3;
869
+ if (lead >= 240 && lead <= 244) return 4;
870
+ return 0;
871
+ }
872
+ function utf8CompletePrefixLength(bytes) {
873
+ if (bytes.length === 0) return 0;
874
+ let continuationCount = 0;
875
+ let leadIndex = bytes.length - 1;
876
+ while (leadIndex >= 0 && (bytes[leadIndex] & 192) === 128 && continuationCount < 3) {
877
+ continuationCount++;
878
+ leadIndex--;
879
+ }
880
+ if (leadIndex < 0) return bytes.length;
881
+ const lead = bytes[leadIndex];
882
+ const expectedLength = utf8SequenceLength(lead);
883
+ if (expectedLength === 0) return bytes.length;
884
+ return expectedLength > bytes.length - leadIndex ? leadIndex : bytes.length;
885
+ }
886
+ function decodeInput(data) {
887
+ const bytes = new Uint8Array(utf8PendingBytes.length + data.length);
888
+ bytes.set(utf8PendingBytes, 0);
889
+ bytes.set(data, utf8PendingBytes.length);
890
+ const completeLength = utf8CompletePrefixLength(bytes);
891
+ utf8PendingBytes = Array.from(bytes.slice(completeLength));
892
+ if (completeLength === 0) return "";
893
+ return decoder.decode(bytes.slice(0, completeLength));
894
+ }
351
895
  function makeGrid(c, r) {
352
896
  const g = [];
353
897
  for (let row = 0; row < r; row++) g.push(makeRow(c));
354
898
  return g;
355
899
  }
356
900
  function makeRow(c) {
357
- const row = [];
358
- for (let col = 0; col < c; col++) row.push(EMPTY_CELL);
359
- return row;
901
+ return makePackedRow(c);
902
+ }
903
+ /**
904
+ * Edit-path escape hatch for the rare column-splice operations (insert/delete
905
+ * chars & columns, rect fill/copy/attr): materialize the row to a fixed-length
906
+ * cell array, run the ordinary array edit, then repack. Length-preserving edits
907
+ * (splice+push/pop pairs) keep the row exactly `cols` wide. Not the hot path —
908
+ * plain printing goes straight through {@link PackedRow.writeFromAttrs}.
909
+ */
910
+ function mutateRowAsCells(row, fn) {
911
+ const cells = row.toCells();
912
+ fn(cells);
913
+ row.replaceAllFromCells(cells);
360
914
  }
361
915
  function resetAttrs() {
362
916
  return {
@@ -388,14 +942,35 @@ function createScreen(options = {}) {
388
942
  for (let r = top; r < bottom; r++) {
389
943
  const srcRow = grid[r + 1];
390
944
  const dstRow = grid[r];
391
- for (let c = lm; c <= rm && c < cols; c++) dstRow[c] = srcRow[c];
945
+ for (let c = lm; c <= rm && c < cols; c++) dstRow.copyCellFrom(c, srcRow, c);
392
946
  }
393
947
  const bottomRow = grid[bottom];
394
- for (let c = lm; c <= rm && c < cols; c++) bottomRow[c] = EMPTY_CELL;
948
+ for (let c = lm; c <= rm && c < cols; c++) bottomRow.setEmpty(c);
949
+ markScreenRowsDirty(top, bottom);
395
950
  } else {
396
- if (grid === mainGrid && top === 0) {
951
+ const enteredScrollback = grid === mainGrid && top === 0;
952
+ if (enteredScrollback) {
397
953
  scrollback.push(grid[0]);
398
- if (scrollback.length > scrollbackLimit * 2) scrollback.splice(0, scrollback.length - scrollbackLimit);
954
+ scrollbackSoftWrapped.push(softWrapped[0] ?? false);
955
+ markAbsoluteRowDirty(scrollback.length - 1);
956
+ if (scrollback.length > scrollbackLimit * 2) {
957
+ const over = scrollback.length - scrollbackLimit;
958
+ scrollback.splice(0, over);
959
+ scrollbackSoftWrapped.splice(0, over);
960
+ if (!dirtyAll && dirtyRows.size > 0) {
961
+ const shifted = /* @__PURE__ */ new Set();
962
+ for (const r of dirtyRows) if (r >= over) shifted.add(r - over);
963
+ dirtyRows = shifted;
964
+ dirtyLastAbs = -1;
965
+ }
966
+ if (sigDamageActive && !sigDirtyAll && sigDirtyRows.size > 0) {
967
+ const shifted = /* @__PURE__ */ new Set();
968
+ for (const r of sigDirtyRows) if (r >= over) shifted.add(r - over);
969
+ sigDirtyRows = shifted;
970
+ sigDirtyLastAbs = -1;
971
+ }
972
+ trimmedRowCount += over;
973
+ }
399
974
  }
400
975
  for (let i = top; i < bottom; i++) {
401
976
  grid[i] = grid[i + 1];
@@ -403,6 +978,11 @@ function createScreen(options = {}) {
403
978
  }
404
979
  grid[bottom] = makeRow(cols);
405
980
  softWrapped[bottom] = false;
981
+ if (enteredScrollback) {
982
+ dirtyScrolled++;
983
+ if (sigDamageActive) sigDirtyScrolled++;
984
+ markScreenRowDirty(bottom);
985
+ } else markScreenRowsDirty(top, bottom);
406
986
  }
407
987
  }
408
988
  function scrollDown(top, bottom) {
@@ -412,10 +992,10 @@ function createScreen(options = {}) {
412
992
  for (let r = bottom; r > top; r--) {
413
993
  const srcRow = grid[r - 1];
414
994
  const dstRow = grid[r];
415
- for (let c = lm; c <= rm && c < cols; c++) dstRow[c] = srcRow[c];
995
+ for (let c = lm; c <= rm && c < cols; c++) dstRow.copyCellFrom(c, srcRow, c);
416
996
  }
417
997
  const topRow = grid[top];
418
- for (let c = lm; c <= rm && c < cols; c++) topRow[c] = EMPTY_CELL;
998
+ for (let c = lm; c <= rm && c < cols; c++) topRow.setEmpty(c);
419
999
  } else {
420
1000
  for (let i = bottom; i > top; i--) {
421
1001
  grid[i] = grid[i - 1];
@@ -424,11 +1004,17 @@ function createScreen(options = {}) {
424
1004
  grid[top] = makeRow(cols);
425
1005
  softWrapped[top] = false;
426
1006
  }
1007
+ markScreenRowsDirty(top, bottom);
427
1008
  }
428
1009
  function scrollViewport(delta) {
429
1010
  viewportOffset = Math.max(0, Math.min(scrollback.length, viewportOffset + delta));
430
1011
  }
431
- /** Find the previous non-spacer cell (the cell before curX, skipping wide-char spacers) */
1012
+ /**
1013
+ * Locate the previous non-spacer cell position (before curX, skipping a wide-char
1014
+ * spacer). Returns the packed row and column so callers mutate through the row's API
1015
+ * (combining marks append, VS-16 widens). The spacer is detected structurally — a
1016
+ * blank char whose left neighbour is wide — rather than by object identity.
1017
+ */
432
1018
  function getPrevCell() {
433
1019
  if (curX === 0 && curY === 0) return null;
434
1020
  let prevCol = curX - 1;
@@ -439,42 +1025,17 @@ function createScreen(options = {}) {
439
1025
  prevCol = cols - 1;
440
1026
  }
441
1027
  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;
1028
+ if (prevCol > 0 && row.getChar(prevCol) === "" && row.isWide(prevCol - 1)) prevCol--;
1029
+ if (row.isEmpty(prevCol)) return null;
448
1030
  return {
449
- cell,
450
- col: prevCol
1031
+ row,
1032
+ col: prevCol,
1033
+ rowIdx: prevRow
451
1034
  };
452
1035
  }
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
- }
1036
+ /** Widen the cell at `col` to 2 columns, blanking the trailing spacer at col+1. */
1037
+ function widenCell(row, col) {
1038
+ row.widen(col);
478
1039
  }
479
1040
  function writeChar(ch) {
480
1041
  if (charsetG0 && ch.length === 1) {
@@ -484,10 +1045,10 @@ function createScreen(options = {}) {
484
1045
  const codePoint = ch.codePointAt(0) ?? 0;
485
1046
  if (isVS16(codePoint)) {
486
1047
  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);
1048
+ if (prev && !prev.row.isWide(prev.col)) {
1049
+ prev.row.appendChar(prev.col, ch);
1050
+ widenCell(prev.row, prev.col);
1051
+ markScreenRowDirty(prev.rowIdx);
491
1052
  curX = prev.col + 2;
492
1053
  if (curX >= cols) curX = cols - 1;
493
1054
  }
@@ -495,18 +1056,25 @@ function createScreen(options = {}) {
495
1056
  }
496
1057
  if (isCombining(codePoint) && !isVS16(codePoint)) {
497
1058
  const prev = getPrevCell();
498
- if (prev) prev.cell.char += ch;
1059
+ if (prev) {
1060
+ prev.row.appendChar(prev.col, ch);
1061
+ markScreenRowDirty(prev.rowIdx);
1062
+ }
499
1063
  return;
500
1064
  }
501
1065
  if (isEmojiModifier(codePoint)) {
502
1066
  const prev = getPrevCell();
503
- if (prev) prev.cell.char += ch;
1067
+ if (prev) {
1068
+ prev.row.appendChar(prev.col, ch);
1069
+ markScreenRowDirty(prev.rowIdx);
1070
+ }
504
1071
  return;
505
1072
  }
506
1073
  if (isZWJ(codePoint)) {
507
1074
  const prev = getPrevCell();
508
1075
  if (prev) {
509
- prev.cell.char += ch;
1076
+ prev.row.appendChar(prev.col, ch);
1077
+ markScreenRowDirty(prev.rowIdx);
510
1078
  afterZWJ = true;
511
1079
  }
512
1080
  return;
@@ -515,7 +1083,8 @@ function createScreen(options = {}) {
515
1083
  afterZWJ = false;
516
1084
  const prev = getPrevCell();
517
1085
  if (prev) {
518
- prev.cell.char += ch;
1086
+ prev.row.appendChar(prev.col, ch);
1087
+ markScreenRowDirty(prev.rowIdx);
519
1088
  return;
520
1089
  }
521
1090
  }
@@ -551,35 +1120,18 @@ function createScreen(options = {}) {
551
1120
  }
552
1121
  } else curX = wrapBoundary - charWidth;
553
1122
  if (insertMode) {
554
- const row = grid[curY];
555
1123
  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
- }
1124
+ mutateRowAsCells(grid[curY], (cells) => {
1125
+ for (let i = 0; i < charWidth; i++) {
1126
+ cells.splice(insertEnd - 1, 1);
1127
+ cells.splice(curX, 0, emptyCell());
1128
+ }
1129
+ });
560
1130
  }
561
1131
  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);
1132
+ row.writeFromAttrs(curX, ch, attrs, wide);
1133
+ if (wide) widenCell(row, curX);
1134
+ markScreenRowDirty(curY);
583
1135
  curX += charWidth;
584
1136
  lastChar = ch;
585
1137
  }
@@ -634,14 +1186,23 @@ function createScreen(options = {}) {
634
1186
  const charCode = parts[0] ?? 32;
635
1187
  if (charCode < 32 || charCode > 126) return;
636
1188
  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;
1189
+ const fillChar = String.fromCharCode(charCode);
1190
+ for (let row = t; row <= b && row < rows; row++) {
1191
+ const gr = grid[row];
1192
+ for (let col = l; col <= r && col < cols; col++) {
1193
+ const cell = emptyCell();
1194
+ cell.char = fillChar;
1195
+ gr.setCellRaw(col, cell);
1196
+ }
641
1197
  }
1198
+ markScreenRowsDirty(t, Math.min(b, rows - 1));
642
1199
  } else if (finalByte === "z" || finalByte === "{") {
643
1200
  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();
1201
+ for (let row = t; row <= b && row < rows; row++) {
1202
+ const gr = grid[row];
1203
+ for (let col = l; col <= r && col < cols; col++) gr.setEmpty(col);
1204
+ }
1205
+ markScreenRowsDirty(t, Math.min(b, rows - 1));
645
1206
  } else if (finalByte === "v") {
646
1207
  const src = normalizeRect(parts[0] ?? 1, parts[1] ?? 1, parts[2] ?? rows, parts[3] ?? cols);
647
1208
  const dstTop = Math.max(1, parts[5] ?? 1) - 1;
@@ -650,34 +1211,35 @@ function createScreen(options = {}) {
650
1211
  const w = src.r - src.l + 1;
651
1212
  const snapshot = [];
652
1213
  for (let row = 0; row < h; row++) {
1214
+ const srcRow = grid[src.t + row];
653
1215
  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
- }
1216
+ for (let col = 0; col < w; col++) line.push(srcRow ? srcRow.getCellRaw(src.l + col) : emptyCell());
658
1217
  snapshot.push(line);
659
1218
  }
660
1219
  for (let row = 0; row < h; row++) {
661
1220
  const dr = dstTop + row;
662
1221
  if (dr < 0 || dr >= rows) continue;
1222
+ const dstRow = grid[dr];
663
1223
  for (let col = 0; col < w; col++) {
664
1224
  const dc = dstLeft + col;
665
1225
  if (dc < 0 || dc >= cols) continue;
666
- grid[dr][dc] = snapshot[row][col];
1226
+ dstRow.setCellRaw(dc, snapshot[row][col]);
667
1227
  }
668
1228
  }
1229
+ markScreenRowsDirty(Math.max(0, dstTop), Math.min(dstTop + h - 1, rows - 1));
669
1230
  } else if (finalByte === "r" || finalByte === "t") {
670
1231
  const { t, l, b, r } = normalizeRect(parts[0] ?? 1, parts[1] ?? 1, parts[2] ?? rows, parts[3] ?? cols);
671
1232
  const sgrParts = parts.slice(4);
672
1233
  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;
1234
+ for (let row = t; row <= b && row < rows; row++) {
1235
+ const gr = grid[row];
1236
+ for (let col = l; col <= r && col < cols; col++) {
1237
+ const cell = gr.getCellRaw(col);
1238
+ applyRectAttrs(cell, sgrParts, reverse);
1239
+ gr.setCellRaw(col, cell);
678
1240
  }
679
- applyRectAttrs(cell, sgrParts, reverse);
680
1241
  }
1242
+ markScreenRowsDirty(t, Math.min(b, rows - 1));
681
1243
  }
682
1244
  return;
683
1245
  }
@@ -693,11 +1255,15 @@ function createScreen(options = {}) {
693
1255
  const b = Math.min(bArg, rows) - 1;
694
1256
  const r = Math.min(rArg, cols) - 1;
695
1257
  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;
1258
+ for (let row = t; row <= b && row < rows; row++) {
1259
+ const gr = grid[row];
1260
+ if (!gr) continue;
1261
+ for (let col = l; col <= r && col < cols; col++) {
1262
+ const ch = gr.getChar(col);
1263
+ if (ch) {
1264
+ const cp = ch.codePointAt(0) ?? 0;
1265
+ sum = sum + cp & 65535;
1266
+ }
701
1267
  }
702
1268
  }
703
1269
  const hex = sum.toString(16).toUpperCase().padStart(4, "0");
@@ -916,6 +1482,13 @@ function createScreen(options = {}) {
916
1482
  if (onResponse) {
917
1483
  if (params === "" || params === "0") onResponse("\x1BP!|00000000\x1B\\");
918
1484
  }
1485
+ } else if (finalByte === "u") {
1486
+ const parts = params.split(";").map((s) => s === "" ? 0 : parseInt(s, 10));
1487
+ const flags = parts[0] ?? 0;
1488
+ const mode = parts[1] ?? 1;
1489
+ if (mode === 2) kittyKeyboardFlags |= flags;
1490
+ else if (mode === 3) kittyKeyboardFlags &= ~flags;
1491
+ else kittyKeyboardFlags = flags;
919
1492
  }
920
1493
  }
921
1494
  function handleCSIPrivate(params, intermediates, finalByte) {
@@ -1037,10 +1610,12 @@ function createScreen(options = {}) {
1037
1610
  useAltScreen = true;
1038
1611
  grid = altGrid;
1039
1612
  softWrapped = altSoftWrapped;
1613
+ markAllDirty();
1040
1614
  } else if (!set && useAltScreen) {
1041
1615
  useAltScreen = false;
1042
1616
  grid = mainGrid;
1043
1617
  softWrapped = mainSoftWrapped;
1618
+ markAllDirty();
1044
1619
  }
1045
1620
  break;
1046
1621
  case 66:
@@ -1072,6 +1647,7 @@ function createScreen(options = {}) {
1072
1647
  softWrapped = altSoftWrapped;
1073
1648
  curX = 0;
1074
1649
  curY = 0;
1650
+ markAllDirty();
1075
1651
  } else if (!set && useAltScreen) {
1076
1652
  useAltScreen = false;
1077
1653
  grid = mainGrid;
@@ -1079,6 +1655,7 @@ function createScreen(options = {}) {
1079
1655
  curX = savedCurX;
1080
1656
  curY = savedCurY;
1081
1657
  clampCursor();
1658
+ markAllDirty();
1082
1659
  }
1083
1660
  break;
1084
1661
  case 69:
@@ -1135,6 +1712,7 @@ function createScreen(options = {}) {
1135
1712
  case 3:
1136
1713
  for (let row = 0; row < rows; row++) eraseCells(row, 0, row, cols - 1);
1137
1714
  if (mode === 3) scrollback.length = 0;
1715
+ markAllDirty();
1138
1716
  break;
1139
1717
  }
1140
1718
  }
@@ -1156,11 +1734,8 @@ function createScreen(options = {}) {
1156
1734
  function eraseCells(row, startCol, _endRow, endCol) {
1157
1735
  const r = grid[row];
1158
1736
  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
- }
1737
+ for (let col = startCol; col <= endCol && col < cols; col++) r.eraseWithBg(col, attrs.bg);
1738
+ markScreenRowDirty(row);
1164
1739
  }
1165
1740
  function handleInsertLines(count) {
1166
1741
  if (curY < scrollTop || curY > scrollBottom) return;
@@ -1173,32 +1748,36 @@ function createScreen(options = {}) {
1173
1748
  function handleDeleteChars(count) {
1174
1749
  const row = grid[curY];
1175
1750
  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());
1751
+ mutateRowAsCells(row, (cells) => {
1752
+ if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
1753
+ for (let i = 0; i < count; i++) if (curX <= rightMargin) {
1754
+ cells.splice(curX, 1);
1755
+ cells.splice(rightMargin, 0, emptyCell());
1756
+ }
1757
+ } else for (let i = 0; i < count; i++) if (curX < cols) {
1758
+ cells.splice(curX, 1);
1759
+ cells.push(emptyCell());
1180
1760
  }
1181
- } else for (let i = 0; i < count; i++) if (curX < cols) {
1182
- row.splice(curX, 1);
1183
- row.push(emptyCell());
1184
- }
1761
+ });
1762
+ markScreenRowDirty(curY);
1185
1763
  }
1186
1764
  function handleInsertChars(count) {
1187
1765
  const row = grid[curY];
1188
1766
  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
- }
1767
+ mutateRowAsCells(row, (cells) => {
1768
+ if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) for (let i = 0; i < count; i++) {
1769
+ cells.splice(rightMargin, 1);
1770
+ cells.splice(curX, 0, emptyCell());
1771
+ }
1772
+ else for (let i = 0; i < count; i++) {
1773
+ cells.splice(curX, 0, emptyCell());
1774
+ cells.pop();
1775
+ }
1776
+ });
1777
+ markScreenRowDirty(curY);
1197
1778
  }
1198
1779
  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();
1780
+ eraseCells(curY, curX, curY, Math.min(curX + count - 1, cols - 1));
1202
1781
  }
1203
1782
  /** Bounds of the active scroll/margin rectangle (inclusive). */
1204
1783
  function activeRect() {
@@ -1216,9 +1795,11 @@ function createScreen(options = {}) {
1216
1795
  if (!r) continue;
1217
1796
  for (let col = left; col <= right; col++) {
1218
1797
  const src = col + count;
1219
- r[col] = src <= right ? r[src] : emptyCell();
1798
+ if (src <= right) r.copyCellFrom(col, r, src);
1799
+ else r.setEmpty(col);
1220
1800
  }
1221
1801
  }
1802
+ markScreenRowsDirty(top, bottom);
1222
1803
  }
1223
1804
  function handleShiftRight(count) {
1224
1805
  const { top, left, bottom, right } = activeRect();
@@ -1227,9 +1808,11 @@ function createScreen(options = {}) {
1227
1808
  if (!r) continue;
1228
1809
  for (let col = right; col >= left; col--) {
1229
1810
  const src = col - count;
1230
- r[col] = src >= left ? r[src] : emptyCell();
1811
+ if (src >= left) r.copyCellFrom(col, r, src);
1812
+ else r.setEmpty(col);
1231
1813
  }
1232
1814
  }
1815
+ markScreenRowsDirty(top, bottom);
1233
1816
  }
1234
1817
  function handleInsertColumn(count) {
1235
1818
  const { top, left, bottom, right } = activeRect();
@@ -1237,9 +1820,10 @@ function createScreen(options = {}) {
1237
1820
  for (let row = top; row <= bottom; row++) {
1238
1821
  const r = grid[row];
1239
1822
  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();
1823
+ for (let col = right; col >= curX + count; col--) r.copyCellFrom(col, r, col - count);
1824
+ for (let col = curX; col < curX + count && col <= right; col++) r.setEmpty(col);
1242
1825
  }
1826
+ markScreenRowsDirty(top, bottom);
1243
1827
  }
1244
1828
  function handleDeleteColumn(count) {
1245
1829
  const { top, left, bottom, right } = activeRect();
@@ -1247,9 +1831,10 @@ function createScreen(options = {}) {
1247
1831
  for (let row = top; row <= bottom; row++) {
1248
1832
  const r = grid[row];
1249
1833
  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();
1834
+ for (let col = curX; col + count <= right; col++) r.copyCellFrom(col, r, col + count);
1835
+ for (let col = right - count + 1; col <= right && col >= 0; col++) r.setEmpty(col);
1252
1836
  }
1837
+ markScreenRowsDirty(top, bottom);
1253
1838
  }
1254
1839
  /**
1255
1840
  * Apply (or reverse-toggle) a list of SGR attribute codes to a single cell.
@@ -1403,7 +1988,10 @@ function createScreen(options = {}) {
1403
1988
  case 35:
1404
1989
  case 36:
1405
1990
  case 37:
1406
- attrs.fg = { ...palette256[code - 30] };
1991
+ attrs.fg = {
1992
+ ...palette256[code - 30],
1993
+ index: code - 30
1994
+ };
1407
1995
  break;
1408
1996
  case 38: {
1409
1997
  const subs = subParams.get(i);
@@ -1430,7 +2018,10 @@ function createScreen(options = {}) {
1430
2018
  case 45:
1431
2019
  case 46:
1432
2020
  case 47:
1433
- attrs.bg = { ...palette256[code - 40] };
2021
+ attrs.bg = {
2022
+ ...palette256[code - 40],
2023
+ index: code - 40
2024
+ };
1434
2025
  break;
1435
2026
  case 48: {
1436
2027
  const subs = subParams.get(i);
@@ -1480,7 +2071,10 @@ function createScreen(options = {}) {
1480
2071
  case 95:
1481
2072
  case 96:
1482
2073
  case 97:
1483
- attrs.fg = { ...palette256[code - 90 + 8] };
2074
+ attrs.fg = {
2075
+ ...palette256[code - 90 + 8],
2076
+ index: code - 90 + 8
2077
+ };
1484
2078
  break;
1485
2079
  case 100:
1486
2080
  case 101:
@@ -1490,7 +2084,10 @@ function createScreen(options = {}) {
1490
2084
  case 105:
1491
2085
  case 106:
1492
2086
  case 107:
1493
- attrs.bg = { ...palette256[code - 100 + 8] };
2087
+ attrs.bg = {
2088
+ ...palette256[code - 100 + 8],
2089
+ index: code - 100 + 8
2090
+ };
1494
2091
  break;
1495
2092
  }
1496
2093
  i++;
@@ -1501,12 +2098,16 @@ function createScreen(options = {}) {
1501
2098
  const type = params[startIndex + 1];
1502
2099
  if (type === 5 && startIndex + 2 < params.length) {
1503
2100
  const idx = params[startIndex + 2];
2101
+ const entry = palette256[idx];
1504
2102
  return {
1505
- color: { ...palette256[idx] ?? {
2103
+ color: entry ? {
2104
+ ...entry,
2105
+ index: idx
2106
+ } : {
1506
2107
  r: 0,
1507
2108
  g: 0,
1508
2109
  b: 0
1509
- } },
2110
+ },
1510
2111
  nextIndex: startIndex + 3
1511
2112
  };
1512
2113
  } else if (type === 2 && startIndex + 4 < params.length) return {
@@ -1525,11 +2126,15 @@ function createScreen(options = {}) {
1525
2126
  const type = subs[1];
1526
2127
  if (type === 5 && subs.length >= 3) {
1527
2128
  const idx = subs[2];
1528
- return { ...palette256[idx] ?? {
2129
+ const entry = palette256[idx];
2130
+ return entry ? {
2131
+ ...entry,
2132
+ index: idx
2133
+ } : {
1529
2134
  r: 0,
1530
2135
  g: 0,
1531
2136
  b: 0
1532
- } };
2137
+ };
1533
2138
  } else if (type === 2) {
1534
2139
  if (subs.length >= 5) {
1535
2140
  if (subs.length >= 6) return {
@@ -1622,7 +2227,10 @@ function createScreen(options = {}) {
1622
2227
  if (isNaN(idx) || idx < 0 || idx > 4) continue;
1623
2228
  if (spec === "?") {
1624
2229
  const c = specialColors.get(idx);
1625
- if (onResponse) onResponse(`\x1b]5;${idx};${c ? formatColorResponse(c) : "rgb:0000/0000/0000"}\x1b\\`);
2230
+ if (onResponse) {
2231
+ const payload = c ? formatColorResponse(c) : "rgb:0000/0000/0000";
2232
+ onResponse(`\x1b]5;${idx};${payload}\x1b\\`);
2233
+ }
1626
2234
  } else {
1627
2235
  const c = parseColorSpec(spec);
1628
2236
  if (c) specialColors.set(idx, c);
@@ -1836,7 +2444,10 @@ function createScreen(options = {}) {
1836
2444
  if (clipSemi !== -1) {
1837
2445
  const data = value.substring(clipSemi + 1);
1838
2446
  if (data === "?") {
1839
- if (onResponse) onResponse(`\x1b]52;c;${btoa(clipboard)}\x1b\\`);
2447
+ if (onResponse) {
2448
+ const encoded = btoa(clipboard);
2449
+ onResponse(`\x1b]52;c;${encoded}\x1b\\`);
2450
+ }
1840
2451
  } else {
1841
2452
  try {
1842
2453
  clipboard = atob(data);
@@ -1938,8 +2549,10 @@ function createScreen(options = {}) {
1938
2549
  break;
1939
2550
  }
1940
2551
  case 5522:
1941
- if (value === "?" && onResponse) onResponse(`\x1b]5522;${btoa(advancedClipboard)}\x1b\\`);
1942
- else try {
2552
+ if (value === "?" && onResponse) {
2553
+ const encoded = btoa(advancedClipboard);
2554
+ onResponse(`\x1b]5522;${encoded}\x1b\\`);
2555
+ } else try {
1943
2556
  advancedClipboard = atob(value);
1944
2557
  } catch {
1945
2558
  advancedClipboard = "";
@@ -1968,8 +2581,10 @@ function createScreen(options = {}) {
1968
2581
  return;
1969
2582
  }
1970
2583
  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\\");
2584
+ if (data.substring(2) === "544e") {
2585
+ const hexValue = Array.from(new TextEncoder().encode("vterm")).map((b) => b.toString(16).padStart(2, "0")).join("");
2586
+ onResponse(`\x1bP1+r544e=${hexValue}\x1b\\`);
2587
+ } else onResponse("\x1BP0+r\x1B\\");
1973
2588
  return;
1974
2589
  }
1975
2590
  }
@@ -2015,6 +2630,7 @@ function createScreen(options = {}) {
2015
2630
  altGrid = makeGrid(cols, rows);
2016
2631
  grid = mainGrid;
2017
2632
  scrollback = [];
2633
+ scrollbackSoftWrapped = [];
2018
2634
  curX = 0;
2019
2635
  curY = 0;
2020
2636
  curVisible = true;
@@ -2086,14 +2702,155 @@ function createScreen(options = {}) {
2086
2702
  parserState = "ground";
2087
2703
  escBuf = "";
2088
2704
  oscBuf = "";
2705
+ dcsBuf = "";
2706
+ dcsStartRow = 0;
2707
+ dcsStartCol = 0;
2708
+ dcsReceivedLength = 0;
2709
+ dcsOverflow = false;
2089
2710
  apcBuf = "";
2711
+ apcStartRow = 0;
2712
+ apcStartCol = 0;
2713
+ apcReceivedLength = 0;
2714
+ apcOverflow = false;
2715
+ utf8PendingBytes = [];
2090
2716
  semanticZones = [];
2091
2717
  mainSoftWrapped = new Array(rows).fill(false);
2092
2718
  altSoftWrapped = new Array(rows).fill(false);
2093
2719
  softWrapped = mainSoftWrapped;
2720
+ markAllDirty();
2721
+ dirtyScrolled = 0;
2722
+ if (sigDamageActive) sigDirtyScrolled = 0;
2723
+ trimmedRowCount = 0;
2724
+ flushSignals();
2725
+ }
2726
+ function emitParserEvent(ev) {
2727
+ for (const listener of parserListeners) listener(ev);
2728
+ }
2729
+ function flushPrintRun() {
2730
+ if (printRunParts.length === 0) return;
2731
+ const text = printRunParts.join("");
2732
+ printRunParts = [];
2733
+ emitParserEvent({
2734
+ kind: "print",
2735
+ text
2736
+ });
2737
+ }
2738
+ function emitExecute(code) {
2739
+ if (parserListeners.size === 0) return;
2740
+ flushPrintRun();
2741
+ emitParserEvent({
2742
+ kind: "execute",
2743
+ code
2744
+ });
2745
+ }
2746
+ function emitCsiEvent(rawParams, intermediates, final) {
2747
+ if (parserListeners.size === 0) return;
2748
+ flushPrintRun();
2749
+ let prefix;
2750
+ let body = rawParams;
2751
+ const marker = body.charCodeAt(0);
2752
+ if (marker === 63 || marker === 62 || marker === 60 || marker === 61) {
2753
+ prefix = body[0];
2754
+ body = body.slice(1);
2755
+ }
2756
+ const ev = {
2757
+ kind: "csi",
2758
+ final,
2759
+ params: body.split(";").map((s) => s === "" ? 0 : parseInt(s, 10))
2760
+ };
2761
+ if (prefix !== void 0) ev.prefix = prefix;
2762
+ if (intermediates !== "") ev.intermediates = intermediates;
2763
+ emitParserEvent(ev);
2764
+ }
2765
+ function emitOscEvent(osc) {
2766
+ if (parserListeners.size === 0) return;
2767
+ const semi = osc.indexOf(";");
2768
+ const code = semi === -1 ? parseInt(osc, 10) : parseInt(osc.slice(0, semi), 10);
2769
+ if (Number.isNaN(code)) return;
2770
+ flushPrintRun();
2771
+ emitParserEvent({
2772
+ kind: "osc",
2773
+ code,
2774
+ data: semi === -1 ? "" : osc.slice(semi + 1)
2775
+ });
2776
+ }
2777
+ function emitEsc(final, intermediates) {
2778
+ if (parserListeners.size === 0) return;
2779
+ flushPrintRun();
2780
+ const ev = {
2781
+ kind: "esc",
2782
+ final
2783
+ };
2784
+ if (intermediates !== "") ev.intermediates = intermediates;
2785
+ emitParserEvent(ev);
2786
+ }
2787
+ function emitStringEvent(sequence, data, row, col) {
2788
+ if (parserListeners.size === 0) return;
2789
+ flushPrintRun();
2790
+ emitParserEvent({
2791
+ kind: sequence,
2792
+ data,
2793
+ row,
2794
+ col
2795
+ });
2796
+ }
2797
+ function emitStringOverflow(sequence, receivedLength, row, col) {
2798
+ if (parserListeners.size === 0) return;
2799
+ flushPrintRun();
2800
+ emitParserEvent({
2801
+ kind: "string-overflow",
2802
+ sequence,
2803
+ maxLength: maxStringSequenceLength,
2804
+ receivedLength,
2805
+ row,
2806
+ col
2807
+ });
2808
+ }
2809
+ function appendDCS(ch) {
2810
+ dcsReceivedLength += ch.length;
2811
+ if (dcsOverflow) return;
2812
+ if (dcsReceivedLength > maxStringSequenceLength) {
2813
+ dcsOverflow = true;
2814
+ dcsBuf = "";
2815
+ return;
2816
+ }
2817
+ dcsBuf += ch;
2818
+ }
2819
+ function appendAPC(ch) {
2820
+ apcReceivedLength += ch.length;
2821
+ if (apcOverflow) return;
2822
+ if (apcReceivedLength > maxStringSequenceLength) {
2823
+ apcOverflow = true;
2824
+ apcBuf = "";
2825
+ return;
2826
+ }
2827
+ apcBuf += ch;
2828
+ }
2829
+ function finishDCS() {
2830
+ if (dcsOverflow) emitStringOverflow("dcs", dcsReceivedLength, dcsStartRow, dcsStartCol);
2831
+ else {
2832
+ handleDCS(dcsBuf);
2833
+ emitStringEvent("dcs", dcsBuf, dcsStartRow, dcsStartCol);
2834
+ }
2835
+ dcsBuf = "";
2836
+ dcsReceivedLength = 0;
2837
+ dcsOverflow = false;
2838
+ }
2839
+ function finishAPC() {
2840
+ if (apcOverflow) emitStringOverflow("apc", apcReceivedLength, apcStartRow, apcStartCol);
2841
+ else {
2842
+ handleAPC(apcBuf);
2843
+ emitStringEvent("apc", apcBuf, apcStartRow, apcStartCol);
2844
+ }
2845
+ apcBuf = "";
2846
+ apcReceivedLength = 0;
2847
+ apcOverflow = false;
2848
+ }
2849
+ function emitOp(op) {
2850
+ for (const listener of opListeners) listener(op);
2094
2851
  }
2095
2852
  function process(data) {
2096
- const text = decoder.decode(data, { stream: true });
2853
+ const text = decodeInput(data);
2097
2854
  for (let i = 0; i < text.length; i++) {
2098
2855
  const ch = text[i];
2099
2856
  const code = text.charCodeAt(i);
@@ -2102,18 +2859,25 @@ function createScreen(options = {}) {
2102
2859
  if (code === 27) {
2103
2860
  parserState = "escape";
2104
2861
  escBuf = "";
2105
- } else if (code === 7) {} else if (code === 8) {
2862
+ } else if (code === 7) emitExecute(7);
2863
+ else if (code === 8) {
2106
2864
  if (curX > 0) curX--;
2107
- } else if (code === 9) curX = nextTabStop(curX);
2108
- else if (code === 10 || code === 11 || code === 12) {
2865
+ emitExecute(8);
2866
+ } else if (code === 9) {
2867
+ curX = nextTabStop(curX);
2868
+ emitExecute(9);
2869
+ } else if (code === 10 || code === 11 || code === 12) {
2109
2870
  softWrapped[curY] = false;
2110
2871
  curY++;
2111
2872
  if (curY > scrollBottom) {
2112
2873
  curY = scrollBottom;
2113
2874
  scrollUp(scrollTop, scrollBottom);
2114
2875
  }
2115
- } else if (code === 13) curX = 0;
2116
- else if (code >= 32) {
2876
+ emitExecute(code);
2877
+ } else if (code === 13) {
2878
+ curX = 0;
2879
+ emitExecute(13);
2880
+ } else if (code >= 32) {
2117
2881
  let char = ch;
2118
2882
  if (code >= 55296 && code <= 56319 && i + 1 < text.length) {
2119
2883
  const nextCode = text.charCodeAt(i + 1);
@@ -2123,6 +2887,7 @@ function createScreen(options = {}) {
2123
2887
  }
2124
2888
  }
2125
2889
  writeChar(char);
2890
+ if (parserListeners.size > 0) printRunParts.push(char);
2126
2891
  }
2127
2892
  break;
2128
2893
  case "escape":
@@ -2138,9 +2903,12 @@ function createScreen(options = {}) {
2138
2903
  dcsBuf = "";
2139
2904
  dcsStartRow = curY;
2140
2905
  dcsStartCol = curX;
2906
+ dcsReceivedLength = 0;
2907
+ dcsOverflow = false;
2141
2908
  } else if (ch === "c") {
2142
2909
  fullReset();
2143
2910
  parserState = "ground";
2911
+ emitEsc("c", "");
2144
2912
  } else if (ch === "D") {
2145
2913
  curY++;
2146
2914
  if (curY > scrollBottom) {
@@ -2148,6 +2916,7 @@ function createScreen(options = {}) {
2148
2916
  scrollUp(scrollTop, scrollBottom);
2149
2917
  }
2150
2918
  parserState = "ground";
2919
+ emitEsc("D", "");
2151
2920
  } else if (ch === "M") {
2152
2921
  curY--;
2153
2922
  if (curY < scrollTop) {
@@ -2155,6 +2924,7 @@ function createScreen(options = {}) {
2155
2924
  scrollDown(scrollTop, scrollBottom);
2156
2925
  }
2157
2926
  parserState = "ground";
2927
+ emitEsc("M", "");
2158
2928
  } else if (ch === "7") {
2159
2929
  savedState = {
2160
2930
  curX,
@@ -2170,6 +2940,7 @@ function createScreen(options = {}) {
2170
2940
  charsetG0
2171
2941
  };
2172
2942
  parserState = "ground";
2943
+ emitEsc("7", "");
2173
2944
  } else if (ch === "8") {
2174
2945
  curX = savedState.curX;
2175
2946
  curY = savedState.curY;
@@ -2184,6 +2955,7 @@ function createScreen(options = {}) {
2184
2955
  charsetG0 = savedState.charsetG0;
2185
2956
  clampCursor();
2186
2957
  parserState = "ground";
2958
+ emitEsc("8", "");
2187
2959
  } else if (ch === "E") {
2188
2960
  curX = 0;
2189
2961
  curY++;
@@ -2192,43 +2964,57 @@ function createScreen(options = {}) {
2192
2964
  scrollUp(scrollTop, scrollBottom);
2193
2965
  }
2194
2966
  parserState = "ground";
2967
+ emitEsc("E", "");
2195
2968
  } else if (ch === "H") {
2196
2969
  tabStops.add(curX);
2197
2970
  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 === "=") {
2971
+ emitEsc("H", "");
2972
+ } else if (ch === "#") {
2973
+ escIntermediate = "#";
2974
+ parserState = "escape_hash";
2975
+ } else if (ch === "(") {
2976
+ escIntermediate = "(";
2977
+ parserState = "escape_charset";
2978
+ } else if (ch === ")") {
2979
+ escIntermediate = ")";
2980
+ parserState = "escape_charset";
2981
+ } else if (ch === "=") {
2202
2982
  applicationKeypad = true;
2203
2983
  parserState = "ground";
2984
+ emitEsc("=", "");
2204
2985
  } else if (ch === ">") {
2205
2986
  applicationKeypad = false;
2206
2987
  parserState = "ground";
2988
+ emitEsc(">", "");
2207
2989
  } else if (ch === "_") {
2208
2990
  parserState = "apc";
2209
2991
  apcBuf = "";
2992
+ apcStartRow = curY;
2993
+ apcStartCol = curX;
2994
+ apcReceivedLength = 0;
2995
+ apcOverflow = false;
2210
2996
  } else parserState = "ground";
2211
2997
  break;
2212
2998
  case "escape_charset":
2213
2999
  if (ch === "0") charsetG0 = true;
2214
3000
  else charsetG0 = false;
2215
3001
  parserState = "ground";
3002
+ emitEsc(ch, escIntermediate);
2216
3003
  break;
2217
3004
  case "escape_hash":
2218
3005
  if (ch === "8") {
3006
+ const alignCell = emptyCell();
3007
+ alignCell.char = "E";
2219
3008
  for (let r = 0; r < rows; r++) {
2220
3009
  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
- }
3010
+ for (let c = 0; c < cols; c++) row.setCellRaw(c, alignCell);
2226
3011
  softWrapped[r] = false;
2227
3012
  }
2228
3013
  curX = 0;
2229
3014
  curY = 0;
2230
3015
  }
2231
3016
  parserState = "ground";
3017
+ emitEsc(ch, escIntermediate);
2232
3018
  break;
2233
3019
  case "csi":
2234
3020
  if (code >= 64 && code <= 126) {
@@ -2250,6 +3036,7 @@ function createScreen(options = {}) {
2250
3036
  else if (paramPart.startsWith("=")) handleCSIEq(paramPart.substring(1), intermediatePart, ch);
2251
3037
  else handleCSI(paramPart, intermediatePart, ch);
2252
3038
  parserState = "ground";
3039
+ emitCsiEvent(paramPart, intermediatePart, ch);
2253
3040
  } else if (escBuf.length >= 256) parserState = "ground";
2254
3041
  else escBuf += ch;
2255
3042
  break;
@@ -2257,23 +3044,32 @@ function createScreen(options = {}) {
2257
3044
  if (code === 7) {
2258
3045
  handleOSC(oscBuf);
2259
3046
  parserState = "ground";
3047
+ emitOscEvent(oscBuf);
2260
3048
  } else if (code === 27) parserState = "osc_st";
2261
3049
  else if (oscBuf.length >= 4096) parserState = "ground";
2262
3050
  else oscBuf += ch;
2263
3051
  break;
2264
- case "osc_st":
2265
- if (ch === "\\") handleOSC(oscBuf);
3052
+ case "osc_st": {
3053
+ const oscComplete = ch === "\\";
3054
+ if (oscComplete) handleOSC(oscBuf);
2266
3055
  parserState = "ground";
3056
+ if (oscComplete) emitOscEvent(oscBuf);
2267
3057
  break;
3058
+ }
2268
3059
  case "dcs":
2269
3060
  if (code === 27) parserState = "dcs_st";
2270
3061
  else if (code === 7) {
2271
- handleDCS(dcsBuf);
3062
+ finishDCS();
2272
3063
  parserState = "ground";
2273
- } else dcsBuf += ch;
3064
+ } else appendDCS(ch);
2274
3065
  break;
2275
3066
  case "dcs_st":
2276
- if (ch === "\\") handleDCS(dcsBuf);
3067
+ if (ch === "\\") finishDCS();
3068
+ else {
3069
+ dcsBuf = "";
3070
+ dcsReceivedLength = 0;
3071
+ dcsOverflow = false;
3072
+ }
2277
3073
  parserState = "ground";
2278
3074
  break;
2279
3075
  case "dcs_passthrough":
@@ -2282,16 +3078,27 @@ function createScreen(options = {}) {
2282
3078
  case "apc":
2283
3079
  if (code === 27) parserState = "apc_st";
2284
3080
  else if (code === 7) {
2285
- handleAPC(apcBuf);
3081
+ finishAPC();
2286
3082
  parserState = "ground";
2287
- } else apcBuf += ch;
3083
+ } else appendAPC(ch);
2288
3084
  break;
2289
3085
  case "apc_st":
2290
- if (ch === "\\") handleAPC(apcBuf);
3086
+ if (ch === "\\") finishAPC();
3087
+ else {
3088
+ apcBuf = "";
3089
+ apcReceivedLength = 0;
3090
+ apcOverflow = false;
3091
+ }
2291
3092
  parserState = "ground";
2292
3093
  break;
2293
3094
  }
2294
3095
  }
3096
+ flushPrintRun();
3097
+ if (opListeners.size > 0) emitOp({
3098
+ type: "output",
3099
+ data
3100
+ });
3101
+ flushSignals();
2295
3102
  }
2296
3103
  /**
2297
3104
  * Reconstruct logical lines from a grid, joining rows that were soft-wrapped.
@@ -2303,7 +3110,7 @@ function createScreen(options = {}) {
2303
3110
  for (let r = 0; r < srcRows; r++) {
2304
3111
  const row = srcGrid[r];
2305
3112
  if (!row) continue;
2306
- for (let c = 0; c < row.length; c++) currentLine.push(row[c]);
3113
+ for (let c = 0; c < row.length; c++) currentLine.push(row.getCellRaw(c));
2307
3114
  if (srcSoftWrapped[r]) continue;
2308
3115
  logical.push(currentLine);
2309
3116
  currentLine = [];
@@ -2313,20 +3120,34 @@ function createScreen(options = {}) {
2313
3120
  }
2314
3121
  /**
2315
3122
  * Re-wrap logical lines to a new column width, producing grid rows and soft-wrap flags.
3123
+ *
3124
+ * `track` names one position — a logical line index plus a cell offset
3125
+ * within that line (the cursor, expressed content-relative) — and the
3126
+ * returned `tracked` reports the output row/col it lands on, so `resize()`
3127
+ * can move the cursor WITH its logical line instead of leaving it at its
3128
+ * old absolute row. An offset at/beyond the trimmed line end (cursor in
3129
+ * trailing blanks or one past the last cell) lands after the last emitted
3130
+ * cell, advanced by the leftover distance, clamped to the row.
2316
3131
  */
2317
- function rewrapLines(logicalLines, newCols) {
3132
+ function rewrapLines(logicalLines, newCols, track) {
2318
3133
  const outRows = [];
2319
3134
  const outWrapped = [];
2320
- for (const line of logicalLines) {
3135
+ let tracked = null;
3136
+ for (let li = 0; li < logicalLines.length; li++) {
3137
+ const line = logicalLines[li];
2321
3138
  let lineLen = line.length;
2322
3139
  while (lineLen > 0) {
2323
3140
  const cell = line[lineLen - 1];
2324
- if (cell === EMPTY_CELL || cell.char === "" && !cell.wide) lineLen--;
3141
+ if (cell.char === "" && !cell.wide) lineLen--;
2325
3142
  else break;
2326
3143
  }
2327
3144
  if (lineLen === 0) {
2328
3145
  outRows.push(makeRow(newCols));
2329
3146
  outWrapped.push(false);
3147
+ if (track !== void 0 && track.line === li) tracked = {
3148
+ row: outRows.length - 1,
3149
+ col: Math.min(track.offset, newCols - 1)
3150
+ };
2330
3151
  continue;
2331
3152
  }
2332
3153
  let pos = 0;
@@ -2336,34 +3157,65 @@ function createScreen(options = {}) {
2336
3157
  while (col < newCols && pos < lineLen) {
2337
3158
  const cell = line[pos];
2338
3159
  if (cell.wide && col + 2 > newCols) break;
2339
- row[col] = cell === EMPTY_CELL ? EMPTY_CELL : { ...cell };
3160
+ if (track !== void 0 && track.line === li && pos === track.offset) tracked = {
3161
+ row: outRows.length,
3162
+ col
3163
+ };
3164
+ row.setCellRaw(col, cell);
2340
3165
  col++;
2341
3166
  pos++;
2342
3167
  }
2343
3168
  const moreContent = pos < lineLen;
2344
3169
  outRows.push(row);
2345
3170
  outWrapped.push(moreContent);
3171
+ if (!moreContent && track !== void 0 && track.line === li && tracked === null) {
3172
+ const extra = track.offset - lineLen;
3173
+ tracked = {
3174
+ row: outRows.length - 1,
3175
+ col: Math.min(newCols - 1, col + extra)
3176
+ };
3177
+ }
2346
3178
  }
2347
3179
  }
2348
3180
  return {
2349
3181
  rows: outRows,
2350
- wrapped: outWrapped
3182
+ wrapped: outWrapped,
3183
+ tracked
2351
3184
  };
2352
3185
  }
3186
+ /** True when a packed row has no printable content (all columns blank). */
3187
+ function packedRowIsBlank(row) {
3188
+ for (let c = 0; c < row.length; c++) if (!(row.getChar(c) === "" && !row.isWide(c))) return false;
3189
+ return true;
3190
+ }
2353
3191
  /**
2354
3192
  * Trim trailing empty rows from reflowed result, so they don't push content off the top
2355
3193
  * when we take the last newRows rows.
2356
3194
  */
2357
3195
  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;
3196
+ while (result.rows.length > 1) {
3197
+ const lastRow = result.rows[result.rows.length - 1];
3198
+ if (packedRowIsBlank(lastRow) && !result.wrapped[result.rows.length - 2]) {
3199
+ result.rows.pop();
3200
+ result.wrapped.pop();
3201
+ } else break;
3202
+ }
2362
3203
  }
2363
3204
  function resize(newCols, newRows) {
2364
- const mainResult = rewrapLines(getLogicalLines(mainGrid, mainSoftWrapped, rows), newCols);
3205
+ if (newCols === cols && newRows === rows) return;
3206
+ const activeWrappedPre = useAltScreen ? altSoftWrapped : mainSoftWrapped;
3207
+ const cursorRowPre = Math.min(Math.max(curY, 0), rows - 1);
3208
+ let cursorLineStart = cursorRowPre;
3209
+ while (cursorLineStart > 0 && activeWrappedPre[cursorLineStart - 1]) cursorLineStart--;
3210
+ let cursorLineIdx = 0;
3211
+ for (let r = 0; r < cursorLineStart; r++) if (!activeWrappedPre[r]) cursorLineIdx++;
3212
+ const cursorTrack = {
3213
+ line: cursorLineIdx,
3214
+ offset: (cursorRowPre - cursorLineStart) * cols + curX
3215
+ };
3216
+ const mainResult = rewrapLines(getLogicalLines(mainGrid, mainSoftWrapped, rows), newCols, useAltScreen ? void 0 : cursorTrack);
2365
3217
  trimTrailingEmptyRows(mainResult);
2366
- const altResult = rewrapLines(getLogicalLines(altGrid, altSoftWrapped, rows), newCols);
3218
+ const altResult = rewrapLines(getLogicalLines(altGrid, altSoftWrapped, rows), newCols, useAltScreen ? cursorTrack : void 0);
2367
3219
  trimTrailingEmptyRows(altResult);
2368
3220
  const newMain = makeGrid(newCols, newRows);
2369
3221
  const newMainWrapped = new Array(newRows).fill(false);
@@ -2379,6 +3231,12 @@ function createScreen(options = {}) {
2379
3231
  newAlt[r] = altResult.rows[altStartRow + r];
2380
3232
  newAltWrapped[r] = altResult.wrapped[altStartRow + r];
2381
3233
  }
3234
+ const activeResult = useAltScreen ? altResult : mainResult;
3235
+ const activeStartRow = useAltScreen ? altStartRow : mainStartRow;
3236
+ if (activeResult.tracked !== null) {
3237
+ curY = Math.min(activeResult.tracked.row, activeResult.rows.length) - activeStartRow;
3238
+ curX = activeResult.tracked.col;
3239
+ }
2382
3240
  mainGrid = newMain;
2383
3241
  altGrid = newAlt;
2384
3242
  mainSoftWrapped = newMainWrapped;
@@ -2394,31 +3252,279 @@ function createScreen(options = {}) {
2394
3252
  rightMargin = cols - 1;
2395
3253
  if (newCols > oldCols) for (let i = Math.max(8, Math.ceil(oldCols / 8) * 8); i < newCols; i += 8) tabStops.add(i);
2396
3254
  clampCursor();
3255
+ markAllDirty();
3256
+ if (opListeners.size > 0) emitOp({
3257
+ type: "resize",
3258
+ cols: newCols,
3259
+ rows: newRows
3260
+ });
3261
+ flushSignals();
3262
+ }
3263
+ function snapshot() {
3264
+ return {
3265
+ version: 1,
3266
+ cols,
3267
+ rows,
3268
+ scrollbackLimit,
3269
+ activeBuffer: useAltScreen ? "alt" : "main",
3270
+ main: {
3271
+ grid: cloneGridSnapshot(mainGrid),
3272
+ softWrapped: [...mainSoftWrapped]
3273
+ },
3274
+ alt: {
3275
+ grid: cloneGridSnapshot(altGrid),
3276
+ softWrapped: [...altSoftWrapped]
3277
+ },
3278
+ scrollback: cloneGridSnapshot(scrollback),
3279
+ scrollbackSoftWrapped: [...scrollbackSoftWrapped],
3280
+ cursor: {
3281
+ x: curX,
3282
+ y: curY,
3283
+ visible: curVisible,
3284
+ shape: cursorShape,
3285
+ blinking: cursorBlinking,
3286
+ savedX: savedCurX,
3287
+ savedY: savedCurY
3288
+ },
3289
+ savedState: {
3290
+ x: savedState.curX,
3291
+ y: savedState.curY,
3292
+ attrs: cloneAttrsSnapshot(savedState.attrs),
3293
+ originMode: savedState.originMode,
3294
+ autoWrap: savedState.autoWrap,
3295
+ charsetG0: savedState.charsetG0
3296
+ },
3297
+ attrs: cloneAttrsSnapshot(attrs),
3298
+ modes: {
3299
+ bracketedPaste,
3300
+ applicationCursor,
3301
+ applicationKeypad,
3302
+ autoWrap,
3303
+ mouseTracking,
3304
+ mouseTrackingMode,
3305
+ sgrMouse,
3306
+ focusTracking,
3307
+ origin: originMode,
3308
+ insert: insertMode,
3309
+ reverseVideo,
3310
+ syncOutput,
3311
+ kittyKeyboardFlags,
3312
+ kittyKeyboardStack: [...kittyKeyboardStack],
3313
+ kittyGraphics: hasKittyGraphics,
3314
+ colorSchemeReporting,
3315
+ decColumn: decColumnMode,
3316
+ altScroll: altScrollMode,
3317
+ utf8Mouse: utf8MouseMode
3318
+ },
3319
+ margins: {
3320
+ scrollTop,
3321
+ scrollBottom,
3322
+ leftRight: leftRightMarginMode,
3323
+ left: leftMargin,
3324
+ right: rightMargin
3325
+ },
3326
+ colors: {
3327
+ current: snapshotColorState(),
3328
+ stack: colorStack.map(cloneColorStateSnapshot)
3329
+ },
3330
+ tabStops: [...tabStops].sort((a, b) => a - b),
3331
+ title,
3332
+ clipboard,
3333
+ cwd,
3334
+ notifications: [...notifications],
3335
+ viewportOffset,
3336
+ parser: {
3337
+ state: parserState,
3338
+ esc: escBuf,
3339
+ osc: oscBuf,
3340
+ dcs: dcsBuf,
3341
+ dcsStart: {
3342
+ row: dcsStartRow,
3343
+ col: dcsStartCol
3344
+ },
3345
+ dcsReceivedLength,
3346
+ dcsOverflow,
3347
+ apc: apcBuf,
3348
+ apcStart: {
3349
+ row: apcStartRow,
3350
+ col: apcStartCol
3351
+ },
3352
+ apcReceivedLength,
3353
+ apcOverflow,
3354
+ utf8PendingBytes: [...utf8PendingBytes]
3355
+ },
3356
+ unicode: {
3357
+ charsetG0,
3358
+ lastChar,
3359
+ pendingRegionalIndicator,
3360
+ afterZWJ
3361
+ }
3362
+ };
3363
+ }
3364
+ function restore(snapshotValue) {
3365
+ assertSnapshot(snapshotValue);
3366
+ cols = snapshotValue.cols;
3367
+ rows = snapshotValue.rows;
3368
+ scrollbackLimit = snapshotValue.scrollbackLimit;
3369
+ mainGrid = restoreGridSnapshot(snapshotValue.main.grid, rows, cols);
3370
+ altGrid = restoreGridSnapshot(snapshotValue.alt.grid, rows, cols);
3371
+ scrollback = restoreScrollbackSnapshot(snapshotValue.scrollback);
3372
+ scrollbackSoftWrapped = snapshotValue.scrollbackSoftWrapped ? [...snapshotValue.scrollbackSoftWrapped] : new Array(scrollback.length).fill(false);
3373
+ mainSoftWrapped = [...snapshotValue.main.softWrapped];
3374
+ altSoftWrapped = [...snapshotValue.alt.softWrapped];
3375
+ useAltScreen = snapshotValue.activeBuffer === "alt";
3376
+ grid = useAltScreen ? altGrid : mainGrid;
3377
+ softWrapped = useAltScreen ? altSoftWrapped : mainSoftWrapped;
3378
+ curX = snapshotValue.cursor.x;
3379
+ curY = snapshotValue.cursor.y;
3380
+ curVisible = snapshotValue.cursor.visible;
3381
+ cursorShape = snapshotValue.cursor.shape;
3382
+ cursorBlinking = snapshotValue.cursor.blinking;
3383
+ savedCurX = snapshotValue.cursor.savedX;
3384
+ savedCurY = snapshotValue.cursor.savedY;
3385
+ savedState = {
3386
+ curX: snapshotValue.savedState.x,
3387
+ curY: snapshotValue.savedState.y,
3388
+ attrs: cloneAttrsSnapshot(snapshotValue.savedState.attrs),
3389
+ originMode: snapshotValue.savedState.originMode,
3390
+ autoWrap: snapshotValue.savedState.autoWrap,
3391
+ charsetG0: snapshotValue.savedState.charsetG0
3392
+ };
3393
+ attrs = cloneAttrsSnapshot(snapshotValue.attrs);
3394
+ bracketedPaste = snapshotValue.modes.bracketedPaste;
3395
+ applicationCursor = snapshotValue.modes.applicationCursor;
3396
+ applicationKeypad = snapshotValue.modes.applicationKeypad;
3397
+ autoWrap = snapshotValue.modes.autoWrap;
3398
+ mouseTracking = snapshotValue.modes.mouseTracking;
3399
+ mouseTrackingMode = snapshotValue.modes.mouseTrackingMode;
3400
+ sgrMouse = snapshotValue.modes.sgrMouse;
3401
+ focusTracking = snapshotValue.modes.focusTracking;
3402
+ originMode = snapshotValue.modes.origin;
3403
+ insertMode = snapshotValue.modes.insert;
3404
+ reverseVideo = snapshotValue.modes.reverseVideo;
3405
+ syncOutput = snapshotValue.modes.syncOutput;
3406
+ kittyKeyboardFlags = snapshotValue.modes.kittyKeyboardFlags;
3407
+ kittyKeyboardStack = [...snapshotValue.modes.kittyKeyboardStack];
3408
+ hasKittyGraphics = snapshotValue.modes.kittyGraphics;
3409
+ colorSchemeReporting = snapshotValue.modes.colorSchemeReporting;
3410
+ decColumnMode = snapshotValue.modes.decColumn;
3411
+ altScrollMode = snapshotValue.modes.altScroll;
3412
+ utf8MouseMode = snapshotValue.modes.utf8Mouse;
3413
+ scrollTop = snapshotValue.margins.scrollTop;
3414
+ scrollBottom = snapshotValue.margins.scrollBottom;
3415
+ leftRightMarginMode = snapshotValue.margins.leftRight;
3416
+ leftMargin = snapshotValue.margins.left;
3417
+ rightMargin = snapshotValue.margins.right;
3418
+ restoreColorState(snapshotValue.colors.current);
3419
+ colorStack.length = 0;
3420
+ colorStack.push(...snapshotValue.colors.stack.map(cloneColorStateSnapshot));
3421
+ tabStops = new Set(snapshotValue.tabStops);
3422
+ title = snapshotValue.title;
3423
+ clipboard = snapshotValue.clipboard;
3424
+ cwd = snapshotValue.cwd;
3425
+ notifications = [...snapshotValue.notifications];
3426
+ viewportOffset = Math.max(0, Math.min(scrollback.length, snapshotValue.viewportOffset));
3427
+ parserState = snapshotValue.parser.state;
3428
+ escBuf = snapshotValue.parser.esc;
3429
+ oscBuf = snapshotValue.parser.osc;
3430
+ dcsBuf = snapshotValue.parser.dcs;
3431
+ dcsStartRow = snapshotValue.parser.dcsStart.row;
3432
+ dcsStartCol = snapshotValue.parser.dcsStart.col;
3433
+ dcsReceivedLength = snapshotValue.parser.dcsReceivedLength ?? dcsBuf.length;
3434
+ dcsOverflow = snapshotValue.parser.dcsOverflow ?? false;
3435
+ apcBuf = snapshotValue.parser.apc;
3436
+ apcStartRow = snapshotValue.parser.apcStart?.row ?? curY;
3437
+ apcStartCol = snapshotValue.parser.apcStart?.col ?? curX;
3438
+ apcReceivedLength = snapshotValue.parser.apcReceivedLength ?? apcBuf.length;
3439
+ apcOverflow = snapshotValue.parser.apcOverflow ?? false;
3440
+ utf8PendingBytes = [...snapshotValue.parser.utf8PendingBytes];
3441
+ charsetG0 = snapshotValue.unicode.charsetG0;
3442
+ lastChar = snapshotValue.unicode.lastChar;
3443
+ pendingRegionalIndicator = snapshotValue.unicode.pendingRegionalIndicator;
3444
+ afterZWJ = snapshotValue.unicode.afterZWJ;
3445
+ textScale = 1;
3446
+ fontSize = 12;
3447
+ fontWindowSize = 12;
3448
+ locale = "en_US.UTF-8";
3449
+ advancedClipboard = "";
3450
+ semanticZones = [];
3451
+ hasSixel = false;
3452
+ sixelImages = [];
3453
+ if (curX < 0) curX = 0;
3454
+ if (curX > cols) curX = cols;
3455
+ if (curY < 0) curY = 0;
3456
+ if (curY >= rows) curY = rows - 1;
3457
+ markAllDirty();
3458
+ dirtyScrolled = 0;
3459
+ if (sigDamageActive) sigDirtyScrolled = 0;
3460
+ trimmedRowCount = 0;
3461
+ flushSignals();
2397
3462
  }
2398
3463
  function getCell(row, col) {
2399
3464
  const r = grid[row];
2400
3465
  if (!r || col >= cols) return emptyCell();
2401
- return { ...r[col] };
3466
+ return stripCellColorIndex(r.getCellRaw(col));
2402
3467
  }
2403
- function getLine(row) {
3468
+ function getRow(row) {
2404
3469
  const r = grid[row];
2405
- if (!r) return makeRow(cols);
2406
- return r.map((cell) => ({ ...cell }));
3470
+ if (!r) return makeRow(cols).toCells();
3471
+ return r.toCells().map(stripCellColorIndex);
2407
3472
  }
2408
3473
  function getText() {
2409
3474
  const lines = [];
2410
3475
  for (let r = 0; r < rows; r++) lines.push(rowToString(grid[r]));
2411
3476
  return lines.join("\n");
2412
3477
  }
3478
+ function getScrollbackText() {
3479
+ const lines = [];
3480
+ for (const row of scrollback) lines.push(rowToString(row));
3481
+ return lines.join("\n");
3482
+ }
3483
+ function totalRows() {
3484
+ return scrollback.length + rows;
3485
+ }
3486
+ function screenRows() {
3487
+ return rows;
3488
+ }
3489
+ function viewportTop() {
3490
+ return scrollback.length - viewportOffset;
3491
+ }
3492
+ function getRowAbsolute(row) {
3493
+ if (row < 0 || row >= scrollback.length + rows) return makeRow(cols).toCells();
3494
+ return (row < scrollback.length ? scrollback[row] : grid[row - scrollback.length]).toCells().map(stripCellColorIndex);
3495
+ }
3496
+ function firstRetainedRow() {
3497
+ return trimmedRowCount;
3498
+ }
3499
+ function takeDirty() {
3500
+ const cursorChanged = curX !== dirtyCursor.x || curY !== dirtyCursor.y || curVisible !== dirtyCursor.visible || cursorShape !== dirtyCursor.shape || cursorBlinking !== dirtyCursor.blinking;
3501
+ const region = {
3502
+ rows: dirtyAll ? "all" : dirtyRows,
3503
+ cursor: cursorChanged,
3504
+ scrolled: dirtyScrolled
3505
+ };
3506
+ dirtyRows = /* @__PURE__ */ new Set();
3507
+ dirtyAll = false;
3508
+ dirtyScrolled = 0;
3509
+ dirtyLastAbs = -1;
3510
+ dirtyCursor = {
3511
+ x: curX,
3512
+ y: curY,
3513
+ visible: curVisible,
3514
+ shape: cursorShape,
3515
+ blinking: cursorBlinking
3516
+ };
3517
+ return region;
3518
+ }
2413
3519
  function rowToString(row) {
2414
3520
  let line = "";
2415
3521
  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;
3522
+ const ch = row.getChar(i);
3523
+ if (row.isWide(i)) line += ch;
3524
+ else if (ch === "") {
3525
+ if (i > 0 && row.isWide(i - 1)) continue;
2420
3526
  line += " ";
2421
- } else line += cell.char;
3527
+ } else line += ch;
2422
3528
  }
2423
3529
  return line.replace(/\s+$/, "");
2424
3530
  }
@@ -2431,10 +3537,10 @@ function createScreen(options = {}) {
2431
3537
  const colEnd = row === endRow ? endCol : cols;
2432
3538
  let line = "";
2433
3539
  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 || " ";
3540
+ if (col < 0 || col >= r.length) continue;
3541
+ const ch = r.getChar(col);
3542
+ if (ch === "" && col > 0 && r.isWide(col - 1)) continue;
3543
+ line += ch || " ";
2438
3544
  }
2439
3545
  parts.push(line.replace(/\s+$/, ""));
2440
3546
  }
@@ -2464,6 +3570,176 @@ function createScreen(options = {}) {
2464
3570
  default: return false;
2465
3571
  }
2466
3572
  }
3573
+ function apply(op) {
3574
+ switch (op.type) {
3575
+ case "output":
3576
+ process(typeof op.data === "string" ? encoder.encode(op.data) : op.data);
3577
+ return;
3578
+ case "resize":
3579
+ resize(op.cols, op.rows);
3580
+ return;
3581
+ default: throw new Error(`vterm.apply: unhandled op ${JSON.stringify(op)}`);
3582
+ }
3583
+ }
3584
+ function tapOps(listener) {
3585
+ opListeners.add(listener);
3586
+ return () => {
3587
+ opListeners.delete(listener);
3588
+ };
3589
+ }
3590
+ function tapParser(listener) {
3591
+ parserListeners.add(listener);
3592
+ return () => {
3593
+ parserListeners.delete(listener);
3594
+ };
3595
+ }
3596
+ const stateFlushSignals = [];
3597
+ /**
3598
+ * A state signal (title/modes/cursor/size): `get()` reads live; `subscribe` captures the current
3599
+ * value as its baseline (so only later changes deliver) and `flush()` delivers once per flush
3600
+ * boundary iff the value changed. Registered in `stateFlushSignals` so `flushSignals` visits it.
3601
+ */
3602
+ function makeStateSignal(read, eq) {
3603
+ const listeners = /* @__PURE__ */ new Set();
3604
+ let last = read();
3605
+ const signal = {
3606
+ get: read,
3607
+ subscribe(listener) {
3608
+ if (listeners.size === 0) last = read();
3609
+ listeners.add(listener);
3610
+ return () => {
3611
+ listeners.delete(listener);
3612
+ };
3613
+ },
3614
+ flush() {
3615
+ if (listeners.size === 0) return;
3616
+ const current = read();
3617
+ if (eq(current, last)) return;
3618
+ last = current;
3619
+ for (const listener of listeners) listener(current);
3620
+ }
3621
+ };
3622
+ stateFlushSignals.push(signal);
3623
+ return signal;
3624
+ }
3625
+ function modesNow() {
3626
+ return {
3627
+ altScreen: useAltScreen,
3628
+ cursorVisible: curVisible,
3629
+ bracketedPaste,
3630
+ applicationCursor,
3631
+ applicationKeypad,
3632
+ autoWrap,
3633
+ mouseTracking,
3634
+ mouseTrackingMode,
3635
+ sgrMouse,
3636
+ utf8Mouse: utf8MouseMode,
3637
+ focusTracking,
3638
+ originMode,
3639
+ insertMode,
3640
+ reverseVideo,
3641
+ syncOutput,
3642
+ leftRightMargin: leftRightMarginMode,
3643
+ colorSchemeReporting,
3644
+ kittyKeyboard: kittyKeyboardFlags > 0,
3645
+ kittyGraphics: hasKittyGraphics,
3646
+ sixel: hasSixel
3647
+ };
3648
+ }
3649
+ function modesEq(a, b) {
3650
+ return a.altScreen === b.altScreen && a.cursorVisible === b.cursorVisible && a.bracketedPaste === b.bracketedPaste && a.applicationCursor === b.applicationCursor && a.applicationKeypad === b.applicationKeypad && a.autoWrap === b.autoWrap && a.mouseTracking === b.mouseTracking && a.mouseTrackingMode === b.mouseTrackingMode && a.sgrMouse === b.sgrMouse && a.utf8Mouse === b.utf8Mouse && a.focusTracking === b.focusTracking && a.originMode === b.originMode && a.insertMode === b.insertMode && a.reverseVideo === b.reverseVideo && a.syncOutput === b.syncOutput && a.leftRightMargin === b.leftRightMargin && a.colorSchemeReporting === b.colorSchemeReporting && a.kittyKeyboard === b.kittyKeyboard && a.kittyGraphics === b.kittyGraphics && a.sixel === b.sixel;
3651
+ }
3652
+ const damageListeners = /* @__PURE__ */ new Set();
3653
+ let damageLast = {
3654
+ rows: /* @__PURE__ */ new Set(),
3655
+ cursor: false,
3656
+ scrolled: 0
3657
+ };
3658
+ let damageSignal;
3659
+ function getDamageSignal() {
3660
+ return damageSignal ??= {
3661
+ get: () => damageLast,
3662
+ subscribe(listener) {
3663
+ if (damageListeners.size === 0) {
3664
+ sigDamageActive = true;
3665
+ sigDirtyAll = false;
3666
+ sigDirtyRows = /* @__PURE__ */ new Set();
3667
+ sigDirtyScrolled = 0;
3668
+ sigDirtyLastAbs = -1;
3669
+ sigDirtyCursor = {
3670
+ x: curX,
3671
+ y: curY,
3672
+ visible: curVisible,
3673
+ shape: cursorShape,
3674
+ blinking: cursorBlinking
3675
+ };
3676
+ }
3677
+ damageListeners.add(listener);
3678
+ return () => {
3679
+ damageListeners.delete(listener);
3680
+ if (damageListeners.size === 0) sigDamageActive = false;
3681
+ };
3682
+ }
3683
+ };
3684
+ }
3685
+ function emitDamageBatch() {
3686
+ const cursorChanged = curX !== sigDirtyCursor.x || curY !== sigDirtyCursor.y || curVisible !== sigDirtyCursor.visible || cursorShape !== sigDirtyCursor.shape || cursorBlinking !== sigDirtyCursor.blinking;
3687
+ if (!sigDirtyAll && sigDirtyRows.size === 0 && sigDirtyScrolled === 0 && !cursorChanged) return;
3688
+ const region = {
3689
+ rows: sigDirtyAll ? "all" : sigDirtyRows,
3690
+ cursor: cursorChanged,
3691
+ scrolled: sigDirtyScrolled
3692
+ };
3693
+ sigDirtyRows = /* @__PURE__ */ new Set();
3694
+ sigDirtyAll = false;
3695
+ sigDirtyScrolled = 0;
3696
+ sigDirtyLastAbs = -1;
3697
+ sigDirtyCursor = {
3698
+ x: curX,
3699
+ y: curY,
3700
+ visible: curVisible,
3701
+ shape: cursorShape,
3702
+ blinking: cursorBlinking
3703
+ };
3704
+ damageLast = region;
3705
+ for (const listener of damageListeners) listener(region);
3706
+ }
3707
+ let signalsFacade;
3708
+ let titleSignal;
3709
+ let modesSignal;
3710
+ let cursorSignal;
3711
+ let sizeSignal;
3712
+ function getSignals() {
3713
+ return signalsFacade ??= {
3714
+ get title$() {
3715
+ return titleSignal ??= makeStateSignal(() => title, (a, b) => a === b);
3716
+ },
3717
+ get modes$() {
3718
+ return modesSignal ??= makeStateSignal(modesNow, modesEq);
3719
+ },
3720
+ get cursor$() {
3721
+ return cursorSignal ??= makeStateSignal(() => ({
3722
+ col: curX,
3723
+ row: curY
3724
+ }), (a, b) => a.col === b.col && a.row === b.row);
3725
+ },
3726
+ get size$() {
3727
+ return sizeSignal ??= makeStateSignal(() => ({
3728
+ cols,
3729
+ rows
3730
+ }), (a, b) => a.cols === b.cols && a.rows === b.rows);
3731
+ },
3732
+ get damage$() {
3733
+ return getDamageSignal();
3734
+ }
3735
+ };
3736
+ }
3737
+ /** Coalesced end-of-call emission for every accessed signal — the flush boundary (§4). */
3738
+ function flushSignals() {
3739
+ if (signalsFacade === void 0) return;
3740
+ for (const signal of stateFlushSignals) signal.flush();
3741
+ if (sigDamageActive) emitDamageBatch();
3742
+ }
2467
3743
  return {
2468
3744
  get cols() {
2469
3745
  return cols;
@@ -2471,16 +3747,31 @@ function createScreen(options = {}) {
2471
3747
  get rows() {
2472
3748
  return rows;
2473
3749
  },
3750
+ get signals() {
3751
+ return getSignals();
3752
+ },
2474
3753
  process,
2475
3754
  resize,
3755
+ apply,
3756
+ tapOps,
3757
+ tapParser,
2476
3758
  reset: fullReset,
3759
+ snapshot,
3760
+ restore,
2477
3761
  getCell,
2478
- getLine,
3762
+ getRow,
2479
3763
  getText,
3764
+ getScrollbackText,
2480
3765
  getTextRange,
2481
- getCursorPosition: () => ({
2482
- x: curX,
2483
- y: curY
3766
+ totalRows,
3767
+ screenRows,
3768
+ viewportTop,
3769
+ getRowAbsolute,
3770
+ firstRetainedRow,
3771
+ takeDirty,
3772
+ getCursor: () => ({
3773
+ col: curX,
3774
+ row: curY
2484
3775
  }),
2485
3776
  getCursorVisible: () => curVisible,
2486
3777
  getCursorShape: () => cursorShape,
@@ -2494,8 +3785,888 @@ function createScreen(options = {}) {
2494
3785
  getViewportOffset: () => viewportOffset,
2495
3786
  scrollViewport,
2496
3787
  getSemanticZones: () => semanticZones.map((z) => ({ ...z })),
2497
- getSixelImages: () => sixelImages.map((img) => ({ ...img }))
3788
+ getSixelImages: () => sixelImages.map((img) => ({ ...img })),
3789
+ serialize: (options) => serializeSnapshot(snapshot(), options)
3790
+ };
3791
+ }
3792
+ const SERIALIZE_DEFAULT_PEN = Object.freeze({
3793
+ fg: null,
3794
+ bg: null,
3795
+ bold: false,
3796
+ faint: false,
3797
+ italic: false,
3798
+ underline: "none",
3799
+ underlineColor: null,
3800
+ overline: false,
3801
+ strikethrough: false,
3802
+ inverse: false,
3803
+ hidden: false,
3804
+ blink: false
3805
+ });
3806
+ const SERIALIZE_BOOL_CODES = [
3807
+ ["bold", "1"],
3808
+ ["faint", "2"],
3809
+ ["italic", "3"],
3810
+ ["blink", "5"],
3811
+ ["inverse", "7"],
3812
+ ["hidden", "8"],
3813
+ ["strikethrough", "9"],
3814
+ ["overline", "53"]
3815
+ ];
3816
+ /** Colon-subparam underline forms — the lossless spellings vterm itself parses. */
3817
+ function serializeUnderlineParam(style) {
3818
+ switch (style) {
3819
+ case "single": return "4";
3820
+ case "double": return "4:2";
3821
+ case "curly": return "4:3";
3822
+ case "dotted": return "4:4";
3823
+ case "dashed": return "4:5";
3824
+ case "none": return "24";
3825
+ }
3826
+ }
3827
+ function serializeColorEq(a, b) {
3828
+ if (a === null || b === null) return a === b;
3829
+ return a.r === b.r && a.g === b.g && a.b === b.b && a.index === b.index;
3830
+ }
3831
+ function serializePenOf(source) {
3832
+ return {
3833
+ fg: source.fg,
3834
+ bg: source.bg,
3835
+ bold: source.bold,
3836
+ faint: source.faint,
3837
+ italic: source.italic,
3838
+ underline: source.underline,
3839
+ underlineColor: source.underlineColor,
3840
+ overline: source.overline,
3841
+ strikethrough: source.strikethrough,
3842
+ inverse: source.inverse,
3843
+ hidden: source.hidden,
3844
+ blink: source.blink
3845
+ };
3846
+ }
3847
+ function serializePenEq(a, b) {
3848
+ 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);
3849
+ }
3850
+ function serializePenIsDefault(pen) {
3851
+ return serializePenEq(pen, SERIALIZE_DEFAULT_PEN);
3852
+ }
3853
+ function serializeColorParams(prefix, defaultCode, color) {
3854
+ if (color === null) return defaultCode;
3855
+ const idx = color.index;
3856
+ if (idx !== void 0 && idx >= 0 && idx <= 255) {
3857
+ if (prefix === "38") {
3858
+ if (idx <= 7) return String(30 + idx);
3859
+ if (idx <= 15) return String(90 + (idx - 8));
3860
+ return `38;5;${String(idx)}`;
3861
+ }
3862
+ if (prefix === "48") {
3863
+ if (idx <= 7) return String(40 + idx);
3864
+ if (idx <= 15) return String(100 + (idx - 8));
3865
+ return `48;5;${String(idx)}`;
3866
+ }
3867
+ return `${prefix};5;${String(idx)}`;
3868
+ }
3869
+ return `${prefix};2;${String(color.r)};${String(color.g)};${String(color.b)}`;
3870
+ }
3871
+ /**
3872
+ * Reset-on-removal pen diff (the tmux `grid_string_cells` model): emit `SGR 0`
3873
+ * iff a boolean attribute turns off or the underline changes away from a
3874
+ * non-none style, then re-add deltas from the reset base. This makes the
3875
+ * `SGR 22` bold/faint coupling and the underline-off classes structurally
3876
+ * impossible, at a few bytes' cost. The leading `0` is emitted only on an
3877
+ * actual reset (not whenever the previous pen happens to be default) so
3878
+ * output stays byte-deterministic and idempotence-friendly.
3879
+ */
3880
+ function serializeDiffPen(prev, cur) {
3881
+ let boolOff = false;
3882
+ for (const [key] of SERIALIZE_BOOL_CODES) if (prev[key] === true && cur[key] === false) {
3883
+ boolOff = true;
3884
+ break;
3885
+ }
3886
+ const underlineChanged = prev.underline !== cur.underline && prev.underline !== "none";
3887
+ const reset = boolOff || underlineChanged;
3888
+ const base = reset ? SERIALIZE_DEFAULT_PEN : prev;
3889
+ const out = reset ? ["0"] : [];
3890
+ for (const [key, code] of SERIALIZE_BOOL_CODES) if (cur[key] === true && base[key] === false) out.push(code);
3891
+ if (cur.underline !== base.underline) out.push(serializeUnderlineParam(cur.underline));
3892
+ if (!serializeColorEq(cur.fg, base.fg)) out.push(serializeColorParams("38", "39", cur.fg));
3893
+ if (!serializeColorEq(cur.bg, base.bg)) out.push(serializeColorParams("48", "49", cur.bg));
3894
+ if (!serializeColorEq(cur.underlineColor, base.underlineColor)) out.push(serializeColorParams("58", "59", cur.underlineColor));
3895
+ return out;
3896
+ }
3897
+ /**
3898
+ * Last cell worth emitting: trailing UNWRITTEN cells (`char === ""`, default
3899
+ * pen, no url) are trimmed — the receiver's cleared screen already has them.
3900
+ * Erased-with-bg trailing cells (BCE) are kept and reproduced via ECH.
3901
+ */
3902
+ function serializeRowEnd(row) {
3903
+ let end = row.length;
3904
+ while (end > 0) {
3905
+ const cell = row[end - 1];
3906
+ if (cell === void 0) {
3907
+ end--;
3908
+ continue;
3909
+ }
3910
+ if (cell.char.length > 0 || cell.wide || cell.url !== null) break;
3911
+ if (!serializePenIsDefault(serializePenOf(cell))) break;
3912
+ end--;
3913
+ }
3914
+ return end;
3915
+ }
3916
+ /**
3917
+ * Encode one row with per-run pen diffing. Invariants:
3918
+ * - the terminal pen is DEFAULT at entry (rows end with `SGR 0`);
3919
+ * - wide-cell spacers (char `""` following a `wide` cell) emit nothing;
3920
+ * - runs of unwritten cells are SKIPPED via CHA (the sink's cells stay
3921
+ * genuinely empty — not painted as spaces), and erased-with-bg runs are
3922
+ * reproduced via SGR+ECH, so the sink grid equals the source grid natively;
3923
+ * - an open OSC 8 hyperlink is always closed by row end.
3924
+ *
3925
+ * `literalWidth > 0` switches to LITERAL mode for soft-wrap-linked history
3926
+ * rows: every one of `literalWidth` columns is WRITTEN (unwritten cells as
3927
+ * pen-carrying spaces — no CHA skips, no ECH, no trailing trim), so the last
3928
+ * column write arms the receiver's pending-wrap and the next row's first
3929
+ * character re-links the logical line. Cell-level cost: the receiver's
3930
+ * scrollback holds written spaces where the source had unwritten cells —
3931
+ * invisible to the text + wrap-bit projections that scrollback serves.
3932
+ */
3933
+ function serializeEncodeRow(row, hyperlinks, literalWidth = 0) {
3934
+ const literal = literalWidth > 0;
3935
+ const end = literal ? literalWidth : serializeRowEnd(row);
3936
+ let out = "";
3937
+ let pen = SERIALIZE_DEFAULT_PEN;
3938
+ let openUrl = null;
3939
+ let col = 0;
3940
+ while (col < end) {
3941
+ const cell = row[col];
3942
+ if (cell === void 0) {
3943
+ if (!literal) {
3944
+ col++;
3945
+ continue;
3946
+ }
3947
+ const diff = serializeDiffPen(pen, SERIALIZE_DEFAULT_PEN);
3948
+ if (diff.length > 0) {
3949
+ out += `\x1b[${diff.join(";")}m`;
3950
+ pen = SERIALIZE_DEFAULT_PEN;
3951
+ if (diff[0] === "0") openUrl = null;
3952
+ }
3953
+ if (openUrl !== null) {
3954
+ out += "\x1B]8;;\x1B\\";
3955
+ openUrl = null;
3956
+ }
3957
+ out += " ";
3958
+ col++;
3959
+ continue;
3960
+ }
3961
+ if (cell.char.length === 0 && col > 0 && row[col - 1]?.wide === true) {
3962
+ col++;
3963
+ continue;
3964
+ }
3965
+ if (cell.char.length === 0 && cell.url === null && !literal) {
3966
+ const runPen = serializePenOf(cell);
3967
+ const runStart = col;
3968
+ while (col < end) {
3969
+ const next = row[col];
3970
+ if (next === void 0 || next.char.length > 0 || next.url !== null) break;
3971
+ if (!serializePenEq(serializePenOf(next), runPen)) break;
3972
+ col++;
3973
+ }
3974
+ const runLength = col - runStart;
3975
+ if (serializePenIsDefault(runPen)) out += `\x1b[${String(col + 1)}G`;
3976
+ else {
3977
+ const diff = serializeDiffPen(pen, runPen);
3978
+ if (diff.length > 0) {
3979
+ out += `\x1b[${diff.join(";")}m`;
3980
+ pen = runPen;
3981
+ if (diff[0] === "0") openUrl = null;
3982
+ }
3983
+ out += `\x1b[${String(runLength)}X\x1b[${String(col + 1)}G`;
3984
+ }
3985
+ continue;
3986
+ }
3987
+ const cellPen = serializePenOf(cell);
3988
+ const diff = serializeDiffPen(pen, cellPen);
3989
+ if (diff.length > 0) {
3990
+ out += `\x1b[${diff.join(";")}m`;
3991
+ pen = cellPen;
3992
+ if (diff[0] === "0") openUrl = null;
3993
+ }
3994
+ const url = hyperlinks ? cell.url : null;
3995
+ if (url !== openUrl) {
3996
+ out += url === null ? "\x1B]8;;\x1B\\" : `\x1b]8;;${url}\x1b\\`;
3997
+ openUrl = url;
3998
+ }
3999
+ out += cell.char.length > 0 ? cell.char : " ";
4000
+ col++;
4001
+ }
4002
+ if (openUrl !== null) out += "\x1B]8;;\x1B\\";
4003
+ return out;
4004
+ }
4005
+ /**
4006
+ * Paint-safe DECSET boolean flags emitted in phase 3 — set-forms only, emitted
4007
+ * only when true (the receiver is fresh: every flag defaults to false). Origin
4008
+ * and insert are deliberately NOT here — they corrupt a positioned paint and
4009
+ * are restored in finalize instead.
4010
+ */
4011
+ const SERIALIZE_DECSET_FLAGS = [
4012
+ ["applicationCursor", 1],
4013
+ ["reverseVideo", 5],
4014
+ ["focusTracking", 1004],
4015
+ ["utf8Mouse", 1005],
4016
+ ["sgrMouse", 1006],
4017
+ ["altScroll", 1007],
4018
+ ["bracketedPaste", 2004],
4019
+ ["colorSchemeReporting", 2031]
4020
+ ];
4021
+ /** Default palette shared by every serialize call (read-only baseline for OSC 4 diffs). */
4022
+ const SERIALIZE_DEFAULT_PALETTE = buildPalette256();
4023
+ /** `rgb:RR/GG/BB` — the XParseColor spelling vterm's own OSC parser round-trips exactly. */
4024
+ function serializeColorSpec(color) {
4025
+ const hex = (v) => v.toString(16).padStart(2, "0");
4026
+ return `rgb:${hex(color.r)}/${hex(color.g)}/${hex(color.b)}`;
4027
+ }
4028
+ /** DECSCUSR code for (shape, blinking); 1 (blinking block) is the fresh default. */
4029
+ function serializeCursorStyleCode(shape, blinking) {
4030
+ const base = shape === "block" ? 1 : shape === "underline" ? 3 : 5;
4031
+ return blinking ? base : base + 1;
4032
+ }
4033
+ /** True when `stops` (sorted) is exactly the fresh default — a stop every 8 columns. */
4034
+ function serializeTabStopsAreDefault(stops, cols) {
4035
+ let expected = 8;
4036
+ for (const stop of stops) {
4037
+ if (stop !== expected) return false;
4038
+ expected += 8;
4039
+ }
4040
+ return expected >= cols;
4041
+ }
4042
+ /**
4043
+ * Serialize a snapshot to minimal ANSI a FRESH same-size terminal can replay.
4044
+ * Phase order + the hard exclusion set are documented on the section header
4045
+ * above; every ordering constraint is pinned by the mode round-trip matrix.
4046
+ *
4047
+ * Intended divergences (documented, not silent): syncOutput/decColumn are
4048
+ * never emitted; the inactive buffer, SCP saved cursor, DECSC saved state,
4049
+ * color stack, and parser/pending-wrap/mid-parse state are unserializable to
4050
+ * a VT byte stream by design — the binary snapshot carries them.
4051
+ */
4052
+ function serializeSnapshot(snapshot, options = {}) {
4053
+ const hyperlinks = options.hyperlinks !== false;
4054
+ const excluded = new Set(options.excludeModes ?? []);
4055
+ const rows = snapshot.rows;
4056
+ const m = snapshot.modes;
4057
+ const out = [];
4058
+ if (options.includeScrollback !== false && snapshot.scrollback.length > 0) {
4059
+ const wraps = snapshot.scrollbackSoftWrapped;
4060
+ const last = snapshot.scrollback.length - 1;
4061
+ for (let i = 0; i < snapshot.scrollback.length; i++) {
4062
+ const row = snapshot.scrollback[i];
4063
+ if (wraps?.[i] === true && i < last) out.push(serializeEncodeRow(row, hyperlinks, snapshot.cols), "\x1B[0m");
4064
+ else out.push(serializeEncodeRow(row, hyperlinks), "\x1B[0m\r\n");
4065
+ }
4066
+ if (rows > 1) out.push("\n".repeat(rows - 1));
4067
+ }
4068
+ if (snapshot.activeBuffer === "alt") out.push("\x1B[?1049h");
4069
+ if (snapshot.margins.scrollTop !== 0 || snapshot.margins.scrollBottom !== rows - 1) out.push(`\x1b[${String(snapshot.margins.scrollTop + 1)};${String(snapshot.margins.scrollBottom + 1)}r`);
4070
+ for (const [key, code] of SERIALIZE_DECSET_FLAGS) if (m[key] === true && !excluded.has(key)) out.push(`\x1b[?${String(code)}h`);
4071
+ if (m.mouseTracking && m.mouseTrackingMode > 0 && !excluded.has("mouseTracking")) out.push(`\x1b[?${String(m.mouseTrackingMode)}h`);
4072
+ if (m.applicationKeypad && !excluded.has("applicationKeypad")) out.push("\x1B=");
4073
+ if (!excluded.has("kittyKeyboardFlags") && (m.kittyKeyboardFlags !== 0 || m.kittyKeyboardStack.length > 0)) {
4074
+ const stack = m.kittyKeyboardStack;
4075
+ if (stack.length === 0) out.push(`\x1b[=${String(m.kittyKeyboardFlags)}u`);
4076
+ else {
4077
+ out.push(`\x1b[=${String(stack[0])}u`);
4078
+ for (let i = 1; i < stack.length; i++) out.push(`\x1b[>${String(stack[i])}u`);
4079
+ out.push(`\x1b[>${String(m.kittyKeyboardFlags)}u`);
4080
+ }
4081
+ }
4082
+ const colors = snapshot.colors.current;
4083
+ if (colors.defaultFgColor) out.push(`\x1b]10;${serializeColorSpec(colors.defaultFgColor)}\x1b\\`);
4084
+ if (colors.defaultBgColor) out.push(`\x1b]11;${serializeColorSpec(colors.defaultBgColor)}\x1b\\`);
4085
+ for (let i = 0; i < colors.palette256.length; i++) {
4086
+ const cur = colors.palette256[i];
4087
+ const def = SERIALIZE_DEFAULT_PALETTE[i];
4088
+ if (cur !== void 0 && def !== void 0 && !serializeColorEq(cur, def)) out.push(`\x1b]4;${String(i)};${serializeColorSpec(cur)}\x1b\\`);
4089
+ }
4090
+ if (options.includeTitle === true && snapshot.title.length > 0) out.push(`\x1b]0;${snapshot.title}\x1b\\`);
4091
+ out.push("\x1B[?7l", "\x1B[H\x1B[2J");
4092
+ const grid = snapshot.activeBuffer === "alt" ? snapshot.alt.grid : snapshot.main.grid;
4093
+ for (let r = 0; r < rows; r++) {
4094
+ const line = serializeEncodeRow(grid[r] ?? [], hyperlinks);
4095
+ if (line.length === 0) continue;
4096
+ out.push(`\x1b[${String(r + 1)};1H`, line, "\x1B[0m");
4097
+ }
4098
+ out.push(m.autoWrap ? "\x1B[?7h" : "\x1B[?7l");
4099
+ if (snapshot.cursor.shape !== "block" || !snapshot.cursor.blinking) out.push(`\x1b[${String(serializeCursorStyleCode(snapshot.cursor.shape, snapshot.cursor.blinking))} q`);
4100
+ if (snapshot.unicode.charsetG0) out.push("\x1B(0");
4101
+ if (m.insert && !excluded.has("insert")) out.push("\x1B[4h");
4102
+ if (!serializeTabStopsAreDefault(snapshot.tabStops, snapshot.cols)) {
4103
+ out.push("\x1B[3g");
4104
+ for (const stop of snapshot.tabStops) if (stop < snapshot.cols) out.push(`\x1b[${String(stop + 1)}G\x1bH`);
4105
+ }
4106
+ if (snapshot.margins.leftRight) {
4107
+ out.push("\x1B[?69h");
4108
+ 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`);
4109
+ }
4110
+ const penParams = serializeDiffPen(SERIALIZE_DEFAULT_PEN, serializePenOf(snapshot.attrs));
4111
+ if (penParams.length > 0) out.push(`\x1b[${penParams.join(";")}m`);
4112
+ if (hyperlinks && snapshot.attrs.url !== null) out.push(`\x1b]8;;${snapshot.attrs.url}\x1b\\`);
4113
+ out.push(snapshot.cursor.visible ? "\x1B[?25h" : "\x1B[?25l");
4114
+ const origin = m.origin && !excluded.has("origin");
4115
+ const inRegion = snapshot.cursor.y >= snapshot.margins.scrollTop && snapshot.cursor.y <= snapshot.margins.scrollBottom;
4116
+ if (origin && inRegion) {
4117
+ out.push("\x1B[?6h");
4118
+ out.push(`\x1b[${String(snapshot.cursor.y - snapshot.margins.scrollTop + 1)};${String(snapshot.cursor.x + 1)}H`);
4119
+ } else {
4120
+ out.push(`\x1b[${String(snapshot.cursor.y + 1)};${String(snapshot.cursor.x + 1)}H`);
4121
+ if (origin) out.push("\x1B[?6h");
4122
+ }
4123
+ return out.join("");
4124
+ }
4125
+ //#endregion
4126
+ //#region src/snapshot-codec.ts
4127
+ const VERSION = 1;
4128
+ const encoder = new TextEncoder();
4129
+ const decoder = new TextDecoder();
4130
+ const F_BOLD = 1;
4131
+ const F_FAINT = 2;
4132
+ const F_ITALIC = 4;
4133
+ const F_OVERLINE = 8;
4134
+ const F_STRIKE = 16;
4135
+ const F_INVERSE = 32;
4136
+ const F_HIDDEN = 64;
4137
+ const F_BLINK = 128;
4138
+ const F_WIDE = 256;
4139
+ const UL_SHIFT = 9;
4140
+ const UL_MASK = 7;
4141
+ const UNDERLINE_STYLES = [
4142
+ "none",
4143
+ "single",
4144
+ "double",
4145
+ "curly",
4146
+ "dotted",
4147
+ "dashed"
4148
+ ];
4149
+ const UNDERLINE_INDEX = {
4150
+ none: 0,
4151
+ single: 1,
4152
+ double: 2,
4153
+ curly: 3,
4154
+ dotted: 4,
4155
+ dashed: 5
4156
+ };
4157
+ function createWriter() {
4158
+ let buf = /* @__PURE__ */ new Uint8Array(1024);
4159
+ let len = 0;
4160
+ function ensure(extra) {
4161
+ if (len + extra <= buf.length) return;
4162
+ let cap = buf.length;
4163
+ while (cap < len + extra) cap *= 2;
4164
+ const next = new Uint8Array(cap);
4165
+ next.set(buf.subarray(0, len));
4166
+ buf = next;
4167
+ }
4168
+ return {
4169
+ u8(byte) {
4170
+ ensure(1);
4171
+ buf[len++] = byte & 255;
4172
+ },
4173
+ varint(value) {
4174
+ ensure(10);
4175
+ let v = value;
4176
+ while (v >= 128) {
4177
+ buf[len++] = v % 128 + 128;
4178
+ v = Math.floor(v / 128);
4179
+ }
4180
+ buf[len++] = v;
4181
+ },
4182
+ bytes(src) {
4183
+ ensure(src.length);
4184
+ buf.set(src, len);
4185
+ len += src.length;
4186
+ },
4187
+ finish() {
4188
+ return buf.slice(0, len);
4189
+ }
4190
+ };
4191
+ }
4192
+ function createReader(bytes) {
4193
+ let pos = 0;
4194
+ function u8() {
4195
+ if (pos >= bytes.length) throw new Error("vterm snapshot codec: unexpected end of input");
4196
+ return bytes[pos++];
4197
+ }
4198
+ return {
4199
+ u8,
4200
+ varint() {
4201
+ let result = 0;
4202
+ let shift = 1;
4203
+ for (;;) {
4204
+ const byte = u8();
4205
+ result += (byte & 127) * shift;
4206
+ if ((byte & 128) === 0) break;
4207
+ shift *= 128;
4208
+ if (shift > 2 ** 53) throw new Error("vterm snapshot codec: varint too large");
4209
+ }
4210
+ return result;
4211
+ },
4212
+ take(n) {
4213
+ if (pos + n > bytes.length) throw new Error("vterm snapshot codec: unexpected end of input");
4214
+ const slice = bytes.subarray(pos, pos + n);
4215
+ pos += n;
4216
+ return slice;
4217
+ }
4218
+ };
4219
+ }
4220
+ function createRunAccumulator() {
4221
+ const runs = [];
4222
+ let hasLast = false;
4223
+ let lastValue = 0;
4224
+ return {
4225
+ runs,
4226
+ push(value) {
4227
+ if (hasLast && value === lastValue) runs[runs.length - 2] += 1;
4228
+ else {
4229
+ runs.push(1, value);
4230
+ lastValue = value;
4231
+ hasLast = true;
4232
+ }
4233
+ }
4234
+ };
4235
+ }
4236
+ function writeRuns(w, runs) {
4237
+ w.varint(runs.length / 2);
4238
+ for (let i = 0; i < runs.length; i += 2) {
4239
+ w.varint(runs[i]);
4240
+ w.varint(runs[i + 1]);
4241
+ }
4242
+ }
4243
+ function readRuns(r) {
4244
+ const runCount = r.varint();
4245
+ const runs = [];
4246
+ for (let i = 0; i < runCount; i++) {
4247
+ const count = r.varint();
4248
+ const value = r.varint();
4249
+ runs.push(count, value);
4250
+ }
4251
+ return runs;
4252
+ }
4253
+ function createRunCursor(runs) {
4254
+ let total = 0;
4255
+ for (let i = 0; i < runs.length; i += 2) total += runs[i];
4256
+ let idx = 0;
4257
+ let remaining = runs.length > 0 ? runs[0] : 0;
4258
+ let value = runs.length > 1 ? runs[1] : 0;
4259
+ return {
4260
+ total,
4261
+ next() {
4262
+ while (remaining === 0) {
4263
+ idx += 2;
4264
+ if (idx >= runs.length) throw new Error("vterm snapshot codec: run-length underflow");
4265
+ remaining = runs[idx];
4266
+ value = runs[idx + 1];
4267
+ }
4268
+ remaining -= 1;
4269
+ return value;
4270
+ }
4271
+ };
4272
+ }
4273
+ function expandLengths(runs) {
4274
+ const out = [];
4275
+ for (let i = 0; i < runs.length; i += 2) {
4276
+ const count = runs[i];
4277
+ const value = runs[i + 1];
4278
+ for (let k = 0; k < count; k++) out.push(value);
4279
+ }
4280
+ return out;
4281
+ }
4282
+ function writeBits(w, bits) {
4283
+ let acc = 0;
4284
+ let n = 0;
4285
+ for (const bit of bits) {
4286
+ if (bit) acc |= 1 << n;
4287
+ n++;
4288
+ if (n === 8) {
4289
+ w.u8(acc);
4290
+ acc = 0;
4291
+ n = 0;
4292
+ }
4293
+ }
4294
+ if (n > 0) w.u8(acc);
4295
+ }
4296
+ function readBits(r, count) {
4297
+ const out = new Array(count);
4298
+ let acc = 0;
4299
+ let have = 0;
4300
+ for (let i = 0; i < count; i++) {
4301
+ if (have === 0) {
4302
+ acc = r.u8();
4303
+ have = 8;
4304
+ }
4305
+ out[i] = (acc & 1) === 1;
4306
+ acc >>= 1;
4307
+ have--;
4308
+ }
4309
+ return out;
4310
+ }
4311
+ function checkByte(name, value) {
4312
+ if (!Number.isInteger(value) || value < 0 || value > 255) throw new Error(`vterm snapshot codec: ${name} out of 0-255 range: ${String(value)}`);
4313
+ }
4314
+ function createInterner() {
4315
+ const stringList = [""];
4316
+ const stringMap = /* @__PURE__ */ new Map([["", 0]]);
4317
+ const colorList = [];
4318
+ const colorMap = /* @__PURE__ */ new Map();
4319
+ const styleList = [];
4320
+ const styleMap = /* @__PURE__ */ new Map();
4321
+ function internString(s) {
4322
+ let idx = stringMap.get(s);
4323
+ if (idx === void 0) {
4324
+ idx = stringList.length;
4325
+ stringList.push(s);
4326
+ stringMap.set(s, idx);
4327
+ }
4328
+ return idx;
4329
+ }
4330
+ /** null → 0; otherwise color-table index + 1. */
4331
+ function colorRef(c) {
4332
+ if (c === null) return 0;
4333
+ checkByte("color.r", c.r);
4334
+ checkByte("color.g", c.g);
4335
+ checkByte("color.b", c.b);
4336
+ if (c.index !== void 0) checkByte("color.index", c.index);
4337
+ const key = c.index === void 0 ? `${c.r},${c.g},${c.b}` : `${c.r},${c.g},${c.b},${c.index}`;
4338
+ let idx = colorMap.get(key);
4339
+ if (idx === void 0) {
4340
+ idx = colorList.length;
4341
+ colorList.push(c.index === void 0 ? {
4342
+ r: c.r,
4343
+ g: c.g,
4344
+ b: c.b
4345
+ } : {
4346
+ r: c.r,
4347
+ g: c.g,
4348
+ b: c.b,
4349
+ index: c.index
4350
+ });
4351
+ colorMap.set(key, idx);
4352
+ }
4353
+ return idx + 1;
4354
+ }
4355
+ function internStyle(cell) {
4356
+ const fgRef = colorRef(cell.fg);
4357
+ const bgRef = colorRef(cell.bg);
4358
+ const ulRef = colorRef(cell.underlineColor);
4359
+ const urlRef = cell.url === null ? 0 : internString(cell.url) + 1;
4360
+ const ulIdx = UNDERLINE_INDEX[cell.underline];
4361
+ if (ulIdx === void 0) throw new Error(`vterm snapshot codec: unknown underline style ${String(cell.underline)}`);
4362
+ let flags = 0;
4363
+ if (cell.bold) flags |= F_BOLD;
4364
+ if (cell.faint) flags |= F_FAINT;
4365
+ if (cell.italic) flags |= F_ITALIC;
4366
+ if (cell.overline) flags |= F_OVERLINE;
4367
+ if (cell.strikethrough) flags |= F_STRIKE;
4368
+ if (cell.inverse) flags |= F_INVERSE;
4369
+ if (cell.hidden) flags |= F_HIDDEN;
4370
+ if (cell.blink) flags |= F_BLINK;
4371
+ if (cell.wide) flags |= F_WIDE;
4372
+ flags |= ulIdx << UL_SHIFT;
4373
+ const key = `${flags}|${fgRef}|${bgRef}|${ulRef}|${urlRef}`;
4374
+ let idx = styleMap.get(key);
4375
+ if (idx === void 0) {
4376
+ idx = styleList.length;
4377
+ styleList.push({
4378
+ flags,
4379
+ fgRef,
4380
+ bgRef,
4381
+ ulRef,
4382
+ urlRef
4383
+ });
4384
+ styleMap.set(key, idx);
4385
+ }
4386
+ return idx;
4387
+ }
4388
+ return {
4389
+ stringList,
4390
+ colorList,
4391
+ styleList,
4392
+ internString,
4393
+ internStyle
4394
+ };
4395
+ }
4396
+ function buildGridColumns(grid, interner) {
4397
+ const charAcc = createRunAccumulator();
4398
+ const styleAcc = createRunAccumulator();
4399
+ const lenAcc = createRunAccumulator();
4400
+ for (const row of grid) {
4401
+ lenAcc.push(row.length);
4402
+ for (const cell of row) {
4403
+ charAcc.push(interner.internString(cell.char));
4404
+ styleAcc.push(interner.internStyle(cell));
4405
+ }
4406
+ }
4407
+ return {
4408
+ rowCount: grid.length,
4409
+ lenRuns: lenAcc.runs,
4410
+ charRuns: charAcc.runs,
4411
+ styleRuns: styleAcc.runs
4412
+ };
4413
+ }
4414
+ function writeGridSection(w, columns, softWrapped, softWrapNullable) {
4415
+ w.varint(columns.rowCount);
4416
+ if (softWrapNullable && softWrapped === void 0) w.u8(0);
4417
+ else {
4418
+ if (softWrapNullable) w.u8(1);
4419
+ if (softWrapped === void 0) throw new Error("vterm snapshot codec: missing soft-wrap array for a required grid");
4420
+ if (softWrapped.length !== columns.rowCount) throw new Error(`vterm snapshot codec: soft-wrap length ${softWrapped.length} != rowCount ${columns.rowCount}`);
4421
+ writeBits(w, softWrapped);
4422
+ }
4423
+ writeRuns(w, columns.lenRuns);
4424
+ writeRuns(w, columns.charRuns);
4425
+ writeRuns(w, columns.styleRuns);
4426
+ }
4427
+ function writeStringTable(w, list) {
4428
+ w.varint(list.length);
4429
+ for (const s of list) {
4430
+ const b = encoder.encode(s);
4431
+ w.varint(b.length);
4432
+ w.bytes(b);
4433
+ }
4434
+ }
4435
+ function writeColorTable(w, list) {
4436
+ w.varint(list.length);
4437
+ for (const c of list) {
4438
+ w.u8(c.r);
4439
+ w.u8(c.g);
4440
+ w.u8(c.b);
4441
+ w.varint(c.index === void 0 ? 0 : c.index + 1);
4442
+ }
4443
+ }
4444
+ function writeStyleTable(w, list) {
4445
+ w.varint(list.length);
4446
+ for (const st of list) {
4447
+ w.varint(st.flags);
4448
+ w.varint(st.fgRef);
4449
+ w.varint(st.bgRef);
4450
+ w.varint(st.ulRef);
4451
+ w.varint(st.urlRef);
4452
+ }
4453
+ }
4454
+ function readStringTable(r) {
4455
+ const count = r.varint();
4456
+ const list = new Array(count);
4457
+ for (let i = 0; i < count; i++) {
4458
+ const len = r.varint();
4459
+ list[i] = decoder.decode(r.take(len));
4460
+ }
4461
+ return list;
4462
+ }
4463
+ function readColorTable(r) {
4464
+ const count = r.varint();
4465
+ const list = new Array(count);
4466
+ for (let i = 0; i < count; i++) {
4467
+ const cr = r.u8();
4468
+ const cg = r.u8();
4469
+ const cb = r.u8();
4470
+ const idxPlus = r.varint();
4471
+ list[i] = idxPlus === 0 ? {
4472
+ r: cr,
4473
+ g: cg,
4474
+ b: cb
4475
+ } : {
4476
+ r: cr,
4477
+ g: cg,
4478
+ b: cb,
4479
+ index: idxPlus - 1
4480
+ };
4481
+ }
4482
+ return list;
4483
+ }
4484
+ function readStyleTable(r) {
4485
+ const count = r.varint();
4486
+ const list = new Array(count);
4487
+ for (let i = 0; i < count; i++) list[i] = {
4488
+ flags: r.varint(),
4489
+ fgRef: r.varint(),
4490
+ bgRef: r.varint(),
4491
+ ulRef: r.varint(),
4492
+ urlRef: r.varint()
4493
+ };
4494
+ return list;
4495
+ }
4496
+ function colorFromRef(ref, table) {
4497
+ if (ref === 0) return null;
4498
+ const c = table[ref - 1];
4499
+ if (c === void 0) throw new Error(`vterm snapshot codec: color ref ${ref} out of range (table size ${table.length})`);
4500
+ return c.index === void 0 ? {
4501
+ r: c.r,
4502
+ g: c.g,
4503
+ b: c.b
4504
+ } : {
4505
+ r: c.r,
4506
+ g: c.g,
4507
+ b: c.b,
4508
+ index: c.index
4509
+ };
4510
+ }
4511
+ function makeCell(charIdx, styleIdx, strings, colors, styles) {
4512
+ const char = strings[charIdx];
4513
+ if (char === void 0) throw new Error(`vterm snapshot codec: char ref ${charIdx} out of range (table size ${strings.length})`);
4514
+ const style = styles[styleIdx];
4515
+ if (style === void 0) throw new Error(`vterm snapshot codec: style ref ${styleIdx} out of range (table size ${styles.length})`);
4516
+ const underline = UNDERLINE_STYLES[style.flags >> UL_SHIFT & UL_MASK];
4517
+ if (underline === void 0) throw new Error(`vterm snapshot codec: invalid underline bits in style flags ${style.flags}`);
4518
+ let url;
4519
+ if (style.urlRef === 0) url = null;
4520
+ else {
4521
+ const u = strings[style.urlRef - 1];
4522
+ if (u === void 0) throw new Error(`vterm snapshot codec: url ref ${style.urlRef} out of range (table size ${strings.length})`);
4523
+ url = u;
4524
+ }
4525
+ return {
4526
+ char,
4527
+ fg: colorFromRef(style.fgRef, colors),
4528
+ bg: colorFromRef(style.bgRef, colors),
4529
+ bold: (style.flags & F_BOLD) !== 0,
4530
+ faint: (style.flags & F_FAINT) !== 0,
4531
+ italic: (style.flags & F_ITALIC) !== 0,
4532
+ underline,
4533
+ underlineColor: colorFromRef(style.ulRef, colors),
4534
+ overline: (style.flags & F_OVERLINE) !== 0,
4535
+ strikethrough: (style.flags & F_STRIKE) !== 0,
4536
+ inverse: (style.flags & F_INVERSE) !== 0,
4537
+ hidden: (style.flags & F_HIDDEN) !== 0,
4538
+ blink: (style.flags & F_BLINK) !== 0,
4539
+ wide: (style.flags & F_WIDE) !== 0,
4540
+ url
4541
+ };
4542
+ }
4543
+ function readGridSection(r, softWrapNullable, expected, strings, colors, styles) {
4544
+ const rowCount = r.varint();
4545
+ let softWrapped;
4546
+ if (softWrapNullable) {
4547
+ const present = r.u8();
4548
+ if (present === 0) softWrapped = void 0;
4549
+ else if (present === 1) softWrapped = readBits(r, rowCount);
4550
+ else throw new Error(`vterm snapshot codec: invalid soft-wrap presence byte ${present}`);
4551
+ } else softWrapped = readBits(r, rowCount);
4552
+ const rowLengths = expandLengths(readRuns(r));
4553
+ if (rowLengths.length !== rowCount) throw new Error(`vterm snapshot codec: decoded ${rowLengths.length} row lengths but rowCount is ${rowCount}`);
4554
+ if (expected) {
4555
+ if (rowCount !== expected.rows) throw new Error(`vterm snapshot codec: grid rowCount ${rowCount} != expected ${expected.rows}`);
4556
+ for (const len of rowLengths) if (len !== expected.cols) throw new Error(`vterm snapshot codec: grid row length ${len} != expected ${expected.cols}`);
4557
+ }
4558
+ let totalCells = 0;
4559
+ for (const len of rowLengths) totalCells += len;
4560
+ const charCursor = createRunCursor(readRuns(r));
4561
+ const styleCursor = createRunCursor(readRuns(r));
4562
+ if (charCursor.total !== totalCells) throw new Error(`vterm snapshot codec: char column has ${charCursor.total} cells, grid needs ${totalCells}`);
4563
+ if (styleCursor.total !== totalCells) throw new Error(`vterm snapshot codec: style column has ${styleCursor.total} cells, grid needs ${totalCells}`);
4564
+ const grid = new Array(rowCount);
4565
+ for (let row = 0; row < rowCount; row++) {
4566
+ const len = rowLengths[row];
4567
+ const cells = new Array(len);
4568
+ for (let col = 0; col < len; col++) cells[col] = makeCell(charCursor.next(), styleCursor.next(), strings, colors, styles);
4569
+ grid[row] = cells;
4570
+ }
4571
+ return {
4572
+ grid,
4573
+ softWrapped
4574
+ };
4575
+ }
4576
+ /**
4577
+ * Encode a {@link Snapshot} to a compact binary buffer. Output is
4578
+ * deterministic for identical input and decodes back to a deep-equal snapshot.
4579
+ */
4580
+ function encodeScreenSnapshotBinary(snapshot) {
4581
+ const w = createWriter();
4582
+ w.u8(VERSION);
4583
+ const header = {
4584
+ cols: snapshot.cols,
4585
+ rows: snapshot.rows,
4586
+ scrollbackLimit: snapshot.scrollbackLimit,
4587
+ activeBuffer: snapshot.activeBuffer,
4588
+ cursor: snapshot.cursor,
4589
+ savedState: snapshot.savedState,
4590
+ attrs: snapshot.attrs,
4591
+ modes: snapshot.modes,
4592
+ margins: snapshot.margins,
4593
+ colors: snapshot.colors,
4594
+ tabStops: snapshot.tabStops,
4595
+ title: snapshot.title,
4596
+ clipboard: snapshot.clipboard,
4597
+ cwd: snapshot.cwd,
4598
+ notifications: snapshot.notifications,
4599
+ viewportOffset: snapshot.viewportOffset,
4600
+ parser: snapshot.parser,
4601
+ unicode: snapshot.unicode
4602
+ };
4603
+ const headerBytes = encoder.encode(JSON.stringify(header));
4604
+ w.varint(headerBytes.length);
4605
+ w.bytes(headerBytes);
4606
+ const interner = createInterner();
4607
+ const mainColumns = buildGridColumns(snapshot.main.grid, interner);
4608
+ const altColumns = buildGridColumns(snapshot.alt.grid, interner);
4609
+ const scrollbackColumns = buildGridColumns(snapshot.scrollback, interner);
4610
+ writeStringTable(w, interner.stringList);
4611
+ writeColorTable(w, interner.colorList);
4612
+ writeStyleTable(w, interner.styleList);
4613
+ writeGridSection(w, mainColumns, snapshot.main.softWrapped, false);
4614
+ writeGridSection(w, altColumns, snapshot.alt.softWrapped, false);
4615
+ writeGridSection(w, scrollbackColumns, snapshot.scrollbackSoftWrapped, true);
4616
+ return w.finish();
4617
+ }
4618
+ /**
4619
+ * Decode a buffer produced by {@link encodeScreenSnapshotBinary} back into a
4620
+ * {@link Snapshot}. Throws loudly on an unknown version byte, truncated
4621
+ * input, or any internal inconsistency — never returns a partial/normalized result.
4622
+ */
4623
+ function decodeScreenSnapshotBinary(bytes) {
4624
+ const r = createReader(bytes);
4625
+ const version = r.u8();
4626
+ if (version !== VERSION) throw new Error(`vterm snapshot codec: unsupported version byte ${version} (expected ${VERSION})`);
4627
+ const headerLen = r.varint();
4628
+ const header = JSON.parse(decoder.decode(r.take(headerLen)));
4629
+ const strings = readStringTable(r);
4630
+ const colors = readColorTable(r);
4631
+ const styles = readStyleTable(r);
4632
+ const geom = {
4633
+ rows: header.rows,
4634
+ cols: header.cols
4635
+ };
4636
+ const main = readGridSection(r, false, geom, strings, colors, styles);
4637
+ const alt = readGridSection(r, false, geom, strings, colors, styles);
4638
+ const scrollback = readGridSection(r, true, null, strings, colors, styles);
4639
+ return {
4640
+ version: 1,
4641
+ cols: header.cols,
4642
+ rows: header.rows,
4643
+ scrollbackLimit: header.scrollbackLimit,
4644
+ activeBuffer: header.activeBuffer,
4645
+ main: {
4646
+ grid: main.grid,
4647
+ softWrapped: main.softWrapped
4648
+ },
4649
+ alt: {
4650
+ grid: alt.grid,
4651
+ softWrapped: alt.softWrapped
4652
+ },
4653
+ scrollback: scrollback.grid,
4654
+ scrollbackSoftWrapped: scrollback.softWrapped,
4655
+ cursor: header.cursor,
4656
+ savedState: header.savedState,
4657
+ attrs: header.attrs,
4658
+ modes: header.modes,
4659
+ margins: header.margins,
4660
+ colors: header.colors,
4661
+ tabStops: header.tabStops,
4662
+ title: header.title,
4663
+ clipboard: header.clipboard,
4664
+ cwd: header.cwd,
4665
+ notifications: header.notifications,
4666
+ viewportOffset: header.viewportOffset,
4667
+ parser: header.parser,
4668
+ unicode: header.unicode
2498
4669
  };
2499
4670
  }
2500
4671
  //#endregion
2501
- export { createScreen as createVtermScreen };
4672
+ export { DEFAULT_MAX_STRING_SEQUENCE_LENGTH, createScreen as createVtermScreen, decodeScreenSnapshotBinary, encodeScreenSnapshotBinary, serializeSnapshot };