| name | duck-vim |
| description | Use when working with @gentleduck/vim - the keyboard command engine. Covers hotkey parsing, key sequence matching, chord bindings, platform-aware mod resolution, key recording, and the React hooks (useKeyBind, useKeySequence, useKeyCommands, useKeyRecorder). Not for vim-the-editor - this is a keyboard shortcut library. |
| allowed-tools | Read Grep |
| argument-hint | [keybind-or-topic] |
@gentleduck/vim
Expert scope: packages/duck-vim/. Parses hotkey strings, matches keyboard events, supports sequences, records key presses, and provides React hooks. Import by subpath: @gentleduck/vim/{module}.
parser - @gentleduck/vim/parser
Exports: parseKeyBind(binding, platform?) => ParsedKeyBind, normalizeKeyBind(binding, platform?) => string, validateKeyBind(binding) => ValidationResult, keyboardEventToDescriptor(e) => string | null, KEY_ALIASES, MODIFIER_KEYS, types ParsedKeyBind, ValidationResult.
parseKeyBind('mod+shift+k', 'mac')
normalizeKeyBind('Shift+Mod+s', 'mac')
validateKeyBind('ctrl+ctrl+k')
matcher - @gentleduck/vim/matcher
Exports: createKeyBindHandler(config) => (e) => void, createMultiKeyBindHandler(configs[]) => (e) => void, matchesKeyboardEvent(parsed, event, options?) => boolean, isInputElement(el) => boolean, types KeyBindHandlerConfig, MatchOptions, SingleKeyBindOptions.
const handler = createKeyBindHandler({
binding: 'Mod+S', handler: (e) => save(),
options: { preventDefault: true, ignoreInputs: true },
})
document.addEventListener('keydown', handler)
command - @gentleduck/vim/command
Exports: Registry, KeyHandler, types Command, KeyBindOptions, RegistrationHandle, RegistryEntry, RegistryClass.
Command has { name: string, description?: string, execute: (args?) => void | Promise<void> }.
KeyBindOptions fields: enabled?, preventDefault?, stopPropagation?, ignoreInputs?, eventType? ('keydown'|'keyup'), requireReset?, conflictBehavior? ('warn'|'error'|'replace'|'allow', default 'warn').
const registry = new Registry( false)
const handle = registry.register('ctrl+k', { name: 'Palette', execute: () => openPalette() },
{ preventDefault: true, conflictBehavior: 'error' })
handle.unregister()
handle.setEnabled(false)
handle.isEnabled()
handle.resetFired()
registry.getAllCommands()
registry.hasCommand('ctrl+k')
const kh = new KeyHandler(registry, 600, {})
kh.attach(document)
kh.detach(document)
platform - @gentleduck/vim/platform
Exports: detectPlatform() => Platform, isMac(platform?) => boolean, resolveMod(platform?) => 'meta' | 'ctrl', type Platform ('mac' | 'windows' | 'linux').
SSR safe: detectPlatform() falls back to 'linux' when navigator is unavailable.
format - @gentleduck/vim/format
Exports: formatForDisplay(binding, options?) => string, formatWithLabels(binding, options?) => string, SYMBOL_MAP, LABEL_MAP, type FormatOptions { platform?, separator? }.
formatForDisplay('mod+shift+k', { platform: 'mac' })
formatWithLabels('mod+enter', { platform: 'mac' })
sequence - @gentleduck/vim/sequence
Exports: createSequenceMatcher(steps[], handler, options?) => { feed, reset, getState }, SequenceManager, types SequenceOptions, SequenceHandle, SequenceState, SequenceRegistration, SequenceStep.
const seq = createSequenceMatcher(['g', 'g'], () => scrollToTop(), { timeout: 1000 })
seq.feed(event)
const mgr = new SequenceManager()
const handle = mgr.register({ steps: [{ binding: 'g' }, { binding: 'd' }], handler: goToDefinition })
mgr.handleKeyEvent(event)
handle.unregister()
mgr.destroy()
recorder - @gentleduck/vim/recorder
Exports: KeyRecorder, KeyStateTracker, types KeyRecorderOptions, KeyRecorderState, KeyStateSnapshot.
const recorder = new KeyRecorder({ onRecord: (b) => console.log(b), onStart, onStop })
recorder.start(document)
recorder.getState()
recorder.stop(); recorder.reset(); recorder.destroy()
const tracker = new KeyStateTracker()
tracker.attach(document)
tracker.isKeyPressed('shift')
tracker.getSnapshot()
tracker.detach(); tracker.destroy()
react - @gentleduck/vim/react
Exports: useKeyBind, useKeySequence, useKeyCommands, useKeyRecorder, KeyProvider, KeyContext, types KeyContextValue, KeyBindHookOptions, SequenceHookOptions, KeyRecorderReturn.
useKeyBind('mod+k', () => openMenu(), { preventDefault: true, targetRef })
useKeySequence(['g', 'g'], () => scrollToTop(), { timeout: 1000 })
useKeyCommands({ 'ctrl+k': { name: 'Search', execute: openSearch } }, optionalKeyBindOptions)
const { state, start, stop, reset } = useKeyRecorder()
KeyProvider props: debug?, timeoutMs? (default 600), defaultOptions?: Partial<KeyBindOptions>, children.
Recipes
Command palette: <KeyProvider><App /></KeyProvider>, then useKeyBind('mod+k', () => setPaletteOpen(true), { preventDefault: true, ignoreInputs: true }).
Customizable keybinds: Use useKeyRecorder() -- render state.recorded, call start(el) on focus, stop() on blur, persist result, pass to useKeyBind dynamically.
Conflict detection: conflictBehavior: 'error' in dev to catch collisions; 'replace' for user overrides.
Pitfalls
- Binding silent -- check
isInputElement, options.enabled, and that KeyProvider wraps the tree
- Sequence timeout -- default 600ms; increase with
{ timeout: ms }
- Always use
mod+ not ctrl+/meta+; always pass string[] not space-separated to sequences
createKeyBindHandler takes { binding, handler, options } -- not positional args
useKeyCommands requires KeyProvider -- warns and no-ops without it
- For many bindings prefer
useKeyCommands over repeated useKeyBind