@floegence/floeterm-terminal-web 0.14.1 → 0.15.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 +78 -445
- package/dist/core/TerminalInputBridge.d.ts +7 -3
- package/dist/core/TerminalInputBridge.d.ts.map +1 -1
- package/dist/core/TerminalInputBridge.js +63 -4
- package/dist/core/TerminalInputBridge.js.map +1 -1
- package/dist/entries/live.d.ts +4 -4
- package/dist/entries/live.d.ts.map +1 -1
- package/dist/entries/live.js +2 -2
- package/dist/entries/live.js.map +1 -1
- package/dist/entries/semantic.d.ts +9 -0
- package/dist/entries/semantic.d.ts.map +1 -0
- package/dist/entries/semantic.js +5 -0
- package/dist/entries/semantic.js.map +1 -0
- package/dist/entries/sessions.d.ts +1 -1
- package/dist/entries/sessions.d.ts.map +1 -1
- package/dist/index.d.ts +4 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -16
- package/dist/index.js.map +1 -1
- package/dist/live/client.d.ts +4 -6
- package/dist/live/client.d.ts.map +1 -1
- package/dist/live/client.js +19 -72
- package/dist/live/client.js.map +1 -1
- package/dist/live/codec.d.ts +8 -21
- package/dist/live/codec.d.ts.map +1 -1
- package/dist/live/codec.js +79 -111
- package/dist/live/codec.js.map +1 -1
- package/dist/live/transport.d.ts +38 -18
- package/dist/live/transport.d.ts.map +1 -1
- package/dist/live/transport.js +59 -133
- package/dist/live/transport.js.map +1 -1
- package/dist/semantic/RendererSurface.d.ts +65 -0
- package/dist/semantic/RendererSurface.d.ts.map +1 -0
- package/dist/semantic/RendererSurface.js +566 -0
- package/dist/semantic/RendererSurface.js.map +1 -0
- package/dist/semantic/presentation.d.ts +97 -0
- package/dist/semantic/presentation.d.ts.map +1 -0
- package/dist/semantic/presentation.js +102 -0
- package/dist/semantic/presentation.js.map +1 -0
- package/dist/types.d.ts +13 -494
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -37
- package/dist/types.js.map +1 -1
- package/package.json +11 -19
- package/dist/checkpoint/GhosttyCheckpoint.worker.d.ts +0 -2
- package/dist/checkpoint/GhosttyCheckpoint.worker.d.ts.map +0 -1
- package/dist/checkpoint/GhosttyCheckpoint.worker.js +0 -9
- package/dist/checkpoint/GhosttyCheckpoint.worker.js.map +0 -1
- package/dist/checkpoint/GhosttyCheckpointActor.d.ts +0 -44
- package/dist/checkpoint/GhosttyCheckpointActor.d.ts.map +0 -1
- package/dist/checkpoint/GhosttyCheckpointActor.js +0 -284
- package/dist/checkpoint/GhosttyCheckpointActor.js.map +0 -1
- package/dist/checkpoint/GhosttyCheckpointEngine.d.ts +0 -62
- package/dist/checkpoint/GhosttyCheckpointEngine.d.ts.map +0 -1
- package/dist/checkpoint/GhosttyCheckpointEngine.js +0 -175
- package/dist/checkpoint/GhosttyCheckpointEngine.js.map +0 -1
- package/dist/checkpoint/GhosttyCheckpointWorkerRuntime.d.ts +0 -31
- package/dist/checkpoint/GhosttyCheckpointWorkerRuntime.d.ts.map +0 -1
- package/dist/checkpoint/GhosttyCheckpointWorkerRuntime.js +0 -112
- package/dist/checkpoint/GhosttyCheckpointWorkerRuntime.js.map +0 -1
- package/dist/core/PagedTerminalOutputCoordinator.d.ts +0 -130
- package/dist/core/PagedTerminalOutputCoordinator.d.ts.map +0 -1
- package/dist/core/PagedTerminalOutputCoordinator.js +0 -1213
- package/dist/core/PagedTerminalOutputCoordinator.js.map +0 -1
- package/dist/core/TerminalCore.d.ts +0 -286
- package/dist/core/TerminalCore.d.ts.map +0 -1
- package/dist/core/TerminalCore.js +0 -3566
- package/dist/core/TerminalCore.js.map +0 -1
- package/dist/core/TerminalOutputPipeline.d.ts +0 -86
- package/dist/core/TerminalOutputPipeline.d.ts.map +0 -1
- package/dist/core/TerminalOutputPipeline.js +0 -511
- package/dist/core/TerminalOutputPipeline.js.map +0 -1
- package/dist/core/TerminalRenderScheduler.d.ts +0 -45
- package/dist/core/TerminalRenderScheduler.d.ts.map +0 -1
- package/dist/core/TerminalRenderScheduler.js +0 -145
- package/dist/core/TerminalRenderScheduler.js.map +0 -1
- package/dist/core/TerminalScrollbarOverlay.d.ts +0 -63
- package/dist/core/TerminalScrollbarOverlay.d.ts.map +0 -1
- package/dist/core/TerminalScrollbarOverlay.js +0 -493
- package/dist/core/TerminalScrollbarOverlay.js.map +0 -1
- package/dist/entries/history.d.ts +0 -4
- package/dist/entries/history.d.ts.map +0 -1
- package/dist/entries/history.js +0 -2
- package/dist/entries/history.js.map +0 -1
- package/dist/entries/preload.d.ts +0 -3
- package/dist/entries/preload.d.ts.map +0 -1
- package/dist/entries/preload.js +0 -2
- package/dist/entries/preload.js.map +0 -1
- package/dist/fabric/BeamtermFabricRenderer.d.ts +0 -67
- package/dist/fabric/BeamtermFabricRenderer.d.ts.map +0 -1
- package/dist/fabric/BeamtermFabricRenderer.js +0 -659
- package/dist/fabric/BeamtermFabricRenderer.js.map +0 -1
- package/dist/fabric/TerminalFabricCoordinator.d.ts +0 -40
- package/dist/fabric/TerminalFabricCoordinator.d.ts.map +0 -1
- package/dist/fabric/TerminalFabricCoordinator.js +0 -141
- package/dist/fabric/TerminalFabricCoordinator.js.map +0 -1
- package/dist/fabric/TerminalLiveFabric.d.ts +0 -44
- package/dist/fabric/TerminalLiveFabric.d.ts.map +0 -1
- package/dist/fabric/TerminalLiveFabric.js +0 -117
- package/dist/fabric/TerminalLiveFabric.js.map +0 -1
- package/dist/fabric/types.d.ts +0 -152
- package/dist/fabric/types.d.ts.map +0 -1
- package/dist/fabric/types.js +0 -2
- package/dist/fabric/types.js.map +0 -1
- package/dist/internal/BeamtermResourceLoader.d.ts +0 -3
- package/dist/internal/BeamtermResourceLoader.d.ts.map +0 -1
- package/dist/internal/BeamtermResourceLoader.js +0 -14
- package/dist/internal/BeamtermResourceLoader.js.map +0 -1
- package/dist/internal/GhosttyCompatibilityVersion.d.ts +0 -2
- package/dist/internal/GhosttyCompatibilityVersion.d.ts.map +0 -1
- package/dist/internal/GhosttyCompatibilityVersion.js +0 -2
- package/dist/internal/GhosttyCompatibilityVersion.js.map +0 -1
- package/dist/internal/GhosttyScrollbackCompat.d.ts +0 -16
- package/dist/internal/GhosttyScrollbackCompat.d.ts.map +0 -1
- package/dist/internal/GhosttyScrollbackCompat.js +0 -43
- package/dist/internal/GhosttyScrollbackCompat.js.map +0 -1
- package/dist/internal/GhosttyScrollbarCompat.d.ts +0 -12
- package/dist/internal/GhosttyScrollbarCompat.d.ts.map +0 -1
- package/dist/internal/GhosttyScrollbarCompat.js +0 -160
- package/dist/internal/GhosttyScrollbarCompat.js.map +0 -1
- package/dist/internal/SequenceBuffer.d.ts +0 -19
- package/dist/internal/SequenceBuffer.d.ts.map +0 -1
- package/dist/internal/SequenceBuffer.js +0 -83
- package/dist/internal/SequenceBuffer.js.map +0 -1
- package/dist/internal/TerminalInitializationScheduler.d.ts +0 -38
- package/dist/internal/TerminalInitializationScheduler.d.ts.map +0 -1
- package/dist/internal/TerminalInitializationScheduler.js +0 -112
- package/dist/internal/TerminalInitializationScheduler.js.map +0 -1
- package/dist/internal/TerminalResourceLoader.d.ts +0 -40
- package/dist/internal/TerminalResourceLoader.d.ts.map +0 -1
- package/dist/internal/TerminalResourceLoader.js +0 -165
- package/dist/internal/TerminalResourceLoader.js.map +0 -1
- package/dist/internal/UnicodeSafeUrlProvider.d.ts +0 -17
- package/dist/internal/UnicodeSafeUrlProvider.d.ts.map +0 -1
- package/dist/internal/UnicodeSafeUrlProvider.js +0 -32
- package/dist/internal/UnicodeSafeUrlProvider.js.map +0 -1
- package/dist/internal/scheduleUiTurn.d.ts +0 -5
- package/dist/internal/scheduleUiTurn.d.ts.map +0 -1
- package/dist/internal/scheduleUiTurn.js +0 -37
- package/dist/internal/scheduleUiTurn.js.map +0 -1
- package/dist/manager/TerminalInstanceController.d.ts +0 -88
- package/dist/manager/TerminalInstanceController.d.ts.map +0 -1
- package/dist/manager/TerminalInstanceController.js +0 -1076
- package/dist/manager/TerminalInstanceController.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -3
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -11
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/history.d.ts +0 -2
- package/dist/utils/history.d.ts.map +0 -1
- package/dist/utils/history.js +0 -12
- package/dist/utils/history.js.map +0 -1
- package/dist/utils/xtermAutoResponseFilter.d.ts +0 -2
- package/dist/utils/xtermAutoResponseFilter.d.ts.map +0 -1
- package/dist/utils/xtermAutoResponseFilter.js +0 -109
- package/dist/utils/xtermAutoResponseFilter.js.map +0 -1
|
@@ -1,3566 +0,0 @@
|
|
|
1
|
-
import { filterXtermAutoResponses } from '../utils/xtermAutoResponseFilter.js';
|
|
2
|
-
import { createConsoleLogger, noopLogger } from '../utils/logger.js';
|
|
3
|
-
import { cursorToFabricCursor, renderReasonFromForce, terminalLiveFabric, themeToFabricTheme, } from '../fabric/TerminalLiveFabric.js';
|
|
4
|
-
import { terminalFabricCoordinator } from '../fabric/TerminalFabricCoordinator.js';
|
|
5
|
-
import { TerminalState, } from '../types.js';
|
|
6
|
-
import { terminalInitializationScheduler, } from '../internal/TerminalInitializationScheduler.js';
|
|
7
|
-
import { acquireGhosttyRuntime, getGhosttyFitAddonConstructor, getGhosttyLinkConstructors, getGhosttyTerminalConstructor, inspectGhosttyRuntimeMemory, loadGhosttyModules, waitWithAbort, } from '../internal/TerminalResourceLoader.js';
|
|
8
|
-
import { capAutoFitTerminalColumns, mapGhosttyScrollbackRowsForPinnedVersion, validateTerminalColumns, validateTerminalScrollbackRows, } from '../internal/GhosttyScrollbackCompat.js';
|
|
9
|
-
import { installPinnedGhosttyAlternateScreenProjection, installPinnedGhosttyOutputViewportPolicy, isPinnedGhosttyOutputWriteActive, suppressPinnedGhosttyScrollbarBeforeOpen, suppressPinnedGhosttyScrollbarRenderer, } from '../internal/GhosttyScrollbarCompat.js';
|
|
10
|
-
import { createUnicodeSafeUrlProviderTerminal, } from '../internal/UnicodeSafeUrlProvider.js';
|
|
11
|
-
import { resolveTerminalInputElement, TerminalInputBridge } from './TerminalInputBridge.js';
|
|
12
|
-
import { terminalRenderScheduler } from './TerminalRenderScheduler.js';
|
|
13
|
-
import { scheduleUiTurn } from '../internal/scheduleUiTurn.js';
|
|
14
|
-
import { parseThemeColor } from '../utils/themeColor.js';
|
|
15
|
-
import { normalizeTerminalScrollbarOptions, TerminalScrollbarOverlay, } from './TerminalScrollbarOverlay.js';
|
|
16
|
-
const TERMINAL_SNAPSHOT_VERSION = 1;
|
|
17
|
-
const DEFAULT_TERMINAL_SNAPSHOT_MAX_BYTES = 2 * 1024 * 1024;
|
|
18
|
-
const ESTIMATED_RENDERER_BYTES = 4 * 1024 * 1024;
|
|
19
|
-
const SNAPSHOT_RESET_PREFIX = '\x1bc\x1b[2J\x1b[H';
|
|
20
|
-
const encodedByteLength = (value) => new TextEncoder().encode(value).byteLength;
|
|
21
|
-
const sha256Hex = async (bytes) => {
|
|
22
|
-
if (typeof crypto === 'undefined' || !crypto.subtle) {
|
|
23
|
-
throw new Error('Web Crypto SHA-256 is unavailable for checkpoint validation');
|
|
24
|
-
}
|
|
25
|
-
const digest = await crypto.subtle.digest('SHA-256', bytes.slice());
|
|
26
|
-
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
27
|
-
};
|
|
28
|
-
const TERMINAL_SEARCH_MAX_RESULTS = 5000;
|
|
29
|
-
const PRESENTATION_SCALE_EPSILON = 0.0001;
|
|
30
|
-
const GHOSTTY_DEFAULT_SCROLLBAR_RESERVE_PX = 15;
|
|
31
|
-
const MIN_TERMINAL_COLS = 2;
|
|
32
|
-
const MIN_TERMINAL_ROWS = 1;
|
|
33
|
-
// Search highlighting: all matches use yellow background; active match uses yellow + red text.
|
|
34
|
-
const TERMINAL_SEARCH_MATCH_BACKGROUND = 'rgba(255, 234, 0, 0.38)';
|
|
35
|
-
const TERMINAL_SEARCH_ACTIVE_BACKGROUND = 'rgba(255, 234, 0, 0.72)';
|
|
36
|
-
const TERMINAL_SEARCH_ACTIVE_FOREGROUND = '#dc2626';
|
|
37
|
-
const TERMINAL_SELECTION_BACKGROUND = '#f5e6b3';
|
|
38
|
-
const TERMINAL_SELECTION_FOREGROUND = '#1f2328';
|
|
39
|
-
const TERMINAL_THEME_PALETTE_KEYS = [
|
|
40
|
-
'black',
|
|
41
|
-
'red',
|
|
42
|
-
'green',
|
|
43
|
-
'yellow',
|
|
44
|
-
'blue',
|
|
45
|
-
'magenta',
|
|
46
|
-
'cyan',
|
|
47
|
-
'white',
|
|
48
|
-
'brightBlack',
|
|
49
|
-
'brightRed',
|
|
50
|
-
'brightGreen',
|
|
51
|
-
'brightYellow',
|
|
52
|
-
'brightBlue',
|
|
53
|
-
'brightMagenta',
|
|
54
|
-
'brightCyan',
|
|
55
|
-
'brightWhite',
|
|
56
|
-
];
|
|
57
|
-
function parsePositiveCSSPixelValue(value) {
|
|
58
|
-
if (!value) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
const parsed = Number.parseFloat(value);
|
|
62
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
63
|
-
}
|
|
64
|
-
function resolveCanvasLocalDisplaySize(canvas, coordinateRoot) {
|
|
65
|
-
const canvasRect = canvas.getBoundingClientRect();
|
|
66
|
-
const rootRect = coordinateRoot.getBoundingClientRect();
|
|
67
|
-
const rootWidth = coordinateRoot.clientWidth;
|
|
68
|
-
const rootHeight = coordinateRoot.clientHeight;
|
|
69
|
-
const ratioWidth = rootRect.width > 0 && rootWidth > 0 && canvasRect.width > 0
|
|
70
|
-
? rootWidth * (canvasRect.width / rootRect.width)
|
|
71
|
-
: null;
|
|
72
|
-
const ratioHeight = rootRect.height > 0 && rootHeight > 0 && canvasRect.height > 0
|
|
73
|
-
? rootHeight * (canvasRect.height / rootRect.height)
|
|
74
|
-
: null;
|
|
75
|
-
const computed = (() => {
|
|
76
|
-
try {
|
|
77
|
-
return getComputedStyle(canvas);
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
})();
|
|
83
|
-
const cssWidth = ratioWidth
|
|
84
|
-
?? parsePositiveCSSPixelValue(canvas.style.width)
|
|
85
|
-
?? parsePositiveCSSPixelValue(computed?.width)
|
|
86
|
-
?? (canvas.offsetWidth > 0 ? canvas.offsetWidth : null)
|
|
87
|
-
?? (canvas.clientWidth > 0 ? canvas.clientWidth : null)
|
|
88
|
-
?? canvas.width;
|
|
89
|
-
const cssHeight = ratioHeight
|
|
90
|
-
?? parsePositiveCSSPixelValue(canvas.style.height)
|
|
91
|
-
?? parsePositiveCSSPixelValue(computed?.height)
|
|
92
|
-
?? (canvas.offsetHeight > 0 ? canvas.offsetHeight : null)
|
|
93
|
-
?? (canvas.clientHeight > 0 ? canvas.clientHeight : null)
|
|
94
|
-
?? canvas.height;
|
|
95
|
-
const dpr = cssWidth > 0 ? canvas.width / cssWidth : window.devicePixelRatio ?? 1;
|
|
96
|
-
return {
|
|
97
|
-
cssWidth,
|
|
98
|
-
cssHeight,
|
|
99
|
-
dpr: Number.isFinite(dpr) && dpr > 0 ? dpr : 1,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
function syncCanvasOverlaySize(overlay, termCanvas, coordinateRoot) {
|
|
103
|
-
const { cssWidth, cssHeight, dpr } = resolveCanvasLocalDisplaySize(termCanvas, coordinateRoot);
|
|
104
|
-
if (overlay.canvas.width === termCanvas.width
|
|
105
|
-
&& overlay.canvas.height === termCanvas.height
|
|
106
|
-
&& overlay.cssWidth === cssWidth
|
|
107
|
-
&& overlay.cssHeight === cssHeight
|
|
108
|
-
&& overlay.dpr === dpr) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
overlay.dpr = dpr;
|
|
112
|
-
overlay.cssWidth = cssWidth;
|
|
113
|
-
overlay.cssHeight = cssHeight;
|
|
114
|
-
overlay.canvas.style.width = `${cssWidth}px`;
|
|
115
|
-
overlay.canvas.style.height = `${cssHeight}px`;
|
|
116
|
-
overlay.canvas.width = termCanvas.width;
|
|
117
|
-
overlay.canvas.height = termCanvas.height;
|
|
118
|
-
overlay.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
119
|
-
overlay.ctx.scale(dpr, dpr);
|
|
120
|
-
}
|
|
121
|
-
const getPerfProbe = () => {
|
|
122
|
-
if (typeof window === 'undefined') {
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
return window.__floetermPerfProbe;
|
|
126
|
-
};
|
|
127
|
-
export { preloadTerminalResources } from '../internal/TerminalResourceLoader.js';
|
|
128
|
-
const mapThemeToGhostty = (theme) => {
|
|
129
|
-
if (!theme) {
|
|
130
|
-
return {};
|
|
131
|
-
}
|
|
132
|
-
const mapped = {};
|
|
133
|
-
for (const [key, value] of Object.entries(theme)) {
|
|
134
|
-
if (typeof value === 'string') {
|
|
135
|
-
mapped[key] = value;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (mapped.selection && !mapped.selectionBackground) {
|
|
139
|
-
mapped.selectionBackground = mapped.selection;
|
|
140
|
-
delete mapped.selection;
|
|
141
|
-
}
|
|
142
|
-
return mapped;
|
|
143
|
-
};
|
|
144
|
-
function colorKey(color) {
|
|
145
|
-
return `${color.r},${color.g},${color.b}`;
|
|
146
|
-
}
|
|
147
|
-
function cellColorKey(cell, prefix) {
|
|
148
|
-
const r = Number(cell[`${prefix}_r`]);
|
|
149
|
-
const g = Number(cell[`${prefix}_g`]);
|
|
150
|
-
const b = Number(cell[`${prefix}_b`]);
|
|
151
|
-
if (![r, g, b].every(channel => Number.isInteger(channel) && channel >= 0 && channel <= 255)) {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
return `${r},${g},${b}`;
|
|
155
|
-
}
|
|
156
|
-
function colorsEqual(left, right) {
|
|
157
|
-
return left.r === right.r && left.g === right.g && left.b === right.b;
|
|
158
|
-
}
|
|
159
|
-
function setColorTranslation(map, source, target, key) {
|
|
160
|
-
const sourceColor = parseThemeColor(source[key]);
|
|
161
|
-
const targetColor = parseThemeColor(target[key]);
|
|
162
|
-
if (!sourceColor || !targetColor || colorsEqual(sourceColor, targetColor)) {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
map.set(colorKey(sourceColor), targetColor);
|
|
166
|
-
}
|
|
167
|
-
function buildThemeColorTranslator(source, target) {
|
|
168
|
-
const fg = new Map();
|
|
169
|
-
const bg = new Map();
|
|
170
|
-
for (const key of TERMINAL_THEME_PALETTE_KEYS) {
|
|
171
|
-
setColorTranslation(fg, source, target, key);
|
|
172
|
-
setColorTranslation(bg, source, target, key);
|
|
173
|
-
}
|
|
174
|
-
setColorTranslation(fg, source, target, 'foreground');
|
|
175
|
-
setColorTranslation(bg, source, target, 'background');
|
|
176
|
-
if (fg.size === 0 && bg.size === 0) {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
return { fg, bg };
|
|
180
|
-
}
|
|
181
|
-
function buildCellSignature(renderer, cell, row, col) {
|
|
182
|
-
const grapheme = (cell.grapheme_len ?? 0) > 0
|
|
183
|
-
? String(renderer.currentBuffer?.getGraphemeString?.(row, col) ?? '')
|
|
184
|
-
: '';
|
|
185
|
-
return [
|
|
186
|
-
cell.codepoint ?? 0,
|
|
187
|
-
cell.fg_r ?? 0,
|
|
188
|
-
cell.fg_g ?? 0,
|
|
189
|
-
cell.fg_b ?? 0,
|
|
190
|
-
cell.bg_r ?? 0,
|
|
191
|
-
cell.bg_g ?? 0,
|
|
192
|
-
cell.bg_b ?? 0,
|
|
193
|
-
cell.flags ?? 0,
|
|
194
|
-
cell.width ?? 0,
|
|
195
|
-
cell.hyperlink_id ?? 0,
|
|
196
|
-
cell.grapheme_len ?? 0,
|
|
197
|
-
grapheme,
|
|
198
|
-
].join(',');
|
|
199
|
-
}
|
|
200
|
-
function buildRowSignature(renderer, cells, row, cols) {
|
|
201
|
-
const parts = [`cols:${cols}`, `len:${cells.length}`];
|
|
202
|
-
for (let col = 0; col < cells.length; col += 1) {
|
|
203
|
-
parts.push(buildCellSignature(renderer, cells[col] ?? {}, row, col));
|
|
204
|
-
}
|
|
205
|
-
return parts.join(';');
|
|
206
|
-
}
|
|
207
|
-
function canSkipCachedRowRender(renderer, row) {
|
|
208
|
-
if (renderer.__floetermRowRenderCacheForceAll) {
|
|
209
|
-
return false;
|
|
210
|
-
}
|
|
211
|
-
if (hasTransientRendererState(renderer)) {
|
|
212
|
-
renderer.__floetermRowRenderCache?.clear();
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
const cursorY = renderer.currentBuffer?.getCursor?.()?.y;
|
|
216
|
-
if (typeof cursorY === 'number' && cursorY === row) {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
const previousCursorY = renderer.lastCursorPosition?.y;
|
|
220
|
-
if (typeof previousCursorY === 'number' && previousCursorY === row) {
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
return true;
|
|
224
|
-
}
|
|
225
|
-
function canSkipNativeCanvasRowRender(renderer, row, fabricActive) {
|
|
226
|
-
void renderer;
|
|
227
|
-
void row;
|
|
228
|
-
return fabricActive;
|
|
229
|
-
}
|
|
230
|
-
function samePresentationScale(left, right) {
|
|
231
|
-
return Math.abs(left - right) <= PRESENTATION_SCALE_EPSILON;
|
|
232
|
-
}
|
|
233
|
-
function normalizeTerminalDimensions(value, columnsLabel = 'fixedDimensions.cols') {
|
|
234
|
-
const raw = (typeof value === 'object' && value) ? value : null;
|
|
235
|
-
if (!raw) {
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
const cols = validateTerminalColumns(raw.cols, columnsLabel);
|
|
239
|
-
const rows = Math.floor(Number(raw.rows));
|
|
240
|
-
if (!Number.isFinite(rows) || rows <= 0) {
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
return { cols, rows };
|
|
244
|
-
}
|
|
245
|
-
function sameTerminalDimensions(left, right) {
|
|
246
|
-
return Boolean(left && right && left.cols === right.cols && left.rows === right.rows);
|
|
247
|
-
}
|
|
248
|
-
function normalizeNonNegativePixels(value, fallback) {
|
|
249
|
-
const parsed = Number(value);
|
|
250
|
-
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
251
|
-
return fallback;
|
|
252
|
-
}
|
|
253
|
-
return parsed;
|
|
254
|
-
}
|
|
255
|
-
function hasUsableClientSize(element) {
|
|
256
|
-
return Boolean(element && element.clientWidth > 0 && element.clientHeight > 0);
|
|
257
|
-
}
|
|
258
|
-
function formatCssPixelValue(value) {
|
|
259
|
-
const normalized = Number.isFinite(value) ? Math.max(1, value) : 1;
|
|
260
|
-
return `${Math.round(normalized * 1000) / 1000}px`;
|
|
261
|
-
}
|
|
262
|
-
function hasTransientRendererState(renderer) {
|
|
263
|
-
return Boolean(renderer.currentSelectionCoords
|
|
264
|
-
|| (renderer.hoveredHyperlinkId ?? 0) > 0
|
|
265
|
-
|| renderer.hoveredLinkRange);
|
|
266
|
-
}
|
|
267
|
-
function forceTransientRowsOnExit(renderer) {
|
|
268
|
-
const active = hasTransientRendererState(renderer);
|
|
269
|
-
const previous = Boolean(renderer.__floetermRowRenderCacheHadTransientState);
|
|
270
|
-
renderer.__floetermRowRenderCacheHadTransientState = active;
|
|
271
|
-
return previous && !active;
|
|
272
|
-
}
|
|
273
|
-
function normalizeNumberField(record, key) {
|
|
274
|
-
if (!record || typeof record !== 'object') {
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
const value = Number(record[key]);
|
|
278
|
-
return Number.isFinite(value) ? value : null;
|
|
279
|
-
}
|
|
280
|
-
function normalizeSelectionRange(value) {
|
|
281
|
-
const startCol = normalizeNumberField(value, 'startCol');
|
|
282
|
-
const startRow = normalizeNumberField(value, 'startRow');
|
|
283
|
-
const endCol = normalizeNumberField(value, 'endCol');
|
|
284
|
-
const endRow = normalizeNumberField(value, 'endRow');
|
|
285
|
-
if (startCol === null || startRow === null || endCol === null || endRow === null) {
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
288
|
-
return {
|
|
289
|
-
startCol: Math.max(0, Math.floor(startCol)),
|
|
290
|
-
startRow: Math.max(0, Math.floor(startRow)),
|
|
291
|
-
endCol: Math.max(0, Math.floor(endCol)),
|
|
292
|
-
endRow: Math.max(0, Math.floor(endRow)),
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
function normalizeHoverRange(value) {
|
|
296
|
-
const startX = normalizeNumberField(value, 'startX');
|
|
297
|
-
const startY = normalizeNumberField(value, 'startY');
|
|
298
|
-
const endX = normalizeNumberField(value, 'endX');
|
|
299
|
-
const endY = normalizeNumberField(value, 'endY');
|
|
300
|
-
if (startX === null || startY === null || endX === null || endY === null) {
|
|
301
|
-
return null;
|
|
302
|
-
}
|
|
303
|
-
return {
|
|
304
|
-
startX: Math.max(0, Math.floor(startX)),
|
|
305
|
-
startY: Math.max(0, Math.floor(startY)),
|
|
306
|
-
endX: Math.max(0, Math.floor(endX)),
|
|
307
|
-
endY: Math.max(0, Math.floor(endY)),
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
function normalizeFabricIdentifier(value) {
|
|
311
|
-
return value.trim().replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'terminal';
|
|
312
|
-
}
|
|
313
|
-
// TerminalCore provides a focused wrapper around @floegence/ghostty-web (xterm.js API-compatible) and its fit addon.
|
|
314
|
-
export class TerminalCore {
|
|
315
|
-
constructor(container, config = {}, eventHandlers = {}, logger = createConsoleLogger()) {
|
|
316
|
-
this.container = container;
|
|
317
|
-
this.config = config;
|
|
318
|
-
this.terminal = null;
|
|
319
|
-
this.ownedRuntime = null;
|
|
320
|
-
this.ownedRuntimeMemory = null;
|
|
321
|
-
this.fitAddon = null;
|
|
322
|
-
this.needsFullRenderOnNextWrite = false;
|
|
323
|
-
this.demandRenderForceAll = false;
|
|
324
|
-
this.fabricView = null;
|
|
325
|
-
this.fabricAttachSeq = 0;
|
|
326
|
-
this.fabricAttachPromise = null;
|
|
327
|
-
this.fabricAttachWaiters = [];
|
|
328
|
-
this.committedFabricFrameGeneration = 0;
|
|
329
|
-
this.submittedFabricFrameGeneration = 0;
|
|
330
|
-
this.lastCommittedFabricFrame = null;
|
|
331
|
-
this.pendingPresentationRequest = null;
|
|
332
|
-
this.presentationRequests = new Set();
|
|
333
|
-
this.committedFrameRequests = new Set();
|
|
334
|
-
this.cancelFabricAttachSchedule = null;
|
|
335
|
-
this.cancelInputRefocusSchedule = null;
|
|
336
|
-
this.inputElement = null;
|
|
337
|
-
this.viewportHost = null;
|
|
338
|
-
this.renderHost = null;
|
|
339
|
-
this.scrollbarOverlay = null;
|
|
340
|
-
this.resizeObserver = null;
|
|
341
|
-
this.resizeDebounceTimer = null;
|
|
342
|
-
this.resizeRaf = null;
|
|
343
|
-
this.focusResizeRaf = null;
|
|
344
|
-
this.presentationScaleRaf = null;
|
|
345
|
-
this.state = TerminalState.IDLE;
|
|
346
|
-
this.isDisposed = false;
|
|
347
|
-
this.initializationOperation = null;
|
|
348
|
-
this.isReplayingHistory = false;
|
|
349
|
-
this.historyWriteDepth = 0;
|
|
350
|
-
this.replayingHistoryTimer = null;
|
|
351
|
-
this.logicalFontSize = 12;
|
|
352
|
-
this.presentationScale = 1;
|
|
353
|
-
this.fixedDimensions = null;
|
|
354
|
-
this.terminalThemeSource = {};
|
|
355
|
-
this.themeColorTranslator = null;
|
|
356
|
-
this.fontMetricSeq = 0;
|
|
357
|
-
this.fontMetricRefreshPromise = null;
|
|
358
|
-
this.pendingPresentationScale = null;
|
|
359
|
-
this.suppressResizeNotifications = false;
|
|
360
|
-
this.clearResizeSuppressionRaf = null;
|
|
361
|
-
this.hasFocus = false;
|
|
362
|
-
this.resizeNotifySeq = 0;
|
|
363
|
-
this.lastNotifiedSize = null;
|
|
364
|
-
this.unbindResponsiveListeners = null;
|
|
365
|
-
this.searchQuery = '';
|
|
366
|
-
this.searchOptionsKey = '';
|
|
367
|
-
this.searchMatches = [];
|
|
368
|
-
this.searchMatchIndex = -1;
|
|
369
|
-
this.searchRowIndex = new Map();
|
|
370
|
-
this.searchResultsCallback = null;
|
|
371
|
-
this.searchOverlay = null;
|
|
372
|
-
this.searchOverlayRaf = null;
|
|
373
|
-
this.searchOverlayUnsubs = [];
|
|
374
|
-
// Temporary theme override while search is active.
|
|
375
|
-
this.searchThemeRestore = null;
|
|
376
|
-
this.inputBridge = null;
|
|
377
|
-
this.terminalEventDisposables = [];
|
|
378
|
-
this.registeredLinkProviders = new Set();
|
|
379
|
-
this.appliedLinkProviders = new Set();
|
|
380
|
-
this.visualRenderState = {
|
|
381
|
-
suspendDepth: 0,
|
|
382
|
-
nextSuspendId: 1,
|
|
383
|
-
activeReasons: new Map(),
|
|
384
|
-
pendingDemandRender: false,
|
|
385
|
-
pendingForceFullRender: false,
|
|
386
|
-
pendingResizeReason: null,
|
|
387
|
-
pendingSearchOverlayRender: false,
|
|
388
|
-
};
|
|
389
|
-
const explicitCols = config?.cols;
|
|
390
|
-
if (explicitCols !== undefined) {
|
|
391
|
-
validateTerminalColumns(explicitCols);
|
|
392
|
-
}
|
|
393
|
-
this.scrollbackRows = validateTerminalScrollbackRows(config?.scrollback === undefined ? 1000 : config.scrollback);
|
|
394
|
-
this.eventHandlers = eventHandlers;
|
|
395
|
-
this.clipboard = TerminalCore.normalizeClipboardConfig(config?.clipboard);
|
|
396
|
-
this.fit = TerminalCore.normalizeFitConfig(config?.fit);
|
|
397
|
-
this.scrollbarOptions = normalizeTerminalScrollbarOptions(config?.scrollbar);
|
|
398
|
-
this.logger = logger ?? noopLogger;
|
|
399
|
-
this.responsive = TerminalCore.normalizeResponsiveConfig(config?.responsive);
|
|
400
|
-
this.logicalFontSize = TerminalCore.normalizeFontSize(config?.fontSize);
|
|
401
|
-
this.presentationScale = TerminalCore.normalizePresentationScale(config?.presentationScale);
|
|
402
|
-
this.fixedDimensions = normalizeTerminalDimensions(config?.fixedDimensions);
|
|
403
|
-
this.config = {
|
|
404
|
-
...config,
|
|
405
|
-
...(explicitCols === undefined ? {} : { cols: explicitCols }),
|
|
406
|
-
fixedDimensions: this.fixedDimensions,
|
|
407
|
-
scrollback: this.scrollbackRows,
|
|
408
|
-
scrollbar: this.scrollbarOptions,
|
|
409
|
-
};
|
|
410
|
-
this.renderTask = {
|
|
411
|
-
id: TerminalCore.nextRenderTaskId,
|
|
412
|
-
run: forceAll => {
|
|
413
|
-
this.demandRenderForceAll = false;
|
|
414
|
-
this.renderDemandFrame(forceAll);
|
|
415
|
-
},
|
|
416
|
-
};
|
|
417
|
-
this.fabricViewId = `floeterm-view-${this.renderTask.id}`;
|
|
418
|
-
TerminalCore.nextRenderTaskId += 1;
|
|
419
|
-
}
|
|
420
|
-
// initialize creates the @floegence/ghostty-web terminal instance and binds addons.
|
|
421
|
-
async initialize(options = {}) {
|
|
422
|
-
if (this.isDisposed) {
|
|
423
|
-
throw new Error('Cannot initialize a disposed TerminalCore');
|
|
424
|
-
}
|
|
425
|
-
if (this.terminal && this.isReady()) {
|
|
426
|
-
return Promise.resolve();
|
|
427
|
-
}
|
|
428
|
-
const priority = options.priority ?? 'interactive';
|
|
429
|
-
let operation = this.initializationOperation;
|
|
430
|
-
if (operation) {
|
|
431
|
-
if (priority === 'interactive')
|
|
432
|
-
operation.request.promote();
|
|
433
|
-
return this.waitForInitialization(operation, options.signal);
|
|
434
|
-
}
|
|
435
|
-
try {
|
|
436
|
-
this.setState(TerminalState.INITIALIZING);
|
|
437
|
-
}
|
|
438
|
-
catch (error) {
|
|
439
|
-
this.state = TerminalState.IDLE;
|
|
440
|
-
throw error;
|
|
441
|
-
}
|
|
442
|
-
const request = terminalInitializationScheduler.request(priority);
|
|
443
|
-
const lifecycleAbortController = new AbortController();
|
|
444
|
-
operation = {
|
|
445
|
-
request,
|
|
446
|
-
lifecycleAbortController,
|
|
447
|
-
started: false,
|
|
448
|
-
cancelled: false,
|
|
449
|
-
waiters: 0,
|
|
450
|
-
promise: Promise.resolve(),
|
|
451
|
-
};
|
|
452
|
-
operation.promise = (async () => {
|
|
453
|
-
const assertInitializationActive = () => {
|
|
454
|
-
if (this.isDisposed)
|
|
455
|
-
throw new Error('Cannot initialize a disposed TerminalCore');
|
|
456
|
-
lifecycleAbortController.signal.throwIfAborted();
|
|
457
|
-
};
|
|
458
|
-
const permit = await request.permit;
|
|
459
|
-
if (!permit) {
|
|
460
|
-
if (this.isDisposed)
|
|
461
|
-
throw new Error('Cannot initialize a disposed TerminalCore');
|
|
462
|
-
if (this.initializationOperation === operation)
|
|
463
|
-
this.setState(TerminalState.IDLE);
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
operation.started = true;
|
|
467
|
-
try {
|
|
468
|
-
assertInitializationActive();
|
|
469
|
-
await loadGhosttyModules(this.logger, lifecycleAbortController.signal);
|
|
470
|
-
assertInitializationActive();
|
|
471
|
-
const ownedRuntime = await acquireGhosttyRuntime(this.logger);
|
|
472
|
-
assertInitializationActive();
|
|
473
|
-
const ownedRuntimeMemory = inspectGhosttyRuntimeMemory(ownedRuntime);
|
|
474
|
-
assertInitializationActive();
|
|
475
|
-
this.ownedRuntime = ownedRuntime;
|
|
476
|
-
this.ownedRuntimeMemory = ownedRuntimeMemory;
|
|
477
|
-
await this.createTerminalInstance(lifecycleAbortController.signal);
|
|
478
|
-
assertInitializationActive();
|
|
479
|
-
await this.loadAddons();
|
|
480
|
-
assertInitializationActive();
|
|
481
|
-
await this.openTerminal(lifecycleAbortController.signal);
|
|
482
|
-
assertInitializationActive();
|
|
483
|
-
this.setupEventListeners();
|
|
484
|
-
this.setupResponsiveListeners();
|
|
485
|
-
this.startSizeWatching();
|
|
486
|
-
this.setState(TerminalState.READY);
|
|
487
|
-
this.performResize('post_init');
|
|
488
|
-
this.forceFullRender();
|
|
489
|
-
setTimeout(() => {
|
|
490
|
-
if (!this.isReady())
|
|
491
|
-
return;
|
|
492
|
-
this.performResize('post_init');
|
|
493
|
-
}, 100);
|
|
494
|
-
}
|
|
495
|
-
catch (error) {
|
|
496
|
-
this.cleanupFailedInitialization();
|
|
497
|
-
if (!this.isDisposed)
|
|
498
|
-
this.setState(TerminalState.IDLE);
|
|
499
|
-
throw error;
|
|
500
|
-
}
|
|
501
|
-
finally {
|
|
502
|
-
permit.release();
|
|
503
|
-
}
|
|
504
|
-
})().finally(() => {
|
|
505
|
-
if (this.initializationOperation === operation)
|
|
506
|
-
this.initializationOperation = null;
|
|
507
|
-
});
|
|
508
|
-
this.initializationOperation = operation;
|
|
509
|
-
return this.waitForInitialization(operation, options.signal);
|
|
510
|
-
}
|
|
511
|
-
waitForInitialization(operation, signal) {
|
|
512
|
-
operation.waiters += 1;
|
|
513
|
-
let settled = false;
|
|
514
|
-
const releaseWaiter = () => {
|
|
515
|
-
if (settled)
|
|
516
|
-
return;
|
|
517
|
-
settled = true;
|
|
518
|
-
operation.waiters = Math.max(0, operation.waiters - 1);
|
|
519
|
-
if (operation.waiters !== 0 || operation.cancelled)
|
|
520
|
-
return;
|
|
521
|
-
if (operation.started) {
|
|
522
|
-
if (this.initializationOperation === operation
|
|
523
|
-
&& !this.isDisposed
|
|
524
|
-
&& this.state === TerminalState.INITIALIZING) {
|
|
525
|
-
operation.cancelled = true;
|
|
526
|
-
// Ghostty.load() cannot be cancelled. Abort the Core lifecycle so the
|
|
527
|
-
// owned runtime is discarded when that load settles, while the
|
|
528
|
-
// initialization permit remains held by the operation's finally path.
|
|
529
|
-
operation.lifecycleAbortController.abort();
|
|
530
|
-
}
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
operation.cancelled = true;
|
|
534
|
-
operation.request.cancel();
|
|
535
|
-
if (this.initializationOperation === operation) {
|
|
536
|
-
this.initializationOperation = null;
|
|
537
|
-
if (!this.isDisposed)
|
|
538
|
-
this.setState(TerminalState.IDLE);
|
|
539
|
-
}
|
|
540
|
-
};
|
|
541
|
-
return waitWithAbort(operation.promise, signal).finally(releaseWaiter);
|
|
542
|
-
}
|
|
543
|
-
cleanupFailedInitialization() {
|
|
544
|
-
this.unbindResponsiveListeners?.();
|
|
545
|
-
this.unbindResponsiveListeners = null;
|
|
546
|
-
this.resizeObserver?.disconnect();
|
|
547
|
-
this.resizeObserver = null;
|
|
548
|
-
this.disposeInputBridge();
|
|
549
|
-
this.disposeTerminalEventListeners();
|
|
550
|
-
this.scrollbarOverlay?.dispose();
|
|
551
|
-
this.scrollbarOverlay = null;
|
|
552
|
-
this.fabricView?.dispose();
|
|
553
|
-
this.fabricView = null;
|
|
554
|
-
this.terminal?.dispose();
|
|
555
|
-
this.terminal = null;
|
|
556
|
-
this.ownedRuntimeMemory = null;
|
|
557
|
-
this.ownedRuntime = null;
|
|
558
|
-
this.inputElement?.remove();
|
|
559
|
-
this.inputElement = null;
|
|
560
|
-
this.fitAddon = null;
|
|
561
|
-
this.viewportHost?.remove();
|
|
562
|
-
this.viewportHost = null;
|
|
563
|
-
this.renderHost = null;
|
|
564
|
-
}
|
|
565
|
-
async createTerminalInstance(signal) {
|
|
566
|
-
const TerminalCtor = getGhosttyTerminalConstructor();
|
|
567
|
-
if (!TerminalCtor) {
|
|
568
|
-
throw new Error('@floegence/ghostty-web module not loaded');
|
|
569
|
-
}
|
|
570
|
-
if (!this.ownedRuntime) {
|
|
571
|
-
throw new Error('@floegence/ghostty-web runtime not acquired');
|
|
572
|
-
}
|
|
573
|
-
const defaultConfig = {
|
|
574
|
-
cols: 80,
|
|
575
|
-
rows: 24,
|
|
576
|
-
theme: {
|
|
577
|
-
background: '#1a1a1a',
|
|
578
|
-
foreground: '#ffffff',
|
|
579
|
-
cursor: '#ffffff',
|
|
580
|
-
selectionBackground: TERMINAL_SELECTION_BACKGROUND,
|
|
581
|
-
selectionForeground: TERMINAL_SELECTION_FOREGROUND
|
|
582
|
-
},
|
|
583
|
-
fontSize: 12,
|
|
584
|
-
fontFamily: '\"SF Mono\", Monaco, \"Cascadia Code\", \"Roboto Mono\", Consolas, \"Courier New\", monospace',
|
|
585
|
-
cursorBlink: false,
|
|
586
|
-
scrollback: 1000,
|
|
587
|
-
allowTransparency: false,
|
|
588
|
-
convertEol: true,
|
|
589
|
-
cursorStyle: 'block',
|
|
590
|
-
disableStdin: false,
|
|
591
|
-
smoothScrollDuration: 80
|
|
592
|
-
};
|
|
593
|
-
const finalConfig = { ...defaultConfig, ...this.config };
|
|
594
|
-
const initialTheme = mapThemeToGhostty(finalConfig.theme);
|
|
595
|
-
this.terminalThemeSource = initialTheme;
|
|
596
|
-
this.themeColorTranslator = null;
|
|
597
|
-
this.logicalFontSize = TerminalCore.normalizeFontSize(finalConfig.fontSize);
|
|
598
|
-
this.presentationScale = TerminalCore.normalizePresentationScale(finalConfig.presentationScale);
|
|
599
|
-
await waitWithAbort(this.waitForTerminalFontReady(typeof finalConfig.fontFamily === 'string' ? finalConfig.fontFamily : undefined, this.resolveEffectiveFontSize(), 'initial'), signal);
|
|
600
|
-
const initialDimensions = this.fixedDimensions;
|
|
601
|
-
this.terminal = new TerminalCtor({
|
|
602
|
-
cols: initialDimensions?.cols ?? (typeof finalConfig.cols === 'number' ? finalConfig.cols : undefined),
|
|
603
|
-
rows: initialDimensions?.rows ?? (typeof finalConfig.rows === 'number' ? finalConfig.rows : undefined),
|
|
604
|
-
cursorBlink: typeof finalConfig.cursorBlink === 'boolean' ? finalConfig.cursorBlink : undefined,
|
|
605
|
-
cursorStyle: typeof finalConfig.cursorStyle === 'string' ? finalConfig.cursorStyle : undefined,
|
|
606
|
-
theme: initialTheme,
|
|
607
|
-
scrollback: mapGhosttyScrollbackRowsForPinnedVersion(this.scrollbackRows),
|
|
608
|
-
fontSize: this.resolveEffectiveFontSize(),
|
|
609
|
-
fontFamily: typeof finalConfig.fontFamily === 'string' ? finalConfig.fontFamily : undefined,
|
|
610
|
-
allowTransparency: typeof finalConfig.allowTransparency === 'boolean' ? finalConfig.allowTransparency : undefined,
|
|
611
|
-
convertEol: typeof finalConfig.convertEol === 'boolean' ? finalConfig.convertEol : undefined,
|
|
612
|
-
disableStdin: typeof finalConfig.disableStdin === 'boolean' ? finalConfig.disableStdin : undefined,
|
|
613
|
-
smoothScrollDuration: typeof finalConfig.smoothScrollDuration === 'number' ? finalConfig.smoothScrollDuration : undefined,
|
|
614
|
-
ghostty: this.ownedRuntime,
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
async loadAddons() {
|
|
618
|
-
if (!this.terminal) {
|
|
619
|
-
throw new Error('Terminal instance not created');
|
|
620
|
-
}
|
|
621
|
-
const FitAddonCtor = getGhosttyFitAddonConstructor();
|
|
622
|
-
if (!FitAddonCtor) {
|
|
623
|
-
throw new Error('Required @floegence/ghostty-web addons not loaded');
|
|
624
|
-
}
|
|
625
|
-
this.fitAddon = new FitAddonCtor();
|
|
626
|
-
this.terminal.loadAddon(this.fitAddon);
|
|
627
|
-
this.installFitAddonGeometryPatch();
|
|
628
|
-
}
|
|
629
|
-
async openTerminal(signal) {
|
|
630
|
-
if (!this.terminal) {
|
|
631
|
-
throw new Error('Terminal instance not created');
|
|
632
|
-
}
|
|
633
|
-
await waitWithAbort(this.waitForDOMAndFonts(), signal);
|
|
634
|
-
await waitWithAbort(this.ensureContainerReady(), signal);
|
|
635
|
-
const renderHost = this.ensurePresentationHosts();
|
|
636
|
-
this.applyPresentationScaleStyles();
|
|
637
|
-
this.installDemandRenderPatchBeforeOpen();
|
|
638
|
-
suppressPinnedGhosttyScrollbarBeforeOpen(this.terminal);
|
|
639
|
-
this.terminal.open(renderHost);
|
|
640
|
-
installPinnedGhosttyAlternateScreenProjection(this.terminal);
|
|
641
|
-
installPinnedGhosttyOutputViewportPolicy(this.terminal);
|
|
642
|
-
const renderer = this.terminal.renderer;
|
|
643
|
-
suppressPinnedGhosttyScrollbarRenderer(renderer ?? {});
|
|
644
|
-
this.installLinkDetector();
|
|
645
|
-
if (this.config.rendererType === 'webgl') {
|
|
646
|
-
const canvas = this.terminal
|
|
647
|
-
.renderer?.getCanvas?.();
|
|
648
|
-
if (canvas)
|
|
649
|
-
canvas.style.opacity = '0';
|
|
650
|
-
}
|
|
651
|
-
this.mountInputElement(renderHost);
|
|
652
|
-
this.stopGhosttyRenderLoop();
|
|
653
|
-
this.patchDemandRenderTriggersAfterOpen();
|
|
654
|
-
this.patchSelectionManagerClipboardBehavior();
|
|
655
|
-
this.patchSelectionManagerRenderingBehavior();
|
|
656
|
-
this.setupInputBridge(renderHost);
|
|
657
|
-
this.mountScrollbarOverlay();
|
|
658
|
-
this.applyRegisteredLinkProviders();
|
|
659
|
-
void this.refreshFontMetricsAfterLoad('open');
|
|
660
|
-
}
|
|
661
|
-
mountInputElement(renderHost) {
|
|
662
|
-
const input = resolveTerminalInputElement(renderHost);
|
|
663
|
-
if (!input) {
|
|
664
|
-
throw new Error('@floegence/ghostty-web terminal input element was not created');
|
|
665
|
-
}
|
|
666
|
-
const body = this.container.ownerDocument.body;
|
|
667
|
-
if (!body) {
|
|
668
|
-
throw new Error('Terminal input requires a document body');
|
|
669
|
-
}
|
|
670
|
-
body.appendChild(input);
|
|
671
|
-
this.inputElement = input;
|
|
672
|
-
}
|
|
673
|
-
mountScrollbarOverlay() {
|
|
674
|
-
if (!this.terminal || !this.viewportHost || !this.renderHost) {
|
|
675
|
-
throw new Error('Terminal scrollbar requires an initialized presentation host');
|
|
676
|
-
}
|
|
677
|
-
this.scrollbarOverlay?.dispose();
|
|
678
|
-
const terminal = this.terminal;
|
|
679
|
-
const surfaceId = `${this.fabricViewId}-surface`;
|
|
680
|
-
this.renderHost.id = surfaceId;
|
|
681
|
-
this.scrollbarOverlay = new TerminalScrollbarOverlay(this.viewportHost, {
|
|
682
|
-
get rows() {
|
|
683
|
-
return terminal.rows;
|
|
684
|
-
},
|
|
685
|
-
getScrollbackLength: () => terminal.getScrollbackLength(),
|
|
686
|
-
getViewportY: () => terminal.getViewportY(),
|
|
687
|
-
isAlternateScreen: () => terminal.isAlternateScreen(),
|
|
688
|
-
scrollToLine: line => terminal.scrollToLine(line),
|
|
689
|
-
scrollToTop: () => terminal.scrollToTop(),
|
|
690
|
-
scrollToBottom: () => terminal.scrollToBottom(),
|
|
691
|
-
scrollLines: amount => terminal.scrollLines(amount),
|
|
692
|
-
scrollPages: amount => terminal.scrollPages(amount),
|
|
693
|
-
focusTerminal: () => this.scheduleInputSurfaceRefocus({ preventScroll: true }),
|
|
694
|
-
}, this.scrollbarOptions, surfaceId);
|
|
695
|
-
this.scrollbarOverlay.setBackgroundColor(this.terminalThemeSource.background);
|
|
696
|
-
}
|
|
697
|
-
installLinkDetector() {
|
|
698
|
-
if (!this.terminal) {
|
|
699
|
-
throw new Error('Terminal instance not created');
|
|
700
|
-
}
|
|
701
|
-
const { LinkDetector, OSC8LinkProvider, UrlRegexProvider, } = getGhosttyLinkConstructors();
|
|
702
|
-
const detector = new LinkDetector(this.terminal);
|
|
703
|
-
detector.registerProvider(new OSC8LinkProvider(this.terminal));
|
|
704
|
-
detector.registerProvider(new UrlRegexProvider(createUnicodeSafeUrlProviderTerminal(this.terminal)));
|
|
705
|
-
this.terminal.linkDetector = detector;
|
|
706
|
-
}
|
|
707
|
-
installFitAddonGeometryPatch() {
|
|
708
|
-
const fitAddon = this.fitAddon;
|
|
709
|
-
if (!fitAddon || fitAddon.__floetermGeometryPatchApplied) {
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
fitAddon.__floetermGeometryPatchApplied = true;
|
|
713
|
-
fitAddon.proposeDimensions = () => this.proposeFitDimensions();
|
|
714
|
-
}
|
|
715
|
-
proposeFitDimensions() {
|
|
716
|
-
const fabricDimensions = this.proposeFabricFitDimensions();
|
|
717
|
-
if (fabricDimensions) {
|
|
718
|
-
return fabricDimensions;
|
|
719
|
-
}
|
|
720
|
-
const terminalAny = this.terminal;
|
|
721
|
-
const renderer = terminalAny?.renderer;
|
|
722
|
-
const metrics = typeof renderer?.getMetrics === 'function' ? renderer.getMetrics() : null;
|
|
723
|
-
const cellWidth = Number(metrics?.width);
|
|
724
|
-
const cellHeight = Number(metrics?.height);
|
|
725
|
-
if (!Number.isFinite(cellWidth) || !Number.isFinite(cellHeight) || cellWidth <= 0 || cellHeight <= 0) {
|
|
726
|
-
return undefined;
|
|
727
|
-
}
|
|
728
|
-
const element = this.resolveFitElement(terminalAny?.element);
|
|
729
|
-
if (!element || typeof element.clientWidth === 'undefined') {
|
|
730
|
-
return undefined;
|
|
731
|
-
}
|
|
732
|
-
const width = element.clientWidth;
|
|
733
|
-
const height = element.clientHeight;
|
|
734
|
-
if (width === 0 || height === 0) {
|
|
735
|
-
return undefined;
|
|
736
|
-
}
|
|
737
|
-
const style = getComputedStyle(element);
|
|
738
|
-
const paddingTop = Number.parseInt(style.getPropertyValue('padding-top')) || 0;
|
|
739
|
-
const paddingBottom = Number.parseInt(style.getPropertyValue('padding-bottom')) || 0;
|
|
740
|
-
const paddingLeft = Number.parseInt(style.getPropertyValue('padding-left')) || 0;
|
|
741
|
-
const paddingRight = Number.parseInt(style.getPropertyValue('padding-right')) || 0;
|
|
742
|
-
const availableWidth = Math.max(0, width - paddingLeft - paddingRight - this.fit.scrollbarReservePx);
|
|
743
|
-
const availableHeight = Math.max(0, height - paddingTop - paddingBottom);
|
|
744
|
-
return {
|
|
745
|
-
cols: capAutoFitTerminalColumns(Math.max(MIN_TERMINAL_COLS, Math.floor(availableWidth / cellWidth))),
|
|
746
|
-
rows: Math.max(MIN_TERMINAL_ROWS, Math.floor(availableHeight / cellHeight)),
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
proposeFabricFitDimensions() {
|
|
750
|
-
if (this.config.rendererType !== 'webgl') {
|
|
751
|
-
return null;
|
|
752
|
-
}
|
|
753
|
-
const geometry = this.fabricView?.renderer.getGeometry();
|
|
754
|
-
if (!geometry) {
|
|
755
|
-
return null;
|
|
756
|
-
}
|
|
757
|
-
const cellWidth = Number(geometry.cellWidth);
|
|
758
|
-
const hostWidth = Number(this.viewportHost?.clientWidth ?? this.container.clientWidth);
|
|
759
|
-
const visibleCellWidth = cellWidth / this.presentationScale;
|
|
760
|
-
const cols = Number.isFinite(hostWidth) && hostWidth > 0
|
|
761
|
-
&& Number.isFinite(visibleCellWidth) && visibleCellWidth > 0
|
|
762
|
-
? Math.floor(Math.max(0, hostWidth - this.fit.scrollbarReservePx) / visibleCellWidth)
|
|
763
|
-
: Math.floor(Number(geometry.cols));
|
|
764
|
-
const rows = Math.floor(Number(geometry.rows));
|
|
765
|
-
if (!Number.isFinite(cols) || !Number.isFinite(rows) || cols <= 0 || rows <= 0) {
|
|
766
|
-
return null;
|
|
767
|
-
}
|
|
768
|
-
return {
|
|
769
|
-
cols: capAutoFitTerminalColumns(Math.max(MIN_TERMINAL_COLS, cols)),
|
|
770
|
-
rows: Math.max(MIN_TERMINAL_ROWS, rows),
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
syncGhosttyInputGeometryToFabric() {
|
|
774
|
-
if (!this.terminal || !this.isFabricRendererActive()) {
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
const geometry = this.fabricView?.renderer.getGeometry();
|
|
778
|
-
if (!geometry) {
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
const terminalAny = this.terminal;
|
|
782
|
-
const renderer = terminalAny.renderer;
|
|
783
|
-
const canvas = renderer?.getCanvas?.();
|
|
784
|
-
const cols = Math.floor(Number(terminalAny.cols ?? geometry.cols));
|
|
785
|
-
const rows = Math.floor(Number(terminalAny.rows ?? geometry.rows));
|
|
786
|
-
const cellWidth = Number(geometry.cellWidth);
|
|
787
|
-
const cellHeight = Number(geometry.cellHeight);
|
|
788
|
-
if (!renderer
|
|
789
|
-
|| !canvas
|
|
790
|
-
|| !Number.isFinite(cols)
|
|
791
|
-
|| !Number.isFinite(rows)
|
|
792
|
-
|| !Number.isFinite(cellWidth)
|
|
793
|
-
|| !Number.isFinite(cellHeight)
|
|
794
|
-
|| cols <= 0
|
|
795
|
-
|| rows <= 0
|
|
796
|
-
|| cellWidth <= 0
|
|
797
|
-
|| cellHeight <= 0) {
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
const currentMetrics = renderer.getMetrics?.() ?? renderer.metrics ?? {};
|
|
801
|
-
const baselineRatio = Number(currentMetrics.height) > 0
|
|
802
|
-
? Number(currentMetrics.baseline ?? cellHeight * 0.8) / Number(currentMetrics.height)
|
|
803
|
-
: 0.8;
|
|
804
|
-
const nextMetrics = {
|
|
805
|
-
...currentMetrics,
|
|
806
|
-
width: cellWidth,
|
|
807
|
-
height: cellHeight,
|
|
808
|
-
baseline: Math.max(1, Math.round(cellHeight * baselineRatio)),
|
|
809
|
-
};
|
|
810
|
-
renderer.metrics = nextMetrics;
|
|
811
|
-
const gridWidth = cols * cellWidth;
|
|
812
|
-
const gridHeight = rows * cellHeight;
|
|
813
|
-
canvas.style.width = formatCssPixelValue(gridWidth);
|
|
814
|
-
canvas.style.height = formatCssPixelValue(gridHeight);
|
|
815
|
-
canvas.style.maxWidth = 'none';
|
|
816
|
-
canvas.style.maxHeight = 'none';
|
|
817
|
-
const canvasDpr = canvas.width > 0 && gridWidth > 0
|
|
818
|
-
? canvas.width / gridWidth
|
|
819
|
-
: window.devicePixelRatio ?? 1;
|
|
820
|
-
const dpr = Number.isFinite(canvasDpr) && canvasDpr > 0 ? canvasDpr : 1;
|
|
821
|
-
const expectedWidth = Math.max(1, Math.round(gridWidth * dpr));
|
|
822
|
-
const expectedHeight = Math.max(1, Math.round(gridHeight * dpr));
|
|
823
|
-
if (canvas.width !== expectedWidth || canvas.height !== expectedHeight) {
|
|
824
|
-
canvas.width = expectedWidth;
|
|
825
|
-
canvas.height = expectedHeight;
|
|
826
|
-
}
|
|
827
|
-
canvas.style.position = 'absolute';
|
|
828
|
-
canvas.style.left = '0';
|
|
829
|
-
canvas.style.top = '0';
|
|
830
|
-
canvas.style.inset = '0 auto auto 0';
|
|
831
|
-
canvas.style.zIndex = '1';
|
|
832
|
-
this.syncImeInputAnchor();
|
|
833
|
-
}
|
|
834
|
-
syncImeInputAnchor() {
|
|
835
|
-
const input = this.inputElement;
|
|
836
|
-
if (!input || this.shouldPreserveTransientInputPlacement(input)) {
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
const geometry = this.resolveImeInputAnchorGeometry();
|
|
840
|
-
const cursor = this.resolveImeCursorPosition();
|
|
841
|
-
if (!geometry || !cursor) {
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
const canvasRect = geometry.canvas.getBoundingClientRect();
|
|
845
|
-
const canvasClientWidth = Number(geometry.canvas.clientWidth);
|
|
846
|
-
const canvasClientHeight = Number(geometry.canvas.clientHeight);
|
|
847
|
-
if (!Number.isFinite(canvasClientWidth)
|
|
848
|
-
|| !Number.isFinite(canvasClientHeight)
|
|
849
|
-
|| canvasClientWidth <= 0
|
|
850
|
-
|| canvasClientHeight <= 0) {
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
const visibleScaleX = Number(canvasRect.width) / canvasClientWidth;
|
|
854
|
-
const visibleScaleY = Number(canvasRect.height) / canvasClientHeight;
|
|
855
|
-
const cellWidth = geometry.cellWidth * visibleScaleX;
|
|
856
|
-
const cellHeight = geometry.cellHeight * visibleScaleY;
|
|
857
|
-
const left = Number(canvasRect.left) + cursor.x * cellWidth;
|
|
858
|
-
const top = Number(canvasRect.top) + cursor.y * cellHeight;
|
|
859
|
-
if (!Number.isFinite(left)
|
|
860
|
-
|| !Number.isFinite(top)
|
|
861
|
-
|| !Number.isFinite(cellWidth)
|
|
862
|
-
|| !Number.isFinite(cellHeight)
|
|
863
|
-
|| cellWidth <= 0
|
|
864
|
-
|| cellHeight <= 0) {
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
input.style.position = 'fixed';
|
|
868
|
-
input.style.left = formatCssPixelValue(left);
|
|
869
|
-
input.style.top = formatCssPixelValue(top);
|
|
870
|
-
input.style.width = formatCssPixelValue(cellWidth);
|
|
871
|
-
input.style.height = formatCssPixelValue(cellHeight);
|
|
872
|
-
input.style.lineHeight = formatCssPixelValue(cellHeight);
|
|
873
|
-
input.style.padding = '0';
|
|
874
|
-
input.style.border = 'none';
|
|
875
|
-
input.style.margin = '0';
|
|
876
|
-
input.style.opacity = '0';
|
|
877
|
-
input.style.overflow = 'hidden';
|
|
878
|
-
input.style.whiteSpace = 'nowrap';
|
|
879
|
-
input.style.resize = 'none';
|
|
880
|
-
input.style.clipPath = 'none';
|
|
881
|
-
input.style.pointerEvents = 'none';
|
|
882
|
-
input.style.zIndex = '0';
|
|
883
|
-
}
|
|
884
|
-
shouldPreserveTransientInputPlacement(input) {
|
|
885
|
-
return input.style.pointerEvents === 'auto' || input.style.zIndex === '1000';
|
|
886
|
-
}
|
|
887
|
-
resolveImeInputAnchorGeometry() {
|
|
888
|
-
const terminalAny = this.terminal;
|
|
889
|
-
const renderer = terminalAny?.renderer ?? null;
|
|
890
|
-
const canvas = renderer?.getCanvas?.() ?? this.container.querySelector('canvas');
|
|
891
|
-
if (!(canvas instanceof HTMLCanvasElement)) {
|
|
892
|
-
return null;
|
|
893
|
-
}
|
|
894
|
-
const fabricGeometry = this.isFabricRendererActive()
|
|
895
|
-
? this.fabricView?.renderer.getGeometry()
|
|
896
|
-
: null;
|
|
897
|
-
if (fabricGeometry) {
|
|
898
|
-
const cellWidth = Number(fabricGeometry.cellWidth);
|
|
899
|
-
const cellHeight = Number(fabricGeometry.cellHeight);
|
|
900
|
-
const cols = Math.floor(Number(terminalAny?.cols ?? fabricGeometry.cols));
|
|
901
|
-
const rows = Math.floor(Number(terminalAny?.rows ?? fabricGeometry.rows));
|
|
902
|
-
if (this.isUsableAnchorGeometry(cellWidth, cellHeight, cols, rows)) {
|
|
903
|
-
return { canvas, cellWidth, cellHeight, cols, rows };
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
const metrics = renderer?.getMetrics?.() ?? renderer?.metrics ?? {};
|
|
907
|
-
const cols = Math.floor(Number(terminalAny?.cols));
|
|
908
|
-
const rows = Math.floor(Number(terminalAny?.rows));
|
|
909
|
-
const fallbackCellWidth = cols > 0 ? Number(canvas.clientWidth) / cols : Number.NaN;
|
|
910
|
-
const fallbackCellHeight = rows > 0 ? Number(canvas.clientHeight) / rows : Number.NaN;
|
|
911
|
-
const cellWidth = Number(metrics.width ?? renderer?.charWidth ?? fallbackCellWidth);
|
|
912
|
-
const cellHeight = Number(metrics.height ?? renderer?.charHeight ?? fallbackCellHeight);
|
|
913
|
-
if (!this.isUsableAnchorGeometry(cellWidth, cellHeight, cols, rows)) {
|
|
914
|
-
return null;
|
|
915
|
-
}
|
|
916
|
-
return { canvas, cellWidth, cellHeight, cols, rows };
|
|
917
|
-
}
|
|
918
|
-
isUsableAnchorGeometry(cellWidth, cellHeight, cols, rows) {
|
|
919
|
-
return Number.isFinite(cellWidth)
|
|
920
|
-
&& Number.isFinite(cellHeight)
|
|
921
|
-
&& Number.isFinite(cols)
|
|
922
|
-
&& Number.isFinite(rows)
|
|
923
|
-
&& cellWidth > 0
|
|
924
|
-
&& cellHeight > 0
|
|
925
|
-
&& cols > 0
|
|
926
|
-
&& rows > 0;
|
|
927
|
-
}
|
|
928
|
-
resolveImeCursorPosition() {
|
|
929
|
-
const terminalAny = this.terminal;
|
|
930
|
-
const cursor = terminalAny?.wasmTerm?.getCursor?.();
|
|
931
|
-
if (!cursor) {
|
|
932
|
-
return null;
|
|
933
|
-
}
|
|
934
|
-
const cols = Math.max(1, Math.floor(Number(terminalAny?.cols ?? 1)));
|
|
935
|
-
const rows = Math.max(1, Math.floor(Number(terminalAny?.rows ?? 1)));
|
|
936
|
-
const rawX = Number(cursor.viewportX ?? cursor.x);
|
|
937
|
-
const rawY = Number(cursor.viewportY ?? cursor.y);
|
|
938
|
-
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) {
|
|
939
|
-
return null;
|
|
940
|
-
}
|
|
941
|
-
return {
|
|
942
|
-
x: Math.max(0, Math.min(cols - 1, Math.floor(rawX))),
|
|
943
|
-
y: Math.max(0, Math.min(rows - 1, Math.floor(rawY))),
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
focusInputSurface(options) {
|
|
947
|
-
this.inputBridge?.focus(options);
|
|
948
|
-
}
|
|
949
|
-
scheduleInputSurfaceRefocus(options) {
|
|
950
|
-
this.cancelInputRefocusSchedule?.();
|
|
951
|
-
this.cancelInputRefocusSchedule = scheduleUiTurn(() => {
|
|
952
|
-
this.cancelInputRefocusSchedule = null;
|
|
953
|
-
if (this.isDisposed) {
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
|
-
this.focusInputSurface(options);
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
resolveFitElement(terminalElement) {
|
|
960
|
-
const candidates = [
|
|
961
|
-
this.renderHost,
|
|
962
|
-
this.viewportHost,
|
|
963
|
-
this.container,
|
|
964
|
-
terminalElement,
|
|
965
|
-
];
|
|
966
|
-
return candidates.find(hasUsableClientSize) ?? terminalElement ?? this.renderHost ?? this.container;
|
|
967
|
-
}
|
|
968
|
-
installDemandRenderPatchBeforeOpen() {
|
|
969
|
-
const terminalAny = this.terminal;
|
|
970
|
-
if (!terminalAny || terminalAny.__floetermDemandRenderLoopPatched) {
|
|
971
|
-
return;
|
|
972
|
-
}
|
|
973
|
-
terminalAny.__floetermDemandRenderLoopPatched = true;
|
|
974
|
-
// @floegence/ghostty-web currently starts a perpetual RAF loop from open(). TerminalCore
|
|
975
|
-
// already knows every meaningful invalidation point, so keep rendering
|
|
976
|
-
// demand-driven while preserving the upstream terminal surface.
|
|
977
|
-
terminalAny.startRenderLoop = () => {
|
|
978
|
-
this.requestDemandRender(false);
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
patchDemandRenderTriggersAfterOpen() {
|
|
982
|
-
const terminalAny = this.terminal;
|
|
983
|
-
if (!terminalAny || terminalAny.__floetermDemandRenderTriggersPatched) {
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
terminalAny.__floetermDemandRenderTriggersPatched = true;
|
|
987
|
-
if (typeof terminalAny.animateScroll === 'function') {
|
|
988
|
-
const originalAnimateScroll = terminalAny.animateScroll.bind(terminalAny);
|
|
989
|
-
terminalAny.animateScroll = (...args) => {
|
|
990
|
-
const result = originalAnimateScroll(...args);
|
|
991
|
-
this.requestDemandRender(false);
|
|
992
|
-
return result;
|
|
993
|
-
};
|
|
994
|
-
}
|
|
995
|
-
const renderer = terminalAny.renderer;
|
|
996
|
-
this.installRendererRowCachePatch(renderer);
|
|
997
|
-
if (!renderer || renderer.__floetermDemandRenderPatched) {
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
renderer.__floetermDemandRenderPatched = true;
|
|
1001
|
-
const wrapRendererInvalidation = (method) => {
|
|
1002
|
-
const original = renderer[method];
|
|
1003
|
-
if (typeof original !== 'function') {
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
renderer[method] = ((...args) => {
|
|
1007
|
-
const result = original.apply(renderer, args);
|
|
1008
|
-
this.requestDemandRender(false);
|
|
1009
|
-
return result;
|
|
1010
|
-
});
|
|
1011
|
-
};
|
|
1012
|
-
wrapRendererInvalidation('setHoveredHyperlinkId');
|
|
1013
|
-
wrapRendererInvalidation('setHoveredLinkRange');
|
|
1014
|
-
wrapRendererInvalidation('setCursorStyle');
|
|
1015
|
-
wrapRendererInvalidation('setCursorBlink');
|
|
1016
|
-
wrapRendererInvalidation('setTheme');
|
|
1017
|
-
wrapRendererInvalidation('setFontSize');
|
|
1018
|
-
wrapRendererInvalidation('setFontFamily');
|
|
1019
|
-
wrapRendererInvalidation('clear');
|
|
1020
|
-
this.patchRendererCursorBlinkForDemandRendering(renderer);
|
|
1021
|
-
}
|
|
1022
|
-
patchRendererCursorBlinkForDemandRendering(renderer) {
|
|
1023
|
-
if (!renderer || renderer.__floetermDemandCursorPatched) {
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
renderer.__floetermDemandCursorPatched = true;
|
|
1027
|
-
renderer.cursorVisible = true;
|
|
1028
|
-
try {
|
|
1029
|
-
renderer.setCursorBlink?.(false);
|
|
1030
|
-
}
|
|
1031
|
-
catch (error) {
|
|
1032
|
-
this.logger.debug('[TerminalCore] Failed to disable cursor blink for demand rendering', { error });
|
|
1033
|
-
}
|
|
1034
|
-
if (this.terminal) {
|
|
1035
|
-
this.terminal.options.cursorBlink = false;
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
keepDemandCursorVisible(renderer) {
|
|
1039
|
-
if (!renderer) {
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
renderer.cursorVisible = true;
|
|
1043
|
-
}
|
|
1044
|
-
installRendererRowCachePatch(renderer) {
|
|
1045
|
-
if (!renderer || renderer.__floetermRowRenderCachePatched) {
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
if (typeof renderer.render !== 'function' || typeof renderer.renderLine !== 'function') {
|
|
1049
|
-
return;
|
|
1050
|
-
}
|
|
1051
|
-
const originalRender = renderer.render.bind(renderer);
|
|
1052
|
-
const originalRenderLine = renderer.renderLine.bind(renderer);
|
|
1053
|
-
const cache = new Map();
|
|
1054
|
-
renderer.__floetermRowRenderCachePatched = true;
|
|
1055
|
-
renderer.__floetermRowRenderCache = cache;
|
|
1056
|
-
renderer.render = (...args) => {
|
|
1057
|
-
const incomingViewportY = Number(args[2] ?? 0);
|
|
1058
|
-
const currentViewportY = Number(renderer.lastViewportY ?? 0);
|
|
1059
|
-
const viewportChanged = Number.isFinite(incomingViewportY)
|
|
1060
|
-
&& Number.isFinite(currentViewportY)
|
|
1061
|
-
&& incomingViewportY !== currentViewportY;
|
|
1062
|
-
const forceTransientExit = forceTransientRowsOnExit(renderer);
|
|
1063
|
-
const forceAll = Boolean(args[1]) || viewportChanged || forceTransientExit;
|
|
1064
|
-
renderer.__floetermRowRenderCacheForceAll = forceAll;
|
|
1065
|
-
const fabricFrame = this.beginFabricFrame(forceAll, args[0]);
|
|
1066
|
-
try {
|
|
1067
|
-
const renderArgs = [...args];
|
|
1068
|
-
renderArgs[1] = forceAll;
|
|
1069
|
-
return originalRender(...renderArgs);
|
|
1070
|
-
}
|
|
1071
|
-
finally {
|
|
1072
|
-
this.finishFabricFrame(fabricFrame);
|
|
1073
|
-
renderer.__floetermRowRenderCacheForceAll = false;
|
|
1074
|
-
}
|
|
1075
|
-
};
|
|
1076
|
-
renderer.renderLine = (cells, row, cols) => {
|
|
1077
|
-
const themedCells = this.translateCellsForTheme(cells);
|
|
1078
|
-
const fabricActive = this.writeFabricRow(renderer, row, themedCells, cols);
|
|
1079
|
-
if (canSkipNativeCanvasRowRender(renderer, row, fabricActive)) {
|
|
1080
|
-
return undefined;
|
|
1081
|
-
}
|
|
1082
|
-
const signature = buildRowSignature(renderer, themedCells, row, cols);
|
|
1083
|
-
if (canSkipCachedRowRender(renderer, row) && cache.get(row) === signature) {
|
|
1084
|
-
return undefined;
|
|
1085
|
-
}
|
|
1086
|
-
const result = originalRenderLine(themedCells, row, cols);
|
|
1087
|
-
if (hasTransientRendererState(renderer)) {
|
|
1088
|
-
cache.delete(row);
|
|
1089
|
-
return result;
|
|
1090
|
-
}
|
|
1091
|
-
cache.set(row, signature);
|
|
1092
|
-
return result;
|
|
1093
|
-
};
|
|
1094
|
-
}
|
|
1095
|
-
beginFabricFrame(forceAll, buffer) {
|
|
1096
|
-
const renderer = this.fabricView?.renderer;
|
|
1097
|
-
if (!renderer?.isActive()) {
|
|
1098
|
-
return null;
|
|
1099
|
-
}
|
|
1100
|
-
const dimensions = this.resolveFabricDimensions(buffer);
|
|
1101
|
-
if (!dimensions) {
|
|
1102
|
-
return null;
|
|
1103
|
-
}
|
|
1104
|
-
const frame = terminalFabricCoordinator.beginFrame(renderReasonFromForce(forceAll), forceAll);
|
|
1105
|
-
const theme = this.resolveFabricTheme();
|
|
1106
|
-
renderer.startFrame(frame, {
|
|
1107
|
-
cols: dimensions.cols,
|
|
1108
|
-
rows: dimensions.rows,
|
|
1109
|
-
theme: themeToFabricTheme(theme),
|
|
1110
|
-
});
|
|
1111
|
-
return frame;
|
|
1112
|
-
}
|
|
1113
|
-
resolveFabricDimensions(buffer) {
|
|
1114
|
-
const raw = buffer;
|
|
1115
|
-
const dimensions = raw?.getDimensions?.();
|
|
1116
|
-
const cols = Math.floor(Number(dimensions?.cols ?? this.terminal?.cols ?? 0));
|
|
1117
|
-
const rows = Math.floor(Number(dimensions?.rows ?? this.terminal?.rows ?? 0));
|
|
1118
|
-
if (!Number.isFinite(cols) || !Number.isFinite(rows) || cols <= 0 || rows <= 0) {
|
|
1119
|
-
return null;
|
|
1120
|
-
}
|
|
1121
|
-
return { cols, rows };
|
|
1122
|
-
}
|
|
1123
|
-
writeFabricRow(renderer, row, cells, cols) {
|
|
1124
|
-
const fabricRenderer = this.fabricView?.renderer;
|
|
1125
|
-
if (!fabricRenderer?.isActive()) {
|
|
1126
|
-
return false;
|
|
1127
|
-
}
|
|
1128
|
-
fabricRenderer.writeRow(renderer, row, cells, cols, this.resolveFabricRowRenderHints(renderer));
|
|
1129
|
-
return true;
|
|
1130
|
-
}
|
|
1131
|
-
resolveFabricRowRenderHints(renderer) {
|
|
1132
|
-
return {
|
|
1133
|
-
selection: this.resolveFabricSelectionHint(renderer),
|
|
1134
|
-
hover: {
|
|
1135
|
-
hyperlinkId: Number(renderer.hoveredHyperlinkId ?? 0),
|
|
1136
|
-
range: normalizeHoverRange(renderer.hoveredLinkRange),
|
|
1137
|
-
},
|
|
1138
|
-
};
|
|
1139
|
-
}
|
|
1140
|
-
resolveFabricSelectionHint(renderer) {
|
|
1141
|
-
const range = normalizeSelectionRange(renderer.currentSelectionCoords);
|
|
1142
|
-
if (!range) {
|
|
1143
|
-
return null;
|
|
1144
|
-
}
|
|
1145
|
-
const theme = this.resolveFabricTheme();
|
|
1146
|
-
return {
|
|
1147
|
-
...range,
|
|
1148
|
-
background: parseThemeColor(theme.selectionBackground ?? TERMINAL_SELECTION_BACKGROUND)
|
|
1149
|
-
?? parseThemeColor(TERMINAL_SELECTION_BACKGROUND),
|
|
1150
|
-
foreground: parseThemeColor(theme.selectionForeground ?? TERMINAL_SELECTION_FOREGROUND)
|
|
1151
|
-
?? parseThemeColor(TERMINAL_SELECTION_FOREGROUND),
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
syncFabricSelectionState() {
|
|
1155
|
-
const renderer = this.terminal?.renderer;
|
|
1156
|
-
const selectionManager = this.terminal?.selectionManager;
|
|
1157
|
-
if (!renderer || !selectionManager) {
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
renderer.currentSelectionCoords = selectionManager.hasSelection?.()
|
|
1161
|
-
? selectionManager.getSelectionCoords?.() ?? null
|
|
1162
|
-
: null;
|
|
1163
|
-
renderer.__floetermRowRenderCache?.clear();
|
|
1164
|
-
this.demandRenderForceAll = true;
|
|
1165
|
-
}
|
|
1166
|
-
finishFabricFrame(frame) {
|
|
1167
|
-
const fabricRenderer = this.fabricView?.renderer;
|
|
1168
|
-
if (!frame || !fabricRenderer?.isActive()) {
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
|
-
const terminalAny = this.terminal;
|
|
1172
|
-
const result = fabricRenderer.finishFrame(cursorToFabricCursor(terminalAny?.wasmTerm?.getCursor?.()));
|
|
1173
|
-
if (result.rendered) {
|
|
1174
|
-
terminalFabricCoordinator.completeFrame(frame, result.renderedRows, result.dirtyCells);
|
|
1175
|
-
this.committedFabricFrameGeneration += 1;
|
|
1176
|
-
this.lastCommittedFabricFrame = {
|
|
1177
|
-
generation: this.committedFabricFrameGeneration,
|
|
1178
|
-
result,
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
resolveFabricTheme() {
|
|
1183
|
-
if (this.searchQuery.trim().length > 0 && this.searchThemeRestore) {
|
|
1184
|
-
return this.applySearchThemeOverride(this.searchThemeRestore);
|
|
1185
|
-
}
|
|
1186
|
-
const configured = mapThemeToGhostty(this.config.theme);
|
|
1187
|
-
return Object.keys(configured).length > 0 ? configured : this.terminalThemeSource;
|
|
1188
|
-
}
|
|
1189
|
-
translateCellsForTheme(cells) {
|
|
1190
|
-
const translator = this.themeColorTranslator;
|
|
1191
|
-
if (!translator) {
|
|
1192
|
-
return cells;
|
|
1193
|
-
}
|
|
1194
|
-
let translatedCells = null;
|
|
1195
|
-
for (let index = 0; index < cells.length; index += 1) {
|
|
1196
|
-
const cell = translatedCells?.[index] ?? cells[index] ?? {};
|
|
1197
|
-
const translated = this.translateCellForTheme(cell, translator);
|
|
1198
|
-
if (translated !== cell && !translatedCells) {
|
|
1199
|
-
translatedCells = cells.slice(0, index);
|
|
1200
|
-
}
|
|
1201
|
-
translatedCells?.push(translated);
|
|
1202
|
-
}
|
|
1203
|
-
return translatedCells ?? cells;
|
|
1204
|
-
}
|
|
1205
|
-
translateCellForTheme(cell, translator) {
|
|
1206
|
-
const fgKey = cellColorKey(cell, 'fg');
|
|
1207
|
-
const bgKey = cellColorKey(cell, 'bg');
|
|
1208
|
-
const fg = fgKey ? translator.fg.get(fgKey) : undefined;
|
|
1209
|
-
const bg = bgKey ? translator.bg.get(bgKey) : undefined;
|
|
1210
|
-
if (!fg && !bg) {
|
|
1211
|
-
return cell;
|
|
1212
|
-
}
|
|
1213
|
-
return {
|
|
1214
|
-
...cell,
|
|
1215
|
-
...(fg ? { fg_r: fg.r, fg_g: fg.g, fg_b: fg.b } : {}),
|
|
1216
|
-
...(bg ? { bg_r: bg.r, bg_g: bg.g, bg_b: bg.b } : {}),
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
|
-
ensurePresentationHosts() {
|
|
1220
|
-
if (this.viewportHost && this.renderHost) {
|
|
1221
|
-
return this.renderHost;
|
|
1222
|
-
}
|
|
1223
|
-
if (this.viewportHost || this.renderHost) {
|
|
1224
|
-
throw new Error('Terminal presentation host state is inconsistent');
|
|
1225
|
-
}
|
|
1226
|
-
const viewportHost = document.createElement('div');
|
|
1227
|
-
viewportHost.style.position = 'relative';
|
|
1228
|
-
viewportHost.style.width = '100%';
|
|
1229
|
-
viewportHost.style.height = '100%';
|
|
1230
|
-
viewportHost.style.overflow = 'hidden';
|
|
1231
|
-
const renderHost = document.createElement('div');
|
|
1232
|
-
renderHost.style.position = 'absolute';
|
|
1233
|
-
renderHost.style.inset = '0';
|
|
1234
|
-
renderHost.style.overflow = 'hidden';
|
|
1235
|
-
renderHost.style.transformOrigin = 'top left';
|
|
1236
|
-
renderHost.style.willChange = 'transform';
|
|
1237
|
-
viewportHost.appendChild(renderHost);
|
|
1238
|
-
this.container.replaceChildren(viewportHost);
|
|
1239
|
-
this.viewportHost = viewportHost;
|
|
1240
|
-
this.renderHost = renderHost;
|
|
1241
|
-
return renderHost;
|
|
1242
|
-
}
|
|
1243
|
-
applyPresentationScaleStyles() {
|
|
1244
|
-
const renderHost = this.renderHost;
|
|
1245
|
-
if (!renderHost) {
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
const scale = this.pendingPresentationScale ?? this.presentationScale;
|
|
1249
|
-
if (scale <= 1.001) {
|
|
1250
|
-
renderHost.style.width = '100%';
|
|
1251
|
-
renderHost.style.height = '100%';
|
|
1252
|
-
renderHost.style.transform = 'none';
|
|
1253
|
-
return;
|
|
1254
|
-
}
|
|
1255
|
-
renderHost.style.width = `${scale * 100}%`;
|
|
1256
|
-
renderHost.style.height = `${scale * 100}%`;
|
|
1257
|
-
renderHost.style.transform = `scale(${1 / scale})`;
|
|
1258
|
-
}
|
|
1259
|
-
patchSelectionManagerClipboardBehavior() {
|
|
1260
|
-
if (this.clipboard.copyOnSelect) {
|
|
1261
|
-
return;
|
|
1262
|
-
}
|
|
1263
|
-
const selectionManager = (this.terminal
|
|
1264
|
-
?.selectionManager) ?? null;
|
|
1265
|
-
if (!selectionManager || typeof selectionManager.copyToClipboard !== 'function') {
|
|
1266
|
-
return;
|
|
1267
|
-
}
|
|
1268
|
-
selectionManager.copyToClipboard = async () => {
|
|
1269
|
-
// Disable the upstream copy-on-select side effect while keeping
|
|
1270
|
-
// the selection lifecycle intact for explicit copy commands.
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
|
-
patchSelectionManagerRenderingBehavior() {
|
|
1274
|
-
const selectionManager = (this.terminal
|
|
1275
|
-
?.selectionManager) ?? null;
|
|
1276
|
-
if (!selectionManager || selectionManager.__floetermDemandRenderPatched) {
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
selectionManager.__floetermDemandRenderPatched = true;
|
|
1280
|
-
const originalRequestRender = typeof selectionManager.requestRender === 'function'
|
|
1281
|
-
? selectionManager.requestRender.bind(selectionManager)
|
|
1282
|
-
: null;
|
|
1283
|
-
selectionManager.requestRender = () => {
|
|
1284
|
-
originalRequestRender?.();
|
|
1285
|
-
this.syncFabricSelectionState();
|
|
1286
|
-
this.requestDemandRender(false);
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
|
-
async attachFabricRenderer(seq) {
|
|
1290
|
-
if (this.fabricView || this.config.rendererType !== 'webgl' || !this.terminal) {
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
const terminalAny = this.terminal;
|
|
1294
|
-
const theme = Object.keys(this.terminalThemeSource).length > 0
|
|
1295
|
-
? this.terminalThemeSource
|
|
1296
|
-
: mapThemeToGhostty(this.config.theme);
|
|
1297
|
-
const fontFamily = typeof terminalAny.options?.fontFamily === 'string'
|
|
1298
|
-
? terminalAny.options.fontFamily
|
|
1299
|
-
: typeof this.config.fontFamily === 'string'
|
|
1300
|
-
? this.config.fontFamily
|
|
1301
|
-
: '"JetBrains Mono", monospace';
|
|
1302
|
-
const fontSize = Number(terminalAny.options?.fontSize ?? this.resolveEffectiveFontSize());
|
|
1303
|
-
const sessionId = normalizeFabricIdentifier(typeof this.config.sessionId === 'string' ? this.config.sessionId : this.fabricViewId);
|
|
1304
|
-
const fabricView = await terminalLiveFabric.attachView({
|
|
1305
|
-
sessionId,
|
|
1306
|
-
viewId: this.fabricViewId,
|
|
1307
|
-
container: this.renderHost ?? this.container,
|
|
1308
|
-
logger: this.logger,
|
|
1309
|
-
fontFamily,
|
|
1310
|
-
fontSize: Number.isFinite(fontSize) && fontSize > 0 ? fontSize : this.resolveEffectiveFontSize(),
|
|
1311
|
-
theme,
|
|
1312
|
-
getGhosttyCanvas: () => terminalAny.renderer?.getCanvas?.() ?? null,
|
|
1313
|
-
focusInputSurface: () => this.inputBridge?.focus(),
|
|
1314
|
-
forwardWheel: event => {
|
|
1315
|
-
this.container.dispatchEvent(new WheelEvent(event.type, event));
|
|
1316
|
-
},
|
|
1317
|
-
onRendererError: error => this.handleRendererFailure(error),
|
|
1318
|
-
});
|
|
1319
|
-
if (this.isDisposed || seq !== this.fabricAttachSeq || this.fabricView) {
|
|
1320
|
-
fabricView.dispose();
|
|
1321
|
-
if (this.isDisposed) {
|
|
1322
|
-
throw new Error('TerminalCore was disposed before the Beamterm renderer attached');
|
|
1323
|
-
}
|
|
1324
|
-
return;
|
|
1325
|
-
}
|
|
1326
|
-
this.fabricView = fabricView;
|
|
1327
|
-
this.fabricView.renderer.setAppearance({ theme, fontFamily, fontSize });
|
|
1328
|
-
this.performResize('force');
|
|
1329
|
-
this.syncGhosttyInputGeometryToFabric();
|
|
1330
|
-
this.forceFullRender();
|
|
1331
|
-
this.resolveFabricAttachWaiters();
|
|
1332
|
-
}
|
|
1333
|
-
waitForFabricRendererAttach() {
|
|
1334
|
-
if (this.isFabricRendererActive())
|
|
1335
|
-
return Promise.resolve();
|
|
1336
|
-
if (this.isDisposed)
|
|
1337
|
-
return Promise.reject(new Error('Cannot attach a disposed TerminalCore'));
|
|
1338
|
-
const waiter = new Promise((resolve, reject) => {
|
|
1339
|
-
this.fabricAttachWaiters.push({ resolve, reject });
|
|
1340
|
-
});
|
|
1341
|
-
if (!this.fabricAttachPromise) {
|
|
1342
|
-
this.cancelFabricAttachSchedule?.();
|
|
1343
|
-
this.cancelFabricAttachSchedule = null;
|
|
1344
|
-
const seq = ++this.fabricAttachSeq;
|
|
1345
|
-
void this.startFabricRendererAttach(seq).catch(() => undefined);
|
|
1346
|
-
}
|
|
1347
|
-
return waiter;
|
|
1348
|
-
}
|
|
1349
|
-
resolveFabricAttachWaiters() {
|
|
1350
|
-
const waiters = this.fabricAttachWaiters;
|
|
1351
|
-
this.fabricAttachWaiters = [];
|
|
1352
|
-
for (const waiter of waiters)
|
|
1353
|
-
waiter.resolve();
|
|
1354
|
-
}
|
|
1355
|
-
rejectFabricAttachWaiters(error) {
|
|
1356
|
-
const waiters = this.fabricAttachWaiters;
|
|
1357
|
-
this.fabricAttachWaiters = [];
|
|
1358
|
-
for (const waiter of waiters)
|
|
1359
|
-
waiter.reject(error);
|
|
1360
|
-
}
|
|
1361
|
-
startFabricRendererAttach(seq) {
|
|
1362
|
-
if (this.fabricView)
|
|
1363
|
-
return Promise.resolve();
|
|
1364
|
-
if (this.fabricAttachPromise)
|
|
1365
|
-
return this.fabricAttachPromise;
|
|
1366
|
-
const operation = this.attachFabricRenderer(seq)
|
|
1367
|
-
.catch(error => {
|
|
1368
|
-
if (!this.isDisposed && seq === this.fabricAttachSeq) {
|
|
1369
|
-
this.handleRendererFailure(error);
|
|
1370
|
-
}
|
|
1371
|
-
throw error;
|
|
1372
|
-
})
|
|
1373
|
-
.finally(() => {
|
|
1374
|
-
if (this.fabricAttachPromise === operation) {
|
|
1375
|
-
this.fabricAttachPromise = null;
|
|
1376
|
-
}
|
|
1377
|
-
});
|
|
1378
|
-
this.fabricAttachPromise = operation;
|
|
1379
|
-
return operation;
|
|
1380
|
-
}
|
|
1381
|
-
scheduleFabricRendererAttach() {
|
|
1382
|
-
if (this.config.rendererType !== 'webgl' || this.fabricView || this.fabricAttachPromise || !this.terminal) {
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
if (this.cancelFabricAttachSchedule !== null) {
|
|
1386
|
-
return;
|
|
1387
|
-
}
|
|
1388
|
-
const seq = ++this.fabricAttachSeq;
|
|
1389
|
-
const attach = () => {
|
|
1390
|
-
this.cancelFabricAttachSchedule = null;
|
|
1391
|
-
if (this.isDisposed || seq !== this.fabricAttachSeq || this.fabricView) {
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
void this.startFabricRendererAttach(seq).catch(() => undefined);
|
|
1395
|
-
};
|
|
1396
|
-
this.cancelFabricAttachSchedule = scheduleUiTurn(() => {
|
|
1397
|
-
if (this.isDisposed || seq !== this.fabricAttachSeq || this.fabricView) {
|
|
1398
|
-
this.cancelFabricAttachSchedule = null;
|
|
1399
|
-
return;
|
|
1400
|
-
}
|
|
1401
|
-
this.cancelFabricAttachSchedule = scheduleUiTurn(attach);
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1404
|
-
handleRendererFailure(value) {
|
|
1405
|
-
if (this.isDisposed)
|
|
1406
|
-
return;
|
|
1407
|
-
const error = value instanceof Error ? value : new Error(String(value));
|
|
1408
|
-
this.cancelDemandRender();
|
|
1409
|
-
this.rejectFabricAttachWaiters(error);
|
|
1410
|
-
this.rejectAllPresentationRequests(error);
|
|
1411
|
-
this.logger.error('[TerminalCore] Beamterm WebGL2 renderer failed', { error });
|
|
1412
|
-
this.setState(TerminalState.ERROR);
|
|
1413
|
-
this.eventHandlers.onError?.(error);
|
|
1414
|
-
}
|
|
1415
|
-
setupInputBridge(inputHost) {
|
|
1416
|
-
this.disposeInputBridge();
|
|
1417
|
-
const input = this.inputElement;
|
|
1418
|
-
if (!input) {
|
|
1419
|
-
throw new Error('Terminal input element was not mounted');
|
|
1420
|
-
}
|
|
1421
|
-
this.inputBridge = new TerminalInputBridge({
|
|
1422
|
-
inputHost,
|
|
1423
|
-
inputElement: input,
|
|
1424
|
-
onData: (data) => {
|
|
1425
|
-
this.restoreTerminalViewportForUserInput();
|
|
1426
|
-
getPerfProbe()?.onTerminalInput?.(data.length);
|
|
1427
|
-
this.eventHandlers.onData?.(data);
|
|
1428
|
-
},
|
|
1429
|
-
logger: this.logger,
|
|
1430
|
-
hasSelection: () => this.hasSelection(),
|
|
1431
|
-
copySelection: (source, clipboardData) => this.performSelectionCopy(source, clipboardData),
|
|
1432
|
-
syncInputGeometry: () => this.syncImeInputAnchor(),
|
|
1433
|
-
});
|
|
1434
|
-
this.syncImeInputAnchor();
|
|
1435
|
-
if (this.isContainerFocused()) {
|
|
1436
|
-
this.scheduleInputSurfaceRefocus();
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
async waitForDOMAndFonts() {
|
|
1440
|
-
if (document.readyState !== 'complete') {
|
|
1441
|
-
await new Promise((resolve) => {
|
|
1442
|
-
const handler = () => {
|
|
1443
|
-
if (document.readyState === 'complete') {
|
|
1444
|
-
document.removeEventListener('readystatechange', handler);
|
|
1445
|
-
resolve();
|
|
1446
|
-
}
|
|
1447
|
-
};
|
|
1448
|
-
document.addEventListener('readystatechange', handler);
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
if (typeof document.fonts !== 'undefined' && document.fonts.ready) {
|
|
1452
|
-
try {
|
|
1453
|
-
await document.fonts.ready;
|
|
1454
|
-
}
|
|
1455
|
-
catch (error) {
|
|
1456
|
-
this.logger.warn('[TerminalCore] Font loading failed, continuing', { error });
|
|
1457
|
-
await new Promise(resolve => setTimeout(resolve, 200));
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
else {
|
|
1461
|
-
await new Promise(resolve => setTimeout(resolve, 200));
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
async waitForTerminalFontReady(fontFamily, fontSize, reason) {
|
|
1465
|
-
if (typeof document === 'undefined') {
|
|
1466
|
-
return;
|
|
1467
|
-
}
|
|
1468
|
-
await this.waitForDOMAndFonts();
|
|
1469
|
-
const fonts = document.fonts;
|
|
1470
|
-
if (!fontFamily || typeof fonts === 'undefined' || typeof fonts.load !== 'function') {
|
|
1471
|
-
return;
|
|
1472
|
-
}
|
|
1473
|
-
const cssFont = `${Math.max(1, fontSize)}px ${fontFamily}`;
|
|
1474
|
-
try {
|
|
1475
|
-
await fonts.load(cssFont, 'MMMMMMMMMM');
|
|
1476
|
-
if (fonts.ready) {
|
|
1477
|
-
await fonts.ready;
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
catch (error) {
|
|
1481
|
-
this.logger.warn('[TerminalCore] Terminal font loading failed, continuing', { error, reason, fontFamily });
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
refreshFontMetricsAfterLoad(reason) {
|
|
1485
|
-
if (!this.terminal) {
|
|
1486
|
-
return Promise.resolve();
|
|
1487
|
-
}
|
|
1488
|
-
const terminalAny = this.terminal;
|
|
1489
|
-
const fontFamily = terminalAny.options?.fontFamily ?? this.config.fontFamily;
|
|
1490
|
-
const fontSize = Number(terminalAny.options?.fontSize ?? this.resolveEffectiveFontSize());
|
|
1491
|
-
const seq = ++this.fontMetricSeq;
|
|
1492
|
-
const refresh = this.applyFontMetricsAfterLoad(terminalAny, typeof fontFamily === 'string' ? fontFamily : undefined, fontSize, reason, seq);
|
|
1493
|
-
this.fontMetricRefreshPromise = refresh;
|
|
1494
|
-
return refresh;
|
|
1495
|
-
}
|
|
1496
|
-
async applyFontMetricsAfterLoad(terminal, fontFamily, fontSize, reason, seq) {
|
|
1497
|
-
await this.waitForTerminalFontReady(fontFamily, fontSize, reason);
|
|
1498
|
-
if (this.isDisposed || seq !== this.fontMetricSeq || !this.terminal) {
|
|
1499
|
-
return;
|
|
1500
|
-
}
|
|
1501
|
-
try {
|
|
1502
|
-
terminal.renderer?.remeasureFont?.();
|
|
1503
|
-
}
|
|
1504
|
-
catch (error) {
|
|
1505
|
-
this.logger.debug('[TerminalCore] Terminal font remeasure failed', { error, reason });
|
|
1506
|
-
}
|
|
1507
|
-
this.performResize('font');
|
|
1508
|
-
this.forceFullRender();
|
|
1509
|
-
this.scheduleRenderSearchOverlay();
|
|
1510
|
-
}
|
|
1511
|
-
async waitForStableFontMetrics() {
|
|
1512
|
-
while (true) {
|
|
1513
|
-
if (this.isDisposed) {
|
|
1514
|
-
throw new Error('TerminalCore was disposed before font metrics stabilized');
|
|
1515
|
-
}
|
|
1516
|
-
const seq = this.fontMetricSeq;
|
|
1517
|
-
const refresh = this.fontMetricRefreshPromise;
|
|
1518
|
-
if (refresh) {
|
|
1519
|
-
await refresh;
|
|
1520
|
-
}
|
|
1521
|
-
if (seq === this.fontMetricSeq && refresh === this.fontMetricRefreshPromise) {
|
|
1522
|
-
return seq;
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
async ensureContainerReady() {
|
|
1527
|
-
const maxRetries = 15;
|
|
1528
|
-
const retryDelay = 50;
|
|
1529
|
-
if (!document.contains(this.container)) {
|
|
1530
|
-
throw new Error('Container element is no longer in the DOM');
|
|
1531
|
-
}
|
|
1532
|
-
for (let i = 0; i < maxRetries; i += 1) {
|
|
1533
|
-
if (!document.contains(this.container)) {
|
|
1534
|
-
throw new Error('Container element removed from DOM during initialization');
|
|
1535
|
-
}
|
|
1536
|
-
const width = this.container.clientWidth;
|
|
1537
|
-
const height = this.container.clientHeight;
|
|
1538
|
-
const style = getComputedStyle(this.container);
|
|
1539
|
-
if (width > 0 && height > 0 && style.display !== 'none' && style.visibility !== 'hidden') {
|
|
1540
|
-
return;
|
|
1541
|
-
}
|
|
1542
|
-
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
1543
|
-
if (i < maxRetries - 1) {
|
|
1544
|
-
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
setupEventListeners() {
|
|
1549
|
-
if (!this.terminal) {
|
|
1550
|
-
return;
|
|
1551
|
-
}
|
|
1552
|
-
this.disposeTerminalEventListeners();
|
|
1553
|
-
let pendingTerminalKeyInput = false;
|
|
1554
|
-
let pendingTerminalKeyInputEpoch = 0;
|
|
1555
|
-
if (this.eventHandlers.onData && typeof this.terminal.onKey === 'function') {
|
|
1556
|
-
const disposable = this.terminal.onKey(() => {
|
|
1557
|
-
pendingTerminalKeyInput = true;
|
|
1558
|
-
const epoch = ++pendingTerminalKeyInputEpoch;
|
|
1559
|
-
queueMicrotask(() => {
|
|
1560
|
-
if (pendingTerminalKeyInputEpoch !== epoch)
|
|
1561
|
-
return;
|
|
1562
|
-
pendingTerminalKeyInput = false;
|
|
1563
|
-
});
|
|
1564
|
-
});
|
|
1565
|
-
this.trackTerminalEventDisposable(disposable);
|
|
1566
|
-
}
|
|
1567
|
-
if (this.eventHandlers.onData) {
|
|
1568
|
-
const disposable = this.terminal.onData((data) => {
|
|
1569
|
-
let filtered = data;
|
|
1570
|
-
if (this.isReplayingHistory || this.historyWriteDepth > 0) {
|
|
1571
|
-
filtered = filterXtermAutoResponses(data);
|
|
1572
|
-
if (filtered.length === 0) {
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
if (pendingTerminalKeyInput
|
|
1577
|
-
&& !isPinnedGhosttyOutputWriteActive(this.terminal)) {
|
|
1578
|
-
pendingTerminalKeyInput = false;
|
|
1579
|
-
pendingTerminalKeyInputEpoch += 1;
|
|
1580
|
-
this.restoreTerminalViewportForUserInput();
|
|
1581
|
-
}
|
|
1582
|
-
getPerfProbe()?.onTerminalInput?.(filtered.length);
|
|
1583
|
-
this.eventHandlers.onData?.(filtered);
|
|
1584
|
-
});
|
|
1585
|
-
this.trackTerminalEventDisposable(disposable);
|
|
1586
|
-
}
|
|
1587
|
-
if (this.eventHandlers.onResize) {
|
|
1588
|
-
const disposable = this.terminal.onResize((size) => {
|
|
1589
|
-
this.emitResize(size, { source: 'terminal' });
|
|
1590
|
-
});
|
|
1591
|
-
this.trackTerminalEventDisposable(disposable);
|
|
1592
|
-
}
|
|
1593
|
-
if (this.eventHandlers.onBell && typeof this.terminal.onBell === 'function') {
|
|
1594
|
-
const disposable = this.terminal.onBell(() => {
|
|
1595
|
-
this.eventHandlers.onBell?.();
|
|
1596
|
-
});
|
|
1597
|
-
this.trackTerminalEventDisposable(disposable);
|
|
1598
|
-
}
|
|
1599
|
-
if (this.eventHandlers.onTitleChange && typeof this.terminal.onTitleChange === 'function') {
|
|
1600
|
-
const disposable = this.terminal.onTitleChange((title) => {
|
|
1601
|
-
this.eventHandlers.onTitleChange?.(title);
|
|
1602
|
-
});
|
|
1603
|
-
this.trackTerminalEventDisposable(disposable);
|
|
1604
|
-
}
|
|
1605
|
-
if (typeof this.terminal.onScroll === 'function') {
|
|
1606
|
-
const disposable = this.terminal.onScroll(() => {
|
|
1607
|
-
this.syncImeInputAnchor();
|
|
1608
|
-
this.scrollbarOverlay?.reveal();
|
|
1609
|
-
this.scrollbarOverlay?.sync();
|
|
1610
|
-
this.requestDemandRender(false);
|
|
1611
|
-
});
|
|
1612
|
-
this.trackTerminalEventDisposable(disposable);
|
|
1613
|
-
}
|
|
1614
|
-
if (typeof this.terminal.onSelectionChange === 'function') {
|
|
1615
|
-
const disposable = this.terminal.onSelectionChange(() => {
|
|
1616
|
-
this.syncFabricSelectionState();
|
|
1617
|
-
this.requestDemandRender(false);
|
|
1618
|
-
});
|
|
1619
|
-
this.trackTerminalEventDisposable(disposable);
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
restoreTerminalViewportForUserInput() {
|
|
1623
|
-
const terminal = this.terminal;
|
|
1624
|
-
if (!terminal)
|
|
1625
|
-
return;
|
|
1626
|
-
if (isPinnedGhosttyOutputWriteActive(terminal))
|
|
1627
|
-
return;
|
|
1628
|
-
terminal.scrollToBottom?.();
|
|
1629
|
-
}
|
|
1630
|
-
setupResponsiveListeners() {
|
|
1631
|
-
const enabled = this.responsive.fitOnFocus || this.responsive.emitResizeOnFocus || this.responsive.notifyResizeOnlyWhenFocused;
|
|
1632
|
-
if (!enabled) {
|
|
1633
|
-
return;
|
|
1634
|
-
}
|
|
1635
|
-
const onFocusIn = () => {
|
|
1636
|
-
this.hasFocus = true;
|
|
1637
|
-
this.scheduleFocusResize();
|
|
1638
|
-
};
|
|
1639
|
-
const onDocumentFocusIn = () => {
|
|
1640
|
-
this.hasFocus = this.isContainerFocused();
|
|
1641
|
-
if (this.hasFocus) {
|
|
1642
|
-
this.scheduleFocusResize();
|
|
1643
|
-
}
|
|
1644
|
-
};
|
|
1645
|
-
const onFocusOut = () => {
|
|
1646
|
-
// focusout fires even when moving focus within the subtree; re-check on next frame.
|
|
1647
|
-
requestAnimationFrame(() => {
|
|
1648
|
-
if (this.isDisposed) {
|
|
1649
|
-
return;
|
|
1650
|
-
}
|
|
1651
|
-
this.hasFocus = this.isContainerFocused();
|
|
1652
|
-
});
|
|
1653
|
-
};
|
|
1654
|
-
const onPointerDown = () => {
|
|
1655
|
-
// pointerdown can happen before focus moves; treat it as "intent to interact".
|
|
1656
|
-
this.hasFocus = true;
|
|
1657
|
-
this.scheduleFocusResize();
|
|
1658
|
-
};
|
|
1659
|
-
this.container.addEventListener('focusin', onFocusIn);
|
|
1660
|
-
this.container.addEventListener('focusout', onFocusOut);
|
|
1661
|
-
this.container.addEventListener('pointerdown', onPointerDown);
|
|
1662
|
-
this.container.ownerDocument.addEventListener('focusin', onDocumentFocusIn);
|
|
1663
|
-
this.container.ownerDocument.addEventListener('focusout', onFocusOut);
|
|
1664
|
-
this.hasFocus = this.isContainerFocused();
|
|
1665
|
-
this.unbindResponsiveListeners = () => {
|
|
1666
|
-
this.container.removeEventListener('focusin', onFocusIn);
|
|
1667
|
-
this.container.removeEventListener('focusout', onFocusOut);
|
|
1668
|
-
this.container.removeEventListener('pointerdown', onPointerDown);
|
|
1669
|
-
this.container.ownerDocument.removeEventListener('focusin', onDocumentFocusIn);
|
|
1670
|
-
this.container.ownerDocument.removeEventListener('focusout', onFocusOut);
|
|
1671
|
-
};
|
|
1672
|
-
}
|
|
1673
|
-
startSizeWatching() {
|
|
1674
|
-
if (!this.container || !this.fitAddon) {
|
|
1675
|
-
return;
|
|
1676
|
-
}
|
|
1677
|
-
if (typeof ResizeObserver === 'undefined') {
|
|
1678
|
-
return;
|
|
1679
|
-
}
|
|
1680
|
-
this.resizeObserver = new ResizeObserver(() => {
|
|
1681
|
-
this.scheduleObservedResize();
|
|
1682
|
-
});
|
|
1683
|
-
const observed = new Set();
|
|
1684
|
-
const observe = (target) => {
|
|
1685
|
-
if (!target || observed.has(target)) {
|
|
1686
|
-
return;
|
|
1687
|
-
}
|
|
1688
|
-
observed.add(target);
|
|
1689
|
-
this.resizeObserver?.observe(target);
|
|
1690
|
-
};
|
|
1691
|
-
observe(this.container);
|
|
1692
|
-
observe(this.container.parentElement);
|
|
1693
|
-
observe(this.viewportHost);
|
|
1694
|
-
observe(this.renderHost);
|
|
1695
|
-
}
|
|
1696
|
-
scheduleObservedResize() {
|
|
1697
|
-
if (this.resizeRaf === null) {
|
|
1698
|
-
this.resizeRaf = requestAnimationFrame(() => {
|
|
1699
|
-
this.resizeRaf = null;
|
|
1700
|
-
this.performResize('observer');
|
|
1701
|
-
});
|
|
1702
|
-
}
|
|
1703
|
-
if (this.resizeDebounceTimer) {
|
|
1704
|
-
clearTimeout(this.resizeDebounceTimer);
|
|
1705
|
-
}
|
|
1706
|
-
this.resizeDebounceTimer = setTimeout(() => {
|
|
1707
|
-
this.resizeDebounceTimer = null;
|
|
1708
|
-
this.performResize('observer');
|
|
1709
|
-
}, 80);
|
|
1710
|
-
}
|
|
1711
|
-
performResize(reason) {
|
|
1712
|
-
if (this.isVisualRenderSuspended()) {
|
|
1713
|
-
this.markPendingResize(reason);
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
|
-
this.performResizeNow(reason);
|
|
1717
|
-
}
|
|
1718
|
-
performResizeNow(reason) {
|
|
1719
|
-
if (!this.isReady() || !this.fitAddon || !this.terminal) {
|
|
1720
|
-
return;
|
|
1721
|
-
}
|
|
1722
|
-
if (this.fixedDimensions) {
|
|
1723
|
-
this.applyFixedDimensions(reason);
|
|
1724
|
-
return;
|
|
1725
|
-
}
|
|
1726
|
-
if (this.container.clientWidth === 0 || this.container.clientHeight === 0) {
|
|
1727
|
-
return;
|
|
1728
|
-
}
|
|
1729
|
-
const startSeq = this.resizeNotifySeq;
|
|
1730
|
-
const before = this.lastNotifiedSize;
|
|
1731
|
-
try {
|
|
1732
|
-
if (this.isFabricRendererActive()) {
|
|
1733
|
-
this.resizeFabricToHost();
|
|
1734
|
-
}
|
|
1735
|
-
this.fitAddon.fit();
|
|
1736
|
-
if (this.isFabricRendererActive()) {
|
|
1737
|
-
this.syncGhosttyInputGeometryToFabric();
|
|
1738
|
-
}
|
|
1739
|
-
if (!this.isFabricRendererActive()) {
|
|
1740
|
-
this.resizeFabricToHost();
|
|
1741
|
-
}
|
|
1742
|
-
this.syncImeInputAnchor();
|
|
1743
|
-
}
|
|
1744
|
-
catch (error) {
|
|
1745
|
-
this.logger.debug('[TerminalCore] Resize failed', { error });
|
|
1746
|
-
}
|
|
1747
|
-
// Some @floegence/ghostty-web builds may not always emit onResize for programmatic fit().
|
|
1748
|
-
// Emit a deduped notification based on the actual terminal dimensions.
|
|
1749
|
-
if (this.resizeNotifySeq !== startSeq) {
|
|
1750
|
-
return;
|
|
1751
|
-
}
|
|
1752
|
-
const dims = this.getDimensions();
|
|
1753
|
-
if (reason === 'focus' && this.responsive.emitResizeOnFocus) {
|
|
1754
|
-
const force = Boolean(before) && before.cols === dims.cols && before.rows === dims.rows;
|
|
1755
|
-
this.emitResize(dims, { source: 'core', force });
|
|
1756
|
-
return;
|
|
1757
|
-
}
|
|
1758
|
-
this.emitResize(dims, { source: 'core' });
|
|
1759
|
-
}
|
|
1760
|
-
applyFixedDimensions(reason) {
|
|
1761
|
-
if (!this.terminal || !this.fixedDimensions) {
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
const dims = this.fixedDimensions;
|
|
1765
|
-
const startSeq = this.resizeNotifySeq;
|
|
1766
|
-
const before = this.lastNotifiedSize;
|
|
1767
|
-
const changed = this.terminal.cols !== dims.cols || this.terminal.rows !== dims.rows;
|
|
1768
|
-
const reportHostDimensions = this.responsive.reportHostDimensionsWithFixedGrid;
|
|
1769
|
-
const previousSuppression = this.suppressResizeNotifications;
|
|
1770
|
-
if (reportHostDimensions && changed) {
|
|
1771
|
-
this.suppressResizeNotifications = true;
|
|
1772
|
-
}
|
|
1773
|
-
try {
|
|
1774
|
-
if (changed) {
|
|
1775
|
-
this.terminal.resize(dims.cols, dims.rows);
|
|
1776
|
-
}
|
|
1777
|
-
this.resizeFabricToHost();
|
|
1778
|
-
this.syncImeInputAnchor();
|
|
1779
|
-
}
|
|
1780
|
-
catch (error) {
|
|
1781
|
-
this.logger.debug('[TerminalCore] Fixed-dimension resize failed', { error });
|
|
1782
|
-
}
|
|
1783
|
-
finally {
|
|
1784
|
-
this.suppressResizeNotifications = previousSuppression;
|
|
1785
|
-
}
|
|
1786
|
-
if (reportHostDimensions) {
|
|
1787
|
-
const hostDimensions = this.proposeFitDimensions();
|
|
1788
|
-
if (!hostDimensions) {
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
const force = reason === 'focus' && this.responsive.emitResizeOnFocus
|
|
1792
|
-
&& Boolean(before) && before.cols === hostDimensions.cols && before.rows === hostDimensions.rows;
|
|
1793
|
-
this.emitResize(hostDimensions, { source: 'core', force });
|
|
1794
|
-
return;
|
|
1795
|
-
}
|
|
1796
|
-
// Some @floegence/ghostty-web builds may not emit onResize for programmatic resize().
|
|
1797
|
-
if (this.resizeNotifySeq !== startSeq) {
|
|
1798
|
-
return;
|
|
1799
|
-
}
|
|
1800
|
-
if (reason === 'focus' && this.responsive.emitResizeOnFocus) {
|
|
1801
|
-
const force = Boolean(before) && before.cols === dims.cols && before.rows === dims.rows;
|
|
1802
|
-
this.emitResize(dims, { source: 'core', force });
|
|
1803
|
-
return;
|
|
1804
|
-
}
|
|
1805
|
-
if (changed) {
|
|
1806
|
-
this.emitResize(dims, { source: 'core' });
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
scheduleFocusResize() {
|
|
1810
|
-
if (!this.responsive.fitOnFocus && !this.responsive.emitResizeOnFocus) {
|
|
1811
|
-
return;
|
|
1812
|
-
}
|
|
1813
|
-
if (this.focusResizeRaf !== null) {
|
|
1814
|
-
return;
|
|
1815
|
-
}
|
|
1816
|
-
this.focusResizeRaf = requestAnimationFrame(() => {
|
|
1817
|
-
this.focusResizeRaf = null;
|
|
1818
|
-
this.performResize('focus');
|
|
1819
|
-
});
|
|
1820
|
-
}
|
|
1821
|
-
emitResize(size, opts) {
|
|
1822
|
-
if (!this.eventHandlers.onResize) {
|
|
1823
|
-
return;
|
|
1824
|
-
}
|
|
1825
|
-
if (this.suppressResizeNotifications) {
|
|
1826
|
-
return;
|
|
1827
|
-
}
|
|
1828
|
-
if (this.responsive.notifyResizeOnlyWhenFocused && !this.hasFocus) {
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
const cols = typeof size?.cols === 'number' ? size.cols : 0;
|
|
1832
|
-
const rows = typeof size?.rows === 'number' ? size.rows : 0;
|
|
1833
|
-
if (!opts.force && this.lastNotifiedSize && this.lastNotifiedSize.cols === cols && this.lastNotifiedSize.rows === rows) {
|
|
1834
|
-
return;
|
|
1835
|
-
}
|
|
1836
|
-
this.lastNotifiedSize = { cols, rows };
|
|
1837
|
-
this.resizeNotifySeq += 1;
|
|
1838
|
-
try {
|
|
1839
|
-
this.eventHandlers.onResize({ cols, rows });
|
|
1840
|
-
}
|
|
1841
|
-
catch (error) {
|
|
1842
|
-
this.logger.warn('[TerminalCore] onResize handler threw', { error });
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
isContainerFocused() {
|
|
1846
|
-
if (typeof document === 'undefined') {
|
|
1847
|
-
return false;
|
|
1848
|
-
}
|
|
1849
|
-
const el = document.activeElement;
|
|
1850
|
-
if (!el) {
|
|
1851
|
-
return false;
|
|
1852
|
-
}
|
|
1853
|
-
return this.container.contains(el) || this.inputBridge?.containsTarget(el) === true;
|
|
1854
|
-
}
|
|
1855
|
-
setState(newState) {
|
|
1856
|
-
if (this.state !== newState) {
|
|
1857
|
-
this.state = newState;
|
|
1858
|
-
this.eventHandlers.onStateChange?.(newState);
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
isReady() {
|
|
1862
|
-
return this.state === TerminalState.READY || this.state === TerminalState.CONNECTED;
|
|
1863
|
-
}
|
|
1864
|
-
// startHistoryReplay enables auto-response filtering for a limited duration.
|
|
1865
|
-
startHistoryReplay(duration = 2000) {
|
|
1866
|
-
if (this.replayingHistoryTimer) {
|
|
1867
|
-
clearTimeout(this.replayingHistoryTimer);
|
|
1868
|
-
}
|
|
1869
|
-
this.isReplayingHistory = true;
|
|
1870
|
-
this.replayingHistoryTimer = setTimeout(() => this.endHistoryReplay(), duration);
|
|
1871
|
-
}
|
|
1872
|
-
// endHistoryReplay disables auto-response filtering.
|
|
1873
|
-
endHistoryReplay() {
|
|
1874
|
-
if (this.replayingHistoryTimer) {
|
|
1875
|
-
clearTimeout(this.replayingHistoryTimer);
|
|
1876
|
-
this.replayingHistoryTimer = null;
|
|
1877
|
-
}
|
|
1878
|
-
this.isReplayingHistory = false;
|
|
1879
|
-
}
|
|
1880
|
-
write(data, callback) {
|
|
1881
|
-
this.writeWithRenderMode(data, callback, false);
|
|
1882
|
-
}
|
|
1883
|
-
writeFrame(data, callback) {
|
|
1884
|
-
this.writeWithRenderMode(data, callback, true);
|
|
1885
|
-
}
|
|
1886
|
-
writeWithRenderMode(data, callback, renderInCurrentFrame) {
|
|
1887
|
-
if (!this.terminal || !this.isReady()) {
|
|
1888
|
-
callback?.();
|
|
1889
|
-
return;
|
|
1890
|
-
}
|
|
1891
|
-
try {
|
|
1892
|
-
const probe = getPerfProbe();
|
|
1893
|
-
probe?.onTerminalWrite?.(typeof data === 'string' ? data.length : data.byteLength);
|
|
1894
|
-
const profileStartedAt = performance.now();
|
|
1895
|
-
let snapshotMs = 0;
|
|
1896
|
-
let parseMs = 0;
|
|
1897
|
-
const beforeSnapshotStartedAt = performance.now();
|
|
1898
|
-
const beforeWrite = this.captureRenderSnapshot();
|
|
1899
|
-
snapshotMs += performance.now() - beforeSnapshotStartedAt;
|
|
1900
|
-
const shouldForce = this.needsFullRenderOnNextWrite;
|
|
1901
|
-
const render = () => {
|
|
1902
|
-
const afterSnapshotStartedAt = performance.now();
|
|
1903
|
-
const shouldForceAfterWrite = shouldForce || this.shouldForceFullRenderAfterWrite(beforeWrite);
|
|
1904
|
-
snapshotMs += performance.now() - afterSnapshotStartedAt;
|
|
1905
|
-
if (shouldForceAfterWrite) {
|
|
1906
|
-
this.needsFullRenderOnNextWrite = false;
|
|
1907
|
-
}
|
|
1908
|
-
if (renderInCurrentFrame) {
|
|
1909
|
-
const forceCurrentFrame = shouldForceAfterWrite || this.demandRenderForceAll;
|
|
1910
|
-
this.cancelDemandRender();
|
|
1911
|
-
this.renderDemandFrame(forceCurrentFrame);
|
|
1912
|
-
}
|
|
1913
|
-
else {
|
|
1914
|
-
this.requestDemandRender(shouldForceAfterWrite);
|
|
1915
|
-
}
|
|
1916
|
-
this.scrollbarOverlay?.sync();
|
|
1917
|
-
callback?.();
|
|
1918
|
-
probe?.onTerminalWriteProfile?.({
|
|
1919
|
-
totalMs: performance.now() - profileStartedAt,
|
|
1920
|
-
parseMs,
|
|
1921
|
-
snapshotMs,
|
|
1922
|
-
});
|
|
1923
|
-
};
|
|
1924
|
-
const parseStartedAt = performance.now();
|
|
1925
|
-
if (callback || shouldForce) {
|
|
1926
|
-
this.terminal.write(data, render);
|
|
1927
|
-
parseMs += performance.now() - parseStartedAt;
|
|
1928
|
-
return;
|
|
1929
|
-
}
|
|
1930
|
-
this.terminal.write(data);
|
|
1931
|
-
parseMs += performance.now() - parseStartedAt;
|
|
1932
|
-
render();
|
|
1933
|
-
}
|
|
1934
|
-
catch (error) {
|
|
1935
|
-
this.logger.error('[TerminalCore] Write failed', { error });
|
|
1936
|
-
callback?.();
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
writeHistory(data, callback) {
|
|
1940
|
-
this.historyWriteDepth += 1;
|
|
1941
|
-
this.write(data, () => {
|
|
1942
|
-
this.historyWriteDepth = Math.max(0, this.historyWriteDepth - 1);
|
|
1943
|
-
callback?.();
|
|
1944
|
-
});
|
|
1945
|
-
}
|
|
1946
|
-
clear() {
|
|
1947
|
-
if (!this.terminal || !this.isReady()) {
|
|
1948
|
-
return;
|
|
1949
|
-
}
|
|
1950
|
-
this.terminal.clear();
|
|
1951
|
-
this.needsFullRenderOnNextWrite = true;
|
|
1952
|
-
this.forceFullRender();
|
|
1953
|
-
}
|
|
1954
|
-
serialize() {
|
|
1955
|
-
return this.captureRestorableSnapshot()?.data ?? '';
|
|
1956
|
-
}
|
|
1957
|
-
captureRestorableSnapshot(options = {}) {
|
|
1958
|
-
if (!this.terminal || !this.isReady()) {
|
|
1959
|
-
return null;
|
|
1960
|
-
}
|
|
1961
|
-
const maxBytes = Number.isFinite(options.maxBytes) && Number(options.maxBytes) > 0
|
|
1962
|
-
? Math.floor(Number(options.maxBytes))
|
|
1963
|
-
: DEFAULT_TERMINAL_SNAPSHOT_MAX_BYTES;
|
|
1964
|
-
const info = this.getTerminalInfo();
|
|
1965
|
-
if (!info) {
|
|
1966
|
-
return null;
|
|
1967
|
-
}
|
|
1968
|
-
const lines = [];
|
|
1969
|
-
for (let row = 0; row < info.bufferLength; row += 1) {
|
|
1970
|
-
lines.push(this.readBufferLine(row, { trimRight: true }));
|
|
1971
|
-
}
|
|
1972
|
-
const suffix = '\x1b[0m';
|
|
1973
|
-
let selected = lines;
|
|
1974
|
-
let data = SNAPSHOT_RESET_PREFIX + selected.join('\r\n') + suffix;
|
|
1975
|
-
let partial = false;
|
|
1976
|
-
while (selected.length > Math.max(1, info.rows) && encodedByteLength(data) > maxBytes) {
|
|
1977
|
-
partial = true;
|
|
1978
|
-
selected = selected.slice(Math.max(1, Math.floor(selected.length / 4)));
|
|
1979
|
-
data = SNAPSHOT_RESET_PREFIX + selected.join('\r\n') + suffix;
|
|
1980
|
-
}
|
|
1981
|
-
if (encodedByteLength(data) > maxBytes) {
|
|
1982
|
-
partial = true;
|
|
1983
|
-
const availableBytes = Math.max(0, maxBytes - encodedByteLength(SNAPSHOT_RESET_PREFIX + suffix));
|
|
1984
|
-
const encoded = new TextEncoder().encode(selected.join('\r\n'));
|
|
1985
|
-
const tail = encoded.slice(Math.max(0, encoded.byteLength - availableBytes));
|
|
1986
|
-
data = SNAPSHOT_RESET_PREFIX + new TextDecoder().decode(tail) + suffix;
|
|
1987
|
-
}
|
|
1988
|
-
return {
|
|
1989
|
-
version: TERMINAL_SNAPSHOT_VERSION,
|
|
1990
|
-
data,
|
|
1991
|
-
byteLength: encodedByteLength(data),
|
|
1992
|
-
partial,
|
|
1993
|
-
coveredThroughSequence: Math.max(0, Math.floor(options.coveredThroughSequence ?? 0)),
|
|
1994
|
-
cols: info.cols,
|
|
1995
|
-
rows: info.rows,
|
|
1996
|
-
createdAtMs: options.now?.() ?? Date.now(),
|
|
1997
|
-
};
|
|
1998
|
-
}
|
|
1999
|
-
async restoreSnapshot(snapshot) {
|
|
2000
|
-
if (!this.terminal || !this.isReady() || snapshot.version !== TERMINAL_SNAPSHOT_VERSION) {
|
|
2001
|
-
return false;
|
|
2002
|
-
}
|
|
2003
|
-
if (!Number.isFinite(snapshot.coveredThroughSequence) || snapshot.coveredThroughSequence < 0) {
|
|
2004
|
-
return false;
|
|
2005
|
-
}
|
|
2006
|
-
this.startHistoryReplay();
|
|
2007
|
-
this.needsFullRenderOnNextWrite = true;
|
|
2008
|
-
return new Promise(resolve => {
|
|
2009
|
-
this.write(snapshot.data, () => {
|
|
2010
|
-
this.endHistoryReplay();
|
|
2011
|
-
resolve(true);
|
|
2012
|
-
});
|
|
2013
|
-
});
|
|
2014
|
-
}
|
|
2015
|
-
/**
|
|
2016
|
-
* Restores a validated checkpoint produced by the published Ghostty engine.
|
|
2017
|
-
* The terminal must already be at the checkpoint geometry; geometry changes
|
|
2018
|
-
* remain owned by the history boundary before restore is attempted.
|
|
2019
|
-
*/
|
|
2020
|
-
async restoreAuthoritativeCheckpoint(checkpoint) {
|
|
2021
|
-
if (!this.terminal || !this.isReady()) {
|
|
2022
|
-
throw new Error('Cannot restore a checkpoint before initialization completes');
|
|
2023
|
-
}
|
|
2024
|
-
if (checkpoint.formatVersion !== 1
|
|
2025
|
-
|| checkpoint.engineId !== 'floegence-ghostty-web'
|
|
2026
|
-
|| !Number.isSafeInteger(checkpoint.coveredThroughSequence)
|
|
2027
|
-
|| checkpoint.coveredThroughSequence <= 0
|
|
2028
|
-
|| !Number.isSafeInteger(checkpoint.geometryGeneration)
|
|
2029
|
-
|| checkpoint.geometryGeneration <= 0
|
|
2030
|
-
|| !Number.isSafeInteger(checkpoint.parserEpoch)
|
|
2031
|
-
|| checkpoint.parserEpoch <= 0
|
|
2032
|
-
|| !Number.isSafeInteger(checkpoint.cols)
|
|
2033
|
-
|| checkpoint.cols <= 0
|
|
2034
|
-
|| !Number.isSafeInteger(checkpoint.rows)
|
|
2035
|
-
|| checkpoint.rows <= 0
|
|
2036
|
-
|| !(checkpoint.bytes instanceof Uint8Array)
|
|
2037
|
-
|| checkpoint.bytes.byteLength === 0
|
|
2038
|
-
|| !/^[0-9a-f]{64}$/.test(checkpoint.stateDigestSha256)) {
|
|
2039
|
-
throw new Error('Invalid authoritative terminal checkpoint metadata');
|
|
2040
|
-
}
|
|
2041
|
-
const terminalAny = this.terminal;
|
|
2042
|
-
const wasmTerm = terminalAny.wasmTerm;
|
|
2043
|
-
if (!wasmTerm
|
|
2044
|
-
|| typeof wasmTerm.validateCheckpoint !== 'function'
|
|
2045
|
-
|| typeof wasmTerm.restoreCheckpoint !== 'function'
|
|
2046
|
-
|| typeof wasmTerm.getStateDigest !== 'function') {
|
|
2047
|
-
throw new Error('Published Ghostty terminal checkpoint API is unavailable');
|
|
2048
|
-
}
|
|
2049
|
-
if (terminalAny.cols !== checkpoint.cols || terminalAny.rows !== checkpoint.rows) {
|
|
2050
|
-
throw new Error('Terminal geometry does not match authoritative checkpoint');
|
|
2051
|
-
}
|
|
2052
|
-
const bytes = checkpoint.bytes.slice();
|
|
2053
|
-
if (await sha256Hex(bytes) !== checkpoint.checksumSha256) {
|
|
2054
|
-
throw new Error('Authoritative terminal checkpoint checksum mismatch');
|
|
2055
|
-
}
|
|
2056
|
-
const coordinates = {
|
|
2057
|
-
historySequence: BigInt(checkpoint.coveredThroughSequence),
|
|
2058
|
-
geometryGeneration: BigInt(checkpoint.geometryGeneration),
|
|
2059
|
-
parserEpoch: BigInt(checkpoint.parserEpoch),
|
|
2060
|
-
};
|
|
2061
|
-
const metadata = wasmTerm.validateCheckpoint(bytes);
|
|
2062
|
-
if (metadata.formatVersion !== checkpoint.formatVersion
|
|
2063
|
-
|| metadata.cols !== checkpoint.cols
|
|
2064
|
-
|| metadata.rows !== checkpoint.rows
|
|
2065
|
-
|| metadata.historySequence !== coordinates.historySequence
|
|
2066
|
-
|| metadata.geometryGeneration !== coordinates.geometryGeneration
|
|
2067
|
-
|| metadata.parserEpoch !== coordinates.parserEpoch) {
|
|
2068
|
-
throw new Error('Authoritative terminal checkpoint coordinates do not match');
|
|
2069
|
-
}
|
|
2070
|
-
wasmTerm.restoreCheckpoint(bytes, coordinates);
|
|
2071
|
-
if (wasmTerm.getStateDigest() !== checkpoint.stateDigestSha256) {
|
|
2072
|
-
throw new Error('Authoritative terminal checkpoint state digest mismatch');
|
|
2073
|
-
}
|
|
2074
|
-
this.needsFullRenderOnNextWrite = true;
|
|
2075
|
-
this.forceFullRender();
|
|
2076
|
-
await this.forceResizeAndWaitForCommittedFrame();
|
|
2077
|
-
}
|
|
2078
|
-
getResourceEstimate() {
|
|
2079
|
-
const wasmMemoryBytes = this.ownedRuntimeMemory?.buffer.byteLength ?? 0;
|
|
2080
|
-
if (wasmMemoryBytes <= 0) {
|
|
2081
|
-
return {
|
|
2082
|
-
bufferBytes: 0,
|
|
2083
|
-
cellCount: 0,
|
|
2084
|
-
wasmMemoryBytes: 0,
|
|
2085
|
-
estimatedBytes: 0,
|
|
2086
|
-
rendererType: this.config.rendererType ?? 'canvas',
|
|
2087
|
-
};
|
|
2088
|
-
}
|
|
2089
|
-
const info = this.getTerminalInfo();
|
|
2090
|
-
const cellCount = info ? Math.max(0, info.cols * info.bufferLength) : 0;
|
|
2091
|
-
let bufferBytes = 0;
|
|
2092
|
-
if (info) {
|
|
2093
|
-
for (let row = 0; row < info.bufferLength; row += 1) {
|
|
2094
|
-
bufferBytes += encodedByteLength(this.readBufferLine(row));
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
return {
|
|
2098
|
-
bufferBytes,
|
|
2099
|
-
cellCount,
|
|
2100
|
-
wasmMemoryBytes,
|
|
2101
|
-
estimatedBytes: ESTIMATED_RENDERER_BYTES + wasmMemoryBytes,
|
|
2102
|
-
rendererType: this.config.rendererType ?? 'canvas',
|
|
2103
|
-
};
|
|
2104
|
-
}
|
|
2105
|
-
getSelectionText() {
|
|
2106
|
-
if (!this.terminal) {
|
|
2107
|
-
return '';
|
|
2108
|
-
}
|
|
2109
|
-
return this.terminal.getSelection();
|
|
2110
|
-
}
|
|
2111
|
-
hasSelection() {
|
|
2112
|
-
return this.getSelectionSnapshot().hasSelection;
|
|
2113
|
-
}
|
|
2114
|
-
async copySelection(source = 'command') {
|
|
2115
|
-
return this.performSelectionCopy(source);
|
|
2116
|
-
}
|
|
2117
|
-
getState() {
|
|
2118
|
-
return this.state;
|
|
2119
|
-
}
|
|
2120
|
-
getSelectionSnapshot() {
|
|
2121
|
-
const text = this.getSelectionText();
|
|
2122
|
-
return {
|
|
2123
|
-
text,
|
|
2124
|
-
hasSelection: text.length > 0,
|
|
2125
|
-
};
|
|
2126
|
-
}
|
|
2127
|
-
async performSelectionCopy(source, clipboardData = null) {
|
|
2128
|
-
const selection = this.getSelectionSnapshot();
|
|
2129
|
-
if (!selection.hasSelection) {
|
|
2130
|
-
return {
|
|
2131
|
-
copied: false,
|
|
2132
|
-
reason: 'empty_selection',
|
|
2133
|
-
source,
|
|
2134
|
-
};
|
|
2135
|
-
}
|
|
2136
|
-
if (clipboardData && typeof clipboardData.setData === 'function') {
|
|
2137
|
-
clipboardData.setData('text/plain', selection.text);
|
|
2138
|
-
return {
|
|
2139
|
-
copied: true,
|
|
2140
|
-
textLength: selection.text.length,
|
|
2141
|
-
source,
|
|
2142
|
-
};
|
|
2143
|
-
}
|
|
2144
|
-
if (await this.copyTextWithNavigatorClipboard(selection.text, source)) {
|
|
2145
|
-
return {
|
|
2146
|
-
copied: true,
|
|
2147
|
-
textLength: selection.text.length,
|
|
2148
|
-
source,
|
|
2149
|
-
};
|
|
2150
|
-
}
|
|
2151
|
-
if (this.copyTextWithExecCommand(selection.text)) {
|
|
2152
|
-
return {
|
|
2153
|
-
copied: true,
|
|
2154
|
-
textLength: selection.text.length,
|
|
2155
|
-
source,
|
|
2156
|
-
};
|
|
2157
|
-
}
|
|
2158
|
-
this.logger.warn('[TerminalCore] Clipboard copy unavailable', {
|
|
2159
|
-
source,
|
|
2160
|
-
textLength: selection.text.length,
|
|
2161
|
-
});
|
|
2162
|
-
return {
|
|
2163
|
-
copied: false,
|
|
2164
|
-
reason: 'clipboard_unavailable',
|
|
2165
|
-
source,
|
|
2166
|
-
};
|
|
2167
|
-
}
|
|
2168
|
-
async copyTextWithNavigatorClipboard(text, source) {
|
|
2169
|
-
const clipboard = typeof navigator !== 'undefined' ? navigator.clipboard : undefined;
|
|
2170
|
-
if (!clipboard || typeof clipboard.writeText !== 'function') {
|
|
2171
|
-
return false;
|
|
2172
|
-
}
|
|
2173
|
-
try {
|
|
2174
|
-
await clipboard.writeText(text);
|
|
2175
|
-
return true;
|
|
2176
|
-
}
|
|
2177
|
-
catch (error) {
|
|
2178
|
-
this.logger.debug('[TerminalCore] navigator.clipboard.writeText failed', {
|
|
2179
|
-
error,
|
|
2180
|
-
source,
|
|
2181
|
-
});
|
|
2182
|
-
return false;
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
copyTextWithExecCommand(text) {
|
|
2186
|
-
if (typeof document === 'undefined' || typeof document.execCommand !== 'function') {
|
|
2187
|
-
return false;
|
|
2188
|
-
}
|
|
2189
|
-
const previousActiveElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
2190
|
-
const restorableSelection = (previousActiveElement instanceof HTMLInputElement ||
|
|
2191
|
-
previousActiveElement instanceof HTMLTextAreaElement)
|
|
2192
|
-
? {
|
|
2193
|
-
element: previousActiveElement,
|
|
2194
|
-
start: previousActiveElement.selectionStart ?? 0,
|
|
2195
|
-
end: previousActiveElement.selectionEnd ?? 0,
|
|
2196
|
-
direction: previousActiveElement.selectionDirection ?? 'none',
|
|
2197
|
-
}
|
|
2198
|
-
: null;
|
|
2199
|
-
const textarea = document.createElement('textarea');
|
|
2200
|
-
textarea.value = text;
|
|
2201
|
-
textarea.setAttribute('readonly', 'true');
|
|
2202
|
-
textarea.setAttribute('aria-hidden', 'true');
|
|
2203
|
-
textarea.style.position = 'fixed';
|
|
2204
|
-
textarea.style.top = '0';
|
|
2205
|
-
textarea.style.left = '-9999px';
|
|
2206
|
-
textarea.style.opacity = '0';
|
|
2207
|
-
textarea.style.pointerEvents = 'none';
|
|
2208
|
-
document.body.appendChild(textarea);
|
|
2209
|
-
textarea.focus({ preventScroll: true });
|
|
2210
|
-
textarea.select();
|
|
2211
|
-
try {
|
|
2212
|
-
textarea.setSelectionRange(0, text.length);
|
|
2213
|
-
}
|
|
2214
|
-
catch {
|
|
2215
|
-
this.logger.debug('[TerminalCore] Failed to prepare textarea selection for clipboard fallback');
|
|
2216
|
-
}
|
|
2217
|
-
let copied = false;
|
|
2218
|
-
try {
|
|
2219
|
-
copied = document.execCommand('copy');
|
|
2220
|
-
}
|
|
2221
|
-
catch (error) {
|
|
2222
|
-
this.logger.debug('[TerminalCore] document.execCommand("copy") failed', { error });
|
|
2223
|
-
}
|
|
2224
|
-
textarea.remove();
|
|
2225
|
-
previousActiveElement?.focus({ preventScroll: true });
|
|
2226
|
-
if (restorableSelection) {
|
|
2227
|
-
try {
|
|
2228
|
-
restorableSelection.element.setSelectionRange(restorableSelection.start, restorableSelection.end, restorableSelection.direction);
|
|
2229
|
-
}
|
|
2230
|
-
catch {
|
|
2231
|
-
this.logger.debug('[TerminalCore] Failed to restore previous selection after clipboard fallback');
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
return copied;
|
|
2235
|
-
}
|
|
2236
|
-
getDimensions() {
|
|
2237
|
-
if (!this.terminal) {
|
|
2238
|
-
return { cols: 0, rows: 0 };
|
|
2239
|
-
}
|
|
2240
|
-
return { cols: this.terminal.cols, rows: this.terminal.rows };
|
|
2241
|
-
}
|
|
2242
|
-
getTerminalInfo() {
|
|
2243
|
-
if (!this.terminal) {
|
|
2244
|
-
return null;
|
|
2245
|
-
}
|
|
2246
|
-
return {
|
|
2247
|
-
rows: this.terminal.rows,
|
|
2248
|
-
cols: this.terminal.cols,
|
|
2249
|
-
bufferLength: this.terminal.buffer.active.length
|
|
2250
|
-
};
|
|
2251
|
-
}
|
|
2252
|
-
readBufferLine(row, options = {}) {
|
|
2253
|
-
if (!this.terminal) {
|
|
2254
|
-
return '';
|
|
2255
|
-
}
|
|
2256
|
-
const normalizedRow = Math.floor(Number(row));
|
|
2257
|
-
if (!Number.isFinite(normalizedRow) || normalizedRow < 0) {
|
|
2258
|
-
return '';
|
|
2259
|
-
}
|
|
2260
|
-
try {
|
|
2261
|
-
const line = this.terminal.buffer?.active?.getLine?.(normalizedRow);
|
|
2262
|
-
const text = line?.translateToString?.(options.trimRight ?? false);
|
|
2263
|
-
return typeof text === 'string' ? text : '';
|
|
2264
|
-
}
|
|
2265
|
-
catch {
|
|
2266
|
-
return '';
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
readBufferLines(startRow, endRowInclusive, options = {}) {
|
|
2270
|
-
const start = Math.floor(Number(startRow));
|
|
2271
|
-
const end = Math.floor(Number(endRowInclusive));
|
|
2272
|
-
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) {
|
|
2273
|
-
return [];
|
|
2274
|
-
}
|
|
2275
|
-
const lines = [];
|
|
2276
|
-
for (let row = Math.max(0, start); row <= end; row += 1) {
|
|
2277
|
-
const text = this.readBufferLine(row, options);
|
|
2278
|
-
if (text.length > 0) {
|
|
2279
|
-
lines.push({ row, text });
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
return lines;
|
|
2283
|
-
}
|
|
2284
|
-
getTouchScrollRuntime() {
|
|
2285
|
-
const terminal = this.terminal;
|
|
2286
|
-
if (!terminal) {
|
|
2287
|
-
return null;
|
|
2288
|
-
}
|
|
2289
|
-
return {
|
|
2290
|
-
scrollLines: amount => {
|
|
2291
|
-
const normalizedAmount = Math.trunc(Number(amount));
|
|
2292
|
-
if (!Number.isFinite(normalizedAmount) || normalizedAmount === 0) {
|
|
2293
|
-
return false;
|
|
2294
|
-
}
|
|
2295
|
-
if (typeof terminal.scrollLines !== 'function') {
|
|
2296
|
-
return false;
|
|
2297
|
-
}
|
|
2298
|
-
terminal.scrollLines(normalizedAmount);
|
|
2299
|
-
this.requestDemandRender(false);
|
|
2300
|
-
return true;
|
|
2301
|
-
},
|
|
2302
|
-
getScrollbackLength: () => {
|
|
2303
|
-
if (typeof terminal.getScrollbackLength !== 'function') {
|
|
2304
|
-
return 0;
|
|
2305
|
-
}
|
|
2306
|
-
const length = Number(terminal.getScrollbackLength());
|
|
2307
|
-
return Number.isFinite(length) && length > 0 ? length : 0;
|
|
2308
|
-
},
|
|
2309
|
-
isAlternateScreen: () => {
|
|
2310
|
-
if (typeof terminal.isAlternateScreen !== 'function') {
|
|
2311
|
-
return false;
|
|
2312
|
-
}
|
|
2313
|
-
return Boolean(terminal.isAlternateScreen());
|
|
2314
|
-
},
|
|
2315
|
-
sendAlternateScreenInput: data => {
|
|
2316
|
-
const text = String(data ?? '');
|
|
2317
|
-
if (!text) {
|
|
2318
|
-
return;
|
|
2319
|
-
}
|
|
2320
|
-
if (typeof terminal.input === 'function') {
|
|
2321
|
-
terminal.input(text, true);
|
|
2322
|
-
return;
|
|
2323
|
-
}
|
|
2324
|
-
getPerfProbe()?.onTerminalInput?.(text.length);
|
|
2325
|
-
this.eventHandlers.onData?.(text);
|
|
2326
|
-
},
|
|
2327
|
-
};
|
|
2328
|
-
}
|
|
2329
|
-
findNext(term, options) {
|
|
2330
|
-
return this.findInternal(term, options, 1);
|
|
2331
|
-
}
|
|
2332
|
-
findPrevious(term, options) {
|
|
2333
|
-
return this.findInternal(term, options, -1);
|
|
2334
|
-
}
|
|
2335
|
-
clearSearch() {
|
|
2336
|
-
this.searchQuery = '';
|
|
2337
|
-
this.searchOptionsKey = '';
|
|
2338
|
-
this.searchMatches = [];
|
|
2339
|
-
this.searchRowIndex = new Map();
|
|
2340
|
-
this.searchMatchIndex = -1;
|
|
2341
|
-
this.clearActiveSearchSelection();
|
|
2342
|
-
this.disposeSearchOverlay();
|
|
2343
|
-
this.restoreSearchTheme();
|
|
2344
|
-
this.notifySearchResults();
|
|
2345
|
-
}
|
|
2346
|
-
setSearchResultsCallback(callback) {
|
|
2347
|
-
this.searchResultsCallback = callback;
|
|
2348
|
-
this.notifySearchResults();
|
|
2349
|
-
}
|
|
2350
|
-
notifySearchResults() {
|
|
2351
|
-
if (!this.searchResultsCallback)
|
|
2352
|
-
return;
|
|
2353
|
-
const count = this.searchMatches.length;
|
|
2354
|
-
const index = this.searchMatchIndex;
|
|
2355
|
-
try {
|
|
2356
|
-
this.searchResultsCallback({ resultIndex: index, resultCount: count });
|
|
2357
|
-
}
|
|
2358
|
-
catch (error) {
|
|
2359
|
-
this.logger.debug('[TerminalCore] Search results callback failed', { error });
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
findInternal(rawTerm, options, dir) {
|
|
2363
|
-
if (!this.terminal || !this.isReady())
|
|
2364
|
-
return false;
|
|
2365
|
-
const term = (rawTerm ?? '').trim();
|
|
2366
|
-
const key = JSON.stringify({
|
|
2367
|
-
term,
|
|
2368
|
-
caseSensitive: Boolean(options?.caseSensitive),
|
|
2369
|
-
wholeWord: Boolean(options?.wholeWord),
|
|
2370
|
-
regex: Boolean(options?.regex)
|
|
2371
|
-
});
|
|
2372
|
-
if (!term) {
|
|
2373
|
-
this.clearSearch();
|
|
2374
|
-
return false;
|
|
2375
|
-
}
|
|
2376
|
-
const shouldRescan = term !== this.searchQuery || key !== this.searchOptionsKey;
|
|
2377
|
-
this.searchQuery = term;
|
|
2378
|
-
this.searchOptionsKey = key;
|
|
2379
|
-
if (shouldRescan) {
|
|
2380
|
-
this.searchMatches = this.scanTerminalMatches(term, options);
|
|
2381
|
-
this.searchRowIndex = this.buildSearchRowIndex(this.searchMatches);
|
|
2382
|
-
this.searchMatchIndex = this.searchMatches.length > 0 ? (dir === 1 ? 0 : this.searchMatches.length - 1) : -1;
|
|
2383
|
-
this.ensureSearchTheme();
|
|
2384
|
-
this.ensureSearchOverlaySubscriptions();
|
|
2385
|
-
this.scheduleRenderSearchOverlay();
|
|
2386
|
-
}
|
|
2387
|
-
else if (this.searchMatches.length > 0) {
|
|
2388
|
-
this.searchMatchIndex = dir === 1
|
|
2389
|
-
? (this.searchMatchIndex + 1) % this.searchMatches.length
|
|
2390
|
-
: (this.searchMatchIndex - 1 + this.searchMatches.length) % this.searchMatches.length;
|
|
2391
|
-
}
|
|
2392
|
-
const match = this.searchMatchIndex >= 0 ? this.searchMatches[this.searchMatchIndex] : null;
|
|
2393
|
-
if (!match) {
|
|
2394
|
-
this.clearActiveSearchSelection();
|
|
2395
|
-
this.scheduleRenderSearchOverlay();
|
|
2396
|
-
this.notifySearchResults();
|
|
2397
|
-
return false;
|
|
2398
|
-
}
|
|
2399
|
-
this.applySearchMatch(match);
|
|
2400
|
-
this.scheduleRenderSearchOverlay();
|
|
2401
|
-
this.notifySearchResults();
|
|
2402
|
-
return true;
|
|
2403
|
-
}
|
|
2404
|
-
scanTerminalMatches(term, options) {
|
|
2405
|
-
const t = this.terminal;
|
|
2406
|
-
if (!t)
|
|
2407
|
-
return [];
|
|
2408
|
-
const buffer = t.buffer?.active;
|
|
2409
|
-
if (!buffer || typeof buffer.length !== 'number')
|
|
2410
|
-
return [];
|
|
2411
|
-
const caseSensitive = Boolean(options?.caseSensitive);
|
|
2412
|
-
const query = caseSensitive ? term : term.toLowerCase();
|
|
2413
|
-
// 模糊搜索:按空白切分为多个 token(例如 "git status" -> ["git", "status"])
|
|
2414
|
-
const tokens = query.split(/\s+/).filter(Boolean);
|
|
2415
|
-
let regex = null;
|
|
2416
|
-
if (options?.regex) {
|
|
2417
|
-
try {
|
|
2418
|
-
regex = new RegExp(term, caseSensitive ? 'g' : 'gi');
|
|
2419
|
-
}
|
|
2420
|
-
catch {
|
|
2421
|
-
regex = null;
|
|
2422
|
-
}
|
|
2423
|
-
}
|
|
2424
|
-
const results = [];
|
|
2425
|
-
for (let row = 0; row < buffer.length; row += 1) {
|
|
2426
|
-
const line = buffer.getLine(row);
|
|
2427
|
-
if (!line)
|
|
2428
|
-
continue;
|
|
2429
|
-
const raw = line.translateToString(false);
|
|
2430
|
-
if (!raw)
|
|
2431
|
-
continue;
|
|
2432
|
-
const text = caseSensitive ? raw : raw.toLowerCase();
|
|
2433
|
-
if (regex) {
|
|
2434
|
-
regex.lastIndex = 0;
|
|
2435
|
-
for (;;) {
|
|
2436
|
-
const m = regex.exec(raw);
|
|
2437
|
-
if (!m || typeof m.index !== 'number')
|
|
2438
|
-
break;
|
|
2439
|
-
const len = Math.max(1, String(m[0] ?? '').length);
|
|
2440
|
-
results.push({ row, col: m.index, len });
|
|
2441
|
-
if (results.length >= TERMINAL_SEARCH_MAX_RESULTS)
|
|
2442
|
-
break;
|
|
2443
|
-
if (len <= 0)
|
|
2444
|
-
break;
|
|
2445
|
-
if (regex.lastIndex === m.index)
|
|
2446
|
-
regex.lastIndex += 1;
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
else if (tokens.length <= 1) {
|
|
2450
|
-
const needle = query;
|
|
2451
|
-
let idx = 0;
|
|
2452
|
-
while (idx <= text.length - needle.length) {
|
|
2453
|
-
const at = text.indexOf(needle, idx);
|
|
2454
|
-
if (at < 0)
|
|
2455
|
-
break;
|
|
2456
|
-
if (options?.wholeWord && !this.isWholeWordMatch(text, at, needle.length)) {
|
|
2457
|
-
idx = at + Math.max(1, needle.length);
|
|
2458
|
-
continue;
|
|
2459
|
-
}
|
|
2460
|
-
results.push({ row, col: at, len: Math.max(1, needle.length) });
|
|
2461
|
-
if (results.length >= TERMINAL_SEARCH_MAX_RESULTS)
|
|
2462
|
-
break;
|
|
2463
|
-
idx = at + Math.max(1, needle.length);
|
|
2464
|
-
}
|
|
2465
|
-
}
|
|
2466
|
-
else {
|
|
2467
|
-
// Multi-token fuzzy: tokens must appear in order; highlight the full span.
|
|
2468
|
-
let from = 0;
|
|
2469
|
-
let start = -1;
|
|
2470
|
-
let end = -1;
|
|
2471
|
-
let ok = true;
|
|
2472
|
-
for (const token of tokens) {
|
|
2473
|
-
const at = text.indexOf(token, from);
|
|
2474
|
-
if (at < 0) {
|
|
2475
|
-
ok = false;
|
|
2476
|
-
break;
|
|
2477
|
-
}
|
|
2478
|
-
if (start < 0)
|
|
2479
|
-
start = at;
|
|
2480
|
-
end = at + token.length;
|
|
2481
|
-
from = end;
|
|
2482
|
-
}
|
|
2483
|
-
if (ok && start >= 0 && end > start) {
|
|
2484
|
-
const len = end - start;
|
|
2485
|
-
if (!options?.wholeWord || this.isWholeWordMatch(text, start, len)) {
|
|
2486
|
-
results.push({ row, col: start, len });
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
if (results.length >= TERMINAL_SEARCH_MAX_RESULTS)
|
|
2491
|
-
break;
|
|
2492
|
-
}
|
|
2493
|
-
// Prefer recent output: sort from bottom to top.
|
|
2494
|
-
results.sort((a, b) => {
|
|
2495
|
-
const dr = b.row - a.row;
|
|
2496
|
-
if (dr !== 0)
|
|
2497
|
-
return dr;
|
|
2498
|
-
return a.col - b.col;
|
|
2499
|
-
});
|
|
2500
|
-
return results;
|
|
2501
|
-
}
|
|
2502
|
-
buildSearchRowIndex(matches) {
|
|
2503
|
-
const byRow = new Map();
|
|
2504
|
-
for (const m of matches) {
|
|
2505
|
-
const list = byRow.get(m.row);
|
|
2506
|
-
if (list)
|
|
2507
|
-
list.push(m);
|
|
2508
|
-
else
|
|
2509
|
-
byRow.set(m.row, [m]);
|
|
2510
|
-
}
|
|
2511
|
-
return byRow;
|
|
2512
|
-
}
|
|
2513
|
-
isWholeWordMatch(haystack, start, len) {
|
|
2514
|
-
const before = start > 0 ? haystack[start - 1] : '';
|
|
2515
|
-
const after = start + len < haystack.length ? haystack[start + len] : '';
|
|
2516
|
-
const isWordChar = (ch) => /[0-9A-Za-z_]/.test(ch);
|
|
2517
|
-
if (before && isWordChar(before))
|
|
2518
|
-
return false;
|
|
2519
|
-
if (after && isWordChar(after))
|
|
2520
|
-
return false;
|
|
2521
|
-
return true;
|
|
2522
|
-
}
|
|
2523
|
-
applySearchMatch(match) {
|
|
2524
|
-
const t = this.terminal;
|
|
2525
|
-
if (!t)
|
|
2526
|
-
return;
|
|
2527
|
-
const scrollbackLen = typeof t.getScrollbackLength === 'function' ? t.getScrollbackLength() : 0;
|
|
2528
|
-
const rows = typeof t.rows === 'number' ? t.rows : 24;
|
|
2529
|
-
// Center the match line in viewport when possible.
|
|
2530
|
-
const desiredTop = match.row >= scrollbackLen ? scrollbackLen : Math.max(0, Math.min(scrollbackLen, match.row - Math.floor(rows / 2)));
|
|
2531
|
-
const viewportY = Math.max(0, Math.min(scrollbackLen, scrollbackLen - desiredTop));
|
|
2532
|
-
if (typeof t.scrollToLine === 'function')
|
|
2533
|
-
t.scrollToLine(viewportY);
|
|
2534
|
-
const sm = t?.selectionManager;
|
|
2535
|
-
const startCol = Math.max(0, match.col);
|
|
2536
|
-
const endCol = Math.max(startCol, startCol + Math.max(1, match.len) - 1);
|
|
2537
|
-
if (sm) {
|
|
2538
|
-
this.patchSearchSelectionManager(sm);
|
|
2539
|
-
sm.__floeterm_searchAllowSingleCellSelection = true;
|
|
2540
|
-
sm.markCurrentSelectionDirty?.();
|
|
2541
|
-
sm.selectionStart = { col: startCol, absoluteRow: match.row };
|
|
2542
|
-
sm.selectionEnd = { col: endCol, absoluteRow: match.row };
|
|
2543
|
-
sm.markCurrentSelectionDirty?.();
|
|
2544
|
-
sm.selectionChangedEmitter?.fire?.();
|
|
2545
|
-
// @floegence/ghostty-web 不会因为 selection change 自动触发 render;主动刷新以保证 UI(按钮/快捷键)操作立即可见。
|
|
2546
|
-
this.forceFullRender();
|
|
2547
|
-
return;
|
|
2548
|
-
}
|
|
2549
|
-
// Fallback: select within viewport row (may fail for scrollback).
|
|
2550
|
-
const viewportRow = Math.max(0, Math.min(rows - 1, Math.floor(rows / 2)));
|
|
2551
|
-
if (typeof t.select === 'function') {
|
|
2552
|
-
t.select(startCol, viewportRow, Math.max(1, match.len));
|
|
2553
|
-
// 同上:确保当前匹配的 selection 颜色立即显示。
|
|
2554
|
-
this.forceFullRender();
|
|
2555
|
-
}
|
|
2556
|
-
}
|
|
2557
|
-
clearActiveSearchSelection() {
|
|
2558
|
-
const t = this.terminal;
|
|
2559
|
-
const sm = t?.selectionManager;
|
|
2560
|
-
if (sm) {
|
|
2561
|
-
sm.__floeterm_searchAllowSingleCellSelection = false;
|
|
2562
|
-
}
|
|
2563
|
-
if (t?.clearSelection) {
|
|
2564
|
-
try {
|
|
2565
|
-
t.clearSelection();
|
|
2566
|
-
}
|
|
2567
|
-
catch {
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
// @floegence/ghostty-web 不会因为 clearSelection 自动触发 render;主动刷新以清除残留高亮。
|
|
2571
|
-
this.forceFullRender();
|
|
2572
|
-
}
|
|
2573
|
-
patchSearchSelectionManager(sm) {
|
|
2574
|
-
if (!sm || sm.__floeterm_searchPatched)
|
|
2575
|
-
return;
|
|
2576
|
-
const original = typeof sm.hasSelection === 'function' ? sm.hasSelection.bind(sm) : null;
|
|
2577
|
-
if (!original)
|
|
2578
|
-
return;
|
|
2579
|
-
sm.__floeterm_searchPatched = true;
|
|
2580
|
-
sm.__floeterm_searchAllowSingleCellSelection = false;
|
|
2581
|
-
sm.hasSelection = () => {
|
|
2582
|
-
// Keep original mouse selection semantics, but allow a single-cell selection for search.
|
|
2583
|
-
if (sm.__floeterm_searchAllowSingleCellSelection && sm.selectionStart && sm.selectionEnd)
|
|
2584
|
-
return true;
|
|
2585
|
-
return original();
|
|
2586
|
-
};
|
|
2587
|
-
}
|
|
2588
|
-
ensureSearchTheme() {
|
|
2589
|
-
if (!this.terminal)
|
|
2590
|
-
return;
|
|
2591
|
-
if (this.searchThemeRestore)
|
|
2592
|
-
return;
|
|
2593
|
-
const terminalAny = this.terminal;
|
|
2594
|
-
const rendererTheme = terminalAny?.renderer?.theme;
|
|
2595
|
-
const base = rendererTheme && typeof rendererTheme === 'object' ? { ...rendererTheme } : mapThemeToGhostty(this.config.theme);
|
|
2596
|
-
this.searchThemeRestore = { ...base };
|
|
2597
|
-
const themed = this.applySearchThemeOverride(base);
|
|
2598
|
-
this.applyRendererTheme(terminalAny, themed);
|
|
2599
|
-
}
|
|
2600
|
-
restoreSearchTheme() {
|
|
2601
|
-
if (!this.terminal)
|
|
2602
|
-
return;
|
|
2603
|
-
if (!this.searchThemeRestore)
|
|
2604
|
-
return;
|
|
2605
|
-
const terminalAny = this.terminal;
|
|
2606
|
-
const restore = this.searchThemeRestore;
|
|
2607
|
-
this.searchThemeRestore = null;
|
|
2608
|
-
this.applyRendererTheme(terminalAny, restore);
|
|
2609
|
-
this.forceFullRender();
|
|
2610
|
-
}
|
|
2611
|
-
applySearchThemeOverride(theme) {
|
|
2612
|
-
return {
|
|
2613
|
-
...theme,
|
|
2614
|
-
selectionBackground: TERMINAL_SEARCH_ACTIVE_BACKGROUND,
|
|
2615
|
-
selectionForeground: TERMINAL_SEARCH_ACTIVE_FOREGROUND,
|
|
2616
|
-
selection: TERMINAL_SEARCH_ACTIVE_BACKGROUND
|
|
2617
|
-
};
|
|
2618
|
-
}
|
|
2619
|
-
applyRendererTheme(terminalAny, theme) {
|
|
2620
|
-
// @floegence/ghostty-web emits a warning when mutating options.theme after open().
|
|
2621
|
-
// Apply theme directly to the renderer to avoid noisy console output.
|
|
2622
|
-
if (!terminalAny.isOpen && this.terminal) {
|
|
2623
|
-
this.terminal.options.theme = theme;
|
|
2624
|
-
}
|
|
2625
|
-
terminalAny.renderer?.setTheme?.(theme);
|
|
2626
|
-
this.fabricView?.renderer.setAppearance({ theme });
|
|
2627
|
-
if (this.isVisualRenderSuspended()) {
|
|
2628
|
-
this.markPendingDemandRender(true);
|
|
2629
|
-
return;
|
|
2630
|
-
}
|
|
2631
|
-
if (terminalAny.renderer?.render && terminalAny.wasmTerm) {
|
|
2632
|
-
try {
|
|
2633
|
-
terminalAny.renderer.render(terminalAny.wasmTerm, true, terminalAny.viewportY ?? 0, terminalAny, terminalAny.scrollbarOpacity);
|
|
2634
|
-
}
|
|
2635
|
-
catch (error) {
|
|
2636
|
-
this.logger.debug('[TerminalCore] Theme render failed', { error });
|
|
2637
|
-
}
|
|
2638
|
-
}
|
|
2639
|
-
}
|
|
2640
|
-
captureRenderSnapshot() {
|
|
2641
|
-
const terminalAny = this.terminal;
|
|
2642
|
-
if (!terminalAny) {
|
|
2643
|
-
return null;
|
|
2644
|
-
}
|
|
2645
|
-
const cursor = terminalAny.wasmTerm?.getCursor?.();
|
|
2646
|
-
const cursorY = Number(cursor?.y);
|
|
2647
|
-
const rows = Number(terminalAny.rows);
|
|
2648
|
-
const scrollbackLength = (() => {
|
|
2649
|
-
if (typeof terminalAny.getScrollbackLength === 'function') {
|
|
2650
|
-
return Number(terminalAny.getScrollbackLength.call(terminalAny));
|
|
2651
|
-
}
|
|
2652
|
-
if (typeof terminalAny.wasmTerm?.getScrollbackLength === 'function') {
|
|
2653
|
-
return Number(terminalAny.wasmTerm.getScrollbackLength.call(terminalAny.wasmTerm));
|
|
2654
|
-
}
|
|
2655
|
-
return Number.NaN;
|
|
2656
|
-
})();
|
|
2657
|
-
const viewportY = Number(terminalAny.viewportY ?? 0);
|
|
2658
|
-
return {
|
|
2659
|
-
cursorY: Number.isFinite(cursorY) ? cursorY : null,
|
|
2660
|
-
rows: Number.isFinite(rows) ? rows : null,
|
|
2661
|
-
scrollbackLength: Number.isFinite(scrollbackLength) ? scrollbackLength : null,
|
|
2662
|
-
viewportY: Number.isFinite(viewportY) ? viewportY : null,
|
|
2663
|
-
};
|
|
2664
|
-
}
|
|
2665
|
-
shouldForceFullRenderAfterWrite(before) {
|
|
2666
|
-
if (!before) {
|
|
2667
|
-
return false;
|
|
2668
|
-
}
|
|
2669
|
-
const after = this.captureRenderSnapshot();
|
|
2670
|
-
if (!after) {
|
|
2671
|
-
return false;
|
|
2672
|
-
}
|
|
2673
|
-
if (before.viewportY !== null
|
|
2674
|
-
&& after.viewportY !== null
|
|
2675
|
-
&& before.viewportY !== after.viewportY) {
|
|
2676
|
-
return true;
|
|
2677
|
-
}
|
|
2678
|
-
if (before.scrollbackLength !== null
|
|
2679
|
-
&& after.scrollbackLength !== null
|
|
2680
|
-
&& before.scrollbackLength !== after.scrollbackLength) {
|
|
2681
|
-
return true;
|
|
2682
|
-
}
|
|
2683
|
-
return Boolean(before.cursorY !== null
|
|
2684
|
-
&& after.cursorY !== null
|
|
2685
|
-
&& before.rows !== null
|
|
2686
|
-
&& before.cursorY >= before.rows - 1
|
|
2687
|
-
&& after.cursorY < before.cursorY);
|
|
2688
|
-
}
|
|
2689
|
-
ensureSearchOverlaySubscriptions() {
|
|
2690
|
-
const t = this.terminal;
|
|
2691
|
-
if (!t)
|
|
2692
|
-
return;
|
|
2693
|
-
if (this.searchOverlayUnsubs.length > 0)
|
|
2694
|
-
return;
|
|
2695
|
-
// Keep overlay synced with scroll/resize.
|
|
2696
|
-
const onScroll = typeof t.onScroll === 'function' ? t.onScroll(() => this.scheduleRenderSearchOverlay()) : null;
|
|
2697
|
-
if (onScroll?.dispose)
|
|
2698
|
-
this.searchOverlayUnsubs.push(() => onScroll.dispose());
|
|
2699
|
-
const onResize = typeof t.onResize === 'function' ? t.onResize(() => this.scheduleRenderSearchOverlay()) : null;
|
|
2700
|
-
if (onResize?.dispose)
|
|
2701
|
-
this.searchOverlayUnsubs.push(() => onResize.dispose());
|
|
2702
|
-
}
|
|
2703
|
-
disposeSearchOverlaySubscriptions() {
|
|
2704
|
-
for (const unsub of this.searchOverlayUnsubs)
|
|
2705
|
-
unsub();
|
|
2706
|
-
this.searchOverlayUnsubs = [];
|
|
2707
|
-
}
|
|
2708
|
-
disposeSearchOverlay() {
|
|
2709
|
-
if (this.searchOverlayRaf !== null) {
|
|
2710
|
-
cancelAnimationFrame(this.searchOverlayRaf);
|
|
2711
|
-
this.searchOverlayRaf = null;
|
|
2712
|
-
}
|
|
2713
|
-
this.disposeSearchOverlaySubscriptions();
|
|
2714
|
-
if (this.searchOverlay?.canvas?.parentElement) {
|
|
2715
|
-
this.searchOverlay.canvas.parentElement.removeChild(this.searchOverlay.canvas);
|
|
2716
|
-
}
|
|
2717
|
-
this.searchOverlay = null;
|
|
2718
|
-
}
|
|
2719
|
-
createCanvasOverlay(zIndex) {
|
|
2720
|
-
const t = this.terminal;
|
|
2721
|
-
if (!t)
|
|
2722
|
-
return null;
|
|
2723
|
-
const renderer = t?.renderer;
|
|
2724
|
-
const termCanvas = renderer?.getCanvas?.();
|
|
2725
|
-
if (!termCanvas)
|
|
2726
|
-
return null;
|
|
2727
|
-
try {
|
|
2728
|
-
const style = getComputedStyle(this.container);
|
|
2729
|
-
if (style.position === 'static')
|
|
2730
|
-
this.container.style.position = 'relative';
|
|
2731
|
-
}
|
|
2732
|
-
catch {
|
|
2733
|
-
}
|
|
2734
|
-
const overlay = document.createElement('canvas');
|
|
2735
|
-
overlay.style.position = 'absolute';
|
|
2736
|
-
overlay.style.top = '0';
|
|
2737
|
-
overlay.style.left = '0';
|
|
2738
|
-
overlay.style.pointerEvents = 'none';
|
|
2739
|
-
overlay.style.zIndex = zIndex;
|
|
2740
|
-
overlay.style.mixBlendMode = 'normal';
|
|
2741
|
-
const ctx = overlay.getContext('2d');
|
|
2742
|
-
if (!ctx)
|
|
2743
|
-
return null;
|
|
2744
|
-
this.container.appendChild(overlay);
|
|
2745
|
-
const state = {
|
|
2746
|
-
canvas: overlay,
|
|
2747
|
-
ctx,
|
|
2748
|
-
dpr: 1,
|
|
2749
|
-
cssWidth: 0,
|
|
2750
|
-
cssHeight: 0,
|
|
2751
|
-
termCanvas,
|
|
2752
|
-
};
|
|
2753
|
-
syncCanvasOverlaySize(state, termCanvas, this.container);
|
|
2754
|
-
return state;
|
|
2755
|
-
}
|
|
2756
|
-
ensureSearchOverlay() {
|
|
2757
|
-
const t = this.terminal;
|
|
2758
|
-
if (!t)
|
|
2759
|
-
return null;
|
|
2760
|
-
const renderer = t?.renderer;
|
|
2761
|
-
const termCanvas = renderer?.getCanvas?.();
|
|
2762
|
-
if (!termCanvas)
|
|
2763
|
-
return null;
|
|
2764
|
-
// Ensure we have a positioning context.
|
|
2765
|
-
try {
|
|
2766
|
-
const style = getComputedStyle(this.container);
|
|
2767
|
-
if (style.position === 'static')
|
|
2768
|
-
this.container.style.position = 'relative';
|
|
2769
|
-
}
|
|
2770
|
-
catch {
|
|
2771
|
-
}
|
|
2772
|
-
if (this.searchOverlay && this.searchOverlay.termCanvas !== termCanvas) {
|
|
2773
|
-
this.disposeSearchOverlay();
|
|
2774
|
-
}
|
|
2775
|
-
if (!this.searchOverlay) {
|
|
2776
|
-
this.searchOverlay = this.createCanvasOverlay('5');
|
|
2777
|
-
}
|
|
2778
|
-
if (this.searchOverlay) {
|
|
2779
|
-
syncCanvasOverlaySize(this.searchOverlay, termCanvas, this.container);
|
|
2780
|
-
}
|
|
2781
|
-
return this.searchOverlay;
|
|
2782
|
-
}
|
|
2783
|
-
scheduleRenderSearchOverlay() {
|
|
2784
|
-
if (this.isVisualRenderSuspended()) {
|
|
2785
|
-
this.visualRenderState.pendingSearchOverlayRender = true;
|
|
2786
|
-
return;
|
|
2787
|
-
}
|
|
2788
|
-
if (this.searchOverlayRaf !== null)
|
|
2789
|
-
return;
|
|
2790
|
-
this.searchOverlayRaf = requestAnimationFrame(() => {
|
|
2791
|
-
this.searchOverlayRaf = null;
|
|
2792
|
-
this.renderSearchOverlay();
|
|
2793
|
-
});
|
|
2794
|
-
}
|
|
2795
|
-
renderSearchOverlay() {
|
|
2796
|
-
const t = this.terminal;
|
|
2797
|
-
const queryActive = this.searchQuery.trim().length > 0;
|
|
2798
|
-
const rowIndex = this.searchRowIndex;
|
|
2799
|
-
if (!queryActive || rowIndex.size === 0 || !t) {
|
|
2800
|
-
this.searchOverlay?.ctx.clearRect(0, 0, this.searchOverlay.cssWidth, this.searchOverlay.cssHeight);
|
|
2801
|
-
return;
|
|
2802
|
-
}
|
|
2803
|
-
const overlay = this.ensureSearchOverlay();
|
|
2804
|
-
if (!overlay)
|
|
2805
|
-
return;
|
|
2806
|
-
const renderer = t?.renderer;
|
|
2807
|
-
const charW = typeof renderer?.charWidth === 'number' ? renderer.charWidth : 0;
|
|
2808
|
-
const charH = typeof renderer?.charHeight === 'number' ? renderer.charHeight : 0;
|
|
2809
|
-
const cols = typeof t.cols === 'number' ? t.cols : 0;
|
|
2810
|
-
const rows = typeof t.rows === 'number' ? t.rows : 0;
|
|
2811
|
-
const scrollbackLen = typeof t.getScrollbackLength === 'function' ? t.getScrollbackLength() : 0;
|
|
2812
|
-
const viewportY = Math.max(0, Math.floor(typeof t.getViewportY === 'function' ? t.getViewportY() : t.viewportY ?? 0));
|
|
2813
|
-
const viewportTop = Math.max(0, scrollbackLen - viewportY);
|
|
2814
|
-
if (charW <= 0 || charH <= 0 || cols <= 0 || rows <= 0) {
|
|
2815
|
-
overlay.ctx.clearRect(0, 0, overlay.cssWidth, overlay.cssHeight);
|
|
2816
|
-
return;
|
|
2817
|
-
}
|
|
2818
|
-
overlay.ctx.clearRect(0, 0, overlay.cssWidth, overlay.cssHeight);
|
|
2819
|
-
overlay.ctx.fillStyle = TERMINAL_SEARCH_MATCH_BACKGROUND;
|
|
2820
|
-
for (let y = 0; y < rows; y += 1) {
|
|
2821
|
-
const absRow = viewportTop + y;
|
|
2822
|
-
const list = rowIndex.get(absRow);
|
|
2823
|
-
if (!list || list.length === 0)
|
|
2824
|
-
continue;
|
|
2825
|
-
for (const m of list) {
|
|
2826
|
-
const col = Math.max(0, Math.min(cols - 1, m.col));
|
|
2827
|
-
const maxLen = Math.max(0, cols - col);
|
|
2828
|
-
const len = Math.max(0, Math.min(maxLen, m.len));
|
|
2829
|
-
if (len <= 0)
|
|
2830
|
-
continue;
|
|
2831
|
-
overlay.ctx.fillRect(col * charW, y * charH, len * charW, charH);
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
focus(options) {
|
|
2836
|
-
this.terminal?.focus();
|
|
2837
|
-
this.focusInputSurface(options);
|
|
2838
|
-
this.scheduleInputSurfaceRefocus(options);
|
|
2839
|
-
}
|
|
2840
|
-
setConnected(isConnected) {
|
|
2841
|
-
this.setState(isConnected ? TerminalState.CONNECTED : TerminalState.READY);
|
|
2842
|
-
if (isConnected) {
|
|
2843
|
-
this.scheduleFabricRendererAttach();
|
|
2844
|
-
}
|
|
2845
|
-
}
|
|
2846
|
-
forceResize() {
|
|
2847
|
-
this.flushPendingPresentationScale();
|
|
2848
|
-
this.performResize('force');
|
|
2849
|
-
this.requestDemandRender(true);
|
|
2850
|
-
}
|
|
2851
|
-
/**
|
|
2852
|
-
* Measures the host capacity without changing the currently rendered PTY grid.
|
|
2853
|
-
* This is used by active shared-geometry views before an explicit transport resize.
|
|
2854
|
-
*/
|
|
2855
|
-
measureHostDimensions() {
|
|
2856
|
-
if (!this.isReady() || !this.terminal) {
|
|
2857
|
-
return undefined;
|
|
2858
|
-
}
|
|
2859
|
-
return this.proposeFitDimensions();
|
|
2860
|
-
}
|
|
2861
|
-
async forceResizeAndWaitForPresentation() {
|
|
2862
|
-
while (true) {
|
|
2863
|
-
const fontMetricSeq = await this.waitForStableFontMetrics();
|
|
2864
|
-
await this.requestPresentationFrame();
|
|
2865
|
-
await this.waitForStableFontMetrics();
|
|
2866
|
-
if (fontMetricSeq === this.fontMetricSeq) {
|
|
2867
|
-
return;
|
|
2868
|
-
}
|
|
2869
|
-
}
|
|
2870
|
-
}
|
|
2871
|
-
/**
|
|
2872
|
-
* Forces a resize and waits until the terminal renderer has committed a full frame.
|
|
2873
|
-
* Unlike {@link forceResizeAndWaitForPresentation}, this does not wait for the
|
|
2874
|
-
* following browser paint frame.
|
|
2875
|
-
*/
|
|
2876
|
-
async forceResizeAndWaitForCommittedFrame() {
|
|
2877
|
-
if (this.isDisposed) {
|
|
2878
|
-
throw new Error('Cannot present a disposed TerminalCore');
|
|
2879
|
-
}
|
|
2880
|
-
if (!this.terminal || !this.isReady()) {
|
|
2881
|
-
throw new Error('Cannot present a TerminalCore before initialization completes');
|
|
2882
|
-
}
|
|
2883
|
-
await new Promise((resolve, reject) => {
|
|
2884
|
-
const request = {
|
|
2885
|
-
baselineGeneration: this.committedFabricFrameGeneration,
|
|
2886
|
-
frameHandle: null,
|
|
2887
|
-
resolve: () => {
|
|
2888
|
-
this.committedFrameRequests.delete(request);
|
|
2889
|
-
resolve();
|
|
2890
|
-
},
|
|
2891
|
-
reject: error => {
|
|
2892
|
-
this.committedFrameRequests.delete(request);
|
|
2893
|
-
reject(error);
|
|
2894
|
-
},
|
|
2895
|
-
};
|
|
2896
|
-
this.committedFrameRequests.add(request);
|
|
2897
|
-
const start = () => {
|
|
2898
|
-
if (this.isDisposed) {
|
|
2899
|
-
this.rejectCommittedFrameRequest(request, new Error('TerminalCore was disposed before the committed frame completed'));
|
|
2900
|
-
return;
|
|
2901
|
-
}
|
|
2902
|
-
if (this.config.rendererType === 'webgl' && !this.isFabricRendererActive()) {
|
|
2903
|
-
void this.waitForFabricRendererAttach().then(() => {
|
|
2904
|
-
if (this.isDisposed) {
|
|
2905
|
-
this.rejectCommittedFrameRequest(request, new Error('TerminalCore was disposed before the committed frame completed'));
|
|
2906
|
-
}
|
|
2907
|
-
else {
|
|
2908
|
-
this.forceCommittedFrame(request);
|
|
2909
|
-
this.scheduleCommittedFrameCheck(request);
|
|
2910
|
-
}
|
|
2911
|
-
}).catch(error => this.rejectCommittedFrameRequest(request, error instanceof Error ? error : new Error(String(error))));
|
|
2912
|
-
return;
|
|
2913
|
-
}
|
|
2914
|
-
this.forceCommittedFrame(request);
|
|
2915
|
-
this.scheduleCommittedFrameCheck(request);
|
|
2916
|
-
};
|
|
2917
|
-
start();
|
|
2918
|
-
});
|
|
2919
|
-
}
|
|
2920
|
-
forceCommittedFrame(request) {
|
|
2921
|
-
this.forceResize();
|
|
2922
|
-
if (!this.committedFrameRequests.has(request)
|
|
2923
|
-
|| this.config.rendererType !== 'webgl'
|
|
2924
|
-
|| !this.isFabricRendererActive()
|
|
2925
|
-
|| this.isVisualRenderSuspended()) {
|
|
2926
|
-
return;
|
|
2927
|
-
}
|
|
2928
|
-
// A committed-frame fence is an explicit interactive presentation request.
|
|
2929
|
-
// Render it in this task so shared background RAF work cannot add a frame of
|
|
2930
|
-
// queueing latency; the full frame subsumes this core's pending demand render.
|
|
2931
|
-
terminalRenderScheduler.cancel(this.renderTask);
|
|
2932
|
-
this.demandRenderForceAll = false;
|
|
2933
|
-
this.renderDemandFrame(true);
|
|
2934
|
-
}
|
|
2935
|
-
requestPresentationFrame() {
|
|
2936
|
-
if (this.isDisposed) {
|
|
2937
|
-
return Promise.reject(new Error('Cannot present a disposed TerminalCore'));
|
|
2938
|
-
}
|
|
2939
|
-
if (!this.terminal || !this.isReady()) {
|
|
2940
|
-
return Promise.reject(new Error('Cannot present a TerminalCore before initialization completes'));
|
|
2941
|
-
}
|
|
2942
|
-
if (this.config.rendererType === 'webgl' && !this.isFabricRendererActive()) {
|
|
2943
|
-
return this.waitForFabricRendererAttach().then(() => {
|
|
2944
|
-
if (this.isDisposed)
|
|
2945
|
-
throw new Error('TerminalCore was disposed before presentation started');
|
|
2946
|
-
this.forceResize();
|
|
2947
|
-
return this.createPresentationRequest();
|
|
2948
|
-
});
|
|
2949
|
-
}
|
|
2950
|
-
return this.createPresentationRequest();
|
|
2951
|
-
}
|
|
2952
|
-
createPresentationRequest() {
|
|
2953
|
-
if (this.pendingPresentationRequest) {
|
|
2954
|
-
this.forceResize();
|
|
2955
|
-
return this.pendingPresentationRequest.promise;
|
|
2956
|
-
}
|
|
2957
|
-
let resolve;
|
|
2958
|
-
let reject;
|
|
2959
|
-
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
2960
|
-
resolve = resolvePromise;
|
|
2961
|
-
reject = rejectPromise;
|
|
2962
|
-
});
|
|
2963
|
-
const request = { promise, resolve, reject, frameHandle: null };
|
|
2964
|
-
this.pendingPresentationRequest = request;
|
|
2965
|
-
this.presentationRequests.add(request);
|
|
2966
|
-
this.forceResize();
|
|
2967
|
-
return promise;
|
|
2968
|
-
}
|
|
2969
|
-
setFixedDimensions(dimensions, options) {
|
|
2970
|
-
const next = normalizeTerminalDimensions(dimensions);
|
|
2971
|
-
if (sameTerminalDimensions(this.fixedDimensions, next) || (!this.fixedDimensions && !next)) {
|
|
2972
|
-
return;
|
|
2973
|
-
}
|
|
2974
|
-
this.fixedDimensions = next;
|
|
2975
|
-
this.config = { ...this.config, fixedDimensions: next };
|
|
2976
|
-
if (!this.terminal) {
|
|
2977
|
-
return;
|
|
2978
|
-
}
|
|
2979
|
-
const previousSuppression = this.suppressResizeNotifications;
|
|
2980
|
-
if (options?.notifyResize === false) {
|
|
2981
|
-
this.suppressResizeNotifications = true;
|
|
2982
|
-
}
|
|
2983
|
-
try {
|
|
2984
|
-
this.forceResize();
|
|
2985
|
-
}
|
|
2986
|
-
finally {
|
|
2987
|
-
this.suppressResizeNotifications = previousSuppression;
|
|
2988
|
-
}
|
|
2989
|
-
}
|
|
2990
|
-
setAppearance(appearance) {
|
|
2991
|
-
if (!appearance || typeof appearance !== 'object') {
|
|
2992
|
-
return;
|
|
2993
|
-
}
|
|
2994
|
-
if (appearance.theme) {
|
|
2995
|
-
this.setTheme(appearance.theme);
|
|
2996
|
-
}
|
|
2997
|
-
if (typeof appearance.fontSize === 'number') {
|
|
2998
|
-
this.setFontSize(appearance.fontSize);
|
|
2999
|
-
}
|
|
3000
|
-
if (typeof appearance.fontFamily === 'string') {
|
|
3001
|
-
this.setFontFamily(appearance.fontFamily);
|
|
3002
|
-
}
|
|
3003
|
-
if (typeof appearance.presentationScale === 'number') {
|
|
3004
|
-
this.setPresentationScale(appearance.presentationScale);
|
|
3005
|
-
}
|
|
3006
|
-
}
|
|
3007
|
-
setScrollbarOptions(options) {
|
|
3008
|
-
normalizeTerminalScrollbarOptions(options);
|
|
3009
|
-
const next = normalizeTerminalScrollbarOptions({ ...this.scrollbarOptions, ...options });
|
|
3010
|
-
this.scrollbarOptions = next;
|
|
3011
|
-
this.config = { ...this.config, scrollbar: next };
|
|
3012
|
-
this.scrollbarOverlay?.setOptions(next);
|
|
3013
|
-
}
|
|
3014
|
-
setTheme(theme) {
|
|
3015
|
-
const mapped = mapThemeToGhostty(theme);
|
|
3016
|
-
// Persist latest theme so a future re-initialization can reuse it.
|
|
3017
|
-
this.config = { ...this.config, theme: mapped };
|
|
3018
|
-
this.themeColorTranslator = buildThemeColorTranslator(this.terminalThemeSource, mapped);
|
|
3019
|
-
this.scrollbarOverlay?.setBackgroundColor(mapped.background);
|
|
3020
|
-
if (!this.terminal) {
|
|
3021
|
-
return;
|
|
3022
|
-
}
|
|
3023
|
-
const terminalAny = this.terminal;
|
|
3024
|
-
// If search is active, apply the theme but keep the search selection colors.
|
|
3025
|
-
if (this.searchQuery.trim().length > 0) {
|
|
3026
|
-
this.searchThemeRestore = { ...mapped };
|
|
3027
|
-
this.applyRendererTheme(terminalAny, this.applySearchThemeOverride(mapped));
|
|
3028
|
-
this.scheduleRenderSearchOverlay();
|
|
3029
|
-
return;
|
|
3030
|
-
}
|
|
3031
|
-
this.applyRendererTheme(terminalAny, mapped);
|
|
3032
|
-
}
|
|
3033
|
-
setFontSize(size) {
|
|
3034
|
-
this.logicalFontSize = TerminalCore.normalizeFontSize(size);
|
|
3035
|
-
this.config = { ...this.config, fontSize: this.logicalFontSize };
|
|
3036
|
-
if (!this.terminal) {
|
|
3037
|
-
return;
|
|
3038
|
-
}
|
|
3039
|
-
this.terminal.options.fontSize = this.resolveEffectiveFontSize();
|
|
3040
|
-
this.fabricView?.renderer.setAppearance({ fontSize: this.resolveEffectiveFontSize() });
|
|
3041
|
-
this.performResize('font');
|
|
3042
|
-
void this.refreshFontMetricsAfterLoad('font_size');
|
|
3043
|
-
}
|
|
3044
|
-
setPresentationScale(scale) {
|
|
3045
|
-
const nextScale = TerminalCore.normalizePresentationScale(scale);
|
|
3046
|
-
const currentTargetScale = this.pendingPresentationScale ?? this.presentationScale;
|
|
3047
|
-
if (samePresentationScale(nextScale, currentTargetScale)) {
|
|
3048
|
-
return;
|
|
3049
|
-
}
|
|
3050
|
-
this.pendingPresentationScale = nextScale;
|
|
3051
|
-
this.config = { ...this.config, presentationScale: nextScale };
|
|
3052
|
-
this.applyPresentationScaleStyles();
|
|
3053
|
-
if (typeof window === 'undefined') {
|
|
3054
|
-
this.commitPresentationScale(nextScale);
|
|
3055
|
-
return;
|
|
3056
|
-
}
|
|
3057
|
-
if (this.presentationScaleRaf !== null) {
|
|
3058
|
-
return;
|
|
3059
|
-
}
|
|
3060
|
-
this.presentationScaleRaf = window.requestAnimationFrame(() => {
|
|
3061
|
-
this.presentationScaleRaf = null;
|
|
3062
|
-
const targetScale = this.pendingPresentationScale ?? this.presentationScale;
|
|
3063
|
-
this.pendingPresentationScale = null;
|
|
3064
|
-
this.commitPresentationScale(targetScale);
|
|
3065
|
-
});
|
|
3066
|
-
}
|
|
3067
|
-
setFontFamily(family) {
|
|
3068
|
-
const nextFamily = String(family ?? '').trim();
|
|
3069
|
-
if (!nextFamily) {
|
|
3070
|
-
return;
|
|
3071
|
-
}
|
|
3072
|
-
this.config = { ...this.config, fontFamily: nextFamily };
|
|
3073
|
-
if (!this.terminal) {
|
|
3074
|
-
return;
|
|
3075
|
-
}
|
|
3076
|
-
this.terminal.options.fontFamily = nextFamily;
|
|
3077
|
-
this.fabricView?.renderer.setAppearance({
|
|
3078
|
-
fontFamily: nextFamily,
|
|
3079
|
-
fontSize: this.resolveEffectiveFontSize(),
|
|
3080
|
-
});
|
|
3081
|
-
this.performResize('font');
|
|
3082
|
-
this.forceFullRender();
|
|
3083
|
-
void this.refreshFontMetricsAfterLoad('font_family');
|
|
3084
|
-
}
|
|
3085
|
-
beginVisualSuspend(options = {}) {
|
|
3086
|
-
const id = this.visualRenderState.nextSuspendId;
|
|
3087
|
-
this.visualRenderState.nextSuspendId += 1;
|
|
3088
|
-
const reason = options.reason ?? 'external';
|
|
3089
|
-
let disposed = false;
|
|
3090
|
-
this.visualRenderState.suspendDepth += 1;
|
|
3091
|
-
this.visualRenderState.activeReasons.set(id, reason);
|
|
3092
|
-
// Legacy compatibility: keep the handle lifecycle observable, but never
|
|
3093
|
-
// freeze visible terminal rendering in the live fabric path.
|
|
3094
|
-
this.visualRenderState.suspendDepth = Math.max(0, this.visualRenderState.suspendDepth - 1);
|
|
3095
|
-
return {
|
|
3096
|
-
id,
|
|
3097
|
-
reason,
|
|
3098
|
-
dispose: () => {
|
|
3099
|
-
if (disposed) {
|
|
3100
|
-
return;
|
|
3101
|
-
}
|
|
3102
|
-
disposed = true;
|
|
3103
|
-
this.endVisualSuspend(id);
|
|
3104
|
-
},
|
|
3105
|
-
};
|
|
3106
|
-
}
|
|
3107
|
-
registerLinkProvider(provider) {
|
|
3108
|
-
if (!provider || this.registeredLinkProviders.has(provider)) {
|
|
3109
|
-
return;
|
|
3110
|
-
}
|
|
3111
|
-
this.registeredLinkProviders.add(provider);
|
|
3112
|
-
this.applyRegisteredLinkProviders();
|
|
3113
|
-
}
|
|
3114
|
-
dispose() {
|
|
3115
|
-
this.isDisposed = true;
|
|
3116
|
-
const disposalError = new Error('TerminalCore was disposed before presentation completed');
|
|
3117
|
-
this.rejectFabricAttachWaiters(disposalError);
|
|
3118
|
-
this.rejectAllPresentationRequests(disposalError);
|
|
3119
|
-
this.initializationOperation?.lifecycleAbortController.abort();
|
|
3120
|
-
this.initializationOperation?.request.cancel();
|
|
3121
|
-
if (this.focusResizeRaf !== null) {
|
|
3122
|
-
cancelAnimationFrame(this.focusResizeRaf);
|
|
3123
|
-
this.focusResizeRaf = null;
|
|
3124
|
-
}
|
|
3125
|
-
if (this.presentationScaleRaf !== null) {
|
|
3126
|
-
cancelAnimationFrame(this.presentationScaleRaf);
|
|
3127
|
-
this.presentationScaleRaf = null;
|
|
3128
|
-
}
|
|
3129
|
-
if (this.clearResizeSuppressionRaf !== null) {
|
|
3130
|
-
cancelAnimationFrame(this.clearResizeSuppressionRaf);
|
|
3131
|
-
this.clearResizeSuppressionRaf = null;
|
|
3132
|
-
}
|
|
3133
|
-
if (this.resizeRaf !== null) {
|
|
3134
|
-
cancelAnimationFrame(this.resizeRaf);
|
|
3135
|
-
this.resizeRaf = null;
|
|
3136
|
-
}
|
|
3137
|
-
this.cancelDemandRender();
|
|
3138
|
-
this.unbindResponsiveListeners?.();
|
|
3139
|
-
this.unbindResponsiveListeners = null;
|
|
3140
|
-
this.resizeObserver?.disconnect();
|
|
3141
|
-
this.resizeObserver = null;
|
|
3142
|
-
if (this.resizeDebounceTimer) {
|
|
3143
|
-
clearTimeout(this.resizeDebounceTimer);
|
|
3144
|
-
this.resizeDebounceTimer = null;
|
|
3145
|
-
}
|
|
3146
|
-
this.visualRenderState.suspendDepth = 0;
|
|
3147
|
-
this.visualRenderState.activeReasons.clear();
|
|
3148
|
-
this.visualRenderState.pendingDemandRender = false;
|
|
3149
|
-
this.visualRenderState.pendingForceFullRender = false;
|
|
3150
|
-
this.visualRenderState.pendingResizeReason = null;
|
|
3151
|
-
this.visualRenderState.pendingSearchOverlayRender = false;
|
|
3152
|
-
if (this.replayingHistoryTimer) {
|
|
3153
|
-
clearTimeout(this.replayingHistoryTimer);
|
|
3154
|
-
this.replayingHistoryTimer = null;
|
|
3155
|
-
}
|
|
3156
|
-
this.disposeInputBridge();
|
|
3157
|
-
this.disposeTerminalEventListeners();
|
|
3158
|
-
this.disposeSearchOverlay();
|
|
3159
|
-
this.scrollbarOverlay?.dispose();
|
|
3160
|
-
this.scrollbarOverlay = null;
|
|
3161
|
-
this.fabricAttachSeq += 1;
|
|
3162
|
-
this.cancelFabricAttachSchedule?.();
|
|
3163
|
-
this.cancelFabricAttachSchedule = null;
|
|
3164
|
-
this.cancelInputRefocusSchedule?.();
|
|
3165
|
-
this.cancelInputRefocusSchedule = null;
|
|
3166
|
-
this.fabricView?.dispose();
|
|
3167
|
-
this.fabricView = null;
|
|
3168
|
-
this.terminal?.dispose();
|
|
3169
|
-
this.terminal = null;
|
|
3170
|
-
this.ownedRuntimeMemory = null;
|
|
3171
|
-
this.ownedRuntime = null;
|
|
3172
|
-
this.inputElement?.remove();
|
|
3173
|
-
this.inputElement = null;
|
|
3174
|
-
this.viewportHost?.remove();
|
|
3175
|
-
this.viewportHost = null;
|
|
3176
|
-
this.renderHost = null;
|
|
3177
|
-
this.fitAddon = null;
|
|
3178
|
-
this.registeredLinkProviders.clear();
|
|
3179
|
-
this.appliedLinkProviders.clear();
|
|
3180
|
-
this.setState(TerminalState.DISPOSED);
|
|
3181
|
-
}
|
|
3182
|
-
disposeInputBridge() {
|
|
3183
|
-
this.inputBridge?.dispose();
|
|
3184
|
-
this.inputBridge = null;
|
|
3185
|
-
}
|
|
3186
|
-
disposeTerminalEventListeners() {
|
|
3187
|
-
for (const dispose of this.terminalEventDisposables) {
|
|
3188
|
-
dispose();
|
|
3189
|
-
}
|
|
3190
|
-
this.terminalEventDisposables = [];
|
|
3191
|
-
}
|
|
3192
|
-
trackTerminalEventDisposable(disposable) {
|
|
3193
|
-
if (!disposable?.dispose) {
|
|
3194
|
-
return;
|
|
3195
|
-
}
|
|
3196
|
-
this.terminalEventDisposables.push(() => disposable.dispose?.());
|
|
3197
|
-
}
|
|
3198
|
-
applyRegisteredLinkProviders() {
|
|
3199
|
-
if (!this.terminal || typeof this.terminal.registerLinkProvider !== 'function') {
|
|
3200
|
-
return;
|
|
3201
|
-
}
|
|
3202
|
-
for (const provider of this.registeredLinkProviders) {
|
|
3203
|
-
if (this.appliedLinkProviders.has(provider)) {
|
|
3204
|
-
continue;
|
|
3205
|
-
}
|
|
3206
|
-
try {
|
|
3207
|
-
this.terminal.registerLinkProvider(provider);
|
|
3208
|
-
this.appliedLinkProviders.add(provider);
|
|
3209
|
-
}
|
|
3210
|
-
catch (error) {
|
|
3211
|
-
this.logger.warn('[TerminalCore] Failed to register link provider', { error });
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
}
|
|
3215
|
-
static normalizeResponsiveConfig(value) {
|
|
3216
|
-
const raw = (typeof value === 'object' && value) ? value : {};
|
|
3217
|
-
return {
|
|
3218
|
-
fitOnFocus: Boolean(raw.fitOnFocus),
|
|
3219
|
-
emitResizeOnFocus: Boolean(raw.emitResizeOnFocus),
|
|
3220
|
-
notifyResizeOnlyWhenFocused: Boolean(raw.notifyResizeOnlyWhenFocused),
|
|
3221
|
-
reportHostDimensionsWithFixedGrid: Boolean(raw.reportHostDimensionsWithFixedGrid),
|
|
3222
|
-
};
|
|
3223
|
-
}
|
|
3224
|
-
static normalizeClipboardConfig(value) {
|
|
3225
|
-
const raw = (typeof value === 'object' && value) ? value : {};
|
|
3226
|
-
return {
|
|
3227
|
-
copyOnSelect: raw.copyOnSelect !== false,
|
|
3228
|
-
};
|
|
3229
|
-
}
|
|
3230
|
-
static normalizeFitConfig(value) {
|
|
3231
|
-
const raw = (typeof value === 'object' && value) ? value : {};
|
|
3232
|
-
return {
|
|
3233
|
-
scrollbarReservePx: normalizeNonNegativePixels(raw.scrollbarReservePx, GHOSTTY_DEFAULT_SCROLLBAR_RESERVE_PX),
|
|
3234
|
-
};
|
|
3235
|
-
}
|
|
3236
|
-
static normalizeFontSize(value) {
|
|
3237
|
-
const size = Number(value);
|
|
3238
|
-
if (!Number.isFinite(size) || size <= 0) {
|
|
3239
|
-
return 12;
|
|
3240
|
-
}
|
|
3241
|
-
return size;
|
|
3242
|
-
}
|
|
3243
|
-
static normalizePresentationScale(value) {
|
|
3244
|
-
const scale = Number(value);
|
|
3245
|
-
if (!Number.isFinite(scale) || scale <= 1.001) {
|
|
3246
|
-
return 1;
|
|
3247
|
-
}
|
|
3248
|
-
return Math.min(scale, 4);
|
|
3249
|
-
}
|
|
3250
|
-
resolveEffectiveFontSize(scale = this.pendingPresentationScale ?? this.presentationScale) {
|
|
3251
|
-
return Math.max(1, this.logicalFontSize * scale);
|
|
3252
|
-
}
|
|
3253
|
-
isVisualRenderSuspended() {
|
|
3254
|
-
return this.visualRenderState.suspendDepth > 0;
|
|
3255
|
-
}
|
|
3256
|
-
markPendingDemandRender(forceAll) {
|
|
3257
|
-
this.visualRenderState.pendingDemandRender = true;
|
|
3258
|
-
this.visualRenderState.pendingForceFullRender = this.visualRenderState.pendingForceFullRender || forceAll;
|
|
3259
|
-
this.demandRenderForceAll = this.demandRenderForceAll || forceAll;
|
|
3260
|
-
}
|
|
3261
|
-
markPendingResize(reason) {
|
|
3262
|
-
this.visualRenderState.pendingResizeReason = this.mergePendingResizeReason(this.visualRenderState.pendingResizeReason, reason);
|
|
3263
|
-
}
|
|
3264
|
-
mergePendingResizeReason(current, next) {
|
|
3265
|
-
if (!current) {
|
|
3266
|
-
return next;
|
|
3267
|
-
}
|
|
3268
|
-
if (current === 'force' || next === 'force') {
|
|
3269
|
-
return 'force';
|
|
3270
|
-
}
|
|
3271
|
-
if (current === 'font' || next === 'font') {
|
|
3272
|
-
return 'font';
|
|
3273
|
-
}
|
|
3274
|
-
if (current === 'focus' || next === 'focus') {
|
|
3275
|
-
return 'focus';
|
|
3276
|
-
}
|
|
3277
|
-
if (current === 'post_init' || next === 'post_init') {
|
|
3278
|
-
return 'post_init';
|
|
3279
|
-
}
|
|
3280
|
-
return 'observer';
|
|
3281
|
-
}
|
|
3282
|
-
endVisualSuspend(id) {
|
|
3283
|
-
if (!this.visualRenderState.activeReasons.has(id)) {
|
|
3284
|
-
return;
|
|
3285
|
-
}
|
|
3286
|
-
this.visualRenderState.activeReasons.delete(id);
|
|
3287
|
-
this.visualRenderState.suspendDepth = Math.max(0, this.visualRenderState.suspendDepth - 1);
|
|
3288
|
-
if (this.visualRenderState.suspendDepth > 0) {
|
|
3289
|
-
return;
|
|
3290
|
-
}
|
|
3291
|
-
this.flushDeferredVisualWork();
|
|
3292
|
-
}
|
|
3293
|
-
flushDeferredVisualWork() {
|
|
3294
|
-
if (this.isDisposed || !this.terminal) {
|
|
3295
|
-
return;
|
|
3296
|
-
}
|
|
3297
|
-
const resizeReason = this.visualRenderState.pendingResizeReason;
|
|
3298
|
-
const shouldRender = this.visualRenderState.pendingDemandRender || this.demandRenderForceAll;
|
|
3299
|
-
const shouldForceRender = this.visualRenderState.pendingForceFullRender || this.demandRenderForceAll;
|
|
3300
|
-
const shouldRenderSearchOverlay = this.visualRenderState.pendingSearchOverlayRender;
|
|
3301
|
-
this.visualRenderState.pendingResizeReason = null;
|
|
3302
|
-
this.visualRenderState.pendingDemandRender = false;
|
|
3303
|
-
this.visualRenderState.pendingForceFullRender = false;
|
|
3304
|
-
this.visualRenderState.pendingSearchOverlayRender = false;
|
|
3305
|
-
this.demandRenderForceAll = false;
|
|
3306
|
-
if (resizeReason) {
|
|
3307
|
-
this.performResizeNow(resizeReason);
|
|
3308
|
-
}
|
|
3309
|
-
if (shouldRender) {
|
|
3310
|
-
this.requestDemandRender(shouldForceRender);
|
|
3311
|
-
}
|
|
3312
|
-
if (shouldRenderSearchOverlay) {
|
|
3313
|
-
this.scheduleRenderSearchOverlay();
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
flushPendingPresentationScale() {
|
|
3317
|
-
if (this.pendingPresentationScale === null) {
|
|
3318
|
-
return;
|
|
3319
|
-
}
|
|
3320
|
-
const targetScale = this.pendingPresentationScale;
|
|
3321
|
-
this.pendingPresentationScale = null;
|
|
3322
|
-
if (this.presentationScaleRaf !== null) {
|
|
3323
|
-
cancelAnimationFrame(this.presentationScaleRaf);
|
|
3324
|
-
this.presentationScaleRaf = null;
|
|
3325
|
-
}
|
|
3326
|
-
this.commitPresentationScale(targetScale);
|
|
3327
|
-
}
|
|
3328
|
-
commitPresentationScale(scale) {
|
|
3329
|
-
const nextScale = TerminalCore.normalizePresentationScale(scale);
|
|
3330
|
-
if (samePresentationScale(nextScale, this.presentationScale)) {
|
|
3331
|
-
this.applyPresentationScaleStyles();
|
|
3332
|
-
return;
|
|
3333
|
-
}
|
|
3334
|
-
this.presentationScale = nextScale;
|
|
3335
|
-
this.applyPresentationScaleStyles();
|
|
3336
|
-
if (!this.terminal) {
|
|
3337
|
-
return;
|
|
3338
|
-
}
|
|
3339
|
-
this.suppressResizeNotifications = true;
|
|
3340
|
-
if (this.clearResizeSuppressionRaf !== null) {
|
|
3341
|
-
cancelAnimationFrame(this.clearResizeSuppressionRaf);
|
|
3342
|
-
}
|
|
3343
|
-
this.clearResizeSuppressionRaf = requestAnimationFrame(() => {
|
|
3344
|
-
this.clearResizeSuppressionRaf = null;
|
|
3345
|
-
this.suppressResizeNotifications = false;
|
|
3346
|
-
});
|
|
3347
|
-
this.terminal.options.fontSize = this.resolveEffectiveFontSize(nextScale);
|
|
3348
|
-
this.fabricView?.renderer.setAppearance({ fontSize: this.resolveEffectiveFontSize(nextScale) });
|
|
3349
|
-
if (this.isVisualRenderSuspended()) {
|
|
3350
|
-
this.markPendingResize('force');
|
|
3351
|
-
this.markPendingDemandRender(true);
|
|
3352
|
-
this.scheduleRenderSearchOverlay();
|
|
3353
|
-
return;
|
|
3354
|
-
}
|
|
3355
|
-
this.performResize('force');
|
|
3356
|
-
this.forceFullRender();
|
|
3357
|
-
this.scheduleRenderSearchOverlay();
|
|
3358
|
-
}
|
|
3359
|
-
resizeFabricToHost() {
|
|
3360
|
-
const host = this.renderHost ?? this.container;
|
|
3361
|
-
if (!host) {
|
|
3362
|
-
return;
|
|
3363
|
-
}
|
|
3364
|
-
const width = host.clientWidth;
|
|
3365
|
-
const height = host.clientHeight;
|
|
3366
|
-
if (width <= 0 || height <= 0) {
|
|
3367
|
-
return;
|
|
3368
|
-
}
|
|
3369
|
-
this.fabricView?.renderer.resize(width, height);
|
|
3370
|
-
this.syncGhosttyInputGeometryToFabric();
|
|
3371
|
-
}
|
|
3372
|
-
isFabricRendererActive() {
|
|
3373
|
-
return Boolean(this.fabricView?.renderer.isActive());
|
|
3374
|
-
}
|
|
3375
|
-
forceFullRender() {
|
|
3376
|
-
if (!this.terminal || !this.isReady()) {
|
|
3377
|
-
return;
|
|
3378
|
-
}
|
|
3379
|
-
if (this.isVisualRenderSuspended()) {
|
|
3380
|
-
this.markPendingDemandRender(true);
|
|
3381
|
-
return;
|
|
3382
|
-
}
|
|
3383
|
-
this.renderDemandFrame(true);
|
|
3384
|
-
}
|
|
3385
|
-
requestDemandRender(forceAll) {
|
|
3386
|
-
if (!this.terminal || this.isDisposed) {
|
|
3387
|
-
return;
|
|
3388
|
-
}
|
|
3389
|
-
if (this.isVisualRenderSuspended()) {
|
|
3390
|
-
this.markPendingDemandRender(forceAll);
|
|
3391
|
-
return;
|
|
3392
|
-
}
|
|
3393
|
-
this.demandRenderForceAll = this.demandRenderForceAll || forceAll;
|
|
3394
|
-
terminalRenderScheduler.schedule(this.renderTask, this.demandRenderForceAll);
|
|
3395
|
-
}
|
|
3396
|
-
cancelDemandRender() {
|
|
3397
|
-
terminalRenderScheduler.cancel(this.renderTask);
|
|
3398
|
-
this.demandRenderForceAll = false;
|
|
3399
|
-
this.stopGhosttyRenderLoop();
|
|
3400
|
-
}
|
|
3401
|
-
stopGhosttyRenderLoop() {
|
|
3402
|
-
const terminalAny = this.terminal;
|
|
3403
|
-
if (!terminalAny || typeof terminalAny.animationFrameId !== 'number') {
|
|
3404
|
-
return;
|
|
3405
|
-
}
|
|
3406
|
-
cancelAnimationFrame(terminalAny.animationFrameId);
|
|
3407
|
-
terminalAny.animationFrameId = undefined;
|
|
3408
|
-
}
|
|
3409
|
-
renderDemandFrame(forceAll) {
|
|
3410
|
-
if (!this.terminal || this.isDisposed) {
|
|
3411
|
-
return;
|
|
3412
|
-
}
|
|
3413
|
-
const terminalAny = this.terminal;
|
|
3414
|
-
if (terminalAny.isDisposed || terminalAny.isOpen === false || !terminalAny.renderer?.render || !terminalAny.wasmTerm) {
|
|
3415
|
-
return;
|
|
3416
|
-
}
|
|
3417
|
-
const presentationRequest = forceAll ? this.pendingPresentationRequest : null;
|
|
3418
|
-
const expectedRows = Math.max(0, Math.floor(Number(terminalAny.wasmTerm.getDimensions?.().rows ?? 0)));
|
|
3419
|
-
const fabricFrameGeneration = this.committedFabricFrameGeneration;
|
|
3420
|
-
try {
|
|
3421
|
-
const startedAt = performance.now();
|
|
3422
|
-
this.keepDemandCursorVisible(terminalAny.renderer);
|
|
3423
|
-
terminalAny.renderer.render(terminalAny.wasmTerm, forceAll, terminalAny.viewportY ?? 0, terminalAny, terminalAny.scrollbarOpacity);
|
|
3424
|
-
this.settleCommittedFrameRequests();
|
|
3425
|
-
const cursor = terminalAny.wasmTerm.getCursor?.();
|
|
3426
|
-
if (cursor && typeof cursor.y === 'number' && cursor.y !== terminalAny.lastCursorY) {
|
|
3427
|
-
terminalAny.lastCursorY = cursor.y;
|
|
3428
|
-
terminalAny.cursorMoveEmitter?.fire?.();
|
|
3429
|
-
}
|
|
3430
|
-
this.syncImeInputAnchor();
|
|
3431
|
-
this.scrollbarOverlay?.sync();
|
|
3432
|
-
const durationMs = performance.now() - startedAt;
|
|
3433
|
-
getPerfProbe()?.onTerminalRender?.(durationMs);
|
|
3434
|
-
if (this.config.rendererType !== 'webgl'
|
|
3435
|
-
|| this.committedFabricFrameGeneration > fabricFrameGeneration) {
|
|
3436
|
-
try {
|
|
3437
|
-
this.eventHandlers.onRender?.(durationMs);
|
|
3438
|
-
}
|
|
3439
|
-
catch (error) {
|
|
3440
|
-
this.logger.debug('[TerminalCore] onRender observer failed', { error });
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3443
|
-
if (presentationRequest && this.pendingPresentationRequest === presentationRequest) {
|
|
3444
|
-
if (this.config.rendererType === 'webgl') {
|
|
3445
|
-
const committed = this.lastCommittedFabricFrame;
|
|
3446
|
-
if (!committed
|
|
3447
|
-
|| committed.generation <= fabricFrameGeneration
|
|
3448
|
-
|| expectedRows <= 0
|
|
3449
|
-
|| committed.result.renderedRows !== expectedRows) {
|
|
3450
|
-
this.rejectPresentationRequest(presentationRequest, new Error('The forced Beamterm frame was incomplete '
|
|
3451
|
-
+ `(expected_rows=${expectedRows} rendered_rows=${committed?.result.renderedRows ?? 0})`));
|
|
3452
|
-
return;
|
|
3453
|
-
}
|
|
3454
|
-
const fabricRenderer = this.fabricView?.renderer;
|
|
3455
|
-
if (!fabricRenderer) {
|
|
3456
|
-
throw new Error('The Beamterm renderer detached before its frame completed');
|
|
3457
|
-
}
|
|
3458
|
-
fabricRenderer.finishSubmittedFrame();
|
|
3459
|
-
this.submittedFabricFrameGeneration = committed.generation;
|
|
3460
|
-
}
|
|
3461
|
-
this.pendingPresentationRequest = null;
|
|
3462
|
-
presentationRequest.frameHandle = requestAnimationFrame(() => {
|
|
3463
|
-
if (!this.presentationRequests.has(presentationRequest))
|
|
3464
|
-
return;
|
|
3465
|
-
if (!this.presentationRequests.delete(presentationRequest))
|
|
3466
|
-
return;
|
|
3467
|
-
presentationRequest.frameHandle = null;
|
|
3468
|
-
presentationRequest.resolve();
|
|
3469
|
-
});
|
|
3470
|
-
}
|
|
3471
|
-
}
|
|
3472
|
-
catch (error) {
|
|
3473
|
-
if (presentationRequest && this.presentationRequests.has(presentationRequest)) {
|
|
3474
|
-
this.rejectPresentationRequest(presentationRequest, error instanceof Error ? error : new Error(String(error)));
|
|
3475
|
-
}
|
|
3476
|
-
this.logger.debug('[TerminalCore] Force render failed', { error });
|
|
3477
|
-
}
|
|
3478
|
-
}
|
|
3479
|
-
rejectPresentationRequest(request, error) {
|
|
3480
|
-
if (!this.presentationRequests.delete(request))
|
|
3481
|
-
return;
|
|
3482
|
-
if (this.pendingPresentationRequest === request) {
|
|
3483
|
-
this.pendingPresentationRequest = null;
|
|
3484
|
-
}
|
|
3485
|
-
if (request.frameHandle !== null) {
|
|
3486
|
-
cancelAnimationFrame(request.frameHandle);
|
|
3487
|
-
request.frameHandle = null;
|
|
3488
|
-
}
|
|
3489
|
-
request.reject(error);
|
|
3490
|
-
}
|
|
3491
|
-
scheduleCommittedFrameCheck(request) {
|
|
3492
|
-
if (!this.committedFrameRequests.has(request) || request.frameHandle !== null)
|
|
3493
|
-
return;
|
|
3494
|
-
queueMicrotask(() => {
|
|
3495
|
-
if (!this.committedFrameRequests.has(request) || request.frameHandle !== null)
|
|
3496
|
-
return;
|
|
3497
|
-
request.frameHandle = requestAnimationFrame(() => {
|
|
3498
|
-
request.frameHandle = null;
|
|
3499
|
-
this.checkCommittedFrameRequest(request);
|
|
3500
|
-
});
|
|
3501
|
-
});
|
|
3502
|
-
}
|
|
3503
|
-
checkCommittedFrameRequest(request) {
|
|
3504
|
-
if (!this.committedFrameRequests.has(request))
|
|
3505
|
-
return;
|
|
3506
|
-
if (this.isDisposed) {
|
|
3507
|
-
this.rejectCommittedFrameRequest(request, new Error('TerminalCore was disposed before the committed frame completed'));
|
|
3508
|
-
return;
|
|
3509
|
-
}
|
|
3510
|
-
if (this.config.rendererType !== 'webgl') {
|
|
3511
|
-
this.resolveCommittedFrameRequest(request);
|
|
3512
|
-
return;
|
|
3513
|
-
}
|
|
3514
|
-
if (this.committedFabricFrameGeneration > request.baselineGeneration) {
|
|
3515
|
-
this.settleCommittedFrameRequests();
|
|
3516
|
-
return;
|
|
3517
|
-
}
|
|
3518
|
-
this.scheduleCommittedFrameCheck(request);
|
|
3519
|
-
}
|
|
3520
|
-
settleCommittedFrameRequests() {
|
|
3521
|
-
const ready = [...this.committedFrameRequests].filter(request => this.committedFabricFrameGeneration > request.baselineGeneration);
|
|
3522
|
-
if (ready.length === 0)
|
|
3523
|
-
return;
|
|
3524
|
-
try {
|
|
3525
|
-
if (this.submittedFabricFrameGeneration < this.committedFabricFrameGeneration) {
|
|
3526
|
-
this.fabricView?.renderer.finishSubmittedFrame();
|
|
3527
|
-
this.submittedFabricFrameGeneration = this.committedFabricFrameGeneration;
|
|
3528
|
-
}
|
|
3529
|
-
for (const request of ready)
|
|
3530
|
-
this.resolveCommittedFrameRequest(request);
|
|
3531
|
-
}
|
|
3532
|
-
catch (error) {
|
|
3533
|
-
const failure = error instanceof Error ? error : new Error(String(error));
|
|
3534
|
-
for (const request of ready)
|
|
3535
|
-
this.rejectCommittedFrameRequest(request, failure);
|
|
3536
|
-
}
|
|
3537
|
-
}
|
|
3538
|
-
resolveCommittedFrameRequest(request) {
|
|
3539
|
-
if (!this.committedFrameRequests.has(request))
|
|
3540
|
-
return;
|
|
3541
|
-
if (request.frameHandle !== null) {
|
|
3542
|
-
cancelAnimationFrame(request.frameHandle);
|
|
3543
|
-
request.frameHandle = null;
|
|
3544
|
-
}
|
|
3545
|
-
request.resolve();
|
|
3546
|
-
}
|
|
3547
|
-
rejectCommittedFrameRequest(request, error) {
|
|
3548
|
-
if (!this.committedFrameRequests.delete(request))
|
|
3549
|
-
return;
|
|
3550
|
-
if (request.frameHandle !== null) {
|
|
3551
|
-
cancelAnimationFrame(request.frameHandle);
|
|
3552
|
-
request.frameHandle = null;
|
|
3553
|
-
}
|
|
3554
|
-
request.reject(error);
|
|
3555
|
-
}
|
|
3556
|
-
rejectAllPresentationRequests(error) {
|
|
3557
|
-
for (const request of [...this.presentationRequests]) {
|
|
3558
|
-
this.rejectPresentationRequest(request, error);
|
|
3559
|
-
}
|
|
3560
|
-
for (const request of [...this.committedFrameRequests]) {
|
|
3561
|
-
this.rejectCommittedFrameRequest(request, error);
|
|
3562
|
-
}
|
|
3563
|
-
}
|
|
3564
|
-
}
|
|
3565
|
-
TerminalCore.nextRenderTaskId = 1;
|
|
3566
|
-
//# sourceMappingURL=TerminalCore.js.map
|