Lesson 1: Introduction
Why keyboard shortcuts matter and how duck-vim approaches the problem.
Lesson 1 of 8 — Why keyboard shortcuts matter and how duck-vim approaches the problem.
Why keyboard shortcuts?
Keyboard shortcuts aren't just for power users. They serve three audiences:
Power users want to stay on the keyboard. Every time they reach for the mouse, they lose flow. Apps like VS Code, Figma, and Notion thrive because they offer comprehensive keyboard interfaces.
Accessibility requires keyboard navigation. Users who can't use a mouse depend on keyboard shortcuts. WCAG 2.1 guideline 2.1 requires all functionality to be operable through a keyboard.
Efficiency is measurable. Studies consistently show that keyboard shortcuts reduce task completion time by 30-50% for repetitive operations.
The problem with naive approaches
Most developers start with something like this:
// Don't do this
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'k') {
openPalette()
}
if (e.ctrlKey && e.key === 's') {
e.preventDefault()
save()
}
// ... 50 more if-statements
})// Don't do this
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'k') {
openPalette()
}
if (e.ctrlKey && e.key === 's') {
e.preventDefault()
save()
}
// ... 50 more if-statements
})This approach breaks down quickly:
- No discoverability. Users can't find what shortcuts exist.
- No customization. Bindings are hardcoded.
- No multi-key sequences. You can't do Vim-style G then D.
- No cleanup. Removing a shortcut means finding and deleting code.
- Cross-platform issues. Ctrl on Windows should be Cmd on Mac.
- Input conflicts. Pressing G fires even when the user is typing in a search box.
How duck-vim solves this
duck-vim uses a registry pattern. Instead of imperative event handlers, you declare bindings:
registry.register('ctrl+k', {
name: 'Open Palette',
execute: () => openPalette(),
})registry.register('ctrl+k', {
name: 'Open Palette',
execute: () => openPalette(),
})The registry is the single source of truth for all shortcuts. This unlocks:
- Discoverability: Query
registry.getAllCommands()to build a command palette. - Customization: Unregister and re-register with a different key.
- Sequences: Register
'g+d'and the system handles prefix detection and timeouts. - Cross-platform: Write
'Mod+S'and it resolves to Cmd+S on Mac, Ctrl+S elsewhere. - Input safety: Set
ignoreInputs: trueto skip bindings when the user is typing.
Architecture at a glance
duck-vim is split into small, focused modules:
@gentleduck/vim
├── platform/ -> OS detection, Mod key resolution
├── parser/ -> Parse "ctrl+shift+s" into structured data
├── matcher/ -> Does this KeyboardEvent match this binding?
├── command/ -> Registry + KeyHandler (the core system)
├── sequence/ -> Multi-step sequence matching
├── recorder/ -> Record key combos for settings UIs
├── format/ -> Format bindings for display ("Cmd+S")
└── react/ -> Provider + hooks@gentleduck/vim
├── platform/ -> OS detection, Mod key resolution
├── parser/ -> Parse "ctrl+shift+s" into structured data
├── matcher/ -> Does this KeyboardEvent match this binding?
├── command/ -> Registry + KeyHandler (the core system)
├── sequence/ -> Multi-step sequence matching
├── recorder/ -> Record key combos for settings UIs
├── format/ -> Format bindings for display ("Cmd+S")
└── react/ -> Provider + hooksThe core modules have zero framework dependencies. React bindings are a separate, optional layer.
What we'll build in this course
By the end of this course, you'll be able to:
- Register and manage keyboard shortcuts in any JavaScript application.
- Build multi-key Vim-style sequences.
- Integrate shortcuts into React with providers and hooks.
- Display shortcuts with platform-aware formatting.
- Build a key recorder for shortcut customization UIs.
- Build a command palette powered by the registry.
- Handle edge cases: scoped bindings, conflict detection, input elements.
Let's start.