|
| 1 | +# Keyboard Interaction Architecture |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +DotDir keyboard handling is built from three layers: |
| 6 | + |
| 7 | +1. **DOM event routing** |
| 8 | +2. **Focus-aware command routing** |
| 9 | +3. **Command handlers** |
| 10 | + |
| 11 | +The current design keeps one keyboard abstraction in the system: commands. |
| 12 | + |
| 13 | +- **Surface navigation** uses shared generic commands like `cursorLeft` and `selectRight` |
| 14 | +- **Editing-specific behavior** uses dedicated commands like `commandLine.execute` |
| 15 | + |
| 16 | +## Event Flow |
| 17 | + |
| 18 | +The main entry point is [useCommandRouting.ts](/Users/mike/github/dotdirfm/dotdir/packages/ui/lib/features/commands/useCommandRouting.ts). |
| 19 | + |
| 20 | +For most keys: |
| 21 | + |
| 22 | +1. A `keydown` is captured on the app root. |
| 23 | +2. `CommandRegistry.handleKeyboardEvent(...)` resolves the keybinding to a command id. |
| 24 | +3. The most recently registered handler for that command runs. |
| 25 | + |
| 26 | +There is also a window-level fallback for panel mode: |
| 27 | + |
| 28 | +- plain `Tab` |
| 29 | +- function keys like `F5`, `F6`, `F10`, `F11` |
| 30 | + |
| 31 | +That fallback exists because some special keys do not reliably travel through the normal focused-element path in the webview/browser environment. |
| 32 | + |
| 33 | +## Focus Layers |
| 34 | + |
| 35 | +Keyboard routing depends on the current logical focus layer, not just `document.activeElement`. |
| 36 | + |
| 37 | +Focus layers are managed by [focusContext.ts](/Users/mike/github/dotdirfm/dotdir/packages/ui/lib/focusContext.ts) and exposed to the command system through `CommandRegistry.setFocusLayerGetter(...)`. |
| 38 | + |
| 39 | +Current `when` clauses rely on focus predicates such as: |
| 40 | + |
| 41 | +- `focusPanel` |
| 42 | +- `focusMenu` |
| 43 | +- `focusViewer` |
| 44 | +- `focusEditor` |
| 45 | +- `focusModal` |
| 46 | + |
| 47 | +This is what lets the same physical key mean different things in different surfaces. |
| 48 | + |
| 49 | +## Command Families |
| 50 | + |
| 51 | +### Shared Navigation Commands |
| 52 | + |
| 53 | +These commands are intentionally generic: |
| 54 | + |
| 55 | +- `cursorUp` |
| 56 | +- `cursorDown` |
| 57 | +- `cursorLeft` |
| 58 | +- `cursorRight` |
| 59 | +- `cursorHome` |
| 60 | +- `cursorEnd` |
| 61 | +- `cursorPageUp` |
| 62 | +- `cursorPageDown` |
| 63 | +- `selectUp` |
| 64 | +- `selectDown` |
| 65 | +- `selectLeft` |
| 66 | +- `selectRight` |
| 67 | +- `selectHome` |
| 68 | +- `selectEnd` |
| 69 | +- `selectPageUp` |
| 70 | +- `selectPageDown` |
| 71 | +- `accept` |
| 72 | +- `cancel` |
| 73 | + |
| 74 | +They are defined in [commandIds.ts](/Users/mike/github/dotdirfm/dotdir/packages/ui/lib/features/commands/commandIds.ts). |
| 75 | + |
| 76 | +These commands are appropriate when the meaning is “move around the current interactive surface”. |
| 77 | + |
| 78 | +That is why FileList uses the shared `cursor*` and `select*` commands directly now, instead of older aliases like `filelist.cursorLeft`. |
| 79 | + |
| 80 | +### Command Line Editing Commands |
| 81 | + |
| 82 | +The command line keeps separate command ids for behavior that is truly editing-specific, for example: |
| 83 | + |
| 84 | +- `commandLine.cursorWordLeft` |
| 85 | +- `commandLine.selectWordRight` |
| 86 | +- `commandLine.deleteLeft` |
| 87 | +- `commandLine.execute` |
| 88 | + |
| 89 | +These are registered in [CommandLine.tsx](/Users/mike/github/dotdirfm/dotdir/packages/ui/lib/features/command-line/CommandLine/CommandLine.tsx). |
| 90 | + |
| 91 | +These are not just alternate names for `cursorLeft`. |
| 92 | + |
| 93 | +They represent **text editing behavior**, not generic surface navigation. |
| 94 | + |
| 95 | +## Command Ownership |
| 96 | + |
| 97 | +[CommandRegistry](/Users/mike/github/dotdirfm/dotdir/packages/ui/lib/features/commands/commands.ts) now uses a simple **latest registration wins** rule. |
| 98 | + |
| 99 | +That means: |
| 100 | + |
| 101 | +- multiple parts of the app may still reuse the same command id |
| 102 | +- only one handler runs for a given command execution |
| 103 | +- ownership is expressed by registration lifecycle, not by registry predicates |
| 104 | + |
| 105 | +So the important rule is: |
| 106 | + |
| 107 | +- if a surface owns a shared command right now, it registers it |
| 108 | +- if it stops owning that command, it unregisters it |
| 109 | + |
| 110 | +This is what allows shared navigation commands to be reused safely across panels, menus, command palette, autocomplete, and command line. |
| 111 | + |
| 112 | +## Shared Commands vs Dedicated Commands |
| 113 | + |
| 114 | +The command line now reuses shared navigation commands for: |
| 115 | + |
| 116 | +- `cursorLeft` |
| 117 | +- `cursorRight` |
| 118 | +- `cursorHome` |
| 119 | +- `cursorEnd` |
| 120 | +- `selectLeft` |
| 121 | +- `selectRight` |
| 122 | +- `selectHome` |
| 123 | +- `selectEnd` |
| 124 | + |
| 125 | +Those handlers are registered by [CommandLine.tsx](/Users/mike/github/dotdirfm/dotdir/packages/ui/lib/features/command-line/CommandLine/CommandLine.tsx) only while the command line owns editing navigation. |
| 126 | + |
| 127 | +So the split is now: |
| 128 | + |
| 129 | +- **FileList/menu aliases were redundant and removed** |
| 130 | +- **Command line uses shared commands for basic cursor/selection movement** |
| 131 | +- **Command line keeps dedicated commands for word movement, delete, clipboard, and execute/clear** |
| 132 | + |
| 133 | +## Keybinding Layers |
| 134 | + |
| 135 | +Keybindings are registered through [registerKeybindings.ts](/Users/mike/github/dotdirfm/dotdir/packages/ui/lib/features/commands/registerKeybindings.ts). |
| 136 | + |
| 137 | +Resolution order is: |
| 138 | + |
| 139 | +1. default |
| 140 | +2. extension |
| 141 | +3. user |
| 142 | + |
| 143 | +Later layers override earlier ones. |
| 144 | + |
| 145 | +This means a key like `Left` may resolve to: |
| 146 | + |
| 147 | +- `cursorLeft` in panel/menu contexts |
| 148 | +- `cursorLeft` in command-line editing too, with the command line registering that command while it owns editing |
| 149 | + |
| 150 | +The distinction happens through `when` clauses, focus layers, and registration ownership, not through a separate intent layer. |
| 151 | + |
| 152 | +## Current Rules of Thumb |
| 153 | + |
| 154 | +When adding keyboard behavior: |
| 155 | + |
| 156 | +- Use shared `cursor*` / `select*` / `accept` / `cancel` commands for focus-surface navigation. |
| 157 | +- Use dedicated command ids for true domain-specific behavior, especially editing commands. |
| 158 | +- Prefer `when` clauses plus focus layers over duplicated routing layers. |
| 159 | +- Popup surfaces such as menus, command palette, and autocomplete should expose their own focus layer and command handlers instead of inventing a separate keyboard abstraction. |
| 160 | +- If a key is flaky in the webview, fix it in the routing layer rather than adding one-off listeners across feature components. |
| 161 | + |
| 162 | +## Current Direction |
| 163 | + |
| 164 | +The current architecture is: |
| 165 | + |
| 166 | +- shared commands for navigation semantics |
| 167 | +- dedicated commands for buffer-editing semantics |
| 168 | +- lifecycle-based command ownership so surfaces reuse command ids safely |
| 169 | +- no separate interaction-intent layer between keybindings and commands |
0 commit comments