diff --git a/Cargo.lock b/Cargo.lock index 75b9e262..12bb9bb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,15 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -60,6 +69,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "cc" version = "1.0.99" @@ -99,6 +114,26 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_more" +version = "1.0.0-beta.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7abbfc297053be59290e3152f8cbcd52c8642e0728b69ee187d991d4c1af08d" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0-beta.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bba3e9872d7c58ce7ef0fcf1844fcc3e23ef2a58377b50df35dd98e42a5726e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "directories" version = "5.0.1" @@ -126,6 +161,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "getrandom" version = "0.2.15" @@ -157,15 +198,17 @@ dependencies = [ "bpaf", "chrono", "deranged", + "derive_more", "directories", "indexmap", + "palette", "regex", - "rgb", "serde", "serde_json", "serde_path_to_error", "shell-words", "strum", + "thiserror", "tracing", "tracing-subscriber", "unicode-normalization", @@ -303,6 +346,29 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -367,12 +433,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" -[[package]] -name = "rgb" -version = "0.8.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" - [[package]] name = "rustversion" version = "1.0.17" @@ -407,9 +467,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.118" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", diff --git a/Cargo.toml b/Cargo.toml index 7e6afec7..dcd18736 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,13 @@ license = "MIT" ansi_colours = { version = "1.2.2", default-features = false } anyhow = { version = "1.0.86", default-features = false } bpaf = { version = "0.9.12", default-features = false } +bytemuck = { version = "1.16.1", default-features = false } chrono = { version = "0.4.38", default-features = false } deranged = { version = "0.3.11", default-features = false } derive_more = { version = "1.0.0-beta.6", default-features = false } directories = { version = "5.0.1", default-features = false } indexmap = { version = "2.2.6", default-features = false } +palette = { version = "0.7.6", default-features = false } regex = { version = "1.10.5", default-features = false } rgb = { version = "0.8.37", default-features = false } serde = { version = "1.0.203", default-features = false } @@ -27,6 +29,7 @@ serde_json = { version = "1.0.118", default-features = false } serde_path_to_error = { version = "0.1.16", default-features = false } shell-words = { version = "1.1.0", default-features = false } strum = { version = "0.26.3", default-features = false } +thiserror = { version = "1.0.61", default-features = false } tracing = { version = "0.1.40", default-features = false } tracing-subscriber = { version = "0.3.18", default-features = false } unicode-normalization = { version = "0.1.23", default-features = false } diff --git a/crates/hyfetch/Cargo.toml b/crates/hyfetch/Cargo.toml index da729b80..1fddfb80 100644 --- a/crates/hyfetch/Cargo.toml +++ b/crates/hyfetch/Cargo.toml @@ -13,18 +13,21 @@ default-run = "hyfetch" # ansi_colours = { workspace = true, features = ["rgb"] } anyhow = { workspace = true, features = ["std"] } bpaf = { workspace = true, features = [] } +# bytemuck = { workspace = true, features = [] } chrono = { workspace = true, features = ["clock", "std"] } deranged = { workspace = true, features = ["serde", "std"] } -# derive_more = { workspace = true, features = ["std"] } +derive_more = { workspace = true, features = ["from", "from_str", "into", "std"] } directories = { workspace = true, features = [] } indexmap = { workspace = true, features = ["serde", "std"] } +palette = { workspace = true, features = ["std"] } regex = { workspace = true, features = ["perf", "std", "unicode"] } -rgb = { workspace = true, features = [] } +# 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"] } +thiserror = { workspace = true, features = [] } tracing = { workspace = true, features = ["attributes", "std"] } tracing-subscriber = { workspace = true, features = ["ansi", "fmt", "smallvec", "std", "tracing-log"] } diff --git a/crates/hyfetch/src/bin/hyfetch.rs b/crates/hyfetch/src/bin/hyfetch.rs index e9f81820..2b1f8817 100644 --- a/crates/hyfetch/src/bin/hyfetch.rs +++ b/crates/hyfetch/src/bin/hyfetch.rs @@ -5,16 +5,17 @@ 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 hyfetch::presets::AssignLightness; +use hyfetch::utils::get_cache_path; use tracing::debug; fn main() -> Result<()> { let options = options().run(); - init_tracing_subsriber(options.debug).context("Failed to init tracing subscriber")?; + init_tracing_subsriber(options.debug).context("failed to init tracing subscriber")?; debug!(?options, "CLI options"); @@ -23,7 +24,7 @@ fn main() -> Result<()> { if options.test_print { println!( "{}", - get_distro_ascii(options.distro.as_ref()).context("Failed to get distro ascii")? + get_distro_ascii(options.distro.as_ref()).context("failed to get distro ascii")? ); return Ok(()); } @@ -31,20 +32,18 @@ fn main() -> Result<()> { // TODO let config = if options.config { - create_config(options.config_file).context("Failed to create 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")? + read_config(&options.config_file).context("failed to read config")? { config } else { - create_config(options.config_file).context("Failed to create config")? + create_config(options.config_file).context("failed to create config")? }; + // Check if it's June (pride month) let now = chrono::Local::now(); - let cache_path = ProjectDirs::from("", "", "hyfetch") - .context("Failed to get base dirs")? - .cache_dir() - .to_owned(); + let cache_path = get_cache_path().context("failed to get cache path")?; 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(); @@ -58,14 +57,31 @@ fn main() -> Result<()> { println!(); if !june_path.is_file() { - fs::create_dir_all(cache_path).context("Failed to create cache dir")?; + 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:?}"))?; + .with_context(|| format!("failed to create file {june_path:?}"))?; } } // TODO + // Get preset + let preset = options.preset.unwrap_or(config.preset); + let color_profile = preset.color_profile(); + debug!(?color_profile, "color profile"); + + // Lighten + let color_profile = if let Some(scale) = options.scale { + color_profile.lighten(scale) + } else if let Some(lightness) = options.lightness { + color_profile.with_lightness(AssignLightness::Replace(lightness)) + } else { + color_profile.with_lightness_dl(config.lightness(), config.light_dark) + }; + debug!(?color_profile, "lightened color profile"); + + // TODO + Ok(()) } @@ -85,18 +101,18 @@ where return Ok(None); }, Err(err) => { - return Err(err).with_context(|| format!("Failed to open {path:?}")); + 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:?}"))?; + .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:?}"))?; + .with_context(|| format!("failed to parse {path:?}"))?; debug!(?config, "read config"); @@ -158,5 +174,5 @@ fn init_tracing_subsriber(debug: bool) -> Result<()> { subscriber .try_init() - .context("Failed to set the global default subscriber") + .context("failed to set the global default subscriber") } diff --git a/crates/hyfetch/src/cli_options.rs b/crates/hyfetch/src/cli_options.rs index 9a170c86..84be8b82 100644 --- a/crates/hyfetch/src/cli_options.rs +++ b/crates/hyfetch/src/cli_options.rs @@ -8,6 +8,7 @@ use bpaf::{construct, long, OptionParser, Parser}; use directories::BaseDirs; use strum::VariantNames; +use crate::color_util::Lightness; use crate::presets::Preset; use crate::types::{AnsiMode, Backend}; @@ -19,9 +20,9 @@ pub struct Options { pub mode: Option, pub backend: Option, pub args: Vec, - pub colors_scale: Option, - pub colors_set_lightness: Option, - pub colors_use_overlay: bool, + pub scale: Option, + pub lightness: Option, + pub overlay: bool, pub june: bool, pub debug: bool, pub distro: Option, @@ -42,7 +43,7 @@ pub fn options() -> OptionParser { .fallback_with(|| { Ok::<_, anyhow::Error>( BaseDirs::new() - .context("Failed to get base dirs")? + .context("failed to get base dirs")? .config_dir() .join("hyfetch.json"), ) @@ -107,15 +108,15 @@ BACKEND={{{}}}", .argument::("ARGS") .parse(|s| shell_words::split(&s).context("ARGS should be valid command-line arguments")) .fallback(vec![]); - let colors_scale = long("c-scale") + let scale = long("c-scale") .help("Lighten colors by a multiplier") .argument("SCALE") .optional(); - let colors_set_lightness = long("c-set-l") + let lightness = long("c-set-l") .help("Set lightness value of the colors") - .argument("LIGHT") + .argument("LIGHTNESS") .optional(); - let colors_use_overlay = long("c-overlay") + let overlay = long("c-overlay") .help("Use experimental overlay color adjusting instead of HSL lightness") .switch(); let june = long("june").help("Show pride month easter egg").switch(); @@ -151,9 +152,9 @@ BACKEND={{{}}}", mode, backend, args, - colors_scale, - colors_set_lightness, - colors_use_overlay, + scale, + lightness, + overlay, june, debug, distro, diff --git a/crates/hyfetch/src/color_util.rs b/crates/hyfetch/src/color_util.rs index 9ef2cd0e..e63c2948 100644 --- a/crates/hyfetch/src/color_util.rs +++ b/crates/hyfetch/src/color_util.rs @@ -1,49 +1,113 @@ -use anyhow::{anyhow, Context, Result}; +use std::num::ParseFloatError; + +use anyhow::Result; use deranged::RangedU8; -use rgb::RGB8; +use derive_more::{From, FromStr, Into}; use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// Represents the lightness component in HSL. +/// +/// The range of valid values is +/// `(`[`Lightness::MIN`]`..=`[`Lightness::MAX`]`)`. +#[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Deserialize, Into, Serialize)] +pub struct Lightness(f32); + +#[derive(Debug, Error)] +pub enum LightnessError { + #[error( + "invalid lightness {0}, expected value between {} and {}", + Lightness::MIN, + Lightness::MAX + )] + OutOfRange(f32), +} + +#[derive(Debug, Error)] +pub enum ParseLightnessError { + #[error("invalid float")] + InvalidFloat(#[from] ParseFloatError), + #[error("invalid lightness")] + InvalidLightness(#[from] LightnessError), +} /// 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>); +/// The range of valid values as supported in neofetch is +/// `(`[`NeofetchAsciiIndexedColor::MIN`]`.. +/// =`[`NeofetchAsciiIndexedColor::MAX`]`)`. +#[derive( + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + Debug, + Deserialize, + From, + FromStr, + Into, + Serialize, +)] +pub struct NeofetchAsciiIndexedColor( + RangedU8<{ NeofetchAsciiIndexedColor::MIN }, { NeofetchAsciiIndexedColor::MAX }>, +); /// 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)] +#[derive( + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + Debug, + Deserialize, + From, + FromStr, + Into, + Serialize, +)] pub struct PresetIndexedColor(usize); -pub trait FromHex { - /// Creates color from hex code. - fn from_hex(hex: S) -> Result - where - S: AsRef; -} +impl Lightness { + const MAX: f32 = 1.0f32; + const MIN: f32 = 0.0f32; -impl FromHex for RGB8 { - fn from_hex(hex: S) -> Result - where - S: AsRef, - { - let hex = hex.as_ref(); - - let hex = hex.strip_prefix('#').unwrap_or(hex); - if hex.len() != 6 { - Err(anyhow!("invalid length for hex color"))?; + pub fn new(value: f32) -> Result { + if !(Self::MIN..=Self::MAX).contains(&value) { + return Err(LightnessError::OutOfRange(value)); } - let r = - u8::from_str_radix(&hex[0..2], 16).context("Failed to parse hex color component")?; - let g = - u8::from_str_radix(&hex[2..4], 16).context("Failed to parse hex color component")?; - let b = - u8::from_str_radix(&hex[4..6], 16).context("Failed to parse hex color component")?; - - Ok(RGB8::new(r, g, b)) + Ok(Self(value)) } } + +impl TryFrom for Lightness { + type Error = LightnessError; + + fn try_from(value: f32) -> Result { + Lightness::new(value) + } +} + +impl FromStr for Lightness { + type Err = ParseLightnessError; + + fn from_str(s: &str) -> Result { + Ok(Lightness::new(s.parse()?)?) + } +} + +impl NeofetchAsciiIndexedColor { + const MAX: u8 = 6; + const MIN: u8 = 1; +} diff --git a/crates/hyfetch/src/lib.rs b/crates/hyfetch/src/lib.rs index 7aa0943f..d66f23af 100644 --- a/crates/hyfetch/src/lib.rs +++ b/crates/hyfetch/src/lib.rs @@ -5,3 +5,4 @@ pub mod models; pub mod neofetch_util; pub mod presets; pub mod types; +pub mod utils; diff --git a/crates/hyfetch/src/models.rs b/crates/hyfetch/src/models.rs index 907a9c3a..58a749aa 100644 --- a/crates/hyfetch/src/models.rs +++ b/crates/hyfetch/src/models.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; +use crate::color_util::Lightness; use crate::neofetch_util::ColorAlignment; use crate::presets::Preset; use crate::types::{AnsiMode, Backend, LightDark}; @@ -9,7 +10,7 @@ pub struct Config { pub preset: Preset, pub mode: AnsiMode, pub light_dark: LightDark, - pub lightness: Option, + lightness: Option, pub color_align: ColorAlignment, pub backend: Backend, #[serde(with = "self::args_serde_with")] @@ -18,6 +19,20 @@ pub struct Config { pub pride_month_disable: bool, } +impl Config { + pub fn default_lightness(term: &LightDark) -> Lightness { + match term { + LightDark::Dark => Lightness::new(0.65).unwrap(), + LightDark::Light => Lightness::new(0.4).unwrap(), + } + } + + pub fn lightness(&self) -> Lightness { + self.lightness + .unwrap_or_else(|| Self::default_lightness(&self.light_dark)) + } +} + mod args_serde_with { use std::fmt; diff --git a/crates/hyfetch/src/neofetch_util.rs b/crates/hyfetch/src/neofetch_util.rs index 10f17759..c9be757a 100644 --- a/crates/hyfetch/src/neofetch_util.rs +++ b/crates/hyfetch/src/neofetch_util.rs @@ -16,6 +16,7 @@ use tracing::debug; use crate::color_util::{NeofetchAsciiIndexedColor, PresetIndexedColor}; use crate::distros::Distro; +const NEOFETCH_COLOR_PATTERN: &str = r"\$\{c[0-9]\}"; static NEOFETCH_COLOR_RE: OnceLock = OnceLock::new(); #[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] @@ -41,14 +42,14 @@ pub fn get_command_path() -> Result { let path = path.join("neofetch"); match path.try_exists() { Ok(true) => { - return path.canonicalize().context("Failed to canonicalize path"); + return path.canonicalize().context("failed to canonicalize path"); }, Ok(false) => { Err(anyhow!("{path:?} does not exist or is not readable"))?; }, Err(err) => { Err(err) - .with_context(|| format!("Failed to check for existence of {path:?}"))?; + .with_context(|| format!("failed to check for existence of {path:?}"))?; }, } } @@ -63,7 +64,7 @@ pub fn get_command_path() -> Result { if !path.is_file() { continue; } - return path.canonicalize().context("Failed to canonicalize path"); + return path.canonicalize().context("failed to canonicalize path"); } Err(anyhow!("neofetch command not found")) @@ -80,7 +81,7 @@ where distro.as_ref().into() } else { get_distro_name() - .context("Failed to get distro name")? + .context("failed to get distro name")? .into() }; debug!(%distro, "distro name"); @@ -101,7 +102,7 @@ where let Some(width) = NEOFETCH_COLOR_RE .get_or_init(|| { - Regex::new(r"\$\{c[0-9]\}").expect("neofetch color regex should not be invalid") + Regex::new(NEOFETCH_COLOR_PATTERN).expect("neofetch color regex should not be invalid") }) .replace_all(asc, "") .split('\n') @@ -140,11 +141,11 @@ fn run_neofetch_command_piped(args: &[S]) -> Result where S: AsRef + fmt::Debug, { - let mut command = make_neofetch_command(args).context("Failed to make neofetch command")?; + let mut command = make_neofetch_command(args).context("failed to make neofetch command")?; let output = command .output() - .context("Failed to execute neofetch as child process")?; + .context("failed to execute neofetch as child process")?; debug!(?output, "neofetch output"); if !output.status.success() { @@ -168,7 +169,7 @@ where } let out = String::from_utf8(output.stdout) - .context("Failed to process neofetch output as it contains invalid UTF-8")? + .context("failed to process neofetch output as it contains invalid UTF-8")? .trim() .to_owned(); Ok(out) @@ -181,7 +182,7 @@ where #[cfg(not(windows))] { let mut command = Command::new("bash"); - command.arg(get_command_path().context("Failed to get neofetch command path")?); + command.arg(get_command_path().context("failed to get neofetch command path")?); command.args(args); Ok(command) } @@ -194,5 +195,5 @@ where #[tracing::instrument(level = "debug")] fn get_distro_name() -> Result { run_neofetch_command_piped(&["ascii_distro_name"]) - .context("Failed to get distro name from neofetch") + .context("failed to get distro name from neofetch") } diff --git a/crates/hyfetch/src/presets.rs b/crates/hyfetch/src/presets.rs index 4e242f7b..a1f710d1 100644 --- a/crates/hyfetch/src/presets.rs +++ b/crates/hyfetch/src/presets.rs @@ -2,13 +2,16 @@ use std::iter; use anyhow::{anyhow, Context, Result}; use indexmap::IndexSet; -use rgb::RGB8; +use palette::encoding::{self, Linear}; +use palette::num::ClampAssign; +use palette::{Hsl, IntoColorMut, LinSrgb, Srgb}; use serde::{Deserialize, Serialize}; use strum::{EnumString, VariantNames}; -use crate::color_util::FromHex; +use crate::color_util::Lightness; +use crate::types::LightDark; -#[derive(Clone, Hash, Debug, Deserialize, EnumString, Serialize, VariantNames)] +#[derive(Copy, Clone, Hash, Debug, Deserialize, EnumString, Serialize, VariantNames)] #[serde(rename_all = "kebab-case")] #[strum(serialize_all = "kebab-case")] pub enum Preset { @@ -82,9 +85,16 @@ pub enum Preset { Xenogender, } -#[derive(Clone, Eq, PartialEq, Hash, Debug)] +#[derive(Clone, Eq, PartialEq, Debug)] pub struct ColorProfile { - pub colors: Vec, + pub colors: Vec>, +} + +#[derive(Clone, PartialEq, Debug)] +pub enum AssignLightness { + Replace(Lightness), + ClampMax(Lightness), + ClampMin(Lightness), } impl Preset { @@ -376,7 +386,7 @@ impl Preset { } impl ColorProfile { - pub fn new(colors: Vec) -> Self { + pub fn new(colors: Vec>) -> Self { Self { colors } } @@ -386,9 +396,9 @@ impl ColorProfile { { let colors = hex_colors .into_iter() - .map(RGB8::from_hex) - .collect::>() - .context("Failed to parse hex colors")?; + .map(|s| s.as_ref().parse()) + .collect::>() + .context("failed to parse hex colors")?; Ok(Self::new(colors)) } @@ -414,12 +424,79 @@ impl ColorProfile { Ok(Self::new(weighted_colors)) } + /// Creates a new color profile, with the colors lightened by a multiplier. + pub fn lighten(&self, multiplier: f32) -> Self { + let mut rgb_f32_colors: Vec = + self.colors.iter().map(|c| c.into_linear()).collect(); + + { + let hsl_f32_colors: &mut [Hsl>] = + &mut rgb_f32_colors.into_color_mut(); + + for hsl_f32_color in hsl_f32_colors { + hsl_f32_color.lightness *= multiplier; + } + } + + let rgb_u8_colors: Vec<_> = rgb_f32_colors + .into_iter() + .map(Srgb::::from_linear) + .collect(); + + Self { + colors: rgb_u8_colors, + } + } + + /// Creates a new color profile, with the colors set to the specified HSL + /// lightness value. + pub fn with_lightness(&self, assign_lightness: AssignLightness) -> Self { + let mut rgb_f32_colors: Vec<_> = + self.colors.iter().map(|c| c.into_format::()).collect(); + + { + let hsl_f32_colors: &mut [Hsl] = &mut rgb_f32_colors.into_color_mut(); + + for hsl_f32_color in hsl_f32_colors { + match assign_lightness { + AssignLightness::Replace(lightness) => { + hsl_f32_color.lightness = lightness.into(); + }, + AssignLightness::ClampMax(lightness) => { + hsl_f32_color.lightness.clamp_max_assign(lightness.into()); + }, + AssignLightness::ClampMin(lightness) => { + hsl_f32_color.lightness.clamp_min_assign(lightness.into()); + }, + } + } + } + + let rgb_u8_colors: Vec<_> = rgb_f32_colors + .into_iter() + .map(|c| c.into_format::()) + .collect(); + + Self { + colors: rgb_u8_colors, + } + } + + /// Creates a new color profile, with the colors set to the specified HSL + /// lightness value, with respect to dark/light terminals. + pub fn with_lightness_dl(&self, lightness: Lightness, term: LightDark) -> Self { + match term { + LightDark::Dark => self.with_lightness(AssignLightness::ClampMin(lightness)), + LightDark::Light => self.with_lightness(AssignLightness::ClampMax(lightness)), + } + } + /// Creates another color profile with only the unique colors. pub fn unique_colors(&self) -> Self { - let unique_colors = self.colors.iter().collect::>(); + let unique_colors: IndexSet<[u8; 3]> = self.colors.iter().map(|c| (*c).into()).collect(); let unique_colors = { let mut v = Vec::with_capacity(unique_colors.len()); - v.extend(unique_colors); + v.extend(unique_colors.into_iter().map(Srgb::::from)); v }; Self::new(unique_colors) diff --git a/crates/hyfetch/src/utils.rs b/crates/hyfetch/src/utils.rs new file mode 100644 index 00000000..b4571eda --- /dev/null +++ b/crates/hyfetch/src/utils.rs @@ -0,0 +1,12 @@ +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use directories::ProjectDirs; + +pub fn get_cache_path() -> Result { + let path = ProjectDirs::from("", "", "hyfetch") + .context("failed to get base dirs")? + .cache_dir() + .to_owned(); + Ok(path) +}