use std::{env, path::PathBuf}; use vamp::{session::EditorSession, tui}; const HELP: &str = "vamp — modal editor with isolated Ruby plugins Usage: vamp [--config PATH | --no-config] [FILE] Default keys (configurable with config.bind): All modes: Ctrl+p command palette; type to filter, Ctrl+j/k or arrows select, Enter accept, Esc cancel Normal/Visual: Space is leader; Space Space opens palette; Esc cancels prefix Normal leader: Space w saves; Space b n/p/l/d next/previous/list/close buffer Files: Space o floating tree INSERT; Space e docked tree NORMAL (right by default) tree NORMAL: hjkl navigate; INSERT: type to filter, Ctrl+hjkl navigate; Enter toggles/opens i filters; Esc retains filter and returns NORMAL, then editor; Ctrl+u clears in either mode Ctrl+t view, Ctrl+w close, Ctrl+r refresh Normal: i/a insert, I/A insert at first nonblank/end, o/O new line h/j/k/l move, 0/$ line ends, x delete, J join lines w/b/e word next/previous/end, W/B/E whitespace-delimited WORDs Normal/Visual: f/F seek next/previous on current line; 2fx seeks second x gf follows Markdown file links/reference links and heading fragments (no LSP) Normal/Visual: gt first line, gb last line, at first nonblank; Esc cancels g Ctrl+o older jump location; Ctrl+i/Tab newer (counts supported) _ splits above/below, | left/right; Option/Alt+h/j/k/l focuses neighboring panes Ctrl+Option/Alt+h/l previous/next tab; :tab.new opens current buffer in another tab Normal/Visual: prefix motions with counts (5j, 2w, 3n); Esc cancels count u undo, U redo, : command, / search, n/N next/previous match dd deletes lines; cc changes lines and enters Insert; counts: 3dd, 2cc; Esc cancels prefix Normal/Visual: . repeats last native edit at current targets (one undo group) v characterwise select, V linewise select, p/P paste fragments, Ctrl+k/Ctrl+e clone above/below y/d/c/p/P use the system clipboard by default; config.clipboard_mode = \"separate\" isolates it Esc keeps only the primary caret Visual: motions extend selections, y copy, d/x/Delete cut, c change I/A insert at each selected line's first nonblank/end Ctrl+l split rows, Ctrl+n add occurrence, Ctrl+s skip, Ctrl+a all v/V switches selection shape; same selector or Esc cancels retaining carets Insert: Ctrl+n requests LSP completion at primary caret, then cycles next; Ctrl+j/k next/previous Candidates replace text live; Original text restores it; other keys close and act normally Snippets expand fully: type replaces argument, Tab/Shift+Tab next/previous, final $0 ends Outside snippets Enter inserts newline, Tab inserts tab; Esc keeps text and returns to Normal Search: Enter finds next literal match, Esc cancels (case-sensitive, wraps) Commands: :save [PATH], :quit, :force_quit, :save_and_quit [PATH], :plugins, :PLUGIN [ARGS] Aliases: w=save, q=quit, q!=force_quit, wq=save_and_quit Buffers: :edit PATH, :buffers, :buffer N, :buffer_next, :buffer_previous, :buffer_delete [N], :buffer_force_delete [N] Aliases: bnext, bprev, bdelete, bdelete!; arguments and safety rules are unchanged :quit refuses any dirty buffer; :save_and_quit saves active and quits only if all are clean Tabs: :tab.new, :tab.previous, :tab.next, :tab.close, :tab.force_close Tabs retain pane layouts and independent views; close protects exclusively displayed unsaved edits Markdown links: :follow_link (primary caret; local files only) Diff: :diff (Normal); choose Before then After; Working checkout After edits live unsaved buffers Before and historical/remote After stay read-only; Tab/Shift+Tab categories/Normal focus All text panes use native motions, selections and copy; read-only panes reject edits/saves Switching Before/After preserves the aligned cursor row and display column Selector queries own printable keys, including :; text navigation skips omitted lines Normal [/] explicitly expands context; Insert brackets remain literal text Esc closes Before/After selectors; :diff.close exits an opened comparison explicitly Remote refs/history load into temporary comparison repos without changing active refs/checkout Changed files: normal picker/explorer bindings, Ctrl+w close; A/M/D/R status and rename paths Ruby config: $XDG_CONFIG_HOME/vamp/config.rb or ~/.config/vamp/config.rb Set VAMP_RUBY to choose the Ruby executable. Native LSP is disabled by default: config.lsp[\"rust\"] = true enables a preset Presets: rust -> rust-analyzer, python -> ty server + ruff server, ruby -> solargraph stdio Custom languages: config.lsp[\"my-language\"] = { command: [...], extensions: [...] } Server stacks: config.lsp[\"python\"] = { servers: [{ command: [...] }, { command: [...] }] } LSP commands: :lsp.definition, :lsp.diagnostics, :lsp.hover Semantic highlighting: automatic when the first enabled server supports semantic tokens Hover binding: config.bind \"K\", config.core.lsp.hover (no default key)"; struct Options { file: Option, config: Option, } fn options() -> Result, String> { let mut args = env::args_os().skip(1); let mut file = None; let mut config = None; let mut config_selected = false; let mut positional = false; while let Some(arg) = args.next() { if !positional && (arg == "--help" || arg == "-h") { println!("{HELP}"); return Ok(None); } else if !positional && arg == "--" { positional = true; } else if !positional && arg == "--config" { if config_selected { return Err("use only one of --config and --no-config".into()); } config = Some(PathBuf::from( args.next().ok_or("--config requires a path")?, )); config_selected = true; } else if !positional && arg == "--no-config" { if config_selected { return Err("use only one of --config and --no-config".into()); } config_selected = true; } else if !positional && arg.to_string_lossy().starts_with('-') { return Err(format!("unknown option: {}", arg.to_string_lossy())); } else if file.replace(PathBuf::from(arg)).is_some() { return Err("only one file can be opened".into()); } } if !config_selected { let home = env::var_os("XDG_CONFIG_HOME") .map(PathBuf::from) .or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".config"))); config = home .map(|home| home.join("vamp/config.rb")) .filter(|path| path.exists()); } Ok(Some(Options { file, config })) } fn run() -> Result<(), String> { let Some(options) = options()? else { return Ok(()); }; let mut session = EditorSession::open( options.file, tui::CAPABILITIES.iter().map(|s| (*s).to_owned()).collect(), options.config.as_deref(), ) .map_err(|error| format!("cannot open file: {error}"))?; tui::run(&mut session).map_err(|error| format!("terminal error: {error}")) } fn main() { if let Err(error) = run() { eprintln!("vamp: {error}"); std::process::exit(1); } }