use std::borrow::BorrowMut; use serde::{de::Expected, Deserialize, Serialize}; use strum::IntoEnumIterator; use crate::split; use super::{ attribute::Attribute, color::Color, parser::{ArgParser, FromCommandArgs, ParseError}, }; macro_rules! theme_attr { ($($name:tt($ty:tt),)*) => { #[derive(Debug, Clone, strum::Display, strum::EnumIter)] #[strum(serialize_all = "snake_case")] pub enum ThemeAttr { $( $name($ty), )* } impl PartialEq for ThemeAttr { fn eq(&self, other: &Self) -> bool { match (self, other) { $( (Self::$name(_), Self::$name(_)) => true, )+ _ => false, } } } impl From for Attribute { fn from(value: ThemeAttr) -> Self { match value { $( ThemeAttr::$name(val) => theme_attr!(Attr $ty)(val), )* } } } impl FromCommandArgs for ThemeAttr { fn from_command_args, I: Iterator>( command: &str, args: I, ) -> Result { match ThemeAttr::iter() .find(|attr| attr.attr_path() == command) .map(|attr| -> Result { let mut attr = attr.clone(); let mut parser = ArgParser::from_strings(args.map(|a| a.into())); match attr.borrow_mut() { $( ThemeAttr::$name(val) => *val = parser.next_from_str(concat!("theme_attr(", stringify!($name), ")"))?, )+ }; Ok(attr) }) .map(|res| if let Err(err) = res {panic!("::: {err}")} else {res}) .ok_or(ParseError::InvalidCommand(command.to_string())) { Ok(res) => res, Err(err) => return Err(err), } } } }; (Attr u32) => { Attribute::Uint }; (Attr String) => { Attribute::String }; (Attr bool) => { Attribute::Bool }; (Attr Color) => { Attribute::Color }; (Attr i32) => { Attribute::Int }; } theme_attr!( TitleHeight(u32), TitleWhen(String), TitleFont(String), TitleDepth(i32), InnerWidth(u32), InnerColor(Color), BorderWidth(u32), FloatingBorderWidth(u32), FloatingOuterWidth(u32), TilingOuterWidth(u32), BackgroundColor(Color), ActiveColor(Color), ActiveInnerColor(Color), ActiveOuterColor(Color), NormalColor(Color), NormalInnerColor(Color), NormalOuterColor(Color), NormalTitleColor(Color), UrgentColor(Color), UrgentInnerColor(Color), UrgentOuterColor(Color), TitleColor(Color), ); impl ThemeAttr { pub fn attr_path(&self) -> String { macro_rules! match_section { ($self:ident => $($section:literal),+) => { { let attr_str = $self.to_string(); let mut attr_parts = attr_str.split('_'); let first = attr_parts.next().unwrap(); let remainder = attr_parts.collect::>().join("_"); match first { $( $section => ["theme", $section, &remainder].join("."), )+ _ => ["theme", &attr_str].join("."), } } }; } match_section!(self => "floating", "tiling", "active", "normal", "urgent") } } impl Serialize for ThemeAttr { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(&format!( "{} = {}", self.attr_path(), Attribute::from(self.clone()) )) } } impl<'de> Deserialize<'de> for ThemeAttr { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { pub enum Expect { NoEquals, ParseError(ParseError), } impl Expected for Expect { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Expect::NoEquals => write!(f, "value having an equals sign"), Expect::ParseError(err) => write!(f, "parsing error: {err}"), } } } let str_val: String = Deserialize::deserialize(deserializer)?; let ((first, second), _) = split::on_first_match(&str_val, &['=']).ok_or(serde::de::Error::invalid_value( serde::de::Unexpected::Str(&str_val), &Expect::NoEquals, ))?; Ok( Self::from_command_args(&first.trim(), [second.trim()].into_iter()).map_err(|err| { serde::de::Error::invalid_value( serde::de::Unexpected::Str(&str_val), &Expect::ParseError(err), ) })?, ) } } #[cfg(test)] mod test { use serde::{Deserialize, Serialize}; use crate::hlwm::theme::ThemeAttr; use pretty_assertions::assert_eq; #[test] fn test_partial_eq_theme_attr_string() { assert!(ThemeAttr::TitleFont(String::from("hello")) .eq(&ThemeAttr::TitleFont(String::from("world")))) } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ThemeAttrWrapper { attr: ThemeAttr, } #[test] fn theme_serialize_deserialize() { let theme = ThemeAttrWrapper { attr: ThemeAttr::TitleHeight(15), }; let theme_str = toml::to_string_pretty(&theme).expect("serialization"); let theme_parsed: ThemeAttrWrapper = toml::from_str(&theme_str).expect("deserialization"); assert_eq!(theme.attr, theme_parsed.attr); } }