use std::{ borrow::BorrowMut, cmp::Ordering, convert::Infallible, env, fmt::Display, fs::{self}, io, ops::Deref, path::Path, str::FromStr, string::FromUtf8Error, }; use log::info; use serde::{ de::{Expected, Unexpected}, Deserialize, Serialize, }; use strum::IntoEnumIterator; use thiserror::Error; use crate::{ hlwm::{ attribute::Attribute, color::Color, command::{CommandError, HlwmCommand}, hook::Hook, key::{Key, KeyParseError, KeyUnbind, Keybind, MouseButton, Mousebind, MousebindAction}, rule::{Condition, Consequence, FloatPlacement, Rule}, setting::{FrameLayout, Setting, ShowFrameDecoration, SmartFrameSurroundings}, theme::ThemeAttr, window::Window, Align, Client, Direction, Index, Operator, Separator, StringParseError, ToggleBool, }, rule, window_types, }; #[derive(Debug, Error)] pub enum ConfigError { #[error("IO error")] IoError(#[from] io::Error), #[error("toml deserialization error: {0}")] TomlDeserializeError(#[from] toml::de::Error), #[error("toml serialization error: {0}")] TomlSerializeError(#[from] toml::ser::Error), #[error("json error: {0}")] JsonError(#[from] serde_json::Error), #[error("$HOME must be set")] HomeNotSet, #[error("herbstluft client command error: {0}")] CommandError(#[from] CommandError), #[error("non-utf8 string error: {0}")] Utf8StringError(#[from] FromUtf8Error), #[error("failed parsing keybind: {0}")] KeyParseError(#[from] KeyParseError), #[error("failed parsing value from string: {0}")] StringParseError(#[from] StringParseError), } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Config { pub font: Option, pub font_bold: Option, pub mod_key: Key, pub keybinds: Vec, pub mousebinds: Vec, // services won't be spawned through HlwmCommand::Spawn // so that the running instance can hold the handles // and a reset would kill them pub services: Vec, pub tags: Vec, pub theme: Theme, pub rules: Vec, pub attributes: Vec, pub settings: Vec, } #[derive(Clone, Debug, PartialEq)] pub struct SetAttribute { pub path: String, pub value: Attribute, } impl Display for SetAttribute { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "({}){}={}", self.value.type_string(), &self.path, self.value.to_string() ) } } impl FromStr for SetAttribute { type Err = StringParseError; fn from_str(s: &str) -> Result { let mut parts = s.split(')'); let type_string = parts.next().map(|p| p.strip_prefix('(')).flatten().ok_or( StringParseError::RequiredArgMissing("attribute type".into()), )?; let mut parts = parts .next() .ok_or(StringParseError::RequiredArgMissing( "attribute path/value".into(), ))? .split('=') .map(|v| v.trim()); let path = parts .next() .ok_or(StringParseError::RequiredArgMissing( "attribute path".into(), ))? .to_string(); let value_string = parts.collect::>().join("="); if value_string.is_empty() { return Err(StringParseError::RequiredArgMissing( "attribute value".into(), )); } let value = Attribute::new(type_string, &value_string)?; Ok(Self { path, value }) } } impl Serialize for SetAttribute { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(&self.to_string()) } } impl<'de> Deserialize<'de> for SetAttribute { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct ExpectedKey; impl serde::de::Expected for ExpectedKey { fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("Expected a supported key") } } let str_val: String = Deserialize::deserialize(deserializer)?; Ok(Self::from_str(&str_val).map_err(|_| { serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &ExpectedKey) })?) } } impl Config { const FONT: &'static str = "theme.my_font"; const FONT_BOLD: &'static str = "theme.my_font_bold"; const SERVICES: &'static str = "settings.my_services"; const MOD_KEY: &'static str = "settings.my_mod_key"; pub fn from_file(path: &Path) -> Result { info!( "reading config from: {}", path.to_str().unwrap_or("") ); Self::deserialize(&fs::read_to_string(path)?) } pub fn write_to_file(&self, path: &Path) -> Result<(), ConfigError> { info!( "saving config to: {}", path.to_str().unwrap_or("") ); Ok(fs::write(path, self.serialize()?)?) } pub fn serialize(&self) -> Result { Ok(toml::ser::to_string_pretty(self)?) } pub fn deserialize(str: &str) -> Result { Ok(toml::from_str(&str)?) } pub fn default_path() -> Result { Ok(format!( "{home}/.config/herbstluftwm/hlctl.toml", home = env::var("XDG_CONFIG_HOME") .unwrap_or(env::var("HOME").map_err(|_| ConfigError::HomeNotSet)?) )) } fn new_or_set_attr(path: String, attr: Attribute) -> HlwmCommand { HlwmCommand::Or { separator: Separator::Comma, commands: vec![ HlwmCommand::NewAttr { path: path.clone(), attr: attr.clone().into(), }, HlwmCommand::SetAttr { path, new_value: attr, }, ], } } fn attrs_set(&self) -> Result, ConfigError> { info!("loading attr settings command set"); Ok([ (Self::FONT, self.font.clone()), (Self::MOD_KEY, Some(self.mod_key.to_string())), (Self::FONT_BOLD, self.font_bold.clone()), ( Self::SERVICES, if self.services.len() == 0 { None } else { Some(serde_json::ser::to_string(&self.services)?) }, ), ] .into_iter() .filter(|(_, attr)| attr.is_some()) .map(|(name, attr)| { Self::new_or_set_attr(name.to_string(), Attribute::String(attr.unwrap())) }) .collect()) } fn theme_command_set(&self) -> Vec { info!("loading theme attr command set"); [ HlwmCommand::SetAttr { path: "theme.reset".into(), new_value: Attribute::String("1".into()), }, HlwmCommand::SetAttr { path: "theme.tiling.reset".into(), new_value: Attribute::String("1".into()), }, HlwmCommand::SetAttr { path: "theme.floating.reset".into(), new_value: Attribute::String("1".into()), }, ] .into_iter() .chain( (&self.theme.attributes) .into_iter() .map(|attr| HlwmCommand::SetAttr { path: attr.attr_path(), new_value: Attribute::from(attr.clone()).into(), }), ) .collect() } fn settings_command_set(&self) -> Vec { info!("loading settings command set"); self.settings .clone() .into_iter() .map(|s| HlwmCommand::Set(s)) .collect() } fn service_command_set(&self) -> Vec { info!("loading service command set"); self.services .clone() .into_iter() .map(|s| { [ HlwmCommand::Spawn { executable: "pkill".into(), args: vec!["-9".into(), s.name.clone()], } .to_try() .silent(), HlwmCommand::Spawn { executable: s.name, args: s.arguments, } .to_try(), ] }) .flatten() .collect() } fn rule_command_set(&self) -> Vec { info!("loading rule command set"); self.rules .clone() .into_iter() .map(|r| HlwmCommand::Rule(r)) .collect() } pub fn to_command_set(&self) -> Result, ConfigError> { Ok([ HlwmCommand::EmitHook(Hook::Reload), HlwmCommand::Keyunbind(KeyUnbind::All), HlwmCommand::Mouseunbind, ] .into_iter() .chain( self.keybinds .clone() .into_iter() .map(|key| HlwmCommand::Keybind(key)), ) .chain( self.mousebinds .clone() .into_iter() .map(|mb| HlwmCommand::Mousebind(mb)), ) .chain(self.attrs_set()?) .chain(self.theme_command_set()) .chain(self.tag_command_set()) .chain(self.settings_command_set()) .chain(self.rule_command_set()) .chain(self.service_command_set()) .collect()) } fn tag_command_set(&self) -> Vec { info!("loading tag command set"); (&self.tags) .into_iter() .map(|tag| (tag, tag.key())) .filter(|(_, key)| key.is_some()) .map(|(tag, key)| { let tag_name = tag.to_string(); let key = key.unwrap(); [ HlwmCommand::AddTag(tag_name.clone()), HlwmCommand::Keybind(Keybind::new( [self.mod_key, key], HlwmCommand::UseTag(tag_name.clone()), )), HlwmCommand::Keybind(Keybind::new( [self.mod_key, Key::Shift, key], HlwmCommand::MoveTag(tag_name), )), ] }) .flatten() .chain([ HlwmCommand::And { separator: Separator::Comma, commands: vec![ HlwmCommand::Compare { attribute: "tags.by-name.default.index".into(), operator: Operator::Equal, value: "tags.focus.index".into(), }, HlwmCommand::UseIndex { index: Index::Relative(1), skip_visible: false, }, ], } .to_try(), HlwmCommand::MergeTag { tag: "default".to_string(), target: None, } .to_try(), ]) .collect() } /// Create a config gathered from the herbstluftwm configs, /// using default mouse binds/tags pub fn from_herbstluft() -> Self { fn setting Result>( name: &str, f: F, default: T, ) -> T { match Client::new().get_attr(name.to_string()) { Ok(setting) => f(&setting.to_string()).unwrap_or(default), Err(_) => default, } } let client = Client::new(); let default = Config::default(); Config { font: setting( Self::FONT, |f| -> Result<_, Infallible> { Ok(Some(f.to_string())) }, default.font, ), mod_key: setting(Self::MOD_KEY, Key::from_str, default.mod_key), font_bold: Some( client .get_attr(ThemeAttr::TitleFont(String::new()).attr_path()) .map(|a| a.to_string()) .unwrap_or(default.font_bold.unwrap()), ), services: setting( Self::SERVICES, |v| serde_json::de::from_str(v), default.services, ), theme: Theme { attributes: (|| -> Vec<_> { ThemeAttr::iter() .map(|attr| { let attr_path = attr.attr_path(); let default = (&default.theme.attributes) .into_iter() .find(|f| (*f).eq(&attr)) .unwrap(); ThemeAttr::from_raw_parts( &attr_path, &client .get_attr(attr_path.clone()) .map(|t| t.to_string()) .unwrap_or(default.to_string()), ) .unwrap_or(default.clone()) }) .collect() })(), }, keybinds: Self::active_keybinds(true).unwrap_or(default.keybinds), tags: (|| -> Result, _> { Result::<_, ConfigError>::Ok({ let mut tags = client .tag_status()? .into_iter() .map(|tag| { let tag_result: Result<_, Infallible> = tag.name().parse(); tag_result.unwrap() }) .collect::>(); tags.sort_by(|lhs: &Tag, rhs| match lhs.partial_cmp(rhs) { Some(ord) => ord, None => Ordering::Less, }); tags }) })() .unwrap_or(default.tags), rules: (|| -> Result, ConfigError> { Ok( String::from_utf8(client.execute(HlwmCommand::ListRules)?.stdout)? .split('\n') .map(|l| l.trim()) .filter(|l| !l.is_empty()) .map(|line| Rule::from_str(line)) .collect::>()?, ) })() .unwrap_or(default.rules), settings: (|| -> Result, CommandError> { default .settings .clone() .into_iter() .map(|s| Ok(client.get_setting(s.into())?)) .collect::, CommandError>>() })() .unwrap_or(default.settings), ..default } } fn active_keybinds(omit_tag_binds: bool) -> Result, ConfigError> { String::from_utf8(Client::new().execute(HlwmCommand::ListKeybinds)?.stdout)? .split("\n") .filter(|i| { !omit_tag_binds || match i.split("\t").skip(1).next() { Some(command) => { command != HlwmCommand::UseIndex { index: Index::Absolute(0), skip_visible: false, } .to_string() && command != HlwmCommand::MoveIndex { index: Index::Absolute(0), skip_visible: false, } .to_string() && command != HlwmCommand::UseTag(String::new()).to_string() && command != HlwmCommand::MoveTag(String::new()).to_string() } None => false, } }) .map(|row: &str| Keybind::from_str(row).map_err(|err| err.into())) .collect::, ConfigError>>() } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Theme { pub attributes: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Error)] pub enum InclusiveError { #[error("out of range")] OutOfRange, } impl Expected for InclusiveError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { InclusiveError::OutOfRange => write!(f, "out of range"), } } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct Inclusive(u8); impl Inclusive<10> { fn char(&self) -> char { match self.0 { 1 => '1', 2 => '2', 3 => '3', 4 => '4', 5 => '5', 6 => '6', 7 => '7', 8 => '8', 9 => '9', 0 => '0', _ => unreachable!(), } } fn f_key(&self) -> Option { match self.0 { 1 => Some(Key::F1), 2 => Some(Key::F2), 3 => Some(Key::F3), 4 => Some(Key::F4), 5 => Some(Key::F5), 6 => Some(Key::F6), 7 => Some(Key::F7), 8 => Some(Key::F8), 9 => Some(Key::F9), _ => None, } } } impl Display for Inclusive { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } impl Deref for Inclusive { type Target = u8; fn deref(&self) -> &Self::Target { &self.0 } } impl TryFrom for Inclusive { type Error = InclusiveError; fn try_from(value: u8) -> Result { if value > MAX { return Err(InclusiveError::OutOfRange); } Ok(Inclusive(value)) } } impl From> for u8 { fn from(value: Inclusive) -> Self { value.0 } } impl Serialize for Inclusive { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { u8::serialize(&self.0, serializer) } } impl<'de, const MAX: u8> Deserialize<'de> for Inclusive { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let val = u8::deserialize(deserializer)?; if val > MAX { return Err(serde::de::Error::invalid_value( Unexpected::Unsigned(val as u64), &InclusiveError::OutOfRange, )); } Ok(Inclusive(val)) } } impl Inclusive {} #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Tag { FrontRow(Inclusive<10>), FunctionRow(Inclusive<10>), Other(String), } impl Tag { pub fn key(&self) -> Option { match self { Tag::FrontRow(idx) => Some(Key::Char(idx.char())), Tag::FunctionRow(idx) => Some(match idx.f_key() { Some(f) => f, None => return None, }), Tag::Other(_) => None, } } } impl PartialEq for Tag { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::FrontRow(l0), Self::FrontRow(r0)) => l0 == r0, (Self::FunctionRow(l0), Self::FunctionRow(r0)) => l0 == r0, (Self::Other(l0), Self::Other(r0)) => l0 == r0, _ => false, } } } impl PartialOrd for Tag { fn partial_cmp(&self, other: &Self) -> Option { match (self, other) { (Tag::FrontRow(lhs), Tag::FrontRow(rhs)) => Some(lhs.0.cmp(&rhs.0)), (Tag::FrontRow(_), Tag::FunctionRow(_)) => Some(Ordering::Greater), (Tag::FrontRow(_), Tag::Other(_)) => Some(Ordering::Greater), (Tag::FunctionRow(_), Tag::FrontRow(_)) => Some(Ordering::Less), (Tag::FunctionRow(lhs), Tag::FunctionRow(rhs)) => Some(lhs.0.cmp(&rhs.0)), (Tag::FunctionRow(_), Tag::Other(_)) => Some(Ordering::Greater), (Tag::Other(_), Tag::FrontRow(_)) | (Tag::Other(_), Tag::FunctionRow(_)) => { Some(Ordering::Less) } (Tag::Other(_), Tag::Other(_)) => None, } } } impl FromStr for Tag { type Err = Infallible; fn from_str(s: &str) -> Result { let mut chars = s.chars(); let indicator = match chars.next() { Some(i) => i, None => return Ok(Self::Other(s.to_string())), }; let number = u8::from_str(&chars.next().unwrap_or_default().to_string()).ok(); if number.is_none() { return Ok(Self::Other(s.to_string())); } let number = match number.unwrap().try_into() { Ok(number) => number, Err(_) => return Ok(Self::Other(s.to_string())), }; match indicator { '.' => Ok(Self::FrontRow(number)), 'F' => Ok(Self::FunctionRow(number)), _ => unreachable!(), } } } impl Display for Tag { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Tag::FrontRow(tag) => write!(f, ".{tag}"), Tag::FunctionRow(tag) => write!(f, "F{tag}"), Tag::Other(tag) => f.write_str(tag), } } } impl Default for Config { fn default() -> Self { let resize_step = 0.1; let mod_key = Key::Mod4Super; let font_bold = String::from("-*-fixed-*-*-*-*-13-*-*-*-*-*-*-*"); let active_color = Color::from_hex("#800080").expect("default active color"); let normal_color = Color::from_hex("#330033").expect("default normal color"); let urgent_color = Color::from_hex("#7811A1").expect("default urgent color"); let text_color = Color::from_hex("#898989").expect("default text color"); Self { mod_key, font: Some(String::from("-*-fixed-medium-*-*-*-12-*-*-*-*-*-*-*")), font_bold: Some(font_bold.clone()), keybinds: vec![ Keybind::new( [mod_key, Key::Shift, Key::Char('r')].into_iter(), HlwmCommand::Reload, ), Keybind::new( [mod_key, Key::Shift, Key::Char('c')].into_iter(), HlwmCommand::Close { window: None }, ), Keybind::new( [mod_key, Key::Char('s')].into_iter(), HlwmCommand::Spawn { executable: String::from("flameshot"), args: vec![String::from("gui")], }, ), Keybind::new( [mod_key, Key::Return].into_iter(), HlwmCommand::Spawn { executable: String::from("dmenu_run"), args: vec![], }, ), Keybind::new( [mod_key, Key::Left].into_iter(), HlwmCommand::Focus(Direction::Left), ), Keybind::new( [mod_key, Key::Right].into_iter(), HlwmCommand::Focus(Direction::Right), ), Keybind::new( [mod_key, Key::Up].into_iter(), HlwmCommand::Focus(Direction::Up), ), Keybind::new( [mod_key, Key::Down].into_iter(), HlwmCommand::Focus(Direction::Down), ), Keybind::new( [mod_key, Key::Shift, Key::Left].into_iter(), HlwmCommand::Shift(Direction::Left), ), Keybind::new( [mod_key, Key::Shift, Key::Right].into_iter(), HlwmCommand::Shift(Direction::Right), ), Keybind::new( [mod_key, Key::Shift, Key::Up].into_iter(), HlwmCommand::Shift(Direction::Up), ), Keybind::new( [mod_key, Key::Shift, Key::Down].into_iter(), HlwmCommand::Shift(Direction::Down), ), Keybind::new( [mod_key, Key::Char('u')].into_iter(), HlwmCommand::Split(Align::Bottom(Some(0.5))), ), Keybind::new( [mod_key, Key::Char('o')].into_iter(), HlwmCommand::Split(Align::Right(Some(0.5))), ), Keybind::new([mod_key, Key::Char('r')].into_iter(), HlwmCommand::Remove), Keybind::new( [mod_key, Key::Char('f')].into_iter(), HlwmCommand::Fullscreen(ToggleBool::Toggle), ), Keybind::new( [mod_key, Key::Space].into_iter(), HlwmCommand::Or { separator: Separator::Comma, commands: vec![ HlwmCommand::And { separator: Separator::Period, commands: vec![ HlwmCommand::Compare { attribute: String::from("tags.focus.curframe_wcount"), operator: Operator::Equal, value: String::from("2"), }, HlwmCommand::CycleLayout { delta: Some(Index::Relative(1)), layouts: vec![ FrameLayout::Vertical, FrameLayout::Horizontal, FrameLayout::Max, FrameLayout::Vertical, FrameLayout::Grid, ], }, ], }, HlwmCommand::CycleLayout { delta: Some(Index::Relative(1)), layouts: vec![], }, ], }, ), Keybind::new( [mod_key, Key::Control, Key::Left].into_iter(), HlwmCommand::Resize { direction: Direction::Left, fraction_delta: Some(Index::Relative(resize_step)), }, ), Keybind::new( [mod_key, Key::Control, Key::Down].into_iter(), HlwmCommand::Resize { direction: Direction::Down, fraction_delta: Some(Index::Relative(resize_step)), }, ), Keybind::new( [mod_key, Key::Control, Key::Up].into_iter(), HlwmCommand::Resize { direction: Direction::Up, fraction_delta: Some(Index::Relative(resize_step)), }, ), Keybind::new( [mod_key, Key::Control, Key::Right].into_iter(), HlwmCommand::Resize { direction: Direction::Right, fraction_delta: Some(Index::Relative(resize_step)), }, ), Keybind::new([mod_key, Key::Tab].into_iter(), HlwmCommand::Cycle), Keybind::new( [mod_key, Key::Char('i')].into_iter(), HlwmCommand::JumpTo(Window::Urgent), ), ], services: vec![Service { name: String::from("fcitx5"), arguments: vec![], }], tags: (1..=5) .into_iter() .map(|idx| Tag::FrontRow(idx.try_into().unwrap())) .chain(vec![ Tag::FunctionRow(1.try_into().unwrap()), Tag::FunctionRow(2.try_into().unwrap()), Tag::FunctionRow(3.try_into().unwrap()), Tag::FunctionRow(4.try_into().unwrap()), Tag::FunctionRow(5.try_into().unwrap()), ]) .collect(), mousebinds: vec![ Mousebind::new( mod_key, [Key::Mouse(MouseButton::Button1)].into_iter(), MousebindAction::Move, ), Mousebind::new( mod_key, [Key::Mouse(MouseButton::Button2)].into_iter(), MousebindAction::Zoom, ), Mousebind::new( mod_key, [Key::Mouse(MouseButton::Button3)].into_iter(), MousebindAction::Resize, ), ], theme: Theme { attributes: (|| -> Vec { ThemeAttr::iter() .map(|attr| { let mut attr = attr; match attr.borrow_mut() { ThemeAttr::TitleWhen(when) => *when = "multiple_tabs".into(), ThemeAttr::TitleFont(font) => *font = font_bold.clone(), ThemeAttr::TitleDepth(depth) => *depth = 3, ThemeAttr::InnerWidth(inw) => *inw = 0, ThemeAttr::TitleHeight(h) => *h = 15, ThemeAttr::BorderWidth(w) => *w = 1, ThemeAttr::TilingOuterWidth(w) => *w = 1, ThemeAttr::FloatingOuterWidth(w) => *w = 1, ThemeAttr::FloatingBorderWidth(w) => *w = 4, ThemeAttr::InnerColor(col) => *col = Color::BLACK, ThemeAttr::TitleColor(col) => *col = text_color.clone(), ThemeAttr::ActiveColor(col) => *col = active_color.clone(), ThemeAttr::NormalColor(col) => *col = normal_color.clone(), ThemeAttr::UrgentColor(col) => *col = urgent_color.clone(), ThemeAttr::BackgroundColor(col) => *col = Color::BLACK, ThemeAttr::ActiveInnerColor(col) => *col = active_color.clone(), ThemeAttr::ActiveOuterColor(col) => *col = active_color.clone(), ThemeAttr::NormalInnerColor(col) => *col = normal_color.clone(), ThemeAttr::NormalOuterColor(col) => *col = normal_color.clone(), ThemeAttr::NormalTitleColor(col) => *col = normal_color.clone(), ThemeAttr::UrgentInnerColor(col) => *col = urgent_color.clone(), ThemeAttr::UrgentOuterColor(col) => *col = urgent_color.clone(), } attr }) .collect() })(), }, rules: vec![ rule!( window_types!(Dialog, Utility, Splash), Consequence::Floating(true) ), rule!(Consequence::Focus(true)), rule!(Consequence::FloatPlacement(FloatPlacement::Smart)), rule!(window_types!(Dialog), Consequence::Floating(true)), rule!( window_types!(Notification, Dock, Desktop), Consequence::Manage(false) ), rule!(Condition::FixedSize, Consequence::Floating(true)), ], attributes: vec![], settings: vec![ Setting::FrameBorderWidth(2), Setting::ShowFrameDecorations(ShowFrameDecoration::FocusedIfMultiple), Setting::TreeStyle(String::from("╾│ ├└╼─┐")), Setting::FrameBgTransparent(true.into()), Setting::SmartWindowSurroundings(false.into()), Setting::SmartFrameSurroundings(SmartFrameSurroundings::HideAll), Setting::FrameTransparentWidth(5), Setting::FrameGap(3), Setting::WindowGap(0), Setting::FramePadding(0), Setting::MouseRecenterGap(0), Setting::FrameBorderActiveColor(active_color.clone()), Setting::FrameBgActiveColor(active_color.clone()), ], } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Service { pub name: String, pub arguments: Vec, } #[cfg(test)] mod test { use pretty_assertions::assert_eq; use serde::{Deserialize, Serialize}; use crate::hlwm::{ attribute::Attribute, color::{Color, X11Color}, }; use super::{Config, SetAttribute}; #[test] fn config_serialize_deserialize() { let mut cfg = Config::default(); // Add extra attrs for testing cfg.attributes.push(SetAttribute { path: "my.attr.path".into(), value: Attribute::Color(Color::X11(X11Color::NavajoWhite3)), }); let cfg_serialized = cfg.serialize().unwrap(); let parsed_cfg = Config::deserialize(&cfg_serialized).unwrap(); assert_eq!(cfg, parsed_cfg,) } #[derive(Debug, Serialize, Deserialize)] pub struct AttrWrapper { attr: SetAttribute, } #[test] fn set_attribute_serialize_deserialize() { let attr = AttrWrapper { attr: SetAttribute { path: "MyCool.Path".into(), value: Attribute::Color(Color::X11(X11Color::LemonChiffon2)), }, }; let serialized = toml::to_string_pretty(&attr).expect("serialize"); let parsed: AttrWrapper = toml::from_str(&serialized).expect("unserailize"); assert_eq!(attr.attr, parsed.attr,) } }