Skip to content
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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
*.snap
/parts
/prime
.gitignore.swp
*.swp
.DS_Store
result
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ 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",
"unstable",
] }
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"
Expand Down
146 changes: 112 additions & 34 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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(
Expand All @@ -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)]
Expand All @@ -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)]
Expand All @@ -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...)
///
Expand All @@ -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(
Expand All @@ -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.
Copy link
Collaborator

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.

#[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);
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A wise choice to move this into a Default implementation 👍

}

#[derive(Clone, Debug)]
Expand All @@ -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)
}
}
47 changes: 47 additions & 0 deletions src/configuration.rs
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, maybe these should just be unwraps to show the underlying error. For example, fs::write might fail if there's no more space on the disk. If that happens, the user should probably see that, and only that.

}

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
}
}
Loading