Skip to main content

Lesson 2: Your First Shortcut

Create a Registry, register a command, and attach a KeyHandler to start listening for keyboard events.

The two core classes

duck-vim's shortcut system has two main pieces:

  • Registry stores the mapping from key binding strings to commands.
  • KeyHandler listens for keyboard events and looks up matching commands in the registry.

They're separate because you might want to query the registry (for a command palette) without caring about event handling, or you might want multiple handlers attached to different DOM elements sharing the same registry.


Setting up your first shortcut

Create a registry

import { Registry } from '@gentleduck/vim/command'
 
const registry = new Registry()
import { Registry } from '@gentleduck/vim/command'
 
const registry = new Registry()
const registry = new Registry(true)
const registry = new Registry(true)

Define a command

A command is an object with a name and an execute function:

import type { Command } from '@gentleduck/vim/command'
 
const openPalette: Command = {
  name: 'Open Command Palette',
  description: 'Opens the global command search',
  execute: () => {
    console.log('Palette opened!')
  },
}
import type { Command } from '@gentleduck/vim/command'
 
const openPalette: Command = {
  name: 'Open Command Palette',
  description: 'Opens the global command search',
  execute: () => {
    console.log('Palette opened!')
  },
}

The name is used for debugging and for display in command palettes. The description is optional but recommended.

Register the command

const handle = registry.register('ctrl+k', openPalette, {
  preventDefault: true,
})
const handle = registry.register('ctrl+k', openPalette, {
  preventDefault: true,
})

This says: when the user presses Ctrl+K, run openPalette.execute() and call event.preventDefault() to stop the browser's default behavior (Chrome's address bar focus, for example).

The returned handle lets you manage the binding:

handle.setEnabled(false) // Temporarily disable
handle.setEnabled(true)  // Re-enable
handle.unregister()      // Remove permanently
handle.setEnabled(false) // Temporarily disable
handle.setEnabled(true)  // Re-enable
handle.unregister()      // Remove permanently

Create a KeyHandler and attach it

import { KeyHandler } from '@gentleduck/vim/command'
 
const handler = new KeyHandler(registry, 600)
handler.attach(document)
import { KeyHandler } from '@gentleduck/vim/command'
 
const handler = new KeyHandler(registry, 600)
handler.attach(document)

Now the handler is listening for keydown events on document. When the user presses Ctrl+K, the handler builds a key descriptor ('ctrl+k'), looks it up in the registry, and calls the command's execute function.


Complete example

import { Registry, KeyHandler, type Command } from '@gentleduck/vim/command'
 
// 1. Create registry
const registry = new Registry(true) // debug mode
 
// 2. Create handler
const handler = new KeyHandler(registry, 600)
 
// 3. Register shortcuts
registry.register('ctrl+k', {
  name: 'Command Palette',
  execute: () => console.log('Palette!'),
}, { preventDefault: true })
 
registry.register('ctrl+s', {
  name: 'Save',
  execute: () => console.log('Saved!'),
}, { preventDefault: true })
 
registry.register('ctrl+z', {
  name: 'Undo',
  execute: () => console.log('Undo!'),
}, { preventDefault: true })
 
// 4. Start listening
handler.attach(document)
 
// Open your browser console and press Ctrl+K, Ctrl+S, or Ctrl+Z
import { Registry, KeyHandler, type Command } from '@gentleduck/vim/command'
 
// 1. Create registry
const registry = new Registry(true) // debug mode
 
// 2. Create handler
const handler = new KeyHandler(registry, 600)
 
// 3. Register shortcuts
registry.register('ctrl+k', {
  name: 'Command Palette',
  execute: () => console.log('Palette!'),
}, { preventDefault: true })
 
registry.register('ctrl+s', {
  name: 'Save',
  execute: () => console.log('Saved!'),
}, { preventDefault: true })
 
registry.register('ctrl+z', {
  name: 'Undo',
  execute: () => console.log('Undo!'),
}, { preventDefault: true })
 
// 4. Start listening
handler.attach(document)
 
// Open your browser console and press Ctrl+K, Ctrl+S, or Ctrl+Z

Cleaning up

When you're done (e.g., when a component unmounts or a page navigates away):

handler.detach(document) // Stop listening
registry.clear()         // Remove all bindings
handler.detach(document) // Stop listening
registry.clear()         // Remove all bindings

Or remove individual bindings:

const handle = registry.register('ctrl+k', command)
// Later:
handle.unregister()
const handle = registry.register('ctrl+k', command)
// Later:
handle.unregister()

Checking what's registered

registry.hasCommand('ctrl+k') // true
registry.getCommand('ctrl+k') // { name: 'Command Palette', execute: ... }
registry.getAllCommands()      // Map of all bindings
 
for (const [binding, cmd] of registry.getAllCommands()) {
  console.log(`${binding} -> ${cmd.name}`)
}
registry.hasCommand('ctrl+k') // true
registry.getCommand('ctrl+k') // { name: 'Command Palette', execute: ... }
registry.getAllCommands()      // Map of all bindings
 
for (const [binding, cmd] of registry.getAllCommands()) {
  console.log(`${binding} -> ${cmd.name}`)
}

Try it

Create a new file, paste the complete example above.

Open it in a browser.

Open the console.

Press Ctrl+K. You should see "Palette!" in the console.

Try enabling debug mode and watch the detailed logs as you press keys.


Key takeaways

  • Registry stores bindings. KeyHandler listens for events.
  • register() returns a handle for lifecycle management.
  • Always set preventDefault: true for shortcuts that conflict with browser defaults.
  • Use debug mode during development.