@cplieger/web-terminal-ui 8.6.0-dev.9 → 8.7.0-dev.1
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/package.json +16 -15
- package/src/features/clipboard.ts +4 -3
- package/src/kernel/a11y.ts +1 -1
- package/src/kernel/pane.ts +29 -15
- package/src/kernel/shortcut-letter.ts +9 -0
- package/src/viewport.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.7.0-dev.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -44,32 +44,33 @@
|
|
|
44
44
|
"verify": "bash scripts/verify.sh"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@cplieger/web-terminal-engine": "^6.4.0
|
|
47
|
+
"@cplieger/web-terminal-engine": "^6.4.0"
|
|
48
48
|
},
|
|
49
49
|
"overrides": {
|
|
50
|
-
"qs": "^6.15.2"
|
|
50
|
+
"qs": "^6.15.2",
|
|
51
|
+
"minimatch": "^11.0.1"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"@eslint/js": "10.0.1",
|
|
54
|
-
"@stryker-mutator/core": "10.
|
|
55
|
-
"@stryker-mutator/typescript-checker": "10.
|
|
56
|
-
"@stryker-mutator/vitest-runner": "10.
|
|
57
|
-
"@types/node": "26.6.
|
|
55
|
+
"@stryker-mutator/core": "10.1.1",
|
|
56
|
+
"@stryker-mutator/typescript-checker": "10.1.1",
|
|
57
|
+
"@stryker-mutator/vitest-runner": "10.1.1",
|
|
58
|
+
"@types/node": "26.6.6",
|
|
58
59
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
59
|
-
"@vitest/browser": "5.0
|
|
60
|
-
"@vitest/browser-playwright": "5.0
|
|
61
|
-
"@vitest/coverage-v8": "5.0
|
|
62
|
-
"eslint": "10.
|
|
60
|
+
"@vitest/browser": "5.1.0",
|
|
61
|
+
"@vitest/browser-playwright": "5.1.0",
|
|
62
|
+
"@vitest/coverage-v8": "5.1.0",
|
|
63
|
+
"eslint": "10.14.0",
|
|
63
64
|
"fast-check": "4.10.2",
|
|
64
|
-
"html-validate": "11.
|
|
65
|
-
"knip": "6.
|
|
65
|
+
"html-validate": "11.18.0",
|
|
66
|
+
"knip": "6.44.1",
|
|
66
67
|
"playwright": "1.64.1",
|
|
67
68
|
"prettier": "3.9.10",
|
|
68
69
|
"stylelint": "17.16.0",
|
|
69
70
|
"stylelint-config-standard": "40.0.0",
|
|
70
71
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
71
|
-
"typescript-eslint": "8.
|
|
72
|
-
"vitest": "5.0
|
|
72
|
+
"typescript-eslint": "8.72.1",
|
|
73
|
+
"vitest": "5.1.0"
|
|
73
74
|
},
|
|
74
75
|
"publishConfig": {
|
|
75
76
|
"access": "public"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { windowOf } from "../kernel/realm.js";
|
|
2
2
|
import { selectionTextWithin } from "../kernel/selection.js";
|
|
3
|
+
import { isShortcutLetter } from "../kernel/shortcut-letter.js";
|
|
3
4
|
import type { TerminalFeature } from "../kernel/types.js";
|
|
4
5
|
|
|
5
6
|
/** The value a peer reads through `ctx.use(clipboard())`. Both members are
|
|
@@ -62,11 +63,11 @@ export function clipboard(): TerminalFeature<ClipboardApi> {
|
|
|
62
63
|
// Consuming plain Ctrl+V without preventDefault stops the kernel mapping
|
|
63
64
|
// it to \x16 while the native paste still flows into the textarea, so no
|
|
64
65
|
// clipboard read and no Firefox clipboard-read popup.
|
|
65
|
-
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && ev
|
|
66
|
+
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && isShortcutLetter(ev, "v")) {
|
|
66
67
|
return true;
|
|
67
68
|
}
|
|
68
69
|
if (ev.ctrlKey && ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
|
69
|
-
if (ev
|
|
70
|
+
if (isShortcutLetter(ev, "c")) {
|
|
70
71
|
const sel = selectionTextWithin(surface);
|
|
71
72
|
if (sel !== "") {
|
|
72
73
|
copy(sel);
|
|
@@ -74,7 +75,7 @@ export function clipboard(): TerminalFeature<ClipboardApi> {
|
|
|
74
75
|
ev.preventDefault();
|
|
75
76
|
return true;
|
|
76
77
|
}
|
|
77
|
-
if (ev
|
|
78
|
+
if (isShortcutLetter(ev, "v")) {
|
|
78
79
|
paste();
|
|
79
80
|
ev.preventDefault();
|
|
80
81
|
return true;
|
package/src/kernel/a11y.ts
CHANGED
|
@@ -33,7 +33,7 @@ let REANNOUNCE_DELAY_MS = 100;
|
|
|
33
33
|
|
|
34
34
|
/** @internal Test seam: re-set every later announcement after `ms`; returns the
|
|
35
35
|
* previous value so the caller can restore it. */
|
|
36
|
-
// deadset:ignore
|
|
36
|
+
// deadset:ignore DS1102 -- the browser tests shorten the re-announce delay so they need not wait out the real one
|
|
37
37
|
export function setReannounceDelayMs(ms: number): number {
|
|
38
38
|
const previous = REANNOUNCE_DELAY_MS;
|
|
39
39
|
REANNOUNCE_DELAY_MS = ms;
|
package/src/kernel/pane.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { createRegions } from "./regions.js";
|
|
|
14
14
|
import { createAnnouncer, createPaneTablist, type PaneTablist } from "./a11y.js";
|
|
15
15
|
import { createConnState } from "./conn-state.js";
|
|
16
16
|
import { selectionTextWithin } from "./selection.js";
|
|
17
|
+
import { isShortcutLetter } from "./shortcut-letter.js";
|
|
17
18
|
import { primaryPointerFine } from "./keyboard-presence.js";
|
|
18
19
|
import { windowOf } from "./realm.js";
|
|
19
20
|
import {
|
|
@@ -77,6 +78,14 @@ function typedChar(ev: KeyboardEvent): string | null {
|
|
|
77
78
|
return first !== undefined && String.fromCodePoint(first) === ev.key ? ev.key : null;
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/** Ctrl+C (Shift allowed) or Ctrl+Insert: the chords that copy a selection. */
|
|
82
|
+
function isCopyChord(ev: KeyboardEvent): boolean {
|
|
83
|
+
if (!ev.ctrlKey || ev.altKey || ev.metaKey) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return ev.key === "Insert" || isShortcutLetter(ev, "c");
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
/** iOS sends U+00A0 where a space was typed; shared by every send path so one
|
|
81
90
|
* physical key cannot produce two byte sequences. */
|
|
82
91
|
function normalizeTypedText(text: string): string {
|
|
@@ -986,8 +995,9 @@ function buildPaneInto(
|
|
|
986
995
|
if (!ownsSelection() && !keysUnclaimed()) {
|
|
987
996
|
return;
|
|
988
997
|
}
|
|
989
|
-
// Features while the selection is STILL INTACT and before the
|
|
990
|
-
//
|
|
998
|
+
// Features while the selection is STILL INTACT and before the key is
|
|
999
|
+
// encoded: clipboard's Ctrl+Shift+C reads the selection, and its claim on
|
|
1000
|
+
// Ctrl+V keeps 0x16 off the wire.
|
|
991
1001
|
if (runKeydownChain(ev)) {
|
|
992
1002
|
return;
|
|
993
1003
|
}
|
|
@@ -995,7 +1005,7 @@ function buildPaneInto(
|
|
|
995
1005
|
// beforeinput or input, `_tryToHandlePressesEvent:` in
|
|
996
1006
|
// https://github.com/WebKit/WebKit/blob/main/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm),
|
|
997
1007
|
// so a dead key or an IME takes focus and passes through, ahead of the
|
|
998
|
-
//
|
|
1008
|
+
// decision below, which drops a key that types nothing.
|
|
999
1009
|
if (
|
|
1000
1010
|
ev.key === "Dead" ||
|
|
1001
1011
|
ev.key === "Process" ||
|
|
@@ -1010,21 +1020,25 @@ function buildPaneInto(
|
|
|
1010
1020
|
return;
|
|
1011
1021
|
}
|
|
1012
1022
|
// Decided before taking focus, because a bare modifier press (Shift on its
|
|
1013
|
-
// way to Shift+click) must leave the selection alone.
|
|
1014
|
-
//
|
|
1023
|
+
// way to Shift+click) must leave the selection alone. Modifiers alone never
|
|
1024
|
+
// reject a key the encoder maps, as on the focused path.
|
|
1015
1025
|
const result = mapKeyboardEvent(ev, engine.modes);
|
|
1016
1026
|
const char = result.kind === "ignore" ? typedChar(ev) : null;
|
|
1017
|
-
if (result.kind === "ignore"
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1027
|
+
if (result.kind === "ignore") {
|
|
1028
|
+
if (char === null) {
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
// An unencoded character with Ctrl, Alt or Cmd is the browser's (Cmd+V,
|
|
1032
|
+
// Ctrl+1). A desktop AltGr character needs no exception: it arrives with
|
|
1033
|
+
// neither modifier (the engine's encodeKittyDisambiguate cites why), and
|
|
1034
|
+
// Firefox sets AltGraph on every macOS Option chord.
|
|
1035
|
+
if (ev.ctrlKey || ev.metaKey || ev.altKey) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1026
1038
|
}
|
|
1027
|
-
|
|
1039
|
+
// Tab leaves the page (no keyboard trap); a copy chord over this
|
|
1040
|
+
// terminal's selection is the browser's copy, not a key for the shell.
|
|
1041
|
+
if (ev.key === "Tab" || (ownsSelection() && isCopyChord(ev))) {
|
|
1028
1042
|
return;
|
|
1029
1043
|
}
|
|
1030
1044
|
doc.getSelection()?.removeAllRanges();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** True when `ev` is the shortcut letter `letter` (lowercase a-z): its `key`, either
|
|
2
|
+
* case, when the key types an ASCII letter (Dvorak's C is on KeyI), else its physical
|
|
3
|
+
* `Key<letter>` (Russian С on KeyC, or a non-ASCII Latin letter such as é). */
|
|
4
|
+
export function isShortcutLetter(ev: KeyboardEvent, letter: string): boolean {
|
|
5
|
+
if (ev.key.toLowerCase() === letter) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
return ev.code === `Key${letter.toUpperCase()}` && !/^[a-z]$/i.test(ev.key);
|
|
9
|
+
}
|
package/src/viewport.ts
CHANGED
|
@@ -13,7 +13,7 @@ let SETTLE_MS = 350;
|
|
|
13
13
|
|
|
14
14
|
/** @internal Test seam: arm every settle started from now on at `ms`; returns the
|
|
15
15
|
* previous value so the caller can restore it. */
|
|
16
|
-
// deadset:ignore
|
|
16
|
+
// deadset:ignore DS1102 -- the browser tests shorten the settle delay so they need not wait out the real one
|
|
17
17
|
export function setSettleMs(ms: number): number {
|
|
18
18
|
const previous = SETTLE_MS;
|
|
19
19
|
SETTLE_MS = ms;
|