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.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/binary-storage/BinaryStorage.d.ts +308 -0
- package/dist/binary-storage/BinaryStorage.js +548 -0
- package/dist/binary-storage/StreamingReader.d.ts +53 -0
- package/dist/binary-storage/StreamingReader.js +530 -0
- package/dist/binary-storage/index.d.ts +91 -0
- package/dist/binary-storage/index.js +91 -0
- package/dist/binary-storage/types.d.ts +400 -0
- package/dist/binary-storage/types.js +15 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +214 -0
- package/dist/cli/initTheme.d.ts +14 -0
- package/dist/cli/initTheme.js +124 -0
- package/dist/cli/makeLanguageType.d.ts +46 -0
- package/dist/cli/makeLanguageType.js +264 -0
- package/dist/cli/makeThemeType.d.ts +43 -0
- package/dist/cli/makeThemeType.js +222 -0
- package/dist/components/badge/Badge.d.ts +7 -0
- package/dist/components/badge/Badge.js +9 -0
- package/dist/components/dialog/ConfirmDialog.d.ts +23 -0
- package/dist/components/dialog/ConfirmDialog.js +57 -0
- package/dist/components/dialog/types.d.ts +20 -0
- package/dist/components/dialog/types.js +1 -0
- package/dist/components/divider/Divider.d.ts +8 -0
- package/dist/components/divider/Divider.js +10 -0
- package/dist/components/fold/Fold.d.ts +3 -0
- package/dist/components/fold/Fold.js +44 -0
- package/dist/components/fold/types.d.ts +28 -0
- package/dist/components/fold/types.js +1 -0
- package/dist/components/form/Field.d.ts +30 -0
- package/dist/components/form/Field.js +59 -0
- package/dist/components/form/Form.d.ts +32 -0
- package/dist/components/form/Form.js +163 -0
- package/dist/components/form/context.d.ts +14 -0
- package/dist/components/form/context.js +20 -0
- package/dist/components/form/types.d.ts +115 -0
- package/dist/components/form/types.js +1 -0
- package/dist/components/key-hint/KeyHint.d.ts +10 -0
- package/dist/components/key-hint/KeyHint.js +12 -0
- package/dist/components/multi-select/MultiSelectInput.d.ts +125 -0
- package/dist/components/multi-select/MultiSelectInput.js +287 -0
- package/dist/components/multi-select/types.d.ts +100 -0
- package/dist/components/multi-select/types.js +1 -0
- package/dist/components/number-input/NumberInput.d.ts +11 -0
- package/dist/components/number-input/NumberInput.js +44 -0
- package/dist/components/progress-bar/ProgressBar.d.ts +11 -0
- package/dist/components/progress-bar/ProgressBar.js +9 -0
- package/dist/components/search-input/SearchInput.d.ts +10 -0
- package/dist/components/search-input/SearchInput.js +15 -0
- package/dist/components/select/SelectInput.d.ts +100 -0
- package/dist/components/select/SelectInput.js +147 -0
- package/dist/components/select/types.d.ts +63 -0
- package/dist/components/select/types.js +1 -0
- package/dist/components/spinner/Spinner.d.ts +12 -0
- package/dist/components/spinner/Spinner.js +24 -0
- package/dist/components/tabs/Tabs.d.ts +3 -0
- package/dist/components/tabs/Tabs.js +57 -0
- package/dist/components/tabs/types.d.ts +32 -0
- package/dist/components/tabs/types.js +1 -0
- package/dist/components/text/TextInput.d.ts +39 -0
- package/dist/components/text/TextInput.js +233 -0
- package/dist/components/text/types.d.ts +62 -0
- package/dist/components/text/types.js +1 -0
- package/dist/dev/dev-screen.d.ts +36 -0
- package/dist/dev/dev-screen.js +142 -0
- package/dist/dev/entrance.d.ts +51 -0
- package/dist/dev/entrance.js +67 -0
- package/dist/dev/globalKey-display.d.ts +2 -0
- package/dist/dev/globalKey-display.js +8 -0
- package/dist/dev/types.d.ts +9 -0
- package/dist/dev/types.js +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +50 -0
- package/dist/keyboard/check-global-key.d.ts +18 -0
- package/dist/keyboard/check-global-key.js +52 -0
- package/dist/keyboard/context.d.ts +398 -0
- package/dist/keyboard/context.js +9 -0
- package/dist/keyboard/global-key-processor/index.d.ts +16 -0
- package/dist/keyboard/global-key-processor/index.js +75 -0
- package/dist/keyboard/global-sequence-processor/index.d.ts +14 -0
- package/dist/keyboard/global-sequence-processor/index.js +165 -0
- package/dist/keyboard/hook.d.ts +51 -0
- package/dist/keyboard/hook.js +105 -0
- package/dist/keyboard/index.d.ts +5 -0
- package/dist/keyboard/index.js +3 -0
- package/dist/keyboard/keyNormalizer.d.ts +37 -0
- package/dist/keyboard/keyNormalizer.js +119 -0
- package/dist/keyboard/layer-handler.d.ts +57 -0
- package/dist/keyboard/layer-handler.js +274 -0
- package/dist/keyboard/modal-processor/index.d.ts +16 -0
- package/dist/keyboard/modal-processor/index.js +97 -0
- package/dist/keyboard/overlay-processor/index.d.ts +16 -0
- package/dist/keyboard/overlay-processor/index.js +30 -0
- package/dist/keyboard/pipeline/chain.d.ts +10 -0
- package/dist/keyboard/pipeline/chain.js +45 -0
- package/dist/keyboard/pipeline/context.d.ts +34 -0
- package/dist/keyboard/pipeline/context.js +42 -0
- package/dist/keyboard/pipeline/index.d.ts +3 -0
- package/dist/keyboard/pipeline/index.js +2 -0
- package/dist/keyboard/provider.d.ts +32 -0
- package/dist/keyboard/provider.js +985 -0
- package/dist/keyboard/screen-stack-processor/index.d.ts +14 -0
- package/dist/keyboard/screen-stack-processor/index.js +32 -0
- package/dist/keyboard/types.d.ts +632 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/language/context.d.ts +7 -0
- package/dist/language/context.js +7 -0
- package/dist/language/hook.d.ts +19 -0
- package/dist/language/hook.js +26 -0
- package/dist/language/index.d.ts +3 -0
- package/dist/language/index.js +2 -0
- package/dist/language/provider.d.ts +30 -0
- package/dist/language/provider.js +155 -0
- package/dist/language/types.d.ts +124 -0
- package/dist/language/types.js +1 -0
- package/dist/screen/ModalContext.d.ts +11 -0
- package/dist/screen/ModalContext.js +12 -0
- package/dist/screen/OverlayContext.d.ts +9 -0
- package/dist/screen/OverlayContext.js +10 -0
- package/dist/screen/context.d.ts +61 -0
- package/dist/screen/context.js +8 -0
- package/dist/screen/current-screen.d.ts +16 -0
- package/dist/screen/current-screen.js +38 -0
- package/dist/screen/hook.d.ts +13 -0
- package/dist/screen/hook.js +20 -0
- package/dist/screen/index.d.ts +7 -0
- package/dist/screen/index.js +5 -0
- package/dist/screen/provider.d.ts +112 -0
- package/dist/screen/provider.js +577 -0
- package/dist/screen/registry.d.ts +27 -0
- package/dist/screen/registry.js +71 -0
- package/dist/screen/types.d.ts +227 -0
- package/dist/screen/types.js +1 -0
- package/dist/storage/index.d.ts +45 -0
- package/dist/storage/index.js +46 -0
- package/dist/storage/storage.d.ts +139 -0
- package/dist/storage/storage.js +236 -0
- package/dist/storage/types.d.ts +145 -0
- package/dist/storage/types.js +1 -0
- package/dist/theme/context.d.ts +7 -0
- package/dist/theme/context.js +7 -0
- package/dist/theme/hook.d.ts +11 -0
- package/dist/theme/hook.js +18 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +2 -0
- package/dist/theme/provider.d.ts +18 -0
- package/dist/theme/provider.js +308 -0
- package/dist/theme/types.d.ts +53 -0
- package/dist/theme/types.js +1 -0
- 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,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;
|