@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "8.6.0-dev.9",
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-dev.4"
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.0.0",
55
- "@stryker-mutator/typescript-checker": "10.0.0",
56
- "@stryker-mutator/vitest-runner": "10.0.0",
57
- "@types/node": "26.6.5",
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.5",
60
- "@vitest/browser-playwright": "5.0.5",
61
- "@vitest/coverage-v8": "5.0.5",
62
- "eslint": "10.12.0",
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.16.2",
65
- "knip": "6.41.0",
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.71.2",
72
- "vitest": "5.0.5"
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.code === "KeyV") {
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.code === "KeyC") {
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.code === "KeyV") {
78
+ if (isShortcutLetter(ev, "v")) {
78
79
  paste();
79
80
  ev.preventDefault();
80
81
  return true;
@@ -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 DS1004 -- the browser tests shorten the re-announce delay so they need not wait out the real one
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;
@@ -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 modifier
990
- // gate: clipboard's Ctrl+Shift+C is a modified key that reads the selection.
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
- // modifier gate that would keep Option+e.
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. Computed only for an
1014
- // "ignore", so the encoder always wins a key it maps.
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" && char === null) {
1018
- return;
1019
- }
1020
- // Modified keys stay with the browser except an AltGr CHARACTER, the way
1021
- // some layouts type it; AltGraph alone is not a reliable signal (Firefox
1022
- // reports it for plain Option on macOS), and Alt+ArrowLeft is browser back.
1023
- const altGraphChar = char !== null && ev.getModifierState("AltGraph");
1024
- if ((ev.ctrlKey || ev.metaKey || ev.altKey) && !altGraphChar) {
1025
- return;
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
- if (ev.key === "Tab") {
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 DS1004 -- the browser tests shorten the settle delay so they need not wait out the real one
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;