hlctl/src/hlwm/theme.rs

239 lines
5.8 KiB
Rust

use std::{borrow::BorrowMut, num::ParseIntError};
use serde::{de::Expected, Deserialize, Serialize};
use strum::IntoEnumIterator;
use thiserror::Error;
use crate::hlwm::split;
use super::{
attribute::Attribute,
color::{self, Color},
StringParseError,
};
#[derive(Debug, Error)]
pub enum ThemeAttrParseError {
#[error("path not found")]
PathNotFound,
#[error("integer value parse error: [{0}]")]
ParseIntError(#[from] ParseIntError),
#[error("color value parse error: [{0}]")]
ColorParseError(#[from] color::ParseError),
#[error("string value parse error: [{0}]")]
StringParseError(#[from] StringParseError),
}
macro_rules! theme_attr {
($($name:tt($value:tt),)*) => {
#[derive(Debug, Clone, strum::Display, strum::EnumIter)]
#[strum(serialize_all = "snake_case")]
pub enum ThemeAttr {
$(
$name($value),
)*
}
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 $value)(val),
)*
}
}
}
impl ThemeAttr {
pub fn from_raw_parts(path: &str, value: &str) -> Result<Self, ThemeAttrParseError> {
match ThemeAttr::iter()
.find(|attr| attr.attr_path() == path)
.map(|attr| -> Result<Self, ThemeAttrParseError> {
let mut attr = attr.clone();
match attr.borrow_mut() {
$(
ThemeAttr::$name(val) => *val = theme_attr!(Parse value: $value),
)+
};
Ok(attr)
})
.map(|res| if let Err(err) = res {panic!("::: {err}")} else {res})
.ok_or(ThemeAttrParseError::PathNotFound)
{
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
};
(Default u32) => {
0u32
};
(Default i32) => {
0i32
};
(Default Color) => {
Color::BLACK
};
(Default String) => {
String::new()
};
(Parse $v:tt: String) => {
$v.to_string()
};
(Parse $v:tt: $v_ty:tt) => {
$v.parse::<$v_ty>()?
}
}
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,
ThemeAttrParseError(ThemeAttrParseError),
}
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::ThemeAttrParseError(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_raw_parts(&first.trim(), &second.trim()).map_err(|err| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Str(&str_val),
&Expect::ThemeAttrParseError(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);
}
}