@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.
- package/README.md +84 -0
- package/dist/browser.d.ts +6 -4
- package/dist/browser.js +3 -1
- package/dist/frontend/aitty-sw.js +43 -0
- package/dist/frontend/ansi-sequences.d.ts +7 -2
- package/dist/frontend/ansi-sequences.js +65 -2
- package/dist/frontend/ansi-style-tracker.d.ts +2 -6
- package/dist/frontend/ansi-style-tracker.js +11 -47
- package/dist/frontend/browser-terminal-renderer.d.ts +23 -15
- package/dist/frontend/browser-terminal-renderer.js +266 -102
- package/dist/frontend/cell-width.d.ts +1 -1
- package/dist/frontend/cell-width.js +1 -1
- package/dist/frontend/shell-controls.d.ts +24 -0
- package/dist/frontend/shell-controls.js +221 -0
- package/dist/frontend/terminal-app.d.ts +84 -20
- package/dist/frontend/terminal-app.js +2672 -278
- package/dist/frontend/terminal-config.d.ts +62 -0
- package/dist/frontend/terminal-config.js +126 -0
- package/dist/frontend/terminal-input-policies.d.ts +12 -2
- package/dist/frontend/terminal-input-policies.js +131 -49
- package/dist/frontend/terminal-scroll-anchor.js +25 -0
- package/dist/frontend/terminal-scroll-follow.js +23 -0
- package/dist/frontend/terminal-scrollback-window.js +18 -0
- package/dist/frontend/terminal-theme-protocol.d.ts +1 -1
- package/dist/frontend/terminal-theme-protocol.js +1 -1
- package/dist/frontend/terminal.css +161 -19
- package/dist/frontend/virtual-transcript-window.js +42 -0
- package/package.json +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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 =
|
|
27
|
-
if (resolvedBg === null) resolvedBg =
|
|
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
|
|
35
|
-
|
|
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 (
|
|
38
|
-
if (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
|
|
50
|
-
|
|
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
|
|
68
|
-
return (
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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,
|
|
133
|
-
const colors = resolveColorChannels(fg, bg, flags,
|
|
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:
|
|
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
|
-
|
|
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,
|
|
319
|
-
|
|
320
|
-
rowEl.textContent = "";
|
|
321
|
-
let runStart = 0;
|
|
397
|
+
_buildRowContent(rowEl, getCell, lineLen, cursorCol, rowIndex) {
|
|
398
|
+
const segments = [];
|
|
322
399
|
let runStyle = "";
|
|
323
|
-
let
|
|
324
|
-
const flushRun = (
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const before =
|
|
329
|
-
const cursorChar =
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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 &&
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
357
|
-
col +=
|
|
429
|
+
runCells = [];
|
|
430
|
+
col += 1;
|
|
358
431
|
continue;
|
|
359
432
|
}
|
|
360
433
|
if (inBounds && codePoint >= 9600 && codePoint <= 9631) {
|
|
361
|
-
flushRun(
|
|
362
|
-
const colors = resolveColors(cell.fg, cell.bg, cell.flags,
|
|
363
|
-
|
|
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
|
-
|
|
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,
|
|
442
|
+
const style = inBounds ? buildCellStyle(cell.fg, cell.bg, cell.flags, cell.fgRgb, cell.bgRgb) : "";
|
|
375
443
|
if (style !== runStyle) {
|
|
376
|
-
flushRun(
|
|
377
|
-
runStart = col;
|
|
444
|
+
flushRun();
|
|
378
445
|
runStyle = style;
|
|
379
|
-
|
|
380
|
-
|
|
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(
|
|
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 (
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
392
|
-
rowEl.style.
|
|
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),
|
|
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 =
|
|
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.
|
|
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
|
|
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)
|
|
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),
|
|
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.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 };
|