diff --git a/cspell.yaml b/cspell.yaml index 260e56e952..72f87ad338 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -31,9 +31,11 @@ words: - bugz - busctl - caldav + - ccache - chrono - clippy - CLOEXEC + - cmap - conn - consts - conv @@ -182,15 +184,16 @@ words: - wofi - wttr - xclip + - xcolors - xesam - xkbswitch - XKCD - xrandr + - xresources - xtask - zbus - - zvariant - - zwlr - - zswap - zram + - zswap - Zswapped - - ccache + - zvariant + - zwlr diff --git a/doc/themes.md b/doc/themes.md index 32230c26b5..8d229899f0 100644 --- a/doc/themes.md +++ b/doc/themes.md @@ -124,7 +124,11 @@ cpu_boost_off = "OFF" # Available theme overrides -All `bg` and `fg` overrides are html hex color codes like `#000000` or `#789ABC`. A fourth byte for alpha (like `#acbdef42`) works on some systems. `00` is transparent, `FF` is opaque. +All `bg` and `fg` overrides are either + +* html hex color codes like `#000000` or `#789ABC`; a fourth byte for alpha (like `#acbdef42`) works on some systems. `00` is transparent, `FF` is opaque, or +* a reference to another override, e.g., `{ link = "idle-bg" }`, or +* a reference to a color name defined in `~/.Xresources`, e.g., `x:background` looks for a line like `*background: #aabbcc` in `~/.Xresources` (see also [.Xresources](https://wiki.debian.org/Xresources)). The tints are added to every second block counting from the right. They will therefore always brighten the block and never darken it. The alpha channel, if it works, can also be alternated in the same way. diff --git a/src/themes.rs b/src/themes.rs index c02b555e9d..6419104f26 100644 --- a/src/themes.rs +++ b/src/themes.rs @@ -1,5 +1,6 @@ pub mod color; pub mod separator; +pub mod xresources; use std::fmt; use std::ops::{Deref, DerefMut}; diff --git a/src/themes/color.rs b/src/themes/color.rs index f427a82ba7..22f5171e88 100644 --- a/src/themes/color.rs +++ b/src/themes/color.rs @@ -216,6 +216,12 @@ impl FromStr for Color { let v = components.next().or_error(err_msg)??; let a = components.next().unwrap_or(Ok(100.))?; Color::Hsva(Hsva::new(h, s / 100., v / 100., (a / 100. * 255.) as u8)) + } else if color.starts_with("x:") { + let name = color.split_at(2).1; + super::xresources::get_color(name)? + .or_error(|| format!("color '{name}' not defined in ~/.Xresources"))? + .parse() + .or_error(|| format!("invalid color definition '{name}'"))? } else { let err_msg = || format!("'{color}' is not a valid RGBA color"); let rgb = color.get(1..7).or_error(err_msg)?; diff --git a/src/themes/xresources.rs b/src/themes/xresources.rs new file mode 100644 index 0000000000..df7b718360 --- /dev/null +++ b/src/themes/xresources.rs @@ -0,0 +1,95 @@ +use log::debug; +use regex::Regex; + +use std::collections::HashMap; +use std::sync::LazyLock; + +use crate::errors::*; + +#[cfg(not(test))] +use std::{env, path::PathBuf}; + +#[cfg(not(test))] +fn read_xresources() -> std::io::Result { + use std::io::{Error, ErrorKind}; + let home = + env::var("HOME").map_err(|_| Error::new(ErrorKind::Other, "HOME env var was not set"))?; + let xresources = PathBuf::from(home + "/.Xresources"); + debug!(".Xresources @ {:?}", xresources); + std::fs::read_to_string(xresources) +} + +#[cfg(test)] +use tests::read_xresources; + +static COLOR_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^\s*\*(?[^: ]+)\s*:\s*(?#[a-f0-9]{6,8}).*$").unwrap() +}); + +static COLORS: LazyLock, Error>> = LazyLock::new(|| { + let content = read_xresources().error("could not read .Xresources")?; + debug!(".Xresources content:\n{}", content); + Ok(HashMap::from_iter(content.lines().filter_map(|line| { + COLOR_REGEX + .captures(line) + .map(|caps| (caps["name"].to_string(), caps["color"].to_string())) + }))) +}); + +pub fn get_color(name: &str) -> Result, Error> { + COLORS + .as_ref() + .map(|cmap| cmap.get(name)) + .map_err(Clone::clone) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Result; + + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn read_xresources() -> Result { + static XRESOURCES: &str = "\ + ! this is a comment\n\ + \n\ + *color4 : #feedda\n\ + \n\ + *background: #ee33aa99\n\ + "; + Ok(XRESOURCES.to_string()) + } + + #[test] + fn test_reading_colors() { + let colors = COLORS.as_ref().unwrap(); + assert_eq!(colors.get("color4"), Some(&"#feedda".to_string())); + assert_eq!(colors.get("background"), Some(&"#ee33aa99".to_string())); + assert_eq!(2, colors.len()); + } + + #[test] + fn test_deserializing_xcolors() { + use super::super::color::*; + let mut parsed_color = "x:color4".parse::().unwrap(); + assert_eq!( + parsed_color, + Color::Rgba(Rgba { + r: 254, + g: 237, + b: 218, + a: 255 + }) + ); + parsed_color = "x:background".parse::().unwrap(); + assert_eq!( + parsed_color, + Color::Rgba(Rgba { + r: 238, + g: 51, + b: 170, + a: 153, + }) + ); + } +}