Skip to main content

duck vim

Tiny, framework-agnostic keyboard command engine with optional React bindings.

What is duck-vim?

duck-vim is a keyboard shortcut engine for the browser. Shortcuts are declared in a registry rather than hardcoded in event handlers. That makes them discoverable through command palettes, customizable per user, and composable into multi-key sequences like g then d.

The core is plain DOM with zero dependencies. React bindings sit on top as an optional layer.


Why duck-vim?

Most keyboard shortcut libraries cover Ctrl+K to a function and stop. duck-vim also handles:

  • Multi-key sequences like Vim's g+d (press g, then d within a timeout).
  • Prefix awareness so the UI can show a "waiting for next key..." state.
  • Cross-platform Mod key that resolves to Cmd on Mac and Ctrl elsewhere.
  • Scoped bindings attached to specific DOM elements, not just document.
  • A key recorder for settings UIs where users define their own shortcuts.
  • Display formatting that renders Mod+Shift+S as Cmd+Shift+S on Mac and Ctrl+Shift+S on Windows.

Architecture

duck-vim is organized into independent modules:

ModulePurposeFramework dependency
platformOS detection, Mod key resolutionNone
parserParse and validate key binding stringsNone
matcherMatch keyboard events against parsed bindingsNone
commandRegistry + KeyHandler for managing shortcutsNone
sequenceMulti-step sequence managerNone
recorderRecord key combinations for settings UIsNone
formatFormat bindings for display (Cmd+S, Ctrl+S)None
reactProvider, hooks, and context for React appsReact

Loading diagram...



Installation


npm install @gentleduck/vim

npm install @gentleduck/vim
// Core (framework-agnostic)
import { Registry, KeyHandler } from '@gentleduck/vim/command'
import { parseKeyBind } from '@gentleduck/vim/parser'
import { formatForDisplay } from '@gentleduck/vim/format'
 
// React bindings
import { KeyProvider, useKeyBind, useKeySequence } from '@gentleduck/vim/react'
// Core (framework-agnostic)
import { Registry, KeyHandler } from '@gentleduck/vim/command'
import { parseKeyBind } from '@gentleduck/vim/parser'
import { formatForDisplay } from '@gentleduck/vim/format'
 
// React bindings
import { KeyProvider, useKeyBind, useKeySequence } from '@gentleduck/vim/react'

Minimal example

import { Registry, KeyHandler } from '@gentleduck/vim/command'
 
const registry = new Registry()
const handler = new KeyHandler(registry)
 
registry.register('ctrl+k', {
  name: 'Open Palette',
  execute: () => document.getElementById('palette')?.focus(),
})
 
handler.attach(document)
import { Registry, KeyHandler } from '@gentleduck/vim/command'
 
const registry = new Registry()
const handler = new KeyHandler(registry)
 
registry.register('ctrl+k', {
  name: 'Open Palette',
  execute: () => document.getElementById('palette')?.focus(),
})
 
handler.attach(document)