Back to Skills

commands-hotkeys-system

majiayu000
Updated Today
1 views
58
9
58
View on GitHub
Otherreact

About

This skill provides an Emacs-inspired command and hotkey infrastructure for TMNL, enabling keyboard shortcut management, command palettes, and scope-aware bindings. It offers a decorator DSL and Effect.Service patterns for defining commands with atom-based reactivity. Use it when implementing keybindings, M-x command palettes, which-key popups, or orchestrating Effect-native commands.

Quick Install

Claude Code

Recommended
Plugin CommandRecommended
/plugin add https://github.com/majiayu000/claude-skill-registry
Git CloneAlternative
git clone https://github.com/majiayu000/claude-skill-registry.git ~/.claude/skills/commands-hotkeys-system

Copy and paste this command in Claude Code to install this skill

Documentation

Commands & Hotkeys System for TMNL

Overview

An Emacs-inspired command infrastructure with:

  • Effect-native commands via decorator DSL or functional API
  • Scope-aware keybindings (global, editor, grid, tldraw, modal)
  • Multi-chord sequences (vim-style g i, g g)
  • which-key popups for prefix hints
  • M-x command palette with FlexSearch fuzzy matching
  • Persistent overrides via localStorage
  • Wire system bridging commands to hotkey handlers

Canonical Sources

TMNL Implementations

FilePurposePattern
src/lib/commands/index.tsBarrel exportPublic API surface
src/lib/commands/types.tsCore typesCommandScope, KeyBinding
src/lib/commands/decorators.tsDecorator DSL@command, defineCommand
src/lib/commands/service.tsCommandServiceEffect.Service + atoms
src/lib/commands/defaults.tsBuilt-in commandsDefault bindings
src/lib/commands/wire.tsCommand→hotkey bridgeEffect-based wiring
src/lib/commands/persistence.tslocalStorage syncuseKeybindingPersistence
src/lib/commands/CommandProvider.tsM-x completionsFlexSearch integration
src/lib/hotkeys/index.tsHotkey systemPublic API
src/lib/hotkeys/types.tsKeyChord, KeySequencePrimitives
src/lib/hotkeys/atoms/index.tsReactive stateSource + derived atoms
src/lib/hotkeys/components/WhichKeyPopup.tsxPrefix hintswhich-key UI

Testbeds

  • KeybindingTestbed: /testbed/keybinding — Command execution demo
  • HotkeyTestbed: /testbed/hotkey — Multi-chord sequences

Pattern 1: Command Definition — DECORATOR DSL

When: Defining commands with default keybindings.

Commands use decorator DSL (class-based) OR functional API (preferred).

Functional API (Preferred)

import { defineCommand } from '@/lib/commands'
import { Effect } from 'effect'

export const saveCommand = defineCommand(
  {
    id: 'file.save',
    name: 'Save',
    description: 'Save current file',
    category: 'file',
    scope: 'global',
    keys: 'ctrl+s',  // Default binding
  },
  Effect.gen(function* () {
    yield* Effect.log('Saving...')
    // Your save logic
  })
)

Decorator API (Alternative)

import { command } from '@/lib/commands'
import { Effect } from 'effect'

@command({
  id: 'file.save',
  name: 'Save',
  category: 'file',
  scope: 'global',
  keys: 'ctrl+s',
})
class SaveCommand {
  execute = Effect.gen(function* () {
    yield* Effect.log('Saving...')
  })
}

Entity Commands (Require Context)

For commands that need a target entity (delete row, format selection):

import { defineEntityCommand } from '@/lib/commands'

export const gridDeleteRowCommand = defineEntityCommand<GridRow>(
  {
    id: 'grid.deleteRow',
    name: 'Delete Row',
    category: 'grid',
    scope: 'grid',
    entityType: 'grid.row',
    keys: 'ctrl+backspace',
  },
  (row, ctx) =>
    Effect.gen(function* () {
      yield* Effect.log(`Deleting row ${row.id}`)
      // Delete logic with entity context
    })
)

TMNL Location: src/lib/commands/decorators.ts:162


Pattern 2: CommandService — EFFECT.SERVICE WITH ATOMS

When: Executing commands, managing bindings, or implementing M-x.

