vterm.js 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/screen.ts
2
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;
3
+ const DEFAULT_MAX_STRING_SEQUENCE_LENGTH = 16777216;
4
4
  /** Frozen sentinel for unwritten cells — never mutate, copy-on-write in writeChar(). */
5
5
  const EMPTY_CELL = Object.freeze({
6
6
  char: "",
@@ -17,6 +17,7 @@ const EMPTY_CELL = Object.freeze({
17
17
  hidden: false,
18
18
  blink: false,
19
19
  wide: false,
20
+ protected: false,
20
21
  url: null
21
22
  });
22
23
  function emptyCell() {
@@ -31,7 +32,7 @@ function emptyCell() {
31
32
  * case), so the read path allocates nothing there.
32
33
  */
33
34
  function stripColorIndex(c) {
34
- if (c === null || c.index === void 0) return c;
35
+ if (c?.index === void 0) return c;
35
36
  return {
36
37
  r: c.r,
37
38
  g: c.g,
@@ -57,11 +58,12 @@ const F_HIDDEN$1 = 64;
57
58
  const F_BLINK$1 = 128;
58
59
  const F_WIDE$1 = 256;
59
60
  const UL_SHIFT$1 = 9;
60
- const UL_MASK$1 = 7 << UL_SHIFT$1;
61
+ const UL_MASK$1 = 3584;
61
62
  const F_HAS_FG = 4096;
62
63
  const F_HAS_BG = 8192;
63
64
  const F_HAS_UL = 16384;
64
65
  const F_HAS_URL = 32768;
66
+ const F_PROTECTED$1 = 65536;
65
67
  const UL_STYLES = [
66
68
  "none",
67
69
  "single",
@@ -77,6 +79,17 @@ function underlineToBits(u) {
77
79
  function bitsToUnderline(meta) {
78
80
  return UL_STYLES[(meta & UL_MASK$1) >> UL_SHIFT$1] ?? "none";
79
81
  }
82
+ /** Require an invariant-owned value and fail where the impossible state is first observed. */
83
+ function required(value, subject) {
84
+ if (value === null || value === void 0) throw new Error(`Missing ${subject}`);
85
+ return value;
86
+ }
87
+ /** Read a dense indexed value and fail at the violated invariant instead of propagating `undefined`. */
88
+ function itemAt(items, index, subject) {
89
+ const value = items[index];
90
+ if (value === void 0) throw new RangeError(`${subject}[${index}] is outside its ${items.length}-item range`);
91
+ return value;
92
+ }
80
93
  /** Pack a color's 8-bit channels into a 24-bit `0xRRGGBB` word. */
81
94
  function packRgb(c) {
82
95
  return ((c.r & 255) << 16 | (c.g & 255) << 8 | c.b & 255) >>> 0;
@@ -108,7 +121,7 @@ function makePackedRow(width) {
108
121
  let ulIdx = null;
109
122
  let urlMap = null;
110
123
  function setFg(col, c) {
111
- if (fgRgb === null) {
124
+ if (fgRgb === null || fgIdx === null) {
112
125
  fgRgb = new Uint32Array(width);
113
126
  fgIdx = new Int16Array(width);
114
127
  }
@@ -116,7 +129,7 @@ function makePackedRow(width) {
116
129
  fgIdx[col] = c.index ?? -1;
117
130
  }
118
131
  function setBg(col, c) {
119
- if (bgRgb === null) {
132
+ if (bgRgb === null || bgIdx === null) {
120
133
  bgRgb = new Uint32Array(width);
121
134
  bgIdx = new Int16Array(width);
122
135
  }
@@ -124,7 +137,7 @@ function makePackedRow(width) {
124
137
  bgIdx[col] = c.index ?? -1;
125
138
  }
126
139
  function setUl(col, c) {
127
- if (ulRgb === null) {
140
+ if (ulRgb === null || ulIdx === null) {
128
141
  ulRgb = new Uint32Array(width);
129
142
  ulIdx = new Int16Array(width);
130
143
  }
@@ -147,6 +160,7 @@ function makePackedRow(width) {
147
160
  if (a.inverse) m |= F_INVERSE$1;
148
161
  if (a.hidden) m |= F_HIDDEN$1;
149
162
  if (a.blink) m |= F_BLINK$1;
163
+ if (a.protected) m |= F_PROTECTED$1;
150
164
  if (wide) m |= F_WIDE$1;
151
165
  return m | underlineToBits(a.underline);
152
166
  }
@@ -208,34 +222,35 @@ function makePackedRow(width) {
208
222
  } else meta[col] = 0;
209
223
  },
210
224
  widen(col) {
211
- meta[col] = (meta[col] | F_WIDE$1) >>> 0;
225
+ meta[col] = (itemAt(meta, col, "packed row metadata") | F_WIDE$1) >>> 0;
212
226
  if (col + 1 < width) row.setEmpty(col + 1);
213
227
  },
214
228
  appendChar(col, ch) {
215
- chars[col] = chars[col] + ch;
229
+ chars[col] = itemAt(chars, col, "packed row characters") + ch;
216
230
  },
217
231
  copyCellFrom(col, src, srcCol) {
218
232
  row.setCellRaw(col, src.getCellRaw(srcCol));
219
233
  },
220
234
  getCellRaw(col) {
221
- const m = meta[col];
235
+ const m = itemAt(meta, col, "packed row metadata");
222
236
  if (m === 0 && chars[col] === "") return emptyCell();
223
237
  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,
238
+ char: itemAt(chars, col, "packed row characters"),
239
+ fg: m & F_HAS_FG ? unpackColor(itemAt(required(fgRgb, "foreground color plane"), col, "foreground color plane"), itemAt(required(fgIdx, "foreground color index plane"), col, "foreground color index plane")) : null,
240
+ bg: m & F_HAS_BG ? unpackColor(itemAt(required(bgRgb, "background color plane"), col, "background color plane"), itemAt(required(bgIdx, "background color index plane"), col, "background color index plane")) : null,
227
241
  bold: (m & F_BOLD$1) !== 0,
228
242
  faint: (m & F_FAINT$1) !== 0,
229
243
  italic: (m & F_ITALIC$1) !== 0,
230
244
  underline: bitsToUnderline(m),
231
- underlineColor: m & F_HAS_UL ? unpackColor(ulRgb[col], ulIdx[col]) : null,
245
+ underlineColor: m & F_HAS_UL ? unpackColor(itemAt(required(ulRgb, "underline color plane"), col, "underline color plane"), itemAt(required(ulIdx, "underline color index plane"), col, "underline color index plane")) : null,
232
246
  overline: (m & F_OVERLINE$1) !== 0,
233
247
  strikethrough: (m & F_STRIKE$1) !== 0,
234
248
  inverse: (m & F_INVERSE$1) !== 0,
235
249
  hidden: (m & F_HIDDEN$1) !== 0,
236
250
  blink: (m & F_BLINK$1) !== 0,
251
+ protected: (m & F_PROTECTED$1) !== 0,
237
252
  wide: (m & F_WIDE$1) !== 0,
238
- url: m & F_HAS_URL ? urlMap.get(col) ?? null : null
253
+ url: m & F_HAS_URL ? required(urlMap, "packed row URL map").get(col) ?? null : null
239
254
  };
240
255
  },
241
256
  toCells() {
@@ -250,10 +265,10 @@ function makePackedRow(width) {
250
265
  return meta[col] === 0 && chars[col] === "";
251
266
  },
252
267
  getChar(col) {
253
- return chars[col];
268
+ return itemAt(chars, col, "packed row characters");
254
269
  },
255
270
  isWide(col) {
256
- return (meta[col] & F_WIDE$1) !== 0;
271
+ return (itemAt(meta, col, "packed row metadata") & F_WIDE$1) !== 0;
257
272
  }
258
273
  };
259
274
  return row;
@@ -357,9 +372,9 @@ function buildPalette256() {
357
372
  255
358
373
  ];
359
374
  for (let r = 0; r < 6; r++) for (let g = 0; g < 6; g++) for (let b = 0; b < 6; b++) palette.push({
360
- r: levels[r],
361
- g: levels[g],
362
- b: levels[b]
375
+ r: itemAt(levels, r, "ANSI cube red levels"),
376
+ g: itemAt(levels, g, "ANSI cube green levels"),
377
+ b: itemAt(levels, b, "ANSI cube blue levels")
363
378
  });
364
379
  for (let i = 0; i < 24; i++) {
365
380
  const v = 8 + i * 10;
@@ -385,14 +400,14 @@ function parseColorSpec(spec) {
385
400
  return Math.round(v * 255 / max);
386
401
  };
387
402
  return {
388
- r: scale(rgbMatch[1]),
389
- g: scale(rgbMatch[2]),
390
- b: scale(rgbMatch[3])
403
+ r: scale(itemAt(rgbMatch, 1, "X11 red capture")),
404
+ g: scale(itemAt(rgbMatch, 2, "X11 green capture")),
405
+ b: scale(itemAt(rgbMatch, 3, "X11 blue capture"))
391
406
  };
392
407
  }
393
408
  const hex6 = /^#([0-9a-fA-F]{6})$/.exec(s);
394
409
  if (hex6) {
395
- const v = hex6[1];
410
+ const v = itemAt(hex6, 1, "six-digit hex capture");
396
411
  return {
397
412
  r: parseInt(v.substring(0, 2), 16),
398
413
  g: parseInt(v.substring(2, 4), 16),
@@ -401,12 +416,12 @@ function parseColorSpec(spec) {
401
416
  }
402
417
  const hex3 = /^#([0-9a-fA-F]{3})$/.exec(s);
403
418
  if (hex3) {
404
- const v = hex3[1];
419
+ const v = itemAt(hex3, 1, "three-digit hex capture");
405
420
  const expand = (ch) => parseInt(ch + ch, 16);
406
421
  return {
407
- r: expand(v[0]),
408
- g: expand(v[1]),
409
- b: expand(v[2])
422
+ r: expand(itemAt(v, 0, "short hex red channel")),
423
+ g: expand(itemAt(v, 1, "short hex green channel")),
424
+ b: expand(itemAt(v, 2, "short hex blue channel"))
410
425
  };
411
426
  }
412
427
  return null;
@@ -480,7 +495,9 @@ function createScreen(options = {}) {
480
495
  attrs: resetAttrs(),
481
496
  originMode: false,
482
497
  autoWrap: true,
483
- charsetG0: false
498
+ charsetG0: false,
499
+ charsetG1: false,
500
+ activeG1: false
484
501
  };
485
502
  let savedCurX = 0;
486
503
  let savedCurY = 0;
@@ -497,6 +514,7 @@ function createScreen(options = {}) {
497
514
  let focusTracking = false;
498
515
  let originMode = false;
499
516
  let insertMode = false;
517
+ let newLineMode = false;
500
518
  let reverseVideo = false;
501
519
  let syncOutput = false;
502
520
  let kittyKeyboardFlags = 0;
@@ -658,6 +676,8 @@ function createScreen(options = {}) {
658
676
  assertBoolean("savedState.originMode", saved.originMode);
659
677
  assertBoolean("savedState.autoWrap", saved.autoWrap);
660
678
  assertBoolean("savedState.charsetG0", saved.charsetG0);
679
+ if (Object.hasOwn(saved, "charsetG1")) assertBoolean("savedState.charsetG1", saved.charsetG1);
680
+ if (Object.hasOwn(saved, "activeG1")) assertBoolean("savedState.activeG1", saved.activeG1);
661
681
  assertRecord("attrs", value.attrs);
662
682
  const modes = value.modes;
663
683
  assertRecord("modes", modes);
@@ -740,6 +760,8 @@ function createScreen(options = {}) {
740
760
  const unicode = value.unicode;
741
761
  assertRecord("unicode", unicode);
742
762
  assertBoolean("unicode.charsetG0", unicode.charsetG0);
763
+ if (Object.hasOwn(unicode, "charsetG1")) assertBoolean("unicode.charsetG1", unicode.charsetG1);
764
+ if (Object.hasOwn(unicode, "activeG1")) assertBoolean("unicode.activeG1", unicode.activeG1);
743
765
  assertString("unicode.lastChar", unicode.lastChar);
744
766
  if (unicode.pendingRegionalIndicator !== null) assertString("unicode.pendingRegionalIndicator", unicode.pendingRegionalIndicator);
745
767
  assertBoolean("unicode.afterZWJ", unicode.afterZWJ);
@@ -769,6 +791,8 @@ function createScreen(options = {}) {
769
791
  let advancedClipboard = "";
770
792
  let viewportOffset = 0;
771
793
  let charsetG0 = false;
794
+ let charsetG1 = false;
795
+ let activeG1 = false;
772
796
  let clipboard = "";
773
797
  let cwd = "";
774
798
  let notifications = [];
@@ -778,7 +802,68 @@ function createScreen(options = {}) {
778
802
  let mainSoftWrapped = new Array(rows).fill(false);
779
803
  let altSoftWrapped = new Array(rows).fill(false);
780
804
  let softWrapped = mainSoftWrapped;
805
+ let mainLineAttrs = new Array(rows).fill(0);
806
+ let altLineAttrs = new Array(rows).fill(0);
807
+ let lineAttrs = mainLineAttrs;
781
808
  let scrollbackSoftWrapped = [];
809
+ let scrollbackLineAttrs = [];
810
+ /**
811
+ * Drop every banked scrollback row AND its soft-wrap flag.
812
+ *
813
+ * These two arrays are one data structure wearing two names, and nothing in
814
+ * the type system says so. `ESC [ 3 J` (erase display + scrollback) once
815
+ * emptied the rows and left the flags, which diverged the pair permanently
816
+ * by however many rows were banked at that instant. Every later push
817
+ * extended both and preserved the gap, so the snapshot codec refused to
818
+ * write a frame — `soft-wrap length 3580 != rowCount 2103` — and the seat
819
+ * kept running with its recording silently ended
820
+ * (@i/5-agent-loop/vterm-codec-ends-recording).
821
+ *
822
+ * `ESC [ 3 J` is not exotic: `clear` emits it on a modern terminal. Every
823
+ * clear goes through here so the pairing is structural rather than
824
+ * remembered at each call site.
825
+ */
826
+ /** Dense per-row attributes -> sparse `[row, attr]` pairs, skipping normal rows. */
827
+ function toSparseAttrs(dense) {
828
+ const out = [];
829
+ for (let r = 0; r < dense.length; r++) {
830
+ const a = dense[r] ?? 0;
831
+ if (a !== 0) out.push([r, a]);
832
+ }
833
+ return out;
834
+ }
835
+ /** Sparse pairs -> a dense array of `length`; out-of-range rows are dropped. */
836
+ function fromSparseAttrs(sparse, length) {
837
+ const dense = new Array(length).fill(0);
838
+ for (const pair of sparse ?? []) {
839
+ const [r, a] = pair;
840
+ if (Number.isInteger(r) && r >= 0 && r < length) dense[r] = a;
841
+ }
842
+ return dense;
843
+ }
844
+ function clearScrollback() {
845
+ scrollback.length = 0;
846
+ scrollbackSoftWrapped.length = 0;
847
+ scrollbackLineAttrs.length = 0;
848
+ }
849
+ /**
850
+ * Bank one row into scrollback, trimming to the limit. Returns how many rows
851
+ * were trimmed off the front so a caller tracking absolute rows can rebase.
852
+ *
853
+ * Every scrollback mutation goes through a helper that moves BOTH arrays —
854
+ * see `clearScrollback` for what one unpaired mutation cost.
855
+ */
856
+ function bankScrollbackRow(row, wrapped, lineAttr) {
857
+ scrollback.push(row);
858
+ scrollbackSoftWrapped.push(wrapped);
859
+ scrollbackLineAttrs.push(lineAttr);
860
+ if (scrollback.length <= scrollbackLimit * 2) return 0;
861
+ const over = scrollback.length - scrollbackLimit;
862
+ scrollback.splice(0, over);
863
+ scrollbackSoftWrapped.splice(0, over);
864
+ scrollbackLineAttrs.splice(0, over);
865
+ return over;
866
+ }
782
867
  let lastChar = "";
783
868
  let pendingRegionalIndicator = null;
784
869
  let afterZWJ = false;
@@ -873,13 +958,12 @@ function createScreen(options = {}) {
873
958
  if (bytes.length === 0) return 0;
874
959
  let continuationCount = 0;
875
960
  let leadIndex = bytes.length - 1;
876
- while (leadIndex >= 0 && (bytes[leadIndex] & 192) === 128 && continuationCount < 3) {
961
+ while (leadIndex >= 0 && (itemAt(bytes, leadIndex, "UTF-8 pending bytes") & 192) === 128 && continuationCount < 3) {
877
962
  continuationCount++;
878
963
  leadIndex--;
879
964
  }
880
965
  if (leadIndex < 0) return bytes.length;
881
- const lead = bytes[leadIndex];
882
- const expectedLength = utf8SequenceLength(lead);
966
+ const expectedLength = utf8SequenceLength(itemAt(bytes, leadIndex, "UTF-8 lead byte"));
883
967
  if (expectedLength === 0) return bytes.length;
884
968
  return expectedLength > bytes.length - leadIndex ? leadIndex : bytes.length;
885
969
  }
@@ -912,6 +996,16 @@ function createScreen(options = {}) {
912
996
  fn(cells);
913
997
  row.replaceAllFromCells(cells);
914
998
  }
999
+ /**
1000
+ * SGR 0 resets the pen but NOT DECSCA: character protection is independent
1001
+ * of SGR, and is cleared only by DECSTR/RIS. Using the full `resetAttrs()`
1002
+ * here would let any `ESC [ 0 m` silently unprotect subsequent writes.
1003
+ */
1004
+ function resetSgrAttrs() {
1005
+ const next = resetAttrs();
1006
+ next.protected = attrs.protected;
1007
+ return next;
1008
+ }
915
1009
  function resetAttrs() {
916
1010
  return {
917
1011
  fg: null,
@@ -926,37 +1020,69 @@ function createScreen(options = {}) {
926
1020
  inverse: false,
927
1021
  hidden: false,
928
1022
  blink: false,
929
- url: null
1023
+ url: null,
1024
+ protected: false
930
1025
  };
931
1026
  }
1027
+ /**
1028
+ * Usable columns on a row. A DEC double-width line (DECDWL/DECDHL) renders
1029
+ * each cell twice as wide, so the row addresses and wraps at half the
1030
+ * terminal's columns — an 80-column screen wraps a double-width line at 40.
1031
+ */
1032
+ function rowCols(row) {
1033
+ return (lineAttrs[row] ?? 0) === 0 ? cols : cols >> 1;
1034
+ }
932
1035
  function clampCursor() {
933
1036
  if (curX < 0) curX = 0;
934
- if (curX >= cols) curX = cols - 1;
935
1037
  if (curY < 0) curY = 0;
936
1038
  if (curY >= rows) curY = rows - 1;
1039
+ const usable = rowCols(curY);
1040
+ if (curX >= usable) curX = usable - 1;
1041
+ if (originMode) {
1042
+ if (curY < scrollTop) curY = scrollTop;
1043
+ if (curY > scrollBottom) curY = scrollBottom;
1044
+ }
1045
+ }
1046
+ /**
1047
+ * Move the cursor down one line, scrolling ONLY at the region's bottom edge.
1048
+ *
1049
+ * `curY++; if (curY > scrollBottom) { curY = scrollBottom; scrollUp() }`
1050
+ * looks equivalent and is not. From a row BELOW the scrolling region it
1051
+ * clamps the cursor BACKWARDS into the region and scrolls a region the
1052
+ * cursor was never in — with DECSTBM at 1;10, a linefeed from row 19 landed
1053
+ * on row 9 instead of row 20.
1054
+ *
1055
+ * DECSTBM confines SCROLLING, not movement: a cursor outside the region
1056
+ * simply walks the screen.
1057
+ */
1058
+ function lineFeedDown() {
1059
+ if (curY === scrollBottom) scrollUp(scrollTop, scrollBottom);
1060
+ else if (curY < rows - 1) curY++;
1061
+ }
1062
+ /** RI's mirror: scroll only at the region's top edge, else walk up. */
1063
+ function reverseIndexUp() {
1064
+ if (curY === scrollTop) scrollDown(scrollTop, scrollBottom);
1065
+ else if (curY > 0) curY--;
937
1066
  }
938
1067
  function scrollUp(top, bottom) {
939
1068
  if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
940
1069
  const lm = leftMargin;
941
1070
  const rm = rightMargin;
942
1071
  for (let r = top; r < bottom; r++) {
943
- const srcRow = grid[r + 1];
944
- const dstRow = grid[r];
1072
+ const srcRow = itemAt(grid, r + 1, "scroll source rows");
1073
+ const dstRow = itemAt(grid, r, "scroll destination rows");
945
1074
  for (let c = lm; c <= rm && c < cols; c++) dstRow.copyCellFrom(c, srcRow, c);
946
1075
  }
947
- const bottomRow = grid[bottom];
1076
+ const bottomRow = itemAt(grid, bottom, "scroll bottom row");
948
1077
  for (let c = lm; c <= rm && c < cols; c++) bottomRow.setEmpty(c);
949
1078
  markScreenRowsDirty(top, bottom);
950
1079
  } else {
951
1080
  const enteredScrollback = grid === mainGrid && top === 0;
952
1081
  if (enteredScrollback) {
953
- scrollback.push(grid[0]);
954
- scrollbackSoftWrapped.push(softWrapped[0] ?? false);
955
- markAbsoluteRowDirty(scrollback.length - 1);
956
- if (scrollback.length > scrollbackLimit * 2) {
957
- const over = scrollback.length - scrollbackLimit;
958
- scrollback.splice(0, over);
959
- scrollbackSoftWrapped.splice(0, over);
1082
+ const banked = grid[0];
1083
+ const over = banked === void 0 ? 0 : bankScrollbackRow(banked, softWrapped[0] ?? false, lineAttrs[0] ?? 0);
1084
+ markAbsoluteRowDirty(scrollback.length - 1 + over);
1085
+ if (over > 0) {
960
1086
  if (!dirtyAll && dirtyRows.size > 0) {
961
1087
  const shifted = /* @__PURE__ */ new Set();
962
1088
  for (const r of dirtyRows) if (r >= over) shifted.add(r - over);
@@ -990,16 +1116,16 @@ function createScreen(options = {}) {
990
1116
  const lm = leftMargin;
991
1117
  const rm = rightMargin;
992
1118
  for (let r = bottom; r > top; r--) {
993
- const srcRow = grid[r - 1];
994
- const dstRow = grid[r];
1119
+ const srcRow = itemAt(grid, r - 1, "reverse-scroll source rows");
1120
+ const dstRow = itemAt(grid, r, "reverse-scroll destination rows");
995
1121
  for (let c = lm; c <= rm && c < cols; c++) dstRow.copyCellFrom(c, srcRow, c);
996
1122
  }
997
- const topRow = grid[top];
1123
+ const topRow = itemAt(grid, top, "reverse-scroll top row");
998
1124
  for (let c = lm; c <= rm && c < cols; c++) topRow.setEmpty(c);
999
1125
  } else {
1000
1126
  for (let i = bottom; i > top; i--) {
1001
- grid[i] = grid[i - 1];
1002
- softWrapped[i] = softWrapped[i - 1];
1127
+ grid[i] = itemAt(grid, i - 1, "reverse-scroll source rows");
1128
+ softWrapped[i] = itemAt(softWrapped, i - 1, "reverse-scroll source wrap flags");
1003
1129
  }
1004
1130
  grid[top] = makeRow(cols);
1005
1131
  softWrapped[top] = false;
@@ -1024,7 +1150,7 @@ function createScreen(options = {}) {
1024
1150
  if (prevRow < 0) return null;
1025
1151
  prevCol = cols - 1;
1026
1152
  }
1027
- const row = grid[prevRow];
1153
+ const row = itemAt(grid, prevRow, "previous-cell rows");
1028
1154
  if (prevCol > 0 && row.getChar(prevCol) === "" && row.isWide(prevCol - 1)) prevCol--;
1029
1155
  if (row.isEmpty(prevCol)) return null;
1030
1156
  return {
@@ -1038,7 +1164,7 @@ function createScreen(options = {}) {
1038
1164
  row.widen(col);
1039
1165
  }
1040
1166
  function writeChar(ch) {
1041
- if (charsetG0 && ch.length === 1) {
1167
+ if ((activeG1 ? charsetG1 : charsetG0) && ch.length === 1) {
1042
1168
  const mapped = DEC_SPECIAL_GRAPHICS[ch];
1043
1169
  if (mapped) ch = mapped;
1044
1170
  }
@@ -1088,14 +1214,16 @@ function createScreen(options = {}) {
1088
1214
  return;
1089
1215
  }
1090
1216
  }
1091
- if (isRegionalIndicator(codePoint)) if (pendingRegionalIndicator !== null) {
1092
- const flag = pendingRegionalIndicator + ch;
1093
- pendingRegionalIndicator = null;
1094
- writeCharCore(flag, true);
1095
- return;
1096
- } else {
1097
- pendingRegionalIndicator = ch;
1098
- return;
1217
+ if (isRegionalIndicator(codePoint)) {
1218
+ if (pendingRegionalIndicator !== null) {
1219
+ const flag = pendingRegionalIndicator + ch;
1220
+ pendingRegionalIndicator = null;
1221
+ writeCharCore(flag, true);
1222
+ return;
1223
+ } else {
1224
+ pendingRegionalIndicator = ch;
1225
+ return;
1226
+ }
1099
1227
  }
1100
1228
  if (pendingRegionalIndicator !== null) {
1101
1229
  const ri = pendingRegionalIndicator;
@@ -1108,20 +1236,22 @@ function createScreen(options = {}) {
1108
1236
  function writeCharCore(ch, wide) {
1109
1237
  if (cols <= 0 || rows <= 0) return;
1110
1238
  const charWidth = wide ? 2 : 1;
1111
- const wrapBoundary = leftRightMarginMode ? rightMargin + 1 : cols;
1239
+ const wrapBoundary = leftRightMarginMode ? rightMargin + 1 : rowCols(curY);
1112
1240
  const wrapReturn = leftRightMarginMode ? leftMargin : 0;
1113
- if (curX + charWidth > wrapBoundary) if (autoWrap) {
1114
- softWrapped[curY] = true;
1115
- curX = wrapReturn;
1116
- curY++;
1117
- if (curY > scrollBottom) {
1118
- curY = scrollBottom;
1119
- scrollUp(scrollTop, scrollBottom);
1120
- }
1121
- } else curX = wrapBoundary - charWidth;
1241
+ if (curX + charWidth > wrapBoundary) {
1242
+ if (autoWrap) {
1243
+ softWrapped[curY] = true;
1244
+ curX = wrapReturn;
1245
+ curY++;
1246
+ if (curY > scrollBottom) {
1247
+ curY = scrollBottom;
1248
+ scrollUp(scrollTop, scrollBottom);
1249
+ }
1250
+ } else curX = wrapBoundary - charWidth;
1251
+ }
1122
1252
  if (insertMode) {
1123
1253
  const insertEnd = leftRightMarginMode ? rightMargin + 1 : cols;
1124
- mutateRowAsCells(grid[curY], (cells) => {
1254
+ mutateRowAsCells(itemAt(grid, curY, "cursor row"), (cells) => {
1125
1255
  for (let i = 0; i < charWidth; i++) {
1126
1256
  cells.splice(insertEnd - 1, 1);
1127
1257
  cells.splice(curX, 0, emptyCell());
@@ -1133,6 +1263,7 @@ function createScreen(options = {}) {
1133
1263
  if (wide) widenCell(row, curX);
1134
1264
  markScreenRowDirty(curY);
1135
1265
  curX += charWidth;
1266
+ if (!autoWrap && curX >= wrapBoundary) curX = wrapBoundary - 1;
1136
1267
  lastChar = ch;
1137
1268
  }
1138
1269
  function handleCSI(params, intermediates, finalByte) {
@@ -1163,12 +1294,15 @@ function createScreen(options = {}) {
1163
1294
  case 6:
1164
1295
  cursorShape = "bar";
1165
1296
  cursorBlinking = false;
1166
- break;
1167
1297
  }
1168
1298
  else if (finalByte === "@") handleShiftLeft(Math.max(parts[0] ?? 1, 1));
1169
1299
  else if (finalByte === "A") handleShiftRight(Math.max(parts[0] ?? 1, 1));
1170
1300
  return;
1171
1301
  }
1302
+ if (intermediates === "\"" && finalByte === "q") {
1303
+ attrs.protected = (parts[0] ?? 0) === 1;
1304
+ return;
1305
+ }
1172
1306
  if (intermediates === "!") {
1173
1307
  if (finalByte === "p") softReset();
1174
1308
  return;
@@ -1188,7 +1322,7 @@ function createScreen(options = {}) {
1188
1322
  const { t, l, b, r } = normalizeRect(parts[1] ?? 1, parts[2] ?? 1, parts[3] ?? rows, parts[4] ?? cols);
1189
1323
  const fillChar = String.fromCharCode(charCode);
1190
1324
  for (let row = t; row <= b && row < rows; row++) {
1191
- const gr = grid[row];
1325
+ const gr = itemAt(grid, row, "rectangular erase rows");
1192
1326
  for (let col = l; col <= r && col < cols; col++) {
1193
1327
  const cell = emptyCell();
1194
1328
  cell.char = fillChar;
@@ -1199,7 +1333,7 @@ function createScreen(options = {}) {
1199
1333
  } else if (finalByte === "z" || finalByte === "{") {
1200
1334
  const { t, l, b, r } = normalizeRect(parts[0] ?? 1, parts[1] ?? 1, parts[2] ?? rows, parts[3] ?? cols);
1201
1335
  for (let row = t; row <= b && row < rows; row++) {
1202
- const gr = grid[row];
1336
+ const gr = itemAt(grid, row, "rectangular fill rows");
1203
1337
  for (let col = l; col <= r && col < cols; col++) gr.setEmpty(col);
1204
1338
  }
1205
1339
  markScreenRowsDirty(t, Math.min(b, rows - 1));
@@ -1219,11 +1353,11 @@ function createScreen(options = {}) {
1219
1353
  for (let row = 0; row < h; row++) {
1220
1354
  const dr = dstTop + row;
1221
1355
  if (dr < 0 || dr >= rows) continue;
1222
- const dstRow = grid[dr];
1356
+ const dstRow = itemAt(grid, dr, "rectangular copy destination rows");
1223
1357
  for (let col = 0; col < w; col++) {
1224
1358
  const dc = dstLeft + col;
1225
1359
  if (dc < 0 || dc >= cols) continue;
1226
- dstRow.setCellRaw(dc, snapshot[row][col]);
1360
+ dstRow.setCellRaw(dc, itemAt(itemAt(snapshot, row, "rectangular copy rows"), col, "rectangular copy cells"));
1227
1361
  }
1228
1362
  }
1229
1363
  markScreenRowsDirty(Math.max(0, dstTop), Math.min(dstTop + h - 1, rows - 1));
@@ -1232,7 +1366,7 @@ function createScreen(options = {}) {
1232
1366
  const sgrParts = parts.slice(4);
1233
1367
  const reverse = finalByte === "t";
1234
1368
  for (let row = t; row <= b && row < rows; row++) {
1235
- const gr = grid[row];
1369
+ const gr = itemAt(grid, row, "rectangular attribute rows");
1236
1370
  for (let col = l; col <= r && col < cols; col++) {
1237
1371
  const cell = gr.getCellRaw(col);
1238
1372
  applyRectAttrs(cell, sgrParts, reverse);
@@ -1278,18 +1412,22 @@ function createScreen(options = {}) {
1278
1412
  }
1279
1413
  switch (finalByte) {
1280
1414
  case "A":
1415
+ case "k":
1281
1416
  curY -= Math.max(parts[0] ?? 1, 1);
1282
1417
  clampCursor();
1283
1418
  break;
1284
1419
  case "B":
1420
+ case "e":
1285
1421
  curY += Math.max(parts[0] ?? 1, 1);
1286
1422
  clampCursor();
1287
1423
  break;
1288
1424
  case "C":
1425
+ case "a":
1289
1426
  curX += Math.max(parts[0] ?? 1, 1);
1290
1427
  clampCursor();
1291
1428
  break;
1292
1429
  case "D":
1430
+ case "j":
1293
1431
  curX -= Math.max(parts[0] ?? 1, 1);
1294
1432
  clampCursor();
1295
1433
  break;
@@ -1342,21 +1480,21 @@ function createScreen(options = {}) {
1342
1480
  case 20:
1343
1481
  onResponse(`\x1b]L${title}\x1b\\`);
1344
1482
  break;
1345
- case 21:
1346
- onResponse(`\x1b]l${title}\x1b\\`);
1347
- break;
1483
+ case 21: onResponse(`\x1b]l${title}\x1b\\`);
1348
1484
  }
1349
1485
  break;
1350
1486
  }
1351
1487
  case "H":
1352
1488
  case "f":
1353
1489
  if (originMode) {
1490
+ const originX = leftRightMarginMode ? leftMargin : 0;
1491
+ const limitX = leftRightMarginMode ? rightMargin : cols - 1;
1354
1492
  curY = scrollTop + (parts[0] ?? 1) - 1;
1355
- curX = (parts[1] ?? 1) - 1;
1493
+ curX = originX + (parts[1] ?? 1) - 1;
1356
1494
  if (curY < scrollTop) curY = scrollTop;
1357
1495
  if (curY > scrollBottom) curY = scrollBottom;
1358
- if (curX < 0) curX = 0;
1359
- if (curX >= cols) curX = cols - 1;
1496
+ if (curX < originX) curX = originX;
1497
+ if (curX > limitX) curX = limitX;
1360
1498
  } else {
1361
1499
  curY = (parts[0] ?? 1) - 1;
1362
1500
  curX = (parts[1] ?? 1) - 1;
@@ -1427,10 +1565,16 @@ function createScreen(options = {}) {
1427
1565
  }
1428
1566
  break;
1429
1567
  case "h":
1430
- for (const code of parts) if (code === 4) insertMode = true;
1568
+ for (const code of parts) {
1569
+ if (code === 4) insertMode = true;
1570
+ if (code === 20) newLineMode = true;
1571
+ }
1431
1572
  break;
1432
1573
  case "l":
1433
- for (const code of parts) if (code === 4) insertMode = false;
1574
+ for (const code of parts) {
1575
+ if (code === 4) insertMode = false;
1576
+ if (code === 20) newLineMode = false;
1577
+ }
1434
1578
  break;
1435
1579
  case "s":
1436
1580
  if (leftRightMarginMode && intermediates === "") {
@@ -1455,8 +1599,6 @@ function createScreen(options = {}) {
1455
1599
  curX = savedCurX;
1456
1600
  curY = savedCurY;
1457
1601
  clampCursor();
1458
- break;
1459
- default: break;
1460
1602
  }
1461
1603
  }
1462
1604
  function handleCSIGt(params, _intermediates, finalByte) {
@@ -1551,6 +1693,9 @@ function createScreen(options = {}) {
1551
1693
  case 4:
1552
1694
  value = insertMode ? 1 : 2;
1553
1695
  break;
1696
+ case 20:
1697
+ value = newLineMode ? 1 : 2;
1698
+ break;
1554
1699
  case 3:
1555
1700
  value = decColumnMode ? 1 : 2;
1556
1701
  break;
@@ -1560,9 +1705,7 @@ function createScreen(options = {}) {
1560
1705
  case 1007:
1561
1706
  value = altScrollMode ? 1 : 2;
1562
1707
  break;
1563
- case 1048:
1564
- value = 2;
1565
- break;
1708
+ case 1048: value = 2;
1566
1709
  }
1567
1710
  onResponse(`\x1b[?${mode};${value}$y`);
1568
1711
  }
@@ -1577,11 +1720,11 @@ function createScreen(options = {}) {
1577
1720
  return;
1578
1721
  }
1579
1722
  if (finalByte === "J") {
1580
- handleEraseDisplay(parts[0] ?? 0);
1723
+ handleEraseDisplay(parts[0] ?? 0, true);
1581
1724
  return;
1582
1725
  }
1583
1726
  if (finalByte === "K") {
1584
- handleEraseLine(parts[0] ?? 0);
1727
+ handleEraseLine(parts[0] ?? 0, true);
1585
1728
  return;
1586
1729
  }
1587
1730
  const set = finalByte === "h";
@@ -1610,11 +1753,13 @@ function createScreen(options = {}) {
1610
1753
  useAltScreen = true;
1611
1754
  grid = altGrid;
1612
1755
  softWrapped = altSoftWrapped;
1756
+ lineAttrs = altLineAttrs;
1613
1757
  markAllDirty();
1614
1758
  } else if (!set && useAltScreen) {
1615
1759
  useAltScreen = false;
1616
1760
  grid = mainGrid;
1617
1761
  softWrapped = mainSoftWrapped;
1762
+ lineAttrs = mainLineAttrs;
1618
1763
  markAllDirty();
1619
1764
  }
1620
1765
  break;
@@ -1643,8 +1788,10 @@ function createScreen(options = {}) {
1643
1788
  useAltScreen = true;
1644
1789
  altGrid = makeGrid(cols, rows);
1645
1790
  altSoftWrapped = new Array(rows).fill(false);
1791
+ altLineAttrs = new Array(rows).fill(0);
1646
1792
  grid = altGrid;
1647
1793
  softWrapped = altSoftWrapped;
1794
+ lineAttrs = altLineAttrs;
1648
1795
  curX = 0;
1649
1796
  curY = 0;
1650
1797
  markAllDirty();
@@ -1652,6 +1799,7 @@ function createScreen(options = {}) {
1652
1799
  useAltScreen = false;
1653
1800
  grid = mainGrid;
1654
1801
  softWrapped = mainSoftWrapped;
1802
+ lineAttrs = mainLineAttrs;
1655
1803
  curX = savedCurX;
1656
1804
  curY = savedCurY;
1657
1805
  clampCursor();
@@ -1686,55 +1834,53 @@ function createScreen(options = {}) {
1686
1834
  case 1007:
1687
1835
  altScrollMode = set;
1688
1836
  break;
1689
- case 1048:
1690
- if (set) {
1691
- savedCurX = curX;
1692
- savedCurY = curY;
1693
- } else {
1694
- curX = savedCurX;
1695
- curY = savedCurY;
1696
- clampCursor();
1697
- }
1698
- break;
1837
+ case 1048: if (set) {
1838
+ savedCurX = curX;
1839
+ savedCurY = curY;
1840
+ } else {
1841
+ curX = savedCurX;
1842
+ curY = savedCurY;
1843
+ clampCursor();
1844
+ }
1699
1845
  }
1700
1846
  }
1701
- function handleEraseDisplay(mode) {
1847
+ function handleEraseDisplay(mode, selective = false) {
1702
1848
  switch (mode) {
1703
1849
  case 0:
1704
- eraseCells(curY, curX, curY, cols - 1);
1705
- for (let row = curY + 1; row < rows; row++) eraseCells(row, 0, row, cols - 1);
1850
+ eraseCells(curY, curX, curY, cols - 1, selective);
1851
+ for (let row = curY + 1; row < rows; row++) eraseCells(row, 0, row, cols - 1, selective);
1706
1852
  break;
1707
1853
  case 1:
1708
- for (let row = 0; row < curY; row++) eraseCells(row, 0, row, cols - 1);
1709
- eraseCells(curY, 0, curY, curX);
1854
+ for (let row = 0; row < curY; row++) eraseCells(row, 0, row, cols - 1, selective);
1855
+ eraseCells(curY, 0, curY, curX, selective);
1710
1856
  break;
1711
1857
  case 2:
1712
1858
  case 3:
1713
- for (let row = 0; row < rows; row++) eraseCells(row, 0, row, cols - 1);
1714
- if (mode === 3) scrollback.length = 0;
1859
+ for (let row = 0; row < rows; row++) eraseCells(row, 0, row, cols - 1, selective);
1860
+ if (mode === 3) clearScrollback();
1715
1861
  markAllDirty();
1716
- break;
1717
1862
  }
1718
1863
  }
1719
- function handleEraseLine(mode) {
1864
+ function handleEraseLine(mode, selective = false) {
1720
1865
  const eraseRight = leftRightMarginMode ? rightMargin : cols - 1;
1721
1866
  const eraseLeft = leftRightMarginMode ? leftMargin : 0;
1722
1867
  switch (mode) {
1723
1868
  case 0:
1724
- eraseCells(curY, curX, curY, eraseRight);
1869
+ eraseCells(curY, curX, curY, eraseRight, selective);
1725
1870
  break;
1726
1871
  case 1:
1727
- eraseCells(curY, eraseLeft, curY, curX);
1728
- break;
1729
- case 2:
1730
- eraseCells(curY, eraseLeft, curY, eraseRight);
1872
+ eraseCells(curY, eraseLeft, curY, curX, selective);
1731
1873
  break;
1874
+ case 2: eraseCells(curY, eraseLeft, curY, eraseRight, selective);
1732
1875
  }
1733
1876
  }
1734
- function eraseCells(row, startCol, _endRow, endCol) {
1877
+ function eraseCells(row, startCol, _endRow, endCol, selective = false) {
1735
1878
  const r = grid[row];
1736
1879
  if (!r) return;
1737
- for (let col = startCol; col <= endCol && col < cols; col++) r.eraseWithBg(col, attrs.bg);
1880
+ for (let col = startCol; col <= endCol && col < cols; col++) {
1881
+ if (selective && r.getCellRaw(col).protected) continue;
1882
+ r.eraseWithBg(col, attrs.bg);
1883
+ }
1738
1884
  markScreenRowDirty(row);
1739
1885
  }
1740
1886
  function handleInsertLines(count) {
@@ -1878,9 +2024,7 @@ function createScreen(options = {}) {
1878
2024
  case 25:
1879
2025
  cell.blink = false;
1880
2026
  break;
1881
- case 27:
1882
- cell.inverse = false;
1883
- break;
2027
+ case 27: cell.inverse = false;
1884
2028
  }
1885
2029
  }
1886
2030
  function handleSGR(rawParams) {
@@ -1890,18 +2034,18 @@ function createScreen(options = {}) {
1890
2034
  for (const seg of segments) if (seg.includes(":")) {
1891
2035
  const subs = seg.split(":").map((s) => s === "" ? 0 : parseInt(s, 10));
1892
2036
  subParams.set(params.length, subs);
1893
- params.push(subs[0]);
2037
+ params.push(itemAt(subs, 0, "SGR sub-parameters"));
1894
2038
  } else params.push(seg === "" ? 0 : parseInt(seg, 10));
1895
2039
  if (params.length === 0 || params.length === 1 && params[0] === 0) {
1896
- attrs = resetAttrs();
2040
+ attrs = resetSgrAttrs();
1897
2041
  return;
1898
2042
  }
1899
2043
  let i = 0;
1900
2044
  while (i < params.length) {
1901
- const code = params[i];
2045
+ const code = itemAt(params, i, "SGR parameters");
1902
2046
  switch (code) {
1903
2047
  case 0:
1904
- attrs = resetAttrs();
2048
+ attrs = resetSgrAttrs();
1905
2049
  break;
1906
2050
  case 1:
1907
2051
  attrs.bold = true;
@@ -1914,7 +2058,7 @@ function createScreen(options = {}) {
1914
2058
  break;
1915
2059
  case 4: {
1916
2060
  const subs = subParams.get(i);
1917
- if (subs && subs.length > 1) switch (subs[1]) {
2061
+ if (subs && subs.length > 1) switch (itemAt(subs, 1, "underline style sub-parameters")) {
1918
2062
  case 0:
1919
2063
  attrs.underline = "none";
1920
2064
  break;
@@ -1933,9 +2077,7 @@ function createScreen(options = {}) {
1933
2077
  case 5:
1934
2078
  attrs.underline = "dashed";
1935
2079
  break;
1936
- default:
1937
- attrs.underline = "single";
1938
- break;
2080
+ default: attrs.underline = "single";
1939
2081
  }
1940
2082
  else attrs.underline = "single";
1941
2083
  break;
@@ -1989,7 +2131,7 @@ function createScreen(options = {}) {
1989
2131
  case 36:
1990
2132
  case 37:
1991
2133
  attrs.fg = {
1992
- ...palette256[code - 30],
2134
+ ...itemAt(palette256, code - 30, "basic foreground palette"),
1993
2135
  index: code - 30
1994
2136
  };
1995
2137
  break;
@@ -2019,7 +2161,7 @@ function createScreen(options = {}) {
2019
2161
  case 46:
2020
2162
  case 47:
2021
2163
  attrs.bg = {
2022
- ...palette256[code - 40],
2164
+ ...itemAt(palette256, code - 40, "basic background palette"),
2023
2165
  index: code - 40
2024
2166
  };
2025
2167
  break;
@@ -2072,7 +2214,7 @@ function createScreen(options = {}) {
2072
2214
  case 96:
2073
2215
  case 97:
2074
2216
  attrs.fg = {
2075
- ...palette256[code - 90 + 8],
2217
+ ...itemAt(palette256, code - 90 + 8, "bright foreground palette"),
2076
2218
  index: code - 90 + 8
2077
2219
  };
2078
2220
  break;
@@ -2083,12 +2225,10 @@ function createScreen(options = {}) {
2083
2225
  case 104:
2084
2226
  case 105:
2085
2227
  case 106:
2086
- case 107:
2087
- attrs.bg = {
2088
- ...palette256[code - 100 + 8],
2089
- index: code - 100 + 8
2090
- };
2091
- break;
2228
+ case 107: attrs.bg = {
2229
+ ...itemAt(palette256, code - 100 + 8, "bright background palette"),
2230
+ index: code - 100 + 8
2231
+ };
2092
2232
  }
2093
2233
  i++;
2094
2234
  }
@@ -2097,7 +2237,7 @@ function createScreen(options = {}) {
2097
2237
  if (startIndex + 1 >= params.length) return null;
2098
2238
  const type = params[startIndex + 1];
2099
2239
  if (type === 5 && startIndex + 2 < params.length) {
2100
- const idx = params[startIndex + 2];
2240
+ const idx = itemAt(params, startIndex + 2, "indexed color parameters");
2101
2241
  const entry = palette256[idx];
2102
2242
  return {
2103
2243
  color: entry ? {
@@ -2112,9 +2252,9 @@ function createScreen(options = {}) {
2112
2252
  };
2113
2253
  } else if (type === 2 && startIndex + 4 < params.length) return {
2114
2254
  color: {
2115
- r: params[startIndex + 2],
2116
- g: params[startIndex + 3],
2117
- b: params[startIndex + 4]
2255
+ r: itemAt(params, startIndex + 2, "truecolor red parameter"),
2256
+ g: itemAt(params, startIndex + 3, "truecolor green parameter"),
2257
+ b: itemAt(params, startIndex + 4, "truecolor blue parameter")
2118
2258
  },
2119
2259
  nextIndex: startIndex + 5
2120
2260
  };
@@ -2125,7 +2265,7 @@ function createScreen(options = {}) {
2125
2265
  if (subs.length < 3) return null;
2126
2266
  const type = subs[1];
2127
2267
  if (type === 5 && subs.length >= 3) {
2128
- const idx = subs[2];
2268
+ const idx = itemAt(subs, 2, "indexed color sub-parameters");
2129
2269
  const entry = palette256[idx];
2130
2270
  return entry ? {
2131
2271
  ...entry,
@@ -2138,18 +2278,18 @@ function createScreen(options = {}) {
2138
2278
  } else if (type === 2) {
2139
2279
  if (subs.length >= 5) {
2140
2280
  if (subs.length >= 6) return {
2141
- r: subs[3],
2142
- g: subs[4],
2143
- b: subs[5]
2281
+ r: itemAt(subs, 3, "colorspace red sub-parameter"),
2282
+ g: itemAt(subs, 4, "colorspace green sub-parameter"),
2283
+ b: itemAt(subs, 5, "colorspace blue sub-parameter")
2144
2284
  };
2145
2285
  return {
2146
- r: subs[2],
2147
- g: subs[3],
2148
- b: subs[4]
2286
+ r: itemAt(subs, 2, "truecolor red sub-parameter"),
2287
+ g: itemAt(subs, 3, "truecolor green sub-parameter"),
2288
+ b: itemAt(subs, 4, "truecolor blue sub-parameter")
2149
2289
  };
2150
2290
  } else if (subs.length >= 4) return {
2151
- r: subs[2],
2152
- g: subs[3],
2291
+ r: itemAt(subs, 2, "partial truecolor red sub-parameter"),
2292
+ g: itemAt(subs, 3, "partial truecolor green sub-parameter"),
2153
2293
  b: 0
2154
2294
  };
2155
2295
  }
@@ -2181,14 +2321,11 @@ function createScreen(options = {}) {
2181
2321
  startCol: curX
2182
2322
  });
2183
2323
  break;
2184
- case "C":
2185
- semanticZones.push({
2186
- type: "output",
2187
- startRow: curY,
2188
- startCol: curX
2189
- });
2190
- break;
2191
- case "D": break;
2324
+ case "C": semanticZones.push({
2325
+ type: "output",
2326
+ startRow: curY,
2327
+ startCol: curX
2328
+ });
2192
2329
  }
2193
2330
  break;
2194
2331
  case 1: break;
@@ -2206,8 +2343,8 @@ function createScreen(options = {}) {
2206
2343
  case 4: {
2207
2344
  const fields = value.split(";");
2208
2345
  for (let fi = 0; fi + 1 < fields.length; fi += 2) {
2209
- const idx = parseInt(fields[fi], 10);
2210
- const spec = fields[fi + 1];
2346
+ const idx = parseInt(itemAt(fields, fi, "OSC 4 palette fields"), 10);
2347
+ const spec = itemAt(fields, fi + 1, "OSC 4 palette fields");
2211
2348
  if (isNaN(idx) || idx < 0 || idx > 255) continue;
2212
2349
  if (spec === "?") {
2213
2350
  const c = palette256[idx];
@@ -2222,8 +2359,8 @@ function createScreen(options = {}) {
2222
2359
  case 5: {
2223
2360
  const fields = value.split(";");
2224
2361
  for (let fi = 0; fi + 1 < fields.length; fi += 2) {
2225
- const idx = parseInt(fields[fi], 10);
2226
- const spec = fields[fi + 1];
2362
+ const idx = parseInt(itemAt(fields, fi, "OSC 5 special-color fields"), 10);
2363
+ const spec = itemAt(fields, fi + 1, "OSC 5 special-color fields");
2227
2364
  if (isNaN(idx) || idx < 0 || idx > 4) continue;
2228
2365
  if (spec === "?") {
2229
2366
  const c = specialColors.get(idx);
@@ -2333,70 +2470,76 @@ function createScreen(options = {}) {
2333
2470
  const isQuery = val === "?";
2334
2471
  const paletteMatch = /^color(\d+)$/.exec(key);
2335
2472
  if (paletteMatch) {
2336
- const idx = parseInt(paletteMatch[1], 10);
2473
+ const idx = parseInt(itemAt(paletteMatch, 1, "OSC 104 palette capture"), 10);
2337
2474
  if (idx < 0 || idx > 255) continue;
2338
2475
  if (isQuery) {
2339
2476
  const c = palette256[idx];
2340
2477
  if (c) response.push(`${key}=${formatColorResponse(c)}`);
2341
2478
  } else if (val === "") {
2342
2479
  const fresh = buildPalette256();
2343
- palette256[idx] = fresh[idx];
2480
+ palette256[idx] = itemAt(fresh, idx, "fresh ANSI palette");
2344
2481
  } else {
2345
2482
  const c = parseColorSpec(val);
2346
2483
  if (c) palette256[idx] = c;
2347
2484
  }
2348
- } else if (key === "foreground") if (isQuery) {
2349
- const c = defaultFgColor ?? {
2350
- r: 255,
2351
- g: 255,
2352
- b: 255
2353
- };
2354
- response.push(`${key}=${formatColorResponse(c)}`);
2355
- } else if (val === "") defaultFgColor = null;
2356
- else {
2357
- const c = parseColorSpec(val);
2358
- if (c) defaultFgColor = c;
2359
- }
2360
- else if (key === "background") if (isQuery) {
2361
- const c = defaultBgColor ?? {
2362
- r: 0,
2363
- g: 0,
2364
- b: 0
2365
- };
2366
- response.push(`${key}=${formatColorResponse(c)}`);
2367
- } else if (val === "") defaultBgColor = null;
2368
- else {
2369
- const c = parseColorSpec(val);
2370
- if (c) defaultBgColor = c;
2371
- }
2372
- else if (key === "cursor") if (isQuery) {
2373
- const c = cursorColor ?? defaultFgColor ?? {
2374
- r: 255,
2375
- g: 255,
2376
- b: 255
2377
- };
2378
- response.push(`${key}=${formatColorResponse(c)}`);
2379
- } else if (val === "") cursorColor = null;
2380
- else {
2381
- const c = parseColorSpec(val);
2382
- if (c) cursorColor = c;
2485
+ } else if (key === "foreground") {
2486
+ if (isQuery) {
2487
+ const c = defaultFgColor ?? {
2488
+ r: 255,
2489
+ g: 255,
2490
+ b: 255
2491
+ };
2492
+ response.push(`${key}=${formatColorResponse(c)}`);
2493
+ } else if (val === "") defaultFgColor = null;
2494
+ else {
2495
+ const c = parseColorSpec(val);
2496
+ if (c) defaultFgColor = c;
2497
+ }
2498
+ } else if (key === "background") {
2499
+ if (isQuery) {
2500
+ const c = defaultBgColor ?? {
2501
+ r: 0,
2502
+ g: 0,
2503
+ b: 0
2504
+ };
2505
+ response.push(`${key}=${formatColorResponse(c)}`);
2506
+ } else if (val === "") defaultBgColor = null;
2507
+ else {
2508
+ const c = parseColorSpec(val);
2509
+ if (c) defaultBgColor = c;
2510
+ }
2511
+ } else if (key === "cursor") {
2512
+ if (isQuery) {
2513
+ const c = cursorColor ?? defaultFgColor ?? {
2514
+ r: 255,
2515
+ g: 255,
2516
+ b: 255
2517
+ };
2518
+ response.push(`${key}=${formatColorResponse(c)}`);
2519
+ } else if (val === "") cursorColor = null;
2520
+ else {
2521
+ const c = parseColorSpec(val);
2522
+ if (c) cursorColor = c;
2523
+ }
2524
+ } else if (key === "selection_background") {
2525
+ if (isQuery) {
2526
+ const c = highlightBgColor ?? defaultFgColor ?? {
2527
+ r: 255,
2528
+ g: 255,
2529
+ b: 255
2530
+ };
2531
+ response.push(`${key}=${formatColorResponse(c)}`);
2532
+ } else highlightBgColor = val === "" ? null : parseColorSpec(val);
2533
+ } else if (key === "selection_foreground") {
2534
+ if (isQuery) {
2535
+ const c = highlightFgColor ?? defaultBgColor ?? {
2536
+ r: 0,
2537
+ g: 0,
2538
+ b: 0
2539
+ };
2540
+ response.push(`${key}=${formatColorResponse(c)}`);
2541
+ } else highlightFgColor = val === "" ? null : parseColorSpec(val);
2383
2542
  }
2384
- else if (key === "selection_background") if (isQuery) {
2385
- const c = highlightBgColor ?? defaultFgColor ?? {
2386
- r: 255,
2387
- g: 255,
2388
- b: 255
2389
- };
2390
- response.push(`${key}=${formatColorResponse(c)}`);
2391
- } else highlightBgColor = val === "" ? null : parseColorSpec(val);
2392
- else if (key === "selection_foreground") if (isQuery) {
2393
- const c = highlightFgColor ?? defaultBgColor ?? {
2394
- r: 0,
2395
- g: 0,
2396
- b: 0
2397
- };
2398
- response.push(`${key}=${formatColorResponse(c)}`);
2399
- } else highlightFgColor = val === "" ? null : parseColorSpec(val);
2400
2543
  }
2401
2544
  if (response.length > 0 && onResponse) onResponse(`\x1b]21;${response.join(";")}\x1b\\`);
2402
2545
  break;
@@ -2407,7 +2550,7 @@ function createScreen(options = {}) {
2407
2550
  const fresh = buildPalette256();
2408
2551
  for (const tok of value.split(";")) {
2409
2552
  const idx = parseInt(tok, 10);
2410
- if (!isNaN(idx) && idx >= 0 && idx < 256) palette256[idx] = fresh[idx];
2553
+ if (!isNaN(idx) && idx >= 0 && idx < 256) palette256[idx] = itemAt(fresh, idx, "fresh ANSI palette");
2411
2554
  }
2412
2555
  }
2413
2556
  break;
@@ -2478,14 +2621,11 @@ function createScreen(options = {}) {
2478
2621
  startCol: curX
2479
2622
  });
2480
2623
  break;
2481
- case "C":
2482
- semanticZones.push({
2483
- type: "output",
2484
- startRow: curY,
2485
- startCol: curX
2486
- });
2487
- break;
2488
- case "D": break;
2624
+ case "C": semanticZones.push({
2625
+ type: "output",
2626
+ startRow: curY,
2627
+ startCol: curX
2628
+ });
2489
2629
  }
2490
2630
  break;
2491
2631
  case 66:
@@ -2558,10 +2698,8 @@ function createScreen(options = {}) {
2558
2698
  advancedClipboard = "";
2559
2699
  }
2560
2700
  break;
2561
- case 1337:
2562
- if (value === "ReportCellSize" && onResponse) onResponse("\x1B]1337;ReportCellSize=16;8\x1B\\");
2563
- else if (value === "RequestCapabilities" && onResponse) onResponse("\x1B]1337;Capabilities=\x1B\\");
2564
- break;
2701
+ case 1337: if (value === "ReportCellSize" && onResponse) onResponse("\x1B]1337;ReportCellSize=16;8\x1B\\");
2702
+ else if (value === "RequestCapabilities" && onResponse) onResponse("\x1B]1337;Capabilities=\x1B\\");
2565
2703
  }
2566
2704
  }
2567
2705
  function handleDCS(data) {
@@ -2569,7 +2707,7 @@ function createScreen(options = {}) {
2569
2707
  if (match) {
2570
2708
  hasSixel = true;
2571
2709
  sixelImages.push({
2572
- data: match[2],
2710
+ data: itemAt(match, 2, "sixel payload capture"),
2573
2711
  row: dcsStartRow,
2574
2712
  col: dcsStartCol
2575
2713
  });
@@ -2602,6 +2740,7 @@ function createScreen(options = {}) {
2602
2740
  }
2603
2741
  function softReset() {
2604
2742
  insertMode = false;
2743
+ newLineMode = false;
2605
2744
  originMode = false;
2606
2745
  autoWrap = true;
2607
2746
  curVisible = true;
@@ -2622,6 +2761,8 @@ function createScreen(options = {}) {
2622
2761
  rightMargin = cols - 1;
2623
2762
  attrs = resetAttrs();
2624
2763
  charsetG0 = false;
2764
+ charsetG1 = false;
2765
+ activeG1 = false;
2625
2766
  curX = 0;
2626
2767
  curY = 0;
2627
2768
  }
@@ -2644,7 +2785,9 @@ function createScreen(options = {}) {
2644
2785
  attrs: resetAttrs(),
2645
2786
  originMode: false,
2646
2787
  autoWrap: true,
2647
- charsetG0: false
2788
+ charsetG0: false,
2789
+ charsetG1: false,
2790
+ activeG1: false
2648
2791
  };
2649
2792
  attrs = resetAttrs();
2650
2793
  title = "";
@@ -2659,6 +2802,7 @@ function createScreen(options = {}) {
2659
2802
  focusTracking = false;
2660
2803
  originMode = false;
2661
2804
  insertMode = false;
2805
+ newLineMode = false;
2662
2806
  reverseVideo = false;
2663
2807
  syncOutput = false;
2664
2808
  kittyKeyboardFlags = 0;
@@ -2682,6 +2826,8 @@ function createScreen(options = {}) {
2682
2826
  advancedClipboard = "";
2683
2827
  viewportOffset = 0;
2684
2828
  charsetG0 = false;
2829
+ charsetG1 = false;
2830
+ activeG1 = false;
2685
2831
  clipboard = "";
2686
2832
  cwd = "";
2687
2833
  notifications = [];
@@ -2717,6 +2863,9 @@ function createScreen(options = {}) {
2717
2863
  mainSoftWrapped = new Array(rows).fill(false);
2718
2864
  altSoftWrapped = new Array(rows).fill(false);
2719
2865
  softWrapped = mainSoftWrapped;
2866
+ mainLineAttrs = new Array(rows).fill(0);
2867
+ altLineAttrs = new Array(rows).fill(0);
2868
+ lineAttrs = mainLineAttrs;
2720
2869
  markAllDirty();
2721
2870
  dirtyScrolled = 0;
2722
2871
  if (sigDamageActive) sigDirtyScrolled = 0;
@@ -2868,15 +3017,18 @@ function createScreen(options = {}) {
2868
3017
  emitExecute(9);
2869
3018
  } else if (code === 10 || code === 11 || code === 12) {
2870
3019
  softWrapped[curY] = false;
2871
- curY++;
2872
- if (curY > scrollBottom) {
2873
- curY = scrollBottom;
2874
- scrollUp(scrollTop, scrollBottom);
2875
- }
3020
+ lineFeedDown();
3021
+ if (newLineMode) curX = 0;
2876
3022
  emitExecute(code);
2877
3023
  } else if (code === 13) {
2878
3024
  curX = 0;
2879
3025
  emitExecute(13);
3026
+ } else if (code === 14) {
3027
+ activeG1 = true;
3028
+ emitExecute(14);
3029
+ } else if (code === 15) {
3030
+ activeG1 = false;
3031
+ emitExecute(15);
2880
3032
  } else if (code >= 32) {
2881
3033
  let char = ch;
2882
3034
  if (code >= 55296 && code <= 56319 && i + 1 < text.length) {
@@ -2910,19 +3062,11 @@ function createScreen(options = {}) {
2910
3062
  parserState = "ground";
2911
3063
  emitEsc("c", "");
2912
3064
  } else if (ch === "D") {
2913
- curY++;
2914
- if (curY > scrollBottom) {
2915
- curY = scrollBottom;
2916
- scrollUp(scrollTop, scrollBottom);
2917
- }
3065
+ lineFeedDown();
2918
3066
  parserState = "ground";
2919
3067
  emitEsc("D", "");
2920
3068
  } else if (ch === "M") {
2921
- curY--;
2922
- if (curY < scrollTop) {
2923
- curY = scrollTop;
2924
- scrollDown(scrollTop, scrollBottom);
2925
- }
3069
+ reverseIndexUp();
2926
3070
  parserState = "ground";
2927
3071
  emitEsc("M", "");
2928
3072
  } else if (ch === "7") {
@@ -2937,7 +3081,9 @@ function createScreen(options = {}) {
2937
3081
  },
2938
3082
  originMode,
2939
3083
  autoWrap,
2940
- charsetG0
3084
+ charsetG0,
3085
+ charsetG1,
3086
+ activeG1
2941
3087
  };
2942
3088
  parserState = "ground";
2943
3089
  emitEsc("7", "");
@@ -2953,6 +3099,8 @@ function createScreen(options = {}) {
2953
3099
  originMode = savedState.originMode;
2954
3100
  autoWrap = savedState.autoWrap;
2955
3101
  charsetG0 = savedState.charsetG0;
3102
+ charsetG1 = savedState.charsetG1;
3103
+ activeG1 = savedState.activeG1;
2956
3104
  clampCursor();
2957
3105
  parserState = "ground";
2958
3106
  emitEsc("8", "");
@@ -2974,9 +3122,11 @@ function createScreen(options = {}) {
2974
3122
  parserState = "escape_hash";
2975
3123
  } else if (ch === "(") {
2976
3124
  escIntermediate = "(";
3125
+ escBuf = "(";
2977
3126
  parserState = "escape_charset";
2978
3127
  } else if (ch === ")") {
2979
3128
  escIntermediate = ")";
3129
+ escBuf = ")";
2980
3130
  parserState = "escape_charset";
2981
3131
  } else if (ch === "=") {
2982
3132
  applicationKeypad = true;
@@ -2996,23 +3146,28 @@ function createScreen(options = {}) {
2996
3146
  } else parserState = "ground";
2997
3147
  break;
2998
3148
  case "escape_charset":
2999
- if (ch === "0") charsetG0 = true;
3000
- else charsetG0 = false;
3149
+ const designator = escBuf === ")" ? ")" : "(";
3150
+ if (designator === ")") charsetG1 = ch === "0";
3151
+ else charsetG0 = ch === "0";
3152
+ escBuf = "";
3001
3153
  parserState = "ground";
3002
- emitEsc(ch, escIntermediate);
3154
+ emitEsc(ch, designator);
3003
3155
  break;
3004
3156
  case "escape_hash":
3005
3157
  if (ch === "8") {
3006
3158
  const alignCell = emptyCell();
3007
3159
  alignCell.char = "E";
3008
3160
  for (let r = 0; r < rows; r++) {
3009
- const row = grid[r];
3161
+ const row = itemAt(grid, r, "selective erase rows");
3010
3162
  for (let c = 0; c < cols; c++) row.setCellRaw(c, alignCell);
3011
3163
  softWrapped[r] = false;
3012
3164
  }
3013
3165
  curX = 0;
3014
3166
  curY = 0;
3015
- }
3167
+ } else if (ch === "3") lineAttrs[curY] = 2;
3168
+ else if (ch === "4") lineAttrs[curY] = 3;
3169
+ else if (ch === "5") lineAttrs[curY] = 0;
3170
+ else if (ch === "6") lineAttrs[curY] = 1;
3016
3171
  parserState = "ground";
3017
3172
  emitEsc(ch, escIntermediate);
3018
3173
  break;
@@ -3090,7 +3245,6 @@ function createScreen(options = {}) {
3090
3245
  apcOverflow = false;
3091
3246
  }
3092
3247
  parserState = "ground";
3093
- break;
3094
3248
  }
3095
3249
  }
3096
3250
  flushPrintRun();
@@ -3129,22 +3283,39 @@ function createScreen(options = {}) {
3129
3283
  * trailing blanks or one past the last cell) lands after the last emitted
3130
3284
  * cell, advanced by the leftover distance, clamped to the row.
3131
3285
  */
3286
+ /**
3287
+ * One line attribute per LOGICAL line, taken from the line's FIRST physical
3288
+ * row — the grouping `getLogicalLines` uses, so the two arrays line up.
3289
+ * A rewrap then re-applies the attribute to every row the line becomes.
3290
+ */
3291
+ function logicalLineAttrs(srcGrid, srcSoftWrapped, srcLineAttrs, srcRows) {
3292
+ const out = [];
3293
+ let starting = true;
3294
+ for (let r = 0; r < srcRows; r++) {
3295
+ if (!srcGrid[r]) continue;
3296
+ if (starting) out.push(srcLineAttrs[r] ?? 0);
3297
+ starting = !srcSoftWrapped[r];
3298
+ }
3299
+ return out;
3300
+ }
3132
3301
  function rewrapLines(logicalLines, newCols, track) {
3133
3302
  const outRows = [];
3134
3303
  const outWrapped = [];
3304
+ const outLine = [];
3135
3305
  let tracked = null;
3136
3306
  for (let li = 0; li < logicalLines.length; li++) {
3137
- const line = logicalLines[li];
3307
+ const line = itemAt(logicalLines, li, "reflow logical lines");
3138
3308
  let lineLen = line.length;
3139
3309
  while (lineLen > 0) {
3140
- const cell = line[lineLen - 1];
3310
+ const cell = itemAt(line, lineLen - 1, "reflow logical-line cells");
3141
3311
  if (cell.char === "" && !cell.wide) lineLen--;
3142
3312
  else break;
3143
3313
  }
3144
3314
  if (lineLen === 0) {
3145
3315
  outRows.push(makeRow(newCols));
3146
3316
  outWrapped.push(false);
3147
- if (track !== void 0 && track.line === li) tracked = {
3317
+ outLine.push(li);
3318
+ if (track?.line === li) tracked = {
3148
3319
  row: outRows.length - 1,
3149
3320
  col: Math.min(track.offset, newCols - 1)
3150
3321
  };
@@ -3155,9 +3326,9 @@ function createScreen(options = {}) {
3155
3326
  const row = makeRow(newCols);
3156
3327
  let col = 0;
3157
3328
  while (col < newCols && pos < lineLen) {
3158
- const cell = line[pos];
3329
+ const cell = itemAt(line, pos, "reflow logical-line cells");
3159
3330
  if (cell.wide && col + 2 > newCols) break;
3160
- if (track !== void 0 && track.line === li && pos === track.offset) tracked = {
3331
+ if (track?.line === li && pos === track.offset) tracked = {
3161
3332
  row: outRows.length,
3162
3333
  col
3163
3334
  };
@@ -3168,7 +3339,8 @@ function createScreen(options = {}) {
3168
3339
  const moreContent = pos < lineLen;
3169
3340
  outRows.push(row);
3170
3341
  outWrapped.push(moreContent);
3171
- if (!moreContent && track !== void 0 && track.line === li && tracked === null) {
3342
+ outLine.push(li);
3343
+ if (!moreContent && track?.line === li && tracked === null) {
3172
3344
  const extra = track.offset - lineLen;
3173
3345
  tracked = {
3174
3346
  row: outRows.length - 1,
@@ -3180,6 +3352,7 @@ function createScreen(options = {}) {
3180
3352
  return {
3181
3353
  rows: outRows,
3182
3354
  wrapped: outWrapped,
3355
+ lineIndex: outLine,
3183
3356
  tracked
3184
3357
  };
3185
3358
  }
@@ -3191,15 +3364,19 @@ function createScreen(options = {}) {
3191
3364
  /**
3192
3365
  * Trim trailing empty rows from reflowed result, so they don't push content off the top
3193
3366
  * when we take the last newRows rows.
3367
+ *
3368
+ * Never trims the row the CURSOR is on. A shell that has just emitted its
3369
+ * final newline sits on a blank last row, and dropping it makes the content
3370
+ * measure one row shorter than it is — so a shrink banks one row too few and
3371
+ * every surviving line lands one row low, taking the cursor with it.
3194
3372
  */
3195
3373
  function trimTrailingEmptyRows(result) {
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
- }
3374
+ const floor = result.tracked == null ? 1 : Math.max(1, result.tracked.row + 1);
3375
+ while (result.rows.length > floor) if (packedRowIsBlank(itemAt(result.rows, result.rows.length - 1, "reflow result rows")) && !result.wrapped[result.rows.length - 2]) {
3376
+ result.rows.pop();
3377
+ result.wrapped.pop();
3378
+ result.lineIndex?.pop();
3379
+ } else break;
3203
3380
  }
3204
3381
  function resize(newCols, newRows) {
3205
3382
  if (newCols === cols && newRows === rows) return;
@@ -3213,23 +3390,56 @@ function createScreen(options = {}) {
3213
3390
  line: cursorLineIdx,
3214
3391
  offset: (cursorRowPre - cursorLineStart) * cols + curX
3215
3392
  };
3216
- const mainResult = rewrapLines(getLogicalLines(mainGrid, mainSoftWrapped, rows), newCols, useAltScreen ? void 0 : cursorTrack);
3393
+ let restoredRows = [];
3394
+ let restoredWrapped = [];
3395
+ let restoredAttrs = [];
3396
+ if (!useAltScreen && newRows > rows && scrollback.length > 0) {
3397
+ const probe = rewrapLines(getLogicalLines(mainGrid, mainSoftWrapped, rows), newCols, void 0);
3398
+ trimTrailingEmptyRows(probe);
3399
+ const take = Math.min(Math.max(0, newRows - probe.rows.length), scrollback.length);
3400
+ if (take > 0) {
3401
+ restoredRows = scrollback.splice(scrollback.length - take, take);
3402
+ restoredWrapped = scrollbackSoftWrapped.splice(scrollbackSoftWrapped.length - take, take);
3403
+ restoredAttrs = scrollbackLineAttrs.splice(scrollbackLineAttrs.length - take, take);
3404
+ }
3405
+ }
3406
+ if (restoredRows.length > 0) {
3407
+ let completed = 0;
3408
+ for (const wrapped of restoredWrapped) if (!wrapped) completed++;
3409
+ if (cursorTrack.line === 0 && restoredWrapped[restoredWrapped.length - 1] === true) for (let i = restoredWrapped.length - 1; i >= 0 && restoredWrapped[i]; i--) cursorTrack.offset += itemAt(restoredRows, i, "restored rows").length;
3410
+ cursorTrack.line += completed;
3411
+ }
3412
+ const mainSrc = restoredRows.length > 0 ? [...restoredRows, ...mainGrid] : mainGrid;
3413
+ const mainSrcWrapped = restoredRows.length > 0 ? [...restoredWrapped, ...mainSoftWrapped] : mainSoftWrapped;
3414
+ const mainSrcAttrs = restoredRows.length > 0 ? [...restoredAttrs, ...mainLineAttrs] : mainLineAttrs;
3415
+ const mainSrcRows = restoredRows.length + rows;
3416
+ const mainLogicalAttrs = logicalLineAttrs(mainSrc, mainSrcWrapped, mainSrcAttrs, mainSrcRows);
3417
+ const mainResult = rewrapLines(getLogicalLines(mainSrc, mainSrcWrapped, mainSrcRows), newCols, useAltScreen ? void 0 : cursorTrack);
3217
3418
  trimTrailingEmptyRows(mainResult);
3419
+ const altLogicalAttrs = logicalLineAttrs(altGrid, altSoftWrapped, altLineAttrs, rows);
3218
3420
  const altResult = rewrapLines(getLogicalLines(altGrid, altSoftWrapped, rows), newCols, useAltScreen ? cursorTrack : void 0);
3219
3421
  trimTrailingEmptyRows(altResult);
3220
3422
  const newMain = makeGrid(newCols, newRows);
3221
3423
  const newMainWrapped = new Array(newRows).fill(false);
3424
+ const newMainAttrs = new Array(newRows).fill(0);
3222
3425
  const mainStartRow = Math.max(0, mainResult.rows.length - newRows);
3426
+ for (let r = 0; r < mainStartRow; r++) {
3427
+ const overflow = mainResult.rows[r];
3428
+ if (overflow !== void 0) bankScrollbackRow(overflow, mainResult.wrapped[r] ?? false, mainLogicalAttrs[mainResult.lineIndex[r] ?? 0] ?? 0);
3429
+ }
3223
3430
  for (let r = 0; r < newRows && mainStartRow + r < mainResult.rows.length; r++) {
3224
- newMain[r] = mainResult.rows[mainStartRow + r];
3225
- newMainWrapped[r] = mainResult.wrapped[mainStartRow + r];
3431
+ newMain[r] = itemAt(mainResult.rows, mainStartRow + r, "resized main rows");
3432
+ newMainWrapped[r] = itemAt(mainResult.wrapped, mainStartRow + r, "resized main wrap flags");
3433
+ newMainAttrs[r] = mainLogicalAttrs[mainResult.lineIndex[mainStartRow + r] ?? 0] ?? 0;
3226
3434
  }
3227
3435
  const newAlt = makeGrid(newCols, newRows);
3228
3436
  const newAltWrapped = new Array(newRows).fill(false);
3437
+ const newAltAttrs = new Array(newRows).fill(0);
3229
3438
  const altStartRow = Math.max(0, altResult.rows.length - newRows);
3230
3439
  for (let r = 0; r < newRows && altStartRow + r < altResult.rows.length; r++) {
3231
- newAlt[r] = altResult.rows[altStartRow + r];
3232
- newAltWrapped[r] = altResult.wrapped[altStartRow + r];
3440
+ newAlt[r] = itemAt(altResult.rows, altStartRow + r, "resized alternate rows");
3441
+ newAltWrapped[r] = itemAt(altResult.wrapped, altStartRow + r, "resized alternate wrap flags");
3442
+ newAltAttrs[r] = altLogicalAttrs[altResult.lineIndex[altStartRow + r] ?? 0] ?? 0;
3233
3443
  }
3234
3444
  const activeResult = useAltScreen ? altResult : mainResult;
3235
3445
  const activeStartRow = useAltScreen ? altStartRow : mainStartRow;
@@ -3241,8 +3451,11 @@ function createScreen(options = {}) {
3241
3451
  altGrid = newAlt;
3242
3452
  mainSoftWrapped = newMainWrapped;
3243
3453
  altSoftWrapped = newAltWrapped;
3454
+ mainLineAttrs = newMainAttrs;
3455
+ altLineAttrs = newAltAttrs;
3244
3456
  grid = useAltScreen ? altGrid : mainGrid;
3245
3457
  softWrapped = useAltScreen ? altSoftWrapped : mainSoftWrapped;
3458
+ lineAttrs = useAltScreen ? altLineAttrs : mainLineAttrs;
3246
3459
  const oldCols = cols;
3247
3460
  cols = newCols;
3248
3461
  rows = newRows;
@@ -3277,6 +3490,11 @@ function createScreen(options = {}) {
3277
3490
  },
3278
3491
  scrollback: cloneGridSnapshot(scrollback),
3279
3492
  scrollbackSoftWrapped: [...scrollbackSoftWrapped],
3493
+ lineAttrs: {
3494
+ main: toSparseAttrs(mainLineAttrs),
3495
+ alt: toSparseAttrs(altLineAttrs),
3496
+ scrollback: toSparseAttrs(scrollbackLineAttrs)
3497
+ },
3280
3498
  cursor: {
3281
3499
  x: curX,
3282
3500
  y: curY,
@@ -3292,7 +3510,9 @@ function createScreen(options = {}) {
3292
3510
  attrs: cloneAttrsSnapshot(savedState.attrs),
3293
3511
  originMode: savedState.originMode,
3294
3512
  autoWrap: savedState.autoWrap,
3295
- charsetG0: savedState.charsetG0
3513
+ charsetG0: savedState.charsetG0,
3514
+ charsetG1: savedState.charsetG1,
3515
+ activeG1: savedState.activeG1
3296
3516
  },
3297
3517
  attrs: cloneAttrsSnapshot(attrs),
3298
3518
  modes: {
@@ -3306,6 +3526,7 @@ function createScreen(options = {}) {
3306
3526
  focusTracking,
3307
3527
  origin: originMode,
3308
3528
  insert: insertMode,
3529
+ newLine: newLineMode,
3309
3530
  reverseVideo,
3310
3531
  syncOutput,
3311
3532
  kittyKeyboardFlags,
@@ -3355,6 +3576,8 @@ function createScreen(options = {}) {
3355
3576
  },
3356
3577
  unicode: {
3357
3578
  charsetG0,
3579
+ charsetG1,
3580
+ activeG1,
3358
3581
  lastChar,
3359
3582
  pendingRegionalIndicator,
3360
3583
  afterZWJ
@@ -3372,9 +3595,13 @@ function createScreen(options = {}) {
3372
3595
  scrollbackSoftWrapped = snapshotValue.scrollbackSoftWrapped ? [...snapshotValue.scrollbackSoftWrapped] : new Array(scrollback.length).fill(false);
3373
3596
  mainSoftWrapped = [...snapshotValue.main.softWrapped];
3374
3597
  altSoftWrapped = [...snapshotValue.alt.softWrapped];
3598
+ mainLineAttrs = fromSparseAttrs(snapshotValue.lineAttrs?.main, mainSoftWrapped.length);
3599
+ altLineAttrs = fromSparseAttrs(snapshotValue.lineAttrs?.alt, altSoftWrapped.length);
3600
+ scrollbackLineAttrs = fromSparseAttrs(snapshotValue.lineAttrs?.scrollback, scrollback.length);
3375
3601
  useAltScreen = snapshotValue.activeBuffer === "alt";
3376
3602
  grid = useAltScreen ? altGrid : mainGrid;
3377
3603
  softWrapped = useAltScreen ? altSoftWrapped : mainSoftWrapped;
3604
+ lineAttrs = useAltScreen ? altLineAttrs : mainLineAttrs;
3378
3605
  curX = snapshotValue.cursor.x;
3379
3606
  curY = snapshotValue.cursor.y;
3380
3607
  curVisible = snapshotValue.cursor.visible;
@@ -3388,7 +3615,9 @@ function createScreen(options = {}) {
3388
3615
  attrs: cloneAttrsSnapshot(snapshotValue.savedState.attrs),
3389
3616
  originMode: snapshotValue.savedState.originMode,
3390
3617
  autoWrap: snapshotValue.savedState.autoWrap,
3391
- charsetG0: snapshotValue.savedState.charsetG0
3618
+ charsetG0: snapshotValue.savedState.charsetG0,
3619
+ charsetG1: snapshotValue.savedState.charsetG1 ?? false,
3620
+ activeG1: snapshotValue.savedState.activeG1 ?? false
3392
3621
  };
3393
3622
  attrs = cloneAttrsSnapshot(snapshotValue.attrs);
3394
3623
  bracketedPaste = snapshotValue.modes.bracketedPaste;
@@ -3401,6 +3630,7 @@ function createScreen(options = {}) {
3401
3630
  focusTracking = snapshotValue.modes.focusTracking;
3402
3631
  originMode = snapshotValue.modes.origin;
3403
3632
  insertMode = snapshotValue.modes.insert;
3633
+ newLineMode = snapshotValue.modes.newLine ?? false;
3404
3634
  reverseVideo = snapshotValue.modes.reverseVideo;
3405
3635
  syncOutput = snapshotValue.modes.syncOutput;
3406
3636
  kittyKeyboardFlags = snapshotValue.modes.kittyKeyboardFlags;
@@ -3439,6 +3669,8 @@ function createScreen(options = {}) {
3439
3669
  apcOverflow = snapshotValue.parser.apcOverflow ?? false;
3440
3670
  utf8PendingBytes = [...snapshotValue.parser.utf8PendingBytes];
3441
3671
  charsetG0 = snapshotValue.unicode.charsetG0;
3672
+ charsetG1 = snapshotValue.unicode.charsetG1 ?? false;
3673
+ activeG1 = snapshotValue.unicode.activeG1 ?? false;
3442
3674
  lastChar = snapshotValue.unicode.lastChar;
3443
3675
  pendingRegionalIndicator = snapshotValue.unicode.pendingRegionalIndicator;
3444
3676
  afterZWJ = snapshotValue.unicode.afterZWJ;
@@ -3472,7 +3704,7 @@ function createScreen(options = {}) {
3472
3704
  }
3473
3705
  function getText() {
3474
3706
  const lines = [];
3475
- for (let r = 0; r < rows; r++) lines.push(rowToString(grid[r]));
3707
+ for (let r = 0; r < rows; r++) lines.push(rowToString(itemAt(grid, r, "screen text rows")));
3476
3708
  return lines.join("\n");
3477
3709
  }
3478
3710
  function getScrollbackText() {
@@ -3491,7 +3723,7 @@ function createScreen(options = {}) {
3491
3723
  }
3492
3724
  function getRowAbsolute(row) {
3493
3725
  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);
3726
+ return (row < scrollback.length ? itemAt(scrollback, row, "scrollback rows") : itemAt(grid, row - scrollback.length, "screen rows")).toCells().map(stripCellColorIndex);
3495
3727
  }
3496
3728
  function firstRetainedRow() {
3497
3729
  return trimmedRowCount;
@@ -3558,6 +3790,7 @@ function createScreen(options = {}) {
3558
3790
  case "focusTracking": return focusTracking;
3559
3791
  case "originMode": return originMode;
3560
3792
  case "insertMode": return insertMode;
3793
+ case "newLineMode": return newLineMode;
3561
3794
  case "reverseVideo": return reverseVideo;
3562
3795
  case "syncOutput": return syncOutput;
3563
3796
  case "sgrMouse": return sgrMouse;
@@ -3637,6 +3870,7 @@ function createScreen(options = {}) {
3637
3870
  focusTracking,
3638
3871
  originMode,
3639
3872
  insertMode,
3873
+ newLineMode,
3640
3874
  reverseVideo,
3641
3875
  syncOutput,
3642
3876
  leftRightMargin: leftRightMarginMode,
@@ -3647,7 +3881,7 @@ function createScreen(options = {}) {
3647
3881
  };
3648
3882
  }
3649
3883
  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;
3884
+ return a.altScreen === b.altScreen && a.cursorVisible === b.cursorVisible && a.bracketedPaste === b.bracketedPaste && a.applicationCursor === b.applicationCursor && a.applicationKeypad === b.applicationKeypad && a.autoWrap === b.autoWrap && a.mouseTracking === b.mouseTracking && a.mouseTrackingMode === b.mouseTrackingMode && a.sgrMouse === b.sgrMouse && a.utf8Mouse === b.utf8Mouse && a.focusTracking === b.focusTracking && a.originMode === b.originMode && a.insertMode === b.insertMode && a.newLineMode === b.newLineMode && a.reverseVideo === b.reverseVideo && a.syncOutput === b.syncOutput && a.leftRightMargin === b.leftRightMargin && a.colorSchemeReporting === b.colorSchemeReporting && a.kittyKeyboard === b.kittyKeyboard && a.kittyGraphics === b.kittyGraphics && a.sixel === b.sixel;
3651
3885
  }
3652
3886
  const damageListeners = /* @__PURE__ */ new Set();
3653
3887
  let damageLast = {
@@ -4059,7 +4293,7 @@ function serializeSnapshot(snapshot, options = {}) {
4059
4293
  const wraps = snapshot.scrollbackSoftWrapped;
4060
4294
  const last = snapshot.scrollback.length - 1;
4061
4295
  for (let i = 0; i < snapshot.scrollback.length; i++) {
4062
- const row = snapshot.scrollback[i];
4296
+ const row = itemAt(snapshot.scrollback, i, "snapshot scrollback rows");
4063
4297
  if (wraps?.[i] === true && i < last) out.push(serializeEncodeRow(row, hyperlinks, snapshot.cols), "\x1B[0m");
4064
4298
  else out.push(serializeEncodeRow(row, hyperlinks), "\x1B[0m\r\n");
4065
4299
  }
@@ -4098,6 +4332,8 @@ function serializeSnapshot(snapshot, options = {}) {
4098
4332
  out.push(m.autoWrap ? "\x1B[?7h" : "\x1B[?7l");
4099
4333
  if (snapshot.cursor.shape !== "block" || !snapshot.cursor.blinking) out.push(`\x1b[${String(serializeCursorStyleCode(snapshot.cursor.shape, snapshot.cursor.blinking))} q`);
4100
4334
  if (snapshot.unicode.charsetG0) out.push("\x1B(0");
4335
+ if (snapshot.unicode.charsetG1) out.push("\x1B)0");
4336
+ if (snapshot.unicode.activeG1) out.push("");
4101
4337
  if (m.insert && !excluded.has("insert")) out.push("\x1B[4h");
4102
4338
  if (!serializeTabStopsAreDefault(snapshot.tabStops, snapshot.cols)) {
4103
4339
  out.push("\x1B[3g");
@@ -4112,10 +4348,12 @@ function serializeSnapshot(snapshot, options = {}) {
4112
4348
  if (hyperlinks && snapshot.attrs.url !== null) out.push(`\x1b]8;;${snapshot.attrs.url}\x1b\\`);
4113
4349
  out.push(snapshot.cursor.visible ? "\x1B[?25h" : "\x1B[?25l");
4114
4350
  const origin = m.origin && !excluded.has("origin");
4115
- const inRegion = snapshot.cursor.y >= snapshot.margins.scrollTop && snapshot.cursor.y <= snapshot.margins.scrollBottom;
4351
+ const originX = snapshot.margins.leftRight ? snapshot.margins.left : 0;
4352
+ const limitX = snapshot.margins.leftRight ? snapshot.margins.right : snapshot.cols - 1;
4353
+ const inRegion = snapshot.cursor.y >= snapshot.margins.scrollTop && snapshot.cursor.y <= snapshot.margins.scrollBottom && snapshot.cursor.x >= originX && snapshot.cursor.x <= limitX;
4116
4354
  if (origin && inRegion) {
4117
4355
  out.push("\x1B[?6h");
4118
- out.push(`\x1b[${String(snapshot.cursor.y - snapshot.margins.scrollTop + 1)};${String(snapshot.cursor.x + 1)}H`);
4356
+ out.push(`\x1b[${String(snapshot.cursor.y - snapshot.margins.scrollTop + 1)};${String(snapshot.cursor.x - originX + 1)}H`);
4119
4357
  } else {
4120
4358
  out.push(`\x1b[${String(snapshot.cursor.y + 1)};${String(snapshot.cursor.x + 1)}H`);
4121
4359
  if (origin) out.push("\x1B[?6h");
@@ -4138,6 +4376,7 @@ const F_BLINK = 128;
4138
4376
  const F_WIDE = 256;
4139
4377
  const UL_SHIFT = 9;
4140
4378
  const UL_MASK = 7;
4379
+ const F_PROTECTED = 65536;
4141
4380
  const UNDERLINE_STYLES = [
4142
4381
  "none",
4143
4382
  "single",
@@ -4368,6 +4607,7 @@ function createInterner() {
4368
4607
  if (cell.inverse) flags |= F_INVERSE;
4369
4608
  if (cell.hidden) flags |= F_HIDDEN;
4370
4609
  if (cell.blink) flags |= F_BLINK;
4610
+ if (cell.protected) flags |= F_PROTECTED;
4371
4611
  if (cell.wide) flags |= F_WIDE;
4372
4612
  flags |= ulIdx << UL_SHIFT;
4373
4613
  const key = `${flags}|${fgRef}|${bgRef}|${ulRef}|${urlRef}`;
@@ -4536,6 +4776,7 @@ function makeCell(charIdx, styleIdx, strings, colors, styles) {
4536
4776
  inverse: (style.flags & F_INVERSE) !== 0,
4537
4777
  hidden: (style.flags & F_HIDDEN) !== 0,
4538
4778
  blink: (style.flags & F_BLINK) !== 0,
4779
+ protected: (style.flags & F_PROTECTED) !== 0,
4539
4780
  wide: (style.flags & F_WIDE) !== 0,
4540
4781
  url
4541
4782
  };
@@ -4589,6 +4830,7 @@ function encodeScreenSnapshotBinary(snapshot) {
4589
4830
  savedState: snapshot.savedState,
4590
4831
  attrs: snapshot.attrs,
4591
4832
  modes: snapshot.modes,
4833
+ lineAttrs: snapshot.lineAttrs,
4592
4834
  margins: snapshot.margins,
4593
4835
  colors: snapshot.colors,
4594
4836
  tabStops: snapshot.tabStops,
@@ -4656,6 +4898,7 @@ function decodeScreenSnapshotBinary(bytes) {
4656
4898
  savedState: header.savedState,
4657
4899
  attrs: header.attrs,
4658
4900
  modes: header.modes,
4901
+ lineAttrs: header.lineAttrs,
4659
4902
  margins: header.margins,
4660
4903
  colors: header.colors,
4661
4904
  tabStops: header.tabStops,