-
Notifications
You must be signed in to change notification settings - Fork 279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Config file for theming/customization #790
base: main
Are you sure you want to change the base?
Changes from all commits
3078fee
975a7b7
db01c69
236d47b
75153cf
8ac99cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,6 @@ | |
*.snap | ||
/parts | ||
/prime | ||
.gitignore.swp | ||
*.swp | ||
.DS_Store | ||
result |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,19 +7,24 @@ 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}; | ||
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, 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 <input> 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 | ||
/// | ||
|
@@ -47,6 +52,7 @@ pub struct Config { | |
short = 'c', | ||
value_parser = clap::value_parser!(u8).range(..16), | ||
)] | ||
#[merge(strategy = overwrite_vector)] | ||
pub ascii_colors: Vec<u8>, | ||
/// Allows you to disable FIELD(s) from appearing in the output | ||
#[clap( | ||
|
@@ -57,6 +63,7 @@ pub struct Config { | |
arg_enum, | ||
value_name = "FIELD" | ||
)] | ||
#[merge(strategy = overwrite_vector)] | ||
pub disabled_fields: Vec<InfoType>, | ||
/// Path to the IMAGE file | ||
#[clap(long, short, value_hint = ValueHint::FilePath)] | ||
|
@@ -72,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<PathBuf>, | ||
/// Exclude [bot] commits. Use <REGEX> to override the default pattern | ||
#[clap(long, value_name = "REGEX")] | ||
pub no_bots: Option<Option<MyRegex>>, | ||
/// 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)] | ||
|
@@ -104,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...) | ||
/// | ||
|
@@ -125,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<u8>, | ||
/// 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( | ||
|
@@ -143,12 +166,59 @@ pub struct Config { | |
short = 'T', | ||
arg_enum, | ||
)] | ||
#[merge(strategy = overwrite_vector)] | ||
pub r#type: Vec<LanguageType>, | ||
/// 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<PathBuf>, | ||
/// If provided, outputs the completion file for given SHELL | ||
#[clap(long = "generate", value_name = "SHELL", arg_enum)] | ||
#[serde(skip)] | ||
pub completion: Option<Shell>, | ||
} | ||
|
||
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], | ||
config_path: Default::default(), | ||
completion: Default::default(), | ||
} } | ||
} | ||
|
||
fn overwrite<T>(left: &mut T, right: T) { | ||
*left = right; | ||
} | ||
|
||
fn overwrite_vector<T>(left: &mut Vec<T>, mut right: Vec<T>) { | ||
left.clear(); | ||
left.append(&mut right); | ||
} | ||
|
||
pub fn print_supported_languages() -> Result<()> { | ||
for l in Language::iter() { | ||
println!("{}", l); | ||
|
@@ -184,7 +254,7 @@ pub fn print_completions<G: Generator>(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, | ||
|
@@ -197,13 +267,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; | ||
|
@@ -253,36 +323,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(), | ||
} | ||
} | ||
Comment on lines
-257
to
-285
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A wise choice to move this into a |
||
} | ||
|
||
#[derive(Clone, Debug)] | ||
|
@@ -303,3 +343,41 @@ impl FromStr for MyRegex { | |
Ok(MyRegex(Regex::new(s)?)) | ||
} | ||
} | ||
|
||
impl Serialize for MyRegex { | ||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||
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<E>(self, s: &str) -> Result<Self::Value, E> | ||
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<D>(deserializer: D) -> Result<MyRegex, D::Error> | ||
where | ||
D: Deserializer<'de>, | ||
{ | ||
deserializer.deserialize_str(MyRegexVisitor) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<P: AsRef<Path>>(path: &P) -> Result<Config> { | ||
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<P: AsRef<Path>>(path: Option<&P>) -> Result<Config> { | ||
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<P: AsRef<Path>>(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"); | ||
Comment on lines
+35
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly, maybe these should just be |
||
} | ||
|
||
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 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just an FYI that this is only the default path on Linux if you're using
dirs
, AFAIK.