CommandService is an Effect.Service (Context.Tag) with atom-backed state.

import { CommandService } from '@/lib/commands'
import { Effect } from 'effect'

// Execute a global command
const executeProgram = Effect.gen(function* () {
  const service = yield* CommandService
  yield* service.execute('file.save')
})

// Execute an entity command
const deleteRowProgram = Effect.gen(function* () {
  const service = yield* CommandService
  yield* service.executeEntity('grid.deleteRow', selectedRow, {
    scope: 'grid',
  })
})

// Run with default layer
Effect.runPromise(
  executeProgram.pipe(Effect.provide(CommandService.Default))
)

M-x Command Palette (executeInteractive)

import { CommandService } from '@/lib/commands'

// Open command palette (minibuffer-based)
const openPaletteProgram = Effect.gen(function* () {
  const service = yield* CommandService
  yield* service.executeInteractive({
    animate: 'slide',  // Optional animation
  })
})

Key Methods:

MethodSignaturePurpose
execute(id: string) => Effect<void, CommandError>Execute global command
executeEntity<T>(id, entity, ctx?) => Effect<void>Execute entity command
executeInteractive(options?) => Effect<void>M-x palette
get(id) => Effect<Option<Command>>Retrieve command
list() => Effect<Command[]>All commands
overrideBinding(registry, id, keys, scope?)Override keybinding
resetBinding(registry, id)Reset to default

TMNL Location: src/lib/commands/service.ts:136


Pattern 3: effectiveBindingsAtom — DERIVED BINDINGS

When: Computing final keybindings with user overrides applied.

The effectiveBindingsAtom is a derived atom that merges defaults + overrides.

import { effectiveBindingsAtom, bindingOverridesAtom } from '@/lib/commands'
import { Atom } from '@effect-atom/atom'

// Derived atom (computed)
export const effectiveBindingsAtom = Atom.make((get) => {
  const overrides = get(bindingOverridesAtom)
  const defaults = getDefaultBindings()

  // Build override lookup
  const overrideMap = new Map<string, KeyBindingOverride>()
  for (const override of overrides) {
    overrideMap.set(override.commandId, override)
  }

  // Apply overrides to defaults
  const effective: KeyBinding[] = []
  for (const binding of defaults) {
    const override = overrideMap.get(binding.commandId)
    if (override) {
      // null keys means unbind
      if (override.keys !== null) {
        effective.push({
          ...binding,
          keys: override.keys,
          scope: override.scope ?? binding.scope,
        })
      }
    } else {
      effective.push(binding)
    }
  }

  return effective
})

Usage in React:

import { useAtomValue } from '@effect-atom/atom-react'
import { effectiveBindingsAtom } from '@/lib/commands'

function KeybindingSettings() {
  const bindings = useAtomValue(effectiveBindingsAtom)

  return (
    <table>
      {bindings.map(b => (
        <tr key={b.commandId}>
          <td>{b.keys}</td>
          <td>{b.commandId}</td>
        </tr>
      ))}
    </table>
  )
}

TMNL Location: src/lib/commands/service.ts:35


Pattern 4: Keybinding Override Persistence — LOCALSTORAGE SYNC

When: Persisting user-customized keybindings across sessions.

The useKeybindingPersistence hook syncs bindingOverridesAtom with localStorage.

import { useKeybindingPersistence } from '@/lib/commands'

function App() {
  const { isLoaded, loadedCount } = useKeybindingPersistence({
    debug: true,  // Log load/save operations
  })

  if (!isLoaded) return <Loading />

  return <YourApp />
}

Manual Operations

import { loadOverrides, saveOverrides, clearPersistedOverrides } from '@/lib/commands'

// Load from localStorage
const overrides = loadOverrides()

// Save to localStorage
saveOverrides([
  { commandId: 'file.save', keys: 'ctrl+alt+s', scope: 'global' },
])

// Clear all
clearPersistedOverrides()

Storage Format:

{
  "version": 1,
  "overrides": [
    {
      "commandId": "file.save",
      "keys": "ctrl+alt+s",
      "scope": "global"
    }
  ]
}

TMNL Location: src/lib/commands/persistence.ts:115


Pattern 5: Wire System — COMMAND→HOTKEY BRIDGE

