diff --git a/Cargo.lock b/Cargo.lock index c888d5f43..214ef1ee9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,6 +314,15 @@ dependencies = [ "textwrap 0.15.0", ] +[[package]] +name = "clap_complete" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7ca9141e27e6ebc52e3c378b0c07f3cea52db46ed1cc5861735fb697b56356" +dependencies = [ + "clap 3.1.15", +] + [[package]] name = "clap_lex" version = "0.2.0" @@ -1620,6 +1629,7 @@ dependencies = [ "byte-unit", "bytecount", "clap 3.1.15", + "clap_complete", "color_quant", "git-repository", "git2", diff --git a/Cargo.toml b/Cargo.toml index 4b3e2ca71..55370d0c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ askalono = "0.4.5" byte-unit = "4.0" # to match git-repository's lower requirement, which it needs for MSRV bytecount = "0.6.2" clap = {version = "3.1.15", features = ["cargo", "wrap_help"]} +clap_complete = "3.1" color_quant = "1.1.0" git2 = {version = "0.14.2", default-features = false} git-repository = { version = "0.16.0", features = ["max-performance", "unstable", "serde1"] } diff --git a/src/cli.rs b/src/cli.rs index 992390e62..5798a720e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,9 +5,11 @@ use crate::ui::image_backends; use crate::ui::image_backends::ImageBackend; use crate::ui::printer::SerializationFormat; use anyhow::{Context, Result}; -use clap::{crate_description, crate_name, crate_version, AppSettings, Arg}; +use clap::{crate_description, crate_name, crate_version, AppSettings, Arg, ValueHint}; +use clap_complete::{generate, Generator, Shell}; use image::DynamicImage; use regex::Regex; +use std::io; use std::process::Command; use std::{convert::From, env, str::FromStr}; use strum::IntoEnumIterator; @@ -39,16 +41,206 @@ pub struct Config { pub show_email: bool, pub include_hidden: bool, pub language_types: Vec, + pub completion: Option, } impl Config { pub fn new() -> Result { - let possible_backends = ["kitty", "iterm", "sixel"]; + let matches = build_cli().get_matches(); - let color_values = &[ - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", - ]; - let matches = clap::Command::new(crate_name!()) + let true_color = match matches.value_of("true-color") { + Some("always") => true, + Some("never") => false, + Some("auto") => is_truecolor_terminal(), + _ => unreachable!(), + }; + + let no_bold = matches.is_present("no-bold"); + let no_merges = matches.is_present("no-merges"); + let no_color_palette = matches.is_present("no-palette"); + let print_languages = matches.is_present("languages"); + let print_package_managers = matches.is_present("package-managers"); + let iso_time = matches.is_present("isotime"); + let show_email = matches.is_present("email"); + let include_hidden = matches.is_present("hidden"); + + let output = matches + .value_of("output") + .map(SerializationFormat::from_str) + .transpose()?; + + let fields_to_hide: Vec = if let Some(values) = matches.values_of("disable-fields") + { + values.map(String::from).collect() + } else { + Vec::new() + }; + + let disabled_fields = InfoFieldOff::new(fields_to_hide)?; + + let art_off = match matches.value_of("show-logo") { + Some("always") => false, + Some("never") => true, + Some("auto") => { + if let Some((width, _)) = term_size::dimensions_stdout() { + width < MAX_TERM_WIDTH + } else { + false + } + } + _ => unreachable!(), + }; + + let image = if let Some(image_path) = matches.value_of("image") { + Some(image::open(image_path).with_context(|| "Could not load the specified image")?) + } else { + None + }; + + let image_backend = if image.is_some() { + if let Some(backend_name) = matches.value_of("image-backend") { + image_backends::get_image_backend(backend_name) + } else { + image_backends::get_best_backend() + } + } else { + None + }; + + let image_color_resolution = if let Some(value) = matches.value_of("color-resolution") { + usize::from_str(value)? + } else { + 16 + }; + + let repo_path = matches + .value_of("input") + .map(String::from) + .with_context(|| "Failed to parse input directory")?; + + let ascii_input = matches.value_of("ascii-input").map(String::from); + + let ascii_language = matches + .value_of("ascii-language") + .map(|ascii_language| Language::from_str(&ascii_language.to_lowercase()).unwrap()); + + let ascii_colors = if let Some(values) = matches.values_of("ascii-colors") { + values.map(String::from).collect() + } else { + Vec::new() + }; + + let text_colors = if let Some(values) = matches.values_of("text-colors") { + values.map(String::from).collect() + } else { + Vec::new() + }; + + let number_of_authors: usize = matches.value_of("authors-number").unwrap().parse()?; + + let ignored_directories = + if let Some(user_ignored_directories) = matches.values_of("exclude") { + user_ignored_directories.map(String::from).collect() + } else { + Vec::new() + }; + + let bot_regex_pattern = matches.is_present("no-bots").then(|| { + matches + .value_of("no-bots") + .map_or(Regex::from_str(r"\[bot\]").unwrap(), |s| { + Regex::from_str(s).unwrap() + }) + }); + + let language_types: Vec = matches + .values_of("type") + .unwrap() + .map(|t| LanguageType::from_str(t).unwrap()) + .collect(); + + let completion = matches + .value_of("completion") + .map(Shell::from_str) + .transpose() + .map_err(anyhow::Error::msg) + .context("Could not parse shell")?; + + Ok(Config { + repo_path, + ascii_input, + ascii_language, + ascii_colors, + disabled_fields, + no_bold, + image, + image_backend, + image_color_resolution, + no_merges, + no_color_palette, + number_of_authors, + ignored_directories, + bot_regex_pattern, + print_languages, + print_package_managers, + output, + true_color, + art_off, + text_colors, + iso_time, + show_email, + include_hidden, + language_types, + completion, + }) + } +} + +pub fn print_supported_languages() -> Result<()> { + for l in Language::iter() { + println!("{}", l); + } + + Ok(()) +} + +pub fn print_supported_package_managers() -> Result<()> { + for p in PackageManager::iter() { + println!("{}", p); + } + + Ok(()) +} + +pub fn is_truecolor_terminal() -> bool { + env::var("COLORTERM") + .map(|colorterm| colorterm == "truecolor" || colorterm == "24bit") + .unwrap_or(false) +} + +pub fn get_git_version() -> String { + let version = Command::new("git").arg("--version").output(); + + match version { + Ok(v) => String::from_utf8_lossy(&v.stdout).replace('\n', ""), + Err(_) => String::new(), + } +} + +pub fn print_completions(gen: G) { + let mut cmd = build_cli(); + let name = cmd.get_name().to_string(); + generate(gen, &mut cmd, name, &mut io::stdout()); +} + +pub fn build_cli() -> clap::Command<'static> { + let possible_backends = ["kitty", "iterm", "sixel"]; + + let color_values = &[ + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", + ]; + + clap::Command::new(crate_name!()) .version(crate_version!()) .about(crate_description!()) .setting(AppSettings::DeriveDisplayOrder) @@ -58,6 +250,7 @@ impl Config { .default_value(".") .hide_default_value(true) .help("Run as if onefetch was started in instead of the current working directory.") + .value_hint(ValueHint::DirPath) ) .arg( Arg::new("output") @@ -67,7 +260,7 @@ impl Config { .help("Outputs Onefetch in a specific format (json, yaml).") .takes_value(true) .possible_values( - &SerializationFormat::iter() + SerializationFormat::iter() .map(|format| format.into()) .collect::>()) ) @@ -102,7 +295,8 @@ impl Config { .long("image") .value_name("IMAGE") .takes_value(true) - .help("Path to the IMAGE file."), + .help("Path to the IMAGE file.") + .value_hint(ValueHint::FilePath) ) .arg( Arg::new("image-backend") @@ -110,7 +304,7 @@ impl Config { .value_name("BACKEND") .takes_value(true) .requires("image") - .possible_values(&possible_backends) + .possible_values(possible_backends) .help("Which image BACKEND to use."), ) .arg( @@ -131,7 +325,7 @@ impl Config { .ignore_case(true) .help("Which LANGUAGE's ascii art to print.") .possible_values( - &Language::iter() + Language::iter() .map(|language| language.into()) .collect::>()) ) @@ -238,7 +432,7 @@ impl Config { .ignore_case(true) .help("Allows you to disable FIELD(s) from appearing in the output.") .possible_values( - &InfoField::iter() + InfoField::iter() .map(|field| field.into()) .collect::>()) ) @@ -275,7 +469,8 @@ impl Config { .value_name("EXCLUDE") .multiple_values(true) .takes_value(true) - .help("Ignore all files & directories matching EXCLUDE."), + .help("Ignore all files & directories matching EXCLUDE.") + .value_hint(ValueHint::AnyPath) ) .arg( Arg::new("type") @@ -289,179 +484,15 @@ impl Config { .hide_default_value(true) .help("Filters output by language type (*programming*, *markup*, prose, data).") .possible_values( - &LanguageType::iter() + LanguageType::iter() .map(|t| t.into()) .collect::>()) ) - .get_matches(); - - let true_color = match matches.value_of("true-color") { - Some("always") => true, - Some("never") => false, - Some("auto") => is_truecolor_terminal(), - _ => unreachable!(), - }; - - let no_bold = matches.is_present("no-bold"); - let no_merges = matches.is_present("no-merges"); - let no_color_palette = matches.is_present("no-palette"); - let print_languages = matches.is_present("languages"); - let print_package_managers = matches.is_present("package-managers"); - let iso_time = matches.is_present("isotime"); - let show_email = matches.is_present("email"); - let include_hidden = matches.is_present("hidden"); - - let output = matches - .value_of("output") - .map(SerializationFormat::from_str) - .transpose()?; - - let fields_to_hide: Vec = if let Some(values) = matches.values_of("disable-fields") - { - values.map(String::from).collect() - } else { - Vec::new() - }; - - let disabled_fields = InfoFieldOff::new(fields_to_hide)?; - - let art_off = match matches.value_of("show-logo") { - Some("always") => false, - Some("never") => true, - Some("auto") => { - if let Some((width, _)) = term_size::dimensions_stdout() { - width < MAX_TERM_WIDTH - } else { - false - } - } - _ => unreachable!(), - }; - - let image = if let Some(image_path) = matches.value_of("image") { - Some(image::open(image_path).with_context(|| "Could not load the specified image")?) - } else { - None - }; - - let image_backend = if image.is_some() { - if let Some(backend_name) = matches.value_of("image-backend") { - image_backends::get_image_backend(backend_name) - } else { - image_backends::get_best_backend() - } - } else { - None - }; - - let image_color_resolution = if let Some(value) = matches.value_of("color-resolution") { - usize::from_str(value)? - } else { - 16 - }; - - let repo_path = matches - .value_of("input") - .map(String::from) - .with_context(|| "Failed to parse input directory")?; - - let ascii_input = matches.value_of("ascii-input").map(String::from); - - let ascii_language = matches - .value_of("ascii-language") - .map(|ascii_language| Language::from_str(&ascii_language.to_lowercase()).unwrap()); - - let ascii_colors = if let Some(values) = matches.values_of("ascii-colors") { - values.map(String::from).collect() - } else { - Vec::new() - }; - - let text_colors = if let Some(values) = matches.values_of("text-colors") { - values.map(String::from).collect() - } else { - Vec::new() - }; - - let number_of_authors: usize = matches.value_of("authors-number").unwrap().parse()?; - - let ignored_directories = - if let Some(user_ignored_directories) = matches.values_of("exclude") { - user_ignored_directories.map(String::from).collect() - } else { - Vec::new() - }; - - let bot_regex_pattern = matches.is_present("no-bots").then(|| { - matches - .value_of("no-bots") - .map_or(Regex::from_str(r"\[bot\]").unwrap(), |s| { - Regex::from_str(s).unwrap() - }) - }); - - let language_types: Vec = matches - .values_of("type") - .unwrap() - .map(|t| LanguageType::from_str(t).unwrap()) - .collect(); - - Ok(Config { - repo_path, - ascii_input, - ascii_language, - ascii_colors, - disabled_fields, - no_bold, - image, - image_backend, - image_color_resolution, - no_merges, - no_color_palette, - number_of_authors, - ignored_directories, - bot_regex_pattern, - print_languages, - print_package_managers, - output, - true_color, - art_off, - text_colors, - iso_time, - show_email, - include_hidden, - language_types, - }) - } -} - -pub fn print_supported_languages() -> Result<()> { - for l in Language::iter() { - println!("{}", l); - } - - Ok(()) -} - -pub fn print_supported_package_managers() -> Result<()> { - for p in PackageManager::iter() { - println!("{}", p); - } - - Ok(()) -} - -pub fn is_truecolor_terminal() -> bool { - env::var("COLORTERM") - .map(|colorterm| colorterm == "truecolor" || colorterm == "24bit") - .unwrap_or(false) -} - -pub fn get_git_version() -> String { - let version = Command::new("git").arg("--version").output(); - - match version { - Ok(v) => String::from_utf8_lossy(&v.stdout).replace('\n', ""), - Err(_) => String::new(), - } + .arg( + Arg::new("completion") + .long("completion") + .possible_values(Shell::possible_values()) + .value_name("SHELL") + .help("Prints out SHELL completion script.") + ) } diff --git a/src/main.rs b/src/main.rs index b9b1ac333..e2c73ae15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,11 @@ fn main() -> Result<()> { return cli::print_supported_package_managers(); } + if let Some(generator) = config.completion { + cli::print_completions(generator); + return Ok(()); + } + if !repo::is_valid(&config.repo_path)? { bail!("please run onefetch inside of a non-bare git repository"); }