diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 000000000..93bd629b8 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,108 @@ +use { + crate::info::InfoFields, + clap::{App, AppSettings, Arg}, + strum::{EnumCount, IntoEnumIterator}, +}; + +pub fn build_app() -> App<'static, 'static> { + #[cfg(target_os = "linux")] + let possible_backends = ["kitty", "sixel"]; + #[cfg(not(target_os = "linux"))] + let possible_backends = []; + + App::new(crate_name!()) + .version(crate_version!()) + .about(crate_description!()) + .setting(AppSettings::ColoredHelp) + .setting(AppSettings::DeriveDisplayOrder) + .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) + .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), + ) + .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."), + ) +} diff --git a/src/exit_codes.rs b/src/exit_codes.rs new file mode 100644 index 000000000..9c85d9ce8 --- /dev/null +++ b/src/exit_codes.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ExitCode { + Success, + GeneralError, +} + +impl Into for ExitCode { + fn into(self) -> i32 { + match self { + ExitCode::Success => 0, + ExitCode::GeneralError => 1, + } + } +} diff --git a/src/info.rs b/src/info.rs index 9558fd10d..f45e7931c 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,15 +1,15 @@ use { crate::{ - image_backends::ImageBackend, language::Language, license::Detector, - {AsciiArt, CommitInfo, Error, InfoFieldOn}, + options::Options, + {AsciiArt, CommitInfo, Error}, }, colored::{Color, ColoredString, Colorize}, git2::Repository, - image::DynamicImage, regex::Regex, std::{ffi::OsStr, fmt::Write, fs}, + strum::{EnumCount, EnumIter, EnumString, IntoStaticStr}, tokio::process::Command, }; @@ -17,6 +17,44 @@ type Result = std::result::Result; const LICENSE_FILES: [&str; 3] = ["LICENSE", "LICENCE", "COPYING"]; +#[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 struct Info { git_version: String, git_username: String, @@ -36,13 +74,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 +84,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 +108,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 +138,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 +146,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 +154,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 +162,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 +170,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 +205,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 +235,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 +243,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 +251,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 +259,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 +267,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 +275,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 +283,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 +301,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 +315,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 +345,12 @@ 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)?; + pub async fn new(config: Options) -> Result { + let repo = Repository::discover(&config.path).map_err(|_| Error::NotGitRepo)?; let workdir = repo.workdir().ok_or(Error::BareGitRepo)?; 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 +365,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 +378,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 +400,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, }) } @@ -737,20 +752,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 +774,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; } @@ -796,7 +811,7 @@ 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 { + if self.config.no_bold { formatted_label = formatted_label.bold(); } formatted_label diff --git a/src/language.rs b/src/language.rs index 4b187b407..5c54b60a8 100644 --- a/src/language.rs +++ b/src/language.rs @@ -181,7 +181,7 @@ 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 = @@ -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/main.rs b/src/main.rs index 4d38aefe6..abe153820 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,70 +5,30 @@ extern crate clap; use image_backends::ImageBackend; use { ascii_art::AsciiArt, - clap::{App, Arg}, - colored::*, commit_info::CommitInfo, error::Error, - info::Info, + exit_codes::ExitCode, + info::{Info, InfoFieldOn, InfoFields}, language::Language, - std::{ - convert::From, - process::{Command, Stdio}, - result, - str::FromStr, - }, - strum::{EnumCount, EnumIter, EnumString, IntoEnumIterator, IntoStaticStr}, + process::{Command, Stdio}, + std::{convert::From, env, process, result, str::FromStr}, + strum::IntoEnumIterator, }; +mod app; mod ascii_art; mod commit_info; mod error; +mod exit_codes; mod image_backends; mod info; mod language; mod license; +mod options; 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<()> { +fn run() -> Result<()> { #[cfg(target_os = "windows")] let enabled = ansi_term::enable_ansi_support().is_ok(); @@ -83,131 +43,10 @@ fn main() -> Result<()> { return Err(Error::GitNotInstalled); } - let possible_languages: Vec = Language::iter() - .filter(|language| *language != Language::Unknown) - .map(|language| language.to_string().to_lowercase()) - .collect(); + let matches = app::build_app().get_matches_from(env::args_os()); - #[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!()) - .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 ignored_directories: Vec<&str> = if let Some(user_ignored) = matches.values_of("exclude") { - user_ignored.map(|s| s as &str).collect() + let excluded: Vec = if let Some(user_ignored) = matches.values_of("exclude") { + user_ignored.map(String::from).collect() } else { Vec::new() }; @@ -221,14 +60,15 @@ fn main() -> Result<()> { std::process::exit(0); } - let dir = String::from(matches.value_of("input").unwrap()); + let path = String::from(matches.value_of("input").unwrap()); - let custom_logo: Language = if let Some(ascii_language) = matches.value_of("ascii-language") { + let ascii_language: 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 { + let mut disabled_fields = InfoFieldOn { ..Default::default() }; @@ -243,39 +83,39 @@ fn main() -> Result<()> { .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, + 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, _ => (), } } - let custom_colors: Vec = if let Some(values) = matches.values_of("ascii-colors") { + let ascii_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 no_bold = !matches.is_present("no-bold"); - let custom_image = if let Some(image_path) = matches.value_of("image") { + let 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() { + let image_backend = if image.is_some() { if let Some(backend_name) = matches.value_of("image-backend") { #[cfg(target_os = "linux")] let backend = @@ -298,32 +138,46 @@ fn main() -> Result<()> { let no_merges = matches.is_present("no-merge-commits"); - let color_blocks_flag = matches.is_present("no-color-blocks"); + let no_color_blocks = matches.is_present("no-color-blocks"); - let author_number: usize = if let Some(value) = matches.value_of("authors-number") { + let number_of_authors: 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, + let config = options::Options { + path, + ascii_language, + ascii_colors, + disabled_fields, + no_bold, + image, image_backend, no_merges, - color_blocks_flag, - author_number, - ignored_directories, - )?; + no_color_blocks, + number_of_authors, + excluded, + }; + + let info = Info::new(config)?; print!("{}", info); Ok(()) } +fn main() { + let result = run(); + match result { + Ok(_) => { + process::exit(ExitCode::Success.into()); + } + Err(_) => { + process::exit(ExitCode::GeneralError.into()); + } + } +} + fn is_git_installed() -> bool { Command::new("git") .arg("--version") diff --git a/src/options.rs b/src/options.rs new file mode 100644 index 000000000..480f80a1e --- /dev/null +++ b/src/options.rs @@ -0,0 +1,30 @@ +use { + crate::{image_backends::ImageBackend, info::InfoFieldOn, language::Language}, + image::DynamicImage, +}; + +/// Configuration options for *onefetch*. +pub struct Options { + /// + pub path: String, + /// + pub ascii_language: Language, + /// + pub ascii_colors: Vec, + /// + pub disabled_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, +}