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 = document)

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 = document): void
recorder.start(target: HTMLElement | Document = 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