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,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
+ }