diff --git a/Cargo.lock b/Cargo.lock index 948b7779b..7e4297a54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -470,6 +470,16 @@ dependencies = [ "termcolor", ] +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check", +] + [[package]] name = "failure" version = "0.1.8" @@ -1150,6 +1160,7 @@ dependencies = [ "bytecount", "clap", "colored", + "error-chain", "futures", "git2", "image", @@ -1931,6 +1942,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + [[package]] name = "walkdir" version = "2.3.1" diff --git a/Cargo.toml b/Cargo.toml index 6ea2f6973..3187a3338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ image = "0.23.10" regex = "1" futures = "0.3.6" tokio = { version = "0.2.22", features = ["full"] } +error-chain = "0.12" [target.'cfg(windows)'.dependencies] ansi_term = "0.12" diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index de371e923..000000000 --- a/src/error.rs +++ /dev/null @@ -1,38 +0,0 @@ -/// Custom error type -pub enum Error { - /// Sourcecode could be located - SourceCodeNotFound, - /// Git is not installed or did not function properly - GitNotInstalled, - /// Did not find any git data in the directory - NoGitData, - /// An IO error occoured while reading ./ - ReadDirectory, - /// Not in a Git Repo - NotGitRepo, - /// Error while getting branch info - BareGitRepo, - /// Repository is a bare git repo - ReferenceInfoError, - /// Image probably doesn't exist or has wrong format - ImageLoadError, - /// Could not initialize the license detector - LicenseDetectorError, -} - -impl std::fmt::Debug for Error { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let content = match self { - Error::SourceCodeNotFound => "Could not find any source code in this directory", - Error::GitNotInstalled => "Git failed to execute", - Error::NoGitData => "Could not retrieve git configuration data", - Error::ReadDirectory => "Could not read directory", - Error::NotGitRepo => "Could not find a valid git repo on the current path", - Error::BareGitRepo => "Unable to run onefetch on bare git repos", - Error::ReferenceInfoError => "Error while retrieving reference information", - Error::ImageLoadError => "Could not load the specified image", - Error::LicenseDetectorError => "Could not initialize the license detector", - }; - write!(f, "{}", content) - } -} diff --git a/src/main.rs b/src/main.rs index 4d38aefe6..c1236a4cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,327 +1,44 @@ -#[macro_use] -extern crate clap; +// `error_chain!` can recurse deeply +#![recursion_limit = "1024"] + +use onefetch::{cli::Options, error::*, info}; -#[cfg(target_os = "linux")] -use image_backends::ImageBackend; use { - ascii_art::AsciiArt, - clap::{App, Arg}, - colored::*, - commit_info::CommitInfo, - error::Error, - info::Info, - language::Language, - std::{ - convert::From, - process::{Command, Stdio}, - result, - str::FromStr, - }, - strum::{EnumCount, EnumIter, EnumString, IntoEnumIterator, IntoStaticStr}, + process::{Command, Stdio}, + std::process, }; -mod ascii_art; -mod commit_info; -mod error; -mod image_backends; -mod info; -mod language; -mod license; - -type Result = result::Result; - -#[derive(Default)] -pub struct InfoFieldOn { - git_info: bool, - project: bool, - head: bool, - version: bool, - created: bool, - languages: bool, - authors: bool, - last_change: bool, - repo: bool, - commits: bool, - pending: bool, - lines_of_code: bool, - size: bool, - license: bool, -} - -#[derive(PartialEq, Eq, EnumString, EnumCount, EnumIter, IntoStaticStr)] -#[strum(serialize_all = "snake_case")] -enum InfoFields { - GitInfo, - Project, - HEAD, - Version, - Created, - Languages, - Authors, - LastChange, - Repo, - Commits, - Pending, - LinesOfCode, - Size, - License, - UnrecognizedField, -} - -fn main() -> Result<()> { - #[cfg(target_os = "windows")] - let enabled = ansi_term::enable_ansi_support().is_ok(); - - #[cfg(not(target_os = "windows"))] - let enabled = true; +mod onefetch; - if enabled { - colored::control::set_override(true); - } +fn run() -> Result<()> { + #[cfg(windows)] + let _ = ansi_term::enable_ansi_support(); if !is_git_installed() { - return Err(Error::GitNotInstalled); + return Err("Git failed to execute!".into()); } - let possible_languages: Vec = Language::iter() - .filter(|language| *language != Language::Unknown) - .map(|language| language.to_string().to_lowercase()) - .collect(); - - #[cfg(target_os = "linux")] - let possible_backends = ["kitty", "sixel"]; - #[cfg(not(target_os = "linux"))] - let possible_backends = []; + // Load command line options. + let options = Options::new()?; - let matches = App::new(crate_name!()) - .version(crate_version!()) - .author("o2sh ") - .about(crate_description!()) - .arg(Arg::with_name("input").default_value(".").help( - "Run as if onefetch was started in instead of the current working directory.", - )) - .arg( - Arg::with_name("ascii-language") - .short("a") - .long("ascii-language") - .takes_value(true) - .possible_values( - &possible_languages - .iter() - .map(|l| l.as_str()) - .collect::>(), - ) - .case_insensitive(true) - .help("Which language's ascii art to print."), - ) - .arg( - Arg::with_name("disable-fields") - .long("disable-fields") - .short("d") - .multiple(true) - .takes_value(true) - .case_insensitive(true) - .help("Allows you to disable an info line from appearing in the output.") - .possible_values( - &InfoFields::iter() - .take(InfoFields::COUNT - 1) - .map(|field| field.into()) - .collect::>() - .as_slice(), - ), - ) - .arg( - Arg::with_name("ascii-colors") - .short("c") - .long("ascii-colors") - .multiple(true) - .takes_value(true) - .possible_values(&[ - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", - "15", - ]) - .hide_possible_values(true) - .help(&format!( - "Colors to print the ascii art. Possible values: [{0}{1}{2}{3}{4}{5}{6}{7}]", - "0".black(), - "1".red(), - "2".green(), - "3".yellow(), - "4".blue(), - "5".magenta(), - "6".cyan(), - "7".white() - )), - ) - .arg( - Arg::with_name("no-bold") - .long("no-bold") - .help("Turns off bold formatting."), - ) - .arg( - Arg::with_name("languages") - .short("l") - .long("languages") - .help("Prints out supported languages"), - ) - .arg( - Arg::with_name("image") - .short("i") - .long("image") - .takes_value(true) - .help("Which image to use. Possible values: [/path/to/img]"), - ) - .arg( - Arg::with_name("image-backend") - .long("image-backend") - .takes_value(true) - .possible_values(&possible_backends) - .help("Which image backend to use."), - ) - .arg( - Arg::with_name("no-merge-commits") - .long("no-merge-commits") - .help("Ignores merge commits"), - ) - .arg( - Arg::with_name("no-color-blocks") - .long("no-color-blocks") - .help("Hides the color blocks"), - ) - .arg( - Arg::with_name("authors-number") - .short("A") - .long("authors-number") - .takes_value(true) - .default_value("3") - .help("Number of authors to be shown."), - ) - .arg( - Arg::with_name("exclude") - .short("e") - .long("exclude") - .multiple(true) - .takes_value(true) - .help("Ignore all files & directories matching the pattern."), - ) - .get_matches(); + let info = info::Info::new(options)?; - let ignored_directories: Vec<&str> = if let Some(user_ignored) = matches.values_of("exclude") { - user_ignored.map(|s| s as &str).collect() - } else { - Vec::new() - }; - - if matches.is_present("languages") { - let iterator = Language::iter().filter(|x| *x != Language::Unknown); + print!("{}", info); + Ok(()) +} - for l in iterator { - println!("{}", l); +fn main() { + let result = run(); + match result { + Ok(_) => { + process::exit(0); } - std::process::exit(0); - } - - let dir = String::from(matches.value_of("input").unwrap()); - - let custom_logo: Language = if let Some(ascii_language) = matches.value_of("ascii-language") { - Language::from_str(&ascii_language.to_lowercase()).unwrap() - } else { - Language::Unknown - }; - let mut disable_fields = InfoFieldOn { - ..Default::default() - }; - - let fields_to_hide: Vec = if let Some(values) = matches.values_of("disable-fields") { - values.map(String::from).collect() - } else { - Vec::new() - }; - - for field in fields_to_hide.iter() { - let item = InfoFields::from_str(field.to_lowercase().as_str()) - .unwrap_or(InfoFields::UnrecognizedField); - - match item { - InfoFields::GitInfo => disable_fields.git_info = true, - InfoFields::Project => disable_fields.project = true, - InfoFields::HEAD => disable_fields.head = true, - InfoFields::Version => disable_fields.version = true, - InfoFields::Created => disable_fields.created = true, - InfoFields::Languages => disable_fields.languages = true, - InfoFields::Authors => disable_fields.authors = true, - InfoFields::LastChange => disable_fields.last_change = true, - InfoFields::Repo => disable_fields.repo = true, - InfoFields::Pending => disable_fields.pending = true, - InfoFields::Commits => disable_fields.commits = true, - InfoFields::LinesOfCode => disable_fields.lines_of_code = true, - InfoFields::Size => disable_fields.size = true, - InfoFields::License => disable_fields.license = true, - _ => (), + Err(error) => { + let stderr = std::io::stderr(); + default_error_handler(&error, &mut stderr.lock()); + process::exit(1); } } - - let custom_colors: Vec = if let Some(values) = matches.values_of("ascii-colors") { - values.map(String::from).collect() - } else { - Vec::new() - }; - - let bold_flag = !matches.is_present("no-bold"); - - let custom_image = if let Some(image_path) = matches.value_of("image") { - Some(image::open(image_path).map_err(|_| Error::ImageLoadError)?) - } else { - None - }; - - let image_backend = if custom_image.is_some() { - if let Some(backend_name) = matches.value_of("image-backend") { - #[cfg(target_os = "linux")] - let backend = - Some(match backend_name { - "kitty" => Box::new(image_backends::kitty::KittyBackend::new()) - as Box, - "sixel" => Box::new(image_backends::sixel::SixelBackend::new()) - as Box, - _ => unreachable!(), - }); - #[cfg(not(target_os = "linux"))] - let backend = None; - backend - } else { - crate::image_backends::get_best_backend() - } - } else { - None - }; - - let no_merges = matches.is_present("no-merge-commits"); - - let color_blocks_flag = matches.is_present("no-color-blocks"); - - let author_number: usize = if let Some(value) = matches.value_of("authors-number") { - usize::from_str(value).unwrap() - } else { - 3 - }; - - let info = Info::new( - &dir, - custom_logo, - custom_colors, - disable_fields, - bold_flag, - custom_image, - image_backend, - no_merges, - color_blocks_flag, - author_number, - ignored_directories, - )?; - - print!("{}", info); - Ok(()) } fn is_git_installed() -> bool { diff --git a/src/ascii_art.rs b/src/onefetch/ascii_art.rs similarity index 100% rename from src/ascii_art.rs rename to src/onefetch/ascii_art.rs diff --git a/src/onefetch/cli.rs b/src/onefetch/cli.rs new file mode 100644 index 000000000..baae01daf --- /dev/null +++ b/src/onefetch/cli.rs @@ -0,0 +1,194 @@ +use { + crate::onefetch::{ + error::*, image_backends, info_fields, info_fields::InfoFields, language::Language, + }, + clap::{crate_description, crate_name, crate_version, App, AppSettings, Arg}, + image::DynamicImage, + std::{convert::From, env, str::FromStr}, + strum::{EnumCount, IntoEnumIterator}, +}; + +pub struct Options { + pub path: String, + pub ascii_language: Language, + pub ascii_colors: Vec, + pub disabled_fields: info_fields::InfoFieldOn, + pub no_bold: bool, + pub image: Option, + pub image_backend: Option>, + pub no_merges: bool, + pub no_color_blocks: bool, + pub number_of_authors: usize, + pub excluded: Vec, +} + +impl Options { + /// Build `Options` from command line arguments. + pub fn new() -> Result { + #[cfg(target_os = "linux")] + let possible_backends = ["kitty", "sixel"]; + #[cfg(not(target_os = "linux"))] + let possible_backends = []; + + let matches = App::new(crate_name!()) + .version(crate_version!()) + .about(crate_description!()) + .setting(AppSettings::ColoredHelp) + .setting(AppSettings::DeriveDisplayOrder) + .setting(AppSettings::UnifiedHelpMessage) + .setting(AppSettings::HidePossibleValuesInHelp) + .arg(Arg::with_name("input").default_value(".").hide_default_value(true).help( + "Run as if onefetch was started in instead of the current working directory.", + )) + .arg( + Arg::with_name("ascii-language") + .short("a") + .value_name("LANGUAGE") + .long("ascii-language") + .max_values(1) + .takes_value(true) + .case_insensitive(true) + .help("Which LANGUAGE's ascii art to print.") + .possible_values( + &Language::iter() + .filter(|language| *language != Language::Unknown) + .map(|language| language.into()) + .collect::>() + ), + ) + .arg( + Arg::with_name("disable-fields") + .long("disable-fields") + .short("d") + .value_name("FIELD") + .multiple(true) + .takes_value(true) + .case_insensitive(true) + .help("Allows you to disable FIELD(s) from appearing in the output.") + .possible_values( + &InfoFields::iter() + .take(InfoFields::COUNT - 1) + .map(|field| field.into()) + .collect::>() + ), + ) + .arg( + Arg::with_name("ascii-colors") + .short("c") + .long("ascii-colors") + .value_name("X") + .multiple(true) + .takes_value(true) + .possible_values(&[ + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", + "15", + ]) + .help("Colors (X X X...) to print the ascii art."), + ) + .arg( + Arg::with_name("no-bold") + .long("no-bold") + .help("Turns off bold formatting."), + ) + .arg( + Arg::with_name("image") + .short("i") + .long("image") + .value_name("IMAGE") + .takes_value(true) + .max_values(1) + .help("Path to the IMAGE file"), + ) + .arg( + Arg::with_name("image-backend") + .long("image-backend") + .value_name("BACKEND") + .takes_value(true) + .max_values(1) + .possible_values(&possible_backends) + .help("Which image BACKEND to use."), + ) + .arg( + Arg::with_name("no-merge-commits") + .long("no-merge-commits") + .help("Ignores merge commits"), + ) + .arg( + Arg::with_name("no-color-blocks") + .long("no-color-blocks") + .help("Hides the color blocks"), + ) + .arg( + Arg::with_name("authors-number") + .short("A") + .long("authors-number") + .value_name("NUM") + .takes_value(true) + .max_values(1) + .default_value("3") + .help("NUM of authors to be shown."), + ) + .arg( + Arg::with_name("exclude") + .short("e") + .long("exclude") + .value_name("EXCLUDE") + .multiple(true) + .takes_value(true) + .help("Ignore all files & directories matching EXCLUDE."), + ).get_matches(); + + let fields_to_hide: Vec = if let Some(values) = matches.values_of("disable-fields") + { + values.map(String::from).collect() + } else { + Vec::new() + }; + + let image = if let Some(image_path) = matches.value_of("image") { + Some(image::open(image_path).chain_err(|| "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 + }; + + Ok(Options { + path: String::from(matches.value_of("input").unwrap()), + ascii_language: if let Some(ascii_language) = matches.value_of("ascii-language") { + Language::from_str(&ascii_language.to_lowercase()).unwrap() + } else { + Language::Unknown + }, + ascii_colors: if let Some(values) = matches.values_of("ascii-colors") { + values.map(String::from).collect() + } else { + Vec::new() + }, + disabled_fields: info_fields::get_disabled_fields(fields_to_hide)?, + no_bold: matches.is_present("no-bold"), + image, + image_backend, + no_merges: matches.is_present("no-merge-commits"), + no_color_blocks: matches.is_present("no-color-blocks"), + number_of_authors: if let Some(value) = matches.value_of("authors-number") { + usize::from_str(value).unwrap() + } else { + 3 + }, + excluded: if let Some(user_ignored) = matches.values_of("exclude") { + user_ignored.map(String::from).collect() + } else { + Vec::new() + }, + }) + } +} diff --git a/src/commit_info.rs b/src/onefetch/commit_info.rs similarity index 100% rename from src/commit_info.rs rename to src/onefetch/commit_info.rs diff --git a/src/onefetch/error.rs b/src/onefetch/error.rs new file mode 100644 index 000000000..667602cb8 --- /dev/null +++ b/src/onefetch/error.rs @@ -0,0 +1,19 @@ +use colored::Colorize; +use error_chain::error_chain; +use std::io::Write; + +error_chain! { + foreign_links { + Clap(::clap::Error) #[cfg(feature = "application")]; + Io(::std::io::Error); + ParseIntError(::std::num::ParseIntError); + } +} + +pub fn default_error_handler(e: &Error, output: &mut dyn Write) { + writeln!(output, "{}: {}", "[onefetch error]".red(), e).ok(); + + for e in e.iter().skip(1) { + writeln!(output, "caused by: {}", e).ok(); + } +} diff --git a/src/image_backends/kitty.rs b/src/onefetch/image_backends/kitty.rs similarity index 100% rename from src/image_backends/kitty.rs rename to src/onefetch/image_backends/kitty.rs diff --git a/src/image_backends/mod.rs b/src/onefetch/image_backends/mod.rs similarity index 59% rename from src/image_backends/mod.rs rename to src/onefetch/image_backends/mod.rs index 7d769c766..0fb37aa11 100644 --- a/src/image_backends/mod.rs +++ b/src/onefetch/image_backends/mod.rs @@ -20,6 +20,18 @@ pub fn get_best_backend() -> Option> { } } +pub fn get_image_backend(backend_name: &str) -> Option> { + #[cfg(target_os = "linux")] + let backend = Some(match backend_name { + "kitty" => Box::new(kitty::KittyBackend::new()) as Box, + "sixel" => Box::new(sixel::SixelBackend::new()) as Box, + _ => unreachable!(), + }); + #[cfg(not(target_os = "linux"))] + let backend = None; + backend +} + #[cfg(not(target_os = "linux"))] pub fn get_best_backend() -> Option> { None diff --git a/src/image_backends/sixel.rs b/src/onefetch/image_backends/sixel.rs similarity index 100% rename from src/image_backends/sixel.rs rename to src/onefetch/image_backends/sixel.rs diff --git a/src/info.rs b/src/onefetch/info.rs similarity index 87% rename from src/info.rs rename to src/onefetch/info.rs index 9558fd10d..bb1883118 100644 --- a/src/info.rs +++ b/src/onefetch/info.rs @@ -1,22 +1,19 @@ use { - crate::{ - image_backends::ImageBackend, + crate::onefetch::{ + ascii_art::AsciiArt, + cli::Options, + commit_info::CommitInfo, + error::*, language::Language, - license::Detector, - {AsciiArt, CommitInfo, Error, InfoFieldOn}, + license::{Detector, LICENSE_FILES}, }, colored::{Color, ColoredString, Colorize}, git2::Repository, - image::DynamicImage, regex::Regex, std::{ffi::OsStr, fmt::Write, fs}, tokio::process::Command, }; -type Result = std::result::Result; - -const LICENSE_FILES: [&str; 3] = ["LICENSE", "LICENCE", "COPYING"]; - pub struct Info { git_version: String, git_username: String, @@ -36,13 +33,7 @@ pub struct Info { number_of_tags: usize, number_of_branches: usize, license: String, - custom_logo: Language, - custom_colors: Vec, - disable_fields: InfoFieldOn, - bold_enabled: bool, - no_color_blocks: bool, - custom_image: Option, - image_backend: Option>, + config: Options, } impl std::fmt::Display for Info { @@ -52,7 +43,7 @@ impl std::fmt::Display for Info { Some(&c) => c, None => Color::White, }; - if !self.disable_fields.git_info { + if !self.config.disabled_fields.git_info { let git_info_length; if self.git_username != "" { git_info_length = self.git_username.len() + self.git_version.len() + 3; @@ -76,7 +67,7 @@ impl std::fmt::Display for Info { &separator, )?; } - if !self.disable_fields.project { + if !self.config.disabled_fields.project { let branches_str = match self.number_of_branches { 0 => String::new(), 1 => String::from("1 branch"), @@ -106,7 +97,7 @@ impl std::fmt::Display for Info { )?; } - if !self.disable_fields.head { + if !self.config.disabled_fields.head { write_buf( &mut buf, &self.get_formatted_info_label("HEAD: ", color), @@ -114,7 +105,7 @@ impl std::fmt::Display for Info { )?; } - if !self.disable_fields.pending && self.pending != "" { + if !self.config.disabled_fields.pending && self.pending != "" { write_buf( &mut buf, &self.get_formatted_info_label("Pending: ", color), @@ -122,7 +113,7 @@ impl std::fmt::Display for Info { )?; } - if !self.disable_fields.version { + if !self.config.disabled_fields.version { write_buf( &mut buf, &self.get_formatted_info_label("Version: ", color), @@ -130,7 +121,7 @@ impl std::fmt::Display for Info { )?; } - if !self.disable_fields.created { + if !self.config.disabled_fields.created { write_buf( &mut buf, &self.get_formatted_info_label("Created: ", color), @@ -138,7 +129,7 @@ impl std::fmt::Display for Info { )?; } - if !self.disable_fields.languages && !self.languages.is_empty() { + if !self.config.disabled_fields.languages && !self.languages.is_empty() { if self.languages.len() > 1 { let title = "Languages: "; let pad = " ".repeat(title.len()); @@ -173,7 +164,7 @@ impl std::fmt::Display for Info { }; } - if !self.disable_fields.authors && !self.authors.is_empty() { + if !self.config.disabled_fields.authors && !self.authors.is_empty() { let title = if self.authors.len() > 1 { "Authors: " } else { @@ -203,7 +194,7 @@ impl std::fmt::Display for Info { } } - if !self.disable_fields.last_change { + if !self.config.disabled_fields.last_change { write_buf( &mut buf, &self.get_formatted_info_label("Last change: ", color), @@ -211,7 +202,7 @@ impl std::fmt::Display for Info { )?; } - if !self.disable_fields.repo { + if !self.config.disabled_fields.repo { write_buf( &mut buf, &self.get_formatted_info_label("Repo: ", color), @@ -219,7 +210,7 @@ impl std::fmt::Display for Info { )?; } - if !self.disable_fields.commits { + if !self.config.disabled_fields.commits { write_buf( &mut buf, &self.get_formatted_info_label("Commits: ", color), @@ -227,7 +218,7 @@ impl std::fmt::Display for Info { )?; } - if !self.disable_fields.lines_of_code { + if !self.config.disabled_fields.lines_of_code { write_buf( &mut buf, &self.get_formatted_info_label("Lines of code: ", color), @@ -235,7 +226,7 @@ impl std::fmt::Display for Info { )?; } - if !self.disable_fields.size { + if !self.config.disabled_fields.size { write_buf( &mut buf, &self.get_formatted_info_label("Size: ", color), @@ -243,7 +234,7 @@ impl std::fmt::Display for Info { )?; } - if !self.disable_fields.license { + if !self.config.disabled_fields.license { write_buf( &mut buf, &self.get_formatted_info_label("License: ", color), @@ -251,7 +242,7 @@ impl std::fmt::Display for Info { )?; } - if !self.no_color_blocks { + if !self.config.no_color_blocks { writeln!( buf, "\n{0}{1}{2}{3}{4}{5}{6}{7}", @@ -269,8 +260,8 @@ impl std::fmt::Display for Info { let center_pad = " "; let mut info_lines = buf.lines(); - if let Some(custom_image) = &self.custom_image { - if let Some(image_backend) = &self.image_backend { + if let Some(custom_image) = &self.config.image { + if let Some(image_backend) = &self.config.image_backend { writeln!( f, "{}", @@ -283,7 +274,8 @@ impl std::fmt::Display for Info { panic!("No image backend found") } } else { - let mut logo_lines = AsciiArt::new(self.get_ascii(), self.colors(), self.bold_enabled); + let mut logo_lines = + AsciiArt::new(self.get_ascii(), self.colors(), !self.config.no_bold); loop { match (logo_lines.next(), info_lines.next()) { (Some(logo_line), Some(info_line)) => { @@ -312,24 +304,15 @@ impl std::fmt::Display for Info { impl Info { #[tokio::main] - pub async fn new( - dir: &str, - logo: Language, - colors: Vec, - disabled: InfoFieldOn, - bold_flag: bool, - custom_image: Option, - image_backend: Option>, - no_merges: bool, - color_blocks_flag: bool, - author_nb: usize, - ignored_directories: Vec<&str>, - ) -> Result { - let repo = Repository::discover(&dir).map_err(|_| Error::NotGitRepo)?; - let workdir = repo.workdir().ok_or(Error::BareGitRepo)?; + pub async fn new(config: Options) -> Result { + let repo = Repository::discover(&config.path) + .chain_err(|| "Could not find a valid git repo on the current path")?; + let workdir = repo + .workdir() + .chain_err(|| "Unable to run onefetch on bare git repo")?; let workdir_str = workdir.to_str().unwrap(); let (languages_stats, number_of_lines) = - Language::get_language_stats(workdir_str, ignored_directories)?; + Language::get_language_stats(workdir_str, &config.excluded)?; let ( (repository_name, repository_url), @@ -344,7 +327,7 @@ impl Info { dominant_language, ) = futures::join!( Info::get_repo_name_and_url(&repo), - Info::get_git_history(workdir_str, no_merges), + Info::get_git_history(workdir_str, config.no_merges), Info::get_number_of_tags_branches(workdir_str), Info::get_current_commit_info(&repo), Info::get_git_version_and_username(workdir_str), @@ -357,7 +340,7 @@ impl Info { let creation_date = Info::get_creation_date(&git_history); let number_of_commits = Info::get_number_of_commits(&git_history); - let authors = Info::get_authors(&git_history, author_nb); + let authors = Info::get_authors(&git_history, config.number_of_authors); let last_change = Info::get_date_of_last_commit(&git_history); Ok(Info { @@ -379,13 +362,7 @@ impl Info { number_of_tags, number_of_branches, license: project_license?, - custom_logo: logo, - custom_colors: colors, - disable_fields: disabled, - bold_enabled: bold_flag, - no_color_blocks: color_blocks_flag, - custom_image, - image_backend, + config, }) } @@ -440,7 +417,9 @@ impl Info { } async fn get_repo_name_and_url(repo: &Repository) -> (String, String) { - let config = repo.config().map_err(|_| Error::NoGitData); + let config = repo + .config() + .chain_err(|| "Could not retrieve git configuration data"); let mut remote_url = String::new(); let mut repository_name = String::new(); @@ -473,9 +452,15 @@ impl Info { } async fn get_current_commit_info(repo: &Repository) -> Result { - let head = repo.head().map_err(|_| Error::ReferenceInfoError)?; - let head_oid = head.target().ok_or(Error::ReferenceInfoError)?; - let refs = repo.references().map_err(|_| Error::ReferenceInfoError)?; + let head = repo + .head() + .chain_err(|| "Error while retrieving reference information")?; + let head_oid = head + .target() + .ok_or("Error while retrieving reference information")?; + let refs = repo + .references() + .chain_err(|| "Error while retrieving reference information")?; let refs_info = refs .filter_map(|reference| match reference { Ok(reference) => match (reference.target(), reference.shorthand()) { @@ -708,7 +693,7 @@ impl Info { let detector = Detector::new()?; let mut output = fs::read_dir(dir) - .map_err(|_| Error::ReadDirectory)? + .chain_err(|| "Could not read directory")? .filter_map(std::result::Result::ok) .map(|entry| entry.path()) .filter(|entry| { @@ -737,20 +722,20 @@ impl Info { } fn get_ascii(&self) -> &str { - let language = if let Language::Unknown = self.custom_logo { + let language = if let Language::Unknown = self.config.ascii_language { &self.dominant_language } else { - &self.custom_logo + &self.config.ascii_language }; language.get_ascii_art() } fn colors(&self) -> Vec { - let language = if let Language::Unknown = self.custom_logo { + let language = if let Language::Unknown = self.config.ascii_language { &self.dominant_language } else { - &self.custom_logo + &self.config.ascii_language }; let colors = language.get_colors(); @@ -759,7 +744,7 @@ impl Info { .iter() .enumerate() .map(|(index, default_color)| { - if let Some(color_num) = self.custom_colors.get(index) { + if let Some(color_num) = self.config.ascii_colors.get(index) { if let Some(color) = Info::num_to_color(color_num) { return color; } @@ -795,11 +780,12 @@ impl Info { /// Returns a formatted info label with the desired color and boldness fn get_formatted_info_label(&self, label: &str, color: Color) -> ColoredString { - let mut formatted_label = label.color(color); - if self.bold_enabled { - formatted_label = formatted_label.bold(); + let formatted_label = label.color(color); + if self.config.no_bold { + formatted_label + } else { + formatted_label.bold() } - formatted_label } } diff --git a/src/onefetch/info_fields.rs b/src/onefetch/info_fields.rs new file mode 100644 index 000000000..2005e3ff0 --- /dev/null +++ b/src/onefetch/info_fields.rs @@ -0,0 +1,74 @@ +use { + crate::onefetch::error::*, + std::str::FromStr, + strum::{EnumCount, EnumIter, EnumString, IntoStaticStr}, +}; + +#[derive(Default)] +pub struct InfoFieldOn { + pub git_info: bool, + pub project: bool, + pub head: bool, + pub version: bool, + pub created: bool, + pub languages: bool, + pub authors: bool, + pub last_change: bool, + pub repo: bool, + pub commits: bool, + pub pending: bool, + pub lines_of_code: bool, + pub size: bool, + pub license: bool, +} + +#[derive(PartialEq, Eq, EnumString, EnumCount, EnumIter, IntoStaticStr)] +#[strum(serialize_all = "snake_case")] +pub enum InfoFields { + GitInfo, + Project, + HEAD, + Version, + Created, + Languages, + Authors, + LastChange, + Repo, + Commits, + Pending, + LinesOfCode, + Size, + License, + UnrecognizedField, +} + +pub fn get_disabled_fields(fields_to_hide: Vec) -> Result { + let mut disabled_fields = InfoFieldOn { + ..Default::default() + }; + + for field in fields_to_hide.iter() { + let item = InfoFields::from_str(field.to_lowercase().as_str()) + .unwrap_or(InfoFields::UnrecognizedField); + + match item { + InfoFields::GitInfo => disabled_fields.git_info = true, + InfoFields::Project => disabled_fields.project = true, + InfoFields::HEAD => disabled_fields.head = true, + InfoFields::Version => disabled_fields.version = true, + InfoFields::Created => disabled_fields.created = true, + InfoFields::Languages => disabled_fields.languages = true, + InfoFields::Authors => disabled_fields.authors = true, + InfoFields::LastChange => disabled_fields.last_change = true, + InfoFields::Repo => disabled_fields.repo = true, + InfoFields::Pending => disabled_fields.pending = true, + InfoFields::Commits => disabled_fields.commits = true, + InfoFields::LinesOfCode => disabled_fields.lines_of_code = true, + InfoFields::Size => disabled_fields.size = true, + InfoFields::License => disabled_fields.license = true, + _ => (), + } + } + + Ok(disabled_fields) +} diff --git a/src/language.rs b/src/onefetch/language.rs similarity index 91% rename from src/language.rs rename to src/onefetch/language.rs index 4b187b407..c9b32a9e8 100644 --- a/src/language.rs +++ b/src/onefetch/language.rs @@ -1,16 +1,16 @@ use { - crate::{Error, Result}, + crate::onefetch::error::*, colored::Color, regex::Regex, std::collections::HashMap, - strum::{EnumIter, EnumString}, + strum::{EnumIter, EnumString, IntoStaticStr}, }; macro_rules! define_languages { ($( { $name:ident, $ascii:literal, $display:literal, $colors:expr $(, $serialize:literal )? } ),* ,) => { #[strum(serialize_all = "lowercase")] - #[derive(PartialEq, Eq, Hash, Clone, EnumString, EnumIter)] + #[derive(PartialEq, Eq, Hash, Clone, EnumString, EnumIter, IntoStaticStr)] pub enum Language { $( $( #[strum(serialize = $serialize)] )? @@ -40,8 +40,8 @@ macro_rules! define_languages { impl Language { pub fn get_ascii_art(&self) -> &str { match *self { - $( Language::$name => include_str!(concat!("../resources/", $ascii)), )* - Language::Unknown => include_str!("../resources/unknown.ascii"), + $( Language::$name => include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/resources/", $ascii)), )* + Language::Unknown => include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/resources/unknown.ascii")), } } @@ -78,7 +78,7 @@ macro_rules! define_languages { #[test] #[ignore] fn [<$name:lower _width>] () { - const ASCII: &str = include_str!(concat!("../resources/", $ascii)); + const ASCII: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/resources/", $ascii)); for (line_number, line) in ASCII.lines().enumerate() { let line = COLOR_INTERPOLATION.replace_all(line, ""); @@ -91,7 +91,7 @@ macro_rules! define_languages { #[test] #[ignore] fn [<$name:lower _height>] () { - const ASCII: &str = include_str!(concat!("../resources/", $ascii)); + const ASCII: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/resources/", $ascii)); assert_le!(ASCII.lines().count(), MAX_HEIGHT, concat!($ascii, " is too tall.")); } } @@ -181,11 +181,11 @@ impl Language { pub fn get_language_stats( dir: &str, - ignored_directories: Vec<&str>, + ignored_directories: &[String], ) -> Result<(Vec<(Language, f64)>, usize)> { let tokei_langs = project_languages(&dir, ignored_directories); - let languages_stat = - Language::get_languages_stat(&tokei_langs).ok_or(Error::SourceCodeNotFound)?; + let languages_stat = Language::get_languages_stat(&tokei_langs) + .ok_or("Could not find any source code in this directory")?; let mut stat_vec: Vec<(_, _)> = languages_stat.into_iter().collect(); stat_vec.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap().reverse()); let loc = get_total_loc(&tokei_langs); @@ -205,7 +205,7 @@ fn get_total_loc(languages: &tokei::Languages) -> usize { .fold(0, |sum, val| sum + val.code) } -fn project_languages(dir: &str, ignored_directories: Vec<&str>) -> tokei::Languages { +fn project_languages(dir: &str, ignored_directories: &[String]) -> tokei::Languages { use tokei::Config; let mut languages = tokei::Languages::new(); @@ -219,7 +219,7 @@ fn project_languages(dir: &str, ignored_directories: Vec<&str>) -> tokei::Langua let re = Regex::new(r"((.*)+/)+(.*)").unwrap(); let mut v = Vec::with_capacity(ignored_directories.len()); for ignored in ignored_directories { - if re.is_match(ignored) { + if re.is_match(&ignored) { let p = if ignored.starts_with('/') { "**" } else { @@ -233,7 +233,8 @@ fn project_languages(dir: &str, ignored_directories: Vec<&str>) -> tokei::Langua let ignored_directories_for_ab: Vec<&str> = v.iter().map(|x| &**x).collect(); languages.get_statistics(&[&dir], &ignored_directories_for_ab, &tokei_config); } else { - languages.get_statistics(&[&dir], &ignored_directories, &tokei_config); + let ignored_directories_ref: Vec<&str> = ignored_directories.iter().map(|s| &**s).collect(); + languages.get_statistics(&[&dir], &ignored_directories_ref, &tokei_config); } languages diff --git a/src/license.rs b/src/onefetch/license.rs similarity index 62% rename from src/license.rs rename to src/onefetch/license.rs index 4ac712c95..98870457c 100644 --- a/src/license.rs +++ b/src/onefetch/license.rs @@ -1,10 +1,13 @@ use askalono::{Store, TextData}; -use crate::Error; +use crate::onefetch::error::*; -type Result = std::result::Result; +pub const LICENSE_FILES: [&str; 3] = ["LICENSE", "LICENCE", "COPYING"]; -static CACHE_DATA: &[u8] = include_bytes!("../resources/licenses/cache.bin.zstd"); +static CACHE_DATA: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/resources/licenses/cache.bin.zstd" +)); const MIN_THRESHOLD: f32 = 0.8; pub struct Detector { @@ -15,7 +18,7 @@ impl Detector { pub fn new() -> Result { Store::from_cache(CACHE_DATA) .map(|store| Self { store }) - .map_err(|_| Error::LicenseDetectorError) + .map_err(|_| "Could not initialize the license detector".into()) } pub fn analyze(&self, text: &str) -> Option { diff --git a/src/onefetch/mod.rs b/src/onefetch/mod.rs new file mode 100644 index 000000000..8ac63b957 --- /dev/null +++ b/src/onefetch/mod.rs @@ -0,0 +1,9 @@ +pub mod ascii_art; +pub mod cli; +pub mod commit_info; +pub mod error; +pub mod image_backends; +pub mod info; +pub mod info_fields; +pub mod language; +pub mod license;