Files
vamp/src/main.rs
T

115 lines
5.1 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 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
gf follows Markdown file links/reference links and heading fragments (no LSP)
Normal/Visual: prefix motions with counts (5j, 2w, 3n); Esc cancels count
u undo, U redo, : command, / search, n/N next/previous match
Normal/Visual: . repeats last native edit at current targets (one undo group)
v 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/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
Markdown links: :follow_link (primary caret; local files only)
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<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);
}
}