@hex1b/web-terminal 0.168.0 → 0.169.0-alpha.1608.1.d6a20d4
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 +645 -41
- package/dist/history-state.d.ts +1 -0
- package/dist/history-state.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5954 -6
- package/dist/index.js.map +7 -1
- package/dist/link-detection.d.ts.map +1 -1
- package/dist/marker-pages.d.ts +9 -0
- package/dist/marker-pages.d.ts.map +1 -0
- package/dist/marker-state.d.ts +17 -0
- package/dist/marker-state.d.ts.map +1 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/random-id.d.ts +2 -0
- package/dist/random-id.d.ts.map +1 -0
- package/dist/scrollbar-appearance.d.ts +26 -0
- package/dist/scrollbar-appearance.d.ts.map +1 -0
- package/dist/scrollbar-colors.d.ts +14 -0
- package/dist/scrollbar-colors.d.ts.map +1 -0
- package/dist/scrollbar-renderer.d.ts +11 -0
- package/dist/scrollbar-renderer.d.ts.map +1 -0
- package/dist/scrollbar-tooltip.d.ts +28 -0
- package/dist/scrollbar-tooltip.d.ts.map +1 -0
- package/dist/scrollbar-types.d.ts +140 -0
- package/dist/scrollbar-types.d.ts.map +1 -0
- package/dist/scrollbar.d.ts +91 -0
- package/dist/scrollbar.d.ts.map +1 -0
- package/dist/terminal-layout.d.ts +6 -0
- package/dist/terminal-layout.d.ts.map +1 -0
- package/dist/terminal-theme.d.ts +1 -1
- package/dist/terminal-theme.d.ts.map +1 -1
- package/dist/types.d.ts +37 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/web-terminal.d.ts +13 -0
- package/dist/web-terminal.d.ts.map +1 -1
- package/dist/wire-types.d.ts +21 -0
- package/dist/wire-types.d.ts.map +1 -1
- package/dist/worker-url.d.ts +2 -0
- package/dist/worker-url.d.ts.map +1 -0
- package/package.json +3 -2
- package/dist/backend-selection.js +0 -23
- package/dist/backend-selection.js.map +0 -1
- package/dist/command-mark.js +0 -26
- package/dist/command-mark.js.map +0 -1
- package/dist/history-state.js +0 -194
- package/dist/history-state.js.map +0 -1
- package/dist/hyperlinks.js +0 -28
- package/dist/hyperlinks.js.map +0 -1
- package/dist/input-policy.js +0 -143
- package/dist/input-policy.js.map +0 -1
- package/dist/link-detection-worker.js +0 -100
- package/dist/link-detection-worker.js.map +0 -1
- package/dist/link-detection.js +0 -290
- package/dist/link-detection.js.map +0 -1
- package/dist/link-options.js +0 -108
- package/dist/link-options.js.map +0 -1
- package/dist/link-presentation.js +0 -125
- package/dist/link-presentation.js.map +0 -1
- package/dist/link-text.js +0 -92
- package/dist/link-text.js.map +0 -1
- package/dist/link-types.js +0 -2
- package/dist/link-types.js.map +0 -1
- package/dist/link-worker-protocol.js +0 -10
- package/dist/link-worker-protocol.js.map +0 -1
- package/dist/mouse-input.js +0 -427
- package/dist/mouse-input.js.map +0 -1
- package/dist/protocol.js +0 -337
- package/dist/protocol.js.map +0 -1
- package/dist/render-backend.js +0 -5
- package/dist/render-backend.js.map +0 -1
- package/dist/renderer-options.js +0 -6
- package/dist/renderer-options.js.map +0 -1
- package/dist/renderer.js +0 -473
- package/dist/renderer.js.map +0 -1
- package/dist/selection-input.js +0 -69
- package/dist/selection-input.js.map +0 -1
- package/dist/selection-ui.js +0 -113
- package/dist/selection-ui.js.map +0 -1
- package/dist/terminal-font.js +0 -85
- package/dist/terminal-font.js.map +0 -1
- package/dist/terminal-sizing.js +0 -38
- package/dist/terminal-sizing.js.map +0 -1
- package/dist/terminal-theme.js +0 -39
- package/dist/terminal-theme.js.map +0 -1
- package/dist/terminal-worker.js +0 -331
- package/dist/terminal-worker.js.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/validation.js +0 -7
- package/dist/validation.js.map +0 -1
- package/dist/web-terminal.js +0 -1052
- package/dist/web-terminal.js.map +0 -1
- package/dist/webgl2-backend.js +0 -456
- package/dist/webgl2-backend.js.map +0 -1
- package/dist/webgpu-backend.js +0 -234
- package/dist/webgpu-backend.js.map +0 -1
- package/dist/wire-types.js +0 -2
- package/dist/wire-types.js.map +0 -1
package/README.md
CHANGED
|
@@ -135,39 +135,71 @@ workloads on your target browsers and devices.
|
|
|
135
135
|
|
|
136
136
|
### Module and worker deployment
|
|
137
137
|
|
|
138
|
-
The package
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
138
|
+
The package ships **one JavaScript file: `dist/index.js`**. This ESM bundle
|
|
139
|
+
contains the public main-thread API, terminal worker, and link-detection worker;
|
|
140
|
+
there is no runtime JavaScript module tree to copy. npm imports and public
|
|
141
|
+
exports are unchanged: import from `@hex1b/web-terminal`, not internal modules.
|
|
142
|
+
|
|
143
|
+
For static hosting or vendoring, copy `index.js` and `dist/fonts/` into the same
|
|
144
|
+
served directory, preserving the font paths and their license/provenance files.
|
|
145
|
+
Keep the package's MIT license too. Fonts are separate assets, not embedded in
|
|
146
|
+
the JavaScript. Alternatively, supply your own explicit `font.faces` URLs.
|
|
147
|
+
`index.js.map` is optional for runtime use; declarations and declaration maps
|
|
148
|
+
support TypeScript consumers.
|
|
149
|
+
|
|
150
|
+
With the bundle at `/web-terminal/index.js` and fonts at `/web-terminal/fonts/`,
|
|
151
|
+
this is a complete mount example (the host supplies the matching HWT1 endpoint):
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<div id="terminal" style="width: 100%; height: 480px"></div>
|
|
155
|
+
<script type="module">
|
|
156
|
+
import { WebTerminal } from "/web-terminal/index.js";
|
|
157
|
+
|
|
158
|
+
const terminal = await WebTerminal.mount(document.getElementById("terminal"), {
|
|
159
|
+
url: "/ws/terminal",
|
|
160
|
+
sizing: { mode: "auto", fontSize: 16 },
|
|
161
|
+
onStatus(message, level) { console.log(level, message); }
|
|
162
|
+
});
|
|
163
|
+
terminal.focus();
|
|
164
|
+
window.addEventListener("pagehide", () => terminal.dispose(), { once: true });
|
|
165
|
+
</script>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Workers load the **same bundle URL**, replacing its fragment with
|
|
169
|
+
`#hex1b-terminal-worker` or `#hex1b-link-detection-worker`. The actual filename,
|
|
170
|
+
path, and query string are preserved, so renaming the unmodified bundle works:
|
|
171
|
+
`/vendor/terminal.js?v=42` uses `/vendor/terminal.js?v=42#hex1b-terminal-worker`.
|
|
172
|
+
Worker entry selection runs only in dedicated worker contexts, not when importing
|
|
173
|
+
the API into a page. The default font resolves relative to the JavaScript URL,
|
|
174
|
+
not the page.
|
|
175
|
+
|
|
176
|
+
Both workers are module workers; they need neither Blob URLs nor `eval`.
|
|
177
|
+
Same-origin deployment works with `worker-src 'self'` without adding `blob:`.
|
|
178
|
+
Also configure JavaScript/WOFF2 MIME types and CSP permissions for scripts, fonts,
|
|
179
|
+
and the intended WebSocket endpoint. Browser worker-origin restrictions still apply.
|
|
180
|
+
|
|
181
|
+
**Rebundling into an application is a separate deployment choice.** Defaults
|
|
182
|
+
assume the unmodified ESM bundle's URL, not an arbitrary application bundle with
|
|
183
|
+
DOM startup side effects. A bundler may rewrite asset URLs or tree-shake worker
|
|
184
|
+
code. If needed, separately host the unmodified `index.js` from the same package
|
|
185
|
+
build and explicitly point both workers to it, with explicit font URLs:
|
|
154
186
|
|
|
155
187
|
```ts
|
|
156
188
|
const terminal = await WebTerminal.mount(container, {
|
|
157
189
|
url: "/ws/terminal",
|
|
158
|
-
workerUrl: "/web-terminal/terminal-worker
|
|
190
|
+
workerUrl: "/web-terminal/index.js#hex1b-terminal-worker",
|
|
191
|
+
linkDetectionWorkerUrl: "/web-terminal/index.js#hex1b-link-detection-worker",
|
|
192
|
+
font: {
|
|
193
|
+
family: "My Terminal Font",
|
|
194
|
+
faces: [{ url: "/fonts/my-terminal.woff2", weight: "100 900", style: "normal" }]
|
|
195
|
+
}
|
|
159
196
|
});
|
|
160
197
|
```
|
|
161
198
|
|
|
162
|
-
`workerUrl`
|
|
163
|
-
the page
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
the default font asset URL or provide explicit `font.faces` URLs as shown below.
|
|
167
|
-
The browser's worker origin and CSP restrictions still apply.
|
|
168
|
-
|
|
169
|
-
Configure your server's JavaScript and WOFF2 MIME types and CSP to allow these
|
|
170
|
-
workers, fonts, and the intended WebSocket endpoint.
|
|
199
|
+
`workerUrl` and `linkDetectionWorkerUrl` accept nonempty strings or URLs;
|
|
200
|
+
relative strings resolve against the **page**, not the package. Overrides do
|
|
201
|
+
not copy fonts. Bundlers differ in dependency and asset handling; this package
|
|
202
|
+
does not claim universal or individually verified bundler support.
|
|
171
203
|
|
|
172
204
|
## Configuration and state
|
|
173
205
|
|
|
@@ -183,6 +215,9 @@ workers, fonts, and the intended WebSocket endpoint.
|
|
|
183
215
|
| `renderer` | `"auto"` (prefer WebGPU), `"webgpu"`, or `"webgl2"`; selected once per mount. |
|
|
184
216
|
| `font` | One family and optional downloadable font faces; see below. |
|
|
185
217
|
| `sizing` | `{ mode: "auto", fontSize?: number }` or `{ mode: "fixed", columns, rows, fontSize?: number }`. |
|
|
218
|
+
| `scrollbar` | Auto-hiding canvas overlay by default; `{ placement: "beside" }` reserves a gutter and stays visible while scrollable; `false` disables built-in chrome. |
|
|
219
|
+
| `padding` | Nonnegative CSS pixels: a uniform number or `{ top?, right?, bottom?, left? }`. Omitted edges are zero. |
|
|
220
|
+
| `onLayoutChange`, `onMarkersChange` | Local content/gutter geometry and authoritative retained marker inventory for host-owned chrome. |
|
|
186
221
|
| `readOnly` | Initial per-view input policy; change it later with `setReadOnly(boolean)`. |
|
|
187
222
|
| `label` | Accessible label for the terminal's hidden keyboard input. |
|
|
188
223
|
| `onTitleChange` | Initial authoritative workload title, then distinct presented changes; see below. |
|
|
@@ -194,14 +229,15 @@ workers, fonts, and the intended WebSocket endpoint.
|
|
|
194
229
|
|
|
195
230
|
Font size is an integer from 8–32, defaulting to 16. Import `MIN_FONT_SIZE` and
|
|
196
231
|
`MAX_FONT_SIZE` from `@hex1b/web-terminal` for sizing controls. Requested fixed grids allow
|
|
197
|
-
|
|
232
|
+
1–300 columns and 1–100 rows, matching `Hex1bTerminal`'s minimum of one cell
|
|
233
|
+
in each dimension. Automatic sizing uses the same bounds. The producer still owns actual grid geometry.
|
|
198
234
|
`resize()`, `setSizing()`, and automatic resize requests require primary
|
|
199
235
|
ownership. `requestPrimary()` explicitly requests ownership; inspect `peer` or
|
|
200
236
|
`onRoleChange` to observe the result.
|
|
201
237
|
|
|
202
238
|
The handle exposes `geometry`, `peer`, `connected`, `readOnly`, `title`, `progress`, `shellIntegration`,
|
|
203
239
|
`workingDirectory`, `commandMark`, `stats`, `screenText`,
|
|
204
|
-
`sizing`, `viewport`, `selection`, `inputBindings`, and `inputContext`.
|
|
240
|
+
`sizing`, `viewport`, `layout`, `padding`, `scrollbar`, `markers`, `selection`, `inputBindings`, and `inputContext`.
|
|
205
241
|
Metrics start empty; check optional fields before using them. History may be
|
|
206
242
|
unavailable, and selection can be unavailable, none, pending, valid, or
|
|
207
243
|
invalidated. Narrow `viewport.available` and `selection.status` before using
|
|
@@ -360,9 +396,10 @@ only on a `finished` (D) marker. `rawParameters` is the verbatim
|
|
|
360
396
|
`cmdline_url` extension on marker C), or `null` when none was present; use the
|
|
361
397
|
exported `parseCommandMarkParameters(rawParameters)` helper to parse it into a
|
|
362
398
|
`Map`, or `getCmdlineUrl(mark)` as a shortcut for the `cmdline_url` entry. This
|
|
363
|
-
is **not** a command-mark history — only the latest marker
|
|
364
|
-
`shellIntegration`.
|
|
365
|
-
|
|
399
|
+
is **not** a command-mark history — this getter exposes only the latest marker,
|
|
400
|
+
mirroring `shellIntegration`. Use `markers` / `onMarkersChange` for the retained
|
|
401
|
+
inventory and `getCommandMarkDetails(id)` for raw parameters on demand. Do not
|
|
402
|
+
reconstruct history from coalesced `onCommandMarkChange` callbacks.
|
|
366
403
|
|
|
367
404
|
All four getters return defensive copies. Their callbacks receive the first
|
|
368
405
|
authoritative presented state before mount resolves, then distinct presented
|
|
@@ -444,6 +481,567 @@ supporting outer terminals. Required activity metadata needs the matching
|
|
|
444
481
|
server build; invalid/missing wire fields fail the connection, not silently
|
|
445
482
|
fall back to default state.
|
|
446
483
|
|
|
484
|
+
## Scrollbars, padding, and retained markers
|
|
485
|
+
|
|
486
|
+
The terminal canvas remains GPU-rendered. Scrollbar chrome uses a separate,
|
|
487
|
+
transparent main-thread Canvas2D layer with the same behavior under WebGPU and
|
|
488
|
+
WebGL2. Change presentation without remounting or replacing the WebSocket:
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
terminal.setPadding({ top: 8, right: 16, bottom: 12, left: 24 });
|
|
492
|
+
terminal.setScrollbar({ placement: "beside", width: 12, markers: true });
|
|
493
|
+
terminal.setScrollbar({ placement: "overlay", hideDelay: 900, fadeDuration: 300 });
|
|
494
|
+
terminal.setScrollbar(false); // Keep history/navigation; paint your own chrome.
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Padding is **outside** cells and scrollbar chrome, in CSS pixels, not rows or
|
|
498
|
+
backing-store pixels. `setPadding(8)` sets all four edges; omitted edges in an
|
|
499
|
+
object become zero. Beside mode stays visible without fading while scrollback is
|
|
500
|
+
available, and reserves its gutter even when there is no history. Its default
|
|
501
|
+
frame opacity is always `1`; `hideDelay` and `fadeDuration` apply only to overlay
|
|
502
|
+
mode. Idle beside scrollbars do not schedule fade timers or animation frames.
|
|
503
|
+
Overlay mode auto-hides and does not reduce columns. An auto-sized primary
|
|
504
|
+
can request a new producer grid when the available space changes. Fixed grids
|
|
505
|
+
and secondary/read-only views instead fit the authoritative grid; changing
|
|
506
|
+
chrome never claims primary. Tiny/hidden containers may have no drawable area.
|
|
507
|
+
|
|
508
|
+
`layout` / `onLayoutChange` expose immutable CSS-pixel measurements relative to
|
|
509
|
+
`terminal.element`: outer `width`/`height`, displayed `content` rectangle,
|
|
510
|
+
`scrollbar` rectangle (or `null`), normalized `padding`, and displayed
|
|
511
|
+
`cellWidth`/`cellHeight`. Use the content rectangle for host hit testing, not the
|
|
512
|
+
outer mount box. `padding` and `scrollbar` getters expose normalized current
|
|
513
|
+
settings; the latter is `false` when disabled. Selection, links, input, and
|
|
514
|
+
graphics remain aligned to content, not the added padding/gutter.
|
|
515
|
+
|
|
516
|
+
Layout and marker notifications may occur before `mount()` resolves. Read
|
|
517
|
+
their supplied snapshot during initialization, then initialize host-owned UI
|
|
518
|
+
from the returned handle as well. Related getters update before notification;
|
|
519
|
+
notifications may coalesce and do not form a change log. No notifications run
|
|
520
|
+
after disposal. Check `connected` and `viewport.available` before navigating
|
|
521
|
+
or synchronizing external chrome; retained display state on disconnect does
|
|
522
|
+
not imply an available producer.
|
|
523
|
+
|
|
524
|
+
### Configure the default capsule painter
|
|
525
|
+
|
|
526
|
+
`createDefaultScrollbarRenderer(appearance?)` returns an ordinary synchronous
|
|
527
|
+
`TerminalScrollbarRenderer`. The built-in `renderDefaultScrollbar(frame)` uses
|
|
528
|
+
the same factory with no overrides; there is no separate rendering API or
|
|
529
|
+
controller path for styled defaults. Given a mounted `terminal`:
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
import { createDefaultScrollbarRenderer } from "@hex1b/web-terminal";
|
|
533
|
+
|
|
534
|
+
const painter = createDefaultScrollbarRenderer({
|
|
535
|
+
track: { opacity: 0.12 },
|
|
536
|
+
thumb: { opacity: 0.7 },
|
|
537
|
+
markers: { opacity: 0.85 }
|
|
538
|
+
});
|
|
539
|
+
terminal.setScrollbar({ placement: "beside", render: painter });
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
The readonly `TerminalScrollbarAppearance` has optional `track`, `thumb`, and
|
|
543
|
+
`markers` parts. Each accepts `color?: string` and `opacity?: number`;
|
|
544
|
+
`TerminalScrollbarMarkerAppearance` also accepts `errorColor?: string`.
|
|
545
|
+
|
|
546
|
+
| Part | Default opacity | Default color |
|
|
547
|
+
| --- | --- | --- |
|
|
548
|
+
| Track | `0.35` | `frame.colors.track` |
|
|
549
|
+
| Thumb | `1` | `frame.colors.thumb` |
|
|
550
|
+
| Markers | `1` | Each tick's resolved `color`, distinguishing its kind/outcome; falls back to `frame.colors.marker`/`error` |
|
|
551
|
+
|
|
552
|
+
The default canvas palette is monochrome: a grey thumb (`#999999`) over a
|
|
553
|
+
translucent dark track (`#202020`). Command input, execution, successful
|
|
554
|
+
completion, failed completion, and bookmarks use `#888888`, `#bbbbbb`,
|
|
555
|
+
`#999999`, `#eeeeee`, and `#dddddd`, respectively. Unknown outcomes use
|
|
556
|
+
`#aaaaaa`. These defaults do not inherit the embedding app's accent or danger
|
|
557
|
+
colors. Prompt marks remain omitted from the canvas rail.
|
|
558
|
+
|
|
559
|
+
Embedding CSS can override the live palette through `--cp-scrollbar-track`,
|
|
560
|
+
`--cp-scrollbar-thumb`, `--cp-scrollbar-marker`, `--cp-scrollbar-error`,
|
|
561
|
+
`--cp-scrollbar-command-line`, `--cp-scrollbar-executing`,
|
|
562
|
+
`--cp-scrollbar-success`, and `--cp-scrollbar-custom`. Explicit factory
|
|
563
|
+
`markers.color`/`errorColor` overrides remain available, as do per-marker
|
|
564
|
+
colors. Custom painters receive the resolved default shade on each
|
|
565
|
+
`TerminalScrollbarMarker.color`; `marker.color` is the explicit host override.
|
|
566
|
+
|
|
567
|
+
Part opacity must be finite and between `0` and `1`, inclusive. It **multiplies**
|
|
568
|
+
the frame's fade opacity and incoming `context.globalAlpha`; it does not replace
|
|
569
|
+
either. Color alpha is applied by Canvas2D as usual. A marker's explicit `color`
|
|
570
|
+
takes precedence over configured marker/error colors, then theme fallbacks.
|
|
571
|
+
Invalid per-marker CSS colors retain the safe fallback. The rounded focus
|
|
572
|
+
outline uses the configured thumb color or neutral thumb default and remains
|
|
573
|
+
independent of track/thumb/marker opacity. Thumb dragging suppresses both the
|
|
574
|
+
canvas outline and the DOM focus outline; keyboard focus remains visible.
|
|
575
|
+
|
|
576
|
+
Supplied appearance colors are validated when the factory is called. Use
|
|
577
|
+
nonempty **concrete CSS colors** supported by the browser's Canvas2D parser
|
|
578
|
+
(for example `#8b5cf6`, `rebeccapurple`, or `rgb(139 92 246 / 80%)`), at most
|
|
579
|
+
256 characters. Unresolved `var()`, CSS-wide keywords such as `inherit`,
|
|
580
|
+
`currentColor`, system/context-dependent colors, escapes, and comments are
|
|
581
|
+
rejected rather than silently ignored. Resolve host CSS variables first if
|
|
582
|
+
you want to snapshot their values. **Omit a color to keep theme changes live**:
|
|
583
|
+
the painter reads that fallback from each frame.
|
|
584
|
+
|
|
585
|
+
Options are snapshotted; mutating your original object does not change an
|
|
586
|
+
existing painter. Create and install another renderer to change its overrides.
|
|
587
|
+
The visible thumb is a capsule, inset by `min(2, width / 4)` CSS pixels on each
|
|
588
|
+
side, with radius half its smaller painted dimension. Styling changes painting
|
|
589
|
+
only: track, thumb, marker hit regions, and all navigation APIs remain unchanged.
|
|
590
|
+
|
|
591
|
+
### Custom synchronous painting and fade
|
|
592
|
+
|
|
593
|
+
This complete painter delegates geometry and colors to the default renderer but
|
|
594
|
+
uses a longer quadratic fade. It is the same policy demonstrated by
|
|
595
|
+
[the playground painter](../../samples/WebTerminalDemo/client/scrollbar-renderer.ts).
|
|
596
|
+
This example opts into its own fade policy for overlay placement. The playground
|
|
597
|
+
uses the non-fading default painter for this choice in beside mode.
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
import {
|
|
601
|
+
WebTerminal, renderDefaultScrollbar, type TerminalScrollbarRenderer
|
|
602
|
+
} from "@hex1b/web-terminal";
|
|
603
|
+
|
|
604
|
+
const softFade: TerminalScrollbarRenderer = frame => {
|
|
605
|
+
const { interaction, now } = frame;
|
|
606
|
+
const active = interaction.near || interaction.hovered ||
|
|
607
|
+
interaction.dragging || interaction.focused;
|
|
608
|
+
const elapsed = Math.max(0, now - interaction.lastActivityAt - 1200);
|
|
609
|
+
const remaining = active ? 1 : Math.max(0, 1 - elapsed / 700);
|
|
610
|
+
const opacity = interaction.reducedMotion
|
|
611
|
+
? (active || elapsed === 0 ? 1 : 0)
|
|
612
|
+
: remaining * remaining;
|
|
613
|
+
renderDefaultScrollbar({ ...frame, opacity });
|
|
614
|
+
return !active && opacity > 0; // Request the next animation frame until hidden.
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const host = document.getElementById("terminal");
|
|
618
|
+
if (!host) throw new Error("Missing sized terminal host");
|
|
619
|
+
const terminal = await WebTerminal.mount(host, {
|
|
620
|
+
url: "/ws/terminal",
|
|
621
|
+
padding: 8,
|
|
622
|
+
scrollbar: { placement: "overlay", render: softFade }
|
|
623
|
+
});
|
|
624
|
+
// After changing host-owned painter state or theme colors:
|
|
625
|
+
terminal.refreshScrollbar();
|
|
626
|
+
// On component teardown: terminal.dispose();
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
The callback is synchronous: never return a Promise. `frame.context` is prepared
|
|
630
|
+
for CSS-pixel drawing on `frame.canvas`; context state is isolated between
|
|
631
|
+
calls. The frame includes layout, viewport, nullable `pendingTarget` (the locally
|
|
632
|
+
desired row during navigation), track/thumb rectangles, marker rectangles,
|
|
633
|
+
nullable `hoveredMarker` (the hovered `TerminalScrollbarMarker`, including its bounds),
|
|
634
|
+
interaction state, monotonic `now` and `lastActivityAt`, default
|
|
635
|
+
`opacity`, and resolved theme colors. Return `true` only while another frame is
|
|
636
|
+
needed; an unconditional `true` creates an unnecessary animation loop.
|
|
637
|
+
`refreshScrollbar()` invalidates painting without sending terminal frames.
|
|
638
|
+
Painter failures are local scrollbar errors, not reasons to stop terminal output.
|
|
639
|
+
|
|
640
|
+
Painting does not redefine hit testing: the library owns thumb dragging,
|
|
641
|
+
track paging, marker clicks, proximity activation, and keyboard interaction.
|
|
642
|
+
The thumb takes precedence over overlapping marker ticks so dense markers cannot
|
|
643
|
+
prevent dragging. Alt+ArrowUp/ArrowDown navigates adjacent available markers.
|
|
644
|
+
The default painter respects reduced motion and the embedding theme.
|
|
645
|
+
`markers: false` hides ticks without removing the inventory or disabling
|
|
646
|
+
`scrollToMarker`. Inventory changes briefly reveal the scrollbar, including when
|
|
647
|
+
a bookmark is added after it has faded out. A custom painter can call `renderDefaultScrollbar(frame)` and
|
|
648
|
+
then add decoration; use `frame.colors` or your embedding theme rather than
|
|
649
|
+
assuming a dark terminal.
|
|
650
|
+
|
|
651
|
+
To draw completely different chrome, use the same callback without calling a
|
|
652
|
+
default painter. This snippet uses square geometry and keeps the normal fade;
|
|
653
|
+
the playground's **Custom Canvas2D** mode adds thumb grips and diamond markers.
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
import { type TerminalScrollbarRenderer } from "@hex1b/web-terminal";
|
|
657
|
+
|
|
658
|
+
const squareScrollbar: TerminalScrollbarRenderer = frame => {
|
|
659
|
+
const { context, track, thumb, colors } = frame;
|
|
660
|
+
context.save();
|
|
661
|
+
try {
|
|
662
|
+
const alpha = context.globalAlpha * frame.opacity;
|
|
663
|
+
context.globalAlpha = alpha * 0.2;
|
|
664
|
+
context.fillStyle = colors.track;
|
|
665
|
+
context.fillRect(track.left, track.top, track.width, track.height);
|
|
666
|
+
context.globalAlpha = alpha;
|
|
667
|
+
context.fillStyle = colors.thumb;
|
|
668
|
+
context.fillRect(thumb.left, thumb.top, thumb.width, thumb.height);
|
|
669
|
+
for (const { marker, bounds } of frame.markers) {
|
|
670
|
+
context.fillStyle = marker.exitCode != null && marker.exitCode !== 0
|
|
671
|
+
? colors.error : colors.marker;
|
|
672
|
+
if (marker.color) context.fillStyle = marker.color;
|
|
673
|
+
context.fillRect(bounds.left, bounds.top, bounds.width, bounds.height);
|
|
674
|
+
}
|
|
675
|
+
if (frame.interaction.focused) {
|
|
676
|
+
context.strokeStyle = colors.marker;
|
|
677
|
+
context.lineWidth = 1;
|
|
678
|
+
context.strokeRect(thumb.left + 0.5, thumb.top + 0.5,
|
|
679
|
+
Math.max(0, thumb.width - 1), Math.max(0, thumb.height - 1));
|
|
680
|
+
}
|
|
681
|
+
} finally {
|
|
682
|
+
context.restore();
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
terminal.setScrollbar({ render: squareScrollbar });
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### Marker hover tooltips
|
|
689
|
+
|
|
690
|
+
Canvas marker tooltips are enabled by default. They show a custom bookmark's
|
|
691
|
+
label or retained command details: shell phase, exit code, decoded `cmdline_url`
|
|
692
|
+
when supplied, or raw parameters when no command text was provided. A shell
|
|
693
|
+
mark is **not** proof of a particular command line; the default never invents
|
|
694
|
+
command text. Labels, decoded text, raw parameters, and errors are rendered as
|
|
695
|
+
text, not HTML or navigation links.
|
|
696
|
+
|
|
697
|
+
Use `tooltip: false` to suppress hover content without hiding marker ticks,
|
|
698
|
+
removing the Marks inventory, or disabling marker navigation:
|
|
699
|
+
|
|
700
|
+
```ts
|
|
701
|
+
terminal.setScrollbar({ render: painter, tooltip: false });
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
`tooltip` also accepts a synchronous `TerminalScrollbarTooltipRenderer`:
|
|
705
|
+
`(context: TerminalScrollbarTooltipContext) => HTMLElement | null`. Its readonly
|
|
706
|
+
context contains:
|
|
707
|
+
|
|
708
|
+
| Field | Meaning |
|
|
709
|
+
| --- | --- |
|
|
710
|
+
| `marker` | The retained `TerminalMarker` being hovered. |
|
|
711
|
+
| `anchor` | Marker bounds in terminal-local CSS pixels. |
|
|
712
|
+
| `layout` | Current `TerminalLayout`. |
|
|
713
|
+
| `details` | `TerminalCommandMark` after successful lookup, otherwise `null`. |
|
|
714
|
+
| `loading` | Whether retained command details are being fetched. |
|
|
715
|
+
| `error` | Detail-fetch failure text, otherwise `null`. |
|
|
716
|
+
| `signal` | Aborted when this rendering is replaced or hidden. |
|
|
717
|
+
|
|
718
|
+
Return an element and the library mounts it in a **light-DOM overlay slot**,
|
|
719
|
+
positions it beside the marker, and clamps it inside the terminal. You do not
|
|
720
|
+
need to calculate viewport offsets or install mouse listeners. This is a
|
|
721
|
+
non-interactive hover preview, not a popover of clickable controls.
|
|
722
|
+
|
|
723
|
+
`renderDefaultScrollbarTooltip(context): HTMLElement` builds the same safe
|
|
724
|
+
content as the default. Decorate it without reimplementing command parsing:
|
|
725
|
+
|
|
726
|
+
```ts
|
|
727
|
+
import {
|
|
728
|
+
renderDefaultScrollbarTooltip, type TerminalScrollbarTooltipRenderer
|
|
729
|
+
} from "@hex1b/web-terminal";
|
|
730
|
+
|
|
731
|
+
const tooltip: TerminalScrollbarTooltipRenderer = context => {
|
|
732
|
+
const element = renderDefaultScrollbarTooltip(context);
|
|
733
|
+
// These --cp-* variables belong to this example's embedding application.
|
|
734
|
+
element.style.background = "var(--cp-surface)";
|
|
735
|
+
element.style.color = "var(--cp-text)";
|
|
736
|
+
element.style.borderLeft = "3px solid var(--cp-accent)";
|
|
737
|
+
const heading = document.createElement("strong");
|
|
738
|
+
heading.textContent = context.marker.source === "custom" ? "Bookmark" : "Shell mark";
|
|
739
|
+
heading.style.display = "block";
|
|
740
|
+
element.prepend(heading);
|
|
741
|
+
return element;
|
|
742
|
+
};
|
|
743
|
+
terminal.setScrollbar({ render: painter, tooltip });
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Callbacks run initially (with `loading` for command details), again when details
|
|
747
|
+
or an error arrive, and when relevant geometry changes. **Do not make either
|
|
748
|
+
the painter or tooltip callback `async`, or return a Promise.** The library
|
|
749
|
+
fetches retained details on demand, with the same 8,192 UTF-16-unit bound as
|
|
750
|
+
`getCommandMarkDetails`. Oversized, unavailable, or expired details produce an
|
|
751
|
+
error state rather than truncated or guessed command text. The callback receives
|
|
752
|
+
that state; it does not need to issue its own detail request on every frame.
|
|
753
|
+
|
|
754
|
+
Tooltips are suppressed during thumb dragging and hidden when the pointer
|
|
755
|
+
leaves the marker/terminal, the view disconnects, configuration changes, or the
|
|
756
|
+
terminal is disposed. Replacement/hide aborts the rendering's `signal`, so late
|
|
757
|
+
work cannot resurrect an old tooltip. For externally owned UI, return `null`
|
|
758
|
+
and use that signal to clean up your node. For example, given a host-owned
|
|
759
|
+
`inspector` element:
|
|
760
|
+
|
|
761
|
+
```ts
|
|
762
|
+
terminal.setScrollbar({
|
|
763
|
+
tooltip(context) {
|
|
764
|
+
const element = renderDefaultScrollbarTooltip(context);
|
|
765
|
+
inspector.replaceChildren(element);
|
|
766
|
+
context.signal.addEventListener("abort", () => element.remove(), { once: true });
|
|
767
|
+
return null; // Host owns mounting and placement, not the terminal overlay.
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
These refinements do not change the external layout, viewport, marker,
|
|
773
|
+
navigation, or native-wrapper APIs. `markers: false`, native host chrome, and
|
|
774
|
+
`scrollbar: false` do not acquire canvas hover targets.
|
|
775
|
+
|
|
776
|
+
### Navigation and marker lifetime
|
|
777
|
+
|
|
778
|
+
`scrollToRow(top)` requests an absolute row, clamped to the current scrollable
|
|
779
|
+
range. It does not calculate a relative delta from stale presented state.
|
|
780
|
+
`viewport.pending` distinguishes a requested view from a presented one;
|
|
781
|
+
`viewport.top`, `liveTop`, `totalRows`, and `rowIds` remain authoritative.
|
|
782
|
+
Rejected stale-position requests settle pending state and expose
|
|
783
|
+
`viewport.navigationError`; a new navigation request clears the previous error.
|
|
784
|
+
At the live end, following resumes. Reading history never claims primary,
|
|
785
|
+
and these operations are available in read-only views.
|
|
786
|
+
|
|
787
|
+
The retained `markers` array contains command and custom points with stable
|
|
788
|
+
string `id`, `source`, `buffer`, `row`, and `column`, plus optional shell
|
|
789
|
+
`phase`/`exitCode` or host `label`/`color`. A `null` row means unavailable, **not
|
|
790
|
+
row zero**. Filter by the current buffer before painting an external rail.
|
|
791
|
+
The inventory is not a command-execution event stream. Same-row shell marks
|
|
792
|
+
remain distinct; do not infer command starts from an isolated finish marker.
|
|
793
|
+
Raw command parameters are fetched on demand by command tooltips or an explicit
|
|
794
|
+
`getCommandMarkDetails(id)` call, not included in the marker inventory.
|
|
795
|
+
Details exceeding 8,192 UTF-16 units reject explicitly instead of truncating.
|
|
796
|
+
Large inventories are paged internally and published only after a coherent
|
|
797
|
+
replacement is complete. The accumulated serialized marker index is bounded
|
|
798
|
+
to 8 MiB; exceeding that transport budget fails explicitly rather than exposing
|
|
799
|
+
a silently incomplete inventory.
|
|
800
|
+
|
|
801
|
+
```ts
|
|
802
|
+
const viewport = terminal.viewport;
|
|
803
|
+
if (viewport.available && viewport.rowIds.length) {
|
|
804
|
+
const bookmark = await terminal.addMarker({
|
|
805
|
+
position: {
|
|
806
|
+
generation: viewport.generation,
|
|
807
|
+
rowId: viewport.rowIds[0],
|
|
808
|
+
column: 0
|
|
809
|
+
},
|
|
810
|
+
label: "Review this output"
|
|
811
|
+
});
|
|
812
|
+
// The producer resolves the current anchor, including after retained-text reflow.
|
|
813
|
+
await terminal.scrollToMarker(bookmark.id);
|
|
814
|
+
await terminal.removeMarker(bookmark.id);
|
|
815
|
+
}
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
Registration uses a **presented** producer-backed position. It can reject if
|
|
819
|
+
that position has expired before acceptance. Catch registration/navigation/
|
|
820
|
+
details errors and show them as text. Labels and colors stay in the browser;
|
|
821
|
+
labels and shell details are untrusted and must not be inserted as HTML.
|
|
822
|
+
Anchors track positions, not immutable search matches: recompute search hits
|
|
823
|
+
when their text changes.
|
|
824
|
+
|
|
825
|
+
Surviving text anchors follow supported producer reflow and horizontal character
|
|
826
|
+
insertion/deletion (ICH/DCH). Insertions at a marked cell move the anchor with that
|
|
827
|
+
cell; deleting it or pushing it beyond the right margin collects the marker.
|
|
828
|
+
Insert-mode typing into a blank marked cursor position binds the marker to the
|
|
829
|
+
newly printed text instead. An end-of-row position remains a boundary until the
|
|
830
|
+
following glyph wraps onto the next row. Wide-glyph positions follow the leading
|
|
831
|
+
cell and expire if editing splits and discards the glyph.
|
|
832
|
+
|
|
833
|
+
When backing text is evicted, destructively cleared, reset, or discarded by
|
|
834
|
+
reflow, its markers and
|
|
835
|
+
retained metadata are collected rather than accumulated as unavailable entries.
|
|
836
|
+
Navigation to a collected ID rejects rather than guessing. Main and alternate
|
|
837
|
+
buffers are isolated: switching away from retained main-buffer content does not
|
|
838
|
+
collect its markers; those markers are temporarily unavailable in the other
|
|
839
|
+
buffer. Custom markers belong to their owning view and are also released on
|
|
840
|
+
removal/disconnect/disposal, not carried into a reconnect. Browser-only labels
|
|
841
|
+
and colors are released when their markers leave the authoritative inventory.
|
|
842
|
+
The default server quota is 1,000 custom markers per view, configured through
|
|
843
|
+
`Hex1bTerminalOptions.CustomMarkerLimit`; zero disables registration and exceeding
|
|
844
|
+
the limit rejects explicitly. Collection reclaims custom-marker quota slots.
|
|
845
|
+
Shell markers also have a producer-configured count limit.
|
|
846
|
+
Late direct HWT1 attachment can see retained producer marks. HMP1 negotiates
|
|
847
|
+
**retained text** and **retained OSC 133 command marks** independently, so a late
|
|
848
|
+
relay or fresh reconnect can recover both when supported. Raw command details,
|
|
849
|
+
phase, exit status, and producer IDs are restored without replaying command events.
|
|
850
|
+
Historical graphics and custom/browser-owned markers are not transferred.
|
|
851
|
+
Custom markers still belong to their view and do not survive reconnect.
|
|
852
|
+
|
|
853
|
+
#### HMP1 relay history
|
|
854
|
+
|
|
855
|
+
An HMP1-backed replica needs its own `WithScrollback(capacity)` configuration,
|
|
856
|
+
even when the upstream producer retains history. Local capacity remains
|
|
857
|
+
authoritative: requesting more rows never increases it. For example, this server
|
|
858
|
+
configuration snippet assumes a connected bidirectional HMP1 `stream` and a
|
|
859
|
+
terminal builder named `replicaBuilder`:
|
|
860
|
+
|
|
861
|
+
```csharp
|
|
862
|
+
replicaBuilder
|
|
863
|
+
.WithScrollback(1000)
|
|
864
|
+
.WithHmp1Stream(stream, options =>
|
|
865
|
+
{
|
|
866
|
+
options.ScrollbackHistoryRows = 10_000;
|
|
867
|
+
options.EnableCommandMarkHistory = true;
|
|
868
|
+
});
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
`Hmp1ClientOptions.ScrollbackHistoryRows` defaults to 10,000, accepts 0..100,000,
|
|
872
|
+
and uses `0` to opt out. The producer must have scrollback storage and permit
|
|
873
|
+
transfer through `Hmp1ServerOptions.EnableScrollbackHistory` (or the direct
|
|
874
|
+
`Hmp1PresentationAdapter.EnableScrollbackHistory` property), both defaulting to
|
|
875
|
+
`true`. The first builder listener's setting wins for a shared adapter.
|
|
876
|
+
The separate `EnableCommandMarkHistory` option defaults to `true` on
|
|
877
|
+
`Hmp1ClientOptions`, `Hmp1ServerOptions`, and `Hmp1PresentationAdapter`.
|
|
878
|
+
The server setting is also captured from the first builder listener.
|
|
879
|
+
|
|
880
|
+
This extension does **not** change the HMP1 version. Optional `ClientHello`
|
|
881
|
+
history fields request version `1` and a row limit; `Hello` acknowledges them
|
|
882
|
+
only when supported and enabled. Missing fields preserve screen-only text replay
|
|
883
|
+
with existing HMP1 peers that support the current mandatory `ActivityState`
|
|
884
|
+
baseline, not ancient pre-`ActivityState` peers.
|
|
885
|
+
Command marks use their own `commandMarkHistoryVersion: 1` field in
|
|
886
|
+
`ClientHello` and `Hello`. Missing acknowledgement disables only command-mark
|
|
887
|
+
transfer; negotiated text history still works, and vice versa. Without transferred
|
|
888
|
+
history, only marks backed by the transferred active screen are eligible.
|
|
889
|
+
|
|
890
|
+
After each negotiated screen/activity checkpoint, the replica receives the
|
|
891
|
+
newest contiguous retained suffix, bounded by the requested/accepted row limit,
|
|
892
|
+
32 MiB of row-chunk payloads (including row-length prefixes, not the 8-byte
|
|
893
|
+
header), and two million cells. Complete checkpoints are
|
|
894
|
+
validated before screen, activity, history, and negotiated command marks are atomically applied, then
|
|
895
|
+
graphics and live output resume. Reconnect/resync **replaces**, rather than
|
|
896
|
+
appends to, history, avoiding duplication. An available empty checkpoint clears
|
|
897
|
+
history; an unavailable checkpoint from a relay with a non-supporting upstream
|
|
898
|
+
does not perform an additional history replacement. Explicit history-clearing
|
|
899
|
+
operations in the screen replay still have their normal effect.
|
|
900
|
+
Main-buffer history also transfers while the alternate
|
|
901
|
+
screen is active but stays hidden until returning to the main buffer.
|
|
902
|
+
|
|
903
|
+
Transferred rows preserve graphemes, continuation cells, soft wraps, padding,
|
|
904
|
+
palette colors, and hyperlinks as inert data, never ANSI to execute. Normal
|
|
905
|
+
scrolling output afterward accumulates local history as before. If negotiation
|
|
906
|
+
is absent or disabled, a new replica still starts without pre-attachment history;
|
|
907
|
+
an existing replica retains history according to normal screen/output processing.
|
|
908
|
+
A direct producer-backed
|
|
909
|
+
HWT1 view reads shared retained history without this transport limit.
|
|
910
|
+
|
|
911
|
+
When negotiated, `CommandMarkState` follows activity and any text-history rows.
|
|
912
|
+
It transfers up to 10,000 newest eligible marks in an 8 MiB frame, preserving raw
|
|
913
|
+
OSC 133 parameters (at most 65,536 UTF-8 bytes each), statuses, phases, and IDs.
|
|
914
|
+
Positions refer to the accompanying retained text and active screen, not
|
|
915
|
+
producer-only row identities. Local command-mark and scrollback capacities
|
|
916
|
+
still apply; markers whose backing text was omitted or later redrawn/evicted
|
|
917
|
+
are not kept. Main-history marks remain available after returning from the
|
|
918
|
+
alternate screen, but marks on the untransferred saved main screen are omitted.
|
|
919
|
+
|
|
920
|
+
Available checkpoints replace command history rather than append duplicates;
|
|
921
|
+
unavailable checkpoints do not additionally replace it. ID high-water state
|
|
922
|
+
keeps subsequent live marks aligned even if the latest old record was collected.
|
|
923
|
+
No `CommandMarkAdded` events are synthesized for imported records. On close and
|
|
924
|
+
reattach, restored retained shell marks support normal inventory, details, hover,
|
|
925
|
+
and navigation; custom bookmarks from the closed view are not restored.
|
|
926
|
+
Disable `EnableCommandMarkHistory` to retain legacy locally observed command
|
|
927
|
+
marks independently of the scrollback setting. See
|
|
928
|
+
[CommandMarkState](../../docs/muxer-protocol.md#commandmarkstate-0x10) for the wire contract.
|
|
929
|
+
|
|
930
|
+
These are transport/retention rules, not differences between canvas and HTML
|
|
931
|
+
scrollbars. See the [HMP1 protocol](../../docs/muxer-protocol.md#scrollbackstate-0x0e-and-scrollbackrows-0x0f)
|
|
932
|
+
for wire layout, omitted-row counts, bounds, and failure behavior.
|
|
933
|
+
|
|
934
|
+
### A native HTML scrollbar using only public APIs
|
|
935
|
+
|
|
936
|
+
Keep a fixed terminal mount **beside** an overflowing rail; never place the
|
|
937
|
+
terminal itself inside the spacer. This complete module example uses native
|
|
938
|
+
scrolling and separate clickable marker ticks. It caps the spacer below browser
|
|
939
|
+
scroll-height limits, then maps the browser's **actual** pixel range to rows.
|
|
940
|
+
For a reusable lifecycle-owned version and live mode switching, see
|
|
941
|
+
[`NativeScrollbar`](../../samples/WebTerminalDemo/client/native-scrollbar.ts).
|
|
942
|
+
|
|
943
|
+
```html
|
|
944
|
+
<div id="terminal-wrapper" style="display:flex;width:100%;height:480px">
|
|
945
|
+
<div id="terminal" style="flex:1;min-width:0;overflow:hidden"></div>
|
|
946
|
+
<div id="history" style="display:flex;flex:0 0 32px;align-self:flex-start">
|
|
947
|
+
<div id="ticks" style="position:relative;width:12px"></div>
|
|
948
|
+
<div id="rail" tabindex="0" aria-label="Terminal history"
|
|
949
|
+
style="flex:1;min-width:0;overflow-y:scroll;overflow-x:hidden;overscroll-behavior:contain">
|
|
950
|
+
<div id="spacer" aria-hidden="true" style="width:1px"></div>
|
|
951
|
+
</div>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
<p id="scroll-status" role="status"></p>
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
```js
|
|
958
|
+
import { WebTerminal } from "@hex1b/web-terminal";
|
|
959
|
+
|
|
960
|
+
const host = document.getElementById("terminal");
|
|
961
|
+
const history = document.getElementById("history");
|
|
962
|
+
const rail = document.getElementById("rail");
|
|
963
|
+
const spacer = document.getElementById("spacer");
|
|
964
|
+
const ticks = document.getElementById("ticks");
|
|
965
|
+
const status = document.getElementById("scroll-status");
|
|
966
|
+
const lifetime = new AbortController();
|
|
967
|
+
let terminal, frame = 0, desired, synchronizedTop = 0, markerKey = "";
|
|
968
|
+
const report = error => { status.textContent = String(error); };
|
|
969
|
+
|
|
970
|
+
function update() {
|
|
971
|
+
if (!terminal) return; // Initial notifications can precede mount resolution.
|
|
972
|
+
const { viewport: v, layout: l } = terminal;
|
|
973
|
+
history.style.marginTop = `${l.content.top}px`;
|
|
974
|
+
history.style.height = `${l.content.height}px`;
|
|
975
|
+
history.inert = !terminal.connected || !v.available;
|
|
976
|
+
const maximum = v.available ? v.liveTop : 0;
|
|
977
|
+
spacer.style.height = `${Math.min(8_000_000,
|
|
978
|
+
rail.clientHeight + maximum * l.cellHeight)}px`;
|
|
979
|
+
const range = Math.max(0, rail.scrollHeight - rail.clientHeight);
|
|
980
|
+
if (!v.pending && desired === undefined && !frame)
|
|
981
|
+
rail.scrollTop = maximum ? v.top / maximum * range : 0;
|
|
982
|
+
synchronizedTop = rail.scrollTop;
|
|
983
|
+
|
|
984
|
+
const markers = v.available
|
|
985
|
+
? terminal.markers.filter(m => m.buffer === v.buffer && m.row !== null) : [];
|
|
986
|
+
const nextKey = JSON.stringify([markers, v.totalRows]);
|
|
987
|
+
if (markerKey === nextKey) return;
|
|
988
|
+
markerKey = nextKey;
|
|
989
|
+
ticks.replaceChildren(...markers.map(marker => {
|
|
990
|
+
const tick = document.createElement("button");
|
|
991
|
+
tick.type = "button";
|
|
992
|
+
tick.textContent = "–";
|
|
993
|
+
tick.title = marker.label ?? marker.phase ?? "Bookmark";
|
|
994
|
+
tick.setAttribute("aria-label", tick.title);
|
|
995
|
+
tick.style.cssText = "position:absolute;left:0;padding:0;border:0;" +
|
|
996
|
+
"width:12px;height:6px;line-height:6px;transform:translateY(-50%)";
|
|
997
|
+
tick.style.top = `${marker.row / Math.max(1, v.totalRows - 1) * 100}%`;
|
|
998
|
+
tick.onclick = () => terminal.scrollToMarker(marker.id).catch(report);
|
|
999
|
+
return tick;
|
|
1000
|
+
}));
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
rail.addEventListener("scroll", () => {
|
|
1004
|
+
if (!terminal?.connected || !terminal.viewport.available ||
|
|
1005
|
+
Math.abs(rail.scrollTop - synchronizedTop) < .5) return;
|
|
1006
|
+
synchronizedTop = rail.scrollTop;
|
|
1007
|
+
const range = rail.scrollHeight - rail.clientHeight;
|
|
1008
|
+
desired = range > 0 ? Math.round(rail.scrollTop / range * terminal.viewport.liveTop) : 0;
|
|
1009
|
+
if (!frame) frame = requestAnimationFrame(() => {
|
|
1010
|
+
frame = 0;
|
|
1011
|
+
const target = desired;
|
|
1012
|
+
desired = undefined;
|
|
1013
|
+
if (terminal.connected && target !== undefined) terminal.scrollToRow(target);
|
|
1014
|
+
});
|
|
1015
|
+
}, { signal: lifetime.signal });
|
|
1016
|
+
|
|
1017
|
+
function dispose() {
|
|
1018
|
+
lifetime.abort();
|
|
1019
|
+
cancelAnimationFrame(frame);
|
|
1020
|
+
terminal?.dispose();
|
|
1021
|
+
ticks.replaceChildren();
|
|
1022
|
+
}
|
|
1023
|
+
window.addEventListener("pagehide", dispose, { signal: lifetime.signal });
|
|
1024
|
+
try {
|
|
1025
|
+
terminal = await WebTerminal.mount(host, {
|
|
1026
|
+
url: "/ws/terminal", signal: lifetime.signal, scrollbar: false, padding: 8,
|
|
1027
|
+
onLayoutChange: update, onViewportChange: update, onMarkersChange: update,
|
|
1028
|
+
onClose: () => { history.inert = true; },
|
|
1029
|
+
onStatus: (message, level) => { if (level === "error") report(message); }
|
|
1030
|
+
});
|
|
1031
|
+
update();
|
|
1032
|
+
} catch (error) { report(error); dispose(); }
|
|
1033
|
+
// On SPA component teardown, call dispose() explicitly.
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
Do not synchronize the native thumb to older presentations while navigation is
|
|
1037
|
+
pending; doing so causes feedback oscillation. Coalesce scroll events to one
|
|
1038
|
+
absolute request per animation frame. Native scrollbar appearance and visibility
|
|
1039
|
+
are platform preferences; the spacer does not force a permanently visible OS
|
|
1040
|
+
thumb. At extreme history sizes scaling trades subpixel precision for a bounded
|
|
1041
|
+
DOM extent. The separate marker rail and `scrollToMarker` retain precise
|
|
1042
|
+
producer-backed jumps. The terminal's wheel/key/history controls still work
|
|
1043
|
+
when no built-in scrollbar is painted.
|
|
1044
|
+
|
|
447
1045
|
## Input and clipboard
|
|
448
1046
|
|
|
449
1047
|
Import `InputRoute`, `TerminalAction`, and `defaultInputBindings` to inspect and
|
|
@@ -721,22 +1319,24 @@ errors also use `onStatus`. Detection failure leaves the terminal running.
|
|
|
721
1319
|
Activation failures instead use existing `onInputError`/status handling and
|
|
722
1320
|
never fall back to navigation.
|
|
723
1321
|
|
|
724
|
-
|
|
725
|
-
|
|
1322
|
+
The link-detection worker is included in the same `index.js` bundle as the
|
|
1323
|
+
terminal worker. If your deployment requires explicit worker URLs:
|
|
726
1324
|
|
|
727
1325
|
```ts
|
|
728
1326
|
const terminal = await WebTerminal.mount(container, {
|
|
729
1327
|
url: "/ws/terminal",
|
|
730
|
-
workerUrl: "/web-terminal/terminal-worker
|
|
731
|
-
linkDetectionWorkerUrl: "/web-terminal/link-detection-worker
|
|
1328
|
+
workerUrl: "/web-terminal/index.js#hex1b-terminal-worker",
|
|
1329
|
+
linkDetectionWorkerUrl: "/web-terminal/index.js#hex1b-link-detection-worker",
|
|
732
1330
|
links: { detection: false }
|
|
733
1331
|
});
|
|
734
1332
|
```
|
|
735
1333
|
|
|
736
1334
|
`linkDetectionWorkerUrl` accepts `string | URL`, resolves relative strings
|
|
737
1335
|
against the page like `workerUrl`, and is a mount-time override. Without it the
|
|
738
|
-
entry
|
|
739
|
-
|
|
1336
|
+
entry uses the actual bundle URL with the link-detection fragment, preserving its
|
|
1337
|
+
path, filename, and query. Worker origin/CSP restrictions still apply. For app
|
|
1338
|
+
rebundling and fonts, see [deployment](#module-and-worker-deployment).
|
|
1339
|
+
Rules and matched text are not sent to an external service.
|
|
740
1340
|
See the [opt-in demo](../../samples/WebTerminalDemo/README.md#try-local-link-previews).
|
|
741
1341
|
|
|
742
1342
|
## Selection UI hooks
|
|
@@ -795,10 +1395,14 @@ npm test
|
|
|
795
1395
|
npm pack
|
|
796
1396
|
```
|
|
797
1397
|
|
|
798
|
-
The strict TypeScript build emits
|
|
799
|
-
|
|
800
|
-
`
|
|
801
|
-
|
|
1398
|
+
The strict TypeScript build emits private modules into ignored `.build/`;
|
|
1399
|
+
esbuild bundles the public API and both workers into `dist/index.js`.
|
|
1400
|
+
`dist/` contains that single JavaScript file, `index.js.map`, declarations,
|
|
1401
|
+
declaration maps, and font/license/provenance assets. `.build/` is not shipped.
|
|
1402
|
+
Run `npm run build` **before** `npm test`: private unit tests import `.build/`,
|
|
1403
|
+
while packaging checks exercise the actual `dist/` bundle. Tests use
|
|
1404
|
+
zero-dependency `node:test`, plus strict public-consumer declaration checks in
|
|
1405
|
+
NodeNext and bundler modes.
|
|
802
1406
|
`npm run typecheck` validates sources without emitting.
|
|
803
1407
|
|
|
804
1408
|
`prepack` rebuilds for `npm pack` and manual `npm publish` from this directory.
|