@linxiraos/pi-utils 1.1.27 → 1.2.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +4 -6
  2. package/README.md +1 -1
  3. package/dist/types/ar/index.d.ts +1 -0
  4. package/dist/types/ar/zip-package.d.ts +21 -0
  5. package/dist/types/color.d.ts +2 -0
  6. package/dist/types/dirs.d.ts +31 -12
  7. package/dist/types/docx/converter.d.ts +2 -0
  8. package/dist/types/dom/core.d.ts +15 -4
  9. package/dist/types/dom/parser.d.ts +5 -3
  10. package/dist/types/dom/selector.d.ts +7 -1
  11. package/dist/types/logger/rotating-file.d.ts +4 -2
  12. package/dist/types/loop-phase.d.ts +12 -7
  13. package/dist/types/marked/core.d.ts +7 -6
  14. package/dist/types/marked-list.d.ts +38 -0
  15. package/dist/types/math-delimiters.d.ts +10 -0
  16. package/dist/types/readability/types.d.ts +2 -0
  17. package/dist/types/temp.d.ts +7 -0
  18. package/dist/types/turndown/types.d.ts +7 -0
  19. package/dist/types/vterm/buffer.d.ts +16 -5
  20. package/package.json +2 -2
  21. package/src/ar/index.ts +1 -0
  22. package/src/ar/open.ts +29 -8
  23. package/src/ar/zip-package.ts +57 -0
  24. package/src/chalk.ts +66 -23
  25. package/src/color.ts +6 -0
  26. package/src/dirs.ts +84 -24
  27. package/src/docx/converter.ts +26 -12
  28. package/src/dom/core.ts +354 -111
  29. package/src/dom/parser.ts +42 -19
  30. package/src/dom/selector.ts +328 -95
  31. package/src/fetch-retry.ts +2 -0
  32. package/src/format.ts +5 -1
  33. package/src/json-lexer.ts +54 -0
  34. package/src/json-parse.ts +21 -6
  35. package/src/logger/rotating-file.ts +5 -3
  36. package/src/logger.ts +39 -23
  37. package/src/loop-phase.ts +66 -21
  38. package/src/marked/core.ts +42 -18
  39. package/src/marked-list.ts +73 -0
  40. package/src/math-delimiters.ts +16 -0
  41. package/src/module-timer.ts +5 -5
  42. package/src/postmortem.ts +1 -1
  43. package/src/process-name.ts +2 -2
  44. package/src/prompt.ts +3 -1
  45. package/src/ptree.ts +28 -35
  46. package/src/readability/readability.ts +349 -39
  47. package/src/readability/types.ts +2 -0
  48. package/src/runtime-install.ts +2 -2
  49. package/src/stream.ts +34 -13
  50. package/src/temp.ts +7 -0
  51. package/src/template.ts +7 -4
  52. package/src/timing-buffer.ts +1 -1
  53. package/src/tls-fetch.ts +1 -1
  54. package/src/turndown/gfm.ts +16 -5
  55. package/src/turndown/html.ts +28 -8
  56. package/src/turndown/service.ts +43 -15
  57. package/src/turndown/types.ts +7 -0
  58. package/src/vterm/buffer.ts +31 -11
  59. package/src/vterm/terminal.ts +213 -100
@@ -2,10 +2,10 @@ import {
2
2
  BufferLine,
3
3
  type BufferState,
4
4
  BufferView,
5
- blankCell,
6
5
  type CellAttributes,
7
6
  type CellData,
8
- defaultAttributes,
7
+ DEFAULT_CELL_ATTRIBUTES,
8
+ sharedBlankCell,
9
9
  } from "./buffer";
10
10
 
11
11
  const segmenter = new Intl.Segmenter();
