@tanstack/hotkeys 0.7.1 → 0.9.0

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 (158) hide show
  1. package/README.md +15 -1
  2. package/dist/{manager.utils.js → _event-target.cjs} +5 -41
  3. package/dist/_event-target.cjs.map +1 -0
  4. package/dist/{manager.utils.cjs → _event-target.js} +3 -47
  5. package/dist/_event-target.js.map +1 -0
  6. package/dist/_keyboard-event.cjs +83 -0
  7. package/dist/_keyboard-event.cjs.map +1 -0
  8. package/dist/_keyboard-event.js +76 -0
  9. package/dist/_keyboard-event.js.map +1 -0
  10. package/dist/_match.cjs +49 -0
  11. package/dist/_match.cjs.map +1 -0
  12. package/dist/_match.js +49 -0
  13. package/dist/_match.js.map +1 -0
  14. package/dist/_named-keys.cjs +68 -0
  15. package/dist/_named-keys.cjs.map +1 -0
  16. package/dist/_named-keys.d.cts +8 -0
  17. package/dist/_named-keys.d.ts +8 -0
  18. package/dist/_named-keys.js +66 -0
  19. package/dist/_named-keys.js.map +1 -0
  20. package/dist/_recorder-chord.cjs +44 -0
  21. package/dist/_recorder-chord.cjs.map +1 -0
  22. package/dist/_recorder-chord.js +43 -0
  23. package/dist/_recorder-chord.js.map +1 -0
  24. package/dist/_recording-guard.cjs +59 -0
  25. package/dist/_recording-guard.cjs.map +1 -0
  26. package/dist/_recording-guard.js +56 -0
  27. package/dist/_recording-guard.js.map +1 -0
  28. package/dist/_registration.cjs +54 -0
  29. package/dist/_registration.cjs.map +1 -0
  30. package/dist/_registration.js +50 -0
  31. package/dist/_registration.js.map +1 -0
  32. package/dist/conflicts.cjs +55 -0
  33. package/dist/conflicts.cjs.map +1 -0
  34. package/dist/conflicts.d.cts +39 -0
  35. package/dist/conflicts.d.ts +39 -0
  36. package/dist/conflicts.js +55 -0
  37. package/dist/conflicts.js.map +1 -0
  38. package/dist/constants.cjs +46 -171
  39. package/dist/constants.cjs.map +1 -1
  40. package/dist/constants.d.cts +4 -129
  41. package/dist/constants.d.ts +4 -129
  42. package/dist/constants.js +48 -164
  43. package/dist/constants.js.map +1 -1
  44. package/dist/display-labels.cjs +119 -0
  45. package/dist/display-labels.cjs.map +1 -0
  46. package/dist/display-labels.d.cts +93 -0
  47. package/dist/display-labels.d.ts +93 -0
  48. package/dist/display-labels.js +113 -0
  49. package/dist/display-labels.js.map +1 -0
  50. package/dist/format.cjs +30 -36
  51. package/dist/format.cjs.map +1 -1
  52. package/dist/format.d.cts +46 -7
  53. package/dist/format.d.ts +46 -7
  54. package/dist/format.js +31 -37
  55. package/dist/format.js.map +1 -1
  56. package/dist/hint.cjs +24 -0
  57. package/dist/hint.cjs.map +1 -0
  58. package/dist/hint.d.cts +23 -0
  59. package/dist/hint.d.ts +23 -0
  60. package/dist/hint.js +24 -0
  61. package/dist/hint.js.map +1 -0
  62. package/dist/hotkey-manager.cjs +53 -22
  63. package/dist/hotkey-manager.cjs.map +1 -1
  64. package/dist/hotkey-manager.d.cts +6 -2
  65. package/dist/hotkey-manager.d.ts +6 -2
  66. package/dist/hotkey-manager.js +49 -18
  67. package/dist/hotkey-manager.js.map +1 -1
  68. package/dist/hotkey-recorder.cjs +67 -11
  69. package/dist/hotkey-recorder.cjs.map +1 -1
  70. package/dist/hotkey-recorder.d.cts +6 -3
  71. package/dist/hotkey-recorder.d.ts +6 -3
  72. package/dist/hotkey-recorder.js +66 -10
  73. package/dist/hotkey-recorder.js.map +1 -1
  74. package/dist/hotkey-sequence-recorder.cjs +85 -22
  75. package/dist/hotkey-sequence-recorder.cjs.map +1 -1
  76. package/dist/hotkey-sequence-recorder.d.cts +9 -2
  77. package/dist/hotkey-sequence-recorder.d.ts +9 -2
  78. package/dist/hotkey-sequence-recorder.js +84 -21
  79. package/dist/hotkey-sequence-recorder.js.map +1 -1
  80. package/dist/{hotkey.d.ts → hotkey.types.d.cts} +55 -120
  81. package/dist/{hotkey.d.cts → hotkey.types.d.ts} +55 -120
  82. package/dist/index.cjs +18 -10
  83. package/dist/index.d.cts +14 -9
  84. package/dist/index.d.ts +14 -9
  85. package/dist/index.js +10 -6
  86. package/dist/key-state-tracker.cjs +24 -24
  87. package/dist/key-state-tracker.cjs.map +1 -1
  88. package/dist/key-state-tracker.js +24 -24
  89. package/dist/key-state-tracker.js.map +1 -1
  90. package/dist/key.types.d.cts +82 -0
  91. package/dist/key.types.d.ts +82 -0
  92. package/dist/match.cjs +21 -30
  93. package/dist/match.cjs.map +1 -1
  94. package/dist/match.d.cts +19 -3
  95. package/dist/match.d.ts +19 -3
  96. package/dist/match.js +19 -29
  97. package/dist/match.js.map +1 -1
  98. package/dist/parse.cjs +43 -26
  99. package/dist/parse.cjs.map +1 -1
  100. package/dist/parse.d.cts +10 -4
  101. package/dist/parse.d.ts +10 -4
  102. package/dist/parse.js +35 -19
  103. package/dist/parse.js.map +1 -1
  104. package/dist/platform.cjs +55 -0
  105. package/dist/platform.cjs.map +1 -0
  106. package/dist/platform.d.cts +44 -0
  107. package/dist/platform.d.ts +44 -0
  108. package/dist/platform.js +53 -0
  109. package/dist/platform.js.map +1 -0
  110. package/dist/recorder-options.d.cts +34 -0
  111. package/dist/recorder-options.d.ts +34 -0
  112. package/dist/sequence-manager.cjs +54 -28
  113. package/dist/sequence-manager.cjs.map +1 -1
  114. package/dist/sequence-manager.d.cts +18 -3
  115. package/dist/sequence-manager.d.ts +18 -3
  116. package/dist/sequence-manager.js +49 -23
  117. package/dist/sequence-manager.js.map +1 -1
  118. package/dist/validate.cjs +6 -10
  119. package/dist/validate.cjs.map +1 -1
  120. package/dist/validate.d.cts +13 -2
  121. package/dist/validate.d.ts +13 -2
  122. package/dist/validate.js +7 -11
  123. package/dist/validate.js.map +1 -1
  124. package/package.json +2 -2
  125. package/src/{manager.utils.ts → _event-target.ts} +0 -74
  126. package/src/_keyboard-event.ts +101 -0
  127. package/src/_match.ts +96 -0
  128. package/src/_named-keys.ts +62 -0
  129. package/src/_recorder-chord.ts +76 -0
  130. package/src/_recording-guard.ts +63 -0
  131. package/src/_registration.ts +79 -0
  132. package/src/conflicts.ts +125 -0
  133. package/src/constants.ts +48 -196
  134. package/src/display-labels.ts +123 -0
  135. package/src/format.ts +111 -52
  136. package/src/hint.ts +42 -0
  137. package/src/hotkey-manager.ts +95 -40
  138. package/src/hotkey-recorder.ts +92 -16
  139. package/src/hotkey-sequence-recorder.ts +110 -28
  140. package/src/{hotkey.ts → hotkey.types.ts} +57 -224
  141. package/src/index.ts +11 -5
  142. package/src/key-state-tracker.ts +36 -24
  143. package/src/key.types.ts +268 -0
  144. package/src/match.ts +41 -90
  145. package/src/parse.ts +67 -26
  146. package/src/platform.ts +66 -0
  147. package/src/recorder-options.ts +34 -0
  148. package/src/sequence-manager.ts +77 -42
  149. package/src/validate.ts +29 -21
  150. package/dist/manager.utils.cjs.map +0 -1
  151. package/dist/manager.utils.d.cts +0 -13
  152. package/dist/manager.utils.d.ts +0 -13
  153. package/dist/manager.utils.js.map +0 -1
  154. package/dist/recorder-chord.cjs +0 -18
  155. package/dist/recorder-chord.cjs.map +0 -1
  156. package/dist/recorder-chord.js +0 -18
  157. package/dist/recorder-chord.js.map +0 -1
  158. package/src/recorder-chord.ts +0 -25
