mod atomic; mod cli; mod commands; mod error; mod instance; mod profile; mod ssh; mod supervisor; mod timefmt; mod ui; use clap::{CommandFactory, Parser}; use cli::{Cli, Commands}; fn main() { let args: Vec = std::env::args().collect(); // Plain `porthole`, `-h`/`--help`, or `help` at the top level: show // every subcommand's own flags inline instead of requiring a // per-subcommand `--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::Add(args) => commands::add::run(args), Commands::Open(args) => commands::open::run(args), Commands::Close(args) => commands::close::run(args), Commands::Edit(args) => commands::edit::run(args), Commands::Status(args) => commands::status::run(args), Commands::List(args) => commands::list::run(args), Commands::Remove(args) => commands::remove::run(args), Commands::Wipe(args) => commands::wipe::run(args), Commands::Transfer(args) => commands::transfer::run(args), Commands::Completions { shell } => { commands::completions::run(shell); Ok(()) } // Internal Commands::Supervise { name } => supervisor::run(&name), }; if let Err(e) = result { ui::err(&e.to_string()); std::process::exit(1); } } /// True for a bare `porthole` 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: one summary line per subcommand (name, /// positional args, description), followed by an indented line per flag /// with its own help text. fn print_full_help() { let mut cmd = Cli::command(); cmd.build(); // resolve default value names etc. before introspecting let bin = cmd.get_name().to_string(); if let Some(about) = cmd.get_about() { println!("{about}"); println!(); } println!("{} {bin} {} {}", ui::blue("Usage:"), ui::cyan(""), ui::yellow("[ARGS]")); println!(); println!("{}", ui::blue("Commands:")); let subcommands: Vec<&clap::Command> = cmd.get_subcommands().filter(|s| s.get_name() != "help" && !s.is_hide_set()).collect(); let flag_rows_by_cmd: Vec> = subcommands.iter().map(|s| flag_rows(s)).collect(); let name_w = subcommands.iter().map(|s| s.get_name().len()).max().unwrap_or(0); let pos_w = subcommands.iter().map(|s| visual_width(&positional_args(s))).max().unwrap_or(0); // One width across every subcommand's flags, not just its own, so the // help-text column lines up no matter which command it's under. let flag_w = flag_rows_by_cmd.iter().flatten().map(|(f, _)| visual_width(f)).max().unwrap_or(0); for (i, s) in subcommands.iter().enumerate() { let name = s.get_name(); let positionals = positional_args(s); let about = s.get_about().map(|a| a.to_string()).unwrap_or_default(); println!(" {name:name_w$} {} {about}", pad_visual(&positionals, pos_w)); let rows = &flag_rows_by_cmd[i]; for (flag, help) in rows { println!(" {} {help}", pad_visual(flag, flag_w)); } // Flagless commands stay a tight single line; only the multi-line // (flag-bearing) ones get a blank line to separate them visually. if !rows.is_empty() && i + 1 < subcommands.len() { println!(); } } println!(); let alias_rows: Vec<(String, String)> = subcommands .iter() .filter_map(|s| { let aliases = s.get_visible_aliases().collect::>().join(", "); (!aliases.is_empty()).then(|| (s.get_name().to_string(), aliases)) }) .collect(); if !alias_rows.is_empty() { println!("{}", ui::blue("Aliases:")); let name_w = alias_rows.iter().map(|(n, _)| n.len()).max().unwrap_or(0); for (name, aliases) in &alias_rows { println!(" {name:name_w$} {aliases}"); } 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 required positional arg of `cmd`, `[name]` for an /// optional one, space-joined and colored cyan. fn positional_args(cmd: &clap::Command) -> String { cmd.get_positionals() .map(|a| { let id = a.get_id().as_str(); let text = if a.is_required_set() { format!("<{id}>") } else { format!("[{id}]") }; ui::cyan(&text) }) .collect::>() .join(" ") } /// `(flag display, help text)` for each of `cmd`'s non-positional, non-help /// args, e.g. `("-l/--local <[BIND:]PORT:HOST:PORT>", "Local forward: your /// machine -> remote")`. The flag display is colored yellow. fn flag_rows(cmd: &clap::Command) -> Vec<(String, String)> { cmd.get_arguments() .filter(|a| !a.is_positional() && a.get_id().as_str() != "help") .filter_map(|a| { let flag = flag_names(a)?; let display = if matches!(a.get_action(), clap::ArgAction::Set | clap::ArgAction::Append) { let value = a .get_value_names() .and_then(|v| v.first()) .map(|v| v.to_string()) .unwrap_or_else(|| a.get_id().as_str().to_uppercase()); format!("{flag} <{value}>") } else { flag }; let help = a.get_help().map(|h| h.to_string()).unwrap_or_default(); Some((ui::yellow(&display), help)) }) .collect() } /// `-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, } } /// 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)))) }