use std::{ borrow::BorrowMut, collections::HashMap, env, fmt::{Debug, Display}, fs::{self}, io, ops::Deref, path::Path, str::FromStr, string::FromUtf8Error, }; use log::info; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use thiserror::Error; use crate::{ environ::{self, ActiveKeybinds}, hlwm::{ self, attribute::{Attribute, AttributeType}, color::Color, command::{CommandError, HlwmCommand}, hook::Hook, key::{Key, KeyUnbind, Keybind, MouseButton, Mousebind, MousebindAction}, parser::{ArgParser, FromCommandArgs, FromStrings, ParseError}, rule::{Condition, Consequence, FloatPlacement, Rule, Unrule}, setting::{FrameLayout, Setting, ShowFrameDecoration, SmartFrameSurroundings}, theme::ThemeAttr, window::Window, Align, Client, Direction, Index, Operator, Separator, ToggleBool, }, logerr::UnwrapLog, rule, split, 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 value: {0}")] ParseError(#[from] ParseError), } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Config { pub use_panel: bool, pub font: String, pub font_bold: String, /// Pango font for use where pango fonts are /// used (i.e. panel) pub font_pango: String, /// Bold version of `font_pango` pub font_pango_bold: String, 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 FromStrings for SetAttribute { fn from_strings>(s: I) -> Result { let s = s.collect::>(); let original_str = s.join(" "); let mut parser = ArgParser::from_strings(s.into_iter()); let x = parser.must_string("set_attribute(type/name)")?; let (attr_ty, remainder) = split::parens(&x).ok_or(ParseError::InvalidValue { value: original_str, expected: "does not have a valid type string (type)", })?; if remainder.contains('=') { let mut parser = ArgParser::from_strings(remainder.split('=').map(|c| c.to_string())); return Ok(Self { path: parser.must_string("set_attribute(path)")?, value: Attribute::new( AttributeType::from_str(&attr_ty)?, &parser.must_string("set_attribute(value)")?, )?, }); } if parser.must_string("set_attribute(equals)")? != "=" { return Err(ParseError::ValueMissing); } Ok(Self { path: remainder, value: Attribute::new( AttributeType::from_str(&attr_ty)?, &parser.must_string("set_attribute(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_strings(split::tab_or_space(&str_val).into_iter()).map_err(|_| { serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &ExpectedKey) })?, ) } } impl Config { const USE_PANEL: &'static str = "settings.my_hlctl_panel_enabled"; const FONT: &'static str = "theme.my_font"; const FONT_BOLD: &'static str = "theme.my_font_bold"; const FONT_PANGO: &'static str = "theme.my_font_pango"; const FONT_PANGO_BOLD: &'static str = "theme.my_font_pango_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_log("") ); 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_log("") ); 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::USE_PANEL, Some(self.use_panel.to_string())), (Self::FONT, Some(self.font.clone())), (Self::FONT_BOLD, Some(self.font_bold.clone())), (Self::FONT_PANGO, Some(self.font_pango.clone())), (Self::FONT_PANGO_BOLD, Some(self.font_pango_bold.clone())), ( Self::SERVICES, if self.services.len() == 0 { None } else { Some(serde_json::ser::to_string(&self.services)?) }, ), (Self::MOD_KEY, Some(self.mod_key.to_string())), ] .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, }, ] }) .flatten() .collect() } fn rule_command_set(&self) -> Vec { [HlwmCommand::Unrule(Unrule::All)] .into_iter() .chain(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.theme_command_set()) .chain(self.attrs_set()?) .chain(self.tag_command_set()) .chain(self.settings_command_set()) .chain(self.rule_command_set()) .chain(self.service_command_set()) .chain([HlwmCommand::Unlock]) .collect()) } fn tag_command_set(&self) -> Vec { info!("loading tag command set"); (&self.tags) .into_iter() .map(|tag| tag.to_command_set(self.mod_key)) .flatten() .chain([HlwmCommand::And { separator: Separator::Comma, commands: vec![ HlwmCommand::Substitute { identifier: "CURRENT_INDEX".into(), attribute_path: "tags.focus.index".into(), command: Box::new(HlwmCommand::Compare { attribute: "tags.by-name.default.index".into(), operator: Operator::Equal, value: "CURRENT_INDEX".into(), }), }, HlwmCommand::UseIndex { index: Index::Relative(1), skip_visible: false, }, HlwmCommand::MergeTag { tag: "default".to_string(), target: Some(hlwm::TagSelect::Name( self.tags.first().cloned().unwrap().name().to_string(), )), }, ], } .to_try()]) .collect() } /// Create a config gathered from the herbstluftwm configs, /// using default mouse binds/attributes set pub fn from_herbstluft() -> Self { fn setting Result>( name: &str, f: F, default: T, ) -> T where T: Debug, { match Client::new().get_attr(name.to_string()) { Ok(setting) => f(&setting.to_string()).unwrap_or_log(default), Err(_) => default, } } let client = Client::new(); let default = Config::default(); let mod_key = setting(Self::MOD_KEY, Key::from_str, default.mod_key); Config { use_panel: client .get_from_str_attr(Self::USE_PANEL.to_string()) .unwrap_or_log(default.use_panel), font: client .get_str_attr(Self::FONT.to_string()) .unwrap_or_log(default.font), font_bold: client .get_str_attr(Self::FONT_BOLD.to_string()) .unwrap_or_log(default.font_bold), font_pango: client .get_str_attr(Self::FONT_PANGO.to_string()) .unwrap_or_log(default.font_pango), font_pango_bold: client .get_str_attr(Self::FONT_PANGO_BOLD.to_string()) .unwrap_or_log(default.font_pango_bold), mod_key, 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_command_args( &attr_path, [client .get_attr(attr_path.clone()) .map(|t| t.to_string()) .unwrap_or_log(default.to_string())] .into_iter(), ) .unwrap_or_log(default.clone()) }) .collect() })(), }, keybinds: environ::active_keybinds(ActiveKeybinds::OmitNamedTagBinds) .unwrap_or_log(default.keybinds), tags: Self::active_tags(mod_key).unwrap_or_log(default.tags), rules: (|| -> Result, ConfigError> { Ok(HlwmCommand::ListRules .execute_str()? .split('\n') .map(|l| l.trim()) .filter(|l| !l.is_empty()) .map(|line| Rule::from_strings(split::tab_or_space(line).into_iter())) .collect::>()?) })() .unwrap_or_log(default.rules), settings: (|| -> Result, CommandError> { default .settings .clone() .into_iter() .map(|s| Ok(client.get_setting(s.into())?)) .collect::, CommandError>>() })() .unwrap_or_log(default.settings), ..default } } fn active_tags(mod_key: Key) -> Result, ConfigError> { let mut by_tag: HashMap> = HashMap::new(); environ::active_keybinds(ActiveKeybinds::OnlyNamedTagBinds)? .into_iter() .filter(|bind| match bind.command.deref() { HlwmCommand::UseTag(_) | HlwmCommand::MoveTag(_) => true, _ => false, }) .for_each(|bind| match bind.command.deref() { HlwmCommand::UseTag(tag) | HlwmCommand::MoveTag(tag) => match by_tag.get_mut(tag) { Some(coll) => coll.push(bind.clone()), None => { by_tag.insert(tag.to_string(), vec![bind]); } }, _ => unreachable!(), }); Ok(by_tag .into_iter() .map(|(name, binds)| { let standard = binds.len() != 0 && (&binds).into_iter().all(|bind| match bind.command.deref() { HlwmCommand::UseTag(_) => { bind.keys.len() == 2 && bind.keys[0] == mod_key && bind.keys[1].is_standard() } HlwmCommand::MoveTag(_) => { bind.keys.len() == 3 && bind.keys[0] == mod_key && bind.keys[1] == Key::Shift && bind.keys[2].is_standard() } // Filtered out above _ => unreachable!(), }); if standard { let key = *binds.first().unwrap().keys.last().unwrap(); let key_same = (&binds) .into_iter() .all(|bind| bind.keys.last().unwrap().eq(&key)); if key_same { // Actually standard return Tag::standard(name, key); } } let mut use_keys = None; let mut move_keys = None; for bind in binds { match bind.command.deref() { HlwmCommand::UseTag(_) => { if use_keys.is_none() { use_keys = Some(bind.keys); } } HlwmCommand::MoveTag(_) => { if move_keys.is_none() { move_keys = Some(bind.keys); } } // Filtered out above _ => unreachable!(), } } Tag::other(name, use_keys, move_keys) }) .collect()) } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Theme { pub attributes: Vec, } #[allow(unused)] impl Theme { pub fn active_color(&self) -> Option { for attr in &self.attributes { if let ThemeAttr::ActiveColor(col) = attr { return Some(col.clone()); } } None } pub fn normal_color(&self) -> Option { for attr in &self.attributes { if let ThemeAttr::NormalColor(col) = attr { return Some(col.clone()); } } None } pub fn urgent_color(&self) -> Option { for attr in &self.attributes { if let ThemeAttr::UrgentColor(col) = attr { return Some(col.clone()); } } None } pub fn text_color(&self) -> Option { for attr in &self.attributes { if let ThemeAttr::TitleColor(col) = attr { return Some(col.clone()); } } None } } 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 { use_panel: true, mod_key, font: String::from("-*-fixed-medium-*-*-*-12-*-*-*-*-*-*-*"), font_bold: 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: [ Tag::standard(".1", Key::Char('1')), Tag::standard(".2", Key::Char('2')), Tag::standard(".3", Key::Char('3')), Tag::standard(".4", Key::Char('4')), Tag::standard(".5", Key::Char('5')), Tag::standard("F1", Key::F1), Tag::standard("F2", Key::F2), Tag::standard("F3", Key::F3), Tag::standard("F4", Key::F4), Tag::standard("F5", Key::F5), ] .into(), 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()), ], font_pango: String::from("Monospace 9"), font_pango_bold: String::from("Monospace Bold 9"), } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Service { pub name: String, pub arguments: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum Tag { Standard { name: String, key: Key, }, Other { name: String, use_keys: Option>, move_keys: Option>, }, } impl Tag { pub fn standard>(name: S, key: Key) -> Self { Self::Standard { key, name: name.into(), } } pub fn other, K: IntoIterator>( name: S, use_keys: Option, move_keys: Option, ) -> Self { Self::Other { name: name.into(), use_keys: use_keys.map(|k| k.into_iter().collect()), move_keys: move_keys.map(|k| k.into_iter().collect()), } } pub fn name(&self) -> &str { match self { Tag::Standard { name, key: _ } => name.as_str(), Tag::Other { name, use_keys: _, move_keys: _, } => name.as_str(), } } fn use_keybind(&self, mod_key: Key) -> Option { match self { Tag::Standard { name, key } => Some(Keybind { keys: vec![mod_key, *key], command: Box::new(HlwmCommand::UseTag(name.clone())), }), Tag::Other { name, use_keys, move_keys: _, } => use_keys.clone().map(|use_keys| Keybind { keys: use_keys, command: Box::new(HlwmCommand::UseTag(name.clone())), }), } } fn move_keybind(&self, mod_key: Key) -> Option { match self { Tag::Standard { name, key } => Some(Keybind { keys: vec![mod_key, Key::Shift, *key], command: Box::new(HlwmCommand::MoveTag(name.clone())), }), Tag::Other { name, use_keys: _, move_keys, } => move_keys.clone().map(|move_keys| Keybind { keys: move_keys, command: Box::new(HlwmCommand::MoveTag(name.clone())), }), } } pub fn to_command_set(&self, mod_key: Key) -> Vec { let mut commands = Vec::with_capacity(3); commands.push(HlwmCommand::AddTag(self.name().to_string())); if let Some(keybind) = self.use_keybind(mod_key) { commands.push(HlwmCommand::Keybind(keybind)); } if let Some(keybind) = self.move_keybind(mod_key) { commands.push(HlwmCommand::Keybind(keybind)); } commands } } impl From<(S, Key)> for Tag where S: Into, { fn from((name, key): (S, Key)) -> Self { Self::Standard { name: name.into(), key, } } } #[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,) } }