@hex1b/web-terminal 0.1.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/LICENSE +21 -0
- package/README.md +243 -0
- package/dist/fonts/cascadia-mono-nf/CascadiaMonoNF.woff2 +0 -0
- package/dist/fonts/cascadia-mono-nf/LICENSE.txt +94 -0
- package/dist/fonts/cascadia-mono-nf/README.md +29 -0
- package/dist/history-state.d.ts +27 -0
- package/dist/history-state.d.ts.map +1 -0
- package/dist/history-state.js +194 -0
- package/dist/history-state.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/input-policy.d.ts +42 -0
- package/dist/input-policy.d.ts.map +1 -0
- package/dist/input-policy.js +143 -0
- package/dist/input-policy.js.map +1 -0
- package/dist/mouse-input.d.ts +26 -0
- package/dist/mouse-input.d.ts.map +1 -0
- package/dist/mouse-input.js +356 -0
- package/dist/mouse-input.js.map +1 -0
- package/dist/protocol.d.ts +17 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +270 -0
- package/dist/protocol.js.map +1 -0
- package/dist/renderer.d.ts +113 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +595 -0
- package/dist/renderer.js.map +1 -0
- package/dist/selection-input.d.ts +36 -0
- package/dist/selection-input.d.ts.map +1 -0
- package/dist/selection-input.js +69 -0
- package/dist/selection-input.js.map +1 -0
- package/dist/selection-ui.d.ts +24 -0
- package/dist/selection-ui.d.ts.map +1 -0
- package/dist/selection-ui.js +113 -0
- package/dist/selection-ui.js.map +1 -0
- package/dist/terminal-font.d.ts +23 -0
- package/dist/terminal-font.d.ts.map +1 -0
- package/dist/terminal-font.js +85 -0
- package/dist/terminal-font.js.map +1 -0
- package/dist/terminal-sizing.d.ts +8 -0
- package/dist/terminal-sizing.d.ts.map +1 -0
- package/dist/terminal-sizing.js +38 -0
- package/dist/terminal-sizing.js.map +1 -0
- package/dist/terminal-theme.d.ts +2 -0
- package/dist/terminal-theme.d.ts.map +1 -0
- package/dist/terminal-theme.js +39 -0
- package/dist/terminal-theme.js.map +1 -0
- package/dist/terminal-worker.d.ts +2 -0
- package/dist/terminal-worker.d.ts.map +1 -0
- package/dist/terminal-worker.js +285 -0
- package/dist/terminal-worker.js.map +1 -0
- package/dist/types.d.ts +312 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +3 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +7 -0
- package/dist/validation.js.map +1 -0
- package/dist/web-terminal.d.ts +51 -0
- package/dist/web-terminal.d.ts.map +1 -0
- package/dist/web-terminal.js +728 -0
- package/dist/web-terminal.js.map +1 -0
- package/dist/wire-types.d.ts +218 -0
- package/dist/wire-types.d.ts.map +1 -0
- package/dist/wire-types.js +2 -0
- package/dist/wire-types.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const MIN_FONT_SIZE = 8;
|
|
2
|
+
export const MAX_FONT_SIZE = 32;
|
|
3
|
+
const NATIVE_FONT_SIZE = 16;
|
|
4
|
+
export function dimensions(columns, rows) {
|
|
5
|
+
if (!Number.isInteger(columns) || columns < 20 || columns > 300 ||
|
|
6
|
+
!Number.isInteger(rows) || rows < 10 || rows > 100) {
|
|
7
|
+
throw new RangeError("Requested grid must be 20-300 columns by 10-100 rows");
|
|
8
|
+
}
|
|
9
|
+
return { columns, rows };
|
|
10
|
+
}
|
|
11
|
+
export function normalizeSizing(sizing = { mode: "auto" }, previousFontSize = NATIVE_FONT_SIZE) {
|
|
12
|
+
if (!sizing || !["auto", "fixed"].includes(sizing.mode)) {
|
|
13
|
+
throw new TypeError("Sizing mode must be 'auto' or 'fixed'");
|
|
14
|
+
}
|
|
15
|
+
const fontSize = sizing.fontSize ?? previousFontSize;
|
|
16
|
+
if (!Number.isInteger(fontSize) || fontSize < MIN_FONT_SIZE || fontSize > MAX_FONT_SIZE) {
|
|
17
|
+
throw new RangeError(`Font size must be an integer from ${MIN_FONT_SIZE} to ${MAX_FONT_SIZE}`);
|
|
18
|
+
}
|
|
19
|
+
return sizing.mode === "fixed"
|
|
20
|
+
? { mode: "fixed", fontSize, ...dimensions(sizing.columns, sizing.rows) }
|
|
21
|
+
: { mode: "auto", fontSize };
|
|
22
|
+
}
|
|
23
|
+
export function requestedGrid(size, geometry, sizing) {
|
|
24
|
+
if (sizing.mode === "fixed")
|
|
25
|
+
return { columns: sizing.columns, rows: sizing.rows };
|
|
26
|
+
if (size.width <= 0 || size.height <= 0)
|
|
27
|
+
return null;
|
|
28
|
+
const scale = sizing.fontSize / NATIVE_FONT_SIZE;
|
|
29
|
+
return {
|
|
30
|
+
columns: Math.max(20, Math.min(300, Math.floor(size.width / (geometry.cellWidth * scale)))),
|
|
31
|
+
rows: Math.max(10, Math.min(100, Math.floor(size.height / (geometry.cellHeight * scale))))
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function fittedScale(size, geometry, isPrimary, sizing) {
|
|
35
|
+
const maximum = isPrimary && sizing.mode === "auto" ? sizing.fontSize / NATIVE_FONT_SIZE : Infinity;
|
|
36
|
+
return Math.max(0, Math.min(maximum, size.width / (geometry.columns * geometry.cellWidth), size.height / (geometry.rows * geometry.cellHeight)));
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=terminal-sizing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-sizing.js","sourceRoot":"","sources":["../src/terminal-sizing.ts"],"names":[],"mappings":"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","sourcesContent":["export const MIN_FONT_SIZE = 8;\nexport const MAX_FONT_SIZE = 32;\nconst NATIVE_FONT_SIZE = 16;\n\nexport function dimensions(columns: number, rows: number): TerminalGrid {\n if (!Number.isInteger(columns) || columns < 20 || columns > 300 ||\n !Number.isInteger(rows) || rows < 10 || rows > 100) {\n throw new RangeError(\"Requested grid must be 20-300 columns by 10-100 rows\");\n }\n return { columns, rows };\n}\n\nexport function normalizeSizing(sizing: TerminalSizing = { mode: \"auto\" }, previousFontSize = NATIVE_FONT_SIZE): TerminalSizingState {\n if (!sizing || ![\"auto\", \"fixed\"].includes(sizing.mode)) {\n throw new TypeError(\"Sizing mode must be 'auto' or 'fixed'\");\n }\n const fontSize = sizing.fontSize ?? previousFontSize;\n if (!Number.isInteger(fontSize) || fontSize < MIN_FONT_SIZE || fontSize > MAX_FONT_SIZE) {\n throw new RangeError(`Font size must be an integer from ${MIN_FONT_SIZE} to ${MAX_FONT_SIZE}`);\n }\n return sizing.mode === \"fixed\"\n ? { mode: \"fixed\", fontSize, ...dimensions(sizing.columns, sizing.rows) }\n : { mode: \"auto\", fontSize };\n}\n\nexport function requestedGrid(size: TerminalSize, geometry: TerminalGeometry, sizing: TerminalSizingState): TerminalGrid | null {\n if (sizing.mode === \"fixed\") return { columns: sizing.columns, rows: sizing.rows };\n if (size.width <= 0 || size.height <= 0) return null;\n const scale = sizing.fontSize / NATIVE_FONT_SIZE;\n return {\n columns: Math.max(20, Math.min(300, Math.floor(size.width / (geometry.cellWidth * scale)))),\n rows: Math.max(10, Math.min(100, Math.floor(size.height / (geometry.cellHeight * scale))))\n };\n}\n\nexport function fittedScale(size: TerminalSize, geometry: TerminalGeometry, isPrimary: boolean, sizing: TerminalSizingState): number {\n const maximum = isPrimary && sizing.mode === \"auto\" ? sizing.fontSize / NATIVE_FONT_SIZE : Infinity;\n return Math.max(0, Math.min(maximum,\n size.width / (geometry.columns * geometry.cellWidth),\n size.height / (geometry.rows * geometry.cellHeight)));\n}\nimport type { TerminalGeometry, TerminalGrid, TerminalSize, TerminalSizing, TerminalSizingState } from \"./types.js\";\n"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const terminalThemeCss = "\n :host {\n \n --cp-terminal-surface: #ffffff;\n --cp-terminal-text: #242424;\n --cp-terminal-text-muted: #5c5c5c;\n --cp-terminal-border-strong: #919191;\n --cp-terminal-accent: #b11f4b;\n --cp-terminal-accent-soft: rgba(177, 31, 75, 0.08);\n --cp-terminal-danger: #dc2626;\n\n --cp-terminal-font-family: \"Segoe UI\", Aptos, Calibri, -apple-system, BlinkMacSystemFont, sans-serif;\n }\n @media (prefers-color-scheme: dark) { :host { \n --cp-terminal-surface: #292929;\n --cp-terminal-text: #dedede;\n --cp-terminal-text-muted: #919191;\n --cp-terminal-border-strong: #5f5f5f;\n --cp-terminal-accent: #fd8ea1;\n --cp-terminal-accent-soft: rgba(253, 142, 161, 0.14);\n --cp-terminal-danger: #f87171;\n } }\n :host-context([data-theme=\"light\"]) { \n --cp-terminal-surface: #ffffff;\n --cp-terminal-text: #242424;\n --cp-terminal-text-muted: #5c5c5c;\n --cp-terminal-border-strong: #919191;\n --cp-terminal-accent: #b11f4b;\n --cp-terminal-accent-soft: rgba(177, 31, 75, 0.08);\n --cp-terminal-danger: #dc2626;\n }\n :host-context([data-theme=\"dark\"]) { \n --cp-terminal-surface: #292929;\n --cp-terminal-text: #dedede;\n --cp-terminal-text-muted: #919191;\n --cp-terminal-border-strong: #5f5f5f;\n --cp-terminal-accent: #fd8ea1;\n --cp-terminal-accent-soft: rgba(253, 142, 161, 0.14);\n --cp-terminal-danger: #f87171;\n }\n .viewport {\n --cp-view-surface: var(--cp-surface, var(--cp-terminal-surface));\n --cp-view-text: var(--cp-text, var(--cp-terminal-text));\n --cp-view-text-muted: var(--cp-text-muted, var(--cp-terminal-text-muted));\n --cp-view-border-strong: var(--cp-border-strong, var(--cp-terminal-border-strong));\n --cp-view-accent: var(--cp-accent, var(--cp-terminal-accent));\n --cp-view-accent-soft: var(--cp-accent-soft, var(--cp-terminal-accent-soft));\n --cp-view-danger: var(--cp-danger, var(--cp-terminal-danger));\n --cp-view-font-family: var(--cp-font-family, var(--cp-terminal-font-family));\n }\n";
|
|
2
|
+
//# sourceMappingURL=terminal-theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-theme.d.ts","sourceRoot":"","sources":["../src/terminal-theme.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,gBAAgB,28DAkB5B,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const light = `
|
|
2
|
+
--cp-terminal-surface: #ffffff;
|
|
3
|
+
--cp-terminal-text: #242424;
|
|
4
|
+
--cp-terminal-text-muted: #5c5c5c;
|
|
5
|
+
--cp-terminal-border-strong: #919191;
|
|
6
|
+
--cp-terminal-accent: #b11f4b;
|
|
7
|
+
--cp-terminal-accent-soft: rgba(177, 31, 75, 0.08);
|
|
8
|
+
--cp-terminal-danger: #dc2626;
|
|
9
|
+
`;
|
|
10
|
+
const dark = `
|
|
11
|
+
--cp-terminal-surface: #292929;
|
|
12
|
+
--cp-terminal-text: #dedede;
|
|
13
|
+
--cp-terminal-text-muted: #919191;
|
|
14
|
+
--cp-terminal-border-strong: #5f5f5f;
|
|
15
|
+
--cp-terminal-accent: #fd8ea1;
|
|
16
|
+
--cp-terminal-accent-soft: rgba(253, 142, 161, 0.14);
|
|
17
|
+
--cp-terminal-danger: #f87171;
|
|
18
|
+
`;
|
|
19
|
+
// Shared embedding tokens take precedence; defaults never overwrite inherited --cp-* colors.
|
|
20
|
+
export const terminalThemeCss = `
|
|
21
|
+
:host {
|
|
22
|
+
${light}
|
|
23
|
+
--cp-terminal-font-family: "Segoe UI", Aptos, Calibri, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
24
|
+
}
|
|
25
|
+
@media (prefers-color-scheme: dark) { :host { ${dark} } }
|
|
26
|
+
:host-context([data-theme="light"]) { ${light} }
|
|
27
|
+
:host-context([data-theme="dark"]) { ${dark} }
|
|
28
|
+
.viewport {
|
|
29
|
+
--cp-view-surface: var(--cp-surface, var(--cp-terminal-surface));
|
|
30
|
+
--cp-view-text: var(--cp-text, var(--cp-terminal-text));
|
|
31
|
+
--cp-view-text-muted: var(--cp-text-muted, var(--cp-terminal-text-muted));
|
|
32
|
+
--cp-view-border-strong: var(--cp-border-strong, var(--cp-terminal-border-strong));
|
|
33
|
+
--cp-view-accent: var(--cp-accent, var(--cp-terminal-accent));
|
|
34
|
+
--cp-view-accent-soft: var(--cp-accent-soft, var(--cp-terminal-accent-soft));
|
|
35
|
+
--cp-view-danger: var(--cp-danger, var(--cp-terminal-danger));
|
|
36
|
+
--cp-view-font-family: var(--cp-font-family, var(--cp-terminal-font-family));
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
//# sourceMappingURL=terminal-theme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-theme.js","sourceRoot":"","sources":["../src/terminal-theme.ts"],"names":[],"mappings":"AAAA,MAAM,KAAK,GAAG;;;;;;;;CAQb,CAAC;AAEF,MAAM,IAAI,GAAG;;;;;;;;CAQZ,CAAC;AAEF,6FAA6F;AAC7F,MAAM,CAAC,MAAM,gBAAgB,GAAG;;MAE1B,KAAK;;;kDAGuC,IAAI;0CACZ,KAAK;yCACN,IAAI;;;;;;;;;;;CAW5C,CAAC","sourcesContent":["const light = `\n --cp-terminal-surface: #ffffff;\n --cp-terminal-text: #242424;\n --cp-terminal-text-muted: #5c5c5c;\n --cp-terminal-border-strong: #919191;\n --cp-terminal-accent: #b11f4b;\n --cp-terminal-accent-soft: rgba(177, 31, 75, 0.08);\n --cp-terminal-danger: #dc2626;\n`;\n\nconst dark = `\n --cp-terminal-surface: #292929;\n --cp-terminal-text: #dedede;\n --cp-terminal-text-muted: #919191;\n --cp-terminal-border-strong: #5f5f5f;\n --cp-terminal-accent: #fd8ea1;\n --cp-terminal-accent-soft: rgba(253, 142, 161, 0.14);\n --cp-terminal-danger: #f87171;\n`;\n\n// Shared embedding tokens take precedence; defaults never overwrite inherited --cp-* colors.\nexport const terminalThemeCss = `\n :host {\n ${light}\n --cp-terminal-font-family: \"Segoe UI\", Aptos, Calibri, -apple-system, BlinkMacSystemFont, sans-serif;\n }\n @media (prefers-color-scheme: dark) { :host { ${dark} } }\n :host-context([data-theme=\"light\"]) { ${light} }\n :host-context([data-theme=\"dark\"]) { ${dark} }\n .viewport {\n --cp-view-surface: var(--cp-surface, var(--cp-terminal-surface));\n --cp-view-text: var(--cp-text, var(--cp-terminal-text));\n --cp-view-text-muted: var(--cp-text-muted, var(--cp-terminal-text-muted));\n --cp-view-border-strong: var(--cp-border-strong, var(--cp-terminal-border-strong));\n --cp-view-accent: var(--cp-accent, var(--cp-terminal-accent));\n --cp-view-accent-soft: var(--cp-accent-soft, var(--cp-terminal-accent-soft));\n --cp-view-danger: var(--cp-danger, var(--cp-terminal-danger));\n --cp-view-font-family: var(--cp-font-family, var(--cp-terminal-font-family));\n }\n`;\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-worker.d.ts","sourceRoot":"","sources":["../src/terminal-worker.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { decodeFrame, screenText } from "./protocol.js";
|
|
2
|
+
import { TerminalRenderer } from "./renderer.js";
|
|
3
|
+
import { errorMessage } from "./validation.js";
|
|
4
|
+
let renderer;
|
|
5
|
+
let socket;
|
|
6
|
+
let failed = false;
|
|
7
|
+
let stopped = false;
|
|
8
|
+
let processing = false;
|
|
9
|
+
let drawing = false;
|
|
10
|
+
let frameInFlight = false;
|
|
11
|
+
let scheduled = false;
|
|
12
|
+
let needsRender = false;
|
|
13
|
+
let hasBlink = false;
|
|
14
|
+
let lastBlink = true;
|
|
15
|
+
let metadata;
|
|
16
|
+
let cells = [];
|
|
17
|
+
let localRevision = 0;
|
|
18
|
+
let pendingFrame;
|
|
19
|
+
let renderPromise = Promise.resolve();
|
|
20
|
+
let metricsTimer;
|
|
21
|
+
let blinkTimer;
|
|
22
|
+
let viewport;
|
|
23
|
+
const stats = {
|
|
24
|
+
revision: 0, fullFrames: 0, frames: 0, presentations: 0,
|
|
25
|
+
changedCells: 0, lastChangedCells: 0, discardedFrames: 0,
|
|
26
|
+
imageCount: 0, textureBytes: 0, atlasGlyphs: 0, atlasBytes: 0,
|
|
27
|
+
bytesReceived: 0, imageUploadBytes: 0, imagePayloadBytes: 0,
|
|
28
|
+
gpu: "initializing", connected: false, warnings: [],
|
|
29
|
+
fps: 0, receivedKBps: 0, workloadMBps: 0,
|
|
30
|
+
captureMs: 0, rendererCpuMs: 0, preparationCpuMs: 0,
|
|
31
|
+
workloadBytes: 0, outputBatches: 0, serverElapsedMs: 0,
|
|
32
|
+
};
|
|
33
|
+
let sample = { time: performance.now(), presentations: 0, bytes: 0, workload: 0 };
|
|
34
|
+
function postStatus(message, level = "info") {
|
|
35
|
+
self.postMessage({ type: "status", message, level });
|
|
36
|
+
}
|
|
37
|
+
function send(message) {
|
|
38
|
+
if (socket?.readyState === WebSocket.OPEN)
|
|
39
|
+
socket.send(JSON.stringify(message));
|
|
40
|
+
}
|
|
41
|
+
function emitStats(text) {
|
|
42
|
+
if (renderer && !renderer.disposed)
|
|
43
|
+
Object.assign(stats, renderer.metrics());
|
|
44
|
+
self.postMessage({ type: "stats", stats: { ...stats }, ...(text === undefined ? {} : { text }) });
|
|
45
|
+
}
|
|
46
|
+
function fail(error) {
|
|
47
|
+
if (failed || stopped)
|
|
48
|
+
return;
|
|
49
|
+
failed = true;
|
|
50
|
+
const message = errorMessage(error);
|
|
51
|
+
stats.gpu = "error";
|
|
52
|
+
stats.connected = false;
|
|
53
|
+
clearInterval(metricsTimer);
|
|
54
|
+
clearInterval(blinkTimer);
|
|
55
|
+
socket?.close(1011, "Browser renderer failed");
|
|
56
|
+
emitStats();
|
|
57
|
+
postStatus(message, "error");
|
|
58
|
+
renderer?.dispose();
|
|
59
|
+
}
|
|
60
|
+
self.addEventListener("error", event => {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
fail(event.error || new Error(event.message));
|
|
63
|
+
});
|
|
64
|
+
self.addEventListener("unhandledrejection", event => {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
fail(event.reason);
|
|
67
|
+
});
|
|
68
|
+
/** At most one state frame, one decode, and one GPU submission are outstanding. */
|
|
69
|
+
function scheduleRender() {
|
|
70
|
+
needsRender = true;
|
|
71
|
+
if (scheduled || drawing || processing || failed || stopped || !metadata)
|
|
72
|
+
return;
|
|
73
|
+
scheduled = true;
|
|
74
|
+
self.requestAnimationFrame(() => {
|
|
75
|
+
scheduled = false;
|
|
76
|
+
if (processing || drawing || failed || stopped)
|
|
77
|
+
return;
|
|
78
|
+
renderPromise = drawFrame();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function drawFrame() {
|
|
82
|
+
if (!needsRender || !metadata || !renderer)
|
|
83
|
+
return;
|
|
84
|
+
needsRender = false;
|
|
85
|
+
drawing = true;
|
|
86
|
+
const frame = pendingFrame;
|
|
87
|
+
try {
|
|
88
|
+
renderer.resize(metadata.columns, metadata.rows, viewport);
|
|
89
|
+
const blink = Math.floor(performance.now() / 600) % 2 === 0;
|
|
90
|
+
const result = renderer.render(cells, metadata, blink);
|
|
91
|
+
// This is bounded completion/backpressure, not GPU readback or a GPU timing measurement.
|
|
92
|
+
await renderer.idle();
|
|
93
|
+
if (failed || stopped)
|
|
94
|
+
return;
|
|
95
|
+
lastBlink = blink;
|
|
96
|
+
stats.presentations++;
|
|
97
|
+
stats.rendererCpuMs = result.cpuMs;
|
|
98
|
+
stats.quads = result.quads;
|
|
99
|
+
stats.drawCalls = result.drawCalls;
|
|
100
|
+
stats.warnings = renderer.canvasLimited
|
|
101
|
+
? [...metadata.warnings, `Canvas resolution capped by the GPU; terminal grid remains ${metadata.columns}x${metadata.rows}`]
|
|
102
|
+
: metadata.warnings;
|
|
103
|
+
if (frame) {
|
|
104
|
+
pendingFrame = undefined;
|
|
105
|
+
frameInFlight = false;
|
|
106
|
+
stats.frames++;
|
|
107
|
+
if (frame.full)
|
|
108
|
+
stats.fullFrames++;
|
|
109
|
+
stats.revision = frame.revision;
|
|
110
|
+
stats.changedCells += frame.changedCells;
|
|
111
|
+
stats.lastChangedCells = frame.changedCells;
|
|
112
|
+
stats.captureMs = metadata.stats.captureMs;
|
|
113
|
+
stats.workloadBytes = metadata.stats.workloadBytes;
|
|
114
|
+
stats.outputBatches = metadata.stats.outputBatches;
|
|
115
|
+
stats.serverElapsedMs = metadata.stats.elapsedMs;
|
|
116
|
+
stats.columns = metadata.columns;
|
|
117
|
+
stats.rows = metadata.rows;
|
|
118
|
+
stats.mouseTracking = metadata.mouseTracking;
|
|
119
|
+
stats.peer = metadata.peer;
|
|
120
|
+
stats.history = metadata.history;
|
|
121
|
+
const text = screenText(cells, metadata.columns, metadata.rows);
|
|
122
|
+
self.postMessage({
|
|
123
|
+
type: "geometry", columns: metadata.columns, rows: metadata.rows,
|
|
124
|
+
cellWidth: metadata.cellWidth, cellHeight: metadata.cellHeight,
|
|
125
|
+
mouseTracking: metadata.mouseTracking, peer: metadata.peer,
|
|
126
|
+
history: metadata.history, revision: frame.revision, text
|
|
127
|
+
});
|
|
128
|
+
send({ type: "ack", revision: frame.revision });
|
|
129
|
+
emitStats(text);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
fail(error);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
drawing = false;
|
|
137
|
+
if (needsRender && !processing)
|
|
138
|
+
scheduleRender();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function receiveFrame(buffer) {
|
|
142
|
+
if (failed || stopped || !renderer)
|
|
143
|
+
return;
|
|
144
|
+
if (frameInFlight)
|
|
145
|
+
throw new Error("Server sent a second state frame before acknowledgement");
|
|
146
|
+
frameInFlight = true;
|
|
147
|
+
processing = true;
|
|
148
|
+
stats.bytesReceived += buffer.byteLength || 0;
|
|
149
|
+
try {
|
|
150
|
+
const frame = decodeFrame(buffer);
|
|
151
|
+
const next = frame.metadata;
|
|
152
|
+
if (!next.full && (next.baseRevision !== localRevision || next.revision <= localRevision ||
|
|
153
|
+
!metadata || next.columns !== metadata.columns || next.rows !== metadata.rows)) {
|
|
154
|
+
stats.discardedFrames++;
|
|
155
|
+
frameInFlight = false;
|
|
156
|
+
// A discarded frame must release the server's one-in-flight gate before resync.
|
|
157
|
+
send({ type: "ack", revision: next.revision });
|
|
158
|
+
send({ type: "resync" });
|
|
159
|
+
postStatus(`Revision mismatch (local ${localRevision}, base ${next.baseRevision}); requesting a full frame`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
await renderPromise;
|
|
163
|
+
if (failed || stopped)
|
|
164
|
+
return;
|
|
165
|
+
// No blink presentation may reference textures while this resource transaction is in progress.
|
|
166
|
+
await renderer.idle();
|
|
167
|
+
await renderer.updateImages(frame.images, next.retainedImages);
|
|
168
|
+
if (failed || stopped)
|
|
169
|
+
return;
|
|
170
|
+
const preparationStart = performance.now();
|
|
171
|
+
const nextCells = next.full
|
|
172
|
+
? new Array(next.columns * next.rows) : cells.slice();
|
|
173
|
+
for (const cell of frame.cells)
|
|
174
|
+
nextCells[cell.index] = cell;
|
|
175
|
+
renderer.prepareGlyphs(nextCells);
|
|
176
|
+
cells = nextCells;
|
|
177
|
+
metadata = next;
|
|
178
|
+
localRevision = next.revision;
|
|
179
|
+
pendingFrame = { revision: next.revision, full: next.full, changedCells: frame.cells.length };
|
|
180
|
+
hasBlink = cells.some(cell => cell && (cell.attributes & 16) && !(cell.attributes & 64)) ||
|
|
181
|
+
(next.cursor.visible && (next.cursor.shape === 0 || next.cursor.shape % 2 === 1));
|
|
182
|
+
stats.preparationCpuMs = performance.now() - preparationStart;
|
|
183
|
+
needsRender = true;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
processing = false;
|
|
187
|
+
if (needsRender)
|
|
188
|
+
scheduleRender();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function initialize(message) {
|
|
192
|
+
if (renderer || socket)
|
|
193
|
+
throw new Error("Worker is already initialized");
|
|
194
|
+
if (typeof self.requestAnimationFrame !== "function") {
|
|
195
|
+
throw new Error("This browser does not support requestAnimationFrame in a dedicated OffscreenCanvas worker");
|
|
196
|
+
}
|
|
197
|
+
postStatus("Loading terminal font and initializing WebGPU...");
|
|
198
|
+
renderer = await TerminalRenderer.create(message.canvas, message.scale, fail, message.font);
|
|
199
|
+
if (failed || stopped) {
|
|
200
|
+
renderer?.dispose();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
stats.gpu = "ready";
|
|
204
|
+
stats.backingScale = message.scale;
|
|
205
|
+
emitStats();
|
|
206
|
+
postStatus("WebGPU ready. Attaching terminal view...");
|
|
207
|
+
const url = new URL(message.url);
|
|
208
|
+
if (!["ws:", "wss:"].includes(url.protocol)) {
|
|
209
|
+
throw new Error("The terminal WebSocket URL must use ws: or wss:");
|
|
210
|
+
}
|
|
211
|
+
socket = new WebSocket(url);
|
|
212
|
+
socket.binaryType = "arraybuffer";
|
|
213
|
+
socket.addEventListener("open", () => {
|
|
214
|
+
if (failed || stopped)
|
|
215
|
+
return;
|
|
216
|
+
stats.connected = true;
|
|
217
|
+
self.postMessage({ type: "connected" });
|
|
218
|
+
postStatus("Connected · WebGPU worker · server-authoritative cells and graphics", "ready");
|
|
219
|
+
emitStats();
|
|
220
|
+
});
|
|
221
|
+
socket.addEventListener("message", event => {
|
|
222
|
+
if (!(event.data instanceof ArrayBuffer)) {
|
|
223
|
+
fail(new Error(`Expected binary HWT1 frame, received ${String(event.data).slice(0, 200)}`));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
receiveFrame(event.data).catch(fail);
|
|
227
|
+
});
|
|
228
|
+
socket.addEventListener("error", () => fail(new Error("WebSocket connection failed; verify the demo server is running")));
|
|
229
|
+
socket.addEventListener("close", event => {
|
|
230
|
+
stats.connected = false;
|
|
231
|
+
if (!failed && !stopped) {
|
|
232
|
+
stats.gpu = "stopped";
|
|
233
|
+
stats.fps = 0;
|
|
234
|
+
postStatus(`View disconnected (${event.code}${event.reason ? `: ${event.reason}` : ""}). Attach another view to reconnect.`, "error");
|
|
235
|
+
emitStats();
|
|
236
|
+
stopped = true;
|
|
237
|
+
clearInterval(metricsTimer);
|
|
238
|
+
clearInterval(blinkTimer);
|
|
239
|
+
renderer?.dispose();
|
|
240
|
+
self.postMessage({ type: "disconnected" });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
metricsTimer = setInterval(() => {
|
|
244
|
+
const now = performance.now();
|
|
245
|
+
const seconds = (now - sample.time) / 1000;
|
|
246
|
+
stats.fps = (stats.presentations - sample.presentations) / seconds;
|
|
247
|
+
stats.receivedKBps = (stats.bytesReceived - sample.bytes) / seconds / 1000;
|
|
248
|
+
stats.workloadMBps = Math.max(0, stats.workloadBytes - sample.workload) / seconds / 1000000;
|
|
249
|
+
sample = { time: now, presentations: stats.presentations, bytes: stats.bytesReceived, workload: stats.workloadBytes };
|
|
250
|
+
emitStats();
|
|
251
|
+
}, 1000);
|
|
252
|
+
blinkTimer = setInterval(() => {
|
|
253
|
+
const blinkOn = Math.floor(performance.now() / 600) % 2 === 0;
|
|
254
|
+
if (hasBlink && blinkOn !== lastBlink)
|
|
255
|
+
scheduleRender();
|
|
256
|
+
}, 100);
|
|
257
|
+
}
|
|
258
|
+
self.addEventListener("message", event => {
|
|
259
|
+
const message = event.data;
|
|
260
|
+
if (message.type === "init") {
|
|
261
|
+
initialize(message).catch(fail);
|
|
262
|
+
}
|
|
263
|
+
else if (message.type === "stop") {
|
|
264
|
+
stopped = true;
|
|
265
|
+
clearInterval(metricsTimer);
|
|
266
|
+
clearInterval(blinkTimer);
|
|
267
|
+
socket?.close(1000, "View detached");
|
|
268
|
+
renderer?.dispose();
|
|
269
|
+
self.close();
|
|
270
|
+
}
|
|
271
|
+
else if (message.type === "viewport" && !failed && !stopped) {
|
|
272
|
+
if (!Number.isFinite(message.width) || !Number.isFinite(message.height) || message.width < 0 || message.height < 0) {
|
|
273
|
+
fail(new Error("Invalid mounted viewport dimensions"));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (viewport?.width === message.width && viewport?.height === message.height)
|
|
277
|
+
return;
|
|
278
|
+
viewport = { width: message.width, height: message.height };
|
|
279
|
+
scheduleRender();
|
|
280
|
+
}
|
|
281
|
+
else if (message.type === "command" && !failed && !stopped) {
|
|
282
|
+
send(message.command);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
//# sourceMappingURL=terminal-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-worker.js","sourceRoot":"","sources":["../src/terminal-worker.ts"],"names":[],"mappings":"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","sourcesContent":["import { decodeFrame, screenText } from \"./protocol.js\";\nimport { TerminalRenderer } from \"./renderer.js\";\nimport type { TerminalSize, TerminalStatusLevel } from \"./types.js\";\nimport type { FrameMetadata, TerminalCell, TerminalCommand, WorkerInputMessage, WorkerOutputMessage, WorkerStats } from \"./wire-types.js\";\nimport { errorMessage } from \"./validation.js\";\n\n// This module is only executed as a dedicated worker. Keeping its global local to\n// this module avoids leaking worker/WebGPU ambient dependencies to public declarations.\ndeclare const self: {\n postMessage(message: WorkerOutputMessage): void;\n requestAnimationFrame(callback: FrameRequestCallback): number;\n close(): void;\n addEventListener(type: \"error\", callback: (event: ErrorEvent) => void): void;\n addEventListener(type: \"unhandledrejection\", callback: (event: PromiseRejectionEvent) => void): void;\n addEventListener(type: \"message\", callback: (event: MessageEvent<WorkerInputMessage>) => void): void;\n};\n\nlet renderer: TerminalRenderer | undefined;\nlet socket: WebSocket | undefined;\nlet failed = false;\nlet stopped = false;\nlet processing = false;\nlet drawing = false;\nlet frameInFlight = false;\nlet scheduled = false;\nlet needsRender = false;\nlet hasBlink = false;\nlet lastBlink = true;\nlet metadata: FrameMetadata | undefined;\nlet cells: (TerminalCell | undefined)[] = [];\nlet localRevision = 0;\nlet pendingFrame: { revision: number; full: boolean; changedCells: number } | undefined;\nlet renderPromise = Promise.resolve();\nlet metricsTimer: ReturnType<typeof setInterval> | undefined;\nlet blinkTimer: ReturnType<typeof setInterval> | undefined;\nlet viewport: TerminalSize | undefined;\nconst stats: WorkerStats = {\n revision: 0, fullFrames: 0, frames: 0, presentations: 0,\n changedCells: 0, lastChangedCells: 0, discardedFrames: 0,\n imageCount: 0, textureBytes: 0, atlasGlyphs: 0, atlasBytes: 0,\n bytesReceived: 0, imageUploadBytes: 0, imagePayloadBytes: 0,\n gpu: \"initializing\", connected: false, warnings: [],\n fps: 0, receivedKBps: 0, workloadMBps: 0,\n captureMs: 0, rendererCpuMs: 0, preparationCpuMs: 0,\n workloadBytes: 0, outputBatches: 0, serverElapsedMs: 0,\n};\nlet sample = { time: performance.now(), presentations: 0, bytes: 0, workload: 0 };\n\nfunction postStatus(message: string, level: TerminalStatusLevel = \"info\"): void {\n self.postMessage({ type: \"status\", message, level });\n}\n\nfunction send(message: TerminalCommand): void {\n if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message));\n}\n\nfunction emitStats(text?: string): void {\n if (renderer && !renderer.disposed) Object.assign(stats, renderer.metrics());\n self.postMessage({ type: \"stats\", stats: { ...stats }, ...(text === undefined ? {} : { text }) });\n}\n\nfunction fail(error: unknown): void {\n if (failed || stopped) return;\n failed = true;\n const message = errorMessage(error);\n stats.gpu = \"error\";\n stats.connected = false;\n clearInterval(metricsTimer);\n clearInterval(blinkTimer);\n socket?.close(1011, \"Browser renderer failed\");\n emitStats();\n postStatus(message, \"error\");\n renderer?.dispose();\n}\n\nself.addEventListener(\"error\", event => {\n event.preventDefault();\n fail(event.error || new Error(event.message));\n});\nself.addEventListener(\"unhandledrejection\", event => {\n event.preventDefault();\n fail(event.reason);\n});\n\n/** At most one state frame, one decode, and one GPU submission are outstanding. */\nfunction scheduleRender() {\n needsRender = true;\n if (scheduled || drawing || processing || failed || stopped || !metadata) return;\n scheduled = true;\n self.requestAnimationFrame(() => {\n scheduled = false;\n if (processing || drawing || failed || stopped) return;\n renderPromise = drawFrame();\n });\n}\n\nasync function drawFrame() {\n if (!needsRender || !metadata || !renderer) return;\n needsRender = false;\n drawing = true;\n const frame = pendingFrame;\n try {\n renderer.resize(metadata.columns, metadata.rows, viewport);\n const blink = Math.floor(performance.now() / 600) % 2 === 0;\n const result = renderer.render(cells, metadata, blink);\n // This is bounded completion/backpressure, not GPU readback or a GPU timing measurement.\n await renderer.idle();\n if (failed || stopped) return;\n lastBlink = blink;\n stats.presentations++;\n stats.rendererCpuMs = result.cpuMs;\n stats.quads = result.quads;\n stats.drawCalls = result.drawCalls;\n stats.warnings = renderer.canvasLimited\n ? [...metadata.warnings, `Canvas resolution capped by the GPU; terminal grid remains ${metadata.columns}x${metadata.rows}`]\n : metadata.warnings;\n if (frame) {\n pendingFrame = undefined;\n frameInFlight = false;\n stats.frames++;\n if (frame.full) stats.fullFrames++;\n stats.revision = frame.revision;\n stats.changedCells += frame.changedCells;\n stats.lastChangedCells = frame.changedCells;\n stats.captureMs = metadata.stats.captureMs;\n stats.workloadBytes = metadata.stats.workloadBytes;\n stats.outputBatches = metadata.stats.outputBatches;\n stats.serverElapsedMs = metadata.stats.elapsedMs;\n stats.columns = metadata.columns;\n stats.rows = metadata.rows;\n stats.mouseTracking = metadata.mouseTracking;\n stats.peer = metadata.peer;\n stats.history = metadata.history;\n const text = screenText(cells, metadata.columns, metadata.rows);\n self.postMessage({\n type: \"geometry\", columns: metadata.columns, rows: metadata.rows,\n cellWidth: metadata.cellWidth, cellHeight: metadata.cellHeight,\n mouseTracking: metadata.mouseTracking, peer: metadata.peer,\n history: metadata.history, revision: frame.revision, text\n });\n send({ type: \"ack\", revision: frame.revision });\n emitStats(text);\n }\n } catch (error) {\n fail(error);\n } finally {\n drawing = false;\n if (needsRender && !processing) scheduleRender();\n }\n}\n\nasync function receiveFrame(buffer: ArrayBuffer): Promise<void> {\n if (failed || stopped || !renderer) return;\n if (frameInFlight) throw new Error(\"Server sent a second state frame before acknowledgement\");\n frameInFlight = true;\n processing = true;\n stats.bytesReceived += buffer.byteLength || 0;\n try {\n const frame = decodeFrame(buffer);\n const next = frame.metadata;\n if (!next.full && (next.baseRevision !== localRevision || next.revision <= localRevision ||\n !metadata || next.columns !== metadata.columns || next.rows !== metadata.rows)) {\n stats.discardedFrames++;\n frameInFlight = false;\n // A discarded frame must release the server's one-in-flight gate before resync.\n send({ type: \"ack\", revision: next.revision });\n send({ type: \"resync\" });\n postStatus(`Revision mismatch (local ${localRevision}, base ${next.baseRevision}); requesting a full frame`);\n return;\n }\n await renderPromise;\n if (failed || stopped) return;\n // No blink presentation may reference textures while this resource transaction is in progress.\n await renderer.idle();\n await renderer.updateImages(frame.images, next.retainedImages);\n if (failed || stopped) return;\n const preparationStart = performance.now();\n const nextCells: (TerminalCell | undefined)[] = next.full\n ? new Array<TerminalCell | undefined>(next.columns * next.rows) : cells.slice();\n for (const cell of frame.cells) nextCells[cell.index] = cell;\n renderer.prepareGlyphs(nextCells);\n cells = nextCells;\n metadata = next;\n localRevision = next.revision;\n pendingFrame = { revision: next.revision, full: next.full, changedCells: frame.cells.length };\n hasBlink = cells.some(cell => cell && (cell.attributes & 16) && !(cell.attributes & 64)) ||\n (next.cursor.visible && (next.cursor.shape === 0 || next.cursor.shape % 2 === 1));\n stats.preparationCpuMs = performance.now() - preparationStart;\n needsRender = true;\n } finally {\n processing = false;\n if (needsRender) scheduleRender();\n }\n}\n\nasync function initialize(message: Extract<WorkerInputMessage, { type: \"init\" }>): Promise<void> {\n if (renderer || socket) throw new Error(\"Worker is already initialized\");\n if (typeof self.requestAnimationFrame !== \"function\") {\n throw new Error(\"This browser does not support requestAnimationFrame in a dedicated OffscreenCanvas worker\");\n }\n postStatus(\"Loading terminal font and initializing WebGPU...\");\n renderer = await TerminalRenderer.create(message.canvas, message.scale, fail, message.font);\n if (failed || stopped) {\n renderer?.dispose();\n return;\n }\n stats.gpu = \"ready\";\n stats.backingScale = message.scale;\n emitStats();\n postStatus(\"WebGPU ready. Attaching terminal view...\");\n const url = new URL(message.url);\n if (![\"ws:\", \"wss:\"].includes(url.protocol)) {\n throw new Error(\"The terminal WebSocket URL must use ws: or wss:\");\n }\n socket = new WebSocket(url);\n socket.binaryType = \"arraybuffer\";\n socket.addEventListener(\"open\", () => {\n if (failed || stopped) return;\n stats.connected = true;\n self.postMessage({ type: \"connected\" });\n postStatus(\"Connected · WebGPU worker · server-authoritative cells and graphics\", \"ready\");\n emitStats();\n });\n socket.addEventListener(\"message\", event => {\n if (!(event.data instanceof ArrayBuffer)) {\n fail(new Error(`Expected binary HWT1 frame, received ${String(event.data).slice(0, 200)}`));\n return;\n }\n receiveFrame(event.data).catch(fail);\n });\n socket.addEventListener(\"error\", () => fail(new Error(\"WebSocket connection failed; verify the demo server is running\")));\n socket.addEventListener(\"close\", event => {\n stats.connected = false;\n if (!failed && !stopped) {\n stats.gpu = \"stopped\";\n stats.fps = 0;\n postStatus(`View disconnected (${event.code}${event.reason ? `: ${event.reason}` : \"\"}). Attach another view to reconnect.`, \"error\");\n emitStats();\n stopped = true;\n clearInterval(metricsTimer);\n clearInterval(blinkTimer);\n renderer?.dispose();\n self.postMessage({ type: \"disconnected\" });\n }\n });\n metricsTimer = setInterval(() => {\n const now = performance.now();\n const seconds = (now - sample.time) / 1000;\n stats.fps = (stats.presentations - sample.presentations) / seconds;\n stats.receivedKBps = (stats.bytesReceived - sample.bytes) / seconds / 1000;\n stats.workloadMBps = Math.max(0, stats.workloadBytes - sample.workload) / seconds / 1000000;\n sample = { time: now, presentations: stats.presentations, bytes: stats.bytesReceived, workload: stats.workloadBytes };\n emitStats();\n }, 1000);\n blinkTimer = setInterval(() => {\n const blinkOn = Math.floor(performance.now() / 600) % 2 === 0;\n if (hasBlink && blinkOn !== lastBlink) scheduleRender();\n }, 100);\n}\n\nself.addEventListener(\"message\", event => {\n const message = event.data;\n if (message.type === \"init\") {\n initialize(message).catch(fail);\n } else if (message.type === \"stop\") {\n stopped = true;\n clearInterval(metricsTimer);\n clearInterval(blinkTimer);\n socket?.close(1000, \"View detached\");\n renderer?.dispose();\n self.close();\n } else if (message.type === \"viewport\" && !failed && !stopped) {\n if (!Number.isFinite(message.width) || !Number.isFinite(message.height) || message.width < 0 || message.height < 0) {\n fail(new Error(\"Invalid mounted viewport dimensions\"));\n return;\n }\n if (viewport?.width === message.width && viewport?.height === message.height) return;\n viewport = { width: message.width, height: message.height };\n scheduleRender();\n } else if (message.type === \"command\" && !failed && !stopped) {\n send(message.command);\n }\n});\n"]}
|