When: Registering commands with the hotkey system at app initialization.

The wire system bridges commands to hotkeys using Effect for error accumulation.

import { wireCommandsEffect } from '@/lib/commands'
import { RegistryContext } from '@effect-atom/atom-react'
import { useContext, useEffect } from 'react'

function App() {
  const registry = useContext(RegistryContext)

  useEffect(() => {
    Effect.runPromise(
      wireCommandsEffect(registry).pipe(
        Effect.tap((result) =>
          Effect.log(
            `Wired ${result.commandsRegistered} commands, ${result.bindingsRegistered} bindings`
          )
        ),
        Effect.catchAll((error) =>
          Effect.log(`Wire failed: ${JSON.stringify(error)}`)
        )
      )
    )
  }, [registry])

  return <YourApp />
}

Wire Result

interface WireResult {
  readonly commandsRegistered: number
  readonly bindingsRegistered: number
  readonly errors: readonly (CommandRegistrationError | BindingRegistrationError)[]
}

Error Handling:

Wiring uses non-fail-fast error accumulation. Partial wiring succeeds even if some commands/bindings fail.

// Errors are accumulated, not thrown
const result = yield* wireCommandsEffect(registry)

if (result.errors.length > 0) {
  // Some commands failed to register
  console.warn('Wire completed with errors:', result.errors)
}

TMNL Location: src/lib/commands/wire.ts:224


Pattern 6: which-key Integration — PREFIX HINTS

When: Showing available key continuations after a multi-chord prefix.

The which-key popup appears after timeout when a partial sequence is entered.

Hotkey Atoms

import {
  sequenceSourceAtom,
  whichKeyEntriesAtom,
  hotkeyActions,
} from '@/lib/hotkeys'
import { useAtomValue, useRegistry } from '@effect-atom/atom-react'

function HotkeyListener() {
  const registry = useRegistry()
  const currentSequence = useAtomValue(sequenceSourceAtom)
  const whichKeyEntries = useAtomValue(whichKeyEntriesAtom)

  const handleKeyDown = (e: KeyboardEvent) => {
    // Parse chord from event
    const chord: KeyChord = {
      ctrl: e.ctrlKey,
      alt: e.altKey,
      shift: e.shiftKey,
      meta: e.metaKey,
      key: e.key,
    }

    // Append to sequence
    hotkeyActions.appendToSequence(registry, chord)

    // After timeout, show which-key if partial matches exist
    setTimeout(() => {
      const entries = registry.get(whichKeyEntriesAtom)
      if (entries.length > 0) {
        setShowWhichKey(true)
      }
    }, 500)
  }

  return (
    <>
      <YourApp onKeyDown={handleKeyDown} />
      {showWhichKey && (
        <WhichKeyPopup
          entries={whichKeyEntries}
          prefix={currentSequence}
        />
      )}
    </>
  )
}

WhichKeyPopup Component

import { WhichKeyPopup } from '@/lib/hotkeys'

<WhichKeyPopup
  entries={[
    { key: 'i', label: 'Go to Inbox', isPrefix: false },
    { key: 's', label: 'Go to Starred', isPrefix: false },
  ]}
  prefix={[{ ctrl: false, alt: false, shift: false, meta: false, key: 'g' }]}
/>

Display Format:

┌─────────────────────────────┐
│ which-key   [g]             │
│ i    Go to Inbox            │
│ s    Go to Starred          │
│ g    Go to Top              │
└─────────────────────────────┘

TMNL Location: src/lib/hotkeys/components/WhichKeyPopup.tsx


Pattern 7: Multi-Chord Sequences — VIM-STYLE BINDINGS

When: Implementing vim/Emacs-style multi-key sequences (g i, g g, ctrl+k ctrl+s).

Sequence Definition

import { defineCommand, defineBinding } from '@/lib/commands'

// Two-chord sequence (g i)
export const goToInboxCommand = defineCommand(
  {
    id: 'nav.goToInbox',
    name: 'Go to Inbox',
    category: 'navigation',
    scope: 'global',
    keys: 'g i',  // ← Space-separated chords
  },
  Effect.log('Navigating to inbox...')
)

