use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
#[derive(Parser)]
#[command(
name = "exc",
version,
about = "A fast, lightweight terminal menu launcher",
long_about = "exc is a fast, lightweight menu launcher for your terminal: a searchable, \
keyboard-driven command picker configured entirely from a TOML file. Use it as an everyday \
command hub, or drop a project-local config next to a repo to give it its own menu of tasks \
(deployments, git housekeeping, cleanup, etc.).",
after_help = "EXAMPLES:\n \
exc Launch the interactive picker\n \
exc deploy-staging Run the \"deploy-staging\" command directly\n \
exc list --plain List every command name, one per line\n \
exc init Write a starter config if you don't have one yet\n \
exc man > exc.1 Generate a man page (see `exc man --help`)"
)]
pub struct Cli {
#[arg(long, global = true, value_name = "PATH")]
pub config: Option<PathBuf>,
#[arg(long, global = true, value_name = "NAME")]
pub profile: Option<String>,
#[arg(long, global = true, value_name = "NAME")]
pub theme: Option<String>,
#[arg(long, global = true)]
pub no_color: bool,
#[arg(long, global = true)]
pub no_sysinfo: bool,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand)]
pub enum Commands {
Run {
name: String,
},
List {
#[arg(long, value_name = "NAME")]
profile: Option<String>,
#[arg(long)]
plain: bool,
},
Validate {
#[arg(long)]
strict: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
},
Sysinfo,
Init {
#[arg(long)]
force: bool,
},
Man,
}
#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
Text,
Json,
}
const KNOWN_SUBCOMMANDS: &[&str] = &["run", "list", "validate", "sysinfo", "init", "man", "help"];
const VALUE_FLAGS: &[&str] = &["--config", "--profile", "--theme"];
pub fn parse_args() -> Cli {
let raw: Vec<String> = std::env::args().collect();
match Cli::try_parse_from(&raw) {
Ok(cli) => cli,
Err(e) => {
if let Some(insert_at) = implicit_run_insertion_point(&raw) {
let mut retry = raw.clone();
retry.insert(insert_at, "run".to_string());
if let Ok(cli) = Cli::try_parse_from(&retry) {
return cli;
}
}
e.exit();
}
}
}
fn implicit_run_insertion_point(raw: &[String]) -> Option<usize> {
let mut i = 1;
while i < raw.len() {
let tok = raw[i].as_str();
if VALUE_FLAGS.contains(&tok) {
i += 2; continue;
}
if tok.starts_with('-') {
i += 1; continue;
}
return if KNOWN_SUBCOMMANDS.contains(&tok) { None } else { Some(i) };
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn args(v: &[&str]) -> Vec<String> {
std::iter::once("exc".to_string()).chain(v.iter().map(|s| s.to_string())).collect()
}
#[test]
fn bare_id_at_front_gets_run_inserted() {
assert_eq!(implicit_run_insertion_point(&args(&["73"])), Some(1));
}
#[test]
fn bare_id_after_a_value_flag_still_gets_run_inserted() {
assert_eq!(implicit_run_insertion_point(&args(&["--config", "c.toml", "73"])), Some(3));
}
#[test]
fn bare_id_after_equals_form_flag_still_gets_run_inserted() {
assert_eq!(implicit_run_insertion_point(&args(&["--config=c.toml", "73"])), Some(2));
}
#[test]
fn bare_id_after_no_color_still_gets_run_inserted() {
assert_eq!(implicit_run_insertion_point(&args(&["--no-color", "73"])), Some(2));
}
#[test]
fn known_subcommand_is_left_alone() {
assert_eq!(implicit_run_insertion_point(&args(&["--config", "c.toml", "list"])), None);
}
#[test]
fn no_positional_token_at_all() {
assert_eq!(implicit_run_insertion_point(&args(&["--no-color"])), None);
}
}