@hex1b/web-terminal 0.1.0 → 0.166.0-beta.1554.1.4652df1
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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hex1b/web-terminal
|
|
2
2
|
|
|
3
|
-
The first-party
|
|
3
|
+
The first-party GPU-rendered browser terminal for Hex1b. It renders server-authoritative
|
|
4
4
|
cells and graphics in a module worker, with local input routing, producer-backed
|
|
5
5
|
history and selection, clipboard actions, and primary/secondary view sizing.
|
|
6
6
|
There are no runtime package dependencies.
|
|
@@ -51,12 +51,89 @@ terminal.focus();
|
|
|
51
51
|
or disposes a mounted view. Disposal removes only the appended element and its
|
|
52
52
|
connection, not the container or server-side shared terminal.
|
|
53
53
|
|
|
54
|
+
### Connection closure and workload completion
|
|
55
|
+
|
|
56
|
+
Use `onClose(details)` to observe the browser's actual WebSocket close event.
|
|
57
|
+
`TerminalCloseDetails` contains readonly `code`, `reason`, and `wasClean` fields.
|
|
58
|
+
The callback runs once with the view already disconnected, **even if the socket
|
|
59
|
+
closes before the first HWT frame or authoritative HMP peer state**. A pending
|
|
60
|
+
mount rejects after the callback, so capture any host state before calling mount.
|
|
61
|
+
The details object is frozen. Reasons are untrusted text; do not render them as HTML.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { WebTerminal, type TerminalCloseDetails } from "@hex1b/web-terminal";
|
|
65
|
+
|
|
66
|
+
const container = document.getElementById("terminal");
|
|
67
|
+
if (!container) throw new Error("Missing terminal container");
|
|
68
|
+
let closed: TerminalCloseDetails | undefined;
|
|
69
|
+
try {
|
|
70
|
+
const terminal = await WebTerminal.mount(container, {
|
|
71
|
+
url: "/ws/terminal",
|
|
72
|
+
onClose(details) {
|
|
73
|
+
closed = details;
|
|
74
|
+
console.log("View closed", details.code, details.reason, details.wasClean);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
terminal.focus();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// closed is set for a transport close, but not for local initialization failure.
|
|
80
|
+
console.error("Mount failed", closed, error);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Transport loss is **not workload completion**. Code 1006 means the browser did not
|
|
85
|
+
receive a close frame; even code 1000 and `wasClean: true` only describe transport
|
|
86
|
+
closure, not successful process exit. Hosts can define an application close-code
|
|
87
|
+
contract and send it when their authoritative producer reports completion,
|
|
88
|
+
including before any HWT frame exists. Hex1b does not assign workload meaning to
|
|
89
|
+
close codes or reason strings. HTTP upgrade failures generally surface as 1006;
|
|
90
|
+
browsers do not expose the rejected HTTP response body or status through this API.
|
|
91
|
+
|
|
92
|
+
The client never retries automatically. The host decides whether to mount a new
|
|
93
|
+
view after transport loss or leave an ended tab/dialog visible. Abort, explicit
|
|
94
|
+
disposal, mount timeout, and local initialization/renderer failures do not
|
|
95
|
+
synthesize `onClose`; no callback runs after disposal. Callback exceptions are
|
|
96
|
+
reported to the host, not swallowed or retried, and do not leave mounting pending.
|
|
97
|
+
The API does not retain the producer after exit or promise a final rendered frame.
|
|
98
|
+
|
|
54
99
|
### Browser and deployment requirements
|
|
55
100
|
|
|
56
|
-
Use
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
101
|
+
Use a browser with WebGPU or WebGL2, module workers, transferable OffscreenCanvas,
|
|
102
|
+
worker animation frames, ResizeObserver, and CSS Font Loading. WebGPU requires
|
|
103
|
+
HTTPS or localhost; WebGL2 rendering also works on ordinary HTTP origins.
|
|
104
|
+
Clipboard API access still requires a secure context, browser permission and,
|
|
105
|
+
for relevant actions, a user gesture. There is no Canvas2D terminal-rendering fallback.
|
|
106
|
+
Renderer selection does not change transport security: use HTTPS/WSS to protect
|
|
107
|
+
terminal input and output.
|
|
108
|
+
|
|
109
|
+
### Renderer selection
|
|
110
|
+
|
|
111
|
+
Set the mount-time `renderer` option to `"auto"` (the default), `"webgpu"`, or
|
|
112
|
+
`"webgl2"`. Auto prefers WebGPU and uses WebGL2 if the secure context, API,
|
|
113
|
+
adapter, device acquisition, or presentation context is unavailable. Explicit
|
|
114
|
+
modes require that backend and report an error rather than falling back.
|
|
115
|
+
Shader, font, validation, and unexpected initialization errors are not
|
|
116
|
+
compatibility fallbacks. Runtime GPU/context loss terminates the view with an
|
|
117
|
+
error; it does not switch backends behind the caller's back.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const terminal = await WebTerminal.mount(container, {
|
|
121
|
+
url: "/ws/terminal",
|
|
122
|
+
renderer: "webgl2", // Use WebGL2 even if WebGPU is available.
|
|
123
|
+
onStats(stats) {
|
|
124
|
+
console.log(stats.renderer, stats.rendererFallbackReason);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`stats.renderer` identifies the active backend after initialization.
|
|
130
|
+
`stats.rendererFallbackReason` explains an automatic fallback and is absent
|
|
131
|
+
for explicit selections and successful WebGPU initialization. Both backends
|
|
132
|
+
share glyph rasterization, frame preparation, clipping, and image ordering.
|
|
133
|
+
WebGPU preference is not a performance guarantee; compare representative
|
|
134
|
+
workloads on your target browsers and devices.
|
|
135
|
+
|
|
136
|
+
### Module and worker deployment
|
|
60
137
|
|
|
61
138
|
The package contains browser ES modules, not a single bundle. For bare static
|
|
62
139
|
hosting, copy **all of `dist/`**, preserving its directory structure, and import
|
|
@@ -100,10 +177,14 @@ workers, fonts, and the intended WebSocket endpoint.
|
|
|
100
177
|
| --- | --- |
|
|
101
178
|
| `workerUrl` | Optional module-worker entry; useful when worker assets are deployed separately. |
|
|
102
179
|
| `scale` | GPU backing scale `0.5`–`3`, or `"auto"` (default, bounded device pixel ratio). |
|
|
180
|
+
| `renderer` | `"auto"` (prefer WebGPU), `"webgpu"`, or `"webgl2"`; selected once per mount. |
|
|
103
181
|
| `font` | One family and optional downloadable font faces; see below. |
|
|
104
182
|
| `sizing` | `{ mode: "auto", fontSize?: number }` or `{ mode: "fixed", columns, rows, fontSize?: number }`. |
|
|
105
|
-
| `readOnly` |
|
|
183
|
+
| `readOnly` | Initial per-view input policy; change it later with `setReadOnly(boolean)`. |
|
|
106
184
|
| `label` | Accessible label for the terminal's hidden keyboard input. |
|
|
185
|
+
| `onTitleChange` | Initial authoritative workload title, then distinct presented changes; see below. |
|
|
186
|
+
| `onClose` | Native WebSocket close details, including pre-mount transport failure; not workload completion. |
|
|
187
|
+
| `onProgressChange`, `onShellIntegrationChange` | Initial authoritative activity, then distinct presented changes for host-owned chrome. |
|
|
107
188
|
| `inputBindings`, `onInput`, `actions` | Per-view input policy and custom actions. |
|
|
108
189
|
| `onSelectionUI` | Synchronous, cancelable UI notification hook. |
|
|
109
190
|
|
|
@@ -114,7 +195,7 @@ Font size is an integer from 8–32, defaulting to 16. Import `MIN_FONT_SIZE` an
|
|
|
114
195
|
ownership. `requestPrimary()` explicitly requests ownership; inspect `peer` or
|
|
115
196
|
`onRoleChange` to observe the result.
|
|
116
197
|
|
|
117
|
-
The handle exposes `geometry`, `peer`, `connected`, `stats`, `screenText`,
|
|
198
|
+
The handle exposes `geometry`, `peer`, `connected`, `readOnly`, `title`, `progress`, `shellIntegration`, `stats`, `screenText`,
|
|
118
199
|
`sizing`, `viewport`, `selection`, `inputBindings`, and `inputContext`.
|
|
119
200
|
Metrics start empty; check optional fields before using them. History may be
|
|
120
201
|
unavailable, and selection can be unavailable, none, pending, valid, or
|
|
@@ -122,8 +203,215 @@ invalidated. Narrow `viewport.available` and `selection.status` before using
|
|
|
122
203
|
their state-specific values. `screenText` reflects the presented viewport, not
|
|
123
204
|
an independently reconstructed ANSI buffer.
|
|
124
205
|
|
|
125
|
-
Callbacks include `onGeometry`, `onRoleChange`, `onSizingChange`, `onStats`,
|
|
126
|
-
`onViewportChange`, `onSelectionChange`, `onStatus`, and `onInputError`.
|
|
206
|
+
Callbacks include `onGeometry`, `onRoleChange`, `onTitleChange`, `onSizingChange`, `onStats`,
|
|
207
|
+
`onProgressChange`, `onShellIntegrationChange`, `onViewportChange`, `onSelectionChange`, `onStatus`, and `onInputError`.
|
|
208
|
+
|
|
209
|
+
### Live read-only views
|
|
210
|
+
|
|
211
|
+
Call `terminal.setReadOnly(true)` to disable application input on an already
|
|
212
|
+
mounted view. `terminal.readOnly`, `inputContext.readOnly`, and selection UI
|
|
213
|
+
notifications reflect the new policy. Use `setReadOnly(false)` to re-enable input;
|
|
214
|
+
neither call remounts, reconnects, releases the peer's primary role, or changes
|
|
215
|
+
the server's current grid. A writable primary resumes automatic sizing requests.
|
|
216
|
+
Mutating the original `options.readOnly` after mount has no effect.
|
|
217
|
+
|
|
218
|
+
Read-only blocks keyboard/text/IME input, application mouse reports, direct
|
|
219
|
+
`paste()`/`pasteClipboard()`, the paste paths of `runAction()`, `resize()`,
|
|
220
|
+
`setSizing()`, `requestPrimary()`, and automatic resize requests. Explicit input
|
|
221
|
+
methods throw when disabled; DOM application input is not forwarded. Routing
|
|
222
|
+
overrides cannot bypass this policy. Custom actions can still run local operations,
|
|
223
|
+
but any terminal input method they call remains gated.
|
|
224
|
+
|
|
225
|
+
Active pointer capture and queued mouse movement, pending composition, queued
|
|
226
|
+
resize, and pending clipboard pastes are cancelled on policy change. Quickly
|
|
227
|
+
re-enabling input does not revive a previously pending paste. Commands already
|
|
228
|
+
dispatched cannot be recalled. Output, local selection gestures, history
|
|
229
|
+
navigation, resync, and copying remain available, including a copy already in
|
|
230
|
+
progress. UI notifications follow their usual coalescing rules.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { WebTerminal } from "@hex1b/web-terminal";
|
|
234
|
+
|
|
235
|
+
const container = document.getElementById("terminal");
|
|
236
|
+
const inputEnabled = document.querySelector<HTMLInputElement>("#input-enabled");
|
|
237
|
+
if (!container || !inputEnabled) throw new Error("Missing terminal controls");
|
|
238
|
+
|
|
239
|
+
const terminal = await WebTerminal.mount(container, {
|
|
240
|
+
url: "/ws/terminal",
|
|
241
|
+
readOnly: !inputEnabled.checked
|
|
242
|
+
});
|
|
243
|
+
inputEnabled.addEventListener("change", () => terminal.setReadOnly(!inputEnabled.checked));
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Client policy is not authorization.** Enforce it independently on each server
|
|
247
|
+
view with `Hwt1PresentationAdapter.IsReadOnly`, initially or at runtime. For
|
|
248
|
+
example, in the host's existing connection setup (C# snippet):
|
|
249
|
+
|
|
250
|
+
```csharp
|
|
251
|
+
var presentation = new Hwt1PresentationAdapter { IsReadOnly = true };
|
|
252
|
+
// Attach this presentation to the view's terminal and drive its existing transport loops.
|
|
253
|
+
// Only trusted host policy should grant writes:
|
|
254
|
+
presentation.IsReadOnly = false;
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The adapter ignores producer-mutating browser input, resize, and primary requests
|
|
258
|
+
while read-only, but still processes acknowledgements, resync, history, selection,
|
|
259
|
+
and copy. This is **per presentation**, not a producer-wide input lock: direct
|
|
260
|
+
terminal automation and other authorized viewers continue. A policy change does
|
|
261
|
+
not retract a command the adapter already accepted. The host must update both its
|
|
262
|
+
server policy and browser UX; client changes do not authorize themselves, and the
|
|
263
|
+
server property does not automatically change client UI.
|
|
264
|
+
|
|
265
|
+
### Workload titles
|
|
266
|
+
|
|
267
|
+
The read-only `terminal.title` is the current presented workload title. An empty
|
|
268
|
+
string means unset or explicitly cleared; choose your own fallback. The optional
|
|
269
|
+
`onTitleChange(title)` callback runs once with the first authoritative presented
|
|
270
|
+
value, **including `""`, before mount resolves**. The getter is updated before
|
|
271
|
+
the callback. Later notifications report only distinct presented values. Identical
|
|
272
|
+
updates, same-title resyncs, cursor blinking, and statistics do not notify again.
|
|
273
|
+
Intermediate workload changes can coalesce; this is not an event for every OSC
|
|
274
|
+
sequence.
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { WebTerminal } from "@hex1b/web-terminal";
|
|
278
|
+
|
|
279
|
+
const container = document.getElementById("terminal");
|
|
280
|
+
const header = document.getElementById("terminal-header");
|
|
281
|
+
if (!container || !header) throw new Error("Missing terminal elements");
|
|
282
|
+
const resourceName = "Build service";
|
|
283
|
+
|
|
284
|
+
const terminal = await WebTerminal.mount(container, {
|
|
285
|
+
url: "/ws/terminal",
|
|
286
|
+
onTitleChange(title) {
|
|
287
|
+
header.textContent = title || resourceName;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
console.log(terminal.title);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The callback uses elements and fallback text captured **before** mounting, not
|
|
294
|
+
the still-pending `terminal` result. The component does not change `document.title`,
|
|
295
|
+
your header, or the input's accessible `label` automatically. There is no title
|
|
296
|
+
subscription method or DOM title event.
|
|
297
|
+
|
|
298
|
+
Titles are normalized by the core to at most 4,096 UTF-16 code units, with C0,
|
|
299
|
+
DEL, and C1 controls removed, malformed surrogates replaced with U+FFFD, and
|
|
300
|
+
truncation at a Unicode scalar boundary. They remain **untrusted text**: markup
|
|
301
|
+
and bidi characters are preserved. Use `textContent`, not `innerHTML`; apply your
|
|
302
|
+
own presentation and bidi policies.
|
|
303
|
+
|
|
304
|
+
OSC 0 and OSC 2 set or explicitly clear the window title; OSC 1 is icon-only.
|
|
305
|
+
Use `ESC ] 0 ; text BEL` or `ESC ] 2 ; text BEL`; `ESC \` (ST) may replace BEL.
|
|
306
|
+
The UTF-8 input path also accepts Unicode C1 OSC (`U+009D`) and ST (`U+009C`).
|
|
307
|
+
Semicolons within `text` are literal. Existing OSC 22/23 saved-title extensions
|
|
308
|
+
update the same state, including after a late HMP1 attachment.
|
|
309
|
+
RIS, soft reset, screen clearing, and buffer switching preserve the title and
|
|
310
|
+
existing saved-title behavior. History inspection retains the current workload
|
|
311
|
+
title rather than a title associated with an old row.
|
|
312
|
+
|
|
313
|
+
Disconnect and disposal retain the last known title without a synthetic clear.
|
|
314
|
+
Disposal (including abort) stops title callbacks. Attach a new view to reconnect;
|
|
315
|
+
it receives its own initial current title. Preliminary disconnected relay frames
|
|
316
|
+
do not trigger the initial notification. Callbacks run directly like the other
|
|
317
|
+
state callbacks; thrown host errors are not swallowed or retried.
|
|
318
|
+
|
|
319
|
+
The required title field needs the matching server build. Missing or malformed
|
|
320
|
+
title metadata fails the connection; an older server is not silently treated as
|
|
321
|
+
an empty title.
|
|
322
|
+
The per-title bound is not a limit on all parser buffering or saved-stack depth.
|
|
323
|
+
An HMP1 snapshot with too much saved title state fails its 16 MiB replay limit
|
|
324
|
+
rather than silently discarding saved titles.
|
|
325
|
+
|
|
326
|
+
### Application progress and shell activity
|
|
327
|
+
|
|
328
|
+
`terminal.progress` exposes OSC 9;4 state as `{ state, percentage }`.
|
|
329
|
+
The states are `"none"`, `"normal"`, `"error"`, `"indeterminate"`, and `"warning"`.
|
|
330
|
+
Normal/error/warning percentages are integers from 0 through 100. None and
|
|
331
|
+
indeterminate have `percentage: null`; none means the host should hide its indicator.
|
|
332
|
+
This is application-reported progress, not inferred from output or CPU activity.
|
|
333
|
+
It is independent of `ProgressWidget`, which draws inside terminal cells.
|
|
334
|
+
|
|
335
|
+
`terminal.shellIntegration` exposes OSC 133 as `{ phase, lastExitCode }`.
|
|
336
|
+
The phases are `"unknown"`, `"prompt"` (A), `"commandLine"` (B),
|
|
337
|
+
`"executing"` (C), and `"finished"` (D). B means input after the prompt, **not**
|
|
338
|
+
command execution. Unknown does not mean idle. `lastExitCode` is a signed
|
|
339
|
+
32-bit integer, or null when no status was reported; null is not success.
|
|
340
|
+
A/B/C preserve the last reported result, and D replaces it, including clearing
|
|
341
|
+
it to null when the shell omits its status. No command text, history, or output
|
|
342
|
+
locations are retained by these APIs.
|
|
343
|
+
|
|
344
|
+
Both getters return defensive copies. Their callbacks receive the first
|
|
345
|
+
authoritative presented state before mount resolves, then distinct presented
|
|
346
|
+
changes. Both getters are updated before either activity callback. Callbacks
|
|
347
|
+
use the same direct, synchronous host-callback convention as title changes;
|
|
348
|
+
host exceptions are not swallowed or retried.
|
|
349
|
+
|
|
350
|
+
Frames coalesce: the browser might see only Finished for a fast command, or
|
|
351
|
+
miss an entire command whose final state is unchanged. These callbacks are
|
|
352
|
+
**current-state notifications, not a lossless start/finish event stream**.
|
|
353
|
+
Resync/replay never invent commands, unchanged state does not notify again,
|
|
354
|
+
and a new mount receives its own baseline.
|
|
355
|
+
|
|
356
|
+
This example creates optional chrome outside the terminal:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { WebTerminal } from "@hex1b/web-terminal";
|
|
360
|
+
|
|
361
|
+
const status = document.createElement("span");
|
|
362
|
+
const progress = document.createElement("progress");
|
|
363
|
+
progress.max = 100;
|
|
364
|
+
progress.hidden = true;
|
|
365
|
+
const container = document.createElement("div");
|
|
366
|
+
container.style.cssText = "width:800px;height:480px";
|
|
367
|
+
document.body.append(status, progress, container);
|
|
368
|
+
|
|
369
|
+
const terminal = await WebTerminal.mount(container, {
|
|
370
|
+
url: "/ws/terminal",
|
|
371
|
+
onProgressChange(value) {
|
|
372
|
+
progress.hidden = value.state === "none";
|
|
373
|
+
progress.dataset.state = value.state; // Host CSS can distinguish error/warning.
|
|
374
|
+
if (value.percentage === null) progress.removeAttribute("value");
|
|
375
|
+
else progress.value = value.percentage;
|
|
376
|
+
},
|
|
377
|
+
onShellIntegrationChange(value) {
|
|
378
|
+
status.textContent = value.phase +
|
|
379
|
+
(value.lastExitCode === null ? "" : ` (last exit ${value.lastExitCode})`);
|
|
380
|
+
},
|
|
381
|
+
onStats(stats) {
|
|
382
|
+
if (!stats.connected) {
|
|
383
|
+
progress.hidden = true;
|
|
384
|
+
status.textContent = "Disconnected";
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
console.log(terminal.progress, terminal.shellIntegration);
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
No title, document chrome, or progress UI is changed automatically by the
|
|
392
|
+
component. The sample endpoint must be supplied by your application.
|
|
393
|
+
Callbacks can run before the `terminal` variable is assigned; use their
|
|
394
|
+
arguments during initial mounting.
|
|
395
|
+
|
|
396
|
+
RIS resets progress to None and shell integration to Unknown. Soft reset,
|
|
397
|
+
screen clearing, resize, and buffer switches preserve them. OSC 9;4 state 0
|
|
398
|
+
clears only progress; a shell completion does not implicitly clear it.
|
|
399
|
+
Disconnect, process exit, and disposal retain the last reported values
|
|
400
|
+
without inventing a completion or progress clear. Check `connected` before
|
|
401
|
+
showing active chrome, and remount to reconnect. Disposal stops callbacks.
|
|
402
|
+
Snapshots and historical viewports carry current activity, not activity at
|
|
403
|
+
the time a particular row was printed.
|
|
404
|
+
|
|
405
|
+
The core accepts BEL, ESC-backslash ST, and decoded Unicode C1 terminators
|
|
406
|
+
through its UTF-8 input path. Determinate progress requires unsigned decimal
|
|
407
|
+
0-100; clear/indeterminate allow an omitted percentage and ignore its optional
|
|
408
|
+
value. OSC 133 supports the basic A/B/C forms and D with an optional signed
|
|
409
|
+
decimal exit status (an empty field also means no status). Missing required,
|
|
410
|
+
malformed, overflowed, excess, or unsupported arguments do not change state.
|
|
411
|
+
The raw-output presentation path still forwards the original sequences to
|
|
412
|
+
supporting outer terminals. Required activity metadata needs the matching
|
|
413
|
+
server build; invalid/missing wire fields fail the connection, not silently
|
|
414
|
+
fall back to default state.
|
|
127
415
|
|
|
128
416
|
## Input and clipboard
|
|
129
417
|
|
|
@@ -172,6 +460,20 @@ actions reject if selection/input/focus changes before their asynchronous work
|
|
|
172
460
|
can be applied safely. Errors are surfaced rather than silently reported as
|
|
173
461
|
successful copies or pastes.
|
|
174
462
|
|
|
463
|
+
### Hyperlinks
|
|
464
|
+
|
|
465
|
+
Hold Ctrl or Cmd and click an OSC 8 hyperlink to open its destination in a new
|
|
466
|
+
tab. Hovering shows the destination and activation hint; holding the modifier
|
|
467
|
+
also shows a pointer cursor. Links work in live output, scrollback, and read-only
|
|
468
|
+
views. Plain clicks and drags retain their existing selection/application
|
|
469
|
+
behavior, and explicit input-policy routes or actions take precedence.
|
|
470
|
+
Shift and Alt/Option continue to reserve selection gestures.
|
|
471
|
+
|
|
472
|
+
Only absolute `http:`, `https:`, and `mailto:` destinations are activated
|
|
473
|
+
(`mailto:` handling depends on the browser). New tabs use `noopener,noreferrer`.
|
|
474
|
+
Script, data, file, relative, and custom-scheme URLs are not activated.
|
|
475
|
+
Plain URL text is not automatically detected; the workload must emit OSC 8.
|
|
476
|
+
|
|
175
477
|
## Selection UI hooks
|
|
176
478
|
|
|
177
479
|
`onSelectionUI` receives a typed `SelectionUIEvent`, also dispatched as the
|
|
@@ -239,5 +541,6 @@ Only `dist/`, this README, the MIT license, and package metadata are shipped.
|
|
|
239
541
|
A prepared tarball is self-contained and can be published with
|
|
240
542
|
`npm publish ./hex1b-web-terminal-<version>.tgz --ignore-scripts`; it does not
|
|
241
543
|
need development sources or build scripts. The package name is always
|
|
242
|
-
`@hex1b/web-terminal
|
|
243
|
-
the
|
|
544
|
+
`@hex1b/web-terminal`. CI publishes main/release builds to npmjs; PR builds
|
|
545
|
+
provide the tarball as the `npm-web-terminal` workflow artifact and do not
|
|
546
|
+
publish it to a registry. No registry is pinned in `package.json`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RenderBackend } from "./render-backend.js";
|
|
2
|
+
import type { TerminalRendererPreference } from "./types.js";
|
|
3
|
+
export declare function createRenderBackend(canvas: OffscreenCanvas, onFatal: (error: Error) => void, preference?: TerminalRendererPreference): Promise<{
|
|
4
|
+
backend: RenderBackend;
|
|
5
|
+
fallbackReason?: string;
|
|
6
|
+
}>;
|
|
7
|
+
//# sourceMappingURL=backend-selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend-selection.d.ts","sourceRoot":"","sources":["../src/backend-selection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAE7D,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,EAChG,UAAU,GAAE,0BAAmC,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,aAAa,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAe/G"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RendererUnavailableError } from "./render-backend.js";
|
|
2
|
+
import { normalizeRenderer } from "./renderer-options.js";
|
|
3
|
+
import { WebGpuBackend } from "./webgpu-backend.js";
|
|
4
|
+
import { WebGl2Backend } from "./webgl2-backend.js";
|
|
5
|
+
export async function createRenderBackend(canvas, onFatal, preference = "auto") {
|
|
6
|
+
const requested = normalizeRenderer(preference);
|
|
7
|
+
if (requested === "webgl2")
|
|
8
|
+
return { backend: await WebGl2Backend.create(canvas, onFatal) };
|
|
9
|
+
try {
|
|
10
|
+
return { backend: await WebGpuBackend.create(canvas, onFatal) };
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (requested !== "auto" || !(error instanceof RendererUnavailableError))
|
|
14
|
+
throw error;
|
|
15
|
+
try {
|
|
16
|
+
return { backend: await WebGl2Backend.create(canvas, onFatal), fallbackReason: error.message };
|
|
17
|
+
}
|
|
18
|
+
catch (fallbackError) {
|
|
19
|
+
throw new AggregateError([error, fallbackError], `WebGPU unavailable (${error.message}); WebGL2 initialization failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=backend-selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend-selection.js","sourceRoot":"","sources":["../src/backend-selection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAE/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAAuB,EAAE,OAA+B,EAChG,aAAyC,MAAM;IAC/C,MAAM,SAAS,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;IAChD,IAAI,SAAS,KAAK,QAAQ;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAC5F,IAAI,CAAC;QACH,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAClE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,SAAS,KAAK,MAAM,IAAI,CAAC,CAAC,KAAK,YAAY,wBAAwB,CAAC;YAAE,MAAM,KAAK,CAAC;QACtF,IAAI,CAAC;YACH,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;QACjG,CAAC;QAAC,OAAO,aAAa,EAAE,CAAC;YACvB,MAAM,IAAI,cAAc,CAAC,CAAC,KAAK,EAAE,aAAa,CAAC,EAC7C,uBAAuB,KAAK,CAAC,OAAO,oCAClC,aAAa,YAAY,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import { RendererUnavailableError } from \"./render-backend.js\";\nimport type { RenderBackend } from \"./render-backend.js\";\nimport { normalizeRenderer } from \"./renderer-options.js\";\nimport { WebGpuBackend } from \"./webgpu-backend.js\";\nimport { WebGl2Backend } from \"./webgl2-backend.js\";\nimport type { TerminalRendererPreference } from \"./types.js\";\n\nexport async function createRenderBackend(canvas: OffscreenCanvas, onFatal: (error: Error) => void,\n preference: TerminalRendererPreference = \"auto\"): Promise<{ backend: RenderBackend; fallbackReason?: string }> {\n const requested = normalizeRenderer(preference);\n if (requested === \"webgl2\") return { backend: await WebGl2Backend.create(canvas, onFatal) };\n try {\n return { backend: await WebGpuBackend.create(canvas, onFatal) };\n } catch (error) {\n if (requested !== \"auto\" || !(error instanceof RendererUnavailableError)) throw error;\n try {\n return { backend: await WebGl2Backend.create(canvas, onFatal), fallbackReason: error.message };\n } catch (fallbackError) {\n throw new AggregateError([error, fallbackError],\n `WebGPU unavailable (${error.message}); WebGL2 initialization failed: ${\n fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TerminalPoint } from "./types.js";
|
|
2
|
+
import type { HyperlinkRange } from "./wire-types.js";
|
|
3
|
+
/** OSC 8 destinations are untrusted output, not page-relative navigation. */
|
|
4
|
+
export declare function hyperlinkUri(uri: string): string | null;
|
|
5
|
+
export declare class Hyperlinks {
|
|
6
|
+
#private;
|
|
7
|
+
update(ranges: readonly HyperlinkRange[]): void;
|
|
8
|
+
at(point: TerminalPoint): string | null;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=hyperlinks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hyperlinks.d.ts","sourceRoot":"","sources":["../src/hyperlinks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIvD;AAED,qBAAa,UAAU;;IAGrB,MAAM,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,GAAG,IAAI;IAa/C,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI;CAIxC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** OSC 8 destinations are untrusted output, not page-relative navigation. */
|
|
2
|
+
export function hyperlinkUri(uri) {
|
|
3
|
+
if (/[\u0000-\u0020\u007f]/u.test(uri) || !URL.canParse(uri))
|
|
4
|
+
return null;
|
|
5
|
+
const url = new URL(uri);
|
|
6
|
+
return ["https:", "http:", "mailto:"].includes(url.protocol) ? url.href : null;
|
|
7
|
+
}
|
|
8
|
+
export class Hyperlinks {
|
|
9
|
+
#rows = new Map();
|
|
10
|
+
update(ranges) {
|
|
11
|
+
this.#rows.clear();
|
|
12
|
+
const destinations = new Map();
|
|
13
|
+
for (const range of ranges) {
|
|
14
|
+
if (!destinations.has(range.uri))
|
|
15
|
+
destinations.set(range.uri, hyperlinkUri(range.uri));
|
|
16
|
+
const uri = destinations.get(range.uri);
|
|
17
|
+
if (!uri)
|
|
18
|
+
continue;
|
|
19
|
+
const row = this.#rows.get(range.row) ?? [];
|
|
20
|
+
row.push({ ...range, uri });
|
|
21
|
+
this.#rows.set(range.row, row);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
at(point) {
|
|
25
|
+
return this.#rows.get(point.y)?.find(range => point.x >= range.startColumn && point.x < range.endColumn)?.uri ?? null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=hyperlinks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hyperlinks.js","sourceRoot":"","sources":["../src/hyperlinks.ts"],"names":[],"mappings":"AAGA,6EAA6E;AAC7E,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,IAAI,wBAAwB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1E,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACzB,OAAO,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACjF,CAAC;AAED,MAAM,OAAO,UAAU;IACrB,KAAK,GAAG,IAAI,GAAG,EAA4B,CAAC;IAE5C,MAAM,CAAC,MAAiC;QACtC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,YAAY,GAAG,IAAI,GAAG,EAAyB,CAAC;QACtD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC;gBAAE,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;YACvF,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACxC,IAAI,CAAC,GAAG;gBAAE,SAAS;YACnB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YAC5C,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YAC5B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAED,EAAE,CAAC,KAAoB;QACrB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAC3C,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,EAAE,GAAG,IAAI,IAAI,CAAC;IAC5E,CAAC;CACF","sourcesContent":["import type { TerminalPoint } from \"./types.js\";\nimport type { HyperlinkRange } from \"./wire-types.js\";\n\n/** OSC 8 destinations are untrusted output, not page-relative navigation. */\nexport function hyperlinkUri(uri: string): string | null {\n if (/[\\u0000-\\u0020\\u007f]/u.test(uri) || !URL.canParse(uri)) return null;\n const url = new URL(uri);\n return [\"https:\", \"http:\", \"mailto:\"].includes(url.protocol) ? url.href : null;\n}\n\nexport class Hyperlinks {\n #rows = new Map<number, HyperlinkRange[]>();\n\n update(ranges: readonly HyperlinkRange[]): void {\n this.#rows.clear();\n const destinations = new Map<string, string | null>();\n for (const range of ranges) {\n if (!destinations.has(range.uri)) destinations.set(range.uri, hyperlinkUri(range.uri));\n const uri = destinations.get(range.uri);\n if (!uri) continue;\n const row = this.#rows.get(range.row) ?? [];\n row.push({ ...range, uri });\n this.#rows.set(range.row, row);\n }\n }\n\n at(point: TerminalPoint): string | null {\n return this.#rows.get(point.y)?.find(range =>\n point.x >= range.startColumn && point.x < range.endColumn)?.uri ?? null;\n }\n}\n"]}
|
package/dist/mouse-input.d.ts
CHANGED
|
@@ -14,9 +14,12 @@ interface MouseInspection {
|
|
|
14
14
|
execute?: (decision: Extract<InputDecision, {
|
|
15
15
|
action: unknown;
|
|
16
16
|
}>, input: TerminalInput) => void;
|
|
17
|
+
hyperlink?: (point: TerminalPoint) => string | null;
|
|
18
|
+
openHyperlink?: (uri: string) => void;
|
|
17
19
|
}
|
|
18
20
|
export interface MouseCapture {
|
|
19
21
|
update(columns: number, rows: number, tracking: MouseTrackingMode): void;
|
|
22
|
+
refresh(): void;
|
|
20
23
|
cancel(): void;
|
|
21
24
|
dispose(): void;
|
|
22
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mouse-input.d.ts","sourceRoot":"","sources":["../src/mouse-input.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAmB,iBAAiB,EAAiB,aAAa,EAC3F,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAA6B,YAAY,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"mouse-input.d.ts","sourceRoot":"","sources":["../src/mouse-input.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAmB,iBAAiB,EAAiB,aAAa,EAC3F,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAA6B,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAM/E,UAAU,eAAe;IACvB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IAC5F,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACxC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IAC3D,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,aAAa,CAAC;IAClD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,aAAa,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC,EAAE,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAChG,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,GAAG,IAAI,CAAC;IACpD,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AACD,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACzE,OAAO,IAAI,IAAI,CAAC;IAChB,MAAM,IAAI,IAAI,CAAC;IACf,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,EAC3F,KAAK,EAAE,MAAM,IAAI,EAAE,UAAU,GAAE,eAAoB,GAAG,YAAY,CA2WnE"}
|
package/dist/mouse-input.js
CHANGED
|
@@ -24,10 +24,27 @@ export function captureMouse(canvas, send, focus, inspection = {}) {
|
|
|
24
24
|
let completedClick;
|
|
25
25
|
let routedGesture;
|
|
26
26
|
let contextMenuRoute;
|
|
27
|
+
let hyperlinkClick;
|
|
28
|
+
let hoverEvent;
|
|
29
|
+
let hoverModifiers;
|
|
30
|
+
const originalTitle = canvas.title;
|
|
31
|
+
const originalCursor = canvas.style.cursor;
|
|
27
32
|
const state = () => ({ tracking, ...inspection.state?.() });
|
|
28
33
|
function point(event, clamp = false) {
|
|
29
34
|
return cellPoint(event, canvas.getBoundingClientRect(), columns, rows, clamp);
|
|
30
35
|
}
|
|
36
|
+
function refreshHover(modifiers = hoverModifiers) {
|
|
37
|
+
if (!inspection.hyperlink)
|
|
38
|
+
return;
|
|
39
|
+
hoverModifiers = modifiers;
|
|
40
|
+
const position = hoverEvent && point(hoverEvent);
|
|
41
|
+
const uri = position ? inspection.hyperlink(position) : null;
|
|
42
|
+
canvas.title = uri ? `${uri}\nCtrl/Cmd+click to open link` : originalTitle;
|
|
43
|
+
canvas.style.cursor = uri && modifiers && (modifiers.ctrlKey || modifiers.metaKey) &&
|
|
44
|
+
!modifiers.altKey && !modifiers.shiftKey ? "pointer" : originalCursor;
|
|
45
|
+
if (hyperlinkClick && hyperlinkClick.uri !== uri)
|
|
46
|
+
hyperlinkClick.dragged = true;
|
|
47
|
+
}
|
|
31
48
|
function updateAutoScroll() {
|
|
32
49
|
clearTimeout(autoScroll);
|
|
33
50
|
autoScroll = undefined;
|
|
@@ -83,6 +100,7 @@ export function captureMouse(canvas, send, focus, inspection = {}) {
|
|
|
83
100
|
pointerEvent = undefined;
|
|
84
101
|
selectionClick = undefined;
|
|
85
102
|
completedClick = undefined;
|
|
103
|
+
hyperlinkClick = undefined;
|
|
86
104
|
gesture.end();
|
|
87
105
|
routedGesture = undefined;
|
|
88
106
|
if (report)
|
|
@@ -115,6 +133,8 @@ export function captureMouse(canvas, send, focus, inspection = {}) {
|
|
|
115
133
|
if (!position)
|
|
116
134
|
return;
|
|
117
135
|
if (pointerId !== null) {
|
|
136
|
+
if (hyperlinkClick)
|
|
137
|
+
hyperlinkClick.dragged = true;
|
|
118
138
|
if (routedGesture !== InputRoute.Browser)
|
|
119
139
|
event.preventDefault();
|
|
120
140
|
if ((gesture.owner === "app" || routedGesture === InputRoute.Application) && pointerId === event.pointerId)
|
|
@@ -125,8 +145,20 @@ export function captureMouse(canvas, send, focus, inspection = {}) {
|
|
|
125
145
|
const input = { type: "pointer", button: pointerButtons[event.button],
|
|
126
146
|
point: Object.freeze({ ...position }), ...inputModifiers(event) };
|
|
127
147
|
const decision = inspection.resolve?.(input) ?? { route: InputRoute.Continue };
|
|
128
|
-
|
|
148
|
+
let route = decision.action !== undefined ? InputRoute.Consume : decision.route;
|
|
149
|
+
if (route === InputRoute.Continue && event.button === 0 && (event.ctrlKey || event.metaKey) &&
|
|
150
|
+
!event.altKey && !event.shiftKey) {
|
|
151
|
+
const uri = inspection.hyperlink?.(position);
|
|
152
|
+
if (uri) {
|
|
153
|
+
hyperlinkClick = { uri, x: event.clientX, y: event.clientY, dragged: false };
|
|
154
|
+
route = InputRoute.Consume;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
129
157
|
contextMenuRoute = event.button === 2 ? route : undefined;
|
|
158
|
+
if (hyperlinkClick)
|
|
159
|
+
contextMenuRoute = InputRoute.Consume;
|
|
160
|
+
hoverEvent = event;
|
|
161
|
+
refreshHover(event);
|
|
130
162
|
if (route !== InputRoute.Continue) {
|
|
131
163
|
completedClick = selectionClick = undefined;
|
|
132
164
|
click.count = 0;
|
|
@@ -186,6 +218,10 @@ export function captureMouse(canvas, send, focus, inspection = {}) {
|
|
|
186
218
|
return;
|
|
187
219
|
if (pointerId !== null && pointerId !== event.pointerId)
|
|
188
220
|
return;
|
|
221
|
+
hoverEvent = event;
|
|
222
|
+
refreshHover(event);
|
|
223
|
+
if (hyperlinkClick && Math.hypot(event.clientX - hyperlinkClick.x, event.clientY - hyperlinkClick.y) >= 4)
|
|
224
|
+
hyperlinkClick.dragged = true;
|
|
189
225
|
const position = point(event, pointerId !== null);
|
|
190
226
|
if (!position)
|
|
191
227
|
return;
|
|
@@ -224,6 +260,21 @@ export function captureMouse(canvas, send, focus, inspection = {}) {
|
|
|
224
260
|
canvas.addEventListener("pointerup", event => {
|
|
225
261
|
if (pointerId !== event.pointerId)
|
|
226
262
|
return;
|
|
263
|
+
if (hyperlinkClick) {
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
const link = hyperlinkClick;
|
|
266
|
+
const position = point(event);
|
|
267
|
+
const activate = event.button === 0 && event.buttons === 0 && !link.dragged &&
|
|
268
|
+
Math.hypot(event.clientX - link.x, event.clientY - link.y) < 4 &&
|
|
269
|
+
position && inspection.hyperlink?.(position) === link.uri;
|
|
270
|
+
if (event.buttons === 0)
|
|
271
|
+
cancel(false, false);
|
|
272
|
+
else
|
|
273
|
+
link.dragged = true;
|
|
274
|
+
if (activate)
|
|
275
|
+
inspection.openHyperlink?.(link.uri);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
227
278
|
const position = point(event, true) ?? lastPoint;
|
|
228
279
|
if (routedGesture) {
|
|
229
280
|
if (routedGesture === InputRoute.Application && position)
|
|
@@ -262,11 +313,21 @@ export function captureMouse(canvas, send, focus, inspection = {}) {
|
|
|
262
313
|
inspection.begin?.(completed.point, { mode, extend: false });
|
|
263
314
|
}, options);
|
|
264
315
|
canvas.addEventListener("pointercancel", () => cancel(), options);
|
|
316
|
+
canvas.addEventListener("pointerleave", () => {
|
|
317
|
+
hoverEvent = undefined;
|
|
318
|
+
refreshHover();
|
|
319
|
+
}, options);
|
|
265
320
|
canvas.addEventListener("lostpointercapture", () => {
|
|
266
321
|
if (pointerId !== null)
|
|
267
322
|
cancel();
|
|
268
323
|
}, options);
|
|
269
|
-
window.addEventListener("blur", () =>
|
|
324
|
+
window.addEventListener("blur", () => {
|
|
325
|
+
cancel();
|
|
326
|
+
hoverEvent = undefined;
|
|
327
|
+
refreshHover();
|
|
328
|
+
}, options);
|
|
329
|
+
window.addEventListener("keydown", event => refreshHover(event), { ...options, capture: true });
|
|
330
|
+
window.addEventListener("keyup", event => refreshHover(event), { ...options, capture: true });
|
|
270
331
|
canvas.addEventListener("contextmenu", event => {
|
|
271
332
|
if (contextMenuRoute !== undefined && contextMenuRoute !== InputRoute.Continue) {
|
|
272
333
|
const route = contextMenuRoute;
|
|
@@ -279,6 +340,8 @@ export function captureMouse(canvas, send, focus, inspection = {}) {
|
|
|
279
340
|
event.preventDefault();
|
|
280
341
|
}, options);
|
|
281
342
|
canvas.addEventListener("wheel", event => {
|
|
343
|
+
if (hyperlinkClick)
|
|
344
|
+
hyperlinkClick.dragged = true;
|
|
282
345
|
const input = { type: "wheel", deltaX: event.deltaX, deltaY: event.deltaY,
|
|
283
346
|
deltaMode: event.deltaMode, point: point(event), ...inputModifiers(event) };
|
|
284
347
|
const decision = pointerId === null
|
|
@@ -345,11 +408,15 @@ export function captureMouse(canvas, send, focus, inspection = {}) {
|
|
|
345
408
|
rows = nextRows;
|
|
346
409
|
tracking = nextTracking;
|
|
347
410
|
}
|
|
411
|
+
refreshHover();
|
|
348
412
|
},
|
|
413
|
+
refresh() { refreshHover(); },
|
|
349
414
|
cancel() { cancel(); },
|
|
350
415
|
dispose() {
|
|
351
416
|
cancel(false);
|
|
352
417
|
listeners.abort();
|
|
418
|
+
canvas.title = originalTitle;
|
|
419
|
+
canvas.style.cursor = originalCursor;
|
|
353
420
|
}
|
|
354
421
|
};
|
|
355
422
|
}
|