// Alternative: Add binding separately
defineBinding('g s', 'nav.goToStarred', 'global')

Sequence Processing

The processKeyboardEvent pure function handles sequence matching:

import { processKeyboardEvent } from '@/lib/hotkeys'

const { result, newSequence } = processKeyboardEvent(
  chord,           // Current key press
  currentSequence, // Accumulated sequence
  scopedBindings,  // Filtered to active scope
  commands         // Command registry
)

switch (result.type) {
  case 'exact':
    // Full match - execute command
    executeCommand(result.binding.commandId)
    break

  case 'partial':
    // Prefix match - show which-key
    showWhichKey(result.entries)
    break

  case 'none':
    // No match - reset sequence
    resetSequence()
    break
}

TMNL Location: src/lib/hotkeys/atoms/index.ts:373


Pattern 8: Scope-Aware Bindings — CONTEXT SWITCHING

When: Commands should only be active in specific contexts (editor, grid, modal).

Scope Hierarchy

export const ScopeId = Schema.Literal(
  'global',      // Always active
  'editor',      // Text editor context
  'grid',        // AG-Grid context
  'tldraw',      // Canvas context
  'modal',       // Modal overlay
  'palette',     // Command palette
  'minibuffer'   // Minibuffer prompt
)

Scope Inheritance

const DEFAULT_CONFIG: HotkeyConfig = {
  scopeInheritance: {
    editor: 'global',    // editor inherits global
    grid: 'global',      // grid inherits global
    tldraw: 'global',
    modal: 'global',
    palette: 'modal',    // palette inherits modal
    minibuffer: 'global',
  },
}

Scoped Command Example

// Only active in grid scope
export const gridDeleteRowCommand = defineEntityCommand<GridRow>(
  {
    id: 'grid.deleteRow',
    name: 'Delete Row',
    scope: 'grid',  // ← Scope restriction
    keys: 'ctrl+backspace',
  },
  (row) => Effect.log(`Deleting row ${row.id}`)
)

// Active globally
export const commandPaletteCommand = defineCommand(
  {
    id: 'system.commandPalette',
    name: 'Command Palette',
    scope: 'global',  // ← Available everywhere
    keys: 'ctrl+shift+p',
  },
  Effect.log('Opening palette...')
)

Scope Management

import { hotkeyActions } from '@/lib/hotkeys'

// Set active scope
hotkeyActions.setScope(registry, 'grid')

// Push scope (stack-based)
hotkeyActions.pushScope(registry, 'modal')

// Pop scope
hotkeyActions.popScope(registry)

// Current scope chain (derived atom)
const scopeChain = useAtomValue(scopeChainAtom)
// ['grid', 'global'] - grid scope inherits global

TMNL Location: src/lib/hotkeys/types.ts:126, src/lib/hotkeys/atoms/index.ts:62


Pattern 9: CommandProvider — M-X FUZZY SEARCH

When: Implementing M-x style command completion with FlexSearch.

CommandProvider bridges commands to the minibuffer system with fuzzy search.

import { CommandProvider, registerCommandProvider } from '@/lib/commands'

// Register once at app init
registerCommandProvider()

// Provider automatically handles:
// - Fuzzy search via FlexSearch
// - QueryDSL (regex, dorking operators)
// - Command execution via CommandService

Search Features

QueryResult
saveFuzzy match "Save", "Save As", etc.
scope:gridFilter to grid-scoped commands
/delete.*row/Regex match
scope:grid category:editCombined filters

Provider Interface

export const CommandProvider: CompletionProvider<string> = {
  id: COMMAND_PROVIDER_ID,
  label: "Commands",
  icon: Terminal,
  placeholder: "M-x ",

  complete: (query: string) => Effect<Completion[]>,
  onSelect: (item: Completion) => Effect<void>,
  transformInput: (input: string) => string,
}

TMNL Location: src/lib/commands/CommandProvider.ts:114


Decision Tree: Command vs Hotkey

Need to define an action?
│
├─ Should it appear in M-x palette?
│  YES → Define as Command (commands/)
│     └─ Use defineCommand() or @command
│
└─ Is it only triggered by keybinding?
   NO → Define as Command anyway (discoverability)
   YES → Use hotkey system directly (hotkeys/)
       └─ hotkeyActions.addBinding()