@@ -1,157 +1,4 @@
1
- /**
2
- * All supported modifier key names, including aliases.
3
- * - Control/Ctrl: The Control key
4
- * - Shift: The Shift key
5
- * - Alt/Option: The Alt key (Option on macOS)
6
- * - Command/Cmd: The Command key (macOS only)
7
- * - CommandOrControl/Mod: Command on macOS, Control on other platforms
8
- */
9
- export type Modifier =
10
- | 'Control'
11
- | 'Ctrl'
12
- | 'Shift'
13
- | 'Alt'
14
- | 'Option'
15
- | 'Command'
16
- | 'Cmd'
17
- | 'CommandOrControl'
18
- | 'Mod'
19
-
20
- /**
21
- * Canonical modifier names that map to KeyboardEvent properties.
22
- */
23
- export type CanonicalModifier = 'Control' | 'Shift' | 'Alt' | 'Meta'
24
-
25
- /**
26
- * Letter keys A-Z (case-insensitive in matching).
27
- */
28
- export type LetterKey =
29
- | 'A'
30
- | 'B'
31
- | 'C'
32
- | 'D'
33
- | 'E'
34
- | 'F'
35
- | 'G'
36
- | 'H'
37
- | 'I'
38
- | 'J'
39
- | 'K'
40
- | 'L'
41
- | 'M'
42
- | 'N'
43
- | 'O'
44
- | 'P'
45
- | 'Q'
46
- | 'R'
47
- | 'S'
48
- | 'T'
49
- | 'U'
50
- | 'V'
51
- | 'W'
52
- | 'X'
53
- | 'Y'
54
- | 'Z'
55
-
56
- /**
57
- * Number keys 0-9.
58
- */
59
- export type NumberKey =
60
- | '0'
61
- | '1'
62
- | '2'
63
- | '3'
64
- | '4'
65
- | '5'
66
- | '6'
67
- | '7'
68
- | '8'
69
- | '9'
70
-
71
- /**
72
- * Function keys F1-F12.
73
- */
74
- export type FunctionKey =
75
- | 'F1'
76
- | 'F2'
77
- | 'F3'
78
- | 'F4'
79
- | 'F5'
80
- | 'F6'
81
- | 'F7'
82
- | 'F8'
83
- | 'F9'
84
- | 'F10'
85
- | 'F11'
86
- | 'F12'
87
-
88
- /**
89
- * Navigation keys for cursor movement.
90
- */
91
- export type NavigationKey =
92
- | 'ArrowUp'
93
- | 'ArrowDown'
94
- | 'ArrowLeft'
95
- | 'ArrowRight'
96
- | 'Home'
97
- | 'End'
98
- | 'PageUp'
99
- | 'PageDown'
100
-
101
- /**
102
- * Editing and special keys.
103
- */
104
- export type EditingKey =
105
- | 'Enter'
106
- | 'Escape'
107
- | 'Space'
108
- | 'Tab'
109
- | 'Backspace'
110
- | 'Delete'
111
-
112
- /**
113
- * Punctuation keys commonly used in keyboard shortcuts.
114
- * These are the literal characters as they appear in KeyboardEvent.key
115
- * (layout-dependent, typically US keyboard layout).
116
- */
117
- export type PunctuationKey =
118
- | '/'
119
- | '['
120
- | ']'
121
- | '\\'
122
- | '='
123
- | '-'
124
- | ','
125
- | '.'
126
- | ';'
127
- | '`'
128
-
129
- /**
130
- * Keys that don't change their value when Shift is pressed.
131
- * These keys produce the same `KeyboardEvent.key` value whether Shift is held or not.
132
- *
133
- * Excludes NumberKey (Shift+1 produces '!' on US layout) and PunctuationKey
134
- * (Shift+',' produces '<' on US layout).
135
- *
136
- * Used in hotkey type definitions to prevent layout-dependent issues when Shift
137
- * is part of the modifier combination.
138
- */
139
- type NonPunctuationKey =
140
- | LetterKey
141
- | NumberKey
142
- | EditingKey
143
- | NavigationKey
144
- | FunctionKey
145
-
146
- /**
147
- * All supported non-modifier keys.
148
- */
149
- export type Key = NonPunctuationKey | PunctuationKey
150
-
151
- /**
152
- * Includes both modifier keys and regular keys.
153
- */
154
- export type IndividualKey = CanonicalModifier | Key
1
+ import type { CanonicalModifier, Key, LogicalKey } from './key.types'
155
2
 
