ink-cartridge 3.6.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.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -0
  3. package/dist/binary-storage/BinaryStorage.d.ts +308 -0
  4. package/dist/binary-storage/BinaryStorage.js +548 -0
  5. package/dist/binary-storage/StreamingReader.d.ts +53 -0
  6. package/dist/binary-storage/StreamingReader.js +530 -0
  7. package/dist/binary-storage/index.d.ts +91 -0
  8. package/dist/binary-storage/index.js +91 -0
  9. package/dist/binary-storage/types.d.ts +400 -0
  10. package/dist/binary-storage/types.js +15 -0
  11. package/dist/cli/index.d.ts +2 -0
  12. package/dist/cli/index.js +214 -0
  13. package/dist/cli/initTheme.d.ts +14 -0
  14. package/dist/cli/initTheme.js +124 -0
  15. package/dist/cli/makeLanguageType.d.ts +46 -0
  16. package/dist/cli/makeLanguageType.js +264 -0
  17. package/dist/cli/makeThemeType.d.ts +43 -0
  18. package/dist/cli/makeThemeType.js +222 -0
  19. package/dist/components/badge/Badge.d.ts +7 -0
  20. package/dist/components/badge/Badge.js +9 -0
  21. package/dist/components/dialog/ConfirmDialog.d.ts +23 -0
  22. package/dist/components/dialog/ConfirmDialog.js +57 -0
  23. package/dist/components/dialog/types.d.ts +20 -0
  24. package/dist/components/dialog/types.js +1 -0
  25. package/dist/components/divider/Divider.d.ts +8 -0
  26. package/dist/components/divider/Divider.js +10 -0
  27. package/dist/components/fold/Fold.d.ts +3 -0
  28. package/dist/components/fold/Fold.js +44 -0
  29. package/dist/components/fold/types.d.ts +28 -0
  30. package/dist/components/fold/types.js +1 -0
  31. package/dist/components/form/Field.d.ts +30 -0
  32. package/dist/components/form/Field.js +59 -0
  33. package/dist/components/form/Form.d.ts +32 -0
  34. package/dist/components/form/Form.js +163 -0
  35. package/dist/components/form/context.d.ts +14 -0
  36. package/dist/components/form/context.js +20 -0
  37. package/dist/components/form/types.d.ts +115 -0
  38. package/dist/components/form/types.js +1 -0
  39. package/dist/components/key-hint/KeyHint.d.ts +10 -0
  40. package/dist/components/key-hint/KeyHint.js +12 -0
  41. package/dist/components/multi-select/MultiSelectInput.d.ts +125 -0
  42. package/dist/components/multi-select/MultiSelectInput.js +287 -0
  43. package/dist/components/multi-select/types.d.ts +100 -0
  44. package/dist/components/multi-select/types.js +1 -0
  45. package/dist/components/number-input/NumberInput.d.ts +11 -0
  46. package/dist/components/number-input/NumberInput.js +44 -0
  47. package/dist/components/progress-bar/ProgressBar.d.ts +11 -0
  48. package/dist/components/progress-bar/ProgressBar.js +9 -0
  49. package/dist/components/search-input/SearchInput.d.ts +10 -0
  50. package/dist/components/search-input/SearchInput.js +15 -0
  51. package/dist/components/select/SelectInput.d.ts +100 -0
  52. package/dist/components/select/SelectInput.js +147 -0
  53. package/dist/components/select/types.d.ts +63 -0
  54. package/dist/components/select/types.js +1 -0
  55. package/dist/components/spinner/Spinner.d.ts +12 -0
  56. package/dist/components/spinner/Spinner.js +24 -0
  57. package/dist/components/tabs/Tabs.d.ts +3 -0
  58. package/dist/components/tabs/Tabs.js +57 -0
  59. package/dist/components/tabs/types.d.ts +32 -0
  60. package/dist/components/tabs/types.js +1 -0
  61. package/dist/components/text/TextInput.d.ts +39 -0
  62. package/dist/components/text/TextInput.js +233 -0
  63. package/dist/components/text/types.d.ts +62 -0
  64. package/dist/components/text/types.js +1 -0
  65. package/dist/dev/dev-screen.d.ts +36 -0
  66. package/dist/dev/dev-screen.js +142 -0
  67. package/dist/dev/entrance.d.ts +51 -0
  68. package/dist/dev/entrance.js +67 -0
  69. package/dist/dev/globalKey-display.d.ts +2 -0
  70. package/dist/dev/globalKey-display.js +8 -0
  71. package/dist/dev/types.d.ts +9 -0
  72. package/dist/dev/types.js +1 -0
  73. package/dist/index.d.ts +46 -0
  74. package/dist/index.js +50 -0
  75. package/dist/keyboard/check-global-key.d.ts +18 -0
  76. package/dist/keyboard/check-global-key.js +52 -0
  77. package/dist/keyboard/context.d.ts +398 -0
  78. package/dist/keyboard/context.js +9 -0
  79. package/dist/keyboard/global-key-processor/index.d.ts +16 -0
  80. package/dist/keyboard/global-key-processor/index.js +75 -0
  81. package/dist/keyboard/global-sequence-processor/index.d.ts +14 -0
  82. package/dist/keyboard/global-sequence-processor/index.js +165 -0
  83. package/dist/keyboard/hook.d.ts +51 -0
  84. package/dist/keyboard/hook.js +105 -0
  85. package/dist/keyboard/index.d.ts +5 -0
  86. package/dist/keyboard/index.js +3 -0
  87. package/dist/keyboard/keyNormalizer.d.ts +37 -0
  88. package/dist/keyboard/keyNormalizer.js +119 -0
  89. package/dist/keyboard/layer-handler.d.ts +57 -0
  90. package/dist/keyboard/layer-handler.js +274 -0
  91. package/dist/keyboard/modal-processor/index.d.ts +16 -0
  92. package/dist/keyboard/modal-processor/index.js +97 -0
  93. package/dist/keyboard/overlay-processor/index.d.ts +16 -0
  94. package/dist/keyboard/overlay-processor/index.js +30 -0
  95. package/dist/keyboard/pipeline/chain.d.ts +10 -0
  96. package/dist/keyboard/pipeline/chain.js +45 -0
  97. package/dist/keyboard/pipeline/context.d.ts +34 -0
  98. package/dist/keyboard/pipeline/context.js +42 -0
  99. package/dist/keyboard/pipeline/index.d.ts +3 -0
  100. package/dist/keyboard/pipeline/index.js +2 -0
  101. package/dist/keyboard/provider.d.ts +32 -0
  102. package/dist/keyboard/provider.js +985 -0
  103. package/dist/keyboard/screen-stack-processor/index.d.ts +14 -0
  104. package/dist/keyboard/screen-stack-processor/index.js +32 -0
  105. package/dist/keyboard/types.d.ts +632 -0
  106. package/dist/keyboard/types.js +1 -0
  107. package/dist/language/context.d.ts +7 -0
  108. package/dist/language/context.js +7 -0
  109. package/dist/language/hook.d.ts +19 -0
  110. package/dist/language/hook.js +26 -0
  111. package/dist/language/index.d.ts +3 -0
  112. package/dist/language/index.js +2 -0
  113. package/dist/language/provider.d.ts +30 -0
  114. package/dist/language/provider.js +155 -0
  115. package/dist/language/types.d.ts +124 -0
  116. package/dist/language/types.js +1 -0
  117. package/dist/screen/ModalContext.d.ts +11 -0
  118. package/dist/screen/ModalContext.js +12 -0
  119. package/dist/screen/OverlayContext.d.ts +9 -0
  120. package/dist/screen/OverlayContext.js +10 -0
  121. package/dist/screen/context.d.ts +61 -0
  122. package/dist/screen/context.js +8 -0
  123. package/dist/screen/current-screen.d.ts +16 -0
  124. package/dist/screen/current-screen.js +38 -0
  125. package/dist/screen/hook.d.ts +13 -0
  126. package/dist/screen/hook.js +20 -0
  127. package/dist/screen/index.d.ts +7 -0
  128. package/dist/screen/index.js +5 -0
  129. package/dist/screen/provider.d.ts +112 -0
  130. package/dist/screen/provider.js +577 -0
  131. package/dist/screen/registry.d.ts +27 -0
  132. package/dist/screen/registry.js +71 -0
  133. package/dist/screen/types.d.ts +227 -0
  134. package/dist/screen/types.js +1 -0
  135. package/dist/storage/index.d.ts +45 -0
  136. package/dist/storage/index.js +46 -0
  137. package/dist/storage/storage.d.ts +139 -0
  138. package/dist/storage/storage.js +236 -0
  139. package/dist/storage/types.d.ts +145 -0
  140. package/dist/storage/types.js +1 -0
  141. package/dist/theme/context.d.ts +7 -0
  142. package/dist/theme/context.js +7 -0
  143. package/dist/theme/hook.d.ts +11 -0
  144. package/dist/theme/hook.js +18 -0
  145. package/dist/theme/index.d.ts +4 -0
  146. package/dist/theme/index.js +2 -0
  147. package/dist/theme/provider.d.ts +18 -0
  148. package/dist/theme/provider.js +308 -0
  149. package/dist/theme/types.d.ts +53 -0
  150. package/dist/theme/types.js +1 -0
  151. package/package.json +71 -0