@@ -39,7 +39,7 @@ export interface TerminalBuffers {
39
39
  interface SavedCursor {
40
40
  x: number;
41
41
  y: number;
42
- attrs: CellAttributes;
42
+ attrs: Readonly<CellAttributes>;
43
43
  }
44
44
 
45
45
  function createState(columns: number, rows: number): BufferState {
@@ -52,8 +52,36 @@ function createState(columns: number, rows: number): BufferState {
52
52
  };
53
53
  }
54
54
 
55
- function cloneCell(cell: CellData): CellData {
56
- return { chars: cell.chars, width: cell.width, attrs: { ...cell.attrs } };
55
+ function sameAttributes(a: Readonly<CellAttributes>, b: Readonly<CellAttributes>): boolean {
56
+ return (
57
+ a.bold === b.bold &&
58
+ a.dim === b.dim &&
59
+ a.italic === b.italic &&
60
+ a.underline === b.underline &&
61
+ a.inverse === b.inverse &&
62
+ a.strikethrough === b.strikethrough &&
63
+ a.overline === b.overline &&
64
+ a.fgMode === b.fgMode &&
65
+ a.fg === b.fg &&
66
+ a.bgMode === b.bgMode &&
67
+ a.bg === b.bg
68
+ );
69
+ }
70
+
71
+ /** Parses CSI parameters (`;`/`:` separated); null when the body holds anything but digits. */
72
+ function parseCsiParams(body: string): number[] | null {
73
+ const params: number[] = [];
74
+ let value = 0;
75
+ for (let index = 0; index < body.length; index++) {
76
+ const code = body.charCodeAt(index);
77
+ if (code >= 0x30 && code <= 0x39) value = value * 10 + (code - 0x30);
78
+ else if (code === 0x3b || code === 0x3a) {
79
+ params.push(value);
80
+ value = 0;
81
+ } else return null;
82
+ }
83
+ params.push(value);
84
+ return params;
57
85
  }
58
86
 
59
87
  /** Behavior-compatible reimplementation of @xterm/headless's used surface. */
@@ -72,7 +100,12 @@ export class Terminal {
72
100
  #alternate: BufferState;
73
101
  #active: BufferState;
74
102
  #usingAlternate = false;
75
- #attrs = defaultAttributes();
103
+ // Renditions are immutable and shared by every cell printed with them;
104
+ // SGR builds a new frozen object instead of mutating this one.
105
+ #attrs: Readonly<CellAttributes> = DEFAULT_CELL_ATTRIBUTES;
106
+ // Frozen printable-ASCII cells for #attrs, indexed by char code - 0x20.
107
+ #asciiCells: CellData[] = [];
108
+ #asciiCellsAttrs: Readonly<CellAttributes> | undefined;
76
109
  #scrollTop = 0;
77
110
  #scrollBottom: number;
78
111
  #originMode = false;
@@ -149,13 +182,30 @@ export class Terminal {
149
182
  }
150
183
 
151
184
  #parse(text: string): void {
152
- let printable = "";
153
- const flush = () => {
154
- if (!printable) return;
155
- for (const part of segmenter.segment(printable)) this.#print(part.segment);
156
- printable = "";
157
- };
158
- for (const char of text) {
185
+ const length = text.length;
186
+ let index = 0;
187
+ while (index < length) {
188
+ if (this.#state === "ground") {
189
+ let end = index;
190
+ while (end < length) {
191
+ const code = text.charCodeAt(end);
192
+ if (code < 0x20 || code === 0x7f) break;
193
+ end++;
194
+ }
195
+ if (end > index) {
196
+ this.#printRun(text, index, end);
197
+ index = end;
198
+ continue;
199
+ }
200
+ const char = text[index]!;
201
+ index++;
202
+ if (char === "\x1b") this.#state = "escape";
203
+ else this.#handleControl(char);
204
+ continue;
205
+ }
206
+ const code = text.codePointAt(index)!;
207
+ const char = code > 0xffff ? text.slice(index, index + 2) : text[index]!;
208
+ index += char.length;
159
209
  if (this.#state === "string") {
160
210
  if (this.#stringEscape && char === "\\") {
161
211
  this.#state = "ground";
@@ -172,30 +222,41 @@ export class Terminal {
172
222
  this.#handleEscape(char);
173
223
  continue;
174
224
  }
175
- if (this.#state === "csi") {
176
- if (char >= "@" && char <= "~") {
177
- this.#handleCsi(this.#sequence, char);
178
- this.#sequence = "";
179
- this.#state = "ground";
180
- } else if (char === "\x1b") {
181
- this.#state = "escape";
182
- this.#sequence = "";
183
- } else {
184
- this.#sequence += char;
185
- }
186
- continue;
187
- }
188
- if (char === "\x1b") {
189
- flush();
225
+ if (char >= "@" && char <= "~") {
226
+ this.#handleCsi(this.#sequence, char);
227
+ this.#sequence = "";
228
+ this.#state = "ground";
229
+ } else if (char === "\x1b") {
190
230
  this.#state = "escape";
191
- } else if (char < " " || char === "\x7f") {
192
- flush();
193
- this.#handleControl(char);
231
+ this.#sequence = "";
194
232
  } else {
195
- printable += char;
233
+ this.#sequence += char;
196
234
  }
197
235
  }
198
- flush();
236
+ }
237
+
238
+ /**
239
+ * Prints a run of non-control text. Printable ASCII is one cell wide and
240
+ * every boundary between two ASCII characters is a grapheme break, so ASCII
241
+ * stretches skip segmentation; the last ASCII character before non-ASCII text
242
+ * may start a cluster (combining mark, keycap) and goes to the segmenter.
243
+ */
244
+ #printRun(text: string, start: number, end: number): void {
245
+ let index = start;
246
+ while (index < end) {
247
+ let asciiEnd = index;
248
+ while (asciiEnd < end && text.charCodeAt(asciiEnd) < 0x80) asciiEnd++;
249
+ if (asciiEnd < end && asciiEnd > index) asciiEnd--;
250
+ for (; index < asciiEnd; index++) this.#putAscii(text.charCodeAt(index));
251
+ if (index >= end) return;
252
+ // Segment up to the first break between two ASCII characters.
253
+ let clusterEnd = index + 1;
254
+ while (clusterEnd < end && !(text.charCodeAt(clusterEnd) < 0x80 && text.charCodeAt(clusterEnd - 1) < 0x80)) {
255
+ clusterEnd++;
256
+ }
257
+ for (const part of segmenter.segment(text.slice(index, clusterEnd))) this.#print(part.segment);
258
+ index = clusterEnd;
259
+ }
199
260
  }
200
261
 
201
262
  #handleControl(char: string): void {
@@ -258,15 +319,38 @@ export class Terminal {
258
319
  }
259
320
  }
260
321
 
322
+ /**
323
+ * Dispatches a CSI sequence on its whole identifier (private prefix,
324
+ * intermediates, final), dropping the ones without a handler as xterm does:
325
+ * `CSI < u` pops the kitty keyboard stack rather than restoring the cursor,
326
+ * `CSI > 4;1 m` sets modifyOtherKeys rather than SGR, and `CSI 0 SP q` sets
327
+ * the cursor shape rather than anything this buffer models.
328
+ */
261
329
  #handleCsi(raw: string, final: string): void {
262
- const privateMode = raw.startsWith("?");
263
- const greater = raw.startsWith(">");
264
- const body = privateMode || greater ? raw.slice(1) : raw;
265
- const params = body
266
- .replaceAll(":", ";")
267
- .split(";")
268
- .map(value => (value === "" ? 0 : Number(value)));
330
+ const marker = raw.charCodeAt(0);
331
+ // `<`, `=`, `>`, `?` open a private sequence family.
332
+ const prefixed = marker >= 0x3c && marker <= 0x3f;
333
+ // Null when intermediates or a misplaced private marker follow the parameters.
334
+ const params = parseCsiParams(prefixed ? raw.slice(1) : raw);
335
+ if (!params) return;
269
336
  const first = params[0] ?? 0;
337
+ if (prefixed) {
338
+ if (marker !== 0x3f) return;
339
+ switch (final) {
340
+ case "h":
341
+ case "l":
342
+ this.#setModes(params, true, final === "h");
343
+ break;
344
+ // DECSED/DECSEL: no cell is ever protected, so selective erase is plain erase.
345
+ case "J":
346
+ this.#eraseDisplay(first);
347
+ break;
348
+ case "K":
349
+ this.#eraseLine(first);
350
+ break;
351
+ }
352
+ return;
353
+ }
270
354
  const amount = Math.max(1, first);
271
355
  switch (final) {
272
356
  case "A":
@@ -336,7 +420,7 @@ export class Terminal {
336
420
  this.#setRendition(params);
337
421
  break;
338
422
  case "r":
339
- if (!privateMode) this.#setScrollRegion(params);
423
+ this.#setScrollRegion(params);
340
424
  break;
341
425
  case "s":
342
426
  this.#saveCursor();
@@ -346,25 +430,42 @@ export class Terminal {
346
430
  break;
347
431
  case "h":
348
432
  case "l":
349
- this.#setModes(params, privateMode, final === "h");
433
+ this.#setModes(params, false, final === "h");
350
434
  break;
351
435
  case "n":
352
- if (!privateMode && first === 6)
353
- this.#emitData(`\x1b[${this.#active.cursorY + 1};${this.#active.cursorX + 1}R`);
436
+ if (first === 6) this.#emitData(`\x1b[${this.#active.cursorY + 1};${this.#active.cursorX + 1}R`);
354
437
  break;
355
438
  case "c":
356
- if (!greater) this.#emitData("\x1b[?1;2c");
439
+ this.#emitData("\x1b[?1;2c");
357
440
  break;
358
441
  }
359
442
  }
360
443
 
361
444
  #print(grapheme: string): void {
362
- let width = Bun.stringWidth(grapheme);
445
+ const width = Bun.stringWidth(grapheme);
363
446
  if (width === 0) {
364
447
  this.#appendCombining(grapheme);
365
448
  return;
366
449
  }
367
- width = Math.min(2, width);
450
+ const cellWidth = Math.min(2, width);
451
+ this.#placeCell(Object.freeze({ chars: grapheme, width: cellWidth, attrs: this.#attrs }), cellWidth);
452
+ }
453
+
454
+ #putAscii(code: number): void {
455
+ if (this.#asciiCellsAttrs !== this.#attrs) {
456
+ this.#asciiCells = [];
457
+ this.#asciiCellsAttrs = this.#attrs;
458
+ }
459
+ const slot = code - 0x20;
460
+ let cell = this.#asciiCells[slot];
461
+ if (!cell) {
462
+ cell = Object.freeze({ chars: String.fromCharCode(code), width: 1, attrs: this.#attrs });
463
+ this.#asciiCells[slot] = cell;
464
+ }
465
+ this.#placeCell(cell, 1);
466
+ }
467
+
468
+ #placeCell(cell: CellData, width: number): void {
368
469
  if (this.#pendingWrap || (width === 2 && this.#active.cursorX === this.cols - 1)) {
369
470
  if (!this.#autowrap) {
370
471
  this.#active.cursorX = this.cols - width;
@@ -377,9 +478,9 @@ export class Terminal {
377
478
  const column = Math.min(this.#active.cursorX, this.cols - 1);
378
479
  if (this.#insertMode) this.#shiftCells(line, column, width);
379
480
  this.#clearWideAt(line, column);
380
- line.cells[column] = { chars: grapheme, width, attrs: { ...this.#attrs } };
481
+ line.cells[column] = cell;
381
482
  if (width === 2 && column + 1 < this.cols) {
382
- line.cells[column + 1] = { chars: "", width: 0, attrs: { ...this.#attrs } };
483
+ line.cells[column + 1] = Object.freeze({ chars: "", width: 0, attrs: cell.attrs });
383
484
  }
384
485
  this.#active.cursorX = column + width;
385
486
  this.#pendingWrap = this.#active.cursorX >= this.cols;
@@ -396,7 +497,9 @@ export class Terminal {
396
497
  if (!line) return;
397
498
  while (column >= 0 && line.cells[column]?.width === 0) column--;
398
499
  const cell = line.cells[column];
399
- if (cell?.chars) cell.chars += mark;
500
+ // Cells are frozen and may be shared; replace instead of mutating.
501
+ if (cell?.chars)
502
+ line.cells[column] = Object.freeze({ chars: cell.chars + mark, width: cell.width, attrs: cell.attrs });
400
503
  }
401
504
 
402
505
  #lineFeed(wrapped: boolean): void {
@@ -489,8 +592,8 @@ export class Terminal {
489
592
  }
490
593
 
491
594
  #eraseRange(line: BufferLine, start: number, end: number): void {
492
- for (let column = Math.max(0, start); column < Math.min(this.cols, end); column++)
493
- line.cells[column] = blankCell(this.#attrs);
595
+ const blank = sharedBlankCell(this.#attrs);
596
+ for (let column = Math.max(0, start); column < Math.min(this.cols, end); column++) line.cells[column] = blank;
494
597
  this.#repairWideCells(line);
495
598
  }
496
599
 
@@ -521,14 +624,18 @@ export class Terminal {
521
624
  }
522
625
 
523
626
  #shiftCells(line: BufferLine, column: number, count: number): void {
524
- line.cells.splice(column, 0, ...Array.from({ length: count }, () => blankCell(this.#attrs)));
627
+ // Inserting more than a row's worth only pushes everything past the edge.
628
+ const blank = sharedBlankCell(this.#attrs);
629
+ const blanks = Array.from({ length: Math.min(count, this.cols) }, () => blank);
630
+ line.cells.splice(column, 0, ...blanks);
525
631
  line.cells.length = this.cols;
526
632
  }
527
633
 
528
634
  #deleteCells(count: number): void {
529
635
  const line = this.#currentLine();
530
636
  line.cells.splice(this.#active.cursorX, count);
531
- while (line.cells.length < this.cols) line.cells.push(blankCell(this.#attrs));
637
+ const blank = sharedBlankCell(this.#attrs);
638
+ while (line.cells.length < this.cols) line.cells.push(blank);
532
639
  this.#repairWideCells(line);
533
640
  }
534
641
 
@@ -537,64 +644,67 @@ export class Terminal {
537
644
  }
538
645
 
539
646
  #clearWideAt(line: BufferLine, column: number): void {
540
- if (line.cells[column]?.width === 0 && column > 0) line.cells[column - 1] = blankCell(this.#attrs);
541
- if (line.cells[column]?.width === 2 && column + 1 < this.cols) line.cells[column + 1] = blankCell(this.#attrs);
647
+ const width = line.cells[column]?.width;
648
+ if (width === 0 && column > 0) line.cells[column - 1] = sharedBlankCell(this.#attrs);
649
+ if (width === 2 && column + 1 < this.cols) line.cells[column + 1] = sharedBlankCell(this.#attrs);
542
650
  }
543
651
 
544
652
  #repairWideCells(line: BufferLine): void {
545
653
  for (let column = 0; column < this.cols; column++) {
546
654
  const cell = line.cells[column]!;
547
655
  if (cell.width === 0 && (column === 0 || line.cells[column - 1]?.width !== 2))
548
- line.cells[column] = blankCell(this.#attrs);
656
+ line.cells[column] = sharedBlankCell(this.#attrs);
549
657
  if (cell.width === 2 && (column === this.cols - 1 || line.cells[column + 1]?.width !== 0))
550
- line.cells[column] = blankCell(this.#attrs);
658
+ line.cells[column] = sharedBlankCell(this.#attrs);
551
659
  }
552
660
  }
553
661
 
554
662
  #setRendition(values: number[]): void {
555
663
  const params = values.length === 0 ? [0] : values;
664
+ // Copy-on-write: cells keep referencing the previous rendition object.
665
+ const attrs: CellAttributes = { ...this.#attrs };
556
666
  for (let index = 0; index < params.length; index++) {
557
667
  const code = params[index] ?? 0;
558
- if (code === 0) this.#attrs = defaultAttributes();
559
- else if (code === 1) this.#attrs.bold = true;
560
- else if (code === 2) this.#attrs.dim = true;
561
- else if (code === 3) this.#attrs.italic = true;
562
- else if (code === 4 || code === 21) this.#attrs.underline = true;
563
- else if (code === 7) this.#attrs.inverse = true;
564
- else if (code === 9) this.#attrs.strikethrough = true;
668
+ if (code === 0) Object.assign(attrs, DEFAULT_CELL_ATTRIBUTES);
669
+ else if (code === 1) attrs.bold = true;
670
+ else if (code === 2) attrs.dim = true;
671
+ else if (code === 3) attrs.italic = true;
672
+ else if (code === 4 || code === 21) attrs.underline = true;
673
+ else if (code === 7) attrs.inverse = true;
674
+ else if (code === 9) attrs.strikethrough = true;
565
675
  else if (code === 22) {
566
- this.#attrs.bold = false;
567
- this.#attrs.dim = false;
568
- } else if (code === 23) this.#attrs.italic = false;
569
- else if (code === 24) this.#attrs.underline = false;
570
- else if (code === 27) this.#attrs.inverse = false;
571
- else if (code === 29) this.#attrs.strikethrough = false;
572
- else if (code === 53) this.#attrs.overline = true;
573
- else if (code === 55) this.#attrs.overline = false;
676
+ attrs.bold = false;
677
+ attrs.dim = false;
678
+ } else if (code === 23) attrs.italic = false;
679
+ else if (code === 24) attrs.underline = false;
680
+ else if (code === 27) attrs.inverse = false;
681
+ else if (code === 29) attrs.strikethrough = false;
682
+ else if (code === 53) attrs.overline = true;
683
+ else if (code === 55) attrs.overline = false;
574
684
  else if (code >= 30 && code <= 37) {
575
- this.#attrs.fgMode = 1;
576
- this.#attrs.fg = code - 30;
685
+ attrs.fgMode = 1;
686
+ attrs.fg = code - 30;
577
687
  } else if (code >= 40 && code <= 47) {
578
- this.#attrs.bgMode = 1;
579
- this.#attrs.bg = code - 40;
688
+ attrs.bgMode = 1;
689
+ attrs.bg = code - 40;
580
690
  } else if (code >= 90 && code <= 97) {
581
- this.#attrs.fgMode = 1;
582
- this.#attrs.fg = code - 82;
691
+ attrs.fgMode = 1;
692
+ attrs.fg = code - 82;
583
693
  } else if (code >= 100 && code <= 107) {
584
- this.#attrs.bgMode = 1;
585
- this.#attrs.bg = code - 92;
586
- } else if (code === 39) this.#attrs.fgMode = 0;
587
- else if (code === 49) this.#attrs.bgMode = 0;
694
+ attrs.bgMode = 1;
695
+ attrs.bg = code - 92;
696
+ } else if (code === 39) attrs.fgMode = 0;
697
+ else if (code === 49) attrs.bgMode = 0;
588
698
  else if (code === 38 || code === 48) {
589
699
  const foreground = code === 38;
590
700
  const mode = params[index + 1];
591
701
  if (mode === 5 && params[index + 2] !== undefined) {
592
702
  if (foreground) {
593
- this.#attrs.fgMode = 1;
594
- this.#attrs.fg = params[index + 2]! & 0xff;
703
+ attrs.fgMode = 1;
704
+ attrs.fg = params[index + 2]! & 0xff;
595
705
  } else {
596
- this.#attrs.bgMode = 1;
597
- this.#attrs.bg = params[index + 2]! & 0xff;
706
+ attrs.bgMode = 1;
707
+ attrs.bg = params[index + 2]! & 0xff;
598
708
  }
599
709
  index += 2;
600
710
  } else if (mode === 2 && params[index + 4] !== undefined) {
@@ -603,16 +713,18 @@ export class Terminal {
603
713
  ((params[index + 3]! & 0xff) << 8) |
604
714
  (params[index + 4]! & 0xff);
605
715
  if (foreground) {
606
- this.#attrs.fgMode = 2;
607
- this.#attrs.fg = color;
716
+ attrs.fgMode = 2;
717
+ attrs.fg = color;
608
718
  } else {
609
- this.#attrs.bgMode = 2;
610
- this.#attrs.bg = color;
719
+ attrs.bgMode = 2;
720
+ attrs.bg = color;
611
721
  }
612
722
  index += 4;
613
723
  }
614
724
  }
615
725
  }
726
+ if (sameAttributes(attrs, this.#attrs)) return;
727
+ this.#attrs = sameAttributes(attrs, DEFAULT_CELL_ATTRIBUTES) ? DEFAULT_CELL_ATTRIBUTES : Object.freeze(attrs);
616
728
  }
617
729
 
618
730
  #setScrollRegion(params: number[]): void {
@@ -643,21 +755,20 @@ export class Terminal {
643
755
  }
644
756
 
645
757
  #saveCursor(): void {
646
- this.#saved = { x: this.#active.cursorX, y: this.#active.cursorY, attrs: { ...this.#attrs } };
758
+ this.#saved = { x: this.#active.cursorX, y: this.#active.cursorY, attrs: this.#attrs };
647
759
  }
648
760
 
649
761
  #restoreCursor(): void {
650
762
  if (!this.#saved) return;
651
763
  this.#active.cursorX = Math.min(this.cols - 1, this.#saved.x);
652
764
  this.#active.cursorY = Math.min(this.rows - 1, this.#saved.y);
653
- this.#attrs = { ...this.#saved.attrs };
765
+ this.#attrs = this.#saved.attrs;
654
766
  this.#pendingWrap = false;
655
767
  }
656
768
 
657
769
  #enterAlternate(saveCursor: boolean): void {
658
770
  if (this.#usingAlternate) return;
659
- if (saveCursor)
660
- this.#alternateSaved = { x: this.#normal.cursorX, y: this.#normal.cursorY, attrs: { ...this.#attrs } };
771
+ if (saveCursor) this.#alternateSaved = { x: this.#normal.cursorX, y: this.#normal.cursorY, attrs: this.#attrs };
661
772
  this.#alternate = createState(this.cols, this.rows);
662
773
  this.#active = this.#alternate;
663
774
  this.#usingAlternate = true;
@@ -673,7 +784,7 @@ export class Terminal {
673
784
  if (restoreCursor && this.#alternateSaved) {
674
785
  this.#normal.cursorX = this.#alternateSaved.x;
675
786
  this.#normal.cursorY = this.#alternateSaved.y;
676
- this.#attrs = { ...this.#alternateSaved.attrs };
787
+ this.#attrs = this.#alternateSaved.attrs;
677
788
  }
678
789
  this.#scrollTop = 0;
679
790
  this.#scrollBottom = this.rows - 1;
@@ -681,7 +792,7 @@ export class Terminal {
681
792
  }
682
793
 
683
794
  #reset(): void {
684
- this.#attrs = defaultAttributes();
795
+ this.#attrs = DEFAULT_CELL_ATTRIBUTES;
685
796
  this.#normal = createState(this.cols, this.rows);
686
797
  this.#alternate = createState(this.cols, this.rows);
687
798
  this.#usingAlternate = false;
@@ -704,7 +815,8 @@ export class Terminal {
704
815
  const group = groups.at(-1)!;
705
816
  const used = line.isWrapped || state.lines[row + 1]?.isWrapped ? line.cells.length : this.#usedColumns(line);
706
817
  if (row === absoluteCursor) group.cursorOffset = group.cells.length + state.cursorX;
707
- for (let column = 0; column < used; column++) group.cells.push(cloneCell(line.cells[column]!));
818
+ // Cells are immutable, so the reflowed grid reuses them.
819
+ for (let column = 0; column < used; column++) group.cells.push(line.cells[column]!);
708
820
  }
709
821
  const lines: BufferLine[] = [];
710
822
  let cursorAbsolute = 0;
@@ -725,8 +837,9 @@ export class Terminal {
725
837
  continue;
726
838
  }
727
839
  if (cell.width === 2 && target === columns - 1) break;
728
- line.cells[target] = cloneCell(cell);
729
- if (cell.width === 2) line.cells[target + 1] = { chars: "", width: 0, attrs: { ...cell.attrs } };
840
+ line.cells[target] = cell;
841
+ if (cell.width === 2)
842
+ line.cells[target + 1] = Object.freeze({ chars: "", width: 0, attrs: cell.attrs });
730
843
  target += cell.width;
731
844
  source += cell.width;
732
845
  }