Lesson 3: Understanding Key Binding Strings
Learn how duck-vim parses, normalizes, and validates key binding strings.
What is a key binding string?
A key binding string is a human-readable representation of a key combination:
ctrl+shift+s
Mod+K
alt+enter
space
g
duck-vim parses these strings into structured objects that can be matched against keyboard events. Understanding the parsing rules helps you write correct bindings and debug issues.
Parsing a binding
import { parseKeyBind } from '@gentleduck/vim/parser'
const result = parseKeyBind('ctrl+shift+s')
The result is a ParsedKeyBind:
{
key: 's', // The non-modifier key
ctrl: true,
shift: true,
alt: false,
meta: false,
modifiers: ['ctrl', 'shift'] // Sorted alphabetically
}
Each binding has exactly one non-modifier key and zero or more modifiers.
Normalization rules
duck-vim normalizes all bindings to a canonical form:
- Everything is lowercased.
- Modifiers are sorted alphabetically: Alt, Ctrl, Meta, Shift.
- Aliases are resolved (see below).
This means these are all equivalent:
normalizeKeyBind('Ctrl+Shift+S') // 'ctrl+shift+s'
normalizeKeyBind('shift+ctrl+s') // 'ctrl+shift+s'
normalizeKeyBind('SHIFT+CTRL+S') // 'ctrl+shift+s'
Use normalizeKeyBind when comparing bindings:
import { normalizeKeyBind } from '@gentleduck/vim/parser'
normalizeKeyBind(bindingA) === normalizeKeyBind(bindingB)
Key aliases
Several keys have multiple common names. duck-vim accepts all of them:
| You can write | Resolves to |
|---|---|
command, cmd | meta |
option, opt | alt |
control | ctrl |
escape | esc |
(space character) | space |
Example:
parseKeyBind('command+s') // { key: 's', meta: true, ... }
parseKeyBind('cmd+s') // { key: 's', meta: true, ... }
parseKeyBind('opt+k') // { key: 'k', alt: true, ... }
The Mod key
Mod is a virtual key that resolves based on the platform:
- macOS: Mod becomes Meta (Command key)
- Windows/Linux: Mod becomes Ctrl
// On macOS:
parseKeyBind('Mod+S') // { key: 's', meta: true, ctrl: false, ... }
// On Windows:
parseKeyBind('Mod+S') // { key: 's', meta: false, ctrl: true, ... }
You can force a specific platform:
parseKeyBind('Mod+S', 'mac') // Always meta
parseKeyBind('Mod+S', 'windows') // Always ctrl
Validation
Use validateKeyBind to check a binding without throwing:
import { validateKeyBind } from '@gentleduck/vim/parser'
validateKeyBind('ctrl+k')
// { valid: true, warnings: [], errors: [] }
validateKeyBind('')
// { valid: false, warnings: [], errors: ['Key binding string cannot be empty'] }
validateKeyBind('ctrl+k+j')
// { valid: false, warnings: [], errors: ['Multiple non-modifier keys found'] }
validateKeyBind('ctrl+ctrl+k')
// { valid: false, warnings: [], errors: ["Duplicate modifier: 'ctrl'"] }
validateKeyBind('alt+n')
// { valid: true, warnings: ['Alt+letter may not work on macOS...'], errors: [] }
validateKeyBind before storing user-defined bindings. It catches problems that would silently fail at runtime.Converting keyboard events to descriptors
When the KeyHandler receives a keyboard event, it converts it to a descriptor string:
import { keyboardEventToDescriptor } from '@gentleduck/vim/parser'
document.addEventListener('keydown', (e) => {
const desc = keyboardEventToDescriptor(e)
console.log(desc) // e.g. 'ctrl+k', 'shift+enter', 'a'
})
Returns null for pure modifier key presses (pressing just Shift, Ctrl, Alt, or Meta alone).
Common mistakes
'k+j' is NOT a two-key sequence. It's parsed as a single binding with two non-modifier keys, which throws an error. For sequences, use the Registry's 'g+d' format or the SequenceManager.preventDefault: Browser shortcuts like Ctrl+S (save), Ctrl+W (close tab), and Ctrl+N (new window) will still fire their default behavior unless you set preventDefault: true.ctrl on Mac: macOS users expect Cmd+S, not Ctrl+S. Use Mod+S to get the right behavior on both platforms.Exercises
- Parse
'Mod+Shift+Z'for both Mac and Windows. What are the differences? - What does
validateKeyBind('shift+shift+a')return? - Write code that listens for keydown events and logs the descriptor for each.