Files
Ignacio Perez 455f05cd4f feat: Document keymap flow and update shortcuts
Add a new keymap doc that explains contexts, focus scope, command routing, event flow, and how to add bindings.
Update keyboard shortcuts to match current modal and search input bindings
2025-12-31 07:37:52 -03:00

66 lines
4.8 KiB
Markdown

# Keymap System
This document describes how keyboard input is mapped to commands in TaskWarrior GPUI.
## Components
- `src/keymap/command.rs`: `Command` enum lists every action the UI can handle.
- `src/keymap/context.rs`: `ContextId` defines the active key contexts (Global, Table, TableHeaders, SidebarProjects, SidebarTags, Modal, FilterBar, TextInput).
- `src/keymap/chord.rs`: `Key`, `Mods`, `KeyChord` normalize keys. `KeyChord::from_gpui` builds chords from `gpui::KeyDownEvent`, `KeyChord::parse` parses strings like `Ctrl+F`, and `Display` formats chords as strings.
- `src/keymap/keymap.rs`: `KeymapLayer` stores `ContextId -> (KeyChord -> Command)` bindings. `KeymapStack` resolves by checking the top-most layer first and falling back to `Global` if nothing matches.
- `src/keymap/defaults.rs`: `build_default_keymap` defines all default bindings and is the only layer pushed today.
- `src/keymap/active_context.rs`: `FocusTarget` maps UI focus to `ContextId`.
- `src/keymap/dispatcher.rs`: `CommandDispatcher` trait abstracts command handling.
## Runtime flow
1. The root UI in `src/view/app_layout.rs` registers `on_key_down` on the app container.
2. `App::handle_key_down` in `src/app.rs` receives the event and:
- Converts it to `KeyChord` via `KeyChord::from_gpui`.
- Computes the active `ContextId` via `App::active_context`:
- If the task detail modal is open, the context is `Modal`.
- If focus is on the table and the filter bar is active, the context becomes `TextInput` or `FilterBar` based on `TaskTable::get_active_filter_context`.
- Otherwise it uses `FocusTarget::to_context`.
- Resolves the command with `KeymapStack::resolve`, which checks the latest layer first and falls back to `Global`.
3. If the modal is open, `App::handle_key_down` only allows `CloseModal`, `SaveModal`, `Sync`, and modal scroll commands; all other commands are ignored.
4. Some commands are handled inline in `App::handle_key_down` (focus transitions around the table, filter bar, and search input).
5. Everything else is routed through `CommandDispatcher`:
- `App` implements it in `src/dispatcher.rs` and forwards commands to `TaskTable` or `Sidebar` based on focus.
- `TaskTable` and `Sidebar` implement `CommandDispatcher` to apply selection, filter, and navigation changes.
## Focus, context, and scope
The same keymap can yield different commands depending on the active context. The context is computed from focus and UI state:
- `FocusTarget` (in `src/keymap/active_context.rs`) represents which major area owns focus (table, headers, sidebars).
- `App::active_context` (in `src/app.rs`) converts that focus into a `ContextId` and overrides it when:
- The modal is open (`ContextId::Modal`).
- The filter bar is active (`ContextId::TextInput` or `ContextId::FilterBar` based on `TaskTable::get_active_filter_context`).
- `KeymapStack::resolve` uses that `ContextId` to find a command, so the same key (like `j`) can mean "select next row" in the table context or "scroll down" in the modal context.
## How commands are interpreted by App
- `App::handle_key_down` is the gatekeeper: it resolves the command, enforces modal-only commands when the modal is open, and handles focus-related commands itself.
- Commands that mutate app-level state (focus switching, opening the modal) are handled directly in `App::handle_key_down` or helper methods on `App` (`open_task_detail`, `open_selected_task`).
- All other commands go through `App`'s `CommandDispatcher` implementation in `src/dispatcher.rs`, which decides whether to route the command to `TaskTable`, `Sidebar`, or perform app-level actions (sync, close modal, scroll modal, filter state changes).
## Event flow (short)
1. UI emits `KeyDownEvent` on the root container.
2. `App::handle_key_down` converts it to `KeyChord`, resolves `ContextId`, and maps to a `Command`.
3. `App` either:
- Handles the command directly (focus changes, modal open/close), or
- Dispatches it to `TaskTable`/`Sidebar`, which update their local state and call `cx.notify()`.
## Implementing a new keymap entry
1. Add a new `Command` variant in `src/keymap/command.rs`.
2. Bind a key in `src/keymap/defaults.rs` (or create a new `KeymapLayer` and push it on `KeymapStack` in `App::run`).
3. Handle the command in `App::handle_key_down` (for focus/app-level behavior) or in `src/dispatcher.rs` and the appropriate component dispatcher (`TaskTable` or `Sidebar`).
4. Update `docs/keyboard-shortcuts.md` to reflect the new binding.
## Notes
- The search input (`src/components/input/mod.rs`) handles text editing directly and updates filters on change; it early-returns on `Ctrl+H`/`Ctrl+L` so focus-navigation shortcuts still reach the app-level keymap.
- `src/keymap/mod.rs` still contains legacy `KeyBinding`/`TableAction`/`GlobalAction` types for compatibility; they are not used in the current flow.