hlctl/src/hlwm/theme.rs

212 lines
5.3 KiB
Rust

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<ThemeAttr> 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<S: Into<String>, I: Iterator<Item = S>>(
command: &str,
args: I,
) -> Result<Self, ParseError> {
match ThemeAttr::iter()
.find(|attr| attr.attr_path() == command)
.map(|attr| -> Result<Self, ParseError> {
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::<Vec<_>>().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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!(
"{} = {}",
self.attr_path(),
Attribute::from(self.clone())
))
}
}
impl<'de> Deserialize<'de> for ThemeAttr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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);
}
}