156
3
  // =============================================================================
157
4
  // Hotkey Types
@@ -160,7 +7,6 @@ export type IndividualKey = CanonicalModifier | Key
160
7
  /**
161
8
  * Single modifier + key combinations.
162
9
  * Uses canonical modifiers (4) + Mod (1) = 5 modifiers.
163
- * Shift combinations exclude PunctuationKey to avoid layout-dependent issues.
164
10
  *
165
11
  * The `Mod` modifier is platform-adaptive:
166
12
  * - **macOS**: Resolves to `Meta` (Command key ⌘)
@@ -169,16 +15,15 @@ export type IndividualKey = CanonicalModifier | Key
169
15
  * This enables cross-platform hotkey definitions that automatically adapt to the platform.
170
16
  * For example, `Mod+S` becomes `Command+S` on Mac and `Ctrl+S` on Windows/Linux.
171
17
  */
172
- type SingleModifierHotkey =
18
+ export type SingleModifierHotkey =
173
19
  | `Control+${Key}`
174
20
  | `Alt+${Key}`
175
- | `Shift+${NonPunctuationKey}`
21
+ | `Shift+${Key}`
176
22
  | `Meta+${Key}`
177
23
  | `Mod+${Key}`
178
24
 
179
25
  /**
180
26
  * Two modifier + key combinations.
181
- * Shift combinations exclude Numbers and PunctuationKeys to avoid layout-dependent issues.
182
27
  *
183
28
  * **Platform-adaptive `Mod` combinations:**
184
29
  * - `Mod+Alt` and `Mod+Shift` are included (safe on all platforms)
@@ -186,19 +31,18 @@ type SingleModifierHotkey =
186
31
  * - `Mod+Control` duplicates `Control` on Windows/Linux (Mod = Control)
187
32
  * - `Mod+Meta` duplicates `Meta` on macOS (Mod = Meta)
188
33
  */
