@hex1b/web-terminal 0.1.0 → 0.166.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 +314 -11
- package/dist/backend-selection.d.ts +7 -0
- package/dist/backend-selection.d.ts.map +1 -0
- package/dist/backend-selection.js +23 -0
- package/dist/backend-selection.js.map +1 -0
- package/dist/hyperlinks.d.ts +10 -0
- package/dist/hyperlinks.d.ts.map +1 -0
- package/dist/hyperlinks.js +28 -0
- package/dist/hyperlinks.js.map +1 -0
- package/dist/mouse-input.d.ts +3 -0
- package/dist/mouse-input.d.ts.map +1 -1
- package/dist/mouse-input.js +69 -2
- package/dist/mouse-input.js.map +1 -1
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +40 -0
- package/dist/protocol.js.map +1 -1
- package/dist/render-backend.d.ts +31 -0
- package/dist/render-backend.d.ts.map +1 -0
- package/dist/render-backend.js +5 -0
- package/dist/render-backend.js.map +1 -0
- package/dist/renderer-options.d.ts +3 -0
- package/dist/renderer-options.d.ts.map +1 -0
- package/dist/renderer-options.js +6 -0
- package/dist/renderer-options.js.map +1 -0
- package/dist/renderer.d.ts +12 -24
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +35 -173
- package/dist/renderer.js.map +1 -1
- package/dist/terminal-worker.js +18 -10
- package/dist/terminal-worker.js.map +1 -1
- package/dist/types.d.ts +77 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/web-terminal.d.ts +8 -1
- package/dist/web-terminal.d.ts.map +1 -1
- package/dist/web-terminal.js +130 -17
- package/dist/web-terminal.js.map +1 -1
- package/dist/webgl2-backend.d.ts +38 -0
- package/dist/webgl2-backend.d.ts.map +1 -0
- package/dist/webgl2-backend.js +456 -0
- package/dist/webgl2-backend.js.map +1 -0
- package/dist/webgpu-backend.d.ts +24 -0
- package/dist/webgpu-backend.d.ts.map +1 -0
- package/dist/webgpu-backend.js +234 -0
- package/dist/webgpu-backend.js.map +1 -0
- package/dist/wire-types.d.ts +20 -2
- package/dist/wire-types.d.ts.map +1 -1
- package/dist/wire-types.js.map +1 -1
- package/package.json +2 -2
package/dist/web-terminal.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { captureMouse } from "./mouse-input.js";
|
|
2
2
|
import { normalizeFont } from "./terminal-font.js";
|
|
3
|
+
import { normalizeRenderer } from "./renderer-options.js";
|
|
3
4
|
import { dimensions, normalizeSizing, requestedGrid, fittedScale } from "./terminal-sizing.js";
|
|
4
5
|
import { HistoryState } from "./history-state.js";
|
|
5
6
|
import { terminalThemeCss } from "./terminal-theme.js";
|
|
6
7
|
import { InputPolicy, InputRoute, TerminalAction, inputModifiers } from "./input-policy.js";
|
|
7
8
|
import { assertCommandSize } from "./protocol.js";
|
|
8
9
|
import { SelectionUI } from "./selection-ui.js";
|
|
10
|
+
import { Hyperlinks } from "./hyperlinks.js";
|
|
9
11
|
import { errorMessage, isRecord } from "./validation.js";
|
|
10
12
|
export { InputRoute, TerminalAction, defaultInputBindings } from "./input-policy.js";
|
|
11
13
|
function requiredElement(root, selector, type) {
|
|
@@ -21,6 +23,7 @@ function requiredElement(root, selector, type) {
|
|
|
21
23
|
export class WebTerminal {
|
|
22
24
|
element;
|
|
23
25
|
#options;
|
|
26
|
+
#renderer;
|
|
24
27
|
// DOM and worker fields are initialized by mount before a handle is returned.
|
|
25
28
|
#worker;
|
|
26
29
|
#surface;
|
|
@@ -34,15 +37,23 @@ export class WebTerminal {
|
|
|
34
37
|
#geometry = { columns: 80, rows: 24, cellWidth: 10, cellHeight: 20, mouseTracking: 0 };
|
|
35
38
|
#peer = { id: null, primaryId: null, isPrimary: false };
|
|
36
39
|
#connected = false;
|
|
40
|
+
#closed = false;
|
|
41
|
+
#readOnly;
|
|
37
42
|
#disposed = false;
|
|
38
43
|
#hasGeometry = false;
|
|
39
44
|
#resizeTimer;
|
|
40
45
|
#lastRequested;
|
|
41
46
|
#compositionTimer;
|
|
47
|
+
#resetComposition;
|
|
42
48
|
#ready = Promise.withResolvers();
|
|
43
49
|
#readyTimer;
|
|
44
50
|
#stats = {};
|
|
45
51
|
#screenText = "";
|
|
52
|
+
#title = "";
|
|
53
|
+
#hasTitle = false;
|
|
54
|
+
#progress = { state: "none", percentage: null };
|
|
55
|
+
#shellIntegration = { phase: "unknown", lastExitCode: null };
|
|
56
|
+
#hasActivity = false;
|
|
46
57
|
#history;
|
|
47
58
|
#highlights;
|
|
48
59
|
#inspection;
|
|
@@ -57,6 +68,7 @@ export class WebTerminal {
|
|
|
57
68
|
#selectionOverlay;
|
|
58
69
|
#selectionUIError = "";
|
|
59
70
|
#canvasSize = { width: 0, height: 0 };
|
|
71
|
+
#hyperlinks = new Hyperlinks();
|
|
60
72
|
/** Resolves after a connected terminal frame is presented. Supply signal to cancel mounting. */
|
|
61
73
|
static async mount(container, options) {
|
|
62
74
|
if (!(container instanceof HTMLElement))
|
|
@@ -65,8 +77,9 @@ export class WebTerminal {
|
|
|
65
77
|
throw new TypeError("A terminal WebSocket URL is required");
|
|
66
78
|
if (options.signal?.aborted)
|
|
67
79
|
throw options.signal.reason;
|
|
68
|
-
if (!window.isSecureContext || !navigator.gpu)
|
|
69
|
-
throw new Error("
|
|
80
|
+
if (normalizeRenderer(options.renderer) === "webgpu" && (!window.isSecureContext || !navigator.gpu)) {
|
|
81
|
+
throw new Error("The requested WebGPU renderer requires WebGPU over HTTPS or localhost");
|
|
82
|
+
}
|
|
70
83
|
if (!window.Worker || !window.ResizeObserver || !window.OffscreenCanvas ||
|
|
71
84
|
!HTMLCanvasElement.prototype.transferControlToOffscreen) {
|
|
72
85
|
throw new Error("WebTerminal requires module workers, ResizeObserver, and a transferable OffscreenCanvas");
|
|
@@ -83,6 +96,10 @@ export class WebTerminal {
|
|
|
83
96
|
}
|
|
84
97
|
constructor(options) {
|
|
85
98
|
this.#options = options;
|
|
99
|
+
if (options.readOnly !== undefined && typeof options.readOnly !== "boolean")
|
|
100
|
+
throw new TypeError("readOnly must be a boolean");
|
|
101
|
+
this.#readOnly = options.readOnly ?? false;
|
|
102
|
+
this.#renderer = normalizeRenderer(options.renderer);
|
|
86
103
|
if (options.workerUrl !== undefined && !(options.workerUrl instanceof URL) &&
|
|
87
104
|
(typeof options.workerUrl !== "string" || !options.workerUrl.trim()))
|
|
88
105
|
throw new TypeError("workerUrl must be a nonempty URL string or URL");
|
|
@@ -100,6 +117,11 @@ export class WebTerminal {
|
|
|
100
117
|
get geometry() { return { ...this.#geometry }; }
|
|
101
118
|
get peer() { return { ...this.#peer }; }
|
|
102
119
|
get connected() { return this.#connected; }
|
|
120
|
+
get readOnly() { return this.#readOnly; }
|
|
121
|
+
/** Current presented workload title; retained on disconnect/dispose. Treat as untrusted text. */
|
|
122
|
+
get title() { return this.#title; }
|
|
123
|
+
get progress() { return { ...this.#progress }; }
|
|
124
|
+
get shellIntegration() { return { ...this.#shellIntegration }; }
|
|
103
125
|
get stats() { return { ...this.#stats }; }
|
|
104
126
|
get screenText() { return this.#screenText; }
|
|
105
127
|
get sizing() { return { ...this.#sizing }; }
|
|
@@ -176,7 +198,7 @@ export class WebTerminal {
|
|
|
176
198
|
element: this.element, overlay: this.#selectionOverlay,
|
|
177
199
|
button: requiredElement(this.#inspection, ".copy-selection", HTMLButtonElement), signal: this.#listeners.signal,
|
|
178
200
|
getState: () => ({ selection: this.selection, viewport: this.viewport, geometry: this.geometry,
|
|
179
|
-
canvasSize: this.#canvasSize, connected: this.#connected, readOnly:
|
|
201
|
+
canvasSize: this.#canvasSize, connected: this.#connected, readOnly: this.#readOnly }),
|
|
180
202
|
runAction: this.runAction.bind(this),
|
|
181
203
|
onSelectionUI: this.#options.onSelectionUI,
|
|
182
204
|
reportError: error => {
|
|
@@ -202,14 +224,23 @@ export class WebTerminal {
|
|
|
202
224
|
}
|
|
203
225
|
};
|
|
204
226
|
this.#mouse = captureMouse(this.#canvas, command => this.#inputCommand(command), () => this.focus(), {
|
|
205
|
-
state: () => ({ historical: !this.viewport.following || this.viewport.pending, readOnly:
|
|
227
|
+
state: () => ({ historical: !this.viewport.following || this.viewport.pending, readOnly: this.#readOnly,
|
|
206
228
|
selection: this.selection }),
|
|
207
229
|
begin: (point, selection) => inspect(() => this.#history.begin(point, selection)),
|
|
208
230
|
extend: point => inspect(() => this.#history.extend(point)),
|
|
209
231
|
scroll: (delta, endpoint) => inspect(() => this.#history.scroll(delta, endpoint)),
|
|
210
232
|
end: cancelled => this.#history.endGesture(cancelled),
|
|
211
233
|
resolve: input => this.#resolveInput(input),
|
|
212
|
-
execute: (decision, input) => this.#executeInputAction(decision, input)
|
|
234
|
+
execute: (decision, input) => this.#executeInputAction(decision, input),
|
|
235
|
+
hyperlink: point => this.#connected && !this.viewport.pending ? this.#hyperlinks.at(point) : null,
|
|
236
|
+
openHyperlink: uri => {
|
|
237
|
+
try {
|
|
238
|
+
window.open(uri, "_blank", "noopener,noreferrer");
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
this.#actionFailed(error);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
213
244
|
});
|
|
214
245
|
this.#bindKeyboard();
|
|
215
246
|
requiredElement(this.#inspection, ".return-live", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -250,7 +281,8 @@ export class WebTerminal {
|
|
|
250
281
|
});
|
|
251
282
|
this.#worker.addEventListener("messageerror", () => this.#fail(new Error("Terminal worker message could not be decoded")));
|
|
252
283
|
const canvas = this.#canvas.transferControlToOffscreen();
|
|
253
|
-
this.#post({ type: "init", canvas, url: url.href, scale, font
|
|
284
|
+
this.#post({ type: "init", canvas, url: url.href, scale, font,
|
|
285
|
+
renderer: this.#renderer }, [canvas]);
|
|
254
286
|
}
|
|
255
287
|
#message(message) {
|
|
256
288
|
if (this.#disposed)
|
|
@@ -259,8 +291,19 @@ export class WebTerminal {
|
|
|
259
291
|
this.#connected = true;
|
|
260
292
|
this.#input.disabled = !this.#canInput();
|
|
261
293
|
}
|
|
262
|
-
else if (message.type === "
|
|
263
|
-
this.#
|
|
294
|
+
else if (message.type === "closed") {
|
|
295
|
+
if (this.#closed)
|
|
296
|
+
return;
|
|
297
|
+
this.#closed = true;
|
|
298
|
+
clearTimeout(this.#readyTimer);
|
|
299
|
+
try {
|
|
300
|
+
this.#disconnect();
|
|
301
|
+
if (!this.#disposed)
|
|
302
|
+
this.#options.onClose?.(Object.freeze({ ...message.details }));
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
this.#ready.reject(new Error(`Terminal WebSocket closed (${message.details.code}${message.details.reason ? `: ${message.details.reason}` : ""}) before mounting completed`));
|
|
306
|
+
}
|
|
264
307
|
}
|
|
265
308
|
else if (message.type === "status") {
|
|
266
309
|
if (message.level === "error") {
|
|
@@ -283,7 +326,7 @@ export class WebTerminal {
|
|
|
283
326
|
this.#input.disabled = !this.#canInput();
|
|
284
327
|
if (this.#canInput() && document.activeElement === this.element && !this.element.shadowRoot?.activeElement)
|
|
285
328
|
this.focus();
|
|
286
|
-
this.#
|
|
329
|
+
this.#hyperlinks.update(message.hyperlinks);
|
|
287
330
|
if (geometryChanged || oldPeer.isPrimary !== this.#peer.isPrimary)
|
|
288
331
|
this.#fit();
|
|
289
332
|
if (!this.#peer.isPrimary) {
|
|
@@ -299,6 +342,7 @@ export class WebTerminal {
|
|
|
299
342
|
this.#screenText = message.text;
|
|
300
343
|
this.#history.accept(message.history, message.revision);
|
|
301
344
|
}
|
|
345
|
+
this.#mouse?.update(message.columns, message.rows, message.mouseTracking);
|
|
302
346
|
if (geometryChanged)
|
|
303
347
|
this.#options.onGeometry?.(this.geometry);
|
|
304
348
|
if (first)
|
|
@@ -306,6 +350,24 @@ export class WebTerminal {
|
|
|
306
350
|
if (oldPeer.id !== this.#peer.id || oldPeer.primaryId !== this.#peer.primaryId || oldPeer.isPrimary !== this.#peer.isPrimary) {
|
|
307
351
|
this.#options.onRoleChange?.(this.peer);
|
|
308
352
|
}
|
|
353
|
+
if (!this.#disposed && this.#connected && (this.#peer.id !== null || this.#peer.isPrimary)) {
|
|
354
|
+
const titleChanged = !this.#hasTitle || this.#title !== message.title;
|
|
355
|
+
const progressChanged = !this.#hasActivity || this.#progress.state !== message.progress.state ||
|
|
356
|
+
this.#progress.percentage !== message.progress.percentage;
|
|
357
|
+
const shellChanged = !this.#hasActivity || this.#shellIntegration.phase !== message.shellIntegration.phase ||
|
|
358
|
+
this.#shellIntegration.lastExitCode !== message.shellIntegration.lastExitCode;
|
|
359
|
+
this.#title = message.title;
|
|
360
|
+
this.#hasTitle = true;
|
|
361
|
+
this.#progress = { ...message.progress };
|
|
362
|
+
this.#shellIntegration = { ...message.shellIntegration };
|
|
363
|
+
this.#hasActivity = true;
|
|
364
|
+
if (titleChanged)
|
|
365
|
+
this.#options.onTitleChange?.(this.#title);
|
|
366
|
+
if (!this.#disposed && progressChanged)
|
|
367
|
+
this.#options.onProgressChange?.(this.progress);
|
|
368
|
+
if (!this.#disposed && shellChanged)
|
|
369
|
+
this.#options.onShellIntegrationChange?.(this.shellIntegration);
|
|
370
|
+
}
|
|
309
371
|
}
|
|
310
372
|
else if (message.type === "history") {
|
|
311
373
|
this.#screenText = message.text;
|
|
@@ -315,7 +377,8 @@ export class WebTerminal {
|
|
|
315
377
|
this.#stats = message.stats;
|
|
316
378
|
if (message.text !== undefined)
|
|
317
379
|
this.#screenText = message.text;
|
|
318
|
-
if (message.stats.revision > 0 && this.#
|
|
380
|
+
if (message.stats.revision > 0 && this.#hasTitle && this.#hasActivity && this.#connected &&
|
|
381
|
+
(this.#peer.id !== null || this.#peer.isPrimary)) {
|
|
319
382
|
clearTimeout(this.#readyTimer);
|
|
320
383
|
this.#ready.resolve(this);
|
|
321
384
|
}
|
|
@@ -341,13 +404,13 @@ export class WebTerminal {
|
|
|
341
404
|
#queueResize(includeFixed = false) {
|
|
342
405
|
if (this.#sizing.mode === "fixed" && !includeFixed)
|
|
343
406
|
return;
|
|
344
|
-
if (!this.#
|
|
407
|
+
if (!this.#canInput() || !this.#peer.isPrimary || this.#resizeTimer !== undefined || this.#disposed)
|
|
345
408
|
return;
|
|
346
409
|
// Throttle (rather than debounce) so dragging a primary view updates peers live.
|
|
347
410
|
this.#resizeTimer = setTimeout(() => {
|
|
348
411
|
this.#resizeTimer = undefined;
|
|
349
412
|
const grid = this.#fittedGrid();
|
|
350
|
-
if (!grid || !this.#peer.isPrimary || !this.#
|
|
413
|
+
if (!grid || !this.#peer.isPrimary || !this.#canInput())
|
|
351
414
|
return;
|
|
352
415
|
const key = `${grid.columns}x${grid.rows}`;
|
|
353
416
|
if ((grid.columns === this.#geometry.columns && grid.rows === this.#geometry.rows) || key === this.#lastRequested)
|
|
@@ -361,6 +424,8 @@ export class WebTerminal {
|
|
|
361
424
|
#send(command) {
|
|
362
425
|
if (!this.#connected || this.#disposed)
|
|
363
426
|
throw new Error("Terminal view is not connected");
|
|
427
|
+
if (this.#readOnly && ["input", "paste", "key", "mouse", "resize", "requestPrimary"].includes(command.type))
|
|
428
|
+
throw new Error("Terminal view does not accept input");
|
|
364
429
|
this.#post({ type: "command", command });
|
|
365
430
|
}
|
|
366
431
|
#inputCommand(command) {
|
|
@@ -380,6 +445,7 @@ export class WebTerminal {
|
|
|
380
445
|
#inspectionChanged() {
|
|
381
446
|
const viewport = this.viewport;
|
|
382
447
|
const selection = this.selection;
|
|
448
|
+
this.#mouse?.refresh();
|
|
383
449
|
if (selection.status === "invalidated")
|
|
384
450
|
this.#mouse?.cancel();
|
|
385
451
|
if (this.#highlights) {
|
|
@@ -466,20 +532,23 @@ export class WebTerminal {
|
|
|
466
532
|
}
|
|
467
533
|
}
|
|
468
534
|
#canInput() {
|
|
469
|
-
return this.#connected && this.#hasGeometry && !this.#
|
|
535
|
+
return this.#connected && this.#hasGeometry && !this.#readOnly &&
|
|
470
536
|
(this.#peer.id !== null || this.#peer.isPrimary);
|
|
471
537
|
}
|
|
472
538
|
get inputContext() {
|
|
473
539
|
return Object.freeze({
|
|
474
540
|
terminal: this, selection: this.selection, viewport: this.viewport,
|
|
475
|
-
buffer: this.viewport.buffer ?? null, mouseCaptured: this.#geometry.mouseTracking !== 0,
|
|
541
|
+
buffer: this.viewport.buffer ?? null, mouseCaptured: !this.#readOnly && this.#geometry.mouseTracking !== 0,
|
|
476
542
|
historical: !this.viewport.following || this.viewport.pending,
|
|
477
|
-
readOnly:
|
|
543
|
+
readOnly: this.#readOnly, connected: this.#connected, peer: this.peer
|
|
478
544
|
});
|
|
479
545
|
}
|
|
480
546
|
#resolveInput(input) {
|
|
481
547
|
try {
|
|
482
|
-
|
|
548
|
+
const decision = this.#policy.resolve(Object.freeze(input), this.inputContext);
|
|
549
|
+
if (this.#readOnly && decision.route === InputRoute.Application)
|
|
550
|
+
return { route: input.type === "pointer" || input.type === "wheel" ? InputRoute.Continue : InputRoute.Consume };
|
|
551
|
+
return decision;
|
|
483
552
|
}
|
|
484
553
|
catch (error) {
|
|
485
554
|
this.#actionFailed(error);
|
|
@@ -524,7 +593,7 @@ export class WebTerminal {
|
|
|
524
593
|
try {
|
|
525
594
|
if (this.selection.active || (this.selection.pending && this.selection.canExtend))
|
|
526
595
|
return await this.copySelection({ clear: true });
|
|
527
|
-
if (!this.#
|
|
596
|
+
if (!this.#readOnly)
|
|
528
597
|
return await this.pasteClipboard();
|
|
529
598
|
return;
|
|
530
599
|
}
|
|
@@ -559,6 +628,34 @@ export class WebTerminal {
|
|
|
559
628
|
this.paste(text);
|
|
560
629
|
return text;
|
|
561
630
|
}
|
|
631
|
+
/** Changes per-view input policy without reconnecting; server authorization remains host-owned. */
|
|
632
|
+
setReadOnly(readOnly) {
|
|
633
|
+
if (typeof readOnly !== "boolean")
|
|
634
|
+
throw new TypeError("readOnly must be a boolean");
|
|
635
|
+
if (this.#disposed)
|
|
636
|
+
throw new Error("Terminal view is disposed");
|
|
637
|
+
if (this.#readOnly === readOnly)
|
|
638
|
+
return;
|
|
639
|
+
const inputFocused = document.activeElement === this.element &&
|
|
640
|
+
(!this.element.shadowRoot?.activeElement || this.element.shadowRoot.activeElement === this.#input);
|
|
641
|
+
this.#readOnly = readOnly;
|
|
642
|
+
this.#inputSerial++;
|
|
643
|
+
this.#resetComposition?.();
|
|
644
|
+
if (this.#input) {
|
|
645
|
+
this.#input.value = "";
|
|
646
|
+
this.#input.disabled = !this.#canInput();
|
|
647
|
+
}
|
|
648
|
+
// Set the policy before cancelling so pending moves and button releases cannot leak.
|
|
649
|
+
this.#mouse?.cancel();
|
|
650
|
+
this.#mouse?.refresh();
|
|
651
|
+
clearTimeout(this.#resizeTimer);
|
|
652
|
+
this.#resizeTimer = undefined;
|
|
653
|
+
this.#lastRequested = undefined;
|
|
654
|
+
this.#queueResize(true);
|
|
655
|
+
if (inputFocused)
|
|
656
|
+
this.focus();
|
|
657
|
+
this.#selectionUI?.refresh();
|
|
658
|
+
}
|
|
562
659
|
#forwardInput(input) {
|
|
563
660
|
if (input.type === "key") {
|
|
564
661
|
if (input.meta)
|
|
@@ -599,6 +696,11 @@ export class WebTerminal {
|
|
|
599
696
|
const options = { signal: this.#listeners.signal };
|
|
600
697
|
let composing = false;
|
|
601
698
|
let compositionCommit = null;
|
|
699
|
+
this.#resetComposition = () => {
|
|
700
|
+
composing = false;
|
|
701
|
+
compositionCommit = null;
|
|
702
|
+
clearTimeout(this.#compositionTimer);
|
|
703
|
+
};
|
|
602
704
|
this.element.addEventListener("keydown", event => {
|
|
603
705
|
if (event.defaultPrevented)
|
|
604
706
|
return;
|
|
@@ -619,11 +721,15 @@ export class WebTerminal {
|
|
|
619
721
|
input.value = "";
|
|
620
722
|
}, options);
|
|
621
723
|
input.addEventListener("compositionstart", () => {
|
|
724
|
+
if (!this.#canInput())
|
|
725
|
+
return;
|
|
622
726
|
composing = true;
|
|
623
727
|
compositionCommit = null;
|
|
624
728
|
clearTimeout(this.#compositionTimer);
|
|
625
729
|
}, options);
|
|
626
730
|
input.addEventListener("compositionend", event => {
|
|
731
|
+
if (!composing)
|
|
732
|
+
return;
|
|
627
733
|
composing = false;
|
|
628
734
|
// Accommodate browsers placing the final input before or after compositionend.
|
|
629
735
|
compositionCommit = typeof event.data === "string" ? event.data : input.value;
|
|
@@ -654,6 +760,8 @@ export class WebTerminal {
|
|
|
654
760
|
}
|
|
655
761
|
/** Request HMP1 primary explicitly; peer notifications confirm the result. */
|
|
656
762
|
requestPrimary() {
|
|
763
|
+
if (!this.#canInput())
|
|
764
|
+
throw new Error("Terminal view does not accept input");
|
|
657
765
|
if (this.#size.width <= 0 || this.#size.height <= 0)
|
|
658
766
|
throw new Error("Show the terminal container before taking primary");
|
|
659
767
|
const grid = this.#fittedGrid();
|
|
@@ -670,6 +778,8 @@ export class WebTerminal {
|
|
|
670
778
|
/** Request a grid; never reflow locally before the authoritative response. */
|
|
671
779
|
resize(columns, rows) {
|
|
672
780
|
const grid = dimensions(columns, rows);
|
|
781
|
+
if (!this.#canInput())
|
|
782
|
+
throw new Error("Terminal view does not accept input");
|
|
673
783
|
if (!this.#peer.isPrimary)
|
|
674
784
|
throw new Error("Only the primary view can request a terminal resize");
|
|
675
785
|
this.#send({ type: "resize", ...grid });
|
|
@@ -680,6 +790,8 @@ export class WebTerminal {
|
|
|
680
790
|
const next = normalizeSizing(sizing, this.#sizing.fontSize);
|
|
681
791
|
if (!this.#connected || this.#disposed)
|
|
682
792
|
throw new Error("Terminal view is not connected");
|
|
793
|
+
if (this.#readOnly)
|
|
794
|
+
throw new Error("Terminal view does not accept input");
|
|
683
795
|
if (!this.#peer.isPrimary)
|
|
684
796
|
throw new Error("Only the primary view can change terminal sizing");
|
|
685
797
|
this.#sizing = next;
|
|
@@ -694,6 +806,7 @@ export class WebTerminal {
|
|
|
694
806
|
#disconnect() {
|
|
695
807
|
this.#inputSerial++;
|
|
696
808
|
this.#connected = false;
|
|
809
|
+
this.#hyperlinks.update([]);
|
|
697
810
|
if (this.#input)
|
|
698
811
|
this.#input.disabled = true;
|
|
699
812
|
this.#mouse?.update(1, 1, 0);
|