Parse config
Co-authored-by: Chiew Yan Wei <chiewyanwei@gmail.com> Co-authored-by: Tan Chee Keong <tanck2005@gmail.com>
This commit is contained in:
parent
a0856aa4f5
commit
86e442b8a4
11 changed files with 491 additions and 54 deletions
|
|
@ -14,10 +14,15 @@ default-run = "hyfetch"
|
|||
anyhow = { workspace = true, features = ["std"] }
|
||||
bpaf = { workspace = true, features = [] }
|
||||
chrono = { workspace = true, features = ["clock", "std"] }
|
||||
deranged = { workspace = true, features = ["serde", "std"] }
|
||||
# derive_more = { workspace = true, features = ["std"] }
|
||||
indexmap = { workspace = true, features = ["std"] }
|
||||
directories = { workspace = true, features = [] }
|
||||
indexmap = { workspace = true, features = ["serde", "std"] }
|
||||
regex = { workspace = true, features = ["perf", "std", "unicode"] }
|
||||
rgb = { workspace = true, features = [] }
|
||||
serde = { workspace = true, features = ["derive", "std"] }
|
||||
serde_json = { workspace = true, features = ["std"] }
|
||||
serde_path_to_error = { workspace = true, features = [] }
|
||||
shell-words = { workspace = true, features = ["std"] }
|
||||
strum = { workspace = true, features = ["derive", "std"] }
|
||||
tracing = { workspace = true, features = ["attributes", "std"] }
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
use std::io::{self, IsTerminal};
|
||||
use std::fmt;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, ErrorKind, IsTerminal, Read};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::Datelike;
|
||||
use directories::ProjectDirs;
|
||||
use hyfetch::cli_options::options;
|
||||
use hyfetch::models::Config;
|
||||
use hyfetch::neofetch_util::get_distro_ascii;
|
||||
use tracing::debug;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let options = options().fallback_to_usage().run();
|
||||
let options = options().run();
|
||||
|
||||
init_tracing_subsriber(options.debug).context("Failed to init tracing subscriber")?;
|
||||
|
||||
|
|
@ -25,25 +30,26 @@ fn main() -> Result<()> {
|
|||
|
||||
// TODO
|
||||
|
||||
// TODO
|
||||
// let config = if options.config {
|
||||
// create_config()
|
||||
// } else {
|
||||
// check_config(options.config_file)
|
||||
// };
|
||||
let config = if options.config {
|
||||
create_config(options.config_file).context("Failed to create config")?
|
||||
} else if let Some(config) =
|
||||
read_config(&options.config_file).context("Failed to read config")?
|
||||
{
|
||||
config
|
||||
} else {
|
||||
create_config(options.config_file).context("Failed to create config")?
|
||||
};
|
||||
|
||||
let now = chrono::Local::now();
|
||||
let show_pride_month = options.june
|
||||
|| now.month() == 6
|
||||
// TODO
|
||||
// && !config.pride_month_shown.contains(now.year())
|
||||
// && !june_path.is_file()
|
||||
&& io::stdout().is_terminal();
|
||||
let cache_path = ProjectDirs::from("", "", "hyfetch")
|
||||
.context("Failed to get base dirs")?
|
||||
.cache_dir()
|
||||
.to_owned();
|
||||
let june_path = cache_path.join(format!("animation-displayed-{}", now.year()));
|
||||
let show_pride_month =
|
||||
options.june || now.month() == 6 && !june_path.is_file() && io::stdout().is_terminal();
|
||||
|
||||
if show_pride_month
|
||||
// TODO
|
||||
// && !config.pride_month_disable
|
||||
{
|
||||
if show_pride_month && !config.pride_month_disable {
|
||||
// TODO
|
||||
// pride_month.start_animation();
|
||||
println!();
|
||||
|
|
@ -51,7 +57,11 @@ fn main() -> Result<()> {
|
|||
println!("(You can always view the animation again with `hyfetch --june`)");
|
||||
println!();
|
||||
|
||||
// TODO
|
||||
if !june_path.is_file() {
|
||||
fs::create_dir_all(cache_path).context("Failed to create cache dir")?;
|
||||
File::create(&june_path)
|
||||
.with_context(|| format!("Failed to create file {june_path:?}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
|
@ -59,6 +69,51 @@ fn main() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads config from file.
|
||||
///
|
||||
/// Returns `None` if the config file does not exist.
|
||||
#[tracing::instrument(level = "debug")]
|
||||
fn read_config<P>(path: P) -> Result<Option<Config>>
|
||||
where
|
||||
P: AsRef<Path> + fmt::Debug,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
|
||||
let mut file = match File::open(path) {
|
||||
Ok(file) => file,
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => {
|
||||
return Ok(None);
|
||||
},
|
||||
Err(err) => {
|
||||
return Err(err).with_context(|| format!("Failed to open {path:?}"));
|
||||
},
|
||||
};
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
file.read_to_string(&mut buf)
|
||||
.with_context(|| format!("Failed to read {path:?}"))?;
|
||||
|
||||
let deserializer = &mut serde_json::Deserializer::from_str(&buf);
|
||||
let config: Config = serde_path_to_error::deserialize(deserializer)
|
||||
.with_context(|| format!("Failed to parse {path:?}"))?;
|
||||
|
||||
debug!(?config, "read config");
|
||||
|
||||
Ok(Some(config))
|
||||
}
|
||||
|
||||
/// Creates config interactively.
|
||||
///
|
||||
/// The config is automatically stored to file.
|
||||
#[tracing::instrument(level = "debug")]
|
||||
fn create_config<P>(path: P) -> Result<Config>
|
||||
where
|
||||
P: AsRef<Path> + fmt::Debug,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn init_tracing_subsriber(debug: bool) -> Result<()> {
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use anyhow::Context;
|
|||
#[cfg(feature = "autocomplete")]
|
||||
use bpaf::ShellComp;
|
||||
use bpaf::{construct, long, OptionParser, Parser};
|
||||
use directories::BaseDirs;
|
||||
use strum::VariantNames;
|
||||
|
||||
use crate::presets::Preset;
|
||||
|
|
@ -13,11 +14,11 @@ use crate::types::{AnsiMode, Backend};
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct Options {
|
||||
pub config: bool,
|
||||
pub config_file: Option<PathBuf>,
|
||||
pub config_file: PathBuf,
|
||||
pub preset: Option<Preset>,
|
||||
pub mode: Option<AnsiMode>,
|
||||
pub backend: Option<Backend>,
|
||||
pub backend_args: Vec<String>,
|
||||
pub args: Vec<String>,
|
||||
pub colors_scale: Option<f32>,
|
||||
pub colors_set_lightness: Option<f32>,
|
||||
pub colors_use_overlay: bool,
|
||||
|
|
@ -37,7 +38,16 @@ pub fn options() -> OptionParser<Options> {
|
|||
.argument("CONFIG_FILE");
|
||||
#[cfg(feature = "autocomplete")]
|
||||
let config_file = config_file.complete_shell(ShellComp::Nothing);
|
||||
let config_file = config_file.optional();
|
||||
let config_file = config_file
|
||||
.fallback_with(|| {
|
||||
Ok::<_, anyhow::Error>(
|
||||
BaseDirs::new()
|
||||
.context("Failed to get base dirs")?
|
||||
.config_dir()
|
||||
.join("hyfetch.json"),
|
||||
)
|
||||
})
|
||||
.debug_fallback();
|
||||
let preset = long("preset")
|
||||
.short('p')
|
||||
.help(&*format!(
|
||||
|
|
@ -49,7 +59,11 @@ PRESET={{{}}}",
|
|||
#[cfg(feature = "autocomplete")]
|
||||
let preset = preset.complete(complete_preset);
|
||||
let preset = preset
|
||||
.parse(|s| Preset::from_str(&s).with_context(|| format!("Failed to parse preset `{s}`")))
|
||||
.parse(|s| {
|
||||
Preset::from_str(&s).with_context(|| {
|
||||
format!("PRESET should be one of {{{}}}", Preset::VARIANTS.join(","))
|
||||
})
|
||||
})
|
||||
.optional();
|
||||
let mode = long("mode")
|
||||
.short('m')
|
||||
|
|
@ -62,7 +76,11 @@ MODE={{{}}}",
|
|||
#[cfg(feature = "autocomplete")]
|
||||
let mode = mode.complete(complete_mode);
|
||||
let mode = mode
|
||||
.parse(|s| AnsiMode::from_str(&s).with_context(|| format!("Failed to parse mode `{s}`")))
|
||||
.parse(|s| {
|
||||
AnsiMode::from_str(&s).with_context(|| {
|
||||
format!("MODE should be one of {{{}}}", AnsiMode::VARIANTS.join(","))
|
||||
})
|
||||
})
|
||||
.optional();
|
||||
let backend = long("backend")
|
||||
.short('b')
|
||||
|
|
@ -75,12 +93,19 @@ BACKEND={{{}}}",
|
|||
#[cfg(feature = "autocomplete")]
|
||||
let backend = backend.complete(complete_backend);
|
||||
let backend = backend
|
||||
.parse(|s| Backend::from_str(&s).with_context(|| format!("Failed to parse backend `{s}`")))
|
||||
.parse(|s| {
|
||||
Backend::from_str(&s).with_context(|| {
|
||||
format!(
|
||||
"BACKEND should be one of {{{}}}",
|
||||
Backend::VARIANTS.join(",")
|
||||
)
|
||||
})
|
||||
})
|
||||
.optional();
|
||||
let backend_args = long("args")
|
||||
let args = long("args")
|
||||
.help("Additional arguments pass-through to backend")
|
||||
.argument::<String>("ARGS")
|
||||
.parse(|s| shell_words::split(&s).context("Failed to split args for shell"))
|
||||
.parse(|s| shell_words::split(&s).context("ARGS should be valid command-line arguments"))
|
||||
.fallback(vec![]);
|
||||
let colors_scale = long("c-scale")
|
||||
.help("Lighten colors by a multiplier")
|
||||
|
|
@ -125,7 +150,7 @@ BACKEND={{{}}}",
|
|||
preset,
|
||||
mode,
|
||||
backend,
|
||||
backend_args,
|
||||
args,
|
||||
colors_scale,
|
||||
colors_set_lightness,
|
||||
colors_use_overlay,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,22 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use deranged::RangedU8;
|
||||
use rgb::RGB8;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An indexed color where the color palette is the set of colors used in
|
||||
/// neofetch ascii art.
|
||||
///
|
||||
/// The range of valid values as supported in neofetch is `1`-`6`.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
|
||||
pub struct NeofetchAsciiIndexedColor(RangedU8<1, 6>);
|
||||
|
||||
/// An indexed color where the color palette is the set of unique colors in a
|
||||
/// preset.
|
||||
///
|
||||
/// The range of valid values depends on the number of unique colors in a
|
||||
/// certain preset.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
|
||||
pub struct PresetIndexedColor(usize);
|
||||
|
||||
pub trait FromHex {
|
||||
/// Creates color from hex code.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
pub mod cli_options;
|
||||
pub mod color_util;
|
||||
pub mod distros;
|
||||
pub mod models;
|
||||
pub mod neofetch_util;
|
||||
pub mod presets;
|
||||
pub mod types;
|
||||
|
|
|
|||
71
crates/hyfetch/src/models.rs
Normal file
71
crates/hyfetch/src/models.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::neofetch_util::ColorAlignment;
|
||||
use crate::presets::Preset;
|
||||
use crate::types::{AnsiMode, Backend, LightDark};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub preset: Preset,
|
||||
pub mode: AnsiMode,
|
||||
pub light_dark: LightDark,
|
||||
pub lightness: Option<f32>,
|
||||
pub color_align: ColorAlignment,
|
||||
pub backend: Backend,
|
||||
#[serde(with = "self::args_serde_with")]
|
||||
pub args: Vec<String>,
|
||||
pub distro: Option<String>,
|
||||
pub pride_month_disable: bool,
|
||||
}
|
||||
|
||||
mod args_serde_with {
|
||||
use std::fmt;
|
||||
|
||||
use serde::de::{self, value, Deserialize, Deserializer, SeqAccess, Visitor};
|
||||
use serde::ser::Serializer;
|
||||
|
||||
pub(super) fn serialize<S>(value: &Vec<String>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&shell_words::join(value))
|
||||
}
|
||||
|
||||
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct StringOrVec;
|
||||
|
||||
impl<'de> Visitor<'de> for StringOrVec {
|
||||
type Value = Vec<String>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("string or list of strings")
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
shell_words::split(s).map_err(de::Error::custom)
|
||||
}
|
||||
|
||||
fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
|
||||
where
|
||||
S: SeqAccess<'de>,
|
||||
{
|
||||
Deserialize::deserialize(value::SeqAccessDeserializer::new(seq))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(StringOrVec)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,14 +4,35 @@ use std::ffi::OsStr;
|
|||
use std::os::unix::process::ExitStatusExt as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::OnceLock;
|
||||
use std::{env, fmt};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use indexmap::IndexMap;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::color_util::{NeofetchAsciiIndexedColor, PresetIndexedColor};
|
||||
use crate::distros::Distro;
|
||||
|
||||
static NEOFETCH_COLOR_RE: OnceLock<Regex> = OnceLock::new();
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase", tag = "mode")]
|
||||
pub enum ColorAlignment {
|
||||
Horizontal {
|
||||
fore_back: Option<(NeofetchAsciiIndexedColor, NeofetchAsciiIndexedColor)>,
|
||||
},
|
||||
Vertical {
|
||||
fore_back: Option<(NeofetchAsciiIndexedColor, NeofetchAsciiIndexedColor)>,
|
||||
},
|
||||
Custom {
|
||||
#[serde(rename = "custom_colors")]
|
||||
colors: IndexMap<NeofetchAsciiIndexedColor, PresetIndexedColor>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Gets the absolute path of the neofetch command.
|
||||
pub fn get_command_path() -> Result<PathBuf> {
|
||||
if let Ok(workspace_dir) = env::var("CARGO_WORKSPACE_DIR") {
|
||||
|
|
@ -72,13 +93,16 @@ where
|
|||
}
|
||||
|
||||
/// Gets distro ascii width and height, ignoring color code.
|
||||
pub fn ascii_size<S>(asc: S, neofetch_color_re: &Regex) -> (u8, u8)
|
||||
pub fn ascii_size<S>(asc: S) -> (u8, u8)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let asc = asc.as_ref();
|
||||
|
||||
let Some(width) = neofetch_color_re
|
||||
let Some(width) = NEOFETCH_COLOR_RE
|
||||
.get_or_init(|| {
|
||||
Regex::new(r"\$\{c[0-9]\}").expect("neofetch color regex should not be invalid")
|
||||
})
|
||||
.replace_all(asc, "")
|
||||
.split('\n')
|
||||
.map(|line| line.len())
|
||||
|
|
@ -98,14 +122,11 @@ where
|
|||
{
|
||||
let asc = asc.as_ref();
|
||||
|
||||
let neofetch_color_re =
|
||||
Regex::new(r"\$\{c[0-9]\}").expect("neofetch color regex should not be invalid");
|
||||
let (w, _) = ascii_size(asc);
|
||||
|
||||
let (w, _) = ascii_size(asc, &neofetch_color_re);
|
||||
|
||||
let mut buf = "".to_owned();
|
||||
let mut buf = String::new();
|
||||
for line in asc.split('\n') {
|
||||
let (line_w, _) = ascii_size(line, &neofetch_color_re);
|
||||
let (line_w, _) = ascii_size(line);
|
||||
let pad = " ".repeat((w - line_w) as usize);
|
||||
buf.push_str(&format!("{line}{pad}\n"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ use std::iter;
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use indexmap::IndexSet;
|
||||
use rgb::RGB8;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumString, VariantNames};
|
||||
|
||||
use crate::color_util::FromHex;
|
||||
|
||||
#[derive(Clone, Hash, Debug, EnumString, VariantNames)]
|
||||
#[derive(Clone, Hash, Debug, Deserialize, EnumString, Serialize, VariantNames)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum Preset {
|
||||
Abrosexual,
|
||||
|
|
@ -44,13 +46,16 @@ pub enum Preset {
|
|||
Genderfaun,
|
||||
Genderfluid,
|
||||
Genderflux,
|
||||
#[serde(rename = "gendernonconforming1")]
|
||||
#[strum(serialize = "gendernonconforming1")]
|
||||
GenderNonconforming1,
|
||||
#[serde(rename = "gendernonconforming2")]
|
||||
#[strum(serialize = "gendernonconforming2")]
|
||||
GenderNonconforming2,
|
||||
Gendervoid,
|
||||
Girlflux,
|
||||
Greygender,
|
||||
#[serde(alias = "biromantic2")]
|
||||
Greysexual,
|
||||
Gynesexual,
|
||||
Intergender,
|
||||
|
|
@ -79,7 +84,7 @@ pub enum Preset {
|
|||
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub struct ColorProfile {
|
||||
colors: Vec<RGB8>,
|
||||
pub colors: Vec<RGB8>,
|
||||
}
|
||||
|
||||
impl Preset {
|
||||
|
|
@ -409,10 +414,6 @@ impl ColorProfile {
|
|||
Ok(Self::new(weighted_colors))
|
||||
}
|
||||
|
||||
pub fn colors(&self) -> &[RGB8] {
|
||||
&self.colors
|
||||
}
|
||||
|
||||
/// Creates another color profile with only the unique colors.
|
||||
pub fn unique_colors(&self) -> Self {
|
||||
let unique_colors = self.colors.iter().collect::<IndexSet<_>>();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,25 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumString, VariantNames};
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug, EnumString, VariantNames)]
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, EnumString, Serialize, VariantNames)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum AnsiMode {
|
||||
#[serde(rename = "8bit")]
|
||||
#[strum(serialize = "8bit")]
|
||||
Ansi256,
|
||||
Rgb,
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug, EnumString, VariantNames)]
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LightDark {
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, EnumString, Serialize, VariantNames)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum Backend {
|
||||
Qwqfetch,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue