commands-hotkeys-system
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 add https://github.com/majiayu000/claude-skill-registrygit clone https://github.com/majiayu000/claude-skill-registry.git ~/.claude/skills/commands-hotkeys-systemCopy 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
| File | Purpose | Pattern |
|---|---|---|
src/lib/commands/index.ts | Barrel export | Public API surface |
src/lib/commands/types.ts | Core types | CommandScope, KeyBinding |
src/lib/commands/decorators.ts | Decorator DSL | @command, defineCommand |
src/lib/commands/service.ts | CommandService | Effect.Service + atoms |
src/lib/commands/defaults.ts | Built-in commands | Default bindings |
src/lib/commands/wire.ts | Command→hotkey bridge | Effect-based wiring |
src/lib/commands/persistence.ts | localStorage sync | useKeybindingPersistence |
src/lib/commands/CommandProvider.ts | M-x completions | FlexSearch integration |
src/lib/hotkeys/index.ts | Hotkey system | Public API |
src/lib/hotkeys/types.ts | KeyChord, KeySequence | Primitives |
src/lib/hotkeys/atoms/index.ts | Reactive state | Source + derived atoms |
src/lib/hotkeys/components/WhichKeyPopup.tsx | Prefix hints | which-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:
| Method | Signature | Purpose |
|---|---|---|
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
| Query | Result |
|---|---|
save | Fuzzy match "Save", "Save As", etc. |
scope:grid | Filter to grid-scoped commands |
/delete.*row/ | Regex match |
scope:grid category:edit | Combined 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.Tageffect-atom-integration— Atom.make, derived atomseffect-schema-mastery— Schema.Literal for ScopeId
Used by:
tmnl-testbed-patterns— Keybinding testbedsux-interaction-patterns— Keyboard navigation- Minibuffer system — M-x command palette
Bridges:
- Commands (high-level intent) → Hotkeys (low-level key handling)
Quick Reference
| Task | Pattern | File |
|---|---|---|
| Define global command | defineCommand() | commands/decorators.ts:162 |
| Define entity command | defineEntityCommand() | commands/decorators.ts:216 |
| Execute command | CommandService.execute() | commands/service.ts:149 |
| Open M-x palette | CommandService.executeInteractive() | commands/service.ts:193 |
| Get effective bindings | useAtomValue(effectiveBindingsAtom) | commands/service.ts:35 |
| Override keybinding | CommandService.overrideBinding() | commands/service.ts:211 |
| Wire commands to hotkeys | wireCommandsEffect(registry) | commands/wire.ts:224 |
| Persist overrides | useKeybindingPersistence() | commands/persistence.ts:115 |
| Multi-chord sequence | keys: 'g i' | commands/defaults.ts:211 |
| Show which-key popup | <WhichKeyPopup /> | hotkeys/components/WhichKeyPopup.tsx |
| Process keyboard event | processKeyboardEvent() | hotkeys/atoms/index.ts:373 |
| Set active scope | hotkeyActions.setScope() | hotkeys/atoms/index.ts:327 |
GitHub Repository
Related Skills
content-collections
MetaThis 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.
langchain
MetaLangChain 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.
cloudflare-turnstile
MetaThis 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.
project-structure
MetaThis 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.
