Skip to main content

Sequence

Multi-step keyboard sequence matching with timeout support.

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): SequenceHandle
manager.register(registration: SequenceRegistration): SequenceHandle

Example:

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): boolean
manager.handleKeyEvent(event: KeyboardEvent): boolean

You must wire this up yourself:

document.addEventListener('keydown', (e) => manager.handleKeyEvent(e))
document.addEventListener('keydown', (e) => manager.handleKeyEvent(e))

getState()

Returns the aggregate matching state across all entries.

manager.getState(): SequenceState
manager.getState(): SequenceState

Example: 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(): void
manager.reset(): void

destroy()

Clears all registrations, cancels all timers, and removes all state.

manager.destroy(): void
manager.destroy(): void

createSequenceMatcher(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).