@tanstack/hotkeys 0.0.1 → 0.1.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -45
  3. package/dist/constants.cjs +444 -0
  4. package/dist/constants.cjs.map +1 -0
  5. package/dist/constants.d.cts +226 -0
  6. package/dist/constants.d.ts +226 -0
  7. package/dist/constants.js +428 -0
  8. package/dist/constants.js.map +1 -0
  9. package/dist/format.cjs +178 -0
  10. package/dist/format.cjs.map +1 -0
  11. package/dist/format.d.cts +110 -0
  12. package/dist/format.d.ts +110 -0
  13. package/dist/format.js +175 -0
  14. package/dist/format.js.map +1 -0
  15. package/dist/hotkey-manager.cjs +420 -0
  16. package/dist/hotkey-manager.cjs.map +1 -0
  17. package/dist/hotkey-manager.d.cts +207 -0
  18. package/dist/hotkey-manager.d.ts +207 -0
  19. package/dist/hotkey-manager.js +419 -0
  20. package/dist/hotkey-manager.js.map +1 -0
  21. package/dist/hotkey.d.cts +278 -0
  22. package/dist/hotkey.d.ts +278 -0
  23. package/dist/index.cjs +54 -0
  24. package/dist/index.d.cts +11 -0
  25. package/dist/index.d.ts +11 -0
  26. package/dist/index.js +11 -0
  27. package/dist/key-state-tracker.cjs +197 -0
  28. package/dist/key-state-tracker.cjs.map +1 -0
  29. package/dist/key-state-tracker.d.cts +107 -0
  30. package/dist/key-state-tracker.d.ts +107 -0
  31. package/dist/key-state-tracker.js +196 -0
  32. package/dist/key-state-tracker.js.map +1 -0
  33. package/dist/match.cjs +143 -0
  34. package/dist/match.cjs.map +1 -0
  35. package/dist/match.d.cts +79 -0
  36. package/dist/match.d.ts +79 -0
  37. package/dist/match.js +141 -0
  38. package/dist/match.js.map +1 -0
  39. package/dist/parse.cjs +266 -0
  40. package/dist/parse.cjs.map +1 -0
  41. package/dist/parse.d.cts +169 -0
  42. package/dist/parse.d.ts +169 -0
  43. package/dist/parse.js +258 -0
  44. package/dist/parse.js.map +1 -0
  45. package/dist/recorder.cjs +177 -0
  46. package/dist/recorder.cjs.map +1 -0
  47. package/dist/recorder.d.cts +108 -0
  48. package/dist/recorder.d.ts +108 -0
  49. package/dist/recorder.js +177 -0
  50. package/dist/recorder.js.map +1 -0
  51. package/dist/sequence.cjs +242 -0
  52. package/dist/sequence.cjs.map +1 -0
  53. package/dist/sequence.d.cts +109 -0
  54. package/dist/sequence.d.ts +109 -0
  55. package/dist/sequence.js +240 -0
  56. package/dist/sequence.js.map +1 -0
  57. package/dist/validate.cjs +116 -0
  58. package/dist/validate.cjs.map +1 -0
  59. package/dist/validate.d.cts +56 -0
  60. package/dist/validate.d.ts +56 -0
  61. package/dist/validate.js +114 -0
  62. package/dist/validate.js.map +1 -0
  63. package/package.json +55 -7
  64. package/src/constants.ts +514 -0
  65. package/src/format.ts +261 -0
  66. package/src/hotkey-manager.ts +822 -0
  67. package/src/hotkey.ts +411 -0
  68. package/src/index.ts +10 -0
  69. package/src/key-state-tracker.ts +249 -0
  70. package/src/match.ts +222 -0
  71. package/src/parse.ts +368 -0
  72. package/src/recorder.ts +266 -0
  73. package/src/sequence.ts +391 -0
  74. package/src/validate.ts +171 -0
