feat: expose read-only local review CLI
This commit is contained in:
+84
-9
@@ -2,12 +2,19 @@ use std::{env, io, path::PathBuf};
|
||||
|
||||
use vamp::{session::EditorSession, tui};
|
||||
|
||||
mod review_cli;
|
||||
|
||||
// Only the first argument can be this process-to-process terminal handoff marker.
|
||||
const INHERITED_SCREEN: &str = "--vamp-inherit-alternate-screen";
|
||||
|
||||
const HELP: &str = "vamp — modal editor with isolated Ruby plugins
|
||||
|
||||
Usage: vamp [--config PATH | --no-config] [FILE]
|
||||
vamp [--config PATH | --no-config] --review list [--repo PATH] [--json]
|
||||
vamp [--config PATH | --no-config] --review show ID [--repo PATH] [--json] [--all]
|
||||
|
||||
Review CLI: read-only local storage; show defaults to unresolved comments; --all includes resolved.
|
||||
Use vamp -- --review to edit a file named --review.
|
||||
|
||||
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
|
||||
@@ -114,20 +121,33 @@ Hover binding: config.bind \"K\", config.core.lsp.hover (no default key)";
|
||||
struct Options {
|
||||
file: Option<PathBuf>,
|
||||
config: Option<PathBuf>,
|
||||
review: Option<review_cli::Request>,
|
||||
}
|
||||
|
||||
fn options(inherited_screen: bool) -> Result<Option<Options>, String> {
|
||||
let mut args = env::args_os().skip(1 + usize::from(inherited_screen));
|
||||
parse_options(env::args_os().skip(1 + usize::from(inherited_screen)))
|
||||
}
|
||||
|
||||
fn parse_options(
|
||||
args: impl IntoIterator<Item = std::ffi::OsString>,
|
||||
) -> Result<Option<Options>, String> {
|
||||
let mut args = args.into_iter();
|
||||
let mut file = None;
|
||||
let mut config = None;
|
||||
let mut config_selected = false;
|
||||
let mut positional = false;
|
||||
let mut review = None;
|
||||
while let Some(arg) = args.next() {
|
||||
if !positional && (arg == "--help" || arg == "-h") {
|
||||
println!("{HELP}");
|
||||
return Ok(None);
|
||||
} else if !positional && arg == "--" {
|
||||
if review.is_some() {
|
||||
return Err("unexpected review argument: --".into());
|
||||
}
|
||||
positional = true;
|
||||
} else if !positional && arg == "--review" && file.is_none() && review.is_none() {
|
||||
review = Some(review_cli::Request::parse(&mut args)?);
|
||||
} else if !positional && arg == "--config" {
|
||||
if config_selected {
|
||||
return Err("use only one of --config and --no-config".into());
|
||||
@@ -141,6 +161,8 @@ fn options(inherited_screen: bool) -> Result<Option<Options>, String> {
|
||||
return Err("use only one of --config and --no-config".into());
|
||||
}
|
||||
config_selected = true;
|
||||
} else if !positional && review.is_some() {
|
||||
review.as_mut().unwrap().option(&arg, &mut args)?;
|
||||
} 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() {
|
||||
@@ -155,24 +177,27 @@ fn options(inherited_screen: bool) -> Result<Option<Options>, String> {
|
||||
.map(|home| home.join("vamp/config.rb"))
|
||||
.filter(|path| path.exists());
|
||||
}
|
||||
Ok(Some(Options { file, config }))
|
||||
Ok(Some(Options {
|
||||
file,
|
||||
config,
|
||||
review,
|
||||
}))
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let inherited_screen = env::args_os()
|
||||
.nth(1)
|
||||
.is_some_and(|arg| arg == INHERITED_SCREEN);
|
||||
if inherited_screen {
|
||||
return tui::TerminalSession::run(true, || {
|
||||
run_editor(options(true).map_err(io::Error::other)?, true)
|
||||
})
|
||||
.map_err(|error| error.to_string());
|
||||
let options = options(inherited_screen)?;
|
||||
if let Some(options) = &options {
|
||||
if let Some(request) = &options.review {
|
||||
return review_cli::run(request, options.config.as_deref());
|
||||
}
|
||||
}
|
||||
let options = options(false)?;
|
||||
if options.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
tui::TerminalSession::run(false, || run_editor(options, false))
|
||||
tui::TerminalSession::run(inherited_screen, || run_editor(options, inherited_screen))
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
@@ -226,3 +251,53 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse(args: &[&str]) -> Result<Option<Options>, String> {
|
||||
parse_options(args.iter().map(std::ffi::OsString::from))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_commands_preserve_filenames_and_validate_arguments() {
|
||||
assert_eq!(
|
||||
parse(&["review"]).unwrap().unwrap().file,
|
||||
Some("review".into())
|
||||
);
|
||||
assert_eq!(
|
||||
parse(&["--", "--review"]).unwrap().unwrap().file,
|
||||
Some("--review".into())
|
||||
);
|
||||
assert!(
|
||||
parse(&["--no-config", "--review", "list", "--repo", ".", "--json"])
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.review
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
parse(&["--review", "show", "1", "--all", "--no-config"])
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.review
|
||||
.is_some()
|
||||
);
|
||||
for args in [
|
||||
vec!["--review"],
|
||||
vec!["--review", "show"],
|
||||
vec!["--review", "show", "0"],
|
||||
vec!["--review", "show", "-1"],
|
||||
vec!["--review", "show", "abc"],
|
||||
vec!["--review", "list", "--all"],
|
||||
vec!["--review", "list", "--repo"],
|
||||
vec!["--review", "list", "--repo", ".", "--repo", "."],
|
||||
vec!["--review", "list", "extra"],
|
||||
vec!["--review", "list", "--", "extra"],
|
||||
vec!["--review", "list", "--config", "x", "--no-config"],
|
||||
] {
|
||||
assert!(parse(&args).is_err(), "{args:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user