@@ -0,0 +1,51 @@
1
+ import { KeyboardContextValue } from "./context.js";
2
+ import type { ModalMissCallback, ModalMissOptions } from "./types.js";
3
+ /**
4
+ * Access the keyboard API from within a React component.
5
+ *
6
+ * Returns `{ boundKeyboard, blockedKey, stop, globalKeys, ... }`.
7
+ *
8
+ * When called inside an overlay component (wrapped in OverlayContext.Provider),
9
+ * keyboard bindings are automatically isolated to the overlay's own layer,
10
+ * keyed by overlay ID. This enables multiple instances of the same component
11
+ * to coexist as separate overlays with independent keyboard state.
12
+ *
13
+ * When called inside a modal component (wrapped in ModalContext.Provider),
14
+ * the same isolation mechanism applies: bindings are scoped to the modal's
15
+ * own layer, keyed by modal ID. This is architecturally symmetric to overlay
16
+ * isolation.
17
+ *
18
+ * Must be used inside a {@link KeyboardProvider}.
19
+ *
20
+ * @throws If no provider is found in the component tree.
21
+ */
22
+ export declare function useKeyboard(): KeyboardContextValue;
23
+ /**
24
+ * Subscribe to the focus state of a named focus target.
25
+ *
26
+ * Returns `true` when the target with the given `focusId` is the currently
27
+ * active focus target on the current screen layer, `false` otherwise.
28
+ *
29
+ * Re-renders the component when the focus target changes (via Tab,
30
+ * `focusSet`, `focusNext`, `focusPrev`, or `focusUnregister`).
31
+ *
32
+ * @param focusId The focus target id to watch.
33
+ * @returns Whether the named target is currently focused.
34
+ */
35
+ export declare function useFocusState(focusId: string): boolean;
36
+ /**
37
+ * Subscribe to unhandled key presses inside a modal.
38
+ *
39
+ * When the active modal receives a key that was not consumed by any
40
+ * binding, the callback is invoked. The definition of "consumed" is
41
+ * controlled by {@link ModalMissOptions}.
42
+ *
43
+ * Only functions when called inside a modal component (where
44
+ * {@link ModalContext} is set). Outside a modal the hook is a silent
45
+ * no-op — the callback is never invoked.
46
+ *
47
+ * @param cb - Callback invoked on every key press in the modal.
48
+ * @param options - Controls which mechanics count as "handled".
49
+ * @returns An unsubscribe function.
50
+ */
51
+ export declare function useModalMissListener(cb: ModalMissCallback, options?: ModalMissOptions): () => void;
@@ -0,0 +1,105 @@
1
+ import { useContext, useEffect, useState } from "react";
2
+ import { KeyboardContext } from "./context.js";
3
+ import { OverlayContext } from "../screen/OverlayContext.js";
4
+ import { ModalContext } from "../screen/ModalContext.js";
5
+ /**
6
+ * Access the keyboard API from within a React component.
7
+ *
8
+ * Returns `{ boundKeyboard, blockedKey, stop, globalKeys, ... }`.
9
+ *
10
+ * When called inside an overlay component (wrapped in OverlayContext.Provider),
11
+ * keyboard bindings are automatically isolated to the overlay's own layer,
12
+ * keyed by overlay ID. This enables multiple instances of the same component
13
+ * to coexist as separate overlays with independent keyboard state.
14
+ *
15
+ * When called inside a modal component (wrapped in ModalContext.Provider),
16
+ * the same isolation mechanism applies: bindings are scoped to the modal's
17
+ * own layer, keyed by modal ID. This is architecturally symmetric to overlay
18
+ * isolation.
19
+ *
20
+ * Must be used inside a {@link KeyboardProvider}.
21
+ *
22
+ * @throws If no provider is found in the component tree.
23
+ */
24
+ export function useKeyboard() {
25
+ const ctx = useContext(KeyboardContext);
26
+ const overlayId = useContext(OverlayContext);
27
+ const modalId = useContext(ModalContext);
28
+ if (!ctx) {
29
+ throw new Error("[Ink-Cartridge] useKeyboard() must be called inside a <KeyboardProvider>.");
30
+ }
31
+ // Manage the owner stack for overlay isolation.
32
+ // When inside an overlay, push the overlay ID as the current owner so
33
+ // that boundKeyboard, blockedKey, stop, and focus functions operate on
34
+ // the overlay's own keyboard layer instead of the screen's layer.
35
+ useEffect(() => {
36
+ if (overlayId) {
37
+ ctx._pushOwner(overlayId);
38
+ return () => {
39
+ ctx._popOwner(overlayId);
40
+ };
41
+ }
42
+ return;
43
+ }, [overlayId, ctx._pushOwner, ctx._popOwner]);
44
+ // Manage the owner stack for modal isolation (symmetric to overlay).
45
+ // When inside a modal, push the modal ID as the current owner so that
46
+ // keyboard functions operate on the modal's own layer.
47
+ useEffect(() => {
48
+ if (modalId) {
49
+ ctx._pushOwner(modalId);
50
+ return () => {
51
+ ctx._popOwner(modalId);
52
+ };
53
+ }
54
+ return;
55
+ }, [modalId, ctx._pushOwner, ctx._popOwner]);
56
+ return ctx;
57
+ }
58
+ /**
59
+ * Subscribe to the focus state of a named focus target.
60
+ *
61
+ * Returns `true` when the target with the given `focusId` is the currently
62
+ * active focus target on the current screen layer, `false` otherwise.
63
+ *
64
+ * Re-renders the component when the focus target changes (via Tab,
65
+ * `focusSet`, `focusNext`, `focusPrev`, or `focusUnregister`).
66
+ *
67
+ * @param focusId The focus target id to watch.
68
+ * @returns Whether the named target is currently focused.
69
+ */
70
+ export function useFocusState(focusId) {
71
+ const { focusCurrent, subscribeFocus } = useKeyboard();
72
+ const [isFocused, setIsFocused] = useState(() => focusCurrent() === focusId);
73
+ useEffect(() => {
74
+ return subscribeFocus(() => {
75
+ setIsFocused(focusCurrent() === focusId);
76
+ });
77
+ }, [focusId, focusCurrent, subscribeFocus]);
78
+ return isFocused;
79
+ }
80
+ /**
81
+ * Subscribe to unhandled key presses inside a modal.
82
+ *
83
+ * When the active modal receives a key that was not consumed by any
84
+ * binding, the callback is invoked. The definition of "consumed" is
85
+ * controlled by {@link ModalMissOptions}.
86
+ *
87
+ * Only functions when called inside a modal component (where
88
+ * {@link ModalContext} is set). Outside a modal the hook is a silent
89
+ * no-op — the callback is never invoked.
90
+ *
91
+ * @param cb - Callback invoked on every key press in the modal.
92
+ * @param options - Controls which mechanics count as "handled".
93
+ * @returns An unsubscribe function.
94
+ */
95
+ export function useModalMissListener(cb, options) {
96
+ const ctx = useContext(KeyboardContext);
97
+ const modalId = useContext(ModalContext);
98
+ useEffect(() => {
99
+ if (!ctx || !modalId)
100
+ return;
101
+ const unsub = ctx.useModalMissListener(cb, options);
102
+ return unsub;
103
+ }, [ctx, modalId, cb, options]);
104
+ return () => { };
105
+ }
@@ -0,0 +1,5 @@
1
+ export { KeyboardProvider } from "./provider.js";
2
+ export type { KeyboardProviderProps } from "./provider.js";
3
+ export { useKeyboard, useFocusState, useModalMissListener } from "./hook.js";
4
+ export { normalizeKeyNames, isNormalCharacter } from "./keyNormalizer.js";
5
+ export type { KeyHandler, BoundKeyboardOptions, BlockedKeyOptions, StopOptions, BoundKeyEntry, ScreenKeyboardLayer, FocusTarget, GlobalKeyEntry, GlobalSequenceEntry, ShortcutOperationEntry, SequenceOperationEntry, SequenceOptions, SequenceBinding, PendingSequence, ModalMissEvent, ModalMissCallback, ModalMissOptions, ResolvedGlobalKeyEntry, } from "./types.js";
@@ -0,0 +1,3 @@
1
+ export { KeyboardProvider } from "./provider.js";
2
+ export { useKeyboard, useFocusState, useModalMissListener } from "./hook.js";
3
+ export { normalizeKeyNames, isNormalCharacter } from "./keyNormalizer.js";
@@ -0,0 +1,37 @@
1
+ import type { Key } from 'ink';
2
+ /**
3
+ * Convert an Ink `(input, key)` event into a list of possible key-name
4
+ * strings for matching.
5
+ *
6
+ * For special keys (return, escape, arrows, etc.) it produces the base
7
+ * name plus any modifier-prefixed variants. For character keys it
8
+ * produces the raw character and modifier combinations.
9
+ *
10
+ * Examples:
11
+ * press('s', { ctrl: true }) → ["s", "ctrl+s"]
12
+ * press('', { escape: true }) → ["escape"]
13
+ * press('', { return: true, shift: true }) → ["return", "shift+return"]
14
+ *
15
+ * @param input - Raw character string from Ink's useInput (empty for special keys).
16
+ * @param key - Full Key descriptor from Ink.
17
+ * @returns An ordered array of key-name strings; first match wins in the pipeline.
18
+ */
19
+ export declare function normalizeKeyNames(input: string, key: Key): string[];
20
+ /**
21
+ * Determine whether the Ink key event represents a "normal" character.
22
+ *
23
+ * Only input with actual character content is eligible, and all special
24
+ * keys (arrows, return, escape, tab, backspace, delete, pageup, pagedown,
25
+ * home, end), modifier keys (ctrl, meta, super, hyper), and release events
26
+ * are excluded.
27
+ *
28
+ * This function drives the wildcard `"*"` binding — only normal characters
29
+ * are ever matched by the wildcard.
30
+ *
31
+ * @param input - Raw character from Ink's useInput.
32
+ * @param key - Full Key descriptor from Ink.
33
+ * @returns true when the event should be treated as a normal character.
34
+ *
35
+ * @2026-06-14 v3.4.0
36
+ */
37
+ export declare function isNormalCharacter(input: string, key: Key): boolean;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Convert an Ink `(input, key)` event into a list of possible key-name
3
+ * strings for matching.
4
+ *
5
+ * For special keys (return, escape, arrows, etc.) it produces the base
6
+ * name plus any modifier-prefixed variants. For character keys it
7
+ * produces the raw character and modifier combinations.
8
+ *
9
+ * Examples:
10
+ * press('s', { ctrl: true }) → ["s", "ctrl+s"]
11
+ * press('', { escape: true }) → ["escape"]
12
+ * press('', { return: true, shift: true }) → ["return", "shift+return"]
13
+ *
14
+ * @param input - Raw character string from Ink's useInput (empty for special keys).
15
+ * @param key - Full Key descriptor from Ink.
16
+ * @returns An ordered array of key-name strings; first match wins in the pipeline.
17
+ */
18
+ export function normalizeKeyNames(input, key) {
19
+ const names = [];
20
+ const specialMap = [
21
+ ['return', 'return'],
22
+ ['escape', 'escape'],
23
+ ['backspace', 'backspace'],
24
+ ['delete', 'delete'],
25
+ ['upArrow', 'up'],
26
+ ['downArrow', 'down'],
27
+ ['leftArrow', 'left'],
28
+ ['rightArrow', 'right'],
29
+ ['tab', 'tab'],
30
+ ['pageDown', 'pagedown'],
31
+ ['pageUp', 'pageup'],
32
+ ['home', 'home'],
33
+ ['end', 'end'],
34
+ ];
35
+ for (const [kProp, kName] of specialMap) {
36
+ if (key[kProp]) {
37
+ names.push(kName);
38
+ if (key.ctrl)
39
+ names.push(`ctrl+${kName}`);
40
+ if (key.shift)
41
+ names.push(`shift+${kName}`);
42
+ if (key.meta)
43
+ names.push(`meta+${kName}`);
44
+ if (key.ctrl && key.shift)
45
+ names.push(`ctrl+shift+${kName}`);
46
+ return names;
47
+ }
48
+ }
49
+ if (input) {
50
+ names.push(input);
51
+ if (key.ctrl)
52
+ names.push(`ctrl+${input}`);
53
+ if (key.shift)
54
+ names.push(`shift+${input}`);
55
+ if (key.meta)
56
+ names.push(`meta+${input}`);
57
+ if (key.ctrl && key.shift)
58
+ names.push(`ctrl+shift+${input}`);
59
+ }
60
+ return names;
61
+ }
62
+ /**
63
+ * Determine whether the Ink key event represents a "normal" character.
64
+ *
65
+ * Only input with actual character content is eligible, and all special
66
+ * keys (arrows, return, escape, tab, backspace, delete, pageup, pagedown,
67
+ * home, end), modifier keys (ctrl, meta, super, hyper), and release events
68
+ * are excluded.
69
+ *
70
+ * This function drives the wildcard `"*"` binding — only normal characters
71
+ * are ever matched by the wildcard.
72
+ *
73
+ * @param input - Raw character from Ink's useInput.
74
+ * @param key - Full Key descriptor from Ink.
75
+ * @returns true when the event should be treated as a normal character.
76
+ *
77
+ * @2026-06-14 v3.4.0
78
+ */
79
+ export function isNormalCharacter(input, key) {
80
+ if (!input)
81
+ return false;
82
+ if (key.upArrow)
83
+ return false;
84
+ if (key.downArrow)
85
+ return false;
86
+ if (key.leftArrow)
87
+ return false;
88
+ if (key.rightArrow)
89
+ return false;
90
+ if (key.pageDown)
91
+ return false;
92
+ if (key.pageUp)
93
+ return false;
94
+ if (key.home)
95
+ return false;
96
+ if (key.end)
97
+ return false;
98
+ if (key.return)
99
+ return false;
100
+ if (key.escape)
101
+ return false;
102
+ if (key.tab)
103
+ return false;
104
+ if (key.backspace)
105
+ return false;
106
+ if (key.delete)
107
+ return false;
108
+ if (key.ctrl)
109
+ return false;
110
+ if (key.meta)
111
+ return false;
112
+ if (key.super)
113
+ return false;
114
+ if (key.hyper)
115
+ return false;
116
+ if (key.eventType === 'release')
117
+ return false;
118
+ return true;
119
+ }
@@ -0,0 +1,57 @@
1
+ import type { Key } from 'ink';
2
+ import type { ScreenKeyboardLayer, BoundKeyEntry, KeyRule } from './types.js';
3
+ /**
4
+ * Check whether a normalized key name is covered by a list of key rules.
5
+ *
6
+ * A key is covered when any rule's key matches, AND the rule either has no
7
+ * `when` condition or the condition evaluates to `true`. When all matching
8
+ * rules have `when` returning `false`, the key is NOT covered.
9
+ */
10
+ export declare function keyMatchesRule(keyName: string, rules: KeyRule[]): boolean;
11
+ /**
12
+ * Iterate through a list of bindings and fire the first matching handler.
13
+ *
14
+ * Matches exact key names first, then falls back to the wildcard `"*"` binding
15
+ * for normal character input (see {@link isNormalCharacter}).
16
+ *
17
+ * A binding fires only when ALL of the following are satisfied (AND relationship):
18
+ * 1. `skipBinding` returns `false` / absent (covers `onlyThis`)
19
+ * 2. `when` returns `true` / absent (covers conditional enablement)
20
+ * 3. Key name matches one in `binding.keys`
21
+ *
22
+ * The short-circuit evaluation order is skipBinding → when → keyMatch.
23
+ * This order does NOT create priority among the conditions — they are
24
+ * logical AND peers. Whether skipBinding is checked before or after when,
25
+ * all must pass for the binding to fire.
26
+ *
27
+ * @param bindings Ordered list of key bindings to try.
28
+ * @param unblockedKeys Normalized key names not blocked at this layer.
29
+ * @param input Raw character from Ink's useInput.
30
+ * @param key Full Key descriptor from Ink.
31
+ * @param skipBinding Optional predicate to skip individual bindings
32
+ * (used for `onlyThis` enforcement).
33
+ * @returns `true` if a binding matched and consumed the event.
34
+ */
35
+ export declare function tryMatchBindings(bindings: BoundKeyEntry[], unblockedKeys: string[], input: string, key: Key, skipBinding?: (binding: BoundKeyEntry) => boolean): boolean;
36
+ /**
37
+ * Built-in Tab / Shift+Tab focus rotation for a given layer.
38
+ *
39
+ * Cycles {@link ScreenKeyboardLayer.currentFocusId} through the layer's
40
+ * {@link ScreenKeyboardLayer.focusOrder} list (Tab forward, Shift+Tab backward).
41
+ * Wraps around at both ends.
42
+ *
43
+ * @returns `true` if a tab event was handled and focus was moved.
44
+ */
45
+ export declare function handleTabNavigation(layer: ScreenKeyboardLayer, eventNames: string[], shift: boolean, notifyFocusChange: () => void): boolean;
46
+ /**
47
+ * Handle a keyboard event against a single layer.
48
+ *
49
+ * Evaluates tab navigation, blocked keys, wildcard priority, sequence
50
+ * matching, focus-target bindings, layer-level bindings, and stopped
51
+ * keys — in that order.
52
+ *
53
+ * @returns true if the event was consumed by this layer.
54
+ *
55
+ * @2026-06-14 v3.4.0
56
+ */
57
+ export declare function handleLayer(layer: ScreenKeyboardLayer, eventNames: string[], input: string, key: Key, isTop: boolean, notifyFocusChange: () => void, activeOverlayCount: number, isOverlay: boolean, wildcardFirst: boolean): boolean;
@@ -0,0 +1,274 @@
1
+ import { isNormalCharacter } from './keyNormalizer.js';
2
+ const DEFAULT_SEQUENCE_TIMEOUT = 500;
3
+ /**
4
+ * Check whether a normalized key name is covered by a list of key rules.
5
+ *
6
+ * A key is covered when any rule's key matches, AND the rule either has no
7
+ * `when` condition or the condition evaluates to `true`. When all matching
8
+ * rules have `when` returning `false`, the key is NOT covered.
9
+ */
10
+ export function keyMatchesRule(keyName, rules) {
11
+ for (const rule of rules) {
12
+ if (rule.key === keyName) {
13
+ if (!rule.when || rule.when())
14
+ return true;
15
+ }
16
+ }
17
+ return false;
18
+ }
19
+ /**
20
+ * Iterate through a list of bindings and fire the first matching handler.
21
+ *
22
+ * Matches exact key names first, then falls back to the wildcard `"*"` binding
23
+ * for normal character input (see {@link isNormalCharacter}).
24
+ *
25
+ * A binding fires only when ALL of the following are satisfied (AND relationship):
26
+ * 1. `skipBinding` returns `false` / absent (covers `onlyThis`)
27
+ * 2. `when` returns `true` / absent (covers conditional enablement)
28
+ * 3. Key name matches one in `binding.keys`
29
+ *
30
+ * The short-circuit evaluation order is skipBinding → when → keyMatch.
31
+ * This order does NOT create priority among the conditions — they are
32
+ * logical AND peers. Whether skipBinding is checked before or after when,
33
+ * all must pass for the binding to fire.
34
+ *
35
+ * @param bindings Ordered list of key bindings to try.
36
+ * @param unblockedKeys Normalized key names not blocked at this layer.
37
+ * @param input Raw character from Ink's useInput.
38
+ * @param key Full Key descriptor from Ink.
39
+ * @param skipBinding Optional predicate to skip individual bindings
40
+ * (used for `onlyThis` enforcement).
41
+ * @returns `true` if a binding matched and consumed the event.
42
+ */
43
+ export function tryMatchBindings(bindings, unblockedKeys, input, key, skipBinding) {
44
+ if (unblockedKeys.length === 0)
45
+ return false;
46
+ for (const binding of bindings) {
47
+ if (skipBinding && skipBinding(binding))
48
+ continue;
49
+ if (binding.when?.() === false)
50
+ continue;
51
+ if (binding.keys.some((k) => unblockedKeys.includes(k))) {
52
+ binding.handler(input, key);
53
+ return true;
54
+ }
55
+ }
56
+ const wildcardBinding = bindings.find(b => b.keys.includes('*'));
57
+ if (wildcardBinding && isNormalCharacter(input, key)) {
58
+ if (!skipBinding || !skipBinding(wildcardBinding)) {
59
+ if (wildcardBinding.when?.() === false)
60
+ return false;
61
+ wildcardBinding.handler(input, key);
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+ /**
68
+ * Built-in Tab / Shift+Tab focus rotation for a given layer.
69
+ *
70
+ * Cycles {@link ScreenKeyboardLayer.currentFocusId} through the layer's
71
+ * {@link ScreenKeyboardLayer.focusOrder} list (Tab forward, Shift+Tab backward).
72
+ * Wraps around at both ends.
73
+ *
74
+ * @returns `true` if a tab event was handled and focus was moved.
75
+ */
76
+ export function handleTabNavigation(layer, eventNames, shift, notifyFocusChange) {
77
+ if (!eventNames.includes('tab') || layer.focusOrder.length === 0)
78
+ return false;
79
+ const current = layer.currentFocusId;
80
+ let idx = current ? layer.focusOrder.indexOf(current) : -1;
81
+ if (shift) {
82
+ idx = idx <= 0 ? layer.focusOrder.length - 1 : idx - 1;
83
+ }
84
+ else {
85
+ idx = (idx + 1) % layer.focusOrder.length;
86
+ }
87
+ layer.currentFocusId = layer.focusOrder[idx];
88
+ notifyFocusChange();
89
+ return true;
90
+ }
91
+ /**
92
+ * Handle a keyboard event against a single layer.
93
+ *
94
+ * Evaluates tab navigation, blocked keys, wildcard priority, sequence
95
+ * matching, focus-target bindings, layer-level bindings, and stopped
96
+ * keys — in that order.
97
+ *
98
+ * @returns true if the event was consumed by this layer.
99
+ *
100
+ * @2026-06-14 v3.4.0
101
+ */
102
+ export function handleLayer(layer, eventNames, input, key, isTop, notifyFocusChange, activeOverlayCount, isOverlay, wildcardFirst) {
103
+ // The reason it has the highest priority is to ensure that tab/shift+tab have the highest priority, avoiding conflicts with business-bound actions.
104
+ // However, when there is no Focus Target in the Current Screen, handleTabNavigation will return false, which allows users to retain flexibility. When tab/shift+tab do not need to be enforced,
105
+ // they can also be bound to business-specific keys.
106
+ if (isTop && handleTabNavigation(layer, eventNames, key.shift, notifyFocusChange))
107
+ return true;
108
+ const blocked = layer.blockedKeys;
109
+ const unblocked = eventNames.filter((n) => !keyMatchesRule(n, blocked));
110
+ // onlyThis semantics differ between screens and overlays:
111
+ // - Screen: skip when any overlay is active (activeOverlayCount > 0)
112
+ // - Overlay: skip only when multiple overlays compete (activeOverlayCount > 1)
113
+ const shouldSkipOnlyThis = (b) => {
114
+ if (!b.onlyThis)
115
+ return false;
116
+ if (isOverlay)
117
+ return activeOverlayCount > 1;
118
+ return activeOverlayCount > 0;
119
+ };
120
+ // Wildcard priority pre-check: when enabled, wildcard `*` bindings
121
+ // are evaluated before sequences, exact matches, and everything else.
122
+ // Only normal characters are affected — special keys fall through.
123
+ if (isTop && wildcardFirst && unblocked.length > 0) {
124
+ // Check focus-target wildcard first
125
+ if (layer.currentFocusId) {
126
+ const ft = layer.focusTargets.get(layer.currentFocusId);
127
+ if (ft) {
128
+ const fBlocked = ft.blockedKeys;
129
+ const fUnblocked = unblocked.filter(n => !keyMatchesRule(n, fBlocked));
130
+ if (fUnblocked.length > 0) {
131
+ const wb = ft.bindings.find(b => b.keys.includes('*'));
132
+ if (wb && isNormalCharacter(input, key)) {
133
+ if (wb.when?.() === false) { /* skip */ }
134
+ else if (!shouldSkipOnlyThis(wb)) {
135
+ wb.handler(input, key);
136
+ return true;
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ // Check screen-level wildcard
143
+ const wb = layer.bindings.find(b => b.keys.includes('*'));
144
+ if (wb && isNormalCharacter(input, key)) {
145
+ if (wb.when?.() === false) { /* skip */ }
146
+ else if (!shouldSkipOnlyThis(wb)) {
147
+ wb.handler(input, key);
148
+ return true;
149
+ }
150
+ }
151
+ }
152
+ // Sequence matching: only for the top layer (isTop).
153
+ // Sequences have priority over ordinary boundKeyboard bindings.
154
+ if (isTop && unblocked.length > 0) {
155
+ const pending = layer.pendingSequence;
156
+ // We already have a pending sequence in progress.
157
+ if (pending !== null) {
158
+ // If the when condition changed to false mid-sequence, cancel
159
+ // the pending sequence and let the key fall through to normal processing.
160
+ if (pending.when?.() === false) {
161
+ clearTimeout(pending.timer);
162
+ layer.pendingSequence = null;
163
+ // Fall through to normal bindings — do NOT return true.
164
+ }
165
+ else {
166
+ const expectedKey = pending.sequences[pending.nextIndex];
167
+ if (unblocked.includes(expectedKey)) {
168
+ // Matched the next key in the sequence.
169
+ clearTimeout(pending.timer);
170
+ pending.nextIndex++;
171
+ if (pending.nextIndex === pending.sequences.length) {
172
+ // Full sequence matched — fire handler.
173
+ pending.handler(input, key);
174
+ layer.pendingSequence = null;
175
+ }
176
+ else {
177
+ // Still waiting for more keys — restart the timeout.
178
+ pending.timer = setTimeout(() => {
179
+ if (layer.pendingSequence === pending)
180
+ layer.pendingSequence = null;
181
+ }, pending.timeout);
182
+ }
183
+ return true;
184
+ }
185
+ else {
186
+ // Mismatch.
187
+ if (pending.options?.exclusive === true) {
188
+ // Exclusive mode: ignore the key, keep waiting.
189
+ return true;
190
+ }
191
+ else {
192
+ // Non-exclusive (default): cancel the sequence and let the key
193
+ // fall through to normal bindings.
194
+ clearTimeout(pending.timer);
195
+ layer.pendingSequence = null;
196
+ }
197
+ }
198
+ }
199
+ }
200
+ // No pending sequence — try to start a new one from the first unblocked key.
201
+ if (layer.pendingSequence === null) {
202
+ // Check each unblocked key name (not just the first) to handle
203
+ // modifier combinations like 'ctrl+w' which appear after 'w'.
204
+ for (const keyName of unblocked) {
205
+ // When ctrl/meta modifier is held (but not shift), a bare key name
206
+ // (without '+') does not represent the keystroke the user intended.
207
+ // normalizeKeyNames expands ctrl+d into ['d', 'ctrl+d']; matching 'd'
208
+ // here would incorrectly start a ['d', 'v'] sequence instead of
209
+ // letting boundKeyboard(['ctrl+d'], ...) consume the event.
210
+ // Shift is exempt because it changes the character (d → D), so the
211
+ // bare key name 'D' faithfully represents shift+d.
212
+ // @2026-06-23 v3.6.1
213
+ if ((key.ctrl || key.meta) && !keyName.includes('+')) {
214
+ continue;
215
+ }
216
+ const candidates = layer.sequences.get(keyName);
217
+ if (!candidates || candidates.length === 0)
218
+ continue;
219
+ // Filter by onlyThis, focusId, and when constraints.
220
+ const selected = candidates.find(binding => {
221
+ if (binding.options?.onlyThis) {
222
+ if (isOverlay)
223
+ return activeOverlayCount <= 1;
224
+ else
225
+ return activeOverlayCount === 0;
226
+ }
227
+ if (binding.options?.focusId) {
228
+ return layer.currentFocusId === binding.options.focusId;
229
+ }
230
+ if (binding.when?.() === false)
231
+ return false;
232
+ return true;
233
+ });
234
+ if (selected) {
235
+ const timeout = selected.timeout ?? DEFAULT_SEQUENCE_TIMEOUT;
236
+ const newSeq = {
237
+ sequences: selected.keys,
238
+ nextIndex: 1,
239
+ handler: selected.handler,
240
+ timer: undefined,
241
+ timeout,
242
+ options: selected.options,
243
+ when: selected.when,
244
+ };
245
+ const timer = setTimeout(() => {
246
+ if (layer.pendingSequence === newSeq)
247
+ layer.pendingSequence = null;
248
+ }, timeout);
249
+ newSeq.timer = timer;
250
+ layer.pendingSequence = newSeq;
251
+ return true;
252
+ }
253
+ }
254
+ }
255
+ }
256
+ if (isTop && layer.currentFocusId) {
257
+ const ft = layer.focusTargets.get(layer.currentFocusId);
258
+ if (ft) {
259
+ const fBlocked = ft.blockedKeys;
260
+ const fUnblocked = unblocked.filter((n) => !keyMatchesRule(n, fBlocked));
261
+ if (tryMatchBindings(ft.bindings, fUnblocked, input, key, shouldSkipOnlyThis))
262
+ return true;
263
+ if (eventNames.some((n) => keyMatchesRule(n, ft.stoppedKeys))) {
264
+ return true;
265
+ }
266
+ }
267
+ }
268
+ if (tryMatchBindings(layer.bindings, unblocked, input, key, shouldSkipOnlyThis))
269
+ return true;
270
+ if (isTop && eventNames.some((n) => keyMatchesRule(n, layer.stoppedKeys))) {
271
+ return true;
272
+ }
273
+ return false;
274
+ }
@@ -0,0 +1,16 @@
1
+ import type { PipelineProcessor } from '../types.js';
2
+ /**
3
+ * Create a processor for the modal stage (stage 0 — highest priority).
4
+ *
5
+ * When a modal is active (activeModalId is set), the modal's keyboard layer
6
+ * receives the event. The processor always returns `true`, consuming every
7
+ * event — even keys not bound in the modal — so that no event reaches
8
+ * lower-priority stages (global keys, overlays, or screens).
9
+ *
10
+ * Before blocking, if the layer has an {@link ScreenKeyboardLayer.onMiss}
11
+ * callback registered (via {@link useModalMissListener}), the processor
12
+ * determines whether the key was handled or missed and invokes the callback.
13
+ *
14
+ * @returns A PipelineProcessor for the modal stage.
15
+ */
16
+ export declare function createModalProcessor(): PipelineProcessor;