pub mod complete;
pub mod help;
pub mod macros;
pub mod progress;
pub mod prompt;
pub mod styles;
pub mod summary;
pub mod term;
pub use cli_ui_derive::{CliCommand, CliOptions};
pub use progress::{format_bytes, Progress};
pub use summary::Summary;
use styles::*;
#[derive(Debug)]
pub struct CliError(pub String);
impl std::fmt::Display for CliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for CliError {}
pub type Result<T> = std::result::Result<T, CliError>;
pub trait CliOptions: Sized {
#[doc(hidden)]
fn parse_args(args: &[&str]) -> std::result::Result<Self, String>;
}
impl CliOptions for () {
fn parse_args(_: &[&str]) -> std::result::Result<Self, String> {
Ok(())
}
}
pub trait CliCommand: Sized {
type Global: 'static;
fn parse() -> Result<Self>;
fn global() -> &'static Self::Global
where
Self::Global: Sized;
fn help();
}
#[doc(hidden)]
pub trait ParseInner: Sized {
fn parse_inner(args: &[&str]) -> Result<Self>;
}
#[doc(hidden)]
pub trait PrintHelp {
fn print_help();
}
#[doc(hidden)]
pub trait NestedCompletions {
fn print_completions(shell: &str);
}
#[doc(hidden)]
pub fn parse_global_flags<G: CliOptions>(args: &[String]) -> (G, Vec<String>) {
let split = args
.iter()
.position(|a| !a.starts_with('-'))
.unwrap_or(args.len());
let global_refs: Vec<&str> = args[..split].iter().map(|s| s.as_str()).collect();
let rest: Vec<String> = args[split..].to_vec();
let global = G::parse_args(&global_refs).unwrap_or_else(|e| {
print_error(&format!("invalid global flag: {e}"));
std::process::exit(1);
});
(global, rest)
}
pub fn print_version(name: &str, version: &str, badge: anstyle::Style) {
eprintln!();
eprintln!(
" {} {}",
paint(badge, &format!(" {name} ")),
paint(DIM, &format!("v{version}"))
);
eprintln!();
}
pub fn print_error(msg: &str) {
eprintln!();
eprintln!("{} {}", paint(ERR, CROSS), paint(BOLD, msg));
eprintln!();
}
pub fn print_warning(msg: &str) {
eprintln!();
eprintln!("{} {}", paint(YELLOW, "⚠"), paint(YELLOW, msg));
eprintln!();
}
pub fn print_missing_args(name: &str, _about: &str, _tagline: &str) {
eprintln!();
eprintln!(
"{} {}",
paint(ERR, CROSS),
paint(BOLD, "missing required arguments")
);
eprintln!(
"{} {}",
paint(DIM, BAR),
paint(WHITE, &format!("{name} <INPUT> <OUTPUT>"))
);
eprintln!(
"{} {}",
paint(DIM, ARROW),
paint(WHITE, &format!("run `{name} --help` for usage"))
);
eprintln!();
}
pub fn print_unknown_subcommand(app: &str, cmd: &str, known: &[&str]) {
eprintln!();
eprintln!(
"{} {}",
paint(ERR, CROSS),
paint(BOLD, &format!("unknown command: `{cmd}`"))
);
if let Some(s) = did_you_mean(cmd, known) {
eprintln!(
"{} {}",
paint(DIM, BAR),
paint(WHITE, &format!("did you mean `{s}`?"))
);
}
eprintln!(
"{} {}",
paint(DIM, ARROW),
paint(
WHITE,
&format!("run `{app} --help` to see available commands")
)
);
eprintln!();
}
pub fn print_missing_subcommand(app: &str, known: &[&str]) {
eprintln!();
eprintln!("{} {}", paint(ERR, CROSS), paint(BOLD, "missing command"));
eprintln!(
"{} {}",
paint(DIM, BAR),
paint(WHITE, &format!("available: {}", known.join(", ")))
);
eprintln!(
"{} {}",
paint(DIM, ARROW),
paint(WHITE, &format!("run `{app} --help` for usage"))
);
eprintln!();
}
pub fn print_sub_usage(app: &str, cmd: &str) {
let (_, accent) = styles::theme_styles("cyan");
eprintln!();
eprintln!(
"{} {} {}",
paint(accent, DIAMOND),
paint(BOLD, &format!("{app} {cmd}")),
paint(DIM, "— subcommand options"),
);
}
pub fn print_unit_help(app: &str, cmd: &str, _opts: &[(&str, &str)]) {
let (_, accent) = styles::theme_styles("cyan");
eprintln!();
eprintln!(
"{} {} {}",
paint(accent, DIAMOND),
paint(BOLD, &format!("{app} {cmd}")),
paint(DIM, "— no options")
);
eprintln!(
"{} {}",
paint(DIM, BAR),
paint(DIM, &format!("usage: {app} {cmd}"))
);
eprintln!();
}
pub fn glob_match(pattern: &str, input: &str) -> bool {
glob_match_inner(pattern.as_bytes(), input.as_bytes())
}
fn glob_match_inner(pat: &[u8], s: &[u8]) -> bool {
match (pat.first(), s.first()) {
(None, None) => true,
(Some(b'*'), _) => {
glob_match_inner(&pat[1..], s) || (!s.is_empty() && glob_match_inner(pat, &s[1..]))
}
(Some(b'?'), Some(_)) => glob_match_inner(&pat[1..], &s[1..]),
(Some(b'['), _) => {
let end = pat.iter().position(|&b| b == b']').unwrap_or(pat.len());
let class = &pat[1..end];
if s.is_empty() {
return false;
}
let matched = class.contains(&s[0]);
if matched {
glob_match_inner(&pat[end + 1..], &s[1..])
} else {
false
}
}
(Some(a), Some(b)) if a == b => glob_match_inner(&pat[1..], &s[1..]),
_ => false,
}
}
fn did_you_mean<'a>(input: &str, candidates: &[&'a str]) -> Option<&'a str> {
candidates
.iter()
.filter_map(|c| {
let d = edit_distance(input, c);
if d <= 2 {
Some((d, *c))
} else {
None
}
})
.min_by_key(|(d, _)| *d)
.map(|(_, c)| c)
}
#[allow(clippy::needless_range_loop)]
fn edit_distance(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let (m, n) = (a.len(), b.len());
let mut dp = vec![vec![0usize; n + 1]; m + 1];
for i in 0..=m {
dp[i][0] = i;
}
for j in 0..=n {
dp[0][j] = j;
}
for i in 1..=m {
for j in 1..=n {
dp[i][j] = if a[i - 1] == b[j - 1] {
dp[i - 1][j - 1]
} else {
1 + dp[i - 1][j].min(dp[i][j - 1]).min(dp[i - 1][j - 1])
};
}
}
dp[m][n]
}