Skip to main content

Recorder

Record key combinations for settings UIs where users customize their shortcuts.

import { KeyRecorder, KeyStateTracker } from '@gentleduck/vim/recorder'
import type { KeyRecorderState, KeyRecorderOptions, KeyStateSnapshot } from '@gentleduck/vim/recorder'
import { KeyRecorder, KeyStateTracker } from '@gentleduck/vim/recorder'
import type { KeyRecorderState, KeyRecorderOptions, KeyStateSnapshot } from '@gentleduck/vim/recorder'

Overview

The recorder module provides two classes:

  • KeyRecorder — captures a full key combination (modifiers + key) and outputs a canonical binding string. Designed for settings UIs where users press a shortcut to define it.
  • KeyStateTracker — tracks which keys are currently held down. Simpler than KeyRecorder, no recording logic, just real-time state.

Types

KeyRecorderState

interface KeyRecorderState {
  activeKeys: string[]     // Currently held key descriptors
  recorded: string | null  // The final recorded binding, or null
  isRecording: boolean     // Whether the recorder is listening
}
interface KeyRecorderState {
  activeKeys: string[]     // Currently held key descriptors
  recorded: string | null  // The final recorded binding, or null
  isRecording: boolean     // Whether the recorder is listening
}

KeyRecorderOptions

interface KeyRecorderOptions {
  onRecord?: (binding: string) => void
  onStart?: () => void
  onStop?: () => void
}
interface KeyRecorderOptions {
  onRecord?: (binding: string) => void
  onStart?: () => void
  onStop?: () => void
}

KeyStateSnapshot

interface KeyStateSnapshot {
  pressed: ReadonlySet<string>
  hasModifier: boolean
}
interface KeyStateSnapshot {
  pressed: ReadonlySet<string>
  hasModifier: boolean
}

KeyRecorder

constructor(options?)

new KeyRecorder(options?: KeyRecorderOptions)
new KeyRecorder(options?: KeyRecorderOptions)

The callbacks are optional. onRecord fires every time the user presses a non-modifier key while holding modifiers.

start(target?)

Begin recording on the given target. Defaults to document. Calls event.preventDefault() and event.stopPropagation() on all key events while recording, so the user's keystrokes do not trigger other bindings.

recorder.start(target?: HTMLElement | Document): void
recorder.start(target?: HTMLElement | Document): void

stop()

Stop recording and clean up event listeners.

recorder.stop(): void
recorder.stop(): void

getState()

Get the current recorder state.

recorder.getState(): KeyRecorderState
recorder.getState(): KeyRecorderState

reset()

Clear the recorded binding without stopping.

recorder.reset(): void
recorder.reset(): void

destroy()

Stop recording and clear all state. Call this on cleanup.

recorder.destroy(): void
recorder.destroy(): void

How it works

When start() is called, the recorder listens for keydown and keyup on the target.

Modifier keys are tracked as they are pressed and released.

When a non-modifier key is pressed, the recorder builds a canonical binding string from the held modifiers + the key.

The onRecord callback fires with the binding string.

If the window loses focus, all held keys are cleared to prevent "stuck" modifiers.

Example:

const recorder = new KeyRecorder({
  onRecord: (binding) => {
    console.log('User pressed:', binding) // e.g. 'ctrl+shift+k'
    recorder.stop()
  },
})
 
recorder.start(document.body)
// User presses Ctrl+Shift+K
// onRecord fires with 'ctrl+shift+k'
const recorder = new KeyRecorder({
  onRecord: (binding) => {
    console.log('User pressed:', binding) // e.g. 'ctrl+shift+k'
    recorder.stop()
  },
})
 
recorder.start(document.body)
// User presses Ctrl+Shift+K
// onRecord fires with 'ctrl+shift+k'

KeyStateTracker

A simpler alternative that just tracks which keys are currently down.

attach(target?)

tracker.attach(target?: HTMLElement | Document): void
tracker.attach(target?: HTMLElement | Document): void

detach()

tracker.detach(): void
tracker.detach(): void

getSnapshot()

tracker.getSnapshot(): KeyStateSnapshot
tracker.getSnapshot(): KeyStateSnapshot

isKeyPressed(key)

tracker.isKeyPressed(key: string): boolean
tracker.isKeyPressed(key: string): boolean

destroy()

tracker.destroy(): void
tracker.destroy(): void

Example:

const tracker = new KeyStateTracker()
tracker.attach(document)
 
// In a game loop or animation frame:
function update() {
  if (tracker.isKeyPressed('w')) moveForward()
  if (tracker.isKeyPressed('shift')) sprint()
  requestAnimationFrame(update)
}
const tracker = new KeyStateTracker()
tracker.attach(document)
 
// In a game loop or animation frame:
function update() {
  if (tracker.isKeyPressed('w')) moveForward()
  if (tracker.isKeyPressed('shift')) sprint()
  requestAnimationFrame(update)
}

React hook: useKeyRecorder