Anti-Patterns

Don't: Register commands without wiring

// BANNED - commands exist but aren't executable
defineCommand({ id: 'my.command', ... }, handler)

// App renders - commands not wired to hotkeys
// Pressing keybinding does nothing!

// CORRECT - wire at app init
useEffect(() => {
  Effect.runPromise(wireCommandsEffect(registry))
}, [])

Don't: Override bindings directly in defaults

// BANNED - modifies shared defaults
const defaults = getDefaultBindings()
defaults.push({ keys: 'ctrl+s', commandId: 'my.save' })

// CORRECT - use bindingOverridesAtom
registry.set(bindingOverridesAtom, [
  { commandId: 'file.save', keys: 'ctrl+alt+s' }
])

Don't: Execute commands without Effect runtime

// BANNED - CommandService.execute returns Effect
const service = CommandService.of(...)
service.execute('file.save')  // Does nothing!

// CORRECT - run the Effect
Effect.runPromise(
  service.execute('file.save')
    .pipe(Effect.provide(CommandService.Default))
)

Don't: Use raw KeyboardEvent for sequences

// BANNED - loses sequence context
document.addEventListener('keydown', (e) => {
  if (e.key === 'g') {
    // How do you detect 'g i' sequence?
  }
})

// CORRECT - use hotkeyActions + processKeyboardEvent
hotkeyActions.appendToSequence(registry, chord)
const { result } = processKeyboardEvent(...)

Integration Points

Depends on:

  • effect-patterns — Effect.Service, Context.Tag
  • effect-atom-integration — Atom.make, derived atoms
  • effect-schema-mastery — Schema.Literal for ScopeId

Used by:

  • tmnl-testbed-patterns — Keybinding testbeds
  • ux-interaction-patterns — Keyboard navigation
  • Minibuffer system — M-x command palette

Bridges:

  • Commands (high-level intent) → Hotkeys (low-level key handling)

Quick Reference

TaskPatternFile
Define global commanddefineCommand()commands/decorators.ts:162
Define entity commanddefineEntityCommand()commands/decorators.ts:216
Execute commandCommandService.execute()commands/service.ts:149
Open M-x paletteCommandService.executeInteractive()commands/service.ts:193
Get effective bindingsuseAtomValue(effectiveBindingsAtom)commands/service.ts:35
Override keybindingCommandService.overrideBinding()commands/service.ts:211
Wire commands to hotkeyswireCommandsEffect(registry)commands/wire.ts:224
Persist overridesuseKeybindingPersistence()commands/persistence.ts:115
Multi-chord sequencekeys: 'g i'commands/defaults.ts:211
Show which-key popup<WhichKeyPopup />hotkeys/components/WhichKeyPopup.tsx
Process keyboard eventprocessKeyboardEvent()hotkeys/atoms/index.ts:373
Set active scopehotkeyActions.setScope()hotkeys/atoms/index.ts:327

GitHub Repository

majiayu000/claude-skill-registry
Path: skills/commands-hotkeys-system

Related Skills

content-collections

Meta

This skill provides a production-tested setup for Content Collections, a TypeScript-first tool that transforms Markdown/MDX files into type-safe data collections with Zod validation. Use it when building blogs, documentation sites, or content-heavy Vite + React applications to ensure type safety and automatic content validation. It covers everything from Vite plugin configuration and MDX compilation to deployment optimization and schema validation.

View skill

langchain

Meta

LangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.

View skill

cloudflare-turnstile

Meta

This skill provides comprehensive guidance for implementing Cloudflare Turnstile as a CAPTCHA-alternative bot protection system. It covers integration for forms, login pages, API endpoints, and frameworks like React/Next.js/Hono, while handling invisible challenges that maintain user experience. Use it when migrating from reCAPTCHA, debugging error codes, or implementing token validation and E2E tests.

View skill

project-structure

Meta

This skill provides comprehensive project structure guidelines and best practices for organizing codebases across various project types. It offers standardized directory patterns for monorepos, web frameworks, backend services, and libraries to ensure scalable, maintainable architecture. Use it when designing new project structures, organizing monorepo workspaces, or establishing code organization conventions for teams.

View skill