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,985 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, } from 'react';
|
|
2
|
+
import { useInput } from 'ink';
|
|
3
|
+
import { KeyboardContext } from './context.js';
|
|
4
|
+
import { useScreenSystem } from '../screen/hook.js';
|
|
5
|
+
import { buildPipelineContext } from './pipeline/index.js';
|
|
6
|
+
import { runPipeline } from './pipeline/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Remove keys from {@link ScreenKeyboardLayer.globalKeyOverrides} when no
|
|
9
|
+
* bindings (screen-level or focus-target) still reference them.
|
|
10
|
+
* Keeps the override set consistent after unbind operations.
|
|
11
|
+
*/
|
|
12
|
+
function cleanupGlobalKeyOverrides(layer, keys) {
|
|
13
|
+
for (const k of keys) {
|
|
14
|
+
const stillBound = layer.bindings.some(b => b.keys.includes(k)) ||
|
|
15
|
+
[...layer.focusTargets.values()].some(ft => ft.bindings.some(b => b.keys.includes(k)));
|
|
16
|
+
if (!stillBound) {
|
|
17
|
+
layer.globalKeyOverrides.delete(k);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Remove specific keys from an action's entry in the actionKeysMap.
|
|
23
|
+
* If no keys remain for the action after removal, the entire entry is deleted.
|
|
24
|
+
*
|
|
25
|
+
* Used during unbind to keep the map consistent with the current bindings.
|
|
26
|
+
*/
|
|
27
|
+
function removeKeysFromActionMap(map, actionId, keysToRemove) {
|
|
28
|
+
const arr = map.get(actionId);
|
|
29
|
+
if (!arr)
|
|
30
|
+
return;
|
|
31
|
+
const filtered = arr.filter(k => !keysToRemove.includes(k));
|
|
32
|
+
if (filtered.length === 0) {
|
|
33
|
+
map.delete(actionId);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
map.set(actionId, filtered);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Clear all registered shortcut operations.
|
|
41
|
+
*
|
|
42
|
+
* NOTE: Since the refactoring to per-instance useRef state, this function
|
|
43
|
+
* is a no-op at module level. Shortcut operations are now scoped to each
|
|
44
|
+
* {@link KeyboardProvider} instance and are automatically cleaned up when
|
|
45
|
+
* the provider unmounts.
|
|
46
|
+
*
|
|
47
|
+
* Kept for backward compatibility with tests and external consumers that
|
|
48
|
+
* call this function in cleanup routines.
|
|
49
|
+
*/
|
|
50
|
+
export function clearShortcutOperations() {
|
|
51
|
+
// No-op: state is now per-instance via useRef inside KeyboardProvider
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Keyboard context provider for layered key handling.
|
|
55
|
+
*
|
|
56
|
+
* Manages per-screen-layer key bindings, transparent keys (`blockedKey`),
|
|
57
|
+
* key-stop propagation barriers (`stop`), and global keys (`globalKeys`).
|
|
58
|
+
* Handles the full event priority chain:
|
|
59
|
+
* 1. Global keys with `affectOverlay: true`
|
|
60
|
+
* 2. Broadcast to all active overlays (sorted by zIndex ascending)
|
|
61
|
+
* 3. Global keys with `affectOverlay: false` (default)
|
|
62
|
+
* 4. Screen stack (top → bottom), only if no overlay consumed the event
|
|
63
|
+
* 5. Drop unhandled keys
|
|
64
|
+
*
|
|
65
|
+
* Must be nested inside a {@link ScenarioManagementProvider} so that the
|
|
66
|
+
* current screen path and overlay state are available.
|
|
67
|
+
*/
|
|
68
|
+
export function KeyboardProvider({ children }) {
|
|
69
|
+
const { currentPath, activeOverlayIds, displayedOverlays, activeModalId, displayedModals, } = useScreenSystem();
|
|
70
|
+
// 使用 useRef 替代模块级全局变量,实现多实例隔离
|
|
71
|
+
const pathRef = useRef(currentPath);
|
|
72
|
+
const activeOverlayIdsRef = useRef(new Set());
|
|
73
|
+
const displayedOverlaysRef = useRef(displayedOverlays);
|
|
74
|
+
const activeModalIdRef = useRef(null);
|
|
75
|
+
const globalKeysRef = useRef([]);
|
|
76
|
+
const focusSubscribersRef = useRef(new Set());
|
|
77
|
+
const wildcardPriorityCountRef = useRef(0);
|
|
78
|
+
// Global sequence state: registered entries and current pending state.
|
|
79
|
+
const globalSequencesRef = useRef([]);
|
|
80
|
+
const globalPendingSeqRef = useRef(null);
|
|
81
|
+
const shortcutOperationsRef = useRef(new Map());
|
|
82
|
+
const sequenceOperationsRef = useRef(new Map);
|
|
83
|
+
// Owner stack: top of stack is the current "owner" for keyboard bindings.
|
|
84
|
+
// When inside an overlay, the overlay ID is pushed. When outside, the
|
|
85
|
+
// top component from the screen path is used as fallback.
|
|
86
|
+
const ownerStackRef = useRef([]);
|
|
87
|
+
// 每次渲染同步最新值(无副作用,只是指针赋值)
|
|
88
|
+
pathRef.current = currentPath;
|
|
89
|
+
activeOverlayIdsRef.current = new Set(activeOverlayIds);
|
|
90
|
+
displayedOverlaysRef.current = displayedOverlays;
|
|
91
|
+
activeModalIdRef.current = activeModalId;
|
|
92
|
+
// layersRef now accepts both component types (for screens) and strings (for overlay IDs)
|
|
93
|
+
const layersRef = useRef(new Map());
|
|
94
|
+
const prevPathRef = useRef([]);
|
|
95
|
+
// Clean up layers for removed screens
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const prev = prevPathRef.current;
|
|
98
|
+
for (const comp of prev) {
|
|
99
|
+
if (!currentPath.includes(comp)) {
|
|
100
|
+
const layer = layersRef.current.get(comp);
|
|
101
|
+
if (layer?.pendingSequence) {
|
|
102
|
+
clearTimeout(layer.pendingSequence.timer);
|
|
103
|
+
layer.pendingSequence = null;
|
|
104
|
+
}
|
|
105
|
+
layersRef.current.delete(comp);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
prevPathRef.current = currentPath;
|
|
109
|
+
}, [currentPath]);
|
|
110
|
+
// Clean up layers for removed overlays
|
|
111
|
+
const prevOverlayIdsRef = useRef(new Set());
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const currentIds = new Set(displayedOverlays.map(o => o.id));
|
|
114
|
+
// Only delete keys that were previously known overlay IDs,
|
|
115
|
+
// not all string keys (which may include modal IDs).
|
|
116
|
+
for (const prevId of prevOverlayIdsRef.current) {
|
|
117
|
+
if (!currentIds.has(prevId)) {
|
|
118
|
+
const layer = layersRef.current.get(prevId);
|
|
119
|
+
if (layer?.pendingSequence) {
|
|
120
|
+
clearTimeout(layer.pendingSequence.timer);
|
|
121
|
+
layer.pendingSequence = null;
|
|
122
|
+
}
|
|
123
|
+
layersRef.current.delete(prevId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
prevOverlayIdsRef.current = currentIds;
|
|
127
|
+
}, [displayedOverlays]);
|
|
128
|
+
// Clean up layers for removed modals (symmetric to overlay cleanup)
|
|
129
|
+
const prevModalIdsRef = useRef(new Set());
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const currentIds = new Set(displayedModals.map(m => m.id));
|
|
132
|
+
for (const prevId of prevModalIdsRef.current) {
|
|
133
|
+
if (!currentIds.has(prevId)) {
|
|
134
|
+
const layer = layersRef.current.get(prevId);
|
|
135
|
+
if (layer?.pendingSequence) {
|
|
136
|
+
clearTimeout(layer.pendingSequence.timer);
|
|
137
|
+
layer.pendingSequence = null;
|
|
138
|
+
}
|
|
139
|
+
layersRef.current.delete(prevId);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
prevModalIdsRef.current = currentIds;
|
|
143
|
+
}, [displayedModals]);
|
|
144
|
+
// ---- Owner stack management (internal, used by useKeyboard) ----
|
|
145
|
+
const pushOwner = useCallback((owner) => {
|
|
146
|
+
ownerStackRef.current = [...ownerStackRef.current, owner];
|
|
147
|
+
}, []);
|
|
148
|
+
const popOwner = useCallback((owner) => {
|
|
149
|
+
ownerStackRef.current = ownerStackRef.current.filter(o => o !== owner);
|
|
150
|
+
}, []);
|
|
151
|
+
const enableWildcardPriority = useCallback(() => {
|
|
152
|
+
wildcardPriorityCountRef.current += 1;
|
|
153
|
+
let disabled = false;
|
|
154
|
+
return () => {
|
|
155
|
+
if (disabled)
|
|
156
|
+
return;
|
|
157
|
+
disabled = true;
|
|
158
|
+
wildcardPriorityCountRef.current = Math.max(0, wildcardPriorityCountRef.current - 1);
|
|
159
|
+
};
|
|
160
|
+
}, []);
|
|
161
|
+
// ---- Core keyboard functions ----
|
|
162
|
+
const getLayer = useCallback((owner) => {
|
|
163
|
+
let layer = layersRef.current.get(owner);
|
|
164
|
+
if (!layer) {
|
|
165
|
+
layer = {
|
|
166
|
+
bindings: [],
|
|
167
|
+
blockedKeys: [],
|
|
168
|
+
stoppedKeys: [],
|
|
169
|
+
globalKeyOverrides: new Set(),
|
|
170
|
+
focusTargets: new Map(),
|
|
171
|
+
focusOrder: [],
|
|
172
|
+
currentFocusId: null,
|
|
173
|
+
actionKeysMap: new Map(),
|
|
174
|
+
sequences: new Map(),
|
|
175
|
+
pendingSequence: null,
|
|
176
|
+
};
|
|
177
|
+
layersRef.current.set(owner, layer);
|
|
178
|
+
}
|
|
179
|
+
return layer;
|
|
180
|
+
}, []);
|
|
181
|
+
const getCurrentOwner = useCallback(() => {
|
|
182
|
+
const stack = ownerStackRef.current;
|
|
183
|
+
if (stack.length > 0)
|
|
184
|
+
return stack[stack.length - 1];
|
|
185
|
+
const path = pathRef.current;
|
|
186
|
+
if (path.length === 0)
|
|
187
|
+
return null;
|
|
188
|
+
return path[path.length - 1];
|
|
189
|
+
}, []);
|
|
190
|
+
const notifyFocusChange = useCallback(() => {
|
|
191
|
+
focusSubscribersRef.current.forEach(fn => fn());
|
|
192
|
+
}, []);
|
|
193
|
+
/**
|
|
194
|
+
* Clear the pending sequence timer on a layer, if one is active.
|
|
195
|
+
* Does nothing if the layer has no pending sequence.
|
|
196
|
+
*/
|
|
197
|
+
const clearPendingSequence = useCallback((layer) => {
|
|
198
|
+
if (layer.pendingSequence !== null) {
|
|
199
|
+
clearTimeout(layer.pendingSequence.timer);
|
|
200
|
+
layer.pendingSequence = null;
|
|
201
|
+
}
|
|
202
|
+
}, []);
|
|
203
|
+
const useModalMissListener = useCallback((cb, options) => {
|
|
204
|
+
const owner = getCurrentOwner();
|
|
205
|
+
if (!owner)
|
|
206
|
+
return () => { };
|
|
207
|
+
const layer = getLayer(owner);
|
|
208
|
+
// Per-layer singleton — the most recent caller wins.
|
|
209
|
+
// If multiple components in the same modal call this hook,
|
|
210
|
+
// only the last one's callback is active.
|
|
211
|
+
layer.onMiss = cb;
|
|
212
|
+
layer.onMissOptions = options;
|
|
213
|
+
return () => {
|
|
214
|
+
if (layer.onMiss === cb) {
|
|
215
|
+
layer.onMiss = undefined;
|
|
216
|
+
layer.onMissOptions = undefined;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}, [getCurrentOwner, getLayer]);
|
|
220
|
+
const getOrCreateFocusTarget = useCallback((layer, focusId) => {
|
|
221
|
+
let target = layer.focusTargets.get(focusId);
|
|
222
|
+
if (!target) {
|
|
223
|
+
target = {
|
|
224
|
+
bindings: [],
|
|
225
|
+
blockedKeys: [],
|
|
226
|
+
stoppedKeys: [],
|
|
227
|
+
actionKeysMap: new Map(),
|
|
228
|
+
};
|
|
229
|
+
layer.focusTargets.set(focusId, target);
|
|
230
|
+
layer.focusOrder.push(focusId);
|
|
231
|
+
if (layer.currentFocusId === null) {
|
|
232
|
+
layer.currentFocusId = focusId;
|
|
233
|
+
notifyFocusChange();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return target;
|
|
237
|
+
}, [notifyFocusChange]);
|
|
238
|
+
/**
|
|
239
|
+
* Bind keys on the current layer (screen or overlay).
|
|
240
|
+
*
|
|
241
|
+
* The owner is automatically determined from the owner stack
|
|
242
|
+
* (set by OverlayContext when inside an overlay) or falls back
|
|
243
|
+
* to the top screen component.
|
|
244
|
+
*
|
|
245
|
+
* Overloads:
|
|
246
|
+
* 1. (keys: string | string[], handler: KeyHandler | string, options?: BoundKeyboardOptions)
|
|
247
|
+
* — explicit keys and handler. A single string is normalized to [string].
|
|
248
|
+
* 2. (actionId: string, options: BoundKeyboardOptions)
|
|
249
|
+
* — uses the action's predefined keys
|
|
250
|
+
*/
|
|
251
|
+
const boundKeyboard = useCallback((keysOrActionId, handlerOrOptions, maybeOptions) => {
|
|
252
|
+
function createBoundKeyEntry(keys, handler, onlyThis, owner) {
|
|
253
|
+
if (typeof handler === 'string') {
|
|
254
|
+
const entry = shortcutOperationsRef.current.get(handler);
|
|
255
|
+
if (!entry) {
|
|
256
|
+
throw new Error(`[Ink-Cartridge] The shortcut key you used does not exist with ID ${handler}`);
|
|
257
|
+
}
|
|
258
|
+
return { keys, handler: entry.action, onlyThis, owner };
|
|
259
|
+
}
|
|
260
|
+
return { keys, handler, onlyThis, owner };
|
|
261
|
+
}
|
|
262
|
+
function applyGlobalKeyOverrides(keys, owner, layer, bindingContext) {
|
|
263
|
+
for (const gk of globalKeysRef.current) {
|
|
264
|
+
const gkKeys = Array.isArray(gk.key) ? gk.key : [gk.key];
|
|
265
|
+
const matchingKeys = gkKeys.filter((k) => keys.includes(k));
|
|
266
|
+
if (matchingKeys.length === 0)
|
|
267
|
+
continue;
|
|
268
|
+
const isOverlayOwner = typeof owner === 'string';
|
|
269
|
+
const cat = gk.category;
|
|
270
|
+
let inCategory = false;
|
|
271
|
+
// Category applies to screen components, not overlay IDs
|
|
272
|
+
if (!isOverlayOwner) {
|
|
273
|
+
if (cat === undefined || cat === '*') {
|
|
274
|
+
inCategory = true;
|
|
275
|
+
}
|
|
276
|
+
else if (Array.isArray(cat)) {
|
|
277
|
+
inCategory = cat.includes(owner);
|
|
278
|
+
}
|
|
279
|
+
if (!inCategory)
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const cover = gk.cover ?? true;
|
|
283
|
+
const affectOverlay = gk.affectOverlay ?? false;
|
|
284
|
+
if (isOverlayOwner) {
|
|
285
|
+
// Overlay owners can only override global keys that target overlays
|
|
286
|
+
// (affectOverlay: true). Non-affectOverlay global keys fire after
|
|
287
|
+
// overlays in step 3, so overlays don't need to override them.
|
|
288
|
+
if (!affectOverlay)
|
|
289
|
+
continue;
|
|
290
|
+
if (!cover) {
|
|
291
|
+
throw new Error(`[Ink-Cartridge] Overlay "${owner}" ` +
|
|
292
|
+
`attempted to bind "${matchingKeys[0]}" via ${bindingContext}, ` +
|
|
293
|
+
`but this key is already declared in globalKeys with cover: false, so overriding is not allowed.`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
// Screen owners cannot override global keys that target overlays.
|
|
298
|
+
// Only overlays themselves can override those (the check above).
|
|
299
|
+
if (affectOverlay)
|
|
300
|
+
continue;
|
|
301
|
+
if (!cover) {
|
|
302
|
+
const ownerName = owner.displayName || owner.name || 'anonymous';
|
|
303
|
+
throw new Error(`[Ink-Cartridge] Component "${ownerName}" ` +
|
|
304
|
+
`attempted to bind "${matchingKeys[0]}" via ${bindingContext}, ` +
|
|
305
|
+
`but this key is already declared in globalKeys with cover: false, so overriding is not allowed.`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const k of matchingKeys) {
|
|
309
|
+
layer.globalKeyOverrides.add(k);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// 重载解析:快捷操作模式
|
|
314
|
+
if (typeof keysOrActionId === 'string' && typeof handlerOrOptions !== 'function' && typeof handlerOrOptions !== 'string') {
|
|
315
|
+
const actionId = keysOrActionId;
|
|
316
|
+
const options = handlerOrOptions;
|
|
317
|
+
const entry = shortcutOperationsRef.current.get(actionId);
|
|
318
|
+
if (!entry) {
|
|
319
|
+
throw new Error(`[Ink-Cartridge] Action "${actionId}" is not registered.`);
|
|
320
|
+
}
|
|
321
|
+
if (!entry.keys || entry.keys.length === 0) {
|
|
322
|
+
throw new Error(`[Ink-Cartridge] Action "${actionId}" does not have predefined keys. Please register with keys field or call boundKeyboard with explicit keys.`);
|
|
323
|
+
}
|
|
324
|
+
return boundKeyboard(entry.keys, actionId, options);
|
|
325
|
+
}
|
|
326
|
+
// 原有调用方式
|
|
327
|
+
const keys = Array.isArray(keysOrActionId) ? keysOrActionId : [keysOrActionId];
|
|
328
|
+
const handler = handlerOrOptions;
|
|
329
|
+
const options = maybeOptions;
|
|
330
|
+
const owner = getCurrentOwner();
|
|
331
|
+
if (!owner) {
|
|
332
|
+
throw new Error('[Ink-Cartridge] boundKeyboard() must be called inside a screen component or overlay. There is currently no active screen.');
|
|
333
|
+
}
|
|
334
|
+
// 校验 times 参数
|
|
335
|
+
if (options?.times !== undefined && options.times < 1) {
|
|
336
|
+
throw new Error('[Ink-Cartridge] boundKeyboard() times option must be >= 1.');
|
|
337
|
+
}
|
|
338
|
+
if (options?.times === undefined && options?.observer) {
|
|
339
|
+
throw new Error('[Ink-Cartridge] boundKeyboard() observer option requires times option to be set.');
|
|
340
|
+
}
|
|
341
|
+
const layer = getLayer(owner);
|
|
342
|
+
if (options?.focusId) {
|
|
343
|
+
const fid = options.focusId;
|
|
344
|
+
const target = getOrCreateFocusTarget(layer, fid);
|
|
345
|
+
applyGlobalKeyOverrides(keys, owner, layer, `focusId="${fid}"`);
|
|
346
|
+
const entry = createBoundKeyEntry(keys, handler, options?.onlyThis ?? false, owner);
|
|
347
|
+
entry.when = options?.when;
|
|
348
|
+
target.bindings.push(entry);
|
|
349
|
+
// 如果 handler 是字符串(actionId),将 keys 注册到 focus target 的 actionKeysMap
|
|
350
|
+
if (typeof handler === 'string') {
|
|
351
|
+
const existing = target.actionKeysMap.get(handler) || [];
|
|
352
|
+
for (const k of keys) {
|
|
353
|
+
if (!existing.includes(k))
|
|
354
|
+
existing.push(k);
|
|
355
|
+
}
|
|
356
|
+
target.actionKeysMap.set(handler, existing);
|
|
357
|
+
}
|
|
358
|
+
const doUnbind = () => {
|
|
359
|
+
const idx = target.bindings.indexOf(entry);
|
|
360
|
+
if (idx !== -1)
|
|
361
|
+
target.bindings.splice(idx, 1);
|
|
362
|
+
cleanupGlobalKeyOverrides(layer, entry.keys);
|
|
363
|
+
if (typeof handler === 'string') {
|
|
364
|
+
removeKeysFromActionMap(target.actionKeysMap, handler, keys);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
// Apply times and/or once wrappers
|
|
368
|
+
if (options?.times !== undefined && options.times >= 1) {
|
|
369
|
+
entry.times = options.times;
|
|
370
|
+
entry.pressCount = 0;
|
|
371
|
+
entry.observer = options?.observer;
|
|
372
|
+
const originalHandler = entry.handler;
|
|
373
|
+
entry.handler = (input, key) => {
|
|
374
|
+
entry.pressCount += 1;
|
|
375
|
+
entry.observer?.(entry.times - entry.pressCount);
|
|
376
|
+
if (entry.pressCount < entry.times) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
entry.pressCount = 0;
|
|
380
|
+
if (options?.once) {
|
|
381
|
+
doUnbind();
|
|
382
|
+
}
|
|
383
|
+
originalHandler(input, key);
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
else if (options?.once) {
|
|
387
|
+
const originalHandler = entry.handler;
|
|
388
|
+
entry.handler = (input, key) => {
|
|
389
|
+
doUnbind();
|
|
390
|
+
originalHandler(input, key);
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return doUnbind;
|
|
394
|
+
}
|
|
395
|
+
applyGlobalKeyOverrides(keys, owner, layer, 'boundKeyboard');
|
|
396
|
+
const entry = createBoundKeyEntry(keys, handler, options?.onlyThis ?? false, owner);
|
|
397
|
+
entry.when = options?.when;
|
|
398
|
+
layer.bindings.push(entry);
|
|
399
|
+
// 如果 handler 是字符串(actionId),将 keys 注册到 layer 的 actionKeysMap
|
|
400
|
+
if (typeof handler === 'string') {
|
|
401
|
+
const existing = layer.actionKeysMap.get(handler) || [];
|
|
402
|
+
for (const k of keys) {
|
|
403
|
+
if (!existing.includes(k))
|
|
404
|
+
existing.push(k);
|
|
405
|
+
}
|
|
406
|
+
layer.actionKeysMap.set(handler, existing);
|
|
407
|
+
}
|
|
408
|
+
const doUnbind = () => {
|
|
409
|
+
const idx = layer.bindings.indexOf(entry);
|
|
410
|
+
if (idx !== -1)
|
|
411
|
+
layer.bindings.splice(idx, 1);
|
|
412
|
+
cleanupGlobalKeyOverrides(layer, entry.keys);
|
|
413
|
+
if (typeof handler === 'string') {
|
|
414
|
+
removeKeysFromActionMap(layer.actionKeysMap, handler, keys);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
// Apply times and/or once wrappers
|
|
418
|
+
if (options?.times !== undefined && options.times >= 1) {
|
|
419
|
+
entry.times = options.times;
|
|
420
|
+
entry.pressCount = 0;
|
|
421
|
+
entry.observer = options?.observer;
|
|
422
|
+
const originalHandler = entry.handler;
|
|
423
|
+
entry.handler = (input, key) => {
|
|
424
|
+
entry.pressCount += 1;
|
|
425
|
+
entry.observer?.(entry.times - entry.pressCount);
|
|
426
|
+
if (entry.pressCount < entry.times) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
entry.pressCount = 0;
|
|
430
|
+
if (options?.once) {
|
|
431
|
+
doUnbind();
|
|
432
|
+
}
|
|
433
|
+
originalHandler(input, key);
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
else if (options?.once) {
|
|
437
|
+
const originalHandler = entry.handler;
|
|
438
|
+
entry.handler = (input, key) => {
|
|
439
|
+
doUnbind();
|
|
440
|
+
originalHandler(input, key);
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
return doUnbind;
|
|
444
|
+
}, [getCurrentOwner, getLayer, getOrCreateFocusTarget]);
|
|
445
|
+
/**
|
|
446
|
+
* Mark keys as transparent on the current layer.
|
|
447
|
+
*/
|
|
448
|
+
const penetration = useCallback((keys, options) => {
|
|
449
|
+
const owner = getCurrentOwner();
|
|
450
|
+
if (!owner) {
|
|
451
|
+
throw new Error('[Ink-Cartridge] blockedKey() must be called inside a screen component or overlay.');
|
|
452
|
+
}
|
|
453
|
+
const layer = getLayer(owner);
|
|
454
|
+
const compiledWhen = options?.when;
|
|
455
|
+
if (options?.focusId) {
|
|
456
|
+
const target = getOrCreateFocusTarget(layer, options.focusId);
|
|
457
|
+
const added = [];
|
|
458
|
+
for (const k of keys) {
|
|
459
|
+
const exists = target.blockedKeys.some((r) => r.key === k);
|
|
460
|
+
if (!exists) {
|
|
461
|
+
target.blockedKeys.push({ key: k, when: compiledWhen });
|
|
462
|
+
added.push(k);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return () => {
|
|
466
|
+
target.blockedKeys = target.blockedKeys.filter((r) => !added.includes(r.key));
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const added = [];
|
|
470
|
+
for (const k of keys) {
|
|
471
|
+
const exists = layer.blockedKeys.some((r) => r.key === k);
|
|
472
|
+
if (!exists) {
|
|
473
|
+
layer.blockedKeys.push({ key: k, when: compiledWhen });
|
|
474
|
+
added.push(k);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return () => {
|
|
478
|
+
layer.blockedKeys = layer.blockedKeys.filter((r) => !added.includes(r.key));
|
|
479
|
+
};
|
|
480
|
+
}, [getCurrentOwner, getLayer, getOrCreateFocusTarget]);
|
|
481
|
+
/**
|
|
482
|
+
* Prevent keys from propagating beyond the current layer.
|
|
483
|
+
*/
|
|
484
|
+
const stop = useCallback((keys, options) => {
|
|
485
|
+
const owner = getCurrentOwner();
|
|
486
|
+
if (!owner) {
|
|
487
|
+
throw new Error('[Ink-Cartridge] stop() must be called inside a screen component or overlay.');
|
|
488
|
+
}
|
|
489
|
+
const layer = getLayer(owner);
|
|
490
|
+
// 如果启用 stopAction 模式,则将传入的 action ID 转换为对应的键名
|
|
491
|
+
let effectiveKeys = keys;
|
|
492
|
+
if (options?.stopAction) {
|
|
493
|
+
const map = options.focusId
|
|
494
|
+
? getOrCreateFocusTarget(layer, options.focusId).actionKeysMap
|
|
495
|
+
: layer.actionKeysMap;
|
|
496
|
+
const merged = [];
|
|
497
|
+
const ownerName = typeof owner === 'string' ? owner : (owner.displayName || owner.name || 'Unknown');
|
|
498
|
+
for (const actionId of keys) {
|
|
499
|
+
const boundKeys = map.get(actionId);
|
|
500
|
+
if (!boundKeys) {
|
|
501
|
+
throw new Error(`[Ink-Cartridge] stop(["${actionId}"], { stopAction: true }) on "${ownerName}": ` +
|
|
502
|
+
`action "${actionId}" is not registered or has no keys bound. ` +
|
|
503
|
+
`Register it with defineShortcutAction() and bind it with boundKeyboard() first.`);
|
|
504
|
+
}
|
|
505
|
+
for (const k of boundKeys) {
|
|
506
|
+
if (!merged.includes(k))
|
|
507
|
+
merged.push(k);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
effectiveKeys = merged;
|
|
511
|
+
}
|
|
512
|
+
const compiledWhen = options?.when;
|
|
513
|
+
if (options?.focusId) {
|
|
514
|
+
const target = getOrCreateFocusTarget(layer, options.focusId);
|
|
515
|
+
const added = [];
|
|
516
|
+
for (const k of effectiveKeys) {
|
|
517
|
+
const exists = target.stoppedKeys.some((r) => r.key === k);
|
|
518
|
+
if (!exists) {
|
|
519
|
+
target.stoppedKeys.push({ key: k, when: compiledWhen });
|
|
520
|
+
added.push(k);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return () => {
|
|
524
|
+
target.stoppedKeys = target.stoppedKeys.filter((r) => !added.includes(r.key));
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
const added = [];
|
|
529
|
+
for (const k of effectiveKeys) {
|
|
530
|
+
const exists = layer.stoppedKeys.some((r) => r.key === k);
|
|
531
|
+
if (!exists) {
|
|
532
|
+
layer.stoppedKeys.push({ key: k, when: compiledWhen });
|
|
533
|
+
added.push(k);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return () => {
|
|
537
|
+
layer.stoppedKeys = layer.stoppedKeys.filter((r) => !added.includes(r.key));
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}, [getCurrentOwner, getLayer, getOrCreateFocusTarget]);
|
|
541
|
+
/**
|
|
542
|
+
* Register a multi-key sequence binding.
|
|
543
|
+
*
|
|
544
|
+
* When a sequence's first key is pressed, the layer enters a pending
|
|
545
|
+
* state waiting for subsequent keys. If the full sequence is entered
|
|
546
|
+
* within the timeout, the handler fires. Otherwise the sequence is
|
|
547
|
+
* cancelled.
|
|
548
|
+
*
|
|
549
|
+
* @param keys The ordered key names that make up the sequence
|
|
550
|
+
* (e.g. `['g', 'g']`, `['c', 'w']`). Must have length ≥ 2.
|
|
551
|
+
* @param handler Callback to invoke when the full sequence is matched.
|
|
552
|
+
* @param options Optional: `timeout` (ms, default 500), `onlyThis`,
|
|
553
|
+
* `focusId`, `exclusive` (default false).
|
|
554
|
+
* @returns An unbind function that removes the sequence binding.
|
|
555
|
+
*/
|
|
556
|
+
const boundSequence = useCallback((keysOrActionId, handlerOrOptions, maybeOptions) => {
|
|
557
|
+
// Overload: boundSequence(actionId: string, options?: SequenceOptions)
|
|
558
|
+
// Resolves the action's predefined keys and handler from sequenceOperationsRef.
|
|
559
|
+
if (typeof keysOrActionId === 'string' && (typeof handlerOrOptions === 'undefined' || typeof handlerOrOptions === 'object')) {
|
|
560
|
+
const actionId = keysOrActionId;
|
|
561
|
+
const options = handlerOrOptions;
|
|
562
|
+
const entry = sequenceOperationsRef.current.get(actionId);
|
|
563
|
+
if (!entry) {
|
|
564
|
+
throw new Error(`[Ink-Cartridge] Sequence action "${actionId}" is not registered.`);
|
|
565
|
+
}
|
|
566
|
+
if (!entry.keys || entry.keys.length === 0) {
|
|
567
|
+
throw new Error(`[Ink-Cartridge] Sequence action "${actionId}" does not have predefined keys. Please register with a keys field or call boundSequence with explicit keys.`);
|
|
568
|
+
}
|
|
569
|
+
// Use the action's timeout as default unless overridden by options.
|
|
570
|
+
const mergedOptions = {
|
|
571
|
+
...(entry.timeout !== undefined ? { timeout: entry.timeout } : {}),
|
|
572
|
+
...options,
|
|
573
|
+
};
|
|
574
|
+
return boundSequence(entry.keys, entry.action, mergedOptions);
|
|
575
|
+
}
|
|
576
|
+
const keys = Array.isArray(keysOrActionId) ? keysOrActionId : [keysOrActionId];
|
|
577
|
+
const handler = handlerOrOptions;
|
|
578
|
+
const options = maybeOptions;
|
|
579
|
+
const owner = getCurrentOwner();
|
|
580
|
+
if (!owner) {
|
|
581
|
+
throw new Error('[Ink-Cartridge] boundSequence() must be called inside a screen component or overlay.');
|
|
582
|
+
}
|
|
583
|
+
if (keys.length < 2) {
|
|
584
|
+
throw new Error('[Ink-Cartridge] boundSequence() requires at least 2 keys in the sequence.');
|
|
585
|
+
}
|
|
586
|
+
// Check global sequence cover constraints. Only boundSequence can
|
|
587
|
+
// override a global sequence; when cover: false, attempting to
|
|
588
|
+
// bind a sequence whose first key matches a global sequence with
|
|
589
|
+
// the same first key is forbidden.
|
|
590
|
+
//
|
|
591
|
+
// Screen owners: checked against all global sequences in their
|
|
592
|
+
// category. Overlay owners: only checked against affectOverlay:true
|
|
593
|
+
// global sequences (affectOverlay:false global sequences fire after
|
|
594
|
+
// overlays, so overlays don't need to override them).
|
|
595
|
+
// @2026-06-13 v3.2.0
|
|
596
|
+
const isOverlayOwner = typeof owner === 'string';
|
|
597
|
+
const firstKey = keys[0];
|
|
598
|
+
for (const gs of globalSequencesRef.current) {
|
|
599
|
+
if (gs.cover !== false)
|
|
600
|
+
continue;
|
|
601
|
+
if (gs.keys[0] !== firstKey)
|
|
602
|
+
continue;
|
|
603
|
+
if (isOverlayOwner) {
|
|
604
|
+
// Overlay owners can only override affectOverlay:true global sequences.
|
|
605
|
+
if (!(gs.affectOverlay ?? false))
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
// Screen owners: category check.
|
|
610
|
+
const cat = gs.category;
|
|
611
|
+
if (cat !== undefined && cat !== '*') {
|
|
612
|
+
if (Array.isArray(cat) && !cat.includes(owner))
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const ownerName = isOverlayOwner ? owner : (owner.displayName || owner.name || 'anonymous');
|
|
617
|
+
throw new Error(`[Ink-Cartridge] ${isOverlayOwner ? `Overlay "${ownerName}"` : `Component "${ownerName}"`} ` +
|
|
618
|
+
`attempted to bind sequence [${keys.join(', ')}] via boundSequence, ` +
|
|
619
|
+
`but the first key "${firstKey}" is already declared in globalSequence ` +
|
|
620
|
+
`with cover: false, so overriding is not allowed.`);
|
|
621
|
+
}
|
|
622
|
+
const layer = getLayer(owner);
|
|
623
|
+
const binding = {
|
|
624
|
+
keys,
|
|
625
|
+
handler,
|
|
626
|
+
timeout: options?.timeout,
|
|
627
|
+
options,
|
|
628
|
+
when: options?.when,
|
|
629
|
+
};
|
|
630
|
+
const existing = layer.sequences.get(firstKey) || [];
|
|
631
|
+
existing.push(binding);
|
|
632
|
+
layer.sequences.set(firstKey, existing);
|
|
633
|
+
return () => {
|
|
634
|
+
const arr = layer.sequences.get(firstKey);
|
|
635
|
+
if (arr) {
|
|
636
|
+
const idx = arr.indexOf(binding);
|
|
637
|
+
if (idx !== -1)
|
|
638
|
+
arr.splice(idx, 1);
|
|
639
|
+
if (arr.length === 0)
|
|
640
|
+
layer.sequences.delete(firstKey);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}, [getCurrentOwner, getLayer]);
|
|
644
|
+
const subscribeFocus = useCallback((listener) => {
|
|
645
|
+
focusSubscribersRef.current.add(listener);
|
|
646
|
+
return () => { focusSubscribersRef.current.delete(listener); };
|
|
647
|
+
}, []);
|
|
648
|
+
const focusSet = useCallback((focusId) => {
|
|
649
|
+
const owner = getCurrentOwner();
|
|
650
|
+
if (!owner)
|
|
651
|
+
return;
|
|
652
|
+
const ownerName = typeof owner === 'string' ? owner : (owner.displayName || owner.name || 'Unknown');
|
|
653
|
+
const layer = layersRef.current.get(owner);
|
|
654
|
+
if (!layer) {
|
|
655
|
+
throw new Error(`focusSet("${focusId}"): no keyboard layer found for "${ownerName}". ` +
|
|
656
|
+
`Did you forget to wrap the screen in <KeyboardProvider>?`);
|
|
657
|
+
}
|
|
658
|
+
clearPendingSequence(layer);
|
|
659
|
+
if (!layer.focusTargets.has(focusId)) {
|
|
660
|
+
const available = layer.focusOrder.length > 0
|
|
661
|
+
? layer.focusOrder.map(id => `"${id}"`).join(', ')
|
|
662
|
+
: '(none)';
|
|
663
|
+
throw new Error(`focusSet("${focusId}"): focus target not found on "${ownerName}". ` +
|
|
664
|
+
`Available targets: ${available}`);
|
|
665
|
+
}
|
|
666
|
+
if (layer.currentFocusId !== focusId) {
|
|
667
|
+
layer.currentFocusId = focusId;
|
|
668
|
+
notifyFocusChange();
|
|
669
|
+
}
|
|
670
|
+
}, [getCurrentOwner, notifyFocusChange, clearPendingSequence]);
|
|
671
|
+
const focusNext = useCallback(() => {
|
|
672
|
+
const owner = getCurrentOwner();
|
|
673
|
+
if (!owner)
|
|
674
|
+
return;
|
|
675
|
+
const layer = layersRef.current.get(owner);
|
|
676
|
+
if (!layer || layer.focusOrder.length === 0)
|
|
677
|
+
return;
|
|
678
|
+
clearPendingSequence(layer);
|
|
679
|
+
const current = layer.currentFocusId;
|
|
680
|
+
let idx = current ? layer.focusOrder.indexOf(current) : -1;
|
|
681
|
+
idx = (idx + 1) % layer.focusOrder.length;
|
|
682
|
+
layer.currentFocusId = layer.focusOrder[idx];
|
|
683
|
+
notifyFocusChange();
|
|
684
|
+
}, [getCurrentOwner, notifyFocusChange, clearPendingSequence]);
|
|
685
|
+
const focusPrev = useCallback(() => {
|
|
686
|
+
const owner = getCurrentOwner();
|
|
687
|
+
if (!owner)
|
|
688
|
+
return;
|
|
689
|
+
const layer = layersRef.current.get(owner);
|
|
690
|
+
if (!layer || layer.focusOrder.length === 0)
|
|
691
|
+
return;
|
|
692
|
+
clearPendingSequence(layer);
|
|
693
|
+
const current = layer.currentFocusId;
|
|
694
|
+
let idx = current ? layer.focusOrder.indexOf(current) : -1;
|
|
695
|
+
idx = idx <= 0 ? layer.focusOrder.length - 1 : idx - 1;
|
|
696
|
+
layer.currentFocusId = layer.focusOrder[idx];
|
|
697
|
+
notifyFocusChange();
|
|
698
|
+
}, [getCurrentOwner, notifyFocusChange, clearPendingSequence]);
|
|
699
|
+
const focusCurrent = useCallback(() => {
|
|
700
|
+
const owner = getCurrentOwner();
|
|
701
|
+
if (!owner)
|
|
702
|
+
return null;
|
|
703
|
+
const layer = layersRef.current.get(owner);
|
|
704
|
+
return layer?.currentFocusId ?? null;
|
|
705
|
+
}, [getCurrentOwner]);
|
|
706
|
+
const focusUnregister = useCallback((focusId) => {
|
|
707
|
+
const owner = getCurrentOwner();
|
|
708
|
+
if (!owner)
|
|
709
|
+
return;
|
|
710
|
+
const layer = layersRef.current.get(owner);
|
|
711
|
+
if (!layer)
|
|
712
|
+
return;
|
|
713
|
+
const wasFocused = layer.currentFocusId === focusId;
|
|
714
|
+
layer.focusTargets.delete(focusId);
|
|
715
|
+
layer.focusOrder = layer.focusOrder.filter(id => id !== focusId);
|
|
716
|
+
if (wasFocused) {
|
|
717
|
+
layer.currentFocusId =
|
|
718
|
+
layer.focusOrder.length > 0 ? layer.focusOrder[0] : null;
|
|
719
|
+
notifyFocusChange();
|
|
720
|
+
}
|
|
721
|
+
}, [getCurrentOwner, notifyFocusChange]);
|
|
722
|
+
/**
|
|
723
|
+
* Register global key bindings.
|
|
724
|
+
*/
|
|
725
|
+
const globalKeys = useCallback((entries, options) => {
|
|
726
|
+
const processed = entries.map((each) => {
|
|
727
|
+
if (each.times !== undefined && each.times < 1) {
|
|
728
|
+
throw new Error('[Ink-Cartridge] globalKeys() times option must be >= 1.');
|
|
729
|
+
}
|
|
730
|
+
if (each.times === undefined && each.observer) {
|
|
731
|
+
throw new Error('[Ink-Cartridge] globalKeys() observer option requires times option to be set.');
|
|
732
|
+
}
|
|
733
|
+
if (typeof each.operate === 'string') {
|
|
734
|
+
const entry = shortcutOperationsRef.current.get(each.operate);
|
|
735
|
+
if (!entry) {
|
|
736
|
+
throw new Error(`[Ink-Cartridge]You want to call the shortcut ${each.operate} in the global key, but it is not registered`);
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
key: each.key,
|
|
740
|
+
operate: entry.action,
|
|
741
|
+
cover: each.cover,
|
|
742
|
+
category: each.category,
|
|
743
|
+
affectOverlay: each.affectOverlay,
|
|
744
|
+
times: each.times,
|
|
745
|
+
pressCount: each.times !== undefined ? 0 : undefined,
|
|
746
|
+
observer: each.observer,
|
|
747
|
+
executeWhenNoOverlay: each.executeWhenNoOverlay,
|
|
748
|
+
when: each.when,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
key: each.key,
|
|
753
|
+
operate: each.operate,
|
|
754
|
+
cover: each.cover,
|
|
755
|
+
category: each.category,
|
|
756
|
+
affectOverlay: each.affectOverlay,
|
|
757
|
+
times: each.times,
|
|
758
|
+
pressCount: each.times !== undefined ? 0 : undefined,
|
|
759
|
+
observer: each.observer,
|
|
760
|
+
executeWhenNoOverlay: each.executeWhenNoOverlay,
|
|
761
|
+
when: each.when,
|
|
762
|
+
};
|
|
763
|
+
});
|
|
764
|
+
if (options?.mode === 'add') {
|
|
765
|
+
globalKeysRef.current = [...globalKeysRef.current, ...processed];
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
globalKeysRef.current = processed;
|
|
769
|
+
}
|
|
770
|
+
}, []);
|
|
771
|
+
/**
|
|
772
|
+
* Return a shallow snapshot of the current global key list.
|
|
773
|
+
* Safe for read-only inspection; mutating the returned array has no
|
|
774
|
+
* effect on the live bindings.
|
|
775
|
+
*/
|
|
776
|
+
const getGlobalKeys = useCallback(() => [...globalKeysRef.current], []);
|
|
777
|
+
/**
|
|
778
|
+
* Register global sequence key bindings.
|
|
779
|
+
*
|
|
780
|
+
* Validates each entry (keys length ≥ 2), clears any active global pending
|
|
781
|
+
* sequence on replace, and stores entries for evaluation in the useInput
|
|
782
|
+
* event chain.
|
|
783
|
+
*/
|
|
784
|
+
const globalSequence = useCallback((entries, options) => {
|
|
785
|
+
// Resolve string operate references to actual functions from
|
|
786
|
+
// sequenceOperationsRef, mirroring how globalKeys resolves
|
|
787
|
+
// shortcut action IDs.
|
|
788
|
+
const resolved = entries.map((entry) => {
|
|
789
|
+
if (typeof entry.operate === 'string') {
|
|
790
|
+
const actionEntry = sequenceOperationsRef.current.get(entry.operate);
|
|
791
|
+
if (!actionEntry) {
|
|
792
|
+
throw new Error(`[Ink-Cartridge] You want to call the sequence action "${entry.operate}" in globalSequence, but it is not registered.`);
|
|
793
|
+
}
|
|
794
|
+
return { ...entry, operate: actionEntry.action };
|
|
795
|
+
}
|
|
796
|
+
// TS narrows entry.operate to () => void after the typeof check above.
|
|
797
|
+
return { ...entry, operate: entry.operate };
|
|
798
|
+
});
|
|
799
|
+
for (const entry of resolved) {
|
|
800
|
+
if (entry.keys.length < 2) {
|
|
801
|
+
throw new Error('[Ink-Cartridge] globalSequence() requires at least 2 keys per sequence.');
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (options?.mode === 'add') {
|
|
805
|
+
globalSequencesRef.current = [...globalSequencesRef.current, ...resolved];
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
// Clear any active pending sequence when replacing all entries.
|
|
809
|
+
if (globalPendingSeqRef.current) {
|
|
810
|
+
clearTimeout(globalPendingSeqRef.current.timer);
|
|
811
|
+
globalPendingSeqRef.current = null;
|
|
812
|
+
}
|
|
813
|
+
globalSequencesRef.current = resolved;
|
|
814
|
+
}
|
|
815
|
+
}, []);
|
|
816
|
+
const defineShortcutAction = useCallback((entries) => {
|
|
817
|
+
for (const each of entries) {
|
|
818
|
+
if (shortcutOperationsRef.current.has(each.actionId)) {
|
|
819
|
+
throw new Error(`[Ink-Cartridge]Duplicate shortcut cannot be defined with ID ${each.actionId}`);
|
|
820
|
+
}
|
|
821
|
+
shortcutOperationsRef.current.set(each.actionId, {
|
|
822
|
+
action: each.action,
|
|
823
|
+
keys: each.keys,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}, []);
|
|
827
|
+
const defineSequenceAction = useCallback((entries) => {
|
|
828
|
+
for (const each of entries) {
|
|
829
|
+
if (sequenceOperationsRef.current.has(each.sequenceActionId)) {
|
|
830
|
+
throw new Error(`[Ink-Cartridge]Sequence Action ${each.sequenceActionId} may not be defined repeatedly`);
|
|
831
|
+
}
|
|
832
|
+
sequenceOperationsRef.current.set(each.sequenceActionId, {
|
|
833
|
+
action: each.action,
|
|
834
|
+
keys: each.keys,
|
|
835
|
+
timeout: each.timeout,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}, []);
|
|
839
|
+
const modifySequenceAction = useCallback((actionId, keys, timeout) => {
|
|
840
|
+
const entry = sequenceOperationsRef.current.get(actionId);
|
|
841
|
+
if (!entry) {
|
|
842
|
+
throw new Error(`[Ink-Cartridge]Key not registered to Sequence Action cannot be modified, target ID is ${actionId}`);
|
|
843
|
+
}
|
|
844
|
+
if (entry.keys === undefined) {
|
|
845
|
+
throw new Error(`[Ink-Cartridge]The target Sequence Action has no preset Keys. You cannot modify it. The ID is ${actionId}.`);
|
|
846
|
+
}
|
|
847
|
+
entry.keys = keys;
|
|
848
|
+
if (timeout) {
|
|
849
|
+
if (entry.timeout === undefined) {
|
|
850
|
+
throw new Error(`[Ink-Cartridge]Target Sequence Action has no default Timeout, you cannot modify, ID is ${actionId}`);
|
|
851
|
+
}
|
|
852
|
+
entry.timeout = timeout;
|
|
853
|
+
}
|
|
854
|
+
}, []);
|
|
855
|
+
const modifyAction = useCallback((actionId, keys) => {
|
|
856
|
+
const entry = shortcutOperationsRef.current.get(actionId);
|
|
857
|
+
if (!entry) {
|
|
858
|
+
throw new Error(`[Ink-Cartridge] Cannot modify action "${actionId}": action not registered.`);
|
|
859
|
+
}
|
|
860
|
+
if (entry.keys === undefined) {
|
|
861
|
+
throw new Error(`[Ink-Cartridge] Cannot modify action "${actionId}": action was not registered with a 'keys' field.`);
|
|
862
|
+
}
|
|
863
|
+
entry.keys = keys;
|
|
864
|
+
}, []);
|
|
865
|
+
const addSequenceAction = useCallback((entry) => {
|
|
866
|
+
if (sequenceOperationsRef.current.has(entry.sequenceActionId)) {
|
|
867
|
+
throw new Error(`[Ink-Cartridge] Sequence Action ${entry.sequenceActionId} may not be defined repeatedly`);
|
|
868
|
+
}
|
|
869
|
+
sequenceOperationsRef.current.set(entry.sequenceActionId, {
|
|
870
|
+
action: entry.action,
|
|
871
|
+
keys: entry.keys,
|
|
872
|
+
timeout: entry.timeout,
|
|
873
|
+
});
|
|
874
|
+
}, []);
|
|
875
|
+
const hasSequenceAction = useCallback((sequenceActionId) => {
|
|
876
|
+
return sequenceOperationsRef.current.has(sequenceActionId);
|
|
877
|
+
}, []);
|
|
878
|
+
const removeSequenceAction = useCallback((sequenceActionId) => {
|
|
879
|
+
if (!sequenceOperationsRef.current.has(sequenceActionId)) {
|
|
880
|
+
throw new Error(`[Ink-Cartridge] Cannot remove sequence action "${sequenceActionId}": action not registered.`);
|
|
881
|
+
}
|
|
882
|
+
sequenceOperationsRef.current.delete(sequenceActionId);
|
|
883
|
+
}, []);
|
|
884
|
+
const clearSequenceOperations = useCallback(() => {
|
|
885
|
+
sequenceOperationsRef.current.clear();
|
|
886
|
+
}, []);
|
|
887
|
+
const addAction = useCallback((entry) => {
|
|
888
|
+
if (shortcutOperationsRef.current.has(entry.actionId)) {
|
|
889
|
+
throw new Error(`[Ink-Cartridge] Duplicate shortcut cannot be defined with ID ${entry.actionId}`);
|
|
890
|
+
}
|
|
891
|
+
shortcutOperationsRef.current.set(entry.actionId, {
|
|
892
|
+
action: entry.action,
|
|
893
|
+
keys: entry.keys,
|
|
894
|
+
});
|
|
895
|
+
}, []);
|
|
896
|
+
const hasAction = useCallback((actionId) => {
|
|
897
|
+
return shortcutOperationsRef.current.has(actionId);
|
|
898
|
+
}, []);
|
|
899
|
+
const removeAction = useCallback((actionId) => {
|
|
900
|
+
if (!shortcutOperationsRef.current.has(actionId)) {
|
|
901
|
+
throw new Error(`[Ink-Cartridge] Cannot remove action "${actionId}": action not registered.`);
|
|
902
|
+
}
|
|
903
|
+
shortcutOperationsRef.current.delete(actionId);
|
|
904
|
+
}, []);
|
|
905
|
+
const clearShortcutOperations = useCallback(() => {
|
|
906
|
+
shortcutOperationsRef.current.clear();
|
|
907
|
+
}, []);
|
|
908
|
+
const value = useMemo(() => ({
|
|
909
|
+
boundKeyboard,
|
|
910
|
+
blockedKey: penetration,
|
|
911
|
+
stop,
|
|
912
|
+
globalKeys,
|
|
913
|
+
getGlobalKeys,
|
|
914
|
+
globalSequence,
|
|
915
|
+
focusSet,
|
|
916
|
+
focusNext,
|
|
917
|
+
focusPrev,
|
|
918
|
+
focusCurrent,
|
|
919
|
+
focusUnregister,
|
|
920
|
+
subscribeFocus,
|
|
921
|
+
defineShortcutAction,
|
|
922
|
+
addAction,
|
|
923
|
+
hasAction,
|
|
924
|
+
removeAction,
|
|
925
|
+
modifyAction,
|
|
926
|
+
clearShortcutOperations,
|
|
927
|
+
defineSequenceAction,
|
|
928
|
+
addSequenceAction,
|
|
929
|
+
hasSequenceAction,
|
|
930
|
+
removeSequenceAction,
|
|
931
|
+
modifySequenceAction,
|
|
932
|
+
clearSequenceOperations,
|
|
933
|
+
_pushOwner: pushOwner,
|
|
934
|
+
_popOwner: popOwner,
|
|
935
|
+
boundSequence,
|
|
936
|
+
enableWildcardPriority,
|
|
937
|
+
useModalMissListener,
|
|
938
|
+
}), [
|
|
939
|
+
boundKeyboard,
|
|
940
|
+
penetration,
|
|
941
|
+
stop,
|
|
942
|
+
globalKeys,
|
|
943
|
+
getGlobalKeys,
|
|
944
|
+
globalSequence,
|
|
945
|
+
focusSet,
|
|
946
|
+
focusNext,
|
|
947
|
+
focusPrev,
|
|
948
|
+
focusCurrent,
|
|
949
|
+
focusUnregister,
|
|
950
|
+
subscribeFocus,
|
|
951
|
+
defineShortcutAction,
|
|
952
|
+
addAction,
|
|
953
|
+
hasAction,
|
|
954
|
+
removeAction,
|
|
955
|
+
modifyAction,
|
|
956
|
+
clearShortcutOperations,
|
|
957
|
+
defineSequenceAction,
|
|
958
|
+
addSequenceAction,
|
|
959
|
+
hasSequenceAction,
|
|
960
|
+
removeSequenceAction,
|
|
961
|
+
modifySequenceAction,
|
|
962
|
+
clearSequenceOperations,
|
|
963
|
+
pushOwner,
|
|
964
|
+
popOwner,
|
|
965
|
+
boundSequence,
|
|
966
|
+
enableWildcardPriority,
|
|
967
|
+
useModalMissListener,
|
|
968
|
+
]);
|
|
969
|
+
useInput((input, key) => {
|
|
970
|
+
const ctx = buildPipelineContext(input, key, {
|
|
971
|
+
pathRef,
|
|
972
|
+
globalKeysRef,
|
|
973
|
+
globalSequencesRef,
|
|
974
|
+
activeOverlayIdsRef,
|
|
975
|
+
displayedOverlaysRef,
|
|
976
|
+
activeModalIdRef,
|
|
977
|
+
layersRef,
|
|
978
|
+
globalPendingSeqRef,
|
|
979
|
+
wildcardPriorityCountRef,
|
|
980
|
+
notifyFocusChange,
|
|
981
|
+
});
|
|
982
|
+
runPipeline(ctx);
|
|
983
|
+
});
|
|
984
|
+
return (React.createElement(KeyboardContext.Provider, { value: value }, children));
|
|
985
|
+
}
|