@tanstack/hotkeys 0.10.1 → 0.12.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.
@@ -1,4 +1,4 @@
1
- import { Hotkey } from "./hotkey.types.js";
1
+ import { Hotkey, HotkeyChord, HotkeySequence } from "./hotkey.types.js";
2
2
  import { HotkeyRecorderValidationContext, RecorderOptions } from "./recorder-options.js";
3
3
  import { Store } from "@tanstack/store";
4
4
  //#region src/hotkey-recorder.d.ts
@@ -8,77 +8,97 @@ import { Store } from "@tanstack/store";
8
8
  export interface HotkeyRecorderState {
9
9
  /** Whether recording is currently active */
10
10
  isRecording: boolean;
11
- /** The currently recorded hotkey (for live preview) */
11
+ /** Chords recorded so far in a multi-step recording (empty for `maxSteps: 1`) */
12
+ steps: HotkeySequence;
13
+ /** The last committed hotkey: a chord, or a sequence of two or more steps */
12
14
  recordedHotkey: Hotkey | null;
13
15
  }
14
- /**
15
- * Options for configuring a HotkeyRecorder instance.
16
- */
17
- export interface HotkeyRecorderOptions extends RecorderOptions {
18
- /** Return true to accept, or false/a message to reject while staying in recording mode. */
19
- validate?: (hotkey: Hotkey, context: HotkeyRecorderValidationContext) => boolean | string;
20
- /** Callback when a hotkey is successfully recorded */
21
- onRecord: (hotkey: Hotkey) => void;
16
+ /** Which keys commit a multi-step recording before it reaches `maxSteps`. */
17
+ export type HotkeyRecorderCommitKeys = 'enter' | 'none';
18
+ interface HotkeyRecorderBaseOptions extends RecorderOptions {
22
19
  /** Optional callback when recording is cancelled (Escape pressed) */
23
20
  onCancel?: () => void;
24
- /** Optional callback when shortcut is cleared (Backspace/Delete pressed) */
21
+ /**
22
+ * Optional callback when the recording is cleared: Backspace/Delete with no
23
+ * modifiers and no recorded steps
24
+ */
25
25
  onClear?: () => void;
26
26
  /**
27
27
  * Whether to ignore keyboard events from input-like elements (text inputs,
28
- * textarea, select, contenteditable). When true, typing in inputs passes
29
- * through normally instead of being captured as a hotkey recording.
30
- * Escape always works regardless of this setting.
31
- * @default true
28
+ * textarea, select, contenteditable) while recording. When true, typing in
29
+ * inputs passes through normally (Escape still cancels). Defaults to true.
32
30
  */
33
31
  ignoreInputs?: boolean;
34
32
  }
33
+ /** Options for recording a single chord. This is the default (`maxSteps: 1`). */
34
+ export interface HotkeyChordRecorderOptions extends HotkeyRecorderBaseOptions {
35
+ /** Maximum number of chords to record. Default: 1 */
36
+ maxSteps?: 1;
37
+ /** Return true to accept, or false/a message to reject while staying in recording mode. */
38
+ validate?: (hotkey: HotkeyChord, context: HotkeyRecorderValidationContext) => boolean | string;
39
+ /** Callback when a chord is successfully recorded */
40
+ onRecord: (hotkey: HotkeyChord) => void;
41
+ }
42
+ /**
43
+ * Options for recording up to `maxSteps` chords. A one-step recording commits
44
+ * as a chord string; two or more steps commit as a sequence.
45
+ */
46
+ export interface HotkeySequenceRecorderOptions extends HotkeyRecorderBaseOptions {
47
+ /** Maximum number of chords to record. Reaching it commits automatically. Use `Infinity` for no limit. */
48
+ maxSteps: number;
49
+ /** Return true to accept, or false/a message to reject while staying in recording mode. */
50
+ validate?: (hotkey: Hotkey, context: HotkeyRecorderValidationContext) => boolean | string;
51
+ /** Callback when a hotkey is successfully recorded */
52
+ onRecord: (hotkey: Hotkey) => void;
53
+ /** Whether plain Enter commits the recorded steps. Default: 'enter' */
54
+ commitKeys?: HotkeyRecorderCommitKeys;
55
+ /** Commit automatically after this many milliseconds without a new step. Disabled when unset or 0. */
56
+ idleTimeoutMs?: number;
57
+ }
58
+ /**
59
+ * Options for HotkeyRecorder. `maxSteps` (default 1) selects between recording
60
+ * a single chord and recording a sequence.
61
+ */
62
+ export type HotkeyRecorderOptions = HotkeyChordRecorderOptions | HotkeySequenceRecorderOptions;
35
63
  /**
36
64
  * Framework-agnostic class for recording keyboard shortcuts.
37
65
  *
38
- * This class handles all the complexity of capturing keyboard events,
39
- * converting them to hotkey strings, and handling edge cases like
40
- * Escape to cancel or Backspace/Delete to clear.
66
+ * With the default `maxSteps: 1`, the first valid chord commits. With a larger
67
+ * `maxSteps`, each chord adds a step; the recording commits on Enter (unless
68
+ * `commitKeys: 'none'`), after `idleTimeoutMs`, or when it reaches `maxSteps`.
69
+ * Backspace/Delete removes the last step, and on an empty recording calls
70
+ * `onClear`. Escape cancels. A one-step result is a chord string.
41
71
  *
42
72
  * State Management:
43
73
  * - Uses TanStack Store for reactive state management
44
74
  * - State can be accessed via `recorder.store.state` when using the class directly
45
- * - When using framework adapters (React), use `useSelector` hooks for reactive state
75
+ * - When using framework adapters, use the `useHotkeyRecorder` hook for reactive state
46
76
  *
47
77
  * @example
48
78
  * ```ts
49
79
  * const recorder = new HotkeyRecorder({
50
80
  * onRecord: (hotkey) => {
51
- * console.log('Recorded:', hotkey)
52
- * },
53
- * onCancel: () => {
54
- * console.log('Recording cancelled')
81
+ * console.log('Recorded:', hotkey) // 'Mod+S'
55
82
  * },
56
83
  * })
57
84
  *
58
- * // Start recording
59
- * recorder.start()
60
- *
61
- * // Access state directly
62
- * console.log(recorder.store.state.isRecording) // true
63
- *
64
- * // Subscribe to changes with TanStack Store
65
- * const unsubscribe = recorder.store.subscribe(() => {
66
- * console.log('Recording:', recorder.store.state.isRecording)
85
+ * const sequenceRecorder = new HotkeyRecorder({
86
+ * maxSteps: 3,
87
+ * onRecord: (hotkey) => {
88
+ * console.log('Recorded:', hotkey) // 'Mod+S' or ['G', 'H']
89
+ * },
67
90
  * })
68
91
  *
69
- * // Cleanup
92
+ * recorder.start()
93
+ * // ... later
70
94
  * recorder.destroy()
71
- * unsubscribe()
72
95
  * ```
73
96
  */
74
97
  export declare class HotkeyRecorder {
75
98
  #private;
76
- /**
77
- * The TanStack Store instance containing the recorder state.
78
- * Use this to subscribe to state changes or access current state.
79
- */
80
99
  readonly store: Store<HotkeyRecorderState>;
81
- constructor(options: HotkeyRecorderOptions);
100
+ constructor(options: HotkeyChordRecorderOptions | (() => HotkeyChordRecorderOptions));
101
+ constructor(options: HotkeySequenceRecorderOptions | (() => HotkeySequenceRecorderOptions));
82
102
  /**
83
103
  * Updates the recorder options, including callbacks.
84
104
  * This allows framework adapters to sync callback changes without recreating the recorder.
@@ -88,10 +108,15 @@ export declare class HotkeyRecorder {
88
108
  * Start recording a new hotkey.
89
109
  *
90
110
  * Sets up a keydown event listener that captures keyboard events
91
- * and converts them to hotkey strings. Recording continues until
92
- * a valid hotkey is recorded, Escape is pressed, or stop/cancel is called.
111
+ * and records chords according to `maxSteps`.
93
112
  */
94
113
  start(): void;
114
+ /**
115
+ * Commits the recorded steps after validation and conflict checks.
116
+ * Called automatically at `maxSteps`, on Enter, or after `idleTimeoutMs`.
117
+ * A rejected commit leaves recording active with its steps intact.
118
+ */
119
+ commit(): void;
95
120
  /**
96
121
  * Stop recording (same as cancel, but doesn't call onCancel).
97
122
  *
@@ -1,6 +1,6 @@
1
1
  import { detectPlatform } from "./platform.js";
2
2
  import { normalizeKeyboardEvent } from "./_keyboard-event.js";
3
- import { isModifierKey, parseHotkey } from "./parse.js";
3
+ import { isModifierKey, parseHotkey, simplifyHotkey } from "./parse.js";
4
4
  import { validateHotkey } from "./validate.js";
5
5
  import { beginRecording, captureRecordingEvent, endRecording } from "./_recording-guard.js";
6
6
  import { shouldIgnoreInputEvent } from "./_event-target.js";
@@ -9,59 +9,64 @@ import { chordRejection, hotkeyChordFromKeydown } from "./_recorder-chord.js";
9
9
  import { Store } from "@tanstack/store";
10
10
 
11
11
  //#region src/hotkey-recorder.ts
12
+ const initialState = {
13
+ isRecording: false,
14
+ steps: [],
15
+ recordedHotkey: null
16
+ };
12
17
  /**
13
18
  * Framework-agnostic class for recording keyboard shortcuts.
14
19
  *
15
- * This class handles all the complexity of capturing keyboard events,
16
- * converting them to hotkey strings, and handling edge cases like
17
- * Escape to cancel or Backspace/Delete to clear.
20
+ * With the default `maxSteps: 1`, the first valid chord commits. With a larger
21
+ * `maxSteps`, each chord adds a step; the recording commits on Enter (unless
22
+ * `commitKeys: 'none'`), after `idleTimeoutMs`, or when it reaches `maxSteps`.
23
+ * Backspace/Delete removes the last step, and on an empty recording calls
24
+ * `onClear`. Escape cancels. A one-step result is a chord string.
18
25
  *
19
26
  * State Management:
20
27
  * - Uses TanStack Store for reactive state management
21
28
  * - State can be accessed via `recorder.store.state` when using the class directly
22
- * - When using framework adapters (React), use `useSelector` hooks for reactive state
29
+ * - When using framework adapters, use the `useHotkeyRecorder` hook for reactive state
23
30
  *
24
31
  * @example
25
32
  * ```ts
26
33
  * const recorder = new HotkeyRecorder({
27
34
  * onRecord: (hotkey) => {
28
- * console.log('Recorded:', hotkey)
29
- * },
30
- * onCancel: () => {
31
- * console.log('Recording cancelled')
35
+ * console.log('Recorded:', hotkey) // 'Mod+S'
32
36
  * },
33
37
  * })
34
38
  *
35
- * // Start recording
36
- * recorder.start()
37
- *
38
- * // Access state directly
39
- * console.log(recorder.store.state.isRecording) // true
40
- *
41
- * // Subscribe to changes with TanStack Store
42
- * const unsubscribe = recorder.store.subscribe(() => {
43
- * console.log('Recording:', recorder.store.state.isRecording)
39
+ * const sequenceRecorder = new HotkeyRecorder({
40
+ * maxSteps: 3,
41
+ * onRecord: (hotkey) => {
42
+ * console.log('Recorded:', hotkey) // 'Mod+S' or ['G', 'H']
43
+ * },
44
44
  * })
45
45
  *
46
- * // Cleanup
46
+ * recorder.start()
47
+ * // ... later
47
48
  * recorder.destroy()
48
- * unsubscribe()
49
49
  * ```
50
50
  */
51
51
  var HotkeyRecorder = class {
52
- /**
53
- * The TanStack Store instance containing the recorder state.
54
- * Use this to subscribe to state changes or access current state.
55
- */
56
- store = new Store({
57
- isRecording: false,
58
- recordedHotkey: null
59
- });
52
+ store = new Store(initialState);
60
53
  #keydownHandler = null;
61
- #options;
54
+ #optionsSource;
55
+ #optionOverrides = {};
62
56
  #platform;
57
+ #events = [];
58
+ #idleTimer = null;
59
+ get #options() {
60
+ return {
61
+ ...typeof this.#optionsSource === "function" ? this.#optionsSource() : this.#optionsSource,
62
+ ...this.#optionOverrides
63
+ };
64
+ }
65
+ get #maxSteps() {
66
+ return Math.max(1, this.#options.maxSteps ?? 1);
67
+ }
63
68
  constructor(options) {
64
- this.#options = options;
69
+ this.#optionsSource = options;
65
70
  this.#platform = detectPlatform();
66
71
  }
67
72
  /**
@@ -69,24 +74,40 @@ var HotkeyRecorder = class {
69
74
  * This allows framework adapters to sync callback changes without recreating the recorder.
70
75
  */
71
76
  setOptions(options) {
72
- this.#options = {
73
- ...this.#options,
77
+ this.#optionOverrides = {
78
+ ...this.#optionOverrides,
74
79
  ...options
75
80
  };
76
81
  }
82
+ #clearIdleTimer() {
83
+ if (this.#idleTimer !== null) {
84
+ clearTimeout(this.#idleTimer);
85
+ this.#idleTimer = null;
86
+ }
87
+ }
88
+ #scheduleIdleTimer() {
89
+ this.#clearIdleTimer();
90
+ const ms = this.#maxSteps > 1 ? this.#options.idleTimeoutMs : void 0;
91
+ if (ms === void 0 || ms <= 0 || this.store.state.steps.length === 0) return;
92
+ this.#idleTimer = setTimeout(() => {
93
+ this.#idleTimer = null;
94
+ if (this.#keydownHandler && this.store.state.steps.length >= 1) this.commit();
95
+ }, ms);
96
+ }
77
97
  /**
78
98
  * Start recording a new hotkey.
79
99
  *
80
100
  * Sets up a keydown event listener that captures keyboard events
81
- * and converts them to hotkey strings. Recording continues until
82
- * a valid hotkey is recorded, Escape is pressed, or stop/cancel is called.
101
+ * and records chords according to `maxSteps`.
83
102
  */
