feat: make keybindings configurable

This commit is contained in:
Valdemar Lorenzen
2026-10-05 15:57:33 +02:00
parent 368442ba29
commit 3fd89fcbed
7 changed files with 466 additions and 137 deletions
+32 -4
View File
@@ -13,7 +13,7 @@ cargo run -- --config examples/config.rb notes.txt
Without a file, vamp opens an unnamed buffer. A nonexistent file opens an empty buffer. Use `:w path` to save it. `cargo run -- --help` lists the command-line options.
### Editing
### Default editing keys
| Mode | Keys |
| --- | --- |
@@ -37,7 +37,7 @@ Paste is backend input: it is ignored in Normal and Command modes, and inserts l
One crate, four modules, no workspace or frontend-specific types in the editing backend:
- `editor`: owns buffer, cursor, modes, modal paste policy, undo/redo, saving, command ownership, capabilities, and validated edit effects. Its low-level interface returns an `Invocation` when an external command is selected.
- `editor`: owns buffer, cursor, modes, key notation and binding resolution, modal paste policy, undo/redo, saving, command ownership, capabilities, and validated edit effects. Its low-level interface returns an `Invocation` when an external command is selected.
- `session`: `EditorSession` owns the editor and its Ruby hosts together. It loads configuration, registers eligible plugins before installing bindings, executes selected commands, and retires both host and command ownership on invocation or effect-validation failure.
- `tui`: translates terminal events to session input, renders read-only editor state, and advertises `text` and `styled_text` capabilities. It has no plugin lifecycle callback, host storage, or paste acceptance policy.
- `ruby`: evaluates configuration and runs isolated plugin hosts. Protocol, deadlines, and process cleanup stay inside this module.
@@ -47,7 +47,7 @@ Frontends drive `EditorSession::handle` or `execute` and inspect `editor()`; the
The terminal adapter owns raw mode, alternate-screen state, bracketed paste, and its panic hook for one session lifetime. Entry failure, normal return, I/O errors, and unwinding panics restore the caller's hook; active-terminal cleanup runs before caller panic diagnostics. Panics retain their original payload and propagate rather than being swallowed. Terminal state and panic hooks are process-global: run one TUI at a time and do not replace its hook while that session is active.
Built-in Ex commands register as the protected `core` feature in the same command registry used by plugins. External plugins cannot claim core status, replace existing commands, disable core, or bind over essential modal editing keys. `:plugins` lists active features and frontend capabilities.
Built-in Ex commands register as the protected `core` feature in the same command registry used by plugins. External plugins cannot claim core status, replace existing commands, or disable core. User configuration can override any keybinding without replacing the core implementation. `:plugins` lists active features and frontend capabilities.
A feature declares all capabilities it needs. Registration is atomic: if any requirement is unavailable or any command conflicts, none of its commands are installed. For example, `Vamp.plugin "canvas", requires: ["graphics"]` will be refused by this TUI. Capability names are extensible strings, not a terminal-vs-GUI switch. Capability declarations express compatibility; they are not security permissions.
@@ -59,10 +59,38 @@ By default, vamp loads `$XDG_CONFIG_HOME/vamp/config.rb`, or `~/.config/vamp/con
Vamp.configure do |config|
config.plugin "plugins/greeting.rb"
config.bind "g", "greet"
config.bind "<C-r>", "core.redo"
config.bind "<C-c>", "core.normal", mode: "insert"
end
```
Plugin paths resolve relative to the config file, not the shell's working directory. Bindings are single-character Normal-mode keys targeting registered command names. Configuration errors fall back to built-in editing and appear in the status message; one plugin's load failure does not prevent other plugins loading.
Plugin paths resolve relative to the config file, not the shell's working directory. `config.bind key, target, mode: "normal"` overrides one key in `normal`, `insert`, or `command` mode; the mode defaults to `normal`. Targets are the built-in actions below or exact registered Ex/plugin command names (`"w"`, `"greet"`, etc., without `:` or arguments). Ex/plugin commands can only be bound in Normal mode.
Keys are single non-control Unicode characters (case-sensitive), `<Esc>`, `<Enter>`, `<Backspace>`, `<Delete>`, `<Left>`, `<Right>`, `<Up>`, `<Down>`, `<Home>`, `<End>`, `<Tab>`, `<Space>`, or `<C-a>` through `<C-z>`. Ctrl letters are case-insensitive. Terminals share `<C-h>` with Backspace, `<C-i>` with Tab, and `<C-j>`/`<C-m>` with Enter; these names address the same binding. Key sequences and Alt/Super combinations are not supported.
| Available modes | Built-in targets |
| --- | --- |
| Normal | `core.insert`, `core.insert_first_nonblank`, `core.append`, `core.append_line_end`, `core.open_below`, `core.open_above` |
| Normal | `core.join_lines`, `core.undo`, `core.redo`, `core.command` |
| Normal, Insert | `core.left`, `core.down`, `core.up`, `core.right`, `core.line_start`, `core.line_end`, `core.delete` |
| Insert, Command | `core.normal`, `core.backspace` |
| Insert | `core.newline` |
| Command | `core.submit` |
Adding a binding keeps the original key. Pass `nil` to disable a key, including literal typing in Insert/Command mode:
```ruby
Vamp.configure do |config|
config.bind "n", "core.insert"
config.bind "i", nil
config.bind "<Space>", "core.command"
config.bind ":", nil
end
```
The last valid declaration for a key/mode wins. Invalid keys, modes, or targets leave that binding unchanged and appear in the status message; other valid bindings still load. Ruby syntax/type errors disable the entire configuration and fall back to defaults. One plugin's load failure does not prevent other plugins loading. Disabling a plugin removes its bindings, without restoring any default keys they replaced.
Bindings are resolved inside the editor module, not the terminal adapter. Bracketed paste remains literal and bypasses keybindings. If configuration disables your keys for leaving Insert/Command mode or entering commands, restart with `--no-config` to restore defaults.
## Ruby plugins