@@ -0,0 +1,266 @@
1
+ import { Store } from '@tanstack/store'
2
+ import { detectPlatform } from './constants'
3
+ import {
4
+ convertToModFormat,
5
+ hasNonModifierKey,
6
+ isModifierKey,
7
+ keyboardEventToHotkey,
8
+ } from './parse'
9
+ import type { Hotkey } from './hotkey'
10
+
11
+ /**
12
+ * State interface for the HotkeyRecorder.
13
+ */
14
+ export interface HotkeyRecorderState {
15
+ /** Whether recording is currently active */
16
+ isRecording: boolean
17
+ /** The currently recorded hotkey (for live preview) */
18
+ recordedHotkey: Hotkey | null
19
+ }
20
+
21
+ /**
22
+ * Options for configuring a HotkeyRecorder instance.
23
+ */
24
+ export interface HotkeyRecorderOptions {
25
+ /** Callback when a hotkey is successfully recorded */
26
+ onRecord: (hotkey: Hotkey) => void
27
+ /** Optional callback when recording is cancelled (Escape pressed) */
28
+ onCancel?: () => void
29
+ /** Optional callback when shortcut is cleared (Backspace/Delete pressed) */
30
+ onClear?: () => void
31
+ }
32
+
33
+ /**
34
+ * Framework-agnostic class for recording keyboard shortcuts.
35
+ *
36
+ * This class handles all the complexity of capturing keyboard events,
37
+ * converting them to hotkey strings, and handling edge cases like
38
+ * Escape to cancel or Backspace/Delete to clear.
39
+ *
40
+ * State Management:
41
+ * - Uses TanStack Store for reactive state management
42
+ * - State can be accessed via `recorder.store.state` when using the class directly
43
+ * - When using framework adapters (React), use `useStore` hooks for reactive state
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const recorder = new HotkeyRecorder({
48
+ * onRecord: (hotkey) => {
49
+ * console.log('Recorded:', hotkey)
50
+ * },
51
+ * onCancel: () => {
52
+ * console.log('Recording cancelled')
53
+ * },
54
+ * })
55
+ *
56
+ * // Start recording
57
+ * recorder.start()
58
+ *
59
+ * // Access state directly
60
+ * console.log(recorder.store.state.isRecording) // true
61
+ *
62
+ * // Subscribe to changes with TanStack Store
63
+ * const unsubscribe = recorder.store.subscribe(() => {
64
+ * console.log('Recording:', recorder.store.state.isRecording)
65
+ * })
66
+ *
67
+ * // Cleanup
68
+ * recorder.destroy()
69
+ * unsubscribe()
70
+ * ```
71
+ */
72
+ export class HotkeyRecorder {
73
+ /**
74
+ * The TanStack Store instance containing the recorder state.
75
+ * Use this to subscribe to state changes or access current state.
76
+ */
77
+ readonly store: Store<HotkeyRecorderState> = new Store<HotkeyRecorderState>({
78
+ isRecording: false,
79
+ recordedHotkey: null,
80
+ })
81
+
82
+ #keydownHandler: ((event: KeyboardEvent) => void) | null = null
83
+ #options: HotkeyRecorderOptions
84
+ #platform: 'mac' | 'windows' | 'linux'
85
+
86
+ constructor(options: HotkeyRecorderOptions) {
87
+ this.#options = options
88
+ this.#platform = detectPlatform()
89
+ }
90
+
91
+ /**
92
+ * Updates the recorder options, including callbacks.
93
+ * This allows framework adapters to sync callback changes without recreating the recorder.
94
+ */
95
+ setOptions(options: Partial<HotkeyRecorderOptions>): void {
96
+ this.#options = {
97
+ ...this.#options,
98
+ ...options,
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Start recording a new hotkey.
104
+ *
105
+ * Sets up a keydown event listener that captures keyboard events
106
+ * and converts them to hotkey strings. Recording continues until
107
+ * a valid hotkey is recorded, Escape is pressed, or stop/cancel is called.
108
+ */
109
+ start(): void {
110
+ // Prevent starting recording if already recording
111
+ if (this.#keydownHandler) {
112
+ return
113
+ }
114
+
115
+ // Update store state
116
+ this.store.setState(() => ({
117
+ isRecording: true,
118
+ recordedHotkey: null,
119
+ }))
120
+
121
+ // Create keydown handler
122
+ const handler = (event: KeyboardEvent) => {
123
+ // Check if we're still recording (handler might be called after stop/cancel)
124
+ if (!this.#keydownHandler) {
125
+ return
126
+ }
127
+
128
+ event.preventDefault()
129
+ event.stopPropagation()
130
+
131
+ // Handle Escape to cancel
132
+ if (event.key === 'Escape') {
133
+ this.cancel()
134
+ return
135
+ }
136
+
137
+ // Handle Backspace/Delete to clear shortcut
138
+ if (event.key === 'Backspace' || event.key === 'Delete') {
139
+ if (
140
+ !event.ctrlKey &&
141
+ !event.shiftKey &&
142
+ !event.altKey &&
143
+ !event.metaKey
144
+ ) {
145
+ this.#options.onClear?.()
146
+ this.#options.onRecord('' as Hotkey)
147
+ this.stop()
148
+ return
149
+ }
150
+ }
151
+
152
+ // Ignore pure modifier keys (wait for a non-modifier key)
153
+ if (isModifierKey(event)) {
154
+ return
155
+ }
156
+
157
+ // Convert event to hotkey string using library function
158
+ const hotkey = keyboardEventToHotkey(event)
159
+
160
+ // Always convert to Mod format for portability
161
+ const finalHotkey = convertToModFormat(hotkey, this.#platform)
162
+
163
+ // Validate: must have at least one non-modifier key
164
+ if (hasNonModifierKey(finalHotkey, this.#platform)) {
165
+ // Remove listener FIRST to prevent any additional events
166
+ const handlerToRemove = this.#keydownHandler as
167
+ | ((event: KeyboardEvent) => void)
168
+ | null
169
+ if (handlerToRemove) {
170
+ this.#removeListener(handlerToRemove)
171
+ this.#keydownHandler = null
172
+ }
173
+
174
+ // Update store state immediately
175
+ this.store.setState(() => ({
176
+ isRecording: false,
177
+ recordedHotkey: finalHotkey,
178
+ }))
179
+
180
+ // Call callback AFTER listener is removed and state is set
181
+ this.#options.onRecord(finalHotkey)
182
+ }
183
+ }
184
+
185
+ this.#keydownHandler = handler
186
+ this.#addListener(handler)
187
+ }
188
+
189
+ /**
190
+ * Stop recording (same as cancel, but doesn't call onCancel).
191
+ *
192
+ * Removes the event listener and resets the recording state.
193
+ */
194
+ stop(): void {
195
+ // Remove event listener immediately
196
+ if (this.#keydownHandler) {
197
+ this.#removeListener(this.#keydownHandler)
198
+ this.#keydownHandler = null
199
+ }
200
+
201
+ // Update store state
202
+ this.store.setState(() => ({
203
+ isRecording: false,
204
+ recordedHotkey: null,
205
+ }))
206
+ }
207
+
208
+ /**
209
+ * Cancel recording without saving.
210
+ *
211
+ * Removes the event listener, resets the recording state, and calls
212
+ * the onCancel callback if provided.
213
+ */
214
+ cancel(): void {
215
+ // Remove event listener immediately
216
+ if (this.#keydownHandler) {
217
+ this.#removeListener(this.#keydownHandler)
218
+ this.#keydownHandler = null
219
+ }
220
+
221
+ // Update store state
222
+ this.store.setState(() => ({
223
+ isRecording: false,
224
+ recordedHotkey: null,
225
+ }))
226
+
227
+ // Call cancel callback
228
+ this.#options.onCancel?.()
229
+ }
230
+
231
+ /**
232
+ * Adds the keydown event listener to the document.
233
+ */
234
+ #addListener(handler: (event: KeyboardEvent) => void): void {
235
+ if (typeof document === 'undefined') {
236
+ return // SSR safety
237
+ }
238
+
239
+ document.addEventListener('keydown', handler, true)
240
+ }
241
+
242
+ /**
243
+ * Removes the keydown event listener from the document.
244
+ */
245
+ #removeListener(handler: (event: KeyboardEvent) => void): void {
246
+ if (typeof document === 'undefined') {
247
+ return
248
+ }
249
+
250
+ document.removeEventListener('keydown', handler, true)
251
+ }
252
+
253
+ /**
254
+ * Clean up event listeners and reset state.
255
+ *
256
+ * Call this when you're done with the recorder to ensure
257
+ * all event listeners are properly removed.
258
+ */
259
+ destroy(): void {
260
+ this.stop()
261
+ this.store.setState(() => ({
262
+ isRecording: false,
263
+ recordedHotkey: null,
264
+ }))
265
+ }
266
+ }
@@ -0,0 +1,391 @@
1
+ import { detectPlatform } from './constants'
2
+ import { parseHotkey } from './parse'
3
+ import { matchesKeyboardEvent } from './match'
4
+ import type { HotkeyOptions } from './hotkey-manager'
5
+ import type {
6
+ Hotkey,
7
+ HotkeyCallback,
8
+ HotkeyCallbackContext,
9
+ ParsedHotkey,
10
+ } from './hotkey'
11
+
12
+ /**
13
+ * Options for hotkey sequence matching.
14
+ */
15
+ export interface SequenceOptions extends HotkeyOptions {
16
+ /** Timeout between keys in milliseconds. Default: 1000 */
17
+ timeout?: number
18
+ }
19
+
20
+ /**
21
+ * A sequence of hotkeys for Vim-style shortcuts.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const gotoTop: HotkeySequence = ['G', 'G'] // gg
26
+ * const deleteLine: HotkeySequence = ['D', 'D'] // dd
27
+ * const deleteWord: HotkeySequence = ['D', 'I', 'W'] // diw
28
+ * ```
29
+ */
30
+ export type HotkeySequence = Array<Hotkey>
31
+
32
+ /**
33
+ * Default timeout between keys in a sequence (in milliseconds).
34
+ */
35
+ const DEFAULT_SEQUENCE_TIMEOUT = 1000
36
+
37
+ let sequenceIdCounter = 0
38
+
39
+ /**
40
+ * Generates a unique ID for sequence registrations.
41
+ */
42
+ function generateSequenceId(): string {
43
+ return `sequence_${++sequenceIdCounter}`
44
+ }
45
+
46
+ /**
47
+ * Internal representation of a sequence registration.
48
+ */
49
+ interface SequenceRegistration {
50
+ id: string
51
+ sequence: HotkeySequence
52
+ parsedSequence: Array<ParsedHotkey>
53
+ callback: HotkeyCallback
54
+ options: SequenceOptions
55
+ currentIndex: number
56
+ lastKeyTime: number
57
+ }
58
+
59
+ /**
60
+ * Manages keyboard sequence matching for Vim-style shortcuts.
61
+ *
62
+ * This class allows registering multi-key sequences like 'g g' or 'd d'
63
+ * that trigger callbacks when the full sequence is pressed within
64
+ * a configurable timeout.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const matcher = SequenceManager.getInstance()
69
+ *
70
+ * // Register 'g g' to go to top
71
+ * const unregister = matcher.register(['G', 'G'], (event, context) => {
72
+ * scrollToTop()
73
+ * }, { timeout: 500 })
74
+ *
75
+ * // Later, to unregister:
76
+ * unregister()
77
+ * ```
78
+ */
79
+ export class SequenceManager {
80
+ static #instance: SequenceManager | null = null
81
+
82
+ #registrations: Map<string, SequenceRegistration> = new Map()
83
+ #keydownListener: ((event: KeyboardEvent) => void) | null = null
84
+ #platform: 'mac' | 'windows' | 'linux'
85
+
86
+ private constructor() {
87
+ this.#platform = detectPlatform()
88
+ }
89
+
90
+ /**
91
+ * Gets the singleton instance of SequenceManager.
92
+ */
93
+ static getInstance(): SequenceManager {
94
+ if (!SequenceManager.#instance) {
95
+ SequenceManager.#instance = new SequenceManager()
96
+ }
97
+ return SequenceManager.#instance
98
+ }
99
+
100
+ /**
101
+ * Resets the singleton instance. Useful for testing.
102
+ */
103
+ static resetInstance(): void {
104
+ if (SequenceManager.#instance) {
105
+ SequenceManager.#instance.destroy()
106
+ SequenceManager.#instance = null
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Registers a hotkey sequence handler.
112
+ *
113
+ * @param sequence - Array of hotkey strings that form the sequence
114
+ * @param callback - Function to call when the sequence is completed
115
+ * @param options - Options for the sequence behavior
116
+ * @returns A function to unregister the sequence
117
+ */
118
+ register(
119
+ sequence: HotkeySequence,
120
+ callback: HotkeyCallback,
121
+ options: SequenceOptions = {},
122
+ ): () => void {
123
+ if (sequence.length === 0) {
124
+ throw new Error('Sequence must contain at least one hotkey')
125
+ }
126
+
127
+ const id = generateSequenceId()
128
+ const platform = options.platform ?? this.#platform
129
+ const parsedSequence = sequence.map((hotkey) =>
130
+ parseHotkey(hotkey, platform),
131
+ )
132
+
133
+ const registration: SequenceRegistration = {
134
+ id,
135
+ sequence,
136
+ parsedSequence,
137
+ callback,
138
+ options: {
139
+ timeout: DEFAULT_SEQUENCE_TIMEOUT,
140
+ preventDefault: true,
141
+ stopPropagation: true,
142
+ enabled: true,
143
+ ...options,
144
+ platform,
145
+ },
146
+ currentIndex: 0,
147
+ lastKeyTime: 0,
148
+ }
149
+
150
+ this.#registrations.set(id, registration)
151
+ this.#ensureListener()
152
+
153
+ return () => {
154
+ this.#unregister(id)
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Unregisters a sequence by its registration ID.
160
+ */
161
+ #unregister(id: string): void {
162
+ this.#registrations.delete(id)
163
+
164
+ if (this.#registrations.size === 0) {
165
+ this.#removeListener()
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Ensures the keydown listener is attached.
171
+ */
172
+ #ensureListener(): void {
173
+ if (typeof document === 'undefined') {
174
+ return // SSR safety
175
+ }
176
+
177
+ if (!this.#keydownListener) {
178
+ this.#keydownListener = this.#handleKeyDown.bind(this)
179
+ document.addEventListener('keydown', this.#keydownListener)
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Removes the keydown listener.
185
+ */
186
+ #removeListener(): void {
187
+ if (typeof document === 'undefined') {
188
+ return
189
+ }
190
+
191
+ if (this.#keydownListener) {
192
+ document.removeEventListener('keydown', this.#keydownListener)
193
+ this.#keydownListener = null
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Handles keydown events for sequence matching.
199
+ */
200
+ #handleKeyDown(event: KeyboardEvent): void {
201
+ const now = Date.now()
202
+
203
+ for (const registration of this.#registrations.values()) {
204
+ if (!registration.options.enabled) {
205
+ continue
206
+ }
207
+
208
+ const timeout = registration.options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT
209
+
210
+ // Check if sequence has timed out
211
+ if (
212
+ registration.currentIndex > 0 &&
213
+ now - registration.lastKeyTime > timeout
214
+ ) {
215
+ // Reset the sequence
216
+ registration.currentIndex = 0
217
+ }
218
+
219
+ const expectedHotkey =
220
+ registration.parsedSequence[registration.currentIndex]
221
+ if (!expectedHotkey) {
222
+ continue
223
+ }
224
+
225
+ // Check if current key matches the expected key in sequence
226
+ if (
227
+ matchesKeyboardEvent(
228
+ event,
229
+ expectedHotkey,
230
+ registration.options.platform,
231
+ )
232
+ ) {
233
+ registration.lastKeyTime = now
234
+ registration.currentIndex++
235
+
236
+ // Check if sequence is complete
237
+ if (registration.currentIndex >= registration.parsedSequence.length) {
238
+ // Sequence complete!
239
+ if (registration.options.preventDefault) {
240
+ event.preventDefault()
241
+ }
242
+ if (registration.options.stopPropagation) {
243
+ event.stopPropagation()
244
+ }
245
+
246
+ const context: HotkeyCallbackContext = {
247
+ hotkey: registration.sequence.join(' ') as Hotkey,
248
+ parsedHotkey:
249
+ registration.parsedSequence[
250
+ registration.parsedSequence.length - 1
251
+ ]!,
252
+ }
253
+
254
+ registration.callback(event, context)
255
+
256
+ // Reset for next sequence
257
+ registration.currentIndex = 0
258
+ }
259
+ } else if (registration.currentIndex > 0) {
260
+ // Key didn't match and we were in the middle of a sequence
261
+ // Check if it matches the start of the sequence (for overlapping sequences)
262
+ const firstHotkey = registration.parsedSequence[0]!
263
+ if (
264
+ matchesKeyboardEvent(
265
+ event,
266
+ firstHotkey,
267
+ registration.options.platform,
268
+ )
269
+ ) {
270
+ registration.currentIndex = 1
271
+ registration.lastKeyTime = now
272
+ } else {
273
+ // Reset the sequence
274
+ registration.currentIndex = 0
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Resets all sequence progress.
282
+ */
283
+ resetAll(): void {
284
+ for (const registration of this.#registrations.values()) {
285
+ registration.currentIndex = 0
286
+ registration.lastKeyTime = 0
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Gets the number of registered sequences.
292
+ */
293
+ getRegistrationCount(): number {
294
+ return this.#registrations.size
295
+ }
296
+
297
+ /**
298
+ * Destroys the manager and removes all listeners.
299
+ */
300
+ destroy(): void {
301
+ this.#removeListener()
302
+ this.#registrations.clear()
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Gets the singleton SequenceManager instance.
308
+ * Convenience function for accessing the manager.
309
+ */
310
+ export function getSequenceManager(): SequenceManager {
311
+ return SequenceManager.getInstance()
312
+ }
313
+
314
+ /**
315
+ * Creates a simple sequence matcher for one-off use.
316
+ *
317
+ * @param sequence - The sequence of hotkeys to match
318
+ * @param options - Options including timeout
319
+ * @returns An object with match() and reset() methods
320
+ *
321
+ * @example
322
+ * ```ts
323
+ * const matcher = createSequenceMatcher(['G', 'G'], { timeout: 500 })
324
+ *
325
+ * document.addEventListener('keydown', (event) => {
326
+ * if (matcher.match(event)) {
327
+ * console.log('Sequence matched!')
328
+ * }
329
+ * })
330
+ * ```
331
+ */
332
+ export function createSequenceMatcher(
333
+ sequence: HotkeySequence,
334
+ options: { timeout?: number; platform?: 'mac' | 'windows' | 'linux' } = {},
335
+ ): {
336
+ match: (event: KeyboardEvent) => boolean
337
+ reset: () => void
338
+ getProgress: () => number
339
+ } {
340
+ const platform = options.platform ?? detectPlatform()
341
+ const timeout = options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT
342
+ const parsedSequence = sequence.map((hotkey) => parseHotkey(hotkey, platform))
343
+
344
+ let currentIndex = 0
345
+ let lastKeyTime = 0
346
+
347
+ return {
348
+ match(event: KeyboardEvent): boolean {
349
+ const now = Date.now()
350
+
351
+ // Check timeout
352
+ if (currentIndex > 0 && now - lastKeyTime > timeout) {
353
+ currentIndex = 0
354
+ }
355
+
356
+ const expected = parsedSequence[currentIndex]
357
+ if (!expected) {
358
+ return false
359
+ }
360
+
361
+ if (matchesKeyboardEvent(event, expected, platform)) {
362
+ lastKeyTime = now
363
+ currentIndex++
364
+
365
+ if (currentIndex >= parsedSequence.length) {
366
+ currentIndex = 0
367
+ return true
368
+ }
369
+ } else if (currentIndex > 0) {
370
+ // Check if it matches start of sequence
371
+ if (matchesKeyboardEvent(event, parsedSequence[0]!, platform)) {
372
+ currentIndex = 1
373
+ lastKeyTime = now
374
+ } else {
375
+ currentIndex = 0
376
+ }
377
+ }
378
+
379
+ return false
380
+ },
381
+
382
+ reset(): void {
383
+ currentIndex = 0
384
+ lastKeyTime = 0
385
+ },
386
+
387
+ getProgress(): number {
388
+ return currentIndex
389
+ },
390
+ }
391
+ }