189
- type TwoModifierHotkey =
34
+ export type TwoModifierHotkey =
190
35
  | `Control+Alt+${Key}`
191
- | `Control+Shift+${NonPunctuationKey}`
36
+ | `Control+Shift+${Key}`
192
37
  | `Control+Meta+${Key}`
193
- | `Alt+Shift+${NonPunctuationKey}`
38
+ | `Alt+Shift+${Key}`
194
39
  | `Alt+Meta+${Key}`
195
- | `Shift+Meta+${NonPunctuationKey}`
40
+ | `Shift+Meta+${Key}`
196
41
  | `Mod+Alt+${Key}`
197
- | `Mod+Shift+${NonPunctuationKey}`
42
+ | `Mod+Shift+${Key}`
198
43
 
199
44
  /**
200
45
  * Three modifier + key combinations.
201
- * Shift combinations exclude Numbers and PunctuationKeys to avoid layout-dependent issues.
202
46
  *
203
47
  * **Platform-adaptive `Mod` combinations:**
204
48
  * - `Mod+Alt+Shift` is included (safe on all platforms)
@@ -206,16 +50,15 @@ type TwoModifierHotkey =
206
50
  * - `Mod+Control+Shift` duplicates `Control` on Windows/Linux (Mod = Control)
207
51
  * - `Mod+Shift+Meta` duplicates `Meta` on macOS (Mod = Meta)
208
52
  */