84
103
  start() {
85
104
  if (this.#keydownHandler) return;
105
+ this.#clearIdleTimer();
106
+ this.#events = [];
86
107
  beginRecording(this);
87
108
  this.store.setState(() => ({
88
- isRecording: true,
89
- recordedHotkey: null
109
+ ...initialState,
110
+ isRecording: true
90
111
  }));
91
112
  const handler = (event) => {
92
113
  if (!this.#keydownHandler) return;
@@ -101,120 +122,152 @@ var HotkeyRecorder = class {
101
122
  this.cancel();
102
123
  return;
103
124
  }
104
- if (event.key === "Backspace" || event.key === "Delete") {
105
- if (!event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
125
+ const unmodified = !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey;
126
+ const multiStep = this.#maxSteps > 1;
127
+ if ((event.key === "Backspace" || event.key === "Delete") && unmodified) {
128
+ if (this.store.state.steps.length === 0) {
106
129
  this.stop();
107
130
  this.#options.onClear?.();
108
131
  return;
109
132
  }
133
+ this.#events.pop();
134
+ this.store.setState((s) => ({
135
+ ...s,
136
+ steps: s.steps.slice(0, -1)
137
+ }));
138
+ if (this.store.state.steps.length === 0) this.#clearIdleTimer();
139
+ else this.#scheduleIdleTimer();
140
+ return;
141
+ }
142
+ const options = this.#options;
143
+ if (multiStep && options.commitKeys !== "none" && event.key === "Enter" && unmodified) {
144
+ if (this.store.state.steps.length >= 1) this.commit();
145
+ return;
110
146
  }
111
147
  if (isModifierKey(event.key) || event.key === "AltGraph") return;
112
148
  const rejection = chordRejection(event, {
113
- ...this.#options,
149
+ ...options,
114
150
  platform
115
151
  });
116
152
  if (rejection) {
117
- this.#options.onReject?.(rejection);
153
+ options.onReject?.(rejection);
118
154
  return;
119
155
  }
120
- const finalHotkey = hotkeyChordFromKeydown(event, platform, this.#options.recordBy);
121
- if (finalHotkey === null) return;
122
- const validation = validateHotkey(finalHotkey);
156
+ const chord = hotkeyChordFromKeydown(event, platform, options.recordBy);
157
+ if (chord === null) return;
158
+ const validation = validateHotkey(chord);
123
159
  if (!validation.valid) {
124
- this.#options.onReject?.({
160
+ options.onReject?.({
125
161
  reason: "invalid",
126
162
  message: validation.errors.join("; "),
127
- hotkey: finalHotkey
128
- });
129
- return;
130
- }
131
- const accepted = this.#options.validate?.(finalHotkey, {
132
- event,
133
- parsedHotkey: parseHotkey(finalHotkey, platform)
134
- });
135
- if (accepted !== void 0 && accepted !== true) {
136
- this.#options.onReject?.({
137
- reason: "validation",
138
- message: typeof accepted === "string" ? accepted : "This shortcut is not allowed.",
139
- hotkey: finalHotkey
163
+ hotkey: chord
140
164
  });
141
165
  return;
142
166
  }
143
- if (this.#options.detectConflicts) {
144
- const conflicts = findHotkeyConflicts(finalHotkey, {
145
- ...typeof this.#options.detectConflicts === "object" ? this.#options.detectConflicts : {},
146
- platform,
147
- events: [event]
148
- });
149
- if (conflicts.length) {
150
- this.#options.onReject?.({
151
- reason: "conflict",
152
- message: "This shortcut conflicts with a registered binding.",
153
- hotkey: finalHotkey,
154
- conflicts
155
- });
156
- return;
157
- }
158
- }
159
- endRecording(this);
160
- const handlerToRemove = this.#keydownHandler;
161
- if (handlerToRemove) {
162
- this.#removeListener(handlerToRemove);
163
- this.#keydownHandler = null;
164
- }
165
- this.store.setState(() => ({
166
- isRecording: false,
167
- recordedHotkey: finalHotkey
167
+ this.#events.push(event);
168
+ this.store.setState((s) => ({
169
+ ...s,
170
+ steps: [...s.steps, chord]
168
171
  }));
169
- this.#options.onRecord(finalHotkey);
172
+ if (this.store.state.steps.length >= this.#maxSteps) this.commit();
173
+ else this.#scheduleIdleTimer();
170
174
  };
171
175
  this.#keydownHandler = handler;
172
176
  this.#addListener(handler);
173
177
  }
174
178
  /**
175
- * Stop recording (same as cancel, but doesn't call onCancel).
176
- *
177
- * Removes the event listener and resets the recording state.
179
+ * Commits the recorded steps after validation and conflict checks.
180
+ * Called automatically at `maxSteps`, on Enter, or after `idleTimeoutMs`.
181
+ * A rejected commit leaves recording active with its steps intact.
178
182
  */
179
- stop() {
183
+ commit() {
184
+ const steps = this.store.state.steps;
185
+ if (steps.length < 1) return;
186
+ const options = this.#options;
187
+ const platform = options.platform ?? this.#platform;
188
+ const hotkey = simplifyHotkey(steps);
189
+ const single = this.#maxSteps === 1;
190
+ this.#clearIdleTimer();
191
+ const rejectAndKeepRecording = (rejection) => {
192
+ if (single) {
193
+ this.#events = [];
194
+ this.store.setState((s) => ({
195
+ ...s,
196
+ steps: []
197
+ }));
198
+ }
199
+ options.onReject?.(rejection);
200
+ };
201
+ const parsedSequence = steps.map((step) => parseHotkey(step, platform));
202
+ const accepted = options.validate?.(hotkey, {
203
+ event: this.#events[this.#events.length - 1],
204
+ parsedHotkey: parsedSequence[parsedSequence.length - 1],
205
+ events: [...this.#events],
206
+ parsedSequence
207
+ });
208
+ if (accepted !== void 0 && accepted !== true) {
209
+ rejectAndKeepRecording({
210
+ reason: "validation",
211
+ message: typeof accepted === "string" ? accepted : `This ${single ? "shortcut" : "sequence"} is not allowed.`,
212
+ hotkey
213
+ });
214
+ return;
215
+ }
216
+ if (options.detectConflicts) {
217
+ const conflicts = findHotkeyConflicts(hotkey, {
218
+ ...typeof options.detectConflicts === "object" ? options.detectConflicts : {},
219
+ platform,
220
+ events: this.#events
221
+ });
222
+ if (conflicts.length) {
223
+ rejectAndKeepRecording({
224
+ reason: "conflict",
225
+ message: `This ${single ? "shortcut" : "sequence"} conflicts with a registered hotkey.`,
226
+ hotkey,
227
+ conflicts
228
+ });
229
+ return;
230
+ }
231
+ }
232
+ this.#finish(hotkey);
233
+ options.onRecord(hotkey);
234
+ }
235
+ /** Stops listening and stores the final state. */
236
+ #finish(recordedHotkey) {
180
237
  endRecording(this);
238
+ this.#events = [];
239
+ this.#clearIdleTimer();
181
240
  if (this.#keydownHandler) {
182
241
  this.#removeListener(this.#keydownHandler);
183
242
  this.#keydownHandler = null;
184
243
  }
185
244
  this.store.setState(() => ({
186
- isRecording: false,
187
- recordedHotkey: null
245
+ ...initialState,
246
+ recordedHotkey
188
247
  }));
189
248
  }
190
249
  /**
250
+ * Stop recording (same as cancel, but doesn't call onCancel).
251
+ *
252
+ * Removes the event listener and resets the recording state.
253
+ */
254
+ stop() {
255
+ this.#finish(null);
256
+ }
257
+ /**
191
258
  * Cancel recording without saving.
192
259
  *
193
260
  * Removes the event listener, resets the recording state, and calls
194
261
  * the onCancel callback if provided.
195
262
  */
196
263
  cancel() {
197
- endRecording(this);
198
- if (this.#keydownHandler) {
199
- this.#removeListener(this.#keydownHandler);
200
- this.#keydownHandler = null;
201
- }
202
- this.store.setState(() => ({
203
- isRecording: false,
204
- recordedHotkey: null
205
- }));
264
+ this.#finish(null);
206
265
  this.#options.onCancel?.();
207
266
  }
208
- /**
209
- * Adds the keydown event listener to the document.
210
- */
211
267
  #addListener(handler) {
212
268
  if (typeof document === "undefined") return;
213
269
  document.addEventListener("keydown", handler, true);
214
270
  }
215
- /**
216
- * Removes the keydown event listener from the document.
217
- */
218
271
  #removeListener(handler) {
219
272
  if (typeof document === "undefined") return;
220
273
  document.removeEventListener("keydown", handler, true);
@@ -226,11 +279,7 @@ var HotkeyRecorder = class {
226
279
  * all event listeners are properly removed.
227
280
  */
228
281
  destroy() {
229
- this.stop();
230
- this.store.setState(() => ({
231
- isRecording: false,
232
- recordedHotkey: null
233
- }));
282
+ this.#finish(null);
234
283
  }
235
284
  };
236
285
 
@@ -88,16 +88,16 @@ export type FourModifierHotkey = `Control+Alt+Shift+Meta+${Key}`;
88
88
  * @example
89
89
  * ```ts
90
90
  * // Cross-platform shortcuts (recommended)
91
- * const save: Hotkey = 'Mod+S' // Command+S on Mac, Ctrl+S on Windows/Linux
92
- * const saveAs: Hotkey = 'Mod+Shift+S' // Command+Shift+S on Mac, Ctrl+Shift+S elsewhere
93
- * const comment: Hotkey = 'Mod+/' // Command+/ on Mac, Ctrl+/ elsewhere
91
+ * const save: HotkeyChord = 'Mod+S' // Command+S on Mac, Ctrl+S on Windows/Linux
92
+ * const saveAs: HotkeyChord = 'Mod+Shift+S' // Command+Shift+S on Mac, Ctrl+Shift+S elsewhere
93
+ * const comment: HotkeyChord = 'Mod+/' // Command+/ on Mac, Ctrl+/ elsewhere
94
94
  *
95
95
  * // Platform-specific shortcuts
96
- * const macOnly: Hotkey = 'Meta+S' // Command+S on Mac only
97
- * const windowsOnly: Hotkey = 'Control+S' // Ctrl+S on Windows/Linux only
96
+ * const macOnly: HotkeyChord = 'Meta+S' // Command+S on Mac only
97
+ * const windowsOnly: HotkeyChord = 'Control+S' // Ctrl+S on Windows/Linux only
98
98
  * ```
99
99
  */
100
- export type Hotkey = Key | SingleModifierHotkey | TwoModifierHotkey | ThreeModifierHotkey | FourModifierHotkey;
100
+ export type HotkeyChord = Key | SingleModifierHotkey | TwoModifierHotkey | ThreeModifierHotkey | FourModifierHotkey;
101
101
  /** Resolved modifier flags shared by both parsed binding variants. */
102
102
  export interface ParsedModifiers {
103
103
  /** Whether Control is required. */
@@ -171,17 +171,31 @@ export type RawHotkey = ({
171
171
  key?: never;
172
172
  } & RawModifiers);
173
173
  /**
174
- * A hotkey that can be passed to `HotkeyManager.register()` and `useHotkey()`.
175
- * Either a type-safe string (`Hotkey`) or a raw object (`RawHotkey`).
174
+ * A sequence of chords pressed one after another (Vim-style), such as `['G', 'G']`.
175
+ *
176
+ * Steps may include modifiers; the same modifier can appear on consecutive
177
+ * steps (e.g. `['Shift+R', 'Shift+T']`). Modifier-only key events do not
178
+ * advance or reset matching. Automatic repeats and IME composition are ignored.
179
+ * A one-step sequence is treated as its single chord.
180
+ */
181
+ export type HotkeySequence = ReadonlyArray<HotkeyChord>;
182
+ /**
183
+ * A hotkey: one chord (`'Mod+S'`) or a sequence of chords (`['G', 'G']`).
184
+ */
185
+ export type Hotkey = HotkeyChord | HotkeySequence;
186
+ /** A single chord as a type-safe string (`HotkeyChord`) or a raw object (`RawHotkey`). */
187
+ export type RegisterableHotkeyChord = HotkeyChord | RawHotkey;
188
+ /**
189
+ * Anything that can be registered: a chord string, a raw chord object, or a sequence.
176
190
  */
177
- export type RegisterableHotkey = Hotkey | RawHotkey;
191
+ export type RegisterableHotkey = RegisterableHotkeyChord | HotkeySequence;
178
192
  /**
179
193
  * Context passed to hotkey callbacks along with the keyboard event.
180
194
  */
181
195
  export interface HotkeyCallbackContext {
182
- /** The original hotkey string that was registered */
196
+ /** The registered hotkey: a chord string or a sequence */
183
197
  hotkey: Hotkey;
184
- /** The parsed representation of the hotkey */
198
+ /** The parsed chord that completed the match (the last step of a sequence) */
185
199
  parsedHotkey: ParsedHotkey;
186
200
  }
187
201
  /**
@@ -200,7 +214,7 @@ export interface HotkeyCallbackContext {
200
214
  */
201
215
  export type HotkeyCallback = (event: KeyboardEvent, context: HotkeyCallbackContext) => void;
202
216
  /**
203
- * Metadata for hotkey and sequence registrations.
217
+ * Metadata for hotkey registrations, chords and sequences alike.
204
218
  * Includes `name`, `description`, and `group` by default. Extend via declaration merging:
205
219
  *
206
220
  * @example
package/dist/index.d.ts CHANGED
@@ -1,18 +1,17 @@
1
1
  import { CanonicalModifier, EditingKey, FunctionKey, IndividualKey, Key, LetterKey, LogicalKey, Modifier, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PhysicalKey, PhysicalKeyCode, PunctuationKey } from "./key.types.js";
2
+ import { ConflictBehavior, FourModifierHotkey, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyChord, HotkeyMeta, HotkeySequence, ParsedHotkey, ParsedModifiers, RawHotkey, RawModifiers, RegisterableHotkey, RegisterableHotkeyChord, SingleModifierHotkey, ThreeModifierHotkey, TwoModifierHotkey } from "./hotkey.types.js";
3
+ import { HotkeyChordOptions, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyManager, HotkeyOptions, HotkeyOptionsFor, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeySequenceOptions, HotkeySequenceRegistration, HotkeySequenceRegistrationView, Target, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
4
+ import { AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CustomHotkeys, DefinedAppHotkeys, Register, RegisteredAppHotkeys, defineAppHotkeys, getAppHotkeyRegistrationOptions, isAppHotkey, isCustomHotkey, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey } from "./app-hotkeys.js";
2
5
  import { ALL_KEYS, EDITING_KEYS, FUNCTION_KEYS, LETTER_KEYS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, isSingleLetterKey, normalizeKeyName } from "./constants.js";
3
6
  import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
4
7
  import { detectPlatform, resolveModifier } from "./platform.js";
5
- import { ConflictBehavior, FourModifierHotkey, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyMeta, ParsedHotkey, ParsedModifiers, RawHotkey, RawModifiers, RegisterableHotkey, SingleModifierHotkey, ThreeModifierHotkey, TwoModifierHotkey } from "./hotkey.types.js";
6
- import { DisplayHotkey, FormatDisplayOptions, formatForDisplay, formatHotkey, formatHotkeySequence, formatWithLabels } from "./format.js";
7
- import { hasNonModifierKey, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey } from "./parse.js";
8
- import { ValidationResult, assertValidHotkey, checkHotkey, validateHotkey } from "./validate.js";
9
- import { CreateHotkeyHandlerOptions, KeyboardEventMatch, MultiHotkeyHandler, areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, matchesKeyboardEvent } from "./match.js";
8
+ import { DisplayHotkey, DisplayHotkeyChord, FormatDisplayOptions, formatForDisplay, formatHotkey } from "./format.js";
9
+ import { hasNonModifierKey, isHotkeySequence, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey, simplifyHotkey } from "./parse.js";
10
+ import { ValidatableHotkey, ValidationResult, assertValidHotkey, checkHotkey, validateHotkey } from "./validate.js";
11
+ import { CreateHotkeyHandlerOptions, DEFAULT_SEQUENCE_TIMEOUT, KeyboardEventMatch, MultiHotkeyHandler, areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, matchesKeyboardEvent } from "./match.js";
10
12
  import { HeldModifierOptions, matchesHeldModifiers } from "./hint.js";
11
- import { HotkeyManager, HotkeyOptions, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
12
- import { DEFAULT_SEQUENCE_TIMEOUT, HotkeySequence, SequenceManager, SequenceOptions, SequenceRegistration, SequenceRegistrationHandle, SequenceRegistrationView, Target, createSequenceMatcher, getSequenceManager } from "./sequence-manager.js";
13
13
  import { HotkeyConflict, HotkeyConflictOptions, findHotkeyConflicts } from "./conflicts.js";
14
14
  import { KeyStateTracker, KeyStateTrackerState, getKeyStateTracker } from "./key-state-tracker.js";
15
- import { HotkeyRecorderValidationContext, HotkeySequenceRecorderValidationContext, RecorderKeyMode, RecorderOptions, RecorderRejection } from "./recorder-options.js";
16
- import { HotkeyRecorder, HotkeyRecorderOptions, HotkeyRecorderState } from "./hotkey-recorder.js";
17
- import { HotkeySequenceRecorder, HotkeySequenceRecorderCommitKeys, HotkeySequenceRecorderOptions, HotkeySequenceRecorderState } from "./hotkey-sequence-recorder.js";
18
- export { ALL_KEYS, CanonicalModifier, ConflictBehavior, CreateHotkeyHandlerOptions, DEFAULT_SEQUENCE_TIMEOUT, DisplayHotkey, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkey, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyRecorder, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeySequence, HotkeySequenceRecorder, HotkeySequenceRecorderCommitKeys, HotkeySequenceRecorderOptions, HotkeySequenceRecorderState, HotkeySequenceRecorderValidationContext, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkey, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkey, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, RegisterableHotkey, SequenceManager, SequenceOptions, SequenceRegistration, SequenceRegistrationHandle, SequenceRegistrationView, SingleModifierHotkey, Target, ThreeModifierHotkey, TwoModifierHotkey, ValidationResult, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, formatHotkeySequence, formatWithLabels, getHotkeyManager, getKeyStateTracker, getSequenceManager, hasNonModifierKey, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeKeyName, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey, resolveModifier, toHotkeyRegistrationView, validateHotkey };
15
+ import { HotkeyRecorderValidationContext, RecorderKeyMode, RecorderOptions, RecorderRejection } from "./recorder-options.js";
16
+ import { HotkeyChordRecorderOptions, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeySequenceRecorderOptions } from "./hotkey-recorder.js";
17
+ export { ALL_KEYS, AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CanonicalModifier, ConflictBehavior, CreateHotkeyHandlerOptions, CustomHotkeys, DEFAULT_SEQUENCE_TIMEOUT, DefinedAppHotkeys, DisplayHotkey, DisplayHotkeyChord, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkey, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyChord, HotkeyChordOptions, HotkeyChordRecorderOptions, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyOptionsFor, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeySequence, HotkeySequenceOptions, HotkeySequenceRecorderOptions, HotkeySequenceRegistration, HotkeySequenceRegistrationView, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkey, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkey, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, Register, RegisterableHotkey, RegisterableHotkeyChord, RegisteredAppHotkeys, SingleModifierHotkey, Target, ThreeModifierHotkey, TwoModifierHotkey, ValidatableHotkey, ValidationResult, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeKeyName, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, provideAppHotkeys, publishAppHotkeysStore, rawHotkeyToParsedHotkey, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };