From 3078feeceb865f68dfa071480b33beef537d1b3f Mon Sep 17 00:00:00 2001 From: amelenty Date: Sat, 1 Oct 2022 22:14:04 +0200 Subject: [PATCH 1/6] add (de)serialization support for Config --- src/cli.rs | 46 ++++++++++++++++++++++++++++++++++-- src/info/info_field.rs | 4 +++- src/info/langs/language.rs | 1 - src/info/langs/language.tera | 5 ++-- src/ui/image_backends/mod.rs | 3 ++- src/ui/printer.rs | 3 ++- 6 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index a472a540d..76d1f471b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,13 +8,16 @@ use clap::AppSettings; use clap::{Command, Parser, ValueHint}; use clap_complete::{generate, Generator, Shell}; use regex::Regex; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::de::{self, Visitor}; use std::env; use std::io; +use std::fmt; use std::path::PathBuf; use std::str::FromStr; use strum::IntoEnumIterator; -#[derive(Clone, Debug, Parser, PartialEq, Eq)] +#[derive(Clone, Debug, Parser, PartialEq, Eq, Deserialize, Serialize)] #[clap(version, about, long_about = None, rename_all = "kebab-case")] #[clap(global_setting(AppSettings::DeriveDisplayOrder))] pub struct Config { @@ -146,6 +149,7 @@ pub struct Config { pub r#type: Vec, /// If provided, outputs the completion file for given SHELL #[clap(long = "generate", value_name = "SHELL", arg_enum)] + #[serde(skip)] pub completion: Option, } @@ -184,7 +188,7 @@ pub fn print_completions(gen: G, cmd: &mut Command) { generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); } -#[derive(clap::ValueEnum, Clone, PartialEq, Eq, Debug)] +#[derive(clap::ValueEnum, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub enum When { Auto, Never, @@ -303,3 +307,41 @@ impl FromStr for MyRegex { Ok(MyRegex(Regex::new(s)?)) } } + +impl Serialize for MyRegex { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.0.as_str()) + } +} + +struct MyRegexVisitor; + +impl<'de> Visitor<'de> for MyRegexVisitor { + type Value = MyRegex; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a str representation of a regex") + } + + fn visit_str(self, s: &str) -> Result + where + E: de::Error, + { + match MyRegex::from_str(s) { + Ok(regex) => Ok(regex), + Err(error) => Err(de::Error::custom(error)) + } + } +} + +impl<'de> Deserialize<'de> for MyRegex { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(MyRegexVisitor) + } +} diff --git a/src/info/info_field.rs b/src/info/info_field.rs index 54e4d3482..975cca30d 100644 --- a/src/info/info_field.rs +++ b/src/info/info_field.rs @@ -1,3 +1,5 @@ +use serde::{Deserialize, Serialize}; + pub trait InfoField { const TYPE: InfoType; fn value(&self) -> String; @@ -11,7 +13,7 @@ pub trait InfoField { } } -#[derive(Clone, clap::ValueEnum, Debug, Eq, PartialEq)] +#[derive(Clone, clap::ValueEnum, Debug, Eq, PartialEq, Deserialize, Serialize)] pub enum InfoType { Title, Project, diff --git a/src/info/langs/language.rs b/src/info/langs/language.rs index 6bc3f74fa..382772ada 100644 --- a/src/info/langs/language.rs +++ b/src/info/langs/language.rs @@ -1,6 +1,5 @@ use crate::info::info_field::{InfoField, InfoType}; use owo_colors::OwoColorize; -use serde::Serialize; include!(concat!(env!("OUT_DIR"), "/language.rs")); diff --git a/src/info/langs/language.tera b/src/info/langs/language.tera index 7ba0561ee..440502d78 100644 --- a/src/info/langs/language.tera +++ b/src/info/langs/language.tera @@ -2,6 +2,7 @@ use owo_colors::{ AnsiColors, DynColors::{self, Ansi, Rgb}, }; +use serde::{Deserialize, Serialize}; use std::fmt; use std::fmt::Write; use strum::EnumIter; @@ -11,7 +12,7 @@ pub struct Colors { true_colors: Option>, } -#[derive(Clone, PartialEq, Eq, Debug, clap::ValueEnum)] +#[derive(Clone, PartialEq, Eq, Debug, clap::ValueEnum, Deserialize, Serialize)] pub enum LanguageType { Programming, Markup, @@ -19,7 +20,7 @@ pub enum LanguageType { Data, } -#[derive(Clone, Copy, PartialEq, Eq, Hash, EnumIter, clap::ValueEnum, Debug, Serialize)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, EnumIter, clap::ValueEnum, Debug, Deserialize, Serialize)] #[allow(clippy::upper_case_acronyms)] #[clap(rename_all = "lowercase")] pub enum Language { diff --git a/src/ui/image_backends/mod.rs b/src/ui/image_backends/mod.rs index 789cd3fe4..f868ef914 100644 --- a/src/ui/image_backends/mod.rs +++ b/src/ui/image_backends/mod.rs @@ -1,7 +1,8 @@ use anyhow::Result; use image::DynamicImage; +use serde::{Deserialize, Serialize}; -#[derive(clap::ValueEnum, Clone, PartialEq, Eq, Debug)] +#[derive(clap::ValueEnum, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub enum ImageProtocol { Kitty, Sixel, diff --git a/src/ui/printer.rs b/src/ui/printer.rs index 44eba18cc..0083fea46 100644 --- a/src/ui/printer.rs +++ b/src/ui/printer.rs @@ -6,6 +6,7 @@ use crate::ui::image_backends::ImageBackend; use crate::ui::Language; use anyhow::{Context, Result}; use image::DynamicImage; +use serde::{Deserialize, Serialize}; use std::fmt::Write as _; use std::io::Write; use terminal_size::{terminal_size, Width}; @@ -13,7 +14,7 @@ use terminal_size::{terminal_size, Width}; const CENTER_PAD_LENGTH: usize = 3; const MAX_TERM_WIDTH: u16 = 95; -#[derive(Clone, clap::ValueEnum, PartialEq, Eq, Debug)] +#[derive(Clone, clap::ValueEnum, PartialEq, Eq, Debug, Deserialize, Serialize)] pub enum SerializationFormat { Json, Yaml, From 975a7b7f65aec9a6bdd80ccabc3f85ed9ee956da Mon Sep 17 00:00:00 2001 From: amelenty Date: Sun, 2 Oct 2022 09:05:16 +0200 Subject: [PATCH 2/6] implement Default for Config --- src/cli.rs | 64 +++++++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 76d1f471b..44929bab8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -153,6 +153,36 @@ pub struct Config { pub completion: Option, } +impl Default for Config { + fn default() -> Self { Config { + input: PathBuf::from("."), + ascii_input: Default::default(), + ascii_language: Default::default(), + ascii_colors: Default::default(), + disabled_fields: Default::default(), + image: Default::default(), + image_protocol: Default::default(), + color_resolution: 16, + no_bold: Default::default(), + no_merges: Default::default(), + no_color_palette: Default::default(), + number_of_authors: 3, + exclude: Default::default(), + no_bots: Default::default(), + languages: Default::default(), + package_managers: Default::default(), + output: Default::default(), + true_color: When::Auto, + show_logo: When::Always, + text_colors: Default::default(), + iso_time: Default::default(), + email: Default::default(), + include_hidden: Default::default(), + r#type: vec![LanguageType::Programming, LanguageType::Markup], + completion: Default::default(), + } } +} + pub fn print_supported_languages() -> Result<()> { for l in Language::iter() { println!("{}", l); @@ -201,13 +231,13 @@ mod test { #[test] fn test_default_config() { - let config = get_default_config(); + let config = Config::default(); assert_eq!(config, Config::parse_from(&["onefetch"])) } #[test] fn test_custom_config() { - let mut config = get_default_config(); + let mut config = Config::default(); config.number_of_authors = 4; config.input = PathBuf::from("/tmp/folder"); config.no_merges = true; @@ -257,36 +287,6 @@ mod test { fn test_config_with_text_colors_but_out_of_bounds() { assert!(Config::try_parse_from(&["onefetch", "--text-colors", "17"]).is_err()) } - - fn get_default_config() -> Config { - Config { - input: PathBuf::from("."), - ascii_input: Default::default(), - ascii_language: Default::default(), - ascii_colors: Default::default(), - disabled_fields: Default::default(), - image: Default::default(), - image_protocol: Default::default(), - color_resolution: 16, - no_bold: Default::default(), - no_merges: Default::default(), - no_color_palette: Default::default(), - number_of_authors: 3, - exclude: Default::default(), - no_bots: Default::default(), - languages: Default::default(), - package_managers: Default::default(), - output: Default::default(), - true_color: When::Auto, - show_logo: When::Always, - text_colors: Default::default(), - iso_time: Default::default(), - email: Default::default(), - include_hidden: Default::default(), - r#type: vec![LanguageType::Programming, LanguageType::Markup], - completion: Default::default(), - } - } } #[derive(Clone, Debug)] From db01c690c9bac2fd837808e56cce016212b385b6 Mon Sep 17 00:00:00 2001 From: amelenty Date: Sun, 2 Oct 2022 09:06:01 +0200 Subject: [PATCH 3/6] add *.swp to .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 31419fd17..65ee752b5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,6 @@ *.snap /parts /prime -.gitignore.swp +*.swp .DS_Store result From 236d47be64b4ba2b5178d6eec0eaec5eb0075e48 Mon Sep 17 00:00:00 2001 From: amelenty Date: Mon, 3 Oct 2022 10:56:17 +0200 Subject: [PATCH 4/6] add merge strategy for Config --- Cargo.toml | 1 + src/cli.rs | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9c3115564..3f4cfb353 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ git-repository = { version = "0.23.1", default-features = false, features = [ ] } git2 = { version = "0.15.0", default-features = false } image = "0.24.3" +merge = "0.1.0" owo-colors = "3.5.0" regex = "1.6.0" serde = "1.0.144" diff --git a/src/cli.rs b/src/cli.rs index 44929bab8..4c2ee1efa 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,6 +7,7 @@ use anyhow::Result; use clap::AppSettings; use clap::{Command, Parser, ValueHint}; use clap_complete::{generate, Generator, Shell}; +use merge::Merge; use regex::Regex; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::{self, Visitor}; @@ -17,12 +18,13 @@ use std::path::PathBuf; use std::str::FromStr; use strum::IntoEnumIterator; -#[derive(Clone, Debug, Parser, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Clone, Debug, Parser, PartialEq, Eq, Deserialize, Serialize, Merge)] #[clap(version, about, long_about = None, rename_all = "kebab-case")] #[clap(global_setting(AppSettings::DeriveDisplayOrder))] pub struct Config { /// Run as if onefetch was started in instead of the current working directory #[clap(default_value = ".", hide_default_value = true, value_hint = ValueHint::DirPath)] + #[merge(skip)] pub input: PathBuf, /// Takes a non-empty STRING as input to replace the ASCII logo /// @@ -50,6 +52,7 @@ pub struct Config { short = 'c', value_parser = clap::value_parser!(u8).range(..16), )] + #[merge(strategy = overwrite_vector)] pub ascii_colors: Vec, /// Allows you to disable FIELD(s) from appearing in the output #[clap( @@ -60,6 +63,7 @@ pub struct Config { arg_enum, value_name = "FIELD" )] + #[merge(strategy = overwrite_vector)] pub disabled_fields: Vec, /// Path to the IMAGE file #[clap(long, short, value_hint = ValueHint::FilePath)] @@ -75,30 +79,40 @@ pub struct Config { default_value_t = 16usize, possible_values = ["16", "32", "64", "128", "256"], )] + #[merge(strategy = overwrite)] pub color_resolution: usize, /// Turns off bold formatting #[clap(long)] + #[merge(strategy = merge::bool::overwrite_false)] pub no_bold: bool, /// Ignores merge commits #[clap(long)] + #[merge(strategy = merge::bool::overwrite_false)] pub no_merges: bool, /// Hides the color palette #[clap(long)] + #[merge(strategy = merge::bool::overwrite_false)] pub no_color_palette: bool, /// NUM of authors to be shown #[clap(long, short, default_value_t = 3usize, value_name = "NUM")] + #[merge(strategy = overwrite)] pub number_of_authors: usize, /// gnore all files & directories matching EXCLUDE #[clap(long, multiple_values = true, short, value_hint = ValueHint::AnyPath)] + #[merge(strategy = overwrite_vector)] pub exclude: Vec, /// Exclude [bot] commits. Use to override the default pattern #[clap(long, value_name = "REGEX")] pub no_bots: Option>, /// Prints out supported languages #[clap(long, short)] + #[serde(skip)] + #[merge(strategy = merge::bool::overwrite_false)] pub languages: bool, /// Prints out supported package managers #[clap(long, short)] + #[serde(skip)] + #[merge(strategy = merge::bool::overwrite_false)] pub package_managers: bool, /// Outputs Onefetch in a specific format #[clap(long, short, value_name = "FORMAT", arg_enum)] @@ -107,11 +121,13 @@ pub struct Config { /// /// If set to auto: true color will be enabled if supported by the terminal #[clap(long, default_value = "auto", value_name = "WHEN", arg_enum)] + #[merge(strategy = overwrite)] pub true_color: When, /// Specify when to show the logo /// /// If set to auto: the logo will be hidden if the terminal's width < 95 #[clap(long, default_value = "always", value_name = "WHEN", arg_enum)] + #[merge(strategy = overwrite)] pub show_logo: When, /// Changes the text colors (X X X...) /// @@ -128,15 +144,19 @@ pub struct Config { value_parser = clap::value_parser!(u8).range(..16), max_values = 6 )] + #[merge(strategy = overwrite_vector)] pub text_colors: Vec, /// Use ISO 8601 formatted timestamps #[clap(long, short = 'z')] + #[merge(strategy = merge::bool::overwrite_false)] pub iso_time: bool, /// Show the email address of each author #[clap(long, short = 'E')] + #[merge(strategy = merge::bool::overwrite_false)] pub email: bool, /// Count hidden files and directories #[clap(long)] + #[merge(strategy = merge::bool::overwrite_false)] pub include_hidden: bool, /// Filters output by language type #[clap( @@ -146,7 +166,13 @@ pub struct Config { short = 'T', arg_enum, )] + #[merge(strategy = overwrite_vector)] pub r#type: Vec, + /// Specify a custom path to a config file. + /// Default config is located at ${HOME}/.config/onefetch/config.conf. + #[clap(long, value_hint = ValueHint::AnyPath)] + #[merge(skip)] + pub config_path: Option, /// If provided, outputs the completion file for given SHELL #[clap(long = "generate", value_name = "SHELL", arg_enum)] #[serde(skip)] @@ -179,10 +205,20 @@ impl Default for Config { email: Default::default(), include_hidden: Default::default(), r#type: vec![LanguageType::Programming, LanguageType::Markup], + config_path: Default::default(), completion: Default::default(), } } } +fn overwrite(left: &mut T, right: T) { + *left = right; +} + +fn overwrite_vector(left: &mut Vec, mut right: Vec) { + left.clear(); + left.append(&mut right); +} + pub fn print_supported_languages() -> Result<()> { for l in Language::iter() { println!("{}", l); From 75153cf1aedeb97a663f02366584d2f561726950 Mon Sep 17 00:00:00 2001 From: amelenty Date: Mon, 3 Oct 2022 10:56:49 +0200 Subject: [PATCH 5/6] add Config (de)serialization --- Cargo.toml | 1 + src/configuration.rs | 47 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 3 ++- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/configuration.rs diff --git a/Cargo.toml b/Cargo.toml index 3f4cfb353..855824193 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ bytecount = "0.6.3" clap = { version = "3.2.22", features = ["derive"] } clap_complete = "3.2.5" color_quant = "1.1.0" +dirs = "4.0.0" git-features-for-configuration-only = { package = "git-features", version = "0.22.4", features = ["zlib-ng-compat"] } git-repository = { version = "0.23.1", default-features = false, features = [ "max-performance-safe", diff --git a/src/configuration.rs b/src/configuration.rs new file mode 100644 index 000000000..ffa72c3e2 --- /dev/null +++ b/src/configuration.rs @@ -0,0 +1,47 @@ +use anyhow::{anyhow, Result}; +use crate::cli::Config; +use dirs::config_dir; +use merge::Merge; +use std::fs; +use std::{fs::File, path::Path}; +use std::io::{BufReader, Read}; + +fn read_config_file>(path: &P) -> Result { + let f = File::open(&path)?; + let mut buf_reader = BufReader::new(f); + let mut contents = String::new(); + buf_reader.read_to_string(&mut contents)?; + Ok(toml::from_str(contents.as_str()).unwrap()) +} + +fn load_config>(path: Option<&P>) -> Result { + match path { + Some(config_path) => read_config_file(config_path), + None => { + let mut default_path = config_dir().unwrap(); + default_path.push("onefetch.toml"); + println!("Default config file path: {}", &default_path.display()); + if default_path.exists() { + read_config_file(&default_path) + } else { + write_default_config(&default_path); + Err(anyhow!("Default config file did not exist: {:?}", default_path)) + } + } + } +} + +fn write_default_config>(default_path: &P) { + let toml = toml::to_string(&Config::default()).expect("Config should be serializable"); + fs::write(default_path, toml).expect("Should be able to write to config dir"); +} + +pub fn populate_config(cli_config: Config) -> Config { + match load_config(cli_config.config_path.as_ref()) { + Ok(mut disk_config) => { + disk_config.merge(cli_config); + disk_config + }, + Err(_) => cli_config + } +} diff --git a/src/main.rs b/src/main.rs index a40db59d7..3441dfbe0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ fn main() -> Result<()> { #[cfg(windows)] let _ = enable_ansi_support::enable_ansi_support(); - let config = Config::parse(); + let config = configuration::populate_config(Config::parse()); if config.languages { return cli::print_supported_languages(); @@ -36,5 +36,6 @@ fn main() -> Result<()> { } mod cli; +mod configuration; mod info; mod ui; From 8ac99cf67d44128cf635e815ce539dce81cfbfe6 Mon Sep 17 00:00:00 2001 From: amelenty Date: Mon, 3 Oct 2022 10:59:05 +0200 Subject: [PATCH 6/6] update Cargo.lock --- Cargo.lock | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a7daabdf0..e605cdeea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1578,6 +1578,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "merge" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9" +dependencies = [ + "merge_derive", + "num-traits", +] + +[[package]] +name = "merge_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1695,6 +1717,7 @@ dependencies = [ "clap 3.2.22", "clap_complete", "color_quant", + "dirs 4.0.0", "enable-ansi-support", "git-features", "git-repository", @@ -1702,6 +1725,7 @@ dependencies = [ "image", "lazy_static", "libc", + "merge", "owo-colors", "regex", "serde",