Define v1 modal keymap grammar

This commit is contained in:
slhx agent
2026-06-21 10:51:28 +02:00
parent f5c81155ea
commit b076d1a35a
3 changed files with 409 additions and 3 deletions
+402
View File
@@ -0,0 +1,402 @@
# KEYMAP.md
This is the durable v1 keymap and operation grammar for `mim`. It is design
authority, not a claim that every operation is implemented today. Implementation
slices should cite the relevant requirements and keep the grammar source-patched,
replayable, and terminal-trace-backed.
## Product stance
`mim` is mobile-first, not phone-only. The same command grammar must work when
SSHing from an iPhone software keyboard, a QWERTZ thumb keyboard, or an attached
physical keyboard. Physical keyboards may add faster direct keys, but they must
not become the only way to perform core editing, navigation, tool, or recovery
actions.
The keymap is optimized for:
- narrow terminal viewports;
- mnemonic commands that can be discovered from the screen;
- common operations as a mode plus one key, or at most a two-key sequence;
- an orthogonal grammar where verbs, objects, modes, panels, and tool operations
compose instead of becoming one-off bindings;
- avoiding required Esc/Ctrl/Alt/function-key chords for core actions;
- replayable terminal events separated from layout profiles and editor intents;
- code editing workflows, including diagnostics, LSP, lint, format, build/test,
and local trusted tools.
## Input layers
The input path has three distinct layers:
1. **Terminal event**: bytes or key events observed from the terminal.
2. **Layout profile**: source-patched keyboard tables backed by recorded traces
such as iOS QWERTZ and physical keyboard traces.
3. **Editor intent**: semantic operations such as `delete line`, `select indent`,
`format buffer`, or `next diagnostic`.
No command should inspect layout quirks directly. A new keyboard layout is a new
source-patched profile and trace fixture, not an ad-hoc branch in command code.
## Orthogonality rules
The keymap should feel small because its parts compose:
- modes decide how raw input is interpreted;
- verbs decide the operation: delete, change, yank, select, format, explain;
- objects decide the range: word, line, indent block, delimiter pair, parameter,
diagnostic, function, panel item;
- counts/repetition decide how often a movement or previous operation applies;
- providers decide the source of language/tool data, not the user-facing command;
- panels decide presentation and filtering, not the operation semantics.
A new operation is suspect if it needs a bespoke binding that cannot reuse this
shape. Prefer adding one verb, one object, or one provider rule over adding many
special-case bindings. Reuse is not allowed to hide ambiguity: if the same key can
mean two things, the current mode or visible rail label must make the meaning
obvious.
Examples:
- `d` + object deletes; `c` + object changes; `y` + object copies; `s` + object
selects. The object grammar is shared.
- `Space l` operates on language features; `Space t` operates on jobs/tools;
both can target buffer, selection, project, or provider without inventing a
separate mode.
- Formatting a selection, buffer, or project should be one format command plus a
scope/provider choice, not unrelated shortcuts.
## Counts and repetition
Counts and repetition are orthogonal modifiers, not separate commands for every
operation.
- A numeric prefix in Normal, Select, or Panel mode applies to the next movement,
object, or panel navigation where that operation is repeatable.
- Physical keyboards can type digits directly: `3 j`, `2 w`, `4 d w`.
- Mobile users can open the repeat rail with `Space r`, enter a small count in a
prompt, then choose the movement/object/operation. This avoids requiring a
comfortable number row for common editing.
- `.` repeats the last mutating edit when the key is available; `Space r .` is
the mobile canonical repeat-last path.
- Counts do not apply to unsafe broad tool operations such as format project,
fix-all, build, test, or external terminal commands unless a visible prompt
confirms the affected scope.
- A count is shown in the status/rail while pending and is cleared after one
operation, cancel, or mode switch.
## Modes
### Normal mode
Default navigation and command mode. Text input does not happen here except via
explicit symbol/punctuation insertion commands. Normal mode owns movement,
selection entry, leader rails, tool commands, save/quit, and recoverability.
### Insert mode
Text-entry mode for ordinary typing. Insert mode preserves terminal text input
and exposes a visible return path to Normal through a non-modifier command rail.
In Insert mode, `Space` enters a pending-space state instead of immediately
committing ambiguity. If the next key is ordinary text, `mim` commits the literal
space and the next character. If the user pauses after `Space`, `mim` opens the
Insert rail. The canonical mobile path back to Normal is `Space` pause, then
`n` for "normal". `Space` pause, then `Space` commits a literal space from the
rail. Physical keyboards may use conventional direct keys such as Escape as
aliases, but those aliases are optional accelerators, not required controls.
### Select mode
Selection-building mode. Movement extends the active selection by default.
Selection commands are semantic: select word, line, current indent block,
matching delimiter pair, enclosing text object, parameter, diagnostic range, and
visible panel item. Select mode can apply operations such as delete, replace,
copy, format, code action, or explain to the selection.
### Panel mode
Transient-panel mode for file picker, project search, diagnostics, git, LSP,
build/test output, terminal escape hatch, and Pi/tool context. Panels share one
navigation grammar: move, open, preview, filter, act, close, and return.
### Prompt mode
Short text prompt mode for current-file search, project search, rename, command
filtering, and tool arguments. Prompt mode is not an ex-command language; it is a
small typed argument surface entered from a visible command.
## Leader keys and rails
`Space` is the primary leader. Pressing and pausing on Space opens the command
rail. The rail shows mnemonic groups and available next keys. A command either
executes after one key or enters a second-level group; common paths should not go
deeper than two keys after the mode/leader.
`m` is the match/object prefix in Normal and Select modes. It is deliberately not
Vim `%`: `%` remains a physical-keyboard alias where available, while `m` is the
mobile mnemonic path.
`g` is the go/navigation prefix. It is used for jumps whose target is elsewhere:
definition, references, next/previous diagnostic, file symbol, project symbol,
and matching syntax target when a direct object prefix would be ambiguous.
`Space Space` reopens the last command rail/panel context. It is the universal
"where was I?" recovery path.
## Mnemonic command grammar
The grammar is verb-first for editing and noun/group-first for panels:
- `i`: insert;
- `a`: append/after;
- `o`: open/new line or open item depending on context;
- `s`: select/search depending on mode/rail group;
- `d`: delete;
- `c`: change;
- `r`: replace;
- `y`: yank/copy;
- `p`: paste/put;
- `u`: undo;
- `R`: redo where uppercase is available, otherwise `Space u` then `r`;
- `f`: find/format/file depending on rail group label;
- `l`: lint/LSP language tools depending on rail group label;
- `t`: tools/tasks/tests;
- `b`: buffer/build depending on rail group label;
- `q`: quit/close;
- `w`: write/save, shown as "write/save" in rails for mnemonic continuity.
Letter reuse is allowed only when the current mode or visible rail label makes
the meaning obvious. Invisible overloading is not allowed.
## Core Normal bindings
These are the mobile canonical paths. Physical keyboards may add direct aliases
such as arrow keys, Home/End, PageUp/PageDown, Escape, or `%`.
| Keys | Intent |
| --- | --- |
| `i` | enter Insert before cursor |
| `a` | enter Insert after cursor |
| `o` | open line below and enter Insert |
| `O` or `Space o` `a` | open line above and enter Insert |
| `h` `j` `k` `l` | left/down/up/right where comfortable; arrows are aliases |
| `w` `b` `e` | word forward/back/end |
| `0` `$` | line start/end when available; `Space g` `l` opens line targets |
| `u` | undo |
| `Space u` `r` | redo |
| `Space w` | write/save current buffer |
| `Space q` | close/quit current surface, with dirty-buffer protection |
| `Space ?` | show contextual help/command rail |
| Insert `Space` pause, `n` | return from Insert to Normal without Esc/Ctrl/Alt |
## Editing operations
| Keys | Intent |
| --- | --- |
| `d` `d` | delete current line |
| `d` then motion/object | delete range |
| `c` `c` | change current line |
| `c` then motion/object | change range and enter Insert |
| `r` key/symbol | replace character or active selection |
| `s` | enter Select mode |
| `y` then motion/object | yank/copy range |
| `p` | paste/put after cursor or replace active selection |
| `Space i` | symbol insertion rail for coding punctuation |
| `Space i` `p` | insert paired delimiter, then place cursor inside |
| `Space i` `q` | insert paired quotes, then place cursor inside |
Delete/change/yank all accept the same object grammar. This keeps operations
orthogonal: verbs decide what happens; objects decide what range is affected.
## Object and match grammar
Objects are shared by Normal and Select mode.
| Keys | Object or jump |
| --- | --- |
| `m` `m` | jump to matching delimiter or quote |
| `m` `s` | select inside matching delimiter/string pair |
| `m` `a` | select around matching delimiter/string pair |
| `m` `(` | jump/select `()` pair, depending on mode |
| `m` `{` | jump/select `{}` pair, depending on mode |
| `m` `[` | jump/select `[]` pair, depending on mode |
| `m` `"` / `m` `'` / `m` `` ` `` | jump/select quoted string where parseable |
| `s` `w` | select word |
| `s` `l` | select line |
| `s` `i` | select current indent block |
| `s` `p` | select current parameter/argument |
| `s` `f` | select current function or syntactic enclosing form where available |
| `s` `d` | select current diagnostic range |
Physical keyboards may expose `%` as an alias for `m m`. The mobile canonical
path stays `m m` because `%` is awkward on software keyboards and QWERTZ.
## Search and navigation
| Keys | Intent |
| --- | --- |
| `/` or `Space s` `f` | search in current file |
| `n` / `N` | next/previous search result; rail aliases exist if uppercase is awkward |
| `Space f` | file finder |
| `Space s` `p` | project text search |
| `Space s` `s` | symbol search in current file |
| `Space g` `d` | go to definition |
| `Space g` `r` | references |
| `Space g` `i` | implementation |
| `Space g` `t` | type definition |
| `Space g` `p` | previous cursor location |
| `Space g` `n` | next cursor location |
| `Space g` `e` | next diagnostic/error |
| `Space g` `E` or `Space g` `p` from diagnostics rail | previous diagnostic/error |
| `Space g` `a` | next parameter/argument in call |
| `Space g` `A` | previous parameter/argument in call |
Search results and navigation targets use transient panels on narrow viewports.
Opening a result returns to the editor without leaving behind permanent splits.
## Panels
`Space` plus a group opens a panel or rail:
| Keys | Surface |
| --- | --- |
| `Space f` | files: file tree/finder/recent |
| `Space s` | search: file/project/symbol |
| `Space d` | diagnostics |
| `Space g` | go/navigation |
| `Space l` | language/LSP tools |
| `Space t` | tasks: build/test/check/lint jobs |
| `Space b` | buffers/build output depending on rail label |
| `Space v` | version control/git workbench |
| `Space !` | terminal escape hatch/job command |
| `Space p` | Pi/local trusted tool context |
Panel controls are consistent:
| Key | Intent |
| --- | --- |
| `j`/`k` or arrows | move item |
| `o` or Enter | open/accept item |
| `p` | preview item |
| `/` | filter/search within panel |
| `a` | act on item; opens contextual action rail |
| `q` | close panel |
| `Space Space` | restore last panel/rail |
## Coding tools and language operations
Tool operations are first-class commands, not hidden side effects.
| Keys | Intent |
| --- | --- |
| `Space l` `h` | hover: compact card first, expandable panel for full text |
| `Space l` `s` | signature help: active parameter first, expandable panel for overloads/docs |
| `Space l` `a` | code actions for cursor/selection/diagnostic |
| `Space l` `r` | rename |
| `Space l` `o` | organize imports |
| `Space l` `f` | format current buffer |
| `Space l` `F` or `Space l` `w` | toggle/show format-on-save policy for this source build |
| `Space l` `d` | show provider details for symbol/diagnostic under cursor |
| `Space t` `l` | lint current file |
| `Space t` `L` | lint project/workspace |
| `Space t` `b` | build |
| `Space t` `t` | test |
| `Space t` `c` | check/typecheck |
| `Space d` | diagnostics panel |
| `Space d` `n` | next diagnostic |
| `Space d` `p` | previous diagnostic |
| `Space d` `s` | filter diagnostics by source/provider |
Format-on-save is a policy command, not a runtime config file. Defaults live in
source profiles. The rail must show whether save will format, which formatter or
LSP provider will run, and how to perform a one-shot save without formatting when
needed. If no formatter/linter/server exists, `mim` reports a clear missing-tool
or unsupported-capability diagnostic instead of installing anything.
### Hover, signature help, and long content
Hover responses are often too large for an iPhone terminal. The default answer is
not to dump all text inline.
`Space l h` shows a compact hover card first:
- symbol/type/signature headline;
- shortest useful documentation excerpt that fits the viewport;
- provider/source label when more than one provider could answer;
- `more`, `open`, `search`, `copy`, and `pin` actions in the rail.
Expanding hover opens a transient scrollable panel. The panel supports search,
line wrapping by terminal cell width, source/provider switching, copy/yank, and
closing back to the original cursor location. Long hovers never permanently steal
editor space, and they should not make diagnostics or signature context
unreachable.
Signature help follows the same rule: show the active overload and parameter
first; move between parameters with the parameter navigation grammar; expand only
when the user asks to read all overloads or full docs.
### Multiple providers
Several LSPs, linters, formatters, or build tools may apply to one buffer.
`mim` must keep provider identity visible when ambiguity matters:
- diagnostics carry source/provider and severity;
- hover and signature help show provider when multiple providers answer;
- formatting chooses a source-profile default and offers provider selection when
multiple formatters are available;
- code actions show provider/source in the action list;
- lint/build/test jobs show command/provider, status, exit code, and captured
diagnostics;
- provider conflicts are surfaced in the language rail instead of silently
choosing surprising behavior.
Safe default: use the source-profile-preferred provider when one exists; otherwise
ask through a transient provider picker for mutating operations such as format,
organize imports, rename, or fix-all. Non-mutating operations may merge results
while preserving source labels.
## Discoverability and feedback
- Pausing after `Space`, `m`, `g`, `d`, `c`, `y`, or `s` shows the valid next
keys for the current mode and selection.
- Rails use verbs and nouns, not internal names: "format buffer", "lint file",
"select indent", "matching brace".
- Dangerous or broad operations show the affected scope before execution.
- Dirty-buffer, missing-tool, provider-conflict, and unsupported-profile states
appear as diagnostics/panel messages, not silent failures.
- Help is contextual: `Space ?` in Insert, Select, Normal, and Panel mode shows
different high-value commands.
## Implementation gap as of this design
Backed by current code/tests:
- source-patched symbol insertion and keyboard-layout acceptance fixtures;
- leader rail seed behavior and command-intent separation;
- transient panel/rendering foundations;
- protocol/session/job/LSP/diagnostic skeletons;
- basic trace commands for open, insert, save, panels, diagnostics, repo files,
and local context.
Design-only and needing future implementation slices:
- explicit modal state machine for Normal/Insert/Select/Panel/Prompt;
- orthogonal command parser for shared verb + object + scope/provider grammar;
- object grammar shared by delete/change/yank/select;
- matching delimiter and quote object behavior;
- select current indent block and syntactic enclosing form;
- current-file search prompt and result navigation;
- parameter navigation in function calls;
- compact-first hover/signature cards with expandable scroll/search panels;
- full provider arbitration for multiple LSP/tool sources;
- lint/format/build/test command execution through the job surface;
- source-profile format-on-save policy and one-shot save-without-format;
- contextual rails for all prefixes listed in this document;
- Insert pending-space rail, including `Space` pause then `n` to Normal;
- counts/repetition grammar with direct digit and mobile repeat-rail paths;
- physical-keyboard trace fixtures and alias table.
Future slices should implement one vertical behavior at a time with replay or
headless tests: for example, `m m` delimiter jump, `s i` indent selection,
current-file search, or `Space l f` format buffer with provider diagnostics.
+2 -2
View File
@@ -2,7 +2,7 @@
## Product thesis
`mim` is a mobile-first terminal code editor for SSH sessions from an iPhone. It runs as one server-side binary and is designed around narrow viewports, QWERTZ/thumb input, source patching, and real coding workflows rather than desktop Vim compatibility.
`mim` is a mobile-first terminal code editor for SSH sessions from an iPhone. It runs as one server-side binary and is designed around narrow viewports, QWERTZ/thumb input, attachable physical keyboards, source patching, and real coding workflows rather than desktop Vim compatibility.
## User job
@@ -14,7 +14,7 @@ The user opens Vim or Neovim over SSH and tolerates bindings, ex commands, split
## Mechanism
`mim` is protocol-first: the editor core owns buffers, selections, panels, jobs, diagnostics, and session state behind a local socket, while the terminal UI is the first thin client over that protocol. The UI provides a thumb-native leader-key command grammar, transient panels instead of desktop splits, UTF-8-aware editing, Tree-sitter highlighting, built-in LSP, git, file search/tree, project text search, build output, terminal, and optional Pi assistant integration. The socket path is exported in the session environment so trusted local tools like `pi` can inspect editor state or send explicit commands through the same small protocol. Defaults live in source and are changed by patching and rebuilding, not runtime config files.
`mim` is protocol-first: the editor core owns buffers, selections, panels, jobs, diagnostics, and session state behind a local socket, while the terminal UI is the first thin client over that protocol. The UI provides a thumb-native leader-key command grammar that remains efficient with an attached physical keyboard, transient panels instead of desktop splits, UTF-8-aware editing, Tree-sitter highlighting, built-in LSP, git, file search/tree, project text search, build output, terminal, and optional Pi assistant integration. The socket path is exported in the session environment so trusted local tools like `pi` can inspect editor state or send explicit commands through the same small protocol. Defaults live in source and are changed by patching and rebuilding, not runtime config files.
## Feature-complete boundary
+5 -1
View File
@@ -16,6 +16,8 @@ Rows are redgate TSV requirements: `ring<TAB>id<TAB>summary [tag]`.
1 003 Frequent coding punctuation SHALL have editor-native insertion paths for mobile keyboards. [mobile]
1 004 Input handling SHALL separate terminal key events, keyboard layout profiles, and editor command intents. [mobile]
2 005 Keyboard layout profiles SHALL be source-patched tables backed by recorded terminal traces, starting with iOS QWERTZ. [mobile]
1 006 Physical keyboard input SHALL be a first-class terminal path with discoverable shortcuts and no loss of mobile no-required-modifier command access. [mobile]
1 007 The v1 keymap SHALL keep modes, verbs, objects, counts/repetition, panels, and tool operations orthogonal so operations compose instead of multiplying bindings. [mobile]
## ui
@@ -27,8 +29,10 @@ Rows are redgate TSV requirements: `ring<TAB>id<TAB>summary [tag]`.
0 001 Buffer and rendering code SHALL preserve UTF-8 boundaries and terminal cell width semantics. [core]
1 002 Tree-sitter syntax highlighting SHALL be built in with source-patched language tables. [syntax]
1 003 LSP support SHALL be built in and spawn existing language servers without installing them. [lsp]
1 004 LSP hover and signature help SHALL use viewport-aware presentation that remains usable on narrow terminals. [lsp]
1 004 LSP hover and signature help SHALL use compact-first viewport-aware presentation with source labels, scrolling/search, and explicit expansion for long content. [lsp]
1 005 When syntax or LSP data is available, `mim` SHALL provide mobile-friendly navigation between parameters in function calls. [lsp]
1 006 Lint, format, format-on-save, organize-imports, code-action, build, test, and check workflows SHALL be first-class editor commands rather than hidden side effects. [tools]
1 007 When multiple LSPs or tools answer the same request, `mim` SHALL preserve provider/source identity and require explicit selection for ambiguous mutating operations. [tools]
## repo