Sequence
Multi-step keyboard sequence matching with timeout support.
Multi-step keyboard sequence matching with timeout support. Handle complex shortcuts like Ctrl+K followed by Ctrl+D, or simple character sequences like g then d.
import { SequenceManager, createSequenceMatcher } from '@gentleduck/vim/sequence'
import type {
SequenceStep,
SequenceOptions,
SequenceRegistration,
SequenceHandle,
SequenceState,
} from '@gentleduck/vim/sequence'import { SequenceManager, createSequenceMatcher } from '@gentleduck/vim/sequence'
import type {
SequenceStep,
SequenceOptions,
SequenceRegistration,
SequenceHandle,
SequenceState,
} from '@gentleduck/vim/sequence'When to use SequenceManager vs Registry
The Registry + KeyHandler system supports sequences like g+d (single character steps) out of the box. It is the right choice for most applications.
Types
SequenceStep
interface SequenceStep {
binding: string // e.g. 'ctrl+k', 'g', 'd'
}interface SequenceStep {
binding: string // e.g. 'ctrl+k', 'g', 'd'
}SequenceOptions
interface SequenceOptions {
timeout?: number // ms between steps, default: 600
enabled?: boolean // default: true
}interface SequenceOptions {
timeout?: number // ms between steps, default: 600
enabled?: boolean // default: true
}SequenceRegistration
interface SequenceRegistration {
steps: SequenceStep[]
handler: () => void
options?: SequenceOptions
}interface SequenceRegistration {
steps: SequenceStep[]
handler: () => void
options?: SequenceOptions
}SequenceHandle
interface SequenceHandle {
unregister: () => void
}interface SequenceHandle {
unregister: () => void
}SequenceState
Reports progress of the most-advanced matching entry.
interface SequenceState {
completedSteps: number
totalSteps: number
isMatching: boolean
}interface SequenceState {
completedSteps: number
totalSteps: number
isMatching: boolean
}SequenceManager
Manages multiple sequence registrations and matches keyboard events against them.
register(registration)
manager.register(registration: SequenceRegistration): SequenceHandlemanager.register(registration: SequenceRegistration): SequenceHandleExample:
const manager = new SequenceManager()
const handle = manager.register({
steps: [{ binding: 'ctrl+k' }, { binding: 'ctrl+d' }],
handler: () => console.log('Ctrl+K, Ctrl+D pressed!'),
options: { timeout: 800 },
})
// Later:
handle.unregister()const manager = new SequenceManager()
const handle = manager.register({
steps: [{ binding: 'ctrl+k' }, { binding: 'ctrl+d' }],
handler: () => console.log('Ctrl+K, Ctrl+D pressed!'),
options: { timeout: 800 },
})
// Later:
handle.unregister()handleKeyEvent(event)
Feed a keyboard event. Returns true if any sequence completed and fired.
manager.handleKeyEvent(event: KeyboardEvent): booleanmanager.handleKeyEvent(event: KeyboardEvent): booleanYou must wire this up yourself:
document.addEventListener('keydown', (e) => manager.handleKeyEvent(e))document.addEventListener('keydown', (e) => manager.handleKeyEvent(e))Or use the React KeyProvider, which does this automatically.
getState()
Returns the aggregate matching state across all entries.
manager.getState(): SequenceStatemanager.getState(): SequenceStateExample: Use this to show a "waiting for next key" indicator:
const state = manager.getState()
if (state.isMatching) {
showHint(`Step ${state.completedSteps}/${state.totalSteps}`)
}const state = manager.getState()
if (state.isMatching) {
showHint(`Step ${state.completedSteps}/${state.totalSteps}`)
}reset()
Resets all in-progress sequence matching state.
manager.reset(): voidmanager.reset(): voiddestroy()
Clears all registrations, cancels all timers, and removes all state.
manager.destroy(): voidmanager.destroy(): voidcreateSequenceMatcher(steps, handler, options?)
A convenience function that creates a lightweight single-sequence matcher without needing to instantiate SequenceManager yourself.
function createSequenceMatcher(
steps: string[],
handler: () => void,
options?: SequenceOptions,
): {
feed: (event: KeyboardEvent) => boolean
reset: () => void
getState: () => SequenceState
}function createSequenceMatcher(
steps: string[],
handler: () => void,
options?: SequenceOptions,
): {
feed: (event: KeyboardEvent) => boolean
reset: () => void
getState: () => SequenceState
}Example:
const matcher = createSequenceMatcher(
['g', 'd'],
() => navigate('/dashboard'),
{ timeout: 500 },
)
document.addEventListener('keydown', (e) => matcher.feed(e))const matcher = createSequenceMatcher(
['g', 'd'],
() => navigate('/dashboard'),
{ timeout: 500 },
)
document.addEventListener('keydown', (e) => matcher.feed(e))Matching behavior
When a keyboard event arrives:
Pure modifier keys (Shift, Control, Alt, Meta) are ignored.
For each enabled entry, check if the event matches the expected next step.
If it matches, advance the step counter.
If the sequence is complete, fire the handler and reset.
If it does not match, reset that entry to step 0 and retry step 0 with the current event (so pressing a wrong key mid-sequence does not lose the first key of a new attempt).
This retry behavior means sequences are forgiving: if you press g, x, g, d, the g+d sequence still fires on the last two keys.