diff --git a/docs/keyboard-shortcuts.md b/docs/keyboard-shortcuts.md index 60b1633..b312e5a 100644 --- a/docs/keyboard-shortcuts.md +++ b/docs/keyboard-shortcuts.md @@ -68,7 +68,7 @@ These shortcuts work when the task table has focus: | Shortcut | Action | |----------|--------| -| `Enter` | Open selected task (not yet wired to a details view) | +| `Enter` | Open selected task details | | `←` | Collapse current project | | `→` | Expand current project | @@ -169,6 +169,7 @@ These shortcuts work when the search input has focus: | Shortcut | Action | |----------|--------| | `Escape` | Blur input (return focus to table) | +| `Enter` | Apply search (filters already update as you type) | | `Ctrl+L` | Focus next filter dropdown | | `Ctrl+H` | Focus previous filter dropdown | | `Ctrl+J` | Focus table headers | @@ -209,7 +210,9 @@ These shortcuts work when viewing task details: | Shortcut | Action | |----------|--------| | `Escape` | Close modal | -| `Ctrl+Enter` | Save changes and close (not yet wired) | +| `j` / `↓` | Scroll down | +| `k` / `↑` | Scroll up | +| `Ctrl+Enter` | Close modal (same as Esc) | ## Search Input Editing diff --git a/docs/keymap.md b/docs/keymap.md new file mode 100644 index 0000000..836f6a1 --- /dev/null +++ b/docs/keymap.md @@ -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.