Skip to main content

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
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')
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:

  1. Everything is lowercased.
  2. Modifiers are sorted alphabetically: Alt, Ctrl, Meta, Shift.
  3. 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 writeResolves to
command, cmdmeta
option, optalt
controlctrl
escapeesc
(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

// 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 ctrl
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: [] }
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: [] }

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


Exercises

  1. Parse 'Mod+Shift+Z' for both Mac and Windows. What are the differences?
  2. What does validateKeyBind('shift+shift+a') return?
  3. Write code that listens for keydown events and logs the descriptor for each.