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 list; Space e docked compact tree (right by default) tree NORMAL: hjkl navigate; INSERT: type to filter, Ctrl+hjkl navigate; Enter toggles/opens i filters in tree INSERT; Esc returns NORMAL, then editor; 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: prefix motions with counts (5j, 2w, 3n); Esc cancels count u undo, U redo, : command, / search, n/N next/previous match v select, p/P paste fragments, Ctrl+k/Ctrl+e clone above/below 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/Esc returns to Normal retaining carets Insert: Esc returns to Normal retaining carets Search: Enter finds next literal match, Esc cancels (case-sensitive, wraps) Commands: :w [PATH], :q, :q!, :wq [PATH], :plugins, :PLUGIN [ARGS] Buffers: :edit PATH, :buffers, :buffer N, :bnext, :bprev, :bdelete[!] [N] :q refuses any dirty buffer; :wq saves active and quits only if all are clean Ruby config: $XDG_CONFIG_HOME/vamp/config.rb or ~/.config/vamp/config.rb Set VAMP_RUBY to choose the Ruby executable."; 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); } }