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
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user