mod cli; mod commands; mod error; mod pw; mod state; mod ui; use clap::{CommandFactory, Parser}; use clap_complete::{generate, Shell}; use cli::{Cli, Commands}; fn main() { let args = normalize_args(std::env::args().collect()); // Plain `vmic`, `-h`/`--help`, or `help` at the top level: show every // subcommand's own flags inline instead of making the user drill into // each one with its own `--help`. if wants_top_level_help(&args) { print_full_help(); std::process::exit(if args.len() <= 1 { 2 } else { 0 }); } let cli = match Cli::try_parse_from(&args) { Ok(cli) => cli, Err(e) => e.exit(), }; let result = match cli.command { Commands::Create(args) => commands::create::run(args), Commands::Route(args) => commands::route::run(args), Commands::Edit(args) => commands::edit::run(args), Commands::Delete(args) => commands::delete::run(args), Commands::List => commands::list::run(), Commands::Wipe => commands::wipe::run(), Commands::Completions { shell } => { print_completions(shell); Ok(()) } }; if let Err(e) = result { ui::err(&e.to_string()); std::process::exit(1); } } /// clap's `#[arg(short)]` only supports single-character short flags, so /// `-sv` can't be a real short flag. Rewrite it to `--source-volume` before /// clap ever sees it. fn normalize_args(args: Vec) -> Vec { args.into_iter() .map(|a| if a == "-sv" { "--source-volume".to_string() } else { a }) .collect() } fn print_completions(shell: Shell) { let mut cmd = Cli::command(); let name = cmd.get_name().to_string(); generate(shell, &mut cmd, name, &mut std::io::stdout()); } /// True for a bare `vmic` invocation, or top-level `-h`/`--help`/`help` - /// i.e. anything that should show the expanded help rather than being /// handled (or rejected) by a specific subcommand. fn wants_top_level_help(args: &[String]) -> bool { match args.get(1..) { Some([]) => true, Some([a]) => a == "-h" || a == "--help" || a == "help", _ => false, } } /// Prints one screen of help: every subcommand's positional args and flags /// summarized inline (e.g. ` [-l/--loopback]`), so nothing requires /// drilling into a subcommand's own `--help` just to see what it takes. fn print_full_help() { let mut cmd = Cli::command(); cmd.build(); // resolve default value names etc. before introspecting if let Some(about) = cmd.get_about() { println!("{about}"); println!(); } println!("{} vmic {} {}", ui::blue("Usage:"), ui::cyan(""), ui::yellow("[ARGS]")); println!(); println!("{}", ui::blue("Commands:")); let rows: Vec<(String, String, String, String)> = cmd .get_subcommands() .filter(|s| s.get_name() != "help") .map(|s| { let aliases = s.get_visible_aliases().collect::>().join("/"); let about = s.get_about().map(|a| a.to_string()).unwrap_or_default(); (s.get_name().to_string(), compact_args(s), about, aliases) }) .collect(); print_table(&rows); println!(); println!("{}", ui::blue("Options:")); let opt_rows: Vec<(String, String)> = cmd .get_arguments() .filter(|a| !a.is_positional()) .filter_map(|a| Some((flag_names(a)?, a.get_help().map(|h| h.to_string()).unwrap_or_default()))) .collect(); let flag_w = opt_rows.iter().map(|(f, _)| f.len()).max().unwrap_or(0); for (flag, help) in &opt_rows { println!(" {flag:flag_w$} {help}"); } } /// `` for each positional, `[-x/--long ]` or `[-x/--long]` for /// each flag, in one space-joined line - a compact stand-in for the /// `[OPTIONS] ` a normal usage line would collapse this to. /// /// Colors match the original fish script's `vmic_usage_line`: positional /// `<...>` args in cyan, optional `[...]` flag groups (whole bracket, /// including any value placeholder inside) in yellow. fn compact_args(cmd: &clap::Command) -> String { let mut parts: Vec = cmd .get_positionals() .map(|a| ui::cyan(&format!("<{}>", a.get_id().as_str()))) .collect(); for arg in cmd.get_arguments() { if arg.is_positional() || arg.get_id().as_str() == "help" { continue; } let Some(flag) = flag_names(arg) else { continue }; let group = if matches!(arg.get_action(), clap::ArgAction::Set | clap::ArgAction::Append) { let value = arg .get_value_names() .and_then(|v| v.first()) .map(|v| v.to_string()) .unwrap_or_else(|| arg.get_id().as_str().to_uppercase()); format!("[{flag} <{value}>]") } else { format!("[{flag}]") }; parts.push(ui::yellow(&group)); } parts.join(" ") } /// `-x, --long` / `-x` / `--long` for a non-positional arg, or `None` for /// one with no visible flag at all. fn flag_names(arg: &clap::Arg) -> Option { match (arg.get_short(), arg.get_long()) { (Some(s), Some(l)) => Some(format!("-{s}/--{l}")), (Some(s), None) => Some(format!("-{s}")), (None, Some(l)) => Some(format!("--{l}")), (None, None) => None, } } /// Prints `(name, args, about, aliases)` rows with the name and args /// columns padded to their widest entry, so the `about` column lines up. /// `args` carries ANSI color codes (see `compact_args`), so padding by /// `.len()`/`{:width$}` would count invisible escape bytes as columns and /// misalign everything - `pad_visual` pads by visible width instead. /// `aliases`, if non-empty, trails after `| `. fn print_table(rows: &[(String, String, String, String)]) { let name_w = rows.iter().map(|(n, ..)| n.len()).max().unwrap_or(0); let args_w = rows.iter().map(|(_, a, ..)| visual_width(a)).max().unwrap_or(0); for (name, args, about, aliases) in rows { print!(" {name:name_w$} {} {about}", pad_visual(args, args_w)); if !aliases.is_empty() { print!(" | {aliases}"); } println!(); } } /// Number of visible columns in `s`, skipping any `\x1b[...m` ANSI SGR /// escape sequences it contains. fn visual_width(s: &str) -> usize { let mut width = 0; let mut in_escape = false; for c in s.chars() { if in_escape { in_escape = c != 'm'; } else if c == '\x1b' { in_escape = true; } else { width += 1; } } width } /// Right-pads `s` with spaces to `width` visible columns, per `visual_width`. fn pad_visual(s: &str, width: usize) -> String { format!("{s}{}", " ".repeat(width.saturating_sub(visual_width(s)))) }