@tanstack/hotkeys 0.8.0 → 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.
- package/README.md +15 -1
- package/dist/{manager.utils.js → _event-target.cjs} +5 -41
- package/dist/_event-target.cjs.map +1 -0
- package/dist/{manager.utils.cjs → _event-target.js} +3 -47
- package/dist/_event-target.js.map +1 -0
- package/dist/_keyboard-event.cjs +83 -0
- package/dist/_keyboard-event.cjs.map +1 -0
- package/dist/_keyboard-event.js +76 -0
- package/dist/_keyboard-event.js.map +1 -0
- package/dist/_match.cjs +49 -0
- package/dist/_match.cjs.map +1 -0
- package/dist/_match.js +49 -0
- package/dist/_match.js.map +1 -0
- package/dist/_named-keys.cjs +68 -0
- package/dist/_named-keys.cjs.map +1 -0
- package/dist/_named-keys.d.cts +8 -0
- package/dist/_named-keys.d.ts +8 -0
- package/dist/_named-keys.js +66 -0
- package/dist/_named-keys.js.map +1 -0
- package/dist/_recorder-chord.cjs +44 -0
- package/dist/_recorder-chord.cjs.map +1 -0
- package/dist/_recorder-chord.js +43 -0
- package/dist/_recorder-chord.js.map +1 -0
- package/dist/_recording-guard.cjs +59 -0
- package/dist/_recording-guard.cjs.map +1 -0
- package/dist/_recording-guard.js +56 -0
- package/dist/_recording-guard.js.map +1 -0
- package/dist/_registration.cjs +54 -0
- package/dist/_registration.cjs.map +1 -0
- package/dist/_registration.js +50 -0
- package/dist/_registration.js.map +1 -0
- package/dist/conflicts.cjs +55 -0
- package/dist/conflicts.cjs.map +1 -0
- package/dist/conflicts.d.cts +39 -0
- package/dist/conflicts.d.ts +39 -0
- package/dist/conflicts.js +55 -0
- package/dist/conflicts.js.map +1 -0
- package/dist/constants.cjs +46 -171
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +4 -129
- package/dist/constants.d.ts +4 -129
- package/dist/constants.js +48 -164
- package/dist/constants.js.map +1 -1
- package/dist/display-labels.cjs +119 -0
- package/dist/display-labels.cjs.map +1 -0
- package/dist/display-labels.d.cts +93 -0
- package/dist/display-labels.d.ts +93 -0
- package/dist/display-labels.js +113 -0
- package/dist/display-labels.js.map +1 -0
- package/dist/format.cjs +30 -36
- package/dist/format.cjs.map +1 -1
- package/dist/format.d.cts +46 -7
- package/dist/format.d.ts +46 -7
- package/dist/format.js +31 -37
- package/dist/format.js.map +1 -1
- package/dist/hint.cjs +24 -0
- package/dist/hint.cjs.map +1 -0
- package/dist/hint.d.cts +23 -0
- package/dist/hint.d.ts +23 -0
- package/dist/hint.js +24 -0
- package/dist/hint.js.map +1 -0
- package/dist/hotkey-manager.cjs +53 -22
- package/dist/hotkey-manager.cjs.map +1 -1
- package/dist/hotkey-manager.d.cts +6 -2
- package/dist/hotkey-manager.d.ts +6 -2
- package/dist/hotkey-manager.js +49 -18
- package/dist/hotkey-manager.js.map +1 -1
- package/dist/hotkey-recorder.cjs +66 -10
- package/dist/hotkey-recorder.cjs.map +1 -1
- package/dist/hotkey-recorder.d.cts +5 -2
- package/dist/hotkey-recorder.d.ts +5 -2
- package/dist/hotkey-recorder.js +65 -9
- package/dist/hotkey-recorder.js.map +1 -1
- package/dist/hotkey-sequence-recorder.cjs +85 -22
- package/dist/hotkey-sequence-recorder.cjs.map +1 -1
- package/dist/hotkey-sequence-recorder.d.cts +9 -2
- package/dist/hotkey-sequence-recorder.d.ts +9 -2
- package/dist/hotkey-sequence-recorder.js +84 -21
- package/dist/hotkey-sequence-recorder.js.map +1 -1
- package/dist/{hotkey.d.ts → hotkey.types.d.cts} +55 -120
- package/dist/{hotkey.d.cts → hotkey.types.d.ts} +55 -120
- package/dist/index.cjs +18 -10
- package/dist/index.d.cts +14 -9
- package/dist/index.d.ts +14 -9
- package/dist/index.js +10 -6
- package/dist/key-state-tracker.cjs +24 -24
- package/dist/key-state-tracker.cjs.map +1 -1
- package/dist/key-state-tracker.js +24 -24
- package/dist/key-state-tracker.js.map +1 -1
- package/dist/key.types.d.cts +82 -0
- package/dist/key.types.d.ts +82 -0
- package/dist/match.cjs +21 -30
- package/dist/match.cjs.map +1 -1
- package/dist/match.d.cts +19 -3
- package/dist/match.d.ts +19 -3
- package/dist/match.js +19 -29
- package/dist/match.js.map +1 -1
- package/dist/parse.cjs +43 -26
- package/dist/parse.cjs.map +1 -1
- package/dist/parse.d.cts +10 -4
- package/dist/parse.d.ts +10 -4
- package/dist/parse.js +35 -19
- package/dist/parse.js.map +1 -1
- package/dist/platform.cjs +55 -0
- package/dist/platform.cjs.map +1 -0
- package/dist/platform.d.cts +44 -0
- package/dist/platform.d.ts +44 -0
- package/dist/platform.js +53 -0
- package/dist/platform.js.map +1 -0
- package/dist/recorder-options.d.cts +34 -0
- package/dist/recorder-options.d.ts +34 -0
- package/dist/sequence-manager.cjs +54 -28
- package/dist/sequence-manager.cjs.map +1 -1
- package/dist/sequence-manager.d.cts +18 -3
- package/dist/sequence-manager.d.ts +18 -3
- package/dist/sequence-manager.js +49 -23
- package/dist/sequence-manager.js.map +1 -1
- package/dist/validate.cjs +6 -10
- package/dist/validate.cjs.map +1 -1
- package/dist/validate.d.cts +13 -2
- package/dist/validate.d.ts +13 -2
- package/dist/validate.js +7 -11
- package/dist/validate.js.map +1 -1
- package/package.json +1 -1
- package/src/{manager.utils.ts → _event-target.ts} +0 -74
- package/src/_keyboard-event.ts +101 -0
- package/src/_match.ts +96 -0
- package/src/_named-keys.ts +62 -0
- package/src/_recorder-chord.ts +76 -0
- package/src/_recording-guard.ts +63 -0
- package/src/_registration.ts +79 -0
- package/src/conflicts.ts +125 -0
- package/src/constants.ts +48 -196
- package/src/display-labels.ts +123 -0
- package/src/format.ts +111 -52
- package/src/hint.ts +42 -0
- package/src/hotkey-manager.ts +95 -40
- package/src/hotkey-recorder.ts +91 -15
- package/src/hotkey-sequence-recorder.ts +110 -28
- package/src/{hotkey.ts → hotkey.types.ts} +57 -224
- package/src/index.ts +11 -5
- package/src/key-state-tracker.ts +36 -24
- package/src/key.types.ts +268 -0
- package/src/match.ts +41 -90
- package/src/parse.ts +67 -26
- package/src/platform.ts +66 -0
- package/src/recorder-options.ts +34 -0
- package/src/sequence-manager.ts +77 -42
- package/src/validate.ts +29 -21
- package/dist/manager.utils.cjs.map +0 -1
- package/dist/manager.utils.d.cts +0 -13
- package/dist/manager.utils.d.ts +0 -13
- package/dist/manager.utils.js.map +0 -1
- package/dist/recorder-chord.cjs +0 -18
- package/dist/recorder-chord.cjs.map +0 -1
- package/dist/recorder-chord.js +0 -18
- package/dist/recorder-chord.js.map +0 -1
- package/src/recorder-chord.ts +0 -25
package/src/key.types.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import type { LOGICAL_ONLY_NAMED_KEYS, SHARED_NAMED_KEYS } from './_named-keys'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* All supported modifier key names, including aliases.
|
|
5
|
+
* - Control/Ctrl: The Control key
|
|
6
|
+
* - Shift: The Shift key
|
|
7
|
+
* - Alt/Option: The Alt key (Option on macOS)
|
|
8
|
+
* - Command/Cmd: The Command key (macOS only)
|
|
9
|
+
* - CommandOrControl/Mod: Command on macOS, Control on other platforms
|
|
10
|
+
*/
|
|
11
|
+
export type Modifier =
|
|
12
|
+
| 'Control'
|
|
13
|
+
| 'Ctrl'
|
|
14
|
+
| 'Shift'
|
|
15
|
+
| 'Alt'
|
|
16
|
+
| 'Option'
|
|
17
|
+
| 'Command'
|
|
18
|
+
| 'Cmd'
|
|
19
|
+
| 'CommandOrControl'
|
|
20
|
+
| 'Mod'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Canonical modifier names that map to KeyboardEvent properties.
|
|
24
|
+
*/
|
|
25
|
+
export type CanonicalModifier = 'Control' | 'Shift' | 'Alt' | 'Meta'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Letter keys A-Z (case-insensitive in matching).
|
|
29
|
+
*/
|
|
30
|
+
export type LetterKey =
|
|
31
|
+
| 'A'
|
|
32
|
+
| 'B'
|
|
33
|
+
| 'C'
|
|
34
|
+
| 'D'
|
|
35
|
+
| 'E'
|
|
36
|
+
| 'F'
|
|
37
|
+
| 'G'
|
|
38
|
+
| 'H'
|
|
39
|
+
| 'I'
|
|
40
|
+
| 'J'
|
|
41
|
+
| 'K'
|
|
42
|
+
| 'L'
|
|
43
|
+
| 'M'
|
|
44
|
+
| 'N'
|
|
45
|
+
| 'O'
|
|
46
|
+
| 'P'
|
|
47
|
+
| 'Q'
|
|
48
|
+
| 'R'
|
|
49
|
+
| 'S'
|
|
50
|
+
| 'T'
|
|
51
|
+
| 'U'
|
|
52
|
+
| 'V'
|
|
53
|
+
| 'W'
|
|
54
|
+
| 'X'
|
|
55
|
+
| 'Y'
|
|
56
|
+
| 'Z'
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Number keys 0-9.
|
|
60
|
+
*/
|
|
61
|
+
export type NumberKey =
|
|
62
|
+
| '0'
|
|
63
|
+
| '1'
|
|
64
|
+
| '2'
|
|
65
|
+
| '3'
|
|
66
|
+
| '4'
|
|
67
|
+
| '5'
|
|
68
|
+
| '6'
|
|
69
|
+
| '7'
|
|
70
|
+
| '8'
|
|
71
|
+
| '9'
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Function keys F1-F24, supported as both logical keys and physical codes.
|
|
75
|
+
*/
|
|
76
|
+
export type FunctionKey =
|
|
77
|
+
| 'F1'
|
|
78
|
+
| 'F2'
|
|
79
|
+
| 'F3'
|
|
80
|
+
| 'F4'
|
|
81
|
+
| 'F5'
|
|
82
|
+
| 'F6'
|
|
83
|
+
| 'F7'
|
|
84
|
+
| 'F8'
|
|
85
|
+
| 'F9'
|
|
86
|
+
| 'F10'
|
|
87
|
+
| 'F11'
|
|
88
|
+
| 'F12'
|
|
89
|
+
| 'F13'
|
|
90
|
+
| 'F14'
|
|
91
|
+
| 'F15'
|
|
92
|
+
| 'F16'
|
|
93
|
+
| 'F17'
|
|
94
|
+
| 'F18'
|
|
95
|
+
| 'F19'
|
|
96
|
+
| 'F20'
|
|
97
|
+
| 'F21'
|
|
98
|
+
| 'F22'
|
|
99
|
+
| 'F23'
|
|
100
|
+
| 'F24'
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Navigation keys for cursor movement.
|
|
104
|
+
*/
|
|
105
|
+
export type NavigationKey =
|
|
106
|
+
| 'ArrowUp'
|
|
107
|
+
| 'ArrowDown'
|
|
108
|
+
| 'ArrowLeft'
|
|
109
|
+
| 'ArrowRight'
|
|
110
|
+
| 'Home'
|
|
111
|
+
| 'End'
|
|
112
|
+
| 'PageUp'
|
|
113
|
+
| 'PageDown'
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Editing and special keys.
|
|
117
|
+
*/
|
|
118
|
+
export type EditingKey =
|
|
119
|
+
| 'Enter'
|
|
120
|
+
| 'Escape'
|
|
121
|
+
| 'Space'
|
|
122
|
+
| 'Tab'
|
|
123
|
+
| 'Backspace'
|
|
124
|
+
| 'Delete'
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Printable punctuation glyphs used in keyboard shortcuts. Matching uses the
|
|
128
|
+
* final KeyboardEvent.key glyph, independent of which layout produced it.
|
|
129
|
+
*/
|
|
130
|
+
export type PunctuationKey =
|
|
131
|
+
| '/'
|
|
132
|
+
| '['
|
|
133
|
+
| ']'
|
|
134
|
+
| '\\'
|
|
135
|
+
| '='
|
|
136
|
+
| '-'
|
|
137
|
+
| ','
|
|
138
|
+
| '.'
|
|
139
|
+
| ';'
|
|
140
|
+
| '`'
|
|
141
|
+
| "'"
|
|
142
|
+
| '+'
|
|
143
|
+
| '?'
|
|
144
|
+
| '!'
|
|
145
|
+
| '@'
|
|
146
|
+
| '#'
|
|
147
|
+
| '$'
|
|
148
|
+
| '%'
|
|
149
|
+
| '^'
|
|
150
|
+
| '&'
|
|
151
|
+
| '*'
|
|
152
|
+
| '('
|
|
153
|
+
| ')'
|
|
154
|
+
| '_'
|
|
155
|
+
| '{'
|
|
156
|
+
| '}'
|
|
157
|
+
| '|'
|
|
158
|
+
| ':'
|
|
159
|
+
| '"'
|
|
160
|
+
| '<'
|
|
161
|
+
| '>'
|
|
162
|
+
| '~'
|
|
163
|
+
|
|
164
|
+
/** Additional named logical keys, including media, browser, lock, and input-mode keys. */
|
|
165
|
+
export type NamedKey =
|
|
166
|
+
| (typeof SHARED_NAMED_KEYS)[number]
|
|
167
|
+
| (typeof LOGICAL_ONLY_NAMED_KEYS)[number]
|
|
168
|
+
|
|
169
|
+
/** Supported logical key names other than printable punctuation. */
|
|
170
|
+
export type NonPunctuationKey =
|
|
171
|
+
| LetterKey
|
|
172
|
+
| NumberKey
|
|
173
|
+
| EditingKey
|
|
174
|
+
| NavigationKey
|
|
175
|
+
| FunctionKey
|
|
176
|
+
| NamedKey
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Supported physical KeyboardEvent.code names, independent of keyboard layout.
|
|
180
|
+
* Letter, digit, numpad, and sided modifier names use finite template unions.
|
|
181
|
+
* `Unidentified` is excluded because it cannot identify a physical key.
|
|
182
|
+
*
|
|
183
|
+
* @see https://www.w3.org/TR/uievents-code/
|
|
184
|
+
*/
|
|
185
|
+
export type PhysicalKeyCode =
|
|
186
|
+
| `Key${LetterKey}`
|
|
187
|
+
| `Digit${NumberKey}`
|
|
188
|
+
| `Numpad${NumberKey}`
|
|
189
|
+
| `${'Alt' | 'Control' | 'Meta' | 'Shift'}${'Left' | 'Right'}`
|
|
190
|
+
| EditingKey
|
|
191
|
+
| NavigationKey
|
|
192
|
+
| FunctionKey
|
|
193
|
+
| (typeof SHARED_NAMED_KEYS)[number]
|
|
194
|
+
| 'Backquote'
|
|
195
|
+
| 'Backslash'
|
|
196
|
+
| 'BracketLeft'
|
|
197
|
+
| 'BracketRight'
|
|
198
|
+
| 'Comma'
|
|
199
|
+
| 'Equal'
|
|
200
|
+
| 'IntlBackslash'
|
|
201
|
+
| 'IntlRo'
|
|
202
|
+
| 'IntlYen'
|
|
203
|
+
| 'Minus'
|
|
204
|
+
| 'Period'
|
|
205
|
+
| 'Quote'
|
|
206
|
+
| 'Semicolon'
|
|
207
|
+
| 'Slash'
|
|
208
|
+
| `Lang${1 | 2 | 3 | 4 | 5}`
|
|
209
|
+
| `Numpad${
|
|
210
|
+
| 'Add'
|
|
211
|
+
| 'Backspace'
|
|
212
|
+
| 'Clear'
|
|
213
|
+
| 'ClearEntry'
|
|
214
|
+
| 'Comma'
|
|
215
|
+
| 'Decimal'
|
|
216
|
+
| 'Divide'
|
|
217
|
+
| 'Enter'
|
|
218
|
+
| 'Equal'
|
|
219
|
+
| 'Hash'
|
|
220
|
+
| 'MemoryAdd'
|
|
221
|
+
| 'MemoryClear'
|
|
222
|
+
| 'MemoryRecall'
|
|
223
|
+
| 'MemoryStore'
|
|
224
|
+
| 'MemorySubtract'
|
|
225
|
+
| 'Multiply'
|
|
226
|
+
| 'ParenLeft'
|
|
227
|
+
| 'ParenRight'
|
|
228
|
+
| 'Star'
|
|
229
|
+
| 'Subtract'}`
|
|
230
|
+
| 'LaunchApp1'
|
|
231
|
+
| 'LaunchApp2'
|
|
232
|
+
| 'MediaSelect'
|
|
233
|
+
| 'Sleep'
|
|
234
|
+
| 'Turbo'
|
|
235
|
+
| 'Abort'
|
|
236
|
+
| 'Resume'
|
|
237
|
+
| 'Suspend'
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* An explicit physical key token. All codes use brackets, including names such
|
|
241
|
+
* as `[Enter]` that also exist as logical keys. Invalid code names are rejected
|
|
242
|
+
* by TypeScript, and supported names are available in autocomplete.
|
|
243
|
+
*/
|
|
244
|
+
export type PhysicalKey = `[${PhysicalKeyCode}]`
|
|
245
|
+
|
|
246
|
+
/** Supported logical key names, excluding bracketed physical-code syntax. */
|
|
247
|
+
export type LogicalKey = NonPunctuationKey | PunctuationKey
|
|
248
|
+
|
|
249
|
+
/** All supported logical keys and bracketed physical key tokens. */
|
|
250
|
+
export type Key = LogicalKey | PhysicalKey
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Includes both modifier keys and regular keys.
|
|
254
|
+
*/
|
|
255
|
+
export type IndividualKey = CanonicalModifier | Key
|
|
256
|
+
|
|
257
|
+
/** Keyboard event properties normalized for internal matching and recording. */
|
|
258
|
+
export interface NormalizedKeyboardEvent {
|
|
259
|
+
key: string
|
|
260
|
+
code: string
|
|
261
|
+
ctrl: boolean
|
|
262
|
+
shift: boolean
|
|
263
|
+
alt: boolean
|
|
264
|
+
meta: boolean
|
|
265
|
+
altGraph: boolean
|
|
266
|
+
location: number
|
|
267
|
+
isComposing: boolean
|
|
268
|
+
}
|
package/src/match.ts
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
normalizeKeyName,
|
|
6
|
-
} from './constants'
|
|
7
|
-
import { parseHotkey } from './parse'
|
|
1
|
+
import { matchKeyboardEvent } from './_match'
|
|
2
|
+
import { parseHotkey, parseRegisterableHotkey } from './parse'
|
|
3
|
+
import { detectPlatform } from './platform'
|
|
4
|
+
import { keysEqual } from './_keyboard-event'
|
|
8
5
|
import type {
|
|
9
6
|
Hotkey,
|
|
10
7
|
HotkeyCallback,
|
|
11
8
|
HotkeyCallbackContext,
|
|
12
9
|
ParsedHotkey,
|
|
13
|
-
|
|
10
|
+
RegisterableHotkey,
|
|
11
|
+
} from './hotkey.types'
|
|
12
|
+
|
|
13
|
+
export interface KeyboardEventMatch {
|
|
14
|
+
matched: boolean
|
|
15
|
+
score: 0 | 1 | 2 | 3
|
|
16
|
+
source?: 'key' | 'code' | 'fallback'
|
|
17
|
+
identity?: { key: string; code: string }
|
|
18
|
+
}
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* Checks if a KeyboardEvent matches a hotkey.
|
|
17
22
|
*
|
|
18
|
-
*
|
|
23
|
+
* Physical bindings such as `Mod+[KeyS]` match `event.code` exactly.
|
|
24
|
+
* Logical bindings use `event.key`, with a fallback to `code`
|
|
19
25
|
* for letter keys, digit keys (0-9), and punctuation keys when `key` produces special
|
|
20
26
|
* characters (e.g., macOS Option+letter, Shift+number, or Option+punctuation).
|
|
21
27
|
* Letter keys are matched case-insensitively.
|
|
@@ -45,85 +51,7 @@ export function matchesKeyboardEvent(
|
|
|
45
51
|
hotkey: Hotkey | ParsedHotkey,
|
|
46
52
|
platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
|
|
47
53
|
): boolean {
|
|
48
|
-
|
|
49
|
-
typeof hotkey === 'string' ? parseHotkey(hotkey, platform) : hotkey
|
|
50
|
-
|
|
51
|
-
// Check modifiers
|
|
52
|
-
if (event.ctrlKey !== parsed.ctrl) {
|
|
53
|
-
return false
|
|
54
|
-
}
|
|
55
|
-
if (event.shiftKey !== parsed.shift) {
|
|
56
|
-
return false
|
|
57
|
-
}
|
|
58
|
-
if (event.altKey !== parsed.alt) {
|
|
59
|
-
return false
|
|
60
|
-
}
|
|
61
|
-
if (event.metaKey !== parsed.meta) {
|
|
62
|
-
return false
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Check key (case-insensitive for letters)
|
|
66
|
-
const eventKey = normalizeKeyName(event.key)
|
|
67
|
-
const hotkeyKey = parsed.key
|
|
68
|
-
|
|
69
|
-
// For single-character keys (not dead keys), try direct event.key match first
|
|
70
|
-
if (eventKey !== 'Dead' && eventKey.length === 1 && hotkeyKey.length === 1) {
|
|
71
|
-
if (eventKey.toUpperCase() === hotkeyKey.toUpperCase()) {
|
|
72
|
-
return true
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// If event.key is a letter, we usually trust the keyboard layout.
|
|
76
|
-
// For ASCII letters: always trust layout (Dvorak, Colemak, AZERTY support).
|
|
77
|
-
// For non-ASCII letters without Alt: trust layout (e.g., Cyrillic keyboards).
|
|
78
|
-
//
|
|
79
|
-
// For non-ASCII letters WITH Alt held down: fall through to the event.code
|
|
80
|
-
// fallback. macOS Option+letter combinations produce non-ASCII letters as
|
|
81
|
-
// event.key (e.g., Option+A → 'å', Option+' → 'æ', Option+P → 'π'), but
|
|
82
|
-
// event.code still reflects the physical key. We want Alt+A to match even
|
|
83
|
-
// when Option+A fires event.key='å'.
|
|
84
|
-
if (
|
|
85
|
-
isSingleLetterKey(eventKey) &&
|
|
86
|
-
(/^[A-Za-z]$/.test(eventKey) || !event.altKey)
|
|
87
|
-
) {
|
|
88
|
-
return false
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Fallback to event.code for dead keys or single-char mismatches where
|
|
93
|
-
// event.key is a non-letter special character.
|
|
94
|
-
// Dead keys: Option+letter on macOS, international layouts produce event.key === 'Dead'
|
|
95
|
-
// Single-char mismatches: Cmd+Option+T gives '†' instead of 'T', Shift+4 gives '$'
|
|
96
|
-
if (
|
|
97
|
-
event.code &&
|
|
98
|
-
(eventKey === 'Dead' || (eventKey.length === 1 && hotkeyKey.length === 1))
|
|
99
|
-
) {
|
|
100
|
-
// fallback for letter keys (common with mac option + letter)
|
|
101
|
-
if (event.code.startsWith('Key')) {
|
|
102
|
-
const codeLetter = event.code.slice(3)
|
|
103
|
-
if (codeLetter.length === 1 && /^[A-Za-z]$/.test(codeLetter)) {
|
|
104
|
-
return codeLetter.toUpperCase() === hotkeyKey.toUpperCase()
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// fallback for number keys (common with mac option + num)
|
|
109
|
-
if (event.code.startsWith('Digit')) {
|
|
110
|
-
const codeDigit = event.code.slice(5)
|
|
111
|
-
if (codeDigit.length === 1 && /^[0-9]$/.test(codeDigit)) {
|
|
112
|
-
return codeDigit === hotkeyKey
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
// Fallback for punctuation keys (e.g., Minus, Slash, BracketLeft).
|
|
116
|
-
// On macOS, Option+punctuation produces composed characters (e.g., Option+- → '–'),
|
|
117
|
-
// but event.code still reports the physical key.
|
|
118
|
-
if (event.code in PUNCTUATION_CODE_MAP) {
|
|
119
|
-
return PUNCTUATION_CODE_MAP[event.code] === hotkeyKey
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return false
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// For special keys, compare exactly (after normalization)
|
|
126
|
-
return eventKey === hotkeyKey
|
|
54
|
+
return matchKeyboardEvent(event, hotkey, platform).matched
|
|
127
55
|
}
|
|
128
56
|
|
|
129
57
|
/**
|
|
@@ -187,7 +115,7 @@ export function createHotkeyHandler(
|
|
|
187
115
|
}
|
|
188
116
|
}
|
|
189
117
|
|
|
190
|
-
type MultiHotkeyHandler = { [K in Hotkey]?: HotkeyCallback }
|
|
118
|
+
export type MultiHotkeyHandler = { [K in Hotkey]?: HotkeyCallback }
|
|
191
119
|
|
|
192
120
|
/**
|
|
193
121
|
* Creates a handler that matches multiple hotkeys.
|
|
@@ -253,7 +181,30 @@ function formatParsedHotkey(parsed: ParsedHotkey): Hotkey {
|
|
|
253
181
|
if (parsed.alt) parts.push('Alt')
|
|
254
182
|
if (parsed.shift) parts.push('Shift')
|
|
255
183
|
if (parsed.meta) parts.push('Meta')
|
|
256
|
-
parts.push(parsed.key)
|
|
184
|
+
parts.push(parsed.code !== undefined ? `[${parsed.code}]` : parsed.key)
|
|
257
185
|
|
|
258
186
|
return parts.join('+') as Hotkey
|
|
259
187
|
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Compares normalized binding identity, including all four modifiers.
|
|
191
|
+
* Aliases resolve on the supplied platform. A physical code and a logical key
|
|
192
|
+
* remain distinct even when one keyboard event could match both.
|
|
193
|
+
*/
|
|
194
|
+
export function areHotkeysEqual(
|
|
195
|
+
left: RegisterableHotkey,
|
|
196
|
+
right: RegisterableHotkey,
|
|
197
|
+
platform = detectPlatform(),
|
|
198
|
+
): boolean {
|
|
199
|
+
const a = parseRegisterableHotkey(left, platform)
|
|
200
|
+
const b = parseRegisterableHotkey(right, platform)
|
|
201
|
+
return (
|
|
202
|
+
a.ctrl === b.ctrl &&
|
|
203
|
+
a.alt === b.alt &&
|
|
204
|
+
a.shift === b.shift &&
|
|
205
|
+
a.meta === b.meta &&
|
|
206
|
+
(a.code !== undefined || b.code !== undefined
|
|
207
|
+
? a.code === b.code
|
|
208
|
+
: keysEqual(a.key, b.key))
|
|
209
|
+
)
|
|
210
|
+
}
|
package/src/parse.ts
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from './
|
|
2
|
+
assertLogicalKey,
|
|
3
|
+
normalizeKeyboardEvent,
|
|
4
|
+
splitHotkeyParts,
|
|
5
|
+
} from './_keyboard-event'
|
|
6
|
+
import { MODIFIER_ALIASES, MODIFIER_ORDER, normalizeKeyName } from './constants'
|
|
7
|
+
import { detectPlatform, resolveModifier } from './platform'
|
|
8
|
+
import type { CanonicalModifier, Key } from './key.types'
|
|
8
9
|
import type {
|
|
9
|
-
CanonicalModifier,
|
|
10
10
|
Hotkey,
|
|
11
|
-
Key,
|
|
12
11
|
ParsedHotkey,
|
|
13
12
|
RawHotkey,
|
|
14
13
|
RegisterableHotkey,
|
|
15
|
-
} from './hotkey'
|
|
14
|
+
} from './hotkey.types'
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* Parses a hotkey string into its component parts.
|
|
19
18
|
*
|
|
20
19
|
* @param hotkey - The hotkey string to parse (e.g., 'Mod+Shift+S')
|
|
21
20
|
* @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
|
|
22
|
-
* @returns A ParsedHotkey
|
|
21
|
+
* @returns A ParsedHotkey with either logical key or physical code and modifier flags
|
|
23
22
|
*
|
|
24
23
|
* @example
|
|
25
24
|
* ```ts
|
|
@@ -32,7 +31,7 @@ export function parseHotkey(
|
|
|
32
31
|
hotkey: Hotkey | (string & {}),
|
|
33
32
|
platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
|
|
34
33
|
): ParsedHotkey {
|
|
35
|
-
const parts = hotkey
|
|
34
|
+
const parts = splitHotkeyParts(hotkey)
|
|
36
35
|
const modifiers: Set<CanonicalModifier> = new Set()
|
|
37
36
|
let key = ''
|
|
38
37
|
|
|
@@ -65,8 +64,9 @@ export function parseHotkey(
|
|
|
65
64
|
key = normalizeKeyName(parts[parts.length - 1]!.trim())
|
|
66
65
|
}
|
|
67
66
|
|
|
67
|
+
const code = /^\[([A-Za-z][A-Za-z0-9]*)\]$/.exec(key)?.[1]
|
|
68
68
|
return {
|
|
69
|
-
key,
|
|
69
|
+
...(code !== undefined ? { code } : { key }),
|
|
70
70
|
ctrl: modifiers.has('Control'),
|
|
71
71
|
shift: modifiers.has('Shift'),
|
|
72
72
|
alt: modifiers.has('Alt'),
|
|
@@ -100,6 +100,7 @@ export function rawHotkeyToParsedHotkey(
|
|
|
100
100
|
raw: RawHotkey,
|
|
101
101
|
platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
|
|
102
102
|
): ParsedHotkey {
|
|
103
|
+
if (raw.code === undefined) assertLogicalKey(raw.key)
|
|
103
104
|
let ctrl = raw.ctrl ?? false
|
|
104
105
|
const shift = raw.shift ?? false
|
|
105
106
|
const alt = raw.alt ?? false
|
|
@@ -129,7 +130,9 @@ export function rawHotkeyToParsedHotkey(
|
|
|
129
130
|
}
|
|
130
131
|
})
|
|
131
132
|
return {
|
|
132
|
-
|
|
133
|
+
...(raw.code !== undefined
|
|
134
|
+
? { code: raw.code }
|
|
135
|
+
: { key: normalizeKeyName(raw.key) }),
|
|
133
136
|
ctrl,
|
|
134
137
|
shift,
|
|
135
138
|
alt,
|
|
@@ -147,6 +150,7 @@ function normalizedHotkeyStringFromParsed(
|
|
|
147
150
|
parsed: ParsedHotkey,
|
|
148
151
|
platform: 'mac' | 'windows' | 'linux',
|
|
149
152
|
): Hotkey {
|
|
153
|
+
if (parsed.code === undefined) assertLogicalKey(parsed.key)
|
|
150
154
|
const canUseMod =
|
|
151
155
|
platform === 'mac'
|
|
152
156
|
? parsed.meta && !parsed.ctrl
|
|
@@ -170,7 +174,11 @@ function normalizedHotkeyStringFromParsed(
|
|
|
170
174
|
}
|
|
171
175
|
}
|
|
172
176
|
|
|
173
|
-
parts.push(
|
|
177
|
+
parts.push(
|
|
178
|
+
parsed.code !== undefined
|
|
179
|
+
? `[${parsed.code}]`
|
|
180
|
+
: normalizeKeyName(parsed.key),
|
|
181
|
+
)
|
|
174
182
|
return parts.join('+') as Hotkey
|
|
175
183
|
}
|
|
176
184
|
|
|
@@ -263,22 +271,31 @@ export function isModifierKey(
|
|
|
263
271
|
* })
|
|
264
272
|
* ```
|
|
265
273
|
*/
|
|
266
|
-
export function parseKeyboardEvent(
|
|
267
|
-
|
|
274
|
+
export function parseKeyboardEvent(
|
|
275
|
+
event: KeyboardEvent,
|
|
276
|
+
platform?: 'mac' | 'windows' | 'linux',
|
|
277
|
+
): ParsedHotkey {
|
|
278
|
+
const normalized = normalizeKeyboardEvent(event, platform)
|
|
279
|
+
// AltGraph is a character-production modifier, not an ordinary Control+Alt
|
|
280
|
+
// shortcut. Record the produced glyph instead of a synthetic Ctrl+Alt chord.
|
|
281
|
+
const ctrl = normalized.altGraph ? false : normalized.ctrl
|
|
282
|
+
const alt = normalized.altGraph ? false : normalized.alt
|
|
283
|
+
// Shift is a real modifier even during AltGraph input; preserve it for replay.
|
|
284
|
+
const shift = normalized.shift
|
|
268
285
|
|
|
269
286
|
// Build modifiers array in canonical order
|
|
270
287
|
const modifiers: Array<CanonicalModifier> = []
|
|
271
|
-
if (
|
|
272
|
-
if (
|
|
273
|
-
if (
|
|
274
|
-
if (
|
|
288
|
+
if (ctrl) modifiers.push('Control')
|
|
289
|
+
if (alt) modifiers.push('Alt')
|
|
290
|
+
if (shift) modifiers.push('Shift')
|
|
291
|
+
if (normalized.meta) modifiers.push('Meta')
|
|
275
292
|
|
|
276
293
|
return {
|
|
277
|
-
key:
|
|
278
|
-
ctrl
|
|
279
|
-
shift
|
|
280
|
-
alt
|
|
281
|
-
meta:
|
|
294
|
+
key: normalized.key,
|
|
295
|
+
ctrl,
|
|
296
|
+
shift,
|
|
297
|
+
alt,
|
|
298
|
+
meta: normalized.meta,
|
|
282
299
|
modifiers,
|
|
283
300
|
}
|
|
284
301
|
}
|
|
@@ -293,7 +310,10 @@ export function normalizeHotkeyFromEvent(
|
|
|
293
310
|
event: KeyboardEvent,
|
|
294
311
|
platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
|
|
295
312
|
): Hotkey {
|
|
296
|
-
return normalizedHotkeyStringFromParsed(
|
|
313
|
+
return normalizedHotkeyStringFromParsed(
|
|
314
|
+
parseKeyboardEvent(event, platform),
|
|
315
|
+
platform,
|
|
316
|
+
)
|
|
297
317
|
}
|
|
298
318
|
|
|
299
319
|
/**
|
|
@@ -320,9 +340,30 @@ export function hasNonModifierKey(
|
|
|
320
340
|
const parsed =
|
|
321
341
|
typeof hotkey === 'string' ? parseHotkey(hotkey, platform) : hotkey
|
|
322
342
|
|
|
343
|
+
if (parsed.code !== undefined) {
|
|
344
|
+
// Physical modifier codes identify a side, but still are not action keys.
|
|
345
|
+
return (
|
|
346
|
+
parsed.code.length > 0 &&
|
|
347
|
+
!/^(Control|Shift|Alt|Meta)(Left|Right)$/.test(parsed.code)
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
323
351
|
// Check if the key part is actually a modifier
|
|
324
352
|
const keyIsModifier = isModifierKey(parsed.key)
|
|
325
353
|
|
|
326
354
|
// A valid hotkey must have a non-modifier key
|
|
327
355
|
return !keyIsModifier && parsed.key.length > 0
|
|
328
356
|
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Parses a string or raw binding, resolving Mod for the supplied platform.
|
|
360
|
+
* Physical tokens such as `[KeyQ]` retain `code`; they never become logical Q.
|
|
361
|
+
*/
|
|
362
|
+
export function parseRegisterableHotkey(
|
|
363
|
+
hotkey: RegisterableHotkey,
|
|
364
|
+
platform = detectPlatform(),
|
|
365
|
+
): ParsedHotkey {
|
|
366
|
+
return typeof hotkey === 'string'
|
|
367
|
+
? parseHotkey(hotkey, platform)
|
|
368
|
+
: rawHotkeyToParsedHotkey(hotkey, platform)
|
|
369
|
+
}
|
package/src/platform.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { CanonicalModifier } from './key.types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detects the current platform based on browser navigator properties.
|
|
5
|
+
*
|
|
6
|
+
* Used internally to resolve platform-adaptive modifiers like 'Mod' (Command on Mac,
|
|
7
|
+
* Control elsewhere) and for platform-specific hotkey formatting.
|
|
8
|
+
*
|
|
9
|
+
* @returns The detected platform: 'mac', 'windows', or 'linux'
|
|
10
|
+
* @remarks Defaults to 'linux' in SSR environments where navigator is undefined
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const platform = detectPlatform() // 'mac' | 'windows' | 'linux'
|
|
15
|
+
* const modifier = resolveModifier('Mod', platform) // 'Meta' on Mac, 'Control' elsewhere
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function detectPlatform(): 'mac' | 'windows' | 'linux' {
|
|
19
|
+
if (typeof navigator === 'undefined') {
|
|
20
|
+
return 'linux' // Default for SSR
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
24
|
+
const platform = navigator.platform?.toLowerCase() ?? ''
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
26
|
+
const userAgent = navigator.userAgent?.toLowerCase() ?? ''
|
|
27
|
+
|
|
28
|
+
if (platform.includes('mac') || userAgent.includes('mac')) {
|
|
29
|
+
return 'mac'
|
|
30
|
+
}
|
|
31
|
+
if (platform.includes('win') || userAgent.includes('win')) {
|
|
32
|
+
return 'windows'
|
|
33
|
+
}
|
|
34
|
+
return 'linux'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the platform-adaptive 'Mod' modifier to the appropriate canonical modifier.
|
|
39
|
+
*
|
|
40
|
+
* The 'Mod' token represents the "primary modifier" on each platform:
|
|
41
|
+
* - macOS: 'Meta' (Command key ⌘)
|
|
42
|
+
* - Windows/Linux: 'Control' (Ctrl key)
|
|
43
|
+
*
|
|
44
|
+
* This enables cross-platform hotkey definitions like 'Mod+S' that automatically
|
|
45
|
+
* map to Command+S on Mac and Ctrl+S on Windows/Linux.
|
|
46
|
+
*
|
|
47
|
+
* @param modifier - The modifier to resolve. If 'Mod', resolves based on platform.
|
|
48
|
+
* @param platform - The target platform. Defaults to auto-detection.
|
|
49
|
+
* @returns The canonical modifier name ('Control', 'Shift', 'Alt', or 'Meta')
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* resolveModifier('Mod', 'mac') // 'Meta'
|
|
54
|
+
* resolveModifier('Mod', 'windows') // 'Control'
|
|
55
|
+
* resolveModifier('Control', 'mac') // 'Control' (unchanged)
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function resolveModifier(
|
|
59
|
+
modifier: CanonicalModifier | 'Mod',
|
|
60
|
+
platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
|
|
61
|
+
): CanonicalModifier {
|
|
62
|
+
if (modifier === 'Mod') {
|
|
63
|
+
return platform === 'mac' ? 'Meta' : 'Control'
|
|
64
|
+
}
|
|
65
|
+
return modifier
|
|
66
|
+
}
|