Files
vamp/src/main.rs
T
2026-10-08 16:40:35 +02:00

102 lines
4.2 KiB
Rust

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<PathBuf>,
config: Option<PathBuf>,
}
fn options() -> Result<Option<Options>, 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);
}
}