Lesson 3: Understanding Key Binding Strings
Learn how duck-vim parses, normalizes, and validates key binding strings.
Lesson 3 of 8 — 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
gctrl+shift+s
Mod+K
alt+enter
space
gduck-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')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
}{
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'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)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, ... }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, ... }// 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 ctrlparseKeyBind('Mod+S', 'mac') // Always meta
parseKeyBind('Mod+S', 'windows') // Always ctrlBest practice: Always use Mod instead of Ctrl or Meta for shortcuts that should work on all platforms. Reserve explicit Ctrl or Meta only when you specifically need that modifier.
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: [] }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: [] }The last example is important: Alt+letter combinations produce special characters on macOS (e.g., Alt+N types a tilde), so the shortcut might not fire. The validator warns you about this.
Use 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'
})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
Multiple non-modifier keys: '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.
Forgetting 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.
Using 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.