@next_term/web 0.1.0-next.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/dist/accessibility.d.ts +46 -0
- package/dist/accessibility.d.ts.map +1 -0
- package/dist/accessibility.js +196 -0
- package/dist/accessibility.js.map +1 -0
- package/dist/addon.d.ts.map +1 -0
- package/dist/addon.js +2 -0
- package/dist/addon.js.map +1 -0
- package/dist/addons/fit.d.ts.map +1 -0
- package/dist/addons/fit.js +40 -0
- package/dist/addons/fit.js.map +1 -0
- package/dist/addons/search.d.ts +56 -0
- package/dist/addons/search.d.ts.map +1 -0
- package/dist/addons/search.js +178 -0
- package/dist/addons/search.js.map +1 -0
- package/dist/addons/web-links.d.ts +30 -0
- package/dist/addons/web-links.d.ts.map +1 -0
- package/dist/addons/web-links.js +170 -0
- package/dist/addons/web-links.js.map +1 -0
- package/dist/fit.d.ts.map +1 -0
- package/dist/fit.js +14 -0
- package/dist/fit.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/input-handler.d.ts +185 -0
- package/dist/input-handler.d.ts.map +1 -0
- package/dist/input-handler.js +1197 -0
- package/dist/input-handler.js.map +1 -0
- package/dist/parser-worker.d.ts.map +1 -0
- package/dist/parser-worker.js +128 -0
- package/dist/parser-worker.js.map +1 -0
- package/dist/render-bridge.d.ts +56 -0
- package/dist/render-bridge.d.ts.map +1 -0
- package/dist/render-bridge.js +158 -0
- package/dist/render-bridge.js.map +1 -0
- package/dist/render-worker.d.ts +62 -0
- package/dist/render-worker.d.ts.map +1 -0
- package/dist/render-worker.js +720 -0
- package/dist/render-worker.js.map +1 -0
- package/dist/renderer.d.ts +86 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +454 -0
- package/dist/renderer.js.map +1 -0
- package/dist/shared-context.d.ts +93 -0
- package/dist/shared-context.d.ts.map +1 -0
- package/dist/shared-context.js +561 -0
- package/dist/shared-context.js.map +1 -0
- package/dist/web-terminal.d.ts +152 -0
- package/dist/web-terminal.d.ts.map +1 -0
- package/dist/web-terminal.js +684 -0
- package/dist/web-terminal.js.map +1 -0
- package/dist/webgl-renderer.d.ts +146 -0
- package/dist/webgl-renderer.d.ts.map +1 -0
- package/dist/webgl-renderer.js +1047 -0
- package/dist/webgl-renderer.js.map +1 -0
- package/dist/worker-bridge.d.ts +51 -0
- package/dist/worker-bridge.d.ts.map +1 -0
- package/dist/worker-bridge.js +185 -0
- package/dist/worker-bridge.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebTerminal — main entry point for the @next_term/web package.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the core BufferSet/VTParser, Canvas2DRenderer, InputHandler,
|
|
5
|
+
* and the requestAnimationFrame render loop.
|
|
6
|
+
*
|
|
7
|
+
* When `useWorker` is enabled (default: true when SAB is available) the VT
|
|
8
|
+
* parser runs in a Web Worker via WorkerBridge; otherwise it runs on the main
|
|
9
|
+
* thread as before.
|
|
10
|
+
*
|
|
11
|
+
* When `renderMode` is `'offscreen'` or `'auto'` (with SAB + OffscreenCanvas
|
|
12
|
+
* available), the WebGL2 render loop runs in a separate Web Worker via
|
|
13
|
+
* RenderBridge, leaving the main thread free for DOM event handling only.
|
|
14
|
+
*/
|
|
15
|
+
import { BufferSet, CellGrid, DEFAULT_THEME, VTParser } from "@next_term/core";
|
|
16
|
+
import { AccessibilityManager } from "./accessibility.js";
|
|
17
|
+
import { calculateFit } from "./fit.js";
|
|
18
|
+
import { InputHandler } from "./input-handler.js";
|
|
19
|
+
import { canUseOffscreenCanvas, RenderBridge } from "./render-bridge.js";
|
|
20
|
+
import { Canvas2DRenderer } from "./renderer.js";
|
|
21
|
+
import { WebGLRenderer } from "./webgl-renderer.js";
|
|
22
|
+
import { WorkerBridge } from "./worker-bridge.js";
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Feature detection
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
const SAB_AVAILABLE = typeof SharedArrayBuffer !== "undefined" &&
|
|
27
|
+
(typeof crossOriginIsolated !== "undefined" ? crossOriginIsolated : true);
|
|
28
|
+
const OFFSCREEN_CANVAS_AVAILABLE = canUseOffscreenCanvas();
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Defaults
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
const DEFAULT_COLS = 80;
|
|
33
|
+
const DEFAULT_ROWS = 24;
|
|
34
|
+
const DEFAULT_FONT_SIZE = 14;
|
|
35
|
+
const DEFAULT_FONT_FAMILY = "'Menlo', 'DejaVu Sans Mono', 'Consolas', monospace";
|
|
36
|
+
const DEFAULT_SCROLLBACK = 1000;
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// WebTerminal
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
function mergeTheme(partial) {
|
|
41
|
+
if (!partial)
|
|
42
|
+
return { ...DEFAULT_THEME };
|
|
43
|
+
return { ...DEFAULT_THEME, ...partial };
|
|
44
|
+
}
|
|
45
|
+
export class WebTerminal {
|
|
46
|
+
container;
|
|
47
|
+
canvas;
|
|
48
|
+
bufferSet;
|
|
49
|
+
parser = null;
|
|
50
|
+
renderer;
|
|
51
|
+
inputHandler;
|
|
52
|
+
disposed = false;
|
|
53
|
+
addons = [];
|
|
54
|
+
accessibilityManager = null;
|
|
55
|
+
/** WorkerBridge when using off-thread parsing, null otherwise. */
|
|
56
|
+
workerBridge = null;
|
|
57
|
+
/** RenderBridge when using off-thread rendering, null otherwise. */
|
|
58
|
+
renderBridge = null;
|
|
59
|
+
/** Whether the worker mode is active. */
|
|
60
|
+
useWorkerMode;
|
|
61
|
+
/** Whether the offscreen render mode is active. */
|
|
62
|
+
useOffscreenRender;
|
|
63
|
+
/** Track whether alternate buffer is active so we can detect switches. */
|
|
64
|
+
wasAlternate = false;
|
|
65
|
+
/** Track sync output mode to detect transitions. */
|
|
66
|
+
_syncedOutput = false;
|
|
67
|
+
// Scrollback viewport: 0 = live (bottom), positive = lines scrolled back
|
|
68
|
+
viewportOffset = 0;
|
|
69
|
+
/** Temporary display grid used when scrolled into scrollback. */
|
|
70
|
+
displayGrid = null;
|
|
71
|
+
/** Scrollbar overlay element. */
|
|
72
|
+
scrollbarEl = null;
|
|
73
|
+
/** Scrollbar thumb element. */
|
|
74
|
+
scrollbarThumb = null;
|
|
75
|
+
/** Timer to auto-hide scrollbar. */
|
|
76
|
+
scrollbarHideTimer = null;
|
|
77
|
+
// Text encoder for string -> Uint8Array
|
|
78
|
+
encoder = new TextEncoder();
|
|
79
|
+
// Callbacks
|
|
80
|
+
onDataCallback;
|
|
81
|
+
onResizeCallback;
|
|
82
|
+
onTitleChangeCallback;
|
|
83
|
+
constructor(container, options) {
|
|
84
|
+
this.container = container;
|
|
85
|
+
const cols = options?.cols ?? DEFAULT_COLS;
|
|
86
|
+
const rows = options?.rows ?? DEFAULT_ROWS;
|
|
87
|
+
const fontSize = options?.fontSize ?? DEFAULT_FONT_SIZE;
|
|
88
|
+
const fontFamily = options?.fontFamily ?? DEFAULT_FONT_FAMILY;
|
|
89
|
+
const theme = mergeTheme(options?.theme);
|
|
90
|
+
const scrollback = options?.scrollback ?? DEFAULT_SCROLLBACK;
|
|
91
|
+
this.onDataCallback = options?.onData ?? null;
|
|
92
|
+
this.onResizeCallback = options?.onResize ?? null;
|
|
93
|
+
this.onTitleChangeCallback = options?.onTitleChange ?? null;
|
|
94
|
+
// Determine whether to use the worker.
|
|
95
|
+
this.useWorkerMode = options?.useWorker ?? SAB_AVAILABLE;
|
|
96
|
+
// Determine render mode.
|
|
97
|
+
const renderMode = options?.renderMode ?? "auto";
|
|
98
|
+
if (renderMode === "offscreen") {
|
|
99
|
+
this.useOffscreenRender = true;
|
|
100
|
+
}
|
|
101
|
+
else if (renderMode === "main") {
|
|
102
|
+
this.useOffscreenRender = false;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// 'auto': use offscreen if SAB + OffscreenCanvas both available
|
|
106
|
+
this.useOffscreenRender = SAB_AVAILABLE && OFFSCREEN_CANVAS_AVAILABLE;
|
|
107
|
+
}
|
|
108
|
+
// Create core buffer set
|
|
109
|
+
this.bufferSet = new BufferSet(cols, rows, scrollback);
|
|
110
|
+
// Create canvas element
|
|
111
|
+
this.canvas = document.createElement("canvas");
|
|
112
|
+
this.canvas.style.display = "block";
|
|
113
|
+
container.style.position = container.style.position || "relative";
|
|
114
|
+
container.style.overflow = "hidden";
|
|
115
|
+
container.appendChild(this.canvas);
|
|
116
|
+
// Create scrollbar overlay
|
|
117
|
+
this.createScrollbar(container);
|
|
118
|
+
// Create renderer based on selected backend
|
|
119
|
+
const rendererType = options?.renderer ?? "auto";
|
|
120
|
+
const rendererOpts = {
|
|
121
|
+
fontSize,
|
|
122
|
+
fontFamily,
|
|
123
|
+
theme,
|
|
124
|
+
devicePixelRatio: options?.devicePixelRatio,
|
|
125
|
+
};
|
|
126
|
+
if (this.useOffscreenRender && this.bufferSet.active.grid.isShared) {
|
|
127
|
+
// Full worker mode: rendering happens in a Web Worker via RenderBridge.
|
|
128
|
+
// We still need a main-thread renderer for getCellSize() measurements.
|
|
129
|
+
this.renderer = new Canvas2DRenderer(rendererOpts);
|
|
130
|
+
this.renderBridge = new RenderBridge(this.canvas, {
|
|
131
|
+
fontSize,
|
|
132
|
+
fontFamily,
|
|
133
|
+
theme,
|
|
134
|
+
devicePixelRatio: options?.devicePixelRatio,
|
|
135
|
+
onError: (message) => {
|
|
136
|
+
console.warn("[WebTerminal] Render worker error, falling back:", message);
|
|
137
|
+
this.fallbackToMainThreadRenderer(rendererOpts);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
this.renderBridge.start(this.bufferSet.active.grid.getBuffer(), cols, rows);
|
|
141
|
+
// Sync cursor into SAB so the render worker can read it
|
|
142
|
+
this.syncCursorToSAB();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if (rendererType === "webgl") {
|
|
146
|
+
this.renderer = new WebGLRenderer(rendererOpts);
|
|
147
|
+
}
|
|
148
|
+
else if (rendererType === "canvas2d") {
|
|
149
|
+
this.renderer = new Canvas2DRenderer(rendererOpts);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// 'auto': try WebGL2 first, fall back to Canvas 2D
|
|
153
|
+
let useWebGL = false;
|
|
154
|
+
try {
|
|
155
|
+
const testCanvas = document.createElement("canvas");
|
|
156
|
+
const testGl = testCanvas.getContext("webgl2");
|
|
157
|
+
useWebGL = testGl !== null;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// WebGL2 not available
|
|
161
|
+
}
|
|
162
|
+
this.renderer = useWebGL
|
|
163
|
+
? new WebGLRenderer(rendererOpts)
|
|
164
|
+
: new Canvas2DRenderer(rendererOpts);
|
|
165
|
+
}
|
|
166
|
+
this.renderer.attach(this.canvas, this.bufferSet.active.grid, this.bufferSet.active.cursor);
|
|
167
|
+
}
|
|
168
|
+
// Create input handler
|
|
169
|
+
this.inputHandler = new InputHandler({
|
|
170
|
+
onData: (data) => {
|
|
171
|
+
// User typed something — snap back to live view
|
|
172
|
+
this.snapToBottom();
|
|
173
|
+
this.onDataCallback?.(data);
|
|
174
|
+
},
|
|
175
|
+
onSelectionChange: (sel) => {
|
|
176
|
+
if (this.renderBridge) {
|
|
177
|
+
this.renderBridge.updateSelection(sel);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
this.renderer.setSelection(sel);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
onScroll: (deltaRows) => {
|
|
184
|
+
// GestureHandler already negates: positive = scroll back (older content).
|
|
185
|
+
this.scrollViewport(deltaRows);
|
|
186
|
+
},
|
|
187
|
+
onFontSizeChange: (newFontSize) => {
|
|
188
|
+
this.setFont(newFontSize, fontFamily);
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
this.inputHandler.setGrid(this.bufferSet.active.grid);
|
|
192
|
+
this.inputHandler.setFontSize(fontSize);
|
|
193
|
+
const { width, height } = this.renderer.getCellSize();
|
|
194
|
+
this.inputHandler.attach(container, width, height);
|
|
195
|
+
// Set up parsing: worker mode or direct mode.
|
|
196
|
+
if (this.useWorkerMode) {
|
|
197
|
+
this.startWorkerMode(cols, rows, scrollback);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
this.parser = new VTParser(this.bufferSet);
|
|
201
|
+
// Wire up title change callback
|
|
202
|
+
this.parser.setTitleChangeCallback((title) => {
|
|
203
|
+
this.onTitleChangeCallback?.(title);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
// Start render loop (only when NOT using offscreen — the worker has its own loop)
|
|
207
|
+
if (!this.renderBridge) {
|
|
208
|
+
this.renderer.startRenderLoop();
|
|
209
|
+
}
|
|
210
|
+
// Initialize accessibility manager
|
|
211
|
+
this.accessibilityManager = new AccessibilityManager(container, this.bufferSet.active.grid, rows, cols);
|
|
212
|
+
}
|
|
213
|
+
// -----------------------------------------------------------------------
|
|
214
|
+
// Worker management
|
|
215
|
+
// -----------------------------------------------------------------------
|
|
216
|
+
startWorkerMode(cols, rows, scrollback) {
|
|
217
|
+
try {
|
|
218
|
+
this.workerBridge = new WorkerBridge(this.bufferSet.active.grid, this.bufferSet.active.cursor, (isAlternate) => {
|
|
219
|
+
// When the alternate buffer is toggled the renderer needs to
|
|
220
|
+
// know which grid to read from.
|
|
221
|
+
const activeGrid = isAlternate
|
|
222
|
+
? this.bufferSet.alternate.grid
|
|
223
|
+
: this.bufferSet.normal.grid;
|
|
224
|
+
const activeCursor = isAlternate
|
|
225
|
+
? this.bufferSet.alternate.cursor
|
|
226
|
+
: this.bufferSet.normal.cursor;
|
|
227
|
+
if (this.renderBridge) {
|
|
228
|
+
// In offscreen mode, update the render worker's SAB reference
|
|
229
|
+
this.renderBridge.resize(activeGrid.cols, activeGrid.rows, activeGrid.getBuffer());
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
this.renderer.attach(this.canvas, activeGrid, activeCursor);
|
|
233
|
+
}
|
|
234
|
+
}, (message) => {
|
|
235
|
+
// On worker error, fall back to main-thread parsing.
|
|
236
|
+
console.warn("[WebTerminal] Worker error, falling back to main thread:", message);
|
|
237
|
+
this.fallbackToMainThread();
|
|
238
|
+
});
|
|
239
|
+
this.workerBridge.start(cols, rows, scrollback);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Worker could not be created — fall back silently.
|
|
243
|
+
this.fallbackToMainThread();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
fallbackToMainThread() {
|
|
247
|
+
if (this.workerBridge) {
|
|
248
|
+
this.workerBridge.dispose();
|
|
249
|
+
this.workerBridge = null;
|
|
250
|
+
}
|
|
251
|
+
if (!this.parser) {
|
|
252
|
+
this.parser = new VTParser(this.bufferSet);
|
|
253
|
+
this.parser.setTitleChangeCallback((title) => {
|
|
254
|
+
this.onTitleChangeCallback?.(title);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Fall back from offscreen rendering to main-thread rendering.
|
|
260
|
+
*/
|
|
261
|
+
fallbackToMainThreadRenderer(rendererOpts) {
|
|
262
|
+
if (this.renderBridge) {
|
|
263
|
+
this.renderBridge.dispose();
|
|
264
|
+
this.renderBridge = null;
|
|
265
|
+
}
|
|
266
|
+
// The canvas was transferred; we need a new one.
|
|
267
|
+
const newCanvas = document.createElement("canvas");
|
|
268
|
+
newCanvas.style.display = "block";
|
|
269
|
+
if (this.canvas.parentElement) {
|
|
270
|
+
this.canvas.parentElement.replaceChild(newCanvas, this.canvas);
|
|
271
|
+
}
|
|
272
|
+
this.canvas = newCanvas;
|
|
273
|
+
this.renderer = new Canvas2DRenderer(rendererOpts);
|
|
274
|
+
this.renderer.attach(this.canvas, this.bufferSet.active.grid, this.bufferSet.active.cursor);
|
|
275
|
+
this.renderer.startRenderLoop();
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Write cursor state into the SAB so the render worker can read it.
|
|
279
|
+
*/
|
|
280
|
+
syncCursorToSAB() {
|
|
281
|
+
const cursor = this.bufferSet.active.cursor;
|
|
282
|
+
const grid = this.bufferSet.active.grid;
|
|
283
|
+
grid.setCursor(cursor.row, cursor.col, cursor.visible, cursor.style);
|
|
284
|
+
}
|
|
285
|
+
// -----------------------------------------------------------------------
|
|
286
|
+
// Public API
|
|
287
|
+
// -----------------------------------------------------------------------
|
|
288
|
+
get cols() {
|
|
289
|
+
return this.bufferSet.cols;
|
|
290
|
+
}
|
|
291
|
+
get rows() {
|
|
292
|
+
return this.bufferSet.rows;
|
|
293
|
+
}
|
|
294
|
+
/** The active grid (for addons to read cell data). */
|
|
295
|
+
get activeGrid() {
|
|
296
|
+
return this.bufferSet.active.grid;
|
|
297
|
+
}
|
|
298
|
+
/** The active cursor state (for addons). */
|
|
299
|
+
get activeCursor() {
|
|
300
|
+
return this.bufferSet.active.cursor;
|
|
301
|
+
}
|
|
302
|
+
/** The container element. */
|
|
303
|
+
get element() {
|
|
304
|
+
return this.container;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Write data to the terminal. When worker mode is active the data is
|
|
308
|
+
* forwarded to the Web Worker; otherwise it is parsed on the main thread.
|
|
309
|
+
*/
|
|
310
|
+
write(data) {
|
|
311
|
+
if (this.disposed)
|
|
312
|
+
return;
|
|
313
|
+
// New data arrived — snap back to live view
|
|
314
|
+
this.snapToBottom();
|
|
315
|
+
const bytes = typeof data === "string" ? this.encoder.encode(data) : data;
|
|
316
|
+
if (this.workerBridge) {
|
|
317
|
+
this.workerBridge.write(bytes);
|
|
318
|
+
}
|
|
319
|
+
else if (this.parser) {
|
|
320
|
+
this.parser.write(bytes);
|
|
321
|
+
// Sync mode flags from parser to input handler
|
|
322
|
+
this.syncParserModes();
|
|
323
|
+
}
|
|
324
|
+
// Update accessibility tree (throttled internally to 10 Hz)
|
|
325
|
+
this.accessibilityManager?.update();
|
|
326
|
+
}
|
|
327
|
+
/** Sync parser mode flags to the input handler, and detect buffer switches. */
|
|
328
|
+
syncParserModes() {
|
|
329
|
+
if (!this.parser)
|
|
330
|
+
return;
|
|
331
|
+
this.inputHandler.setApplicationCursorKeys(this.parser.applicationCursorKeys);
|
|
332
|
+
this.inputHandler.setBracketedPasteMode(this.parser.bracketedPasteMode);
|
|
333
|
+
this.inputHandler.setMouseProtocol(this.parser.mouseProtocol);
|
|
334
|
+
this.inputHandler.setMouseEncoding(this.parser.mouseEncoding);
|
|
335
|
+
this.inputHandler.setSendFocusEvents(this.parser.sendFocusEvents);
|
|
336
|
+
this.inputHandler.setKittyFlags(this.parser.kittyFlags);
|
|
337
|
+
// Synchronized output mode 2026: gate the main-thread render loop.
|
|
338
|
+
// The offscreen render worker has its own loop and is not gated here.
|
|
339
|
+
const isSynced = this.parser.syncedOutput;
|
|
340
|
+
if (isSynced !== this._syncedOutput) {
|
|
341
|
+
this._syncedOutput = isSynced;
|
|
342
|
+
if (!this.renderBridge) {
|
|
343
|
+
if (isSynced) {
|
|
344
|
+
this.renderer.stopRenderLoop();
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
this.renderer.startRenderLoop();
|
|
348
|
+
this.renderer.render();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Detect alternate buffer switch and re-attach renderer
|
|
353
|
+
const isAlt = this.bufferSet.isAlternate;
|
|
354
|
+
if (isAlt !== this.wasAlternate) {
|
|
355
|
+
this.wasAlternate = isAlt;
|
|
356
|
+
const activeGrid = this.bufferSet.active.grid;
|
|
357
|
+
const activeCursor = this.bufferSet.active.cursor;
|
|
358
|
+
if (this.renderBridge) {
|
|
359
|
+
this.renderBridge.resize(activeGrid.cols, activeGrid.rows, activeGrid.getBuffer());
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
this.renderer.attach(this.canvas, activeGrid, activeCursor);
|
|
363
|
+
}
|
|
364
|
+
this.inputHandler.setGrid(activeGrid);
|
|
365
|
+
this.accessibilityManager?.setGrid(activeGrid, activeGrid.rows, activeGrid.cols);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
resize(cols, rows) {
|
|
369
|
+
if (this.disposed)
|
|
370
|
+
return;
|
|
371
|
+
// Guard against bad values
|
|
372
|
+
if (!Number.isFinite(cols) || !Number.isFinite(rows) || cols < 2 || rows < 1)
|
|
373
|
+
return;
|
|
374
|
+
const scrollback = this.bufferSet.maxScrollback;
|
|
375
|
+
const oldBufferSet = this.bufferSet;
|
|
376
|
+
const oldGrid = oldBufferSet.active.grid;
|
|
377
|
+
const oldCursor = oldBufferSet.active.cursor;
|
|
378
|
+
const oldCols = oldBufferSet.cols;
|
|
379
|
+
const oldRows = oldBufferSet.rows;
|
|
380
|
+
// Create new buffer set
|
|
381
|
+
this.bufferSet = new BufferSet(cols, rows, scrollback);
|
|
382
|
+
// Copy content from old grid to new grid.
|
|
383
|
+
// When rows shrink, keep the bottom portion (where the cursor is).
|
|
384
|
+
// When rows grow, content stays at the top.
|
|
385
|
+
const newGrid = this.bufferSet.active.grid;
|
|
386
|
+
const copyRows = Math.min(oldRows, rows);
|
|
387
|
+
const _copyCols = Math.min(oldCols, cols);
|
|
388
|
+
// Determine source start row: if cursor was below the new row count,
|
|
389
|
+
// shift content up so cursor remains visible.
|
|
390
|
+
let srcStartRow = 0;
|
|
391
|
+
if (oldCursor.row >= rows) {
|
|
392
|
+
srcStartRow = oldCursor.row - rows + 1;
|
|
393
|
+
}
|
|
394
|
+
for (let r = 0; r < copyRows; r++) {
|
|
395
|
+
const srcRow = srcStartRow + r;
|
|
396
|
+
if (srcRow >= oldRows)
|
|
397
|
+
break;
|
|
398
|
+
const rowData = oldGrid.copyRow(srcRow);
|
|
399
|
+
// If old cols > new cols, the row data is wider — pasteRow handles truncation
|
|
400
|
+
// If old cols < new cols, extra cells remain at default
|
|
401
|
+
newGrid.pasteRow(r, rowData);
|
|
402
|
+
}
|
|
403
|
+
// Adjust cursor position for the new dimensions
|
|
404
|
+
const newCursor = this.bufferSet.active.cursor;
|
|
405
|
+
newCursor.row = Math.max(0, Math.min(oldCursor.row - srcStartRow, rows - 1));
|
|
406
|
+
newCursor.col = Math.min(oldCursor.col, cols - 1);
|
|
407
|
+
newCursor.visible = oldCursor.visible;
|
|
408
|
+
newCursor.style = oldCursor.style;
|
|
409
|
+
// Copy scrollback
|
|
410
|
+
this.bufferSet.scrollback = oldBufferSet.scrollback;
|
|
411
|
+
newGrid.markAllDirty();
|
|
412
|
+
if (this.workerBridge) {
|
|
413
|
+
// Update the bridge's grid reference and notify the worker.
|
|
414
|
+
this.workerBridge.updateGrid(this.bufferSet.active.grid, this.bufferSet.active.cursor);
|
|
415
|
+
this.workerBridge.resize(cols, rows, scrollback);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
this.parser = new VTParser(this.bufferSet);
|
|
419
|
+
this.parser.setTitleChangeCallback((title) => {
|
|
420
|
+
this.onTitleChangeCallback?.(title);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (this.renderBridge) {
|
|
424
|
+
// Notify render worker of resize with new SAB
|
|
425
|
+
this.renderBridge.resize(cols, rows, this.bufferSet.active.grid.getBuffer());
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
// Re-attach renderer with new grid
|
|
429
|
+
this.renderer.attach(this.canvas, this.bufferSet.active.grid, this.bufferSet.active.cursor);
|
|
430
|
+
this.renderer.resize(cols, rows);
|
|
431
|
+
}
|
|
432
|
+
// Update input handler's grid reference
|
|
433
|
+
this.inputHandler.setGrid(this.bufferSet.active.grid);
|
|
434
|
+
// Update accessibility manager with new grid
|
|
435
|
+
if (this.accessibilityManager) {
|
|
436
|
+
this.accessibilityManager.setGrid(this.bufferSet.active.grid, rows, cols);
|
|
437
|
+
}
|
|
438
|
+
this.onResizeCallback?.({ cols, rows });
|
|
439
|
+
}
|
|
440
|
+
/** Fit the terminal to its container. */
|
|
441
|
+
fit() {
|
|
442
|
+
if (this.disposed)
|
|
443
|
+
return;
|
|
444
|
+
const { width, height } = this.renderer.getCellSize();
|
|
445
|
+
if (width <= 0 || height <= 0)
|
|
446
|
+
return;
|
|
447
|
+
const { cols, rows } = calculateFit(this.container, width, height);
|
|
448
|
+
if (cols !== this.bufferSet.cols || rows !== this.bufferSet.rows) {
|
|
449
|
+
this.resize(cols, rows);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
focus() {
|
|
453
|
+
this.inputHandler.focus();
|
|
454
|
+
}
|
|
455
|
+
blur() {
|
|
456
|
+
this.inputHandler.blur();
|
|
457
|
+
}
|
|
458
|
+
setTheme(theme) {
|
|
459
|
+
const merged = mergeTheme(theme);
|
|
460
|
+
if (this.renderBridge) {
|
|
461
|
+
this.renderBridge.setTheme(merged);
|
|
462
|
+
}
|
|
463
|
+
this.renderer.setTheme(merged);
|
|
464
|
+
}
|
|
465
|
+
setFont(fontSize, fontFamily) {
|
|
466
|
+
if (this.renderBridge) {
|
|
467
|
+
this.renderBridge.setFont(fontSize, fontFamily);
|
|
468
|
+
}
|
|
469
|
+
if (this.renderer.setFont) {
|
|
470
|
+
this.renderer.setFont(fontSize, fontFamily);
|
|
471
|
+
}
|
|
472
|
+
const { width, height } = this.renderer.getCellSize();
|
|
473
|
+
this.inputHandler.updateCellSize(width, height);
|
|
474
|
+
this.inputHandler.setFontSize(fontSize);
|
|
475
|
+
}
|
|
476
|
+
getCellSize() {
|
|
477
|
+
return this.renderer.getCellSize();
|
|
478
|
+
}
|
|
479
|
+
onData(callback) {
|
|
480
|
+
this.onDataCallback = callback;
|
|
481
|
+
}
|
|
482
|
+
onResize(callback) {
|
|
483
|
+
this.onResizeCallback = callback;
|
|
484
|
+
}
|
|
485
|
+
/** Load an addon into this terminal. */
|
|
486
|
+
loadAddon(addon) {
|
|
487
|
+
addon.activate(this);
|
|
488
|
+
this.addons.push(addon);
|
|
489
|
+
}
|
|
490
|
+
/** Set highlight ranges on the renderer (used by SearchAddon). */
|
|
491
|
+
setHighlights(highlights) {
|
|
492
|
+
this.renderer.setHighlights(highlights);
|
|
493
|
+
}
|
|
494
|
+
// -----------------------------------------------------------------------
|
|
495
|
+
// Scrollback viewport
|
|
496
|
+
// -----------------------------------------------------------------------
|
|
497
|
+
createScrollbar(container) {
|
|
498
|
+
const bar = document.createElement("div");
|
|
499
|
+
Object.assign(bar.style, {
|
|
500
|
+
position: "absolute",
|
|
501
|
+
right: "0",
|
|
502
|
+
top: "0",
|
|
503
|
+
bottom: "0",
|
|
504
|
+
width: "6px",
|
|
505
|
+
zIndex: "10",
|
|
506
|
+
opacity: "0",
|
|
507
|
+
transition: "opacity 0.3s",
|
|
508
|
+
pointerEvents: "none",
|
|
509
|
+
});
|
|
510
|
+
const thumb = document.createElement("div");
|
|
511
|
+
Object.assign(thumb.style, {
|
|
512
|
+
position: "absolute",
|
|
513
|
+
right: "1px",
|
|
514
|
+
width: "4px",
|
|
515
|
+
borderRadius: "2px",
|
|
516
|
+
backgroundColor: "rgba(255, 255, 255, 0.4)",
|
|
517
|
+
minHeight: "20px",
|
|
518
|
+
});
|
|
519
|
+
bar.appendChild(thumb);
|
|
520
|
+
container.appendChild(bar);
|
|
521
|
+
this.scrollbarEl = bar;
|
|
522
|
+
this.scrollbarThumb = thumb;
|
|
523
|
+
}
|
|
524
|
+
updateScrollbar() {
|
|
525
|
+
if (!this.scrollbarEl || !this.scrollbarThumb)
|
|
526
|
+
return;
|
|
527
|
+
const totalLines = this.bufferSet.scrollback.length + this.bufferSet.rows;
|
|
528
|
+
const visibleRows = this.bufferSet.rows;
|
|
529
|
+
if (totalLines <= visibleRows || this.viewportOffset === 0) {
|
|
530
|
+
// At bottom or no scrollback — hide
|
|
531
|
+
this.scrollbarEl.style.opacity = "0";
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
// Show scrollbar
|
|
535
|
+
this.scrollbarEl.style.opacity = "1";
|
|
536
|
+
// Calculate thumb size and position
|
|
537
|
+
const containerHeight = this.scrollbarEl.clientHeight || visibleRows * this.renderer.getCellSize().height;
|
|
538
|
+
const thumbHeight = Math.max(20, (visibleRows / totalLines) * containerHeight);
|
|
539
|
+
const maxScroll = this.bufferSet.scrollback.length;
|
|
540
|
+
const scrollFraction = (maxScroll - this.viewportOffset) / maxScroll;
|
|
541
|
+
const thumbTop = scrollFraction * (containerHeight - thumbHeight);
|
|
542
|
+
this.scrollbarThumb.style.height = `${thumbHeight}px`;
|
|
543
|
+
this.scrollbarThumb.style.top = `${thumbTop}px`;
|
|
544
|
+
// Auto-hide after 1.5s
|
|
545
|
+
if (this.scrollbarHideTimer)
|
|
546
|
+
clearTimeout(this.scrollbarHideTimer);
|
|
547
|
+
this.scrollbarHideTimer = setTimeout(() => {
|
|
548
|
+
if (this.scrollbarEl)
|
|
549
|
+
this.scrollbarEl.style.opacity = "0";
|
|
550
|
+
}, 1500);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Scroll the viewport into scrollback. deltaLines > 0 scrolls back (older),
|
|
554
|
+
* deltaLines < 0 scrolls forward (newer).
|
|
555
|
+
*/
|
|
556
|
+
scrollViewport(deltaLines) {
|
|
557
|
+
const maxOffset = this.bufferSet.scrollback.length;
|
|
558
|
+
const newOffset = Math.max(0, Math.min(maxOffset, this.viewportOffset + deltaLines));
|
|
559
|
+
if (newOffset === this.viewportOffset)
|
|
560
|
+
return;
|
|
561
|
+
this.viewportOffset = newOffset;
|
|
562
|
+
if (newOffset === 0) {
|
|
563
|
+
// Back to live view — re-attach live grid
|
|
564
|
+
if (this.displayGrid) {
|
|
565
|
+
this.displayGrid = null;
|
|
566
|
+
if (!this.renderBridge) {
|
|
567
|
+
this.renderer.attach(this.canvas, this.bufferSet.active.grid, this.bufferSet.active.cursor);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
// Scrolled back — build display grid from scrollback + buffer
|
|
573
|
+
this.buildDisplayGrid();
|
|
574
|
+
}
|
|
575
|
+
this.updateScrollbar();
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Build a display grid showing the correct mix of scrollback and buffer
|
|
579
|
+
* lines for the current viewportOffset.
|
|
580
|
+
*/
|
|
581
|
+
buildDisplayGrid() {
|
|
582
|
+
const cols = this.bufferSet.cols;
|
|
583
|
+
const rows = this.bufferSet.rows;
|
|
584
|
+
const scrollback = this.bufferSet.scrollback;
|
|
585
|
+
// Reuse display grid if dimensions match, otherwise create new.
|
|
586
|
+
// Only call renderer.attach() when the grid is first created —
|
|
587
|
+
// subsequent updates just populate data and mark dirty.
|
|
588
|
+
let needsAttach = false;
|
|
589
|
+
if (!this.displayGrid || this.displayGrid.cols !== cols || this.displayGrid.rows !== rows) {
|
|
590
|
+
this.displayGrid = new CellGrid(cols, rows);
|
|
591
|
+
needsAttach = true;
|
|
592
|
+
}
|
|
593
|
+
// Virtual line numbering:
|
|
594
|
+
// [0 .. scrollback.length-1] = scrollback lines
|
|
595
|
+
// [scrollback.length .. scrollback.length+rows-1] = live buffer rows
|
|
596
|
+
// viewportTop = scrollback.length - viewportOffset
|
|
597
|
+
const viewportTop = scrollback.length - this.viewportOffset;
|
|
598
|
+
for (let r = 0; r < rows; r++) {
|
|
599
|
+
const virtualLine = viewportTop + r;
|
|
600
|
+
if (virtualLine < 0) {
|
|
601
|
+
// Before scrollback — show empty
|
|
602
|
+
this.displayGrid.clearRow(r);
|
|
603
|
+
}
|
|
604
|
+
else if (virtualLine < scrollback.length) {
|
|
605
|
+
// From scrollback
|
|
606
|
+
this.displayGrid.pasteRow(r, scrollback[virtualLine]);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
// From live buffer
|
|
610
|
+
const bufRow = virtualLine - scrollback.length;
|
|
611
|
+
if (bufRow < rows) {
|
|
612
|
+
const rowData = this.bufferSet.active.grid.copyRow(bufRow);
|
|
613
|
+
this.displayGrid.pasteRow(r, rowData);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
this.displayGrid.clearRow(r);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (!this.renderBridge) {
|
|
621
|
+
if (needsAttach) {
|
|
622
|
+
// Create a fake cursor (hidden) when scrolled back
|
|
623
|
+
const fakeCursor = {
|
|
624
|
+
row: 0,
|
|
625
|
+
col: 0,
|
|
626
|
+
visible: false,
|
|
627
|
+
style: "block",
|
|
628
|
+
wrapPending: false,
|
|
629
|
+
};
|
|
630
|
+
this.renderer.attach(this.canvas, this.displayGrid, fakeCursor);
|
|
631
|
+
}
|
|
632
|
+
// markAllDirty is called by pasteRow/clearRow, but ensure full redraw
|
|
633
|
+
this.displayGrid.markAllDirty();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/** Snap viewport to live (bottom) — called when new data arrives or user types. */
|
|
637
|
+
snapToBottom() {
|
|
638
|
+
if (this.viewportOffset === 0)
|
|
639
|
+
return;
|
|
640
|
+
this.viewportOffset = 0;
|
|
641
|
+
this.displayGrid = null;
|
|
642
|
+
if (!this.renderBridge) {
|
|
643
|
+
this.renderer.attach(this.canvas, this.bufferSet.active.grid, this.bufferSet.active.cursor);
|
|
644
|
+
}
|
|
645
|
+
this.updateScrollbar();
|
|
646
|
+
}
|
|
647
|
+
dispose() {
|
|
648
|
+
if (this.disposed)
|
|
649
|
+
return;
|
|
650
|
+
this.disposed = true;
|
|
651
|
+
if (this.scrollbarHideTimer)
|
|
652
|
+
clearTimeout(this.scrollbarHideTimer);
|
|
653
|
+
for (const addon of this.addons)
|
|
654
|
+
addon.dispose();
|
|
655
|
+
this.addons = [];
|
|
656
|
+
if (this.workerBridge) {
|
|
657
|
+
this.workerBridge.dispose();
|
|
658
|
+
this.workerBridge = null;
|
|
659
|
+
}
|
|
660
|
+
if (this.renderBridge) {
|
|
661
|
+
this.renderBridge.dispose();
|
|
662
|
+
this.renderBridge = null;
|
|
663
|
+
}
|
|
664
|
+
if (this.accessibilityManager) {
|
|
665
|
+
this.accessibilityManager.dispose();
|
|
666
|
+
this.accessibilityManager = null;
|
|
667
|
+
}
|
|
668
|
+
this.renderer.dispose();
|
|
669
|
+
this.inputHandler.dispose();
|
|
670
|
+
if (this.canvas.parentElement) {
|
|
671
|
+
this.canvas.parentElement.removeChild(this.canvas);
|
|
672
|
+
}
|
|
673
|
+
if (this.scrollbarEl?.parentElement) {
|
|
674
|
+
this.scrollbarEl.parentElement.removeChild(this.scrollbarEl);
|
|
675
|
+
}
|
|
676
|
+
this.scrollbarEl = null;
|
|
677
|
+
this.scrollbarThumb = null;
|
|
678
|
+
this.displayGrid = null;
|
|
679
|
+
this.onDataCallback = null;
|
|
680
|
+
this.onResizeCallback = null;
|
|
681
|
+
this.onTitleChangeCallback = null;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
//# sourceMappingURL=web-terminal.js.map
|