209
- type ThreeModifierHotkey =
210
- | `Control+Alt+Shift+${NonPunctuationKey}`
53
+ export type ThreeModifierHotkey =
54
+ | `Control+Alt+Shift+${Key}`
211
55
  | `Control+Alt+Meta+${Key}`
212
- | `Control+Shift+Meta+${NonPunctuationKey}`
213
- | `Alt+Shift+Meta+${NonPunctuationKey}`
214
- | `Mod+Alt+Shift+${NonPunctuationKey}`
56
+ | `Control+Shift+Meta+${Key}`
57
+ | `Alt+Shift+Meta+${Key}`
58
+ | `Mod+Alt+Shift+${Key}`
215
59
 
216
60
  /**
217
61
  * Four modifier + key combinations.
218
- * Shift combinations exclude Numbers and PunctuationKeys to avoid layout-dependent issues.
219
62
  *
220
63
  * Only the canonical `Control+Alt+Shift+Meta` combination is included.
221
64
  *
@@ -226,7 +69,7 @@ type ThreeModifierHotkey =
226
69
  * - `Mod+Control+Alt+Shift` → duplicates `Control` on Windows/Linux
227
70
  * - `Mod+Alt+Shift+Meta` → duplicates `Meta` on macOS
228
71
  */
229
- type FourModifierHotkey = `Control+Alt+Shift+Meta+${NonPunctuationKey}`
72
+ export type FourModifierHotkey = `Control+Alt+Shift+Meta+${Key}`
230
73
 
231
74
  /**
232
75
  * A type-safe hotkey string.
@@ -287,39 +130,35 @@ export type Hotkey =
287
130
  | ThreeModifierHotkey
288
131
  | FourModifierHotkey
289
132
 
290
- /**
291
- * A parsed representation of a hotkey string.
292
- *
293
- * This interface provides a flexible fallback when the `Hotkey` type doesn't
294
- * fit your use case. You can pass a `ParsedHotkey` directly to hotkey functions
295
- * instead of a hotkey string, allowing for more dynamic or complex scenarios
296
- * that aren't covered by the type-safe `Hotkey` union.
297
- *
298
- * @example
299
- * ```ts
300
- * // Type-safe hotkey string
301
- * useHotkey('Mod+S', handler)
302
- *
303
- * // Fallback: parsed hotkey for dynamic scenarios
304
- * const parsed = parseHotkey(userInput)
305
- * useHotkey(parsed, handler) // Works even if userInput isn't in Hotkey type
306
- * ```
307
- */
308
- export interface ParsedHotkey {
309
- /** The non-modifier key (e.g., 'S', 'Escape', 'F1', '/', '['). Can be any string for flexibility. */
310
- key: Key | (string & {})
311
- /** Whether the Control key is required */
133
+ /** Resolved modifier flags shared by both parsed binding variants. */
134
+ export interface ParsedModifiers {
135
+ /** Whether Control is required. */
312
136
  ctrl: boolean
313
- /** Whether the Shift key is required */
137
+ /** Whether Shift is required. */
314
138
  shift: boolean
315
- /** Whether the Alt key is required */
139
+ /** Whether Alt is required. */
316
140
  alt: boolean
317
- /** Whether the Meta (Command) key is required */
141
+ /** Whether Meta (Command) is required. */
318
142
  meta: boolean
319
- /** List of canonical modifier names that are required, in canonical order */
143
+ /** Required modifiers in canonical order. */
320
144
  modifiers: Array<CanonicalModifier>
321
145
  }
322
146
 
147
+ /**
148
+ * A parsed binding matches either a logical key or a physical code, never both.
149
+ * Check `code !== undefined` to narrow to a physical binding. Its logical
150
+ * character is unknown without a keyboard event and is not stored in `key`.
151
+ *
152
+ * @example
153
+ * parseHotkey('Alt+S') // { key: 'S', alt: true, ... }
154
+ * parseHotkey('Alt+[KeyS]') // { code: 'KeyS', alt: true, ... } — no key property
155
+ */
156
+ export type ParsedHotkey = ParsedModifiers &
157
+ (
158
+ | { key: LogicalKey | (string & {}); code?: never }
159
+ | { code: string; key?: never }
160
+ )
161
+
323
162
  /**
324
163
  * A raw hotkey object for programmatic registration.
325
164
  *
@@ -338,11 +177,13 @@ export interface ParsedHotkey {
338
177
  * useHotkey({ key: 'Escape' }, handler) // Escape (no modifiers)
339
178
  * useHotkey({ key: 'A', shift: true, meta: true }, handler) // Shift+Meta+A
340
179
  * useHotkey({ key: 'S', mod: true, shift: true }, handler) // Mod+Shift+S
180
+ * useHotkey({ code: 'KeyW', mod: true }, handler) // Physical Mod+KeyW
181
+ * useHotkey({ code: 'NumpadAdd', mod: true }, handler) // Physical numpad plus
341
182
  * ```
342
183
  */
343
- export interface RawHotkey {
184
+ /** Modifier flags shared by logical-key and physical-code registrations. */
185
+ export interface RawModifiers {
344
186
  /** The non-modifier key (e.g., 'S', 'Escape', 'F1'). */
345
- key: Key | (string & {})
346
187
  /** Platform-adaptive modifier: Command on macOS, Control on Windows/Linux. Defaults to false. */
347
188
  mod?: boolean
348
189
  /** Whether the Control key is required. Defaults to false. */
@@ -355,36 +196,16 @@ export interface RawHotkey {
355
196
  meta?: boolean
356
197
  }
357
198
 
199
+ export type RawHotkey =
200
+ | ({ key: LogicalKey | (string & {}); code?: never } & RawModifiers)
201
+ | ({ code: string; key?: never } & RawModifiers)
202
+
358
203
  /**
359
204
  * A hotkey that can be passed to `HotkeyManager.register()` and `useHotkey()`.
360
205
  * Either a type-safe string (`Hotkey`) or a raw object (`RawHotkey`).
361
206
  */
362
207
  export type RegisterableHotkey = Hotkey | RawHotkey
363
208
 
364
- /**
365
- * Options for formatting hotkeys for display.
366
- */
367
- export interface FormatDisplayOptions {
368
- /** The target platform. Defaults to auto-detection. */
369
- platform?: 'mac' | 'windows' | 'linux'
370
- /** Whether to use symbols for the display. Defaults to true. */
371
- useSymbols?: boolean
372
- /** Override the separator between display tokens. Defaults to platform-specific formatting when null/undefined. */
373
- separatorToken?: string | null
374
- }
375
-
376
- /**
377
- * Result of validating a hotkey string.
378
- */
379
- export interface ValidationResult {
380
- /** Whether the hotkey is valid (can still have warnings) */
381
- valid: boolean
382
- /** Warning messages about potential issues */
383
- warnings: Array<string>
384
- /** Error messages about invalid syntax */
385
- errors: Array<string>
386
- }
387
-
388
209
  /**
389
210
  * Context passed to hotkey callbacks along with the keyboard event.
390
211
  */
@@ -416,7 +237,7 @@ export type HotkeyCallback = (
416
237
 
417
238
  /**
418
239
  * Metadata for hotkey and sequence registrations.
419
- * Includes `name` and `description` by default. Extend via declaration merging:
240
+ * Includes `name`, `description`, and `group` by default. Extend via declaration merging:
420
241
  *
421
242
  * @example
422
243
  * ```ts
@@ -433,4 +254,16 @@ export interface HotkeyMeta {
433
254
  name?: string
434
255
  /** Description of what this hotkey does */
435
256
  description?: string
257
+ /** Optional display group. Does not affect matching or scope. */
258
+ group?: string
436
259
  }
260
+
261
+ /**
262
+ * Behavior when registering a hotkey/sequence that conflicts with an existing registration.
263
+ *
264
+ * - `'warn'` - Log a warning to the console but allow both registrations (default)
265
+ * - `'error'` - Throw an error and prevent the new registration
266
+ * - `'replace'` - Unregister the existing registration and register the new one
267
+ * - `'allow'` - Allow multiple registrations without warning
268
+ */
269
+ export type ConflictBehavior = 'warn' | 'error' | 'replace' | 'allow'
package/src/index.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  export * from './constants'
2
+ export * from './display-labels'
3
+ export * from './platform'
4
+ export * from './key.types'
5
+ export * from './hotkey.types'
2
6
  export * from './format'
3
- export * from './hotkey'
7
+ export * from './parse'
8
+ export * from './validate'
9
+ export * from './match'
10
+ export * from './hint'
11
+ export * from './conflicts'
4
12
  export * from './hotkey-manager'
13
+ export * from './sequence-manager'
5
14
  export * from './key-state-tracker'
6
- export * from './match'
7
- export * from './parse'
15
+ export * from './recorder-options'
8
16
  export * from './hotkey-recorder'
9
17
  export * from './hotkey-sequence-recorder'
10
- export * from './sequence-manager'
11
- export * from './validate'
@@ -71,8 +71,7 @@ export class KeyStateTracker {
71
71
  getDefaultKeyStateTrackerState(),
72
72
  )
73
73
 
74
- #heldKeysSet: Set<string> = new Set()
75
- #heldCodesMap: Map<string, string> = new Map()
74
+ #heldEntries: Map<string, { key: string; code: string }> = new Map()
76
75
  #keydownListener: ((event: KeyboardEvent) => void) | null = null
77
76
  #keyupListener: ((event: KeyboardEvent) => void) | null = null
78
77
  #blurListener: (() => void) | null = null
@@ -111,18 +110,23 @@ export class KeyStateTracker {
111
110
 
112
111
  this.#keydownListener = (event: KeyboardEvent) => {
113
112
  const key = normalizeKeyName(event.key)
114
- if (!this.#heldKeysSet.has(key)) {
115
- this.#heldKeysSet.add(key)
116
- this.#heldCodesMap.set(key, event.code)
113
+ const identity = event.code || `key:${key}`
114
+ if (!this.#heldEntries.has(identity)) {
115
+ this.#heldEntries.set(identity, { key, code: event.code })
117
116
  this.#syncState()
118
117
  }
119
118
  }
120
119
 
121
120
  this.#keyupListener = (event: KeyboardEvent) => {
122
121
  const key = normalizeKeyName(event.key)
123
- if (this.#heldKeysSet.has(key)) {
124
- this.#heldKeysSet.delete(key)
125
- this.#heldCodesMap.delete(key)
122
+ const identity = event.code || `key:${key}`
123
+ this.#heldEntries.delete(identity)
124
+
125
+ // Code-less keyup events can only be paired by their logical key.
126
+ if (!event.code) {
127
+ for (const [entryIdentity, entry] of this.#heldEntries) {
128
+ if (entry.key === key) this.#heldEntries.delete(entryIdentity)
129
+ }
126
130
  }
127
131
 
128
132
  // When a modifier key is released, clear any non-modifier keys still
@@ -130,10 +134,9 @@ export class KeyStateTracker {
130
134
  // (e.g. Cmd+S) and swallows the keyup event for the non-modifier key,
131
135
  // leaving it permanently stuck in the held set.
132
136
  if (MODIFIER_KEYS.has(key)) {
133
- for (const heldKey of this.#heldKeysSet) {
134
- if (!MODIFIER_KEYS.has(heldKey)) {
135
- this.#heldKeysSet.delete(heldKey)
136
- this.#heldCodesMap.delete(heldKey)
137
+ for (const [entryIdentity, entry] of this.#heldEntries) {
138
+ if (!MODIFIER_KEYS.has(entry.key)) {
139
+ this.#heldEntries.delete(entryIdentity)
137
140
  }
138
141
  }
139
142
  }
@@ -143,15 +146,14 @@ export class KeyStateTracker {
143
146
 
144
147
  // Clear all keys when window loses focus (keys might be released while not focused)
145
148
  this.#blurListener = () => {
146
- if (this.#heldKeysSet.size > 0) {
147
- this.#heldKeysSet.clear()
148
- this.#heldCodesMap.clear()
149
+ if (this.#heldEntries.size > 0) {
150
+ this.#heldEntries.clear()
149
151
  this.#syncState()
150
152
  }
151
153
  }
152
154
 
153
- document.addEventListener('keydown', this.#keydownListener)
154
- document.addEventListener('keyup', this.#keyupListener)
155
+ document.addEventListener('keydown', this.#keydownListener, true)
156
+ document.addEventListener('keyup', this.#keyupListener, true)
155
157
  window.addEventListener('blur', this.#blurListener)
156
158
  }
157
159
 
@@ -159,9 +161,18 @@ export class KeyStateTracker {
159
161
  * Syncs the internal Set to the Store state.
160
162
  */
161
163
  #syncState(): void {
164
+ const heldKeys = Array.from(
165
+ new Set(Array.from(this.#heldEntries.values(), (entry) => entry.key)),
166
+ )
167
+ const heldCodes = Object.fromEntries(
168
+ Array.from(this.#heldEntries.values(), (entry) => [
169
+ entry.key,
170
+ entry.code,
171
+ ]),
172
+ )
162
173
  this.store.setState(() => ({
163
- heldKeys: Array.from(this.#heldKeysSet),
164
- heldCodes: Object.fromEntries(this.#heldCodesMap),
174
+ heldKeys,
175
+ heldCodes,
165
176
  }))
166
177
  }
167
178
 
@@ -174,12 +185,12 @@ export class KeyStateTracker {
174
185
  }
175
186
 
176
187
  if (this.#keydownListener) {
177
- document.removeEventListener('keydown', this.#keydownListener)
188
+ document.removeEventListener('keydown', this.#keydownListener, true)
178
189
  this.#keydownListener = null
179
190
  }
180
191
 
181
192
  if (this.#keyupListener) {
182
- document.removeEventListener('keyup', this.#keyupListener)
193
+ document.removeEventListener('keyup', this.#keyupListener, true)
183
194
  this.#keyupListener = null
184
195
  }
185
196
 
@@ -206,7 +217,9 @@ export class KeyStateTracker {
206
217
  */
207
218
  isKeyHeld(key: string): boolean {
208
219
  const normalizedKey = normalizeKeyName(key)
209
- return this.#heldKeysSet.has(normalizedKey)
220
+ return Array.from(this.#heldEntries.values()).some(
221
+ (entry) => entry.key === normalizedKey,
222
+ )
210
223
  }
211
224
 
212
225
  /**
@@ -234,8 +247,7 @@ export class KeyStateTracker {
234
247
  */
235
248
  destroy(): void {
236
249
  this.#removeListeners()
237
- this.#heldKeysSet.clear()
238
- this.#heldCodesMap.clear()
250
+ this.#heldEntries.clear()
239
251
  this.store.setState(() => getDefaultKeyStateTrackerState())
240
252
  }
241
253
  }