@aitty/browser 0.1.2 → 0.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.
@@ -1,5 +1,5 @@
1
- import { isWideCodePoint } from "./cell-width.js";
2
- //#region src/frontend/browser-terminal-renderer.ts
1
+ import { resolveRenderedScrollbackWindow } from "./terminal-scrollback-window.js";
2
+ //#region packages/browser/src/frontend/browser-terminal-renderer.ts
3
3
  const DEFAULT_COLOR = 256;
4
4
  const FLAG_BOLD = 1;
5
5
  const FLAG_DIM = 2;
@@ -8,6 +8,12 @@ const FLAG_UNDERLINE = 8;
8
8
  const FLAG_REVERSE = 32;
9
9
  const FLAG_INVISIBLE = 64;
10
10
  const FLAG_STRIKETHROUGH = 128;
11
+ function terminalCellWidth(columns) {
12
+ return columns === 1 ? "var(--term-cell-width, 1ch)" : `calc(var(--term-cell-width, 1ch) * ${columns})`;
13
+ }
14
+ function runColumnSpan(cells) {
15
+ return cells.reduce((total, cell) => total + cell.width, 0);
16
+ }
11
17
  function colorToCSS(index) {
12
18
  if (index === DEFAULT_COLOR) return null;
13
19
  if (index < 16) return `var(--term-color-${index})`;
@@ -18,26 +24,48 @@ function colorToCSS(index) {
18
24
  const level = (index - 232) * 10 + 8;
19
25
  return `rgb(${level}, ${level}, ${level})`;
20
26
  }
21
- function resolveColorChannels(fg, bg, flags, override = null) {
22
- let resolvedFg = override?.fg ?? colorToCSS(fg);
23
- let resolvedBg = override?.bg ?? colorToCSS(bg);
27
+ function rgbToCSS(packed) {
28
+ return `rgb(${packed >> 16 & 255}, ${packed >> 8 & 255}, ${packed & 255})`;
29
+ }
30
+ function cellColorToCSS(index, rgb) {
31
+ return rgb === void 0 ? colorToCSS(index) : rgbToCSS(rgb);
32
+ }
33
+ function resolveColorChannels(fg, bg, flags, fgRgb, bgRgb) {
34
+ let resolvedFg = cellColorToCSS(fg, fgRgb);
35
+ let resolvedBg = cellColorToCSS(bg, bgRgb);
24
36
  if (flags & FLAG_REVERSE) {
25
37
  [resolvedFg, resolvedBg] = [resolvedBg, resolvedFg];
26
- if (resolvedFg === null) resolvedFg = colorToCSS(0);
27
- if (resolvedBg === null) resolvedBg = colorToCSS(7);
38
+ if (resolvedFg === null) resolvedFg = "var(--term-reverse-fg)";
39
+ if (resolvedBg === null) resolvedBg = "var(--term-reverse-bg)";
28
40
  }
29
41
  return {
30
42
  bg: resolvedBg,
31
43
  fg: resolvedFg
32
44
  };
33
45
  }
34
- function buildCellStyle(fg, bg, flags, override = null) {
35
- const colors = resolveColorChannels(fg, bg, flags, override);
46
+ function dimTerminalColor(color) {
47
+ return `color-mix(in srgb, ${color} 58%, var(--term-dim-mix-target))`;
48
+ }
49
+ function contrastTerminalColor(color) {
50
+ return `color-mix(in srgb, ${color} var(--term-foreground-mix-weight), var(--term-foreground-mix-target))`;
51
+ }
52
+ function resolveForegroundColor(color, flags, fallback) {
53
+ const foreground = color ?? fallback;
54
+ if (!foreground) return null;
55
+ if (flags & FLAG_DIM) return dimTerminalColor(foreground);
56
+ if (flags & FLAG_REVERSE) return foreground;
57
+ return contrastTerminalColor(foreground);
58
+ }
59
+ function buildCellStyle(fg, bg, flags, fgRgb, bgRgb) {
60
+ const colors = resolveColorChannels(fg, bg, flags, fgRgb, bgRgb);
61
+ const foreground = resolveForegroundColor(colors.fg, flags, flags & FLAG_DIM ? "var(--term-fg)" : void 0);
36
62
  let style = "";
37
- if (colors.fg) style += `color:${colors.fg};`;
38
- if (colors.bg) style += `background:${colors.bg};`;
63
+ if (foreground) style += `color:${foreground};`;
64
+ if (colors.bg) {
65
+ style += `background:${colors.bg};`;
66
+ style += `box-shadow:0 -1px 0 ${colors.bg},0 1px 0 ${colors.bg};`;
67
+ }
39
68
  if (flags & FLAG_BOLD) style += "font-weight:bold;";
40
- if (flags & FLAG_DIM) style += "opacity:0.5;";
41
69
  if (flags & FLAG_ITALIC) style += "font-style:italic;";
42
70
  const decorations = [];
43
71
  if (flags & FLAG_UNDERLINE) decorations.push("underline");
@@ -46,43 +74,77 @@ function buildCellStyle(fg, bg, flags, override = null) {
46
74
  if (flags & FLAG_INVISIBLE) style += "visibility:hidden;";
47
75
  return style;
48
76
  }
49
- function appendRun(parent, text, style, className = "") {
50
- const span = document.createElement("span");
51
- if (className) span.className = className;
52
- if (style) span.style.cssText = style;
53
- span.textContent = text;
54
- parent.appendChild(span);
55
- }
56
- function appendWideCell(parent, text, style, cursor = false) {
57
- const span = document.createElement("span");
58
- span.className = cursor ? "term-wide term-cursor" : "term-wide";
59
- if (style) span.style.cssText = style;
60
- span.style.width = "2ch";
61
- span.style.minWidth = "2ch";
62
- span.style.maxWidth = "2ch";
63
- span.style.overflow = "hidden";
64
- span.textContent = text;
65
- parent.appendChild(span);
77
+ function resolveCellBackground(cell) {
78
+ return resolveColorChannels(cell.fg, cell.bg, cell.flags, cell.fgRgb, cell.bgRgb).bg ?? "";
66
79
  }
67
- function isWideContinuationCell(cell) {
68
- return (cell?.char ?? 0) === 0;
80
+ function resolveTerminalCellColumnSpan(getCell, col, _lineLength) {
81
+ return getCell(col)?.width === 2 ? 2 : 1;
69
82
  }
70
83
  function serializeRowText(getCell, lineLength) {
71
84
  let text = "";
72
85
  for (let col = 0; col < lineLength; col += 1) {
73
- const codePoint = getCell(col)?.char ?? 0;
74
- if (isWideCodePoint(codePoint)) {
75
- text += String.fromCodePoint(codePoint);
76
- if (col + 1 < lineLength && isWideContinuationCell(getCell(col + 1))) col += 1;
77
- continue;
78
- }
86
+ const cell = getCell(col);
87
+ const codePoint = cell?.char ?? 0;
88
+ if (cell?.width === 0) continue;
79
89
  text += codePoint >= 32 ? String.fromCodePoint(codePoint) : " ";
90
+ if (cell?.width === 2) col += 1;
80
91
  }
81
92
  return normalizeRowText(text);
82
93
  }
83
94
  function normalizeRowText(text) {
84
95
  return text.trimEnd();
85
96
  }
97
+ function appendTextRunSegment(segments, text, style, columns, className = "") {
98
+ segments.push({
99
+ className,
100
+ columns: Math.max(1, columns),
101
+ overflowHidden: false,
102
+ style,
103
+ text
104
+ });
105
+ }
106
+ function appendWideCellSegment(segments, text, style, cursor = false) {
107
+ segments.push({
108
+ className: cursor ? "term-wide term-cursor" : "term-wide",
109
+ columns: 2,
110
+ overflowHidden: true,
111
+ style,
112
+ text
113
+ });
114
+ }
115
+ function rowSegmentKey(segment) {
116
+ return [
117
+ segment.className,
118
+ segment.columns,
119
+ segment.overflowHidden ? "1" : "0",
120
+ segment.style
121
+ ].join("\0");
122
+ }
123
+ function areStringArraysEqual(left, right) {
124
+ return left?.length === right.length && right.every((value, index) => value === left[index]);
125
+ }
126
+ function createRowSegmentElement(segment) {
127
+ const span = document.createElement("span");
128
+ if (segment.className) span.className = segment.className;
129
+ if (segment.style) span.style.cssText = segment.style;
130
+ span.style.width = terminalCellWidth(segment.columns);
131
+ if (segment.overflowHidden) span.style.overflow = "hidden";
132
+ span.textContent = segment.text;
133
+ return span;
134
+ }
135
+ function rebuildRowSegments(rowEl, segments) {
136
+ const fragment = document.createDocumentFragment();
137
+ rowEl.textContent = "";
138
+ for (const segment of segments) fragment.appendChild(createRowSegmentElement(segment));
139
+ rowEl.appendChild(fragment);
140
+ }
141
+ function updateRowSegmentTexts(rowEl, previousTexts, nextTexts) {
142
+ for (let index = 0; index < nextTexts.length; index += 1) {
143
+ if (previousTexts?.[index] === nextTexts[index]) continue;
144
+ const child = rowEl.children[index];
145
+ if (child instanceof HTMLElement) child.textContent = nextTexts[index];
146
+ }
147
+ }
86
148
  function cloneSnapshotRow(snapshot) {
87
149
  const rowElement = snapshot.element.cloneNode(true);
88
150
  rowElement.className = "term-row term-scrollback-row term-restored-scrollback-row";
@@ -129,11 +191,12 @@ function findRowSequence(rows, sequence, minStart = 0) {
129
191
  }
130
192
  return -1;
131
193
  }
132
- function resolveColors(fg, bg, flags, override = null) {
133
- const colors = resolveColorChannels(fg, bg, flags, override);
194
+ function resolveColors(fg, bg, flags, fgRgb, bgRgb) {
195
+ const colors = resolveColorChannels(fg, bg, flags, fgRgb, bgRgb);
196
+ const foreground = resolveForegroundColor(colors.fg, flags, "var(--term-fg)");
134
197
  return {
135
198
  bg: colors.bg ?? "var(--term-bg)",
136
- fg: colors.fg ?? "var(--term-fg)"
199
+ fg: foreground ?? "var(--term-fg)"
137
200
  };
138
201
  }
139
202
  function getBlockBackground(codePoint, fg, bg) {
@@ -249,11 +312,17 @@ var BrowserTerminalRenderer = class {
249
312
  _altScreenMeaningfulScrollbackCount;
250
313
  _altScreenVisibleRowSnapshot;
251
314
  _lastAltScreenState;
315
+ _lastTotalScrollbackCount;
252
316
  _renderedScrollbackCount;
317
+ _renderedScrollbackTexts;
253
318
  _restoreSnapshotCount;
319
+ _rowSegmentKeys;
320
+ _rowSegmentTexts;
254
321
  _scrollbackRowEls;
255
322
  cols;
256
323
  container;
324
+ renderedScrollbackLimit;
325
+ onScrollbackRowsDropped;
257
326
  prevContainerBg;
258
327
  prevCursorCol;
259
328
  prevCursorRow;
@@ -261,14 +330,16 @@ var BrowserTerminalRenderer = class {
261
330
  prevRowBg;
262
331
  rowEls;
263
332
  rows;
264
- styleTracker;
265
- constructor(container, styleTracker) {
333
+ constructor(container, options = {}) {
266
334
  this._altScreenVisibleRowSnapshot = [];
267
335
  this._altScreenMeaningfulScrollbackCount = 0;
268
336
  this._lastAltScreenState = false;
337
+ this._lastTotalScrollbackCount = 0;
269
338
  this._restoreSnapshotCount = 0;
270
339
  this.cols = 0;
271
340
  this.container = container;
341
+ this.renderedScrollbackLimit = Math.max(1, Math.floor(options.renderedScrollbackLimit ?? 1200));
342
+ this.onScrollbackRowsDropped = options.onScrollbackRowsDropped ?? (() => {});
272
343
  this.prevContainerBg = "";
273
344
  this.prevCursorCol = -1;
274
345
  this.prevCursorRow = -1;
@@ -276,8 +347,10 @@ var BrowserTerminalRenderer = class {
276
347
  this.prevRowBg = [];
277
348
  this.rowEls = [];
278
349
  this.rows = 0;
279
- this.styleTracker = styleTracker;
280
350
  this._renderedScrollbackCount = 0;
351
+ this._renderedScrollbackTexts = [];
352
+ this._rowSegmentKeys = [];
353
+ this._rowSegmentTexts = [];
281
354
  this._scrollbackRowEls = [];
282
355
  }
283
356
  _resolveFirstLiveGridRow() {
@@ -299,12 +372,16 @@ var BrowserTerminalRenderer = class {
299
372
  this.prevRowBg = [];
300
373
  this._scrollbackRowEls = [];
301
374
  this._renderedScrollbackCount = 0;
375
+ this._renderedScrollbackTexts = [];
302
376
  this.prevCursorVisible = false;
303
377
  this.prevCursorRow = -1;
304
378
  this.prevCursorCol = -1;
379
+ this._rowSegmentKeys = [];
380
+ this._rowSegmentTexts = [];
305
381
  this._altScreenVisibleRowSnapshot = [];
306
382
  this._altScreenMeaningfulScrollbackCount = 0;
307
383
  this._lastAltScreenState = false;
384
+ this._lastTotalScrollbackCount = 0;
308
385
  this._restoreSnapshotCount = 0;
309
386
  const fragment = document.createDocumentFragment();
310
387
  for (let row = 0; row < rows; row += 1) {
@@ -312,92 +389,117 @@ var BrowserTerminalRenderer = class {
312
389
  rowElement.className = "term-row";
313
390
  fragment.appendChild(rowElement);
314
391
  this.rowEls.push(rowElement);
392
+ this._rowSegmentKeys.push([]);
393
+ this._rowSegmentTexts.push([]);
315
394
  }
316
395
  this.container.appendChild(fragment);
317
396
  }
318
- _buildRowContent(rowEl, getCell, getOverride, lineLen, cursorCol, rowIndex) {
319
- if (rowIndex >= 0) rowEl.className = "term-row";
320
- rowEl.textContent = "";
321
- let runStart = 0;
397
+ _buildRowContent(rowEl, getCell, lineLen, cursorCol, rowIndex) {
398
+ const segments = [];
322
399
  let runStyle = "";
323
- let runText = "";
324
- const flushRun = (endCol) => {
325
- if (!runText) return;
326
- if (cursorCol >= runStart && cursorCol < endCol) {
327
- const offset = cursorCol - runStart;
328
- const before = runText.slice(0, offset);
329
- const cursorChar = runText[offset];
330
- const after = runText.slice(offset + 1);
331
- if (before) appendRun(rowEl, before, runStyle);
332
- appendRun(rowEl, cursorChar, runStyle, "term-cursor");
333
- if (after) appendRun(rowEl, after, runStyle);
334
- } else appendRun(rowEl, runText, runStyle);
400
+ let runCells = [];
401
+ const flushRun = () => {
402
+ if (runCells.length === 0) return;
403
+ const cursorCellIndex = runCells.findIndex((cell) => cursorCol === cell.col);
404
+ if (cursorCellIndex >= 0) {
405
+ const before = runCells.slice(0, cursorCellIndex).map((cell) => cell.text).join("");
406
+ const cursorChar = runCells[cursorCellIndex]?.text ?? " ";
407
+ const cursorWidth = runCells[cursorCellIndex]?.width ?? 1;
408
+ const after = runCells.slice(cursorCellIndex + 1).map((cell) => cell.text).join("");
409
+ if (before) appendTextRunSegment(segments, before, runStyle, runColumnSpan(runCells.slice(0, cursorCellIndex)));
410
+ appendTextRunSegment(segments, cursorChar, runStyle, cursorWidth, "term-cursor");
411
+ if (after) appendTextRunSegment(segments, after, runStyle, runColumnSpan(runCells.slice(cursorCellIndex + 1)));
412
+ } else appendTextRunSegment(segments, runCells.map((cell) => cell.text).join(""), runStyle, runColumnSpan(runCells));
413
+ runCells = [];
335
414
  };
336
415
  for (let col = 0; col < this.cols; col += 1) {
337
416
  const cell = getCell(col);
338
417
  const inBounds = col < lineLen;
339
418
  const codePoint = inBounds ? cell.char : 0;
340
- const override = getOverride(col);
341
- if (override?.hidden === true) {
342
- flushRun(col);
343
- runStart = col + 1;
344
- runStyle = "";
345
- runText = "";
419
+ if (inBounds && cell.width === 0) {
420
+ flushRun();
346
421
  continue;
347
422
  }
348
- if (inBounds && isWideCodePoint(codePoint)) {
349
- const consumedColumns = (col + 1 < lineLen && isWideContinuationCell(getCell(col + 1)) ? getCell(col + 1) : null) ? 2 : 1;
350
- const style = buildCellStyle(cell.fg, cell.bg, cell.flags, override);
351
- const cursorOnWideCell = cursorCol === col || consumedColumns === 2 && cursorCol === col + 1 || cursorCol === col + 1 && getOverride(cursorCol)?.hidden === true;
352
- flushRun(col);
353
- appendWideCell(rowEl, String.fromCodePoint(codePoint), style, cursorOnWideCell);
354
- runStart = col + consumedColumns;
423
+ if (inBounds && cell.width === 2) {
424
+ const style = buildCellStyle(cell.fg, cell.bg, cell.flags, cell.fgRgb, cell.bgRgb);
425
+ const cursorOnWideCell = cursorCol === col || cursorCol === col + 1;
426
+ flushRun();
427
+ appendWideCellSegment(segments, String.fromCodePoint(codePoint), style, cursorOnWideCell);
355
428
  runStyle = "";
356
- runText = "";
357
- col += consumedColumns - 1;
429
+ runCells = [];
430
+ col += 1;
358
431
  continue;
359
432
  }
360
433
  if (inBounds && codePoint >= 9600 && codePoint <= 9631) {
361
- flushRun(col);
362
- const colors = resolveColors(cell.fg, cell.bg, cell.flags, override);
363
- const span = document.createElement("span");
364
- span.className = col === cursorCol ? "term-block term-cursor" : "term-block";
365
- span.style.background = getBlockBackground(codePoint, colors.fg, colors.bg);
366
- if (cell.flags & FLAG_DIM) span.style.opacity = "0.5";
367
- rowEl.appendChild(span);
368
- runStart = col + 1;
434
+ flushRun();
435
+ const colors = resolveColors(cell.fg, cell.bg, cell.flags, cell.fgRgb, cell.bgRgb);
436
+ appendTextRunSegment(segments, "", `background:${getBlockBackground(codePoint, colors.fg, colors.bg)};`, 1, col === cursorCol ? "term-block term-cursor" : "term-block");
369
437
  runStyle = "";
370
- runText = "";
438
+ runCells = [];
371
439
  continue;
372
440
  }
373
441
  const character = inBounds && codePoint >= 32 ? String.fromCodePoint(codePoint) : " ";
374
- const style = inBounds ? buildCellStyle(cell.fg, cell.bg, cell.flags, override) : "";
442
+ const style = inBounds ? buildCellStyle(cell.fg, cell.bg, cell.flags, cell.fgRgb, cell.bgRgb) : "";
375
443
  if (style !== runStyle) {
376
- flushRun(col);
377
- runStart = col;
444
+ flushRun();
378
445
  runStyle = style;
379
- runText = character;
380
- } else runText += character;
446
+ runCells = [{
447
+ col,
448
+ text: character,
449
+ width: 1
450
+ }];
451
+ } else runCells.push({
452
+ col,
453
+ text: character,
454
+ width: 1
455
+ });
381
456
  }
382
- flushRun(this.cols);
457
+ flushRun();
458
+ const rowBackground = lineLen >= this.cols && this.cols > 0 ? resolveCellBackground(getCell(this.cols - 1)) : "";
459
+ const rowBoxShadow = rowBackground ? `0 1px 0 ${rowBackground}` : "";
383
460
  if (rowIndex >= 0) {
384
- if (this.prevRowBg[rowIndex] !== "") {
385
- rowEl.style.background = "";
386
- rowEl.style.boxShadow = "";
387
- this.prevRowBg[rowIndex] = "";
461
+ if (rowEl.className !== "term-row") rowEl.className = "term-row";
462
+ const segmentKeys = segments.map(rowSegmentKey);
463
+ const segmentTexts = segments.map((segment) => segment.text);
464
+ const previousKeys = this._rowSegmentKeys[rowIndex];
465
+ const previousTexts = this._rowSegmentTexts[rowIndex];
466
+ const sameKeys = areStringArraysEqual(previousKeys, segmentKeys);
467
+ const sameTexts = areStringArraysEqual(previousTexts, segmentTexts);
468
+ const sameDomShape = sameKeys && rowEl.children.length === segments.length;
469
+ if (this.prevRowBg[rowIndex] !== rowBackground) {
470
+ rowEl.style.background = rowBackground;
471
+ rowEl.style.boxShadow = rowBoxShadow;
472
+ this.prevRowBg[rowIndex] = rowBackground;
388
473
  }
474
+ if (sameDomShape && sameTexts) return;
475
+ if (sameDomShape) updateRowSegmentTexts(rowEl, previousTexts, segmentTexts);
476
+ else {
477
+ rebuildRowSegments(rowEl, segments);
478
+ this._rowSegmentKeys[rowIndex] = segmentKeys;
479
+ }
480
+ this._rowSegmentTexts[rowIndex] = segmentTexts;
389
481
  return;
390
482
  }
391
- rowEl.style.background = "";
392
- rowEl.style.boxShadow = "";
483
+ rebuildRowSegments(rowEl, segments);
484
+ rowEl.style.background = rowBackground;
485
+ rowEl.style.boxShadow = rowBoxShadow;
393
486
  }
394
487
  _buildScrollbackRowEl(bridge, offset) {
395
488
  const rowElement = document.createElement("div");
396
489
  rowElement.className = "term-row term-scrollback-row";
397
490
  const lineLength = bridge.getScrollbackLineLen(offset);
398
- this._buildRowContent(rowElement, (col) => bridge.getScrollbackCell(offset, col), (col) => this.styleTracker.getScrollbackOverride(offset, col), lineLength, -1, -1);
491
+ this._buildRowContent(rowElement, (col) => bridge.getScrollbackCell(offset, col), lineLength, -1, -1);
399
492
  return rowElement;
400
493
  }
494
+ _readScrollbackRowTexts(bridge, scrollbackCount = bridge.getScrollbackCount()) {
495
+ const rowTexts = [];
496
+ const windowState = resolveRenderedScrollbackWindow({
497
+ limit: this.renderedScrollbackLimit,
498
+ totalRows: scrollbackCount
499
+ });
500
+ for (let offset = windowState.firstOffset; offset >= 0; offset -= 1) rowTexts.push(this._readScrollbackRowText(bridge, offset));
501
+ return rowTexts;
502
+ }
401
503
  _captureVisibleRowSnapshot() {
402
504
  this._altScreenMeaningfulScrollbackCount = this._scrollbackRowEls.reduce((count, rowElement) => count + (normalizeRowText(rowElement.textContent ?? "").length > 0 ? 1 : 0), 0);
403
505
  this._altScreenVisibleRowSnapshot = this.rowEls.map((rowElement) => ({
@@ -425,6 +527,27 @@ var BrowserTerminalRenderer = class {
425
527
  _readScrollbackRowText(bridge, offset) {
426
528
  return serializeRowText((col) => bridge.getScrollbackCell(offset, col), bridge.getScrollbackLineLen(offset));
427
529
  }
530
+ _isShiftedScrollbackMatch(bridge, shiftCount, scrollbackCount) {
531
+ for (let previousIndex = shiftCount; previousIndex < this._renderedScrollbackTexts.length; previousIndex += 1) {
532
+ const currentIndex = previousIndex - shiftCount;
533
+ const currentOffset = scrollbackCount - 1 - currentIndex;
534
+ if (this._renderedScrollbackTexts[previousIndex] !== this._readScrollbackRowText(bridge, currentOffset)) return false;
535
+ }
536
+ return true;
537
+ }
538
+ _resolveShiftedScrollbackCount(bridge, scrollbackCount) {
539
+ if (scrollbackCount === 0 || this._renderedScrollbackTexts.length !== scrollbackCount) return null;
540
+ const previousNewestRow = this._renderedScrollbackTexts[scrollbackCount - 1] ?? "";
541
+ if (this._readScrollbackRowText(bridge, 0) === previousNewestRow) return 0;
542
+ for (let shiftCount = 1; shiftCount < scrollbackCount; shiftCount += 1) {
543
+ if (this._readScrollbackRowText(bridge, shiftCount) !== previousNewestRow) continue;
544
+ if (this._isShiftedScrollbackMatch(bridge, shiftCount, scrollbackCount)) return shiftCount;
545
+ }
546
+ return null;
547
+ }
548
+ getRenderedScrollbackCount() {
549
+ return this._renderedScrollbackCount;
550
+ }
428
551
  _readLiveTranscriptRows(bridge, currentVisibleRows) {
429
552
  const liveRows = [];
430
553
  for (let offset = bridge.getScrollbackCount() - 1; offset >= 0; offset -= 1) {
@@ -464,14 +587,20 @@ var BrowserTerminalRenderer = class {
464
587
  _rebuildScrollback(bridge, restoredSnapshots) {
465
588
  for (const rowElement of this._scrollbackRowEls) rowElement.remove();
466
589
  this._scrollbackRowEls = [];
590
+ this._renderedScrollbackTexts = [];
467
591
  if (bridge.usingAltScreen()) {
468
592
  this._renderedScrollbackCount = 0;
593
+ this._lastTotalScrollbackCount = 0;
469
594
  this._restoreSnapshotCount = 0;
470
595
  return;
471
596
  }
472
597
  const scrollbackCount = bridge.getScrollbackCount();
598
+ const windowState = resolveRenderedScrollbackWindow({
599
+ limit: this.renderedScrollbackLimit,
600
+ totalRows: scrollbackCount
601
+ });
473
602
  const rowElements = [];
474
- for (let offset = scrollbackCount - 1; offset >= 0; offset -= 1) rowElements.push(this._buildScrollbackRowEl(bridge, offset));
603
+ for (let offset = windowState.firstOffset; offset >= 0; offset -= 1) rowElements.push(this._buildScrollbackRowEl(bridge, offset));
475
604
  if (restoredSnapshots.length > 0) {
476
605
  const restoredRowElements = restoredSnapshots.map((snapshot) => cloneSnapshotRow(snapshot));
477
606
  const preservedRowCount = Math.max(0, rowElements.length - restoredRowElements.length);
@@ -481,16 +610,48 @@ var BrowserTerminalRenderer = class {
481
610
  for (const rowElement of rowElements) fragment.appendChild(rowElement);
482
611
  this._insertScrollbackFragment(fragment);
483
612
  this._scrollbackRowEls = rowElements;
484
- this._renderedScrollbackCount = scrollbackCount;
613
+ this._renderedScrollbackTexts = rowElements.map((rowElement) => normalizeRowText(rowElement.textContent ?? ""));
614
+ this._renderedScrollbackCount = windowState.renderedRows;
615
+ this._lastTotalScrollbackCount = scrollbackCount;
485
616
  this._restoreSnapshotCount = restoredSnapshots.length;
486
617
  }
487
618
  syncScrollback(bridge, restoredSnapshots = []) {
488
- const scrollbackCount = bridge.usingAltScreen() ? 0 : bridge.getScrollbackCount();
619
+ const totalScrollbackCount = bridge.usingAltScreen() ? 0 : bridge.getScrollbackCount();
620
+ const scrollbackCount = resolveRenderedScrollbackWindow({
621
+ limit: this.renderedScrollbackLimit,
622
+ totalRows: totalScrollbackCount
623
+ }).renderedRows;
489
624
  if (bridge.usingAltScreen() !== this._lastAltScreenState || restoredSnapshots.length > 0 || this._restoreSnapshotCount > 0) {
490
625
  this._rebuildScrollback(bridge, restoredSnapshots);
491
626
  return;
492
627
  }
493
- if (scrollbackCount === this._renderedScrollbackCount) return;
628
+ if (scrollbackCount === this._renderedScrollbackCount) {
629
+ const shiftedRows = totalScrollbackCount > this._lastTotalScrollbackCount ? totalScrollbackCount - this._lastTotalScrollbackCount : this._resolveShiftedScrollbackCount(bridge, scrollbackCount);
630
+ if (shiftedRows === 0) {
631
+ this._lastTotalScrollbackCount = totalScrollbackCount;
632
+ return;
633
+ }
634
+ if (shiftedRows === null || shiftedRows >= scrollbackCount) {
635
+ this._rebuildScrollback(bridge, []);
636
+ return;
637
+ }
638
+ const droppedRows = this._renderedScrollbackTexts.slice(0, shiftedRows).filter((row) => row.length > 0);
639
+ if (droppedRows.length > 0) this.onScrollbackRowsDropped(droppedRows);
640
+ const fragment = document.createDocumentFragment();
641
+ const nextTexts = [];
642
+ for (let offset = shiftedRows - 1; offset >= 0; offset -= 1) {
643
+ const rowElement = this._buildScrollbackRowEl(bridge, offset);
644
+ fragment.appendChild(rowElement);
645
+ this._scrollbackRowEls.push(rowElement);
646
+ nextTexts.push(this._readScrollbackRowText(bridge, offset));
647
+ }
648
+ for (let index = 0; index < shiftedRows; index += 1) this._scrollbackRowEls.shift()?.remove();
649
+ this._insertScrollbackFragment(fragment);
650
+ this._renderedScrollbackTexts = [...this._renderedScrollbackTexts.slice(shiftedRows), ...nextTexts];
651
+ this._renderedScrollbackCount = scrollbackCount;
652
+ this._lastTotalScrollbackCount = totalScrollbackCount;
653
+ return;
654
+ }
494
655
  if (scrollbackCount > this._renderedScrollbackCount) {
495
656
  const newCount = scrollbackCount - this._renderedScrollbackCount;
496
657
  const fragment = document.createDocumentFragment();
@@ -498,13 +659,16 @@ var BrowserTerminalRenderer = class {
498
659
  const rowElement = this._buildScrollbackRowEl(bridge, offset);
499
660
  fragment.appendChild(rowElement);
500
661
  this._scrollbackRowEls.push(rowElement);
662
+ this._renderedScrollbackTexts.push(this._readScrollbackRowText(bridge, offset));
501
663
  }
502
664
  this._insertScrollbackFragment(fragment);
503
665
  } else {
504
666
  const removeCount = this._renderedScrollbackCount - scrollbackCount;
505
667
  for (let index = 0; index < removeCount; index += 1) this._scrollbackRowEls.shift()?.remove();
668
+ this._renderedScrollbackTexts.splice(0, removeCount);
506
669
  }
507
670
  this._renderedScrollbackCount = scrollbackCount;
671
+ this._lastTotalScrollbackCount = totalScrollbackCount;
508
672
  }
509
673
  render(bridge, options = {}) {
510
674
  const rows = bridge.getRows();
@@ -528,7 +692,7 @@ var BrowserTerminalRenderer = class {
528
692
  const hasCursor = row === cursor.row;
529
693
  if (isDirty || hadCursor || hasCursor && needsCursorUpdate) {
530
694
  const cursorCol = hasCursor && cursorVisible ? cursor.col : -1;
531
- this._buildRowContent(this.rowEls[row], (col) => bridge.getCell(row, col), (col) => this.styleTracker.getGridOverride(row, col), this.cols, cursorCol, row);
695
+ this._buildRowContent(this.rowEls[row], (col) => bridge.getCell(row, col), this.cols, cursorCol, row);
532
696
  }
533
697
  }
534
698
  this.prevCursorRow = cursor.row;
@@ -543,4 +707,4 @@ var BrowserTerminalRenderer = class {
543
707
  }
544
708
  };
545
709
  //#endregion
546
- export { BrowserTerminalRenderer };
710
+ export { BrowserTerminalRenderer, resolveTerminalCellColumnSpan, serializeRowText };
@@ -1,4 +1,4 @@
1
- //#region src/frontend/cell-width.d.ts
1
+ //#region packages/browser/src/frontend/cell-width.d.ts
2
2
  declare function isWideCodePoint(codePoint: number): boolean;
3
3
  declare function cellWidthForCodePoint(codePoint: number): 1 | 2;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- //#region src/frontend/cell-width.ts
1
+ //#region packages/browser/src/frontend/cell-width.ts
2
2
  function isWideCodePoint(codePoint) {
3
3
  return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65135 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127744 && codePoint <= 128591 || codePoint >= 129280 && codePoint <= 129535 || codePoint >= 131072 && codePoint <= 262141);
4
4
  }
@@ -0,0 +1,24 @@
1
+ import { TerminalConfigPatch, TerminalResolvedConfig } from "./terminal-config.js";
2
+
3
+ //#region packages/browser/src/frontend/shell-controls.d.ts
4
+ type BrowserWindow = Window & typeof globalThis;
5
+ type ShellControlScrollAnchor = {
6
+ stickToBottom: boolean;
7
+ };
8
+ type ShellControlOptions<TScrollAnchor extends ShellControlScrollAnchor = ShellControlScrollAnchor> = {
9
+ captureScrollAnchor(): TScrollAnchor;
10
+ doc: Document;
11
+ focusTerminal(): void;
12
+ getConfig(): TerminalResolvedConfig;
13
+ onInterrupt(): void;
14
+ onScrollSurfaceChange?(): void;
15
+ onStickToBottomChange?(stickToBottom: boolean): void;
16
+ resetConfig: TerminalResolvedConfig;
17
+ restoreScrollAnchor(anchor: TScrollAnchor): void;
18
+ shell: HTMLElement;
19
+ updateConfig(config: TerminalConfigPatch): TerminalResolvedConfig;
20
+ windowObject: BrowserWindow;
21
+ };
22
+ declare function installShellControls<TScrollAnchor extends ShellControlScrollAnchor>(options: ShellControlOptions<TScrollAnchor>): () => void;
23
+ //#endregion
24
+ export { ShellControlOptions, ShellControlScrollAnchor, installShellControls };