# vamp A modal Rust editor with a replaceable frontend and subprocess-isolated Ruby plugins. ## Run Requires current stable Rust and an interactive terminal. Ruby 2.6+ with its standard-library JSON support is required only for configuration/plugins; no Ruby gems are needed. ```sh cargo run -- --no-config notes.txt 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 | Mode | Keys | | --- | --- | | Normal | `i` insert before cursor; `a` insert after cursor; `o`/`O` open line below/above | | Normal | `h`/`j`/`k`/`l` or arrows; `0`/`$` or Home/End | | Normal | `x`/Delete delete; `u` undo; Ctrl-r redo; `:` enter command mode | | Insert | Type, Enter, Tab, Backspace/Delete, arrows, Home/End; Esc returns to normal | | Command | Enter executes; Backspace edits; Esc cancels | Commands: `:w [path]`, `:q`, `:q!`, `:wq [path]`, `:plugins`, and `:plugin-command [arguments]`. `:q` refuses unsaved changes. `:wq` quits only after a successful save. Saves use a same-directory temporary file and rename, preserve existing permissions, and reject target symlinks. UTF-8 text is edited by extended grapheme; tabs render at four-cell stops. LF and trailing-newline structure are preserved; CRLF and CR are normalized to LF. Binary/terminal-control-containing text is rejected. Bracketed paste inserts only in Insert mode and never executes pasted commands in Normal mode. ## Module seams One crate, three modules, no workspace or frontend-specific types in the editing backend: - `editor`: owns buffer, cursor, modes, undo/redo, saving, command ownership, capabilities, and validated edit effects. Its interface accepts `Input`, exposes state, and returns an `Invocation` when a plugin command is selected. - `tui`: translates terminal events to `Input`, renders state, and advertises `text` and `styled_text` capabilities. Another frontend can drive the same backend; the backend does not import terminal libraries. - `ruby`: loads configuration and runs one Ruby subprocess per plugin. It returns feature manifests and edit effects; it does not own the buffer or terminal. - The executable wires the modules together and disables a faulted plugin without changing core editing availability. 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. 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. ## Ruby configuration By default, vamp loads `$XDG_CONFIG_HOME/vamp/config.rb`, or `~/.config/vamp/config.rb` when that environment variable is unset. `--config path` selects a file explicitly; `--no-config` bypasses Ruby entirely. Set `VAMP_RUBY` to select a different Ruby executable. ```ruby Vamp.configure do |config| config.plugin "plugins/greeting.rb" config.bind "g", "greet" 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. ## Ruby plugins ```ruby Vamp.plugin "greeting", requires: ["text"] Vamp.command "greet" do |editor, args| name = args.empty? ? "world" : args editor.insert "Hello, #{name}!\n" editor.message "Greeting inserted by Ruby" end ``` Run `:greet vamp`, or press `g` with the example configuration. `examples/plugins/greeting.rb` also provides `:upper` to uppercase the buffer. Callbacks receive an immutable context snapshot: `editor.text`, `editor.row`, `editor.column`, `editor.mode`, and `editor.path`. Row/column are **one-based grapheme positions**; mode is `NORMAL`; path is a string or `nil`. Arguments are the remaining command text, without surrounding whitespace. `editor.insert(text)`, `editor.replace(text)`, and `editor.message(text)` queue effects. The context remains the pre-command snapshot even after queuing an effect. All effects are validated before the backend changes anything; buffer changes from one successful invocation form one undo action. Exceptions, process exits, timeouts, malformed replies, and invalid effects disable that plugin and discard its effects. Other plugins and built-ins remain available. Plugin `puts`, explicit `STDOUT.write`, and stderr cannot overwrite the TUI; diagnostic output is discarded, while exceptions appear in the status message. This is crash isolation, **not a security sandbox**: Ruby configuration and plugins have your user account's filesystem/process permissions. Load only trusted scripts. On Unix, hosts run in their own process groups and their groups are killed on shutdown. ### Messaging Rust and Ruby exchange UTF-8, newline-delimited JSON objects with `version: 1`. Startup returns `manifest` (name, required capabilities, commands) or `config` (plugin paths and bindings). `invoke` requests carry an ID, command, arguments, and context; `result` replies carry the matching ID and typed effects (`insert`, `replace`, `message`). Errors use `error` frames. Unknown fields, wrong versions, and mismatched replies are rejected. Frames and aggregate effect text are bounded to 4 MiB. JSON uses Ruby's standard library. Protocol Buffers is not installed or required; there is no frontend/network wire protocol yet. The frontend currently calls the Rust library directly. ## Current limits - One text buffer; Normal, Insert, and Command modes. No Visual mode, Vim counts/operators, splits, search, LSP, or graphical frontend. - Undo uses full-buffer snapshots and movement scans the current line. This favors straightforward correctness over large-file performance. - Plugin invocation is synchronous and may pause input for up to 500 ms before a host timeout. Config/plugin startup has a 2-second deadline per host. Buffer context must fit within a 4 MiB JSON request; a too-large request disables that plugin, not editing. - Disabling a plugin removes its commands and bindings until the next launch. There is no hot reload. ## Verify ```sh cargo test cargo clippy --all-targets -- -D warnings cargo fmt --check ruby -c src/ruby_host.rb ``` Regression checks exercise Unicode edits, undo/redo, saving and failed saves, capability/command ownership, effect atomicity, real Ruby execution, protocol errors, crash/timeout recovery, and inherited-pipe cleanup. The executable was also exercised in PTY sessions for editing/saving, plugin crash and hang recovery, bracketed paste, scrolling, resize, and terminal restoration.