feat: implement modular modal editor and isolated Ruby plugins
This commit is contained in:
+131
-2
@@ -1,3 +1,132 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
use std::{collections::BTreeMap, env, path::PathBuf};
|
||||
|
||||
use vamp::{
|
||||
editor::Editor,
|
||||
ruby::{Config, RubyPlugin, load_config},
|
||||
tui,
|
||||
};
|
||||
|
||||
const HELP: &str = "vamp — modal editor with isolated Ruby plugins
|
||||
|
||||
Usage: vamp [--config PATH | --no-config] [FILE]
|
||||
|
||||
Normal: i/a insert, o/O new line, h/j/k/l move, 0/$ line ends,
|
||||
x delete, u undo, Ctrl-r redo, : command
|
||||
Insert: Esc returns to normal
|
||||
Commands: :w [PATH], :q, :q!, :wq [PATH], :plugins, :PLUGIN [ARGS]
|
||||
|
||||
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 editor = Editor::open(
|
||||
options.file,
|
||||
tui::CAPABILITIES.iter().map(|s| (*s).to_owned()).collect(),
|
||||
)
|
||||
.map_err(|error| format!("cannot open file: {error}"))?;
|
||||
let mut notices = Vec::new();
|
||||
let config = match options.config {
|
||||
Some(path) => load_config(&path).unwrap_or_else(|error| {
|
||||
notices.push(format!("config disabled: {error}"));
|
||||
Config::default()
|
||||
}),
|
||||
None => Config::default(),
|
||||
};
|
||||
let mut plugins = BTreeMap::new();
|
||||
for path in config.plugins {
|
||||
match RubyPlugin::start(&path) {
|
||||
Ok((plugin, feature)) => {
|
||||
let name = feature.name.clone();
|
||||
match editor.register(feature) {
|
||||
Ok(()) => {
|
||||
plugins.insert(name, plugin);
|
||||
}
|
||||
Err(error) => notices.push(format!("{}: {error}", path.display())),
|
||||
}
|
||||
}
|
||||
Err(error) => notices.push(format!("{}: {error}", path.display())),
|
||||
}
|
||||
}
|
||||
for (key, command) in config.bindings {
|
||||
if let Err(error) = editor.bind(key, command) {
|
||||
notices.push(format!("binding {key:?}: {error}"));
|
||||
}
|
||||
}
|
||||
if notices.is_empty() {
|
||||
editor.set_message("i: insert • Esc: normal • :w save • :q quit • :plugins");
|
||||
} else {
|
||||
editor.set_message(notices.join(" | "));
|
||||
}
|
||||
tui::run(&mut editor, |editor, invocation| {
|
||||
let result = match plugins.get_mut(&invocation.plugin) {
|
||||
Some(plugin) => plugin
|
||||
.invoke(&invocation.command, &invocation.args, &editor.context())
|
||||
.and_then(|effects| editor.apply(effects)),
|
||||
None => Err("plugin host is unavailable".into()),
|
||||
};
|
||||
if let Err(error) = result {
|
||||
if let Some(mut plugin) = plugins.remove(&invocation.plugin) {
|
||||
plugin.disable();
|
||||
}
|
||||
editor.disable_plugin(&invocation.plugin, &error);
|
||||
}
|
||||
})
|
||||
.map_err(|error| format!("terminal error: {error}"))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(error) = run() {
|
||||
eprintln!("vamp: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user