use std::{io, process::ExitStatus, str::FromStr, string::FromUtf8Error}; use strum::IntoEnumIterator; use serde::{de::Expected, Deserialize, Serialize}; use thiserror::Error; use crate::{gen_parse, hlwm::Client}; use super::{ attribute::{Attribute, AttributeError, AttributeOption}, hlwmbool::ToggleBool, hook::Hook, key::{KeyUnbind, Keybind, Mousebind}, rule::Rule, setting::{FrameLayout, Setting, SettingName}, split, window::Window, Align, Direction, Index, Monitor, Operator, Separator, StringParseError, Tag, ToCommandString, }; #[derive(Debug, Clone, strum::Display, strum::EnumIter, PartialEq)] #[strum(serialize_all = "snake_case")] pub enum HlwmCommand { /// Quits herbstluftwm. Quit, /// Prints the version of the running herbstluftwm instance Version, /// Ignores all arguments and always returns success, i.e. 0 True, /// Ignores all arguments and always returns failure, i.e. 1. False, /// Prints all given `args` separated by a single space and a newline afterwards Echo(Vec), /// Executes the autostart file. Reload, /// Closes the specified `window` gracefully or the focused window /// if none is given explicitly Close { window: Option, }, /// Spawns an `executable` with its `args`. Spawn { executable: String, args: Vec, }, /// List currently configured monitors with their index, /// area (as rectangle), name (if named) and currently viewed tag. ListMonitors, /// Lists all bound keys with their associated command ListKeybinds, /// Lists all active rules. ListRules, /// Increases the monitors_locked setting. /// Use this if you want to do multiple window actions at once /// (i.e. without repainting between the single steps). /// /// See also: [Command::Unlock] Lock, /// Decreases the monitors_locked setting. /// If monitors_locked is changed to 0, then all monitors are repainted again. /// See also: [Command::Lock] Unlock, /// Print the value of the specified attribute GetAttr(String), /// Assign `new_value` to the specified `attribute` SetAttr { path: String, new_value: Attribute, }, /// Prints the children and attributes of the given object addressed by `path`. /// If `path` is an attribute, then print the attribute value. /// If `new_value` is given, assign `new_value` to the attribute given by `path`. Attr { path: String, new_value: Option, }, /// Creates a new attribute with the name and in the object specified by `path`. /// Its type is specified by `attr`. /// The attribute name has to begin with my_. /// If `value` is supplied, then it is written to the attribute /// (if this fails the attribute still remains). NewAttr { path: String, attr: AttributeOption, }, /// Print the type of the specified attribute AttrType(String), /// Removes the user defined attribute RemoveAttr(String), /// Sets the specified [Setting]. /// Allowed values for boolean settings are on or true for on, /// off or false for off, toggle to toggle its value Set(Setting), Get(SettingName), /// Emits a custom `hook` to all idling herbstclients EmitHook(Hook), Substitute, Keybind(Keybind), Keyunbind(KeyUnbind), Mousebind(Mousebind), /// Removes all mouse bindings Mouseunbind, UseIndex { index: Index, skip_visible: bool, }, MoveIndex { index: Index, skip_visible: bool, }, #[strum(serialize = "jumpto")] JumpTo(Window), /// Creates a new empty tag with the given name #[strum(serialize = "add")] AddTag(String), /// Switches the focused monitor to specified tag #[strum(serialize = "use")] UseTag(String), /// Moves the focused window to the tag with the given name #[strum(serialize = "move")] MoveTag(String), /// Removes tag named `tag` and moves all its windows to tag `target`. /// If `target` is None, the focused tag will be used MergeTag { tag: String, target: Option, }, Cycle, Focus(Direction), Shift(Direction), Split(Align), Remove, Fullscreen(ToggleBool), CycleLayout { delta: Option>, layouts: Vec, }, Resize { direction: Direction, fraction_delta: Option>, }, Watch, Or { separator: Separator, commands: Vec, }, And { separator: Separator, commands: Vec, }, Compare { attribute: String, operator: Operator, value: String, }, /// Print a tab separated list of all tags for the specified `monitor` index. /// If no `monitor` index is given, the focused monitor is used. TagStatus { monitor: Option, }, /// Defines a rule which will be applied to all new clients Rule(Rule), /// executes the provided command, prints its output, but always returns success, i.e. 0 Try(Box), /// executes the provided command, but discards its output and only returns its exit code. Silent(Box), } impl FromStr for Box { type Err = CommandParseError; fn from_str(s: &str) -> Result { Ok(Box::new(HlwmCommand::from_str(s)?)) } } impl Serialize for HlwmCommand { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(&self.to_command_string().replace("\t", " ")) } } impl<'de> Deserialize<'de> for HlwmCommand { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { pub enum Expect { NotEmpty, } impl Expected for Expect { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Expect::NotEmpty => write!(f, "value not being empty"), } } } let str_val: String = Deserialize::deserialize(deserializer)?; let parts = split::tab_or_space(&str_val); if parts.is_empty() { return Err(serde::de::Error::invalid_length(0, &Expect::NotEmpty)); } let mut parts = parts.into_iter(); let command = parts.next().unwrap(); let args = parts.collect(); Ok(Self::from_raw_parts(&command, args).map_err(|err| { serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &err) })?) } } impl FromStr for HlwmCommand { type Err = CommandParseError; fn from_str(s: &str) -> Result { let mut parts = s.split("\t"); let command = parts .next() .ok_or(CommandParseError::UnknownCommand(s.to_string()))?; let args = parts.map(String::from).collect(); HlwmCommand::from_raw_parts(command, args) } } impl Default for HlwmCommand { fn default() -> Self { HlwmCommand::Quit } } #[derive(Debug, Error)] pub enum CommandParseError { #[error("unknown command [{0}]")] UnknownCommand(String), #[error("bad argument for command [{command}]")] BadArgument { command: String }, #[error("missing required argument")] MissingArgument, #[error("invalid argument count [{0}] at [{1}]")] InvalidArgumentCount(usize, String), #[error("error parsing attribute: [{0}]")] AttributeError(#[from] AttributeError), #[error("command execution error: [{0}]")] CommandError(#[from] CommandError), #[error("string utf8 error")] StringUtf8Error(#[from] FromUtf8Error), #[error("parsing string value error: [{0}]")] StringParseError(#[from] StringParseError), } impl serde::de::Expected for CommandParseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "expected valid command string. Got error: {self}") } } fn trim_quotes(itm: String) -> String { if itm.starts_with('"') && itm.ends_with('"') { itm.trim_matches('"').to_string() } else { itm } } impl HlwmCommand { pub fn silent(self) -> HlwmCommand { HlwmCommand::Silent(Box::new(self)) } pub fn to_try(self) -> HlwmCommand { HlwmCommand::Try(Box::new(self)) } pub fn from_raw_parts(command: &str, args: Vec) -> Result { let command = HlwmCommand::iter() .find(|cmd| cmd.to_string() == command) .ok_or(CommandParseError::UnknownCommand(command.to_string()))?; gen_parse!(command, args); let parsed_command = match command { HlwmCommand::Quit | HlwmCommand::Lock | HlwmCommand::Cycle | HlwmCommand::Watch | HlwmCommand::Reload | HlwmCommand::Remove | HlwmCommand::Unlock | HlwmCommand::Version | HlwmCommand::ListRules | HlwmCommand::False | HlwmCommand::True | HlwmCommand::Substitute | HlwmCommand::Mouseunbind | HlwmCommand::ListMonitors | HlwmCommand::ListKeybinds => Ok::<_, CommandParseError>(command.clone()), HlwmCommand::Echo(_) => Ok(Self::Echo(args)), HlwmCommand::Close { window: _ } => parse!(window: [Option] => Close), HlwmCommand::Spawn { executable: _, args: _, } => { parse!(executable: String, args: [Vec] => Spawn).map(|spawn| match spawn { HlwmCommand::Spawn { executable, args } => HlwmCommand::Spawn { executable: trim_quotes(executable), args: args.into_iter().map(trim_quotes).collect(), }, _ => unreachable!(), }) } HlwmCommand::GetAttr(_) => parse!(String => GetAttr), HlwmCommand::SetAttr { path: _, new_value: _, } => { let mut args = args.into_iter(); let path = args.next().ok_or(CommandParseError::BadArgument { command: command.to_string(), })?; Ok(HlwmCommand::SetAttr { path: path.clone(), new_value: { Attribute::new( &String::from_utf8( Client::new() .execute(HlwmCommand::AttrType(path.clone()))? .stdout, )? .split('\n') .next() .ok_or(CommandParseError::CommandError(CommandError::Empty))?, &args.collect::>().join(" "), )? }, }) } HlwmCommand::Attr { path: _, new_value: _, } => { let mut args = args.into_iter(); Ok(HlwmCommand::Attr { path: args.next().ok_or(CommandParseError::BadArgument { command: command.to_string(), })?, new_value: { let args = args.collect::>(); if args.is_empty() { None } else { Some(Attribute::guess_type(&args.join("\t"))) } }, }) } HlwmCommand::NewAttr { path: _, attr: _ } => { let mut args = args.into_iter(); let attr_type = args.next().ok_or(CommandParseError::BadArgument { command: command.to_string(), })?; let path = args.next().ok_or(CommandParseError::BadArgument { command: command.to_string(), })?; Ok(HlwmCommand::NewAttr { path, attr: { let attr = args.collect::>(); let attr = if attr.len() == 0 { None } else { Some(attr.join("\t")) }; AttributeOption::new(&attr_type, &attr)? }, }) } HlwmCommand::AttrType(_) => parse!(String => AttrType), HlwmCommand::RemoveAttr(_) => parse!(String => RemoveAttr), HlwmCommand::Set(_) => parse!(FromStrAll => Set), HlwmCommand::EmitHook(_) => parse!(FromStr => EmitHook), HlwmCommand::Keybind(_) => parse!(FromStrAll => Keybind), HlwmCommand::Keyunbind(_) => parse!(FromStr => Keyunbind), HlwmCommand::Mousebind(_) => parse!(FromStrAll => Mousebind), HlwmCommand::JumpTo(_) => parse!(FromStr => JumpTo), HlwmCommand::AddTag(_) => parse!(String => AddTag), HlwmCommand::MergeTag { tag: _, target: _ } => { parse!(tag: String, target: [Option] => MergeTag) } HlwmCommand::Focus(_) => parse!(FromStr => Focus), HlwmCommand::Shift(_) => parse!(FromStr => Shift), HlwmCommand::Split(_) => parse!(FromStrAll => Split), HlwmCommand::Fullscreen(_) => parse!(FromStr => Fullscreen), HlwmCommand::CycleLayout { delta: _, layouts: _, } => { if args.is_empty() { Ok(HlwmCommand::CycleLayout { delta: None, layouts: vec![], }) } else { let first = args.first().ok_or(CommandParseError::BadArgument { command: command.to_string(), })?; match FrameLayout::from_str(first) { Ok(_) => { // only frame layouts Ok(HlwmCommand::CycleLayout { delta: None, layouts: args .into_iter() .map(|i| FrameLayout::from_str(&i)) .collect::>() .map_err(|_| CommandParseError::BadArgument { command: command.to_string(), })?, }) } Err(_) => { // Has index let mut args = args.into_iter(); Ok(HlwmCommand::CycleLayout { delta: Some(args.next().unwrap().parse().map_err(|_| { CommandParseError::BadArgument { command: command.to_string(), } })?), layouts: args .map(|i| FrameLayout::from_str(&i)) .collect::>() .map_err(|_| CommandParseError::BadArgument { command: command.to_string(), })?, }) } } } } HlwmCommand::Resize { direction: _, fraction_delta: _, } => parse!(direction: FromStr, fraction_delta: [Option] => Resize), HlwmCommand::Or { separator: _, commands: _, } => parse!(And_Or => Or), HlwmCommand::And { separator: _, commands: _, } => parse!(And_Or => And), HlwmCommand::Compare { attribute: _, operator: _, value: _, } => parse!(attribute: String, operator: FromStr, value: String => Compare), HlwmCommand::TagStatus { monitor: _ } => { parse!(monitor: [Option] => TagStatus) } HlwmCommand::Rule(_) => parse!(FromStrAll => Rule), HlwmCommand::Get(_) => parse!(FromStr => Get), HlwmCommand::MoveIndex { index: _, skip_visible: _, } => { if args.contains(&"--skip-visible".to_string()) { Ok(HlwmCommand::MoveIndex { index: args .into_iter() .filter(|a| a != "--skip-visible") .next() .ok_or(CommandParseError::BadArgument { command: command.to_string(), })? .parse()?, skip_visible: true, }) } else { Ok(HlwmCommand::MoveIndex { index: args .into_iter() .next() .ok_or(CommandParseError::BadArgument { command: command.to_string(), })? .parse()?, skip_visible: false, }) } } HlwmCommand::UseIndex { index: _, skip_visible: _, } => { if args.contains(&"--skip-visible".to_string()) { Ok(HlwmCommand::UseIndex { index: args .into_iter() .filter(|a| a != "--skip-visible") .next() .ok_or(CommandParseError::BadArgument { command: command.to_string(), })? .parse()?, skip_visible: true, }) } else { Ok(HlwmCommand::UseIndex { index: args .into_iter() .next() .ok_or(CommandParseError::BadArgument { command: command.to_string(), })? .parse()?, skip_visible: false, }) } } HlwmCommand::UseTag(_) => parse!(String => UseTag), HlwmCommand::MoveTag(_) => parse!(String => MoveTag), HlwmCommand::Try(_) => parse!(FromStrAll => Try), HlwmCommand::Silent(_) => parse!(FromStrAll => Silent), }?; assert_eq!(command.to_string(), parsed_command.to_string()); Ok(parsed_command) } } impl HlwmCommand { #[inline(always)] pub(crate) fn args(&self) -> Vec { if let Self::Spawn { executable, args } = self { return vec!["spawn".to_string(), executable.to_string()] .into_iter() .chain(args.into_iter().cloned()) .collect(); } self.to_command_string() .split('\t') .map(|a| a.to_string()) .collect() } } #[derive(Debug, Error)] pub enum CommandError { #[error("IO error")] IoError(#[from] io::Error), #[error("exited with status code {0}")] StatusCode(i32, Option), #[error("killed by signal ({signal}); core dumped: {core_dumped}")] KilledBySignal { signal: i32, core_dumped: bool }, #[error("stopped by signal ({0})")] StoppedBySignal(i32), #[error("exit status not checked: {0:?}")] OtherExitStatus(ExitStatus), #[error("invalid utf8 string in response: {0:?}")] UtfError(#[from] FromUtf8Error), #[error("attribute error: {0:?}")] AttributeError(#[from] AttributeError), #[error("string parse error: {0}")] StringParseError(#[from] StringParseError), #[error("unexpected empty result")] Empty, } impl ToCommandString for HlwmCommand { fn to_command_string(&self) -> String { let cmd_string = match self { HlwmCommand::Quit | HlwmCommand::Lock | HlwmCommand::Cycle | HlwmCommand::Unlock | HlwmCommand::Remove | HlwmCommand::Reload | HlwmCommand::Version | HlwmCommand::ListRules | HlwmCommand::Substitute | HlwmCommand::Mouseunbind | HlwmCommand::True | HlwmCommand::False | HlwmCommand::ListMonitors | HlwmCommand::ListKeybinds | HlwmCommand::Watch => self.to_string(), HlwmCommand::Echo(args) => format!("{self}\t{}", args.join("\t")), HlwmCommand::Close { window } => format!( "{self}\t{}", window.as_ref().map(|w| w.to_string()).unwrap_or_default(), ), HlwmCommand::Spawn { executable, args } => { format!( "{self}\t\"{executable}\"\t{}", (&args) .into_iter() .map(|arg| format!("\"{arg}\"")) .collect::>() .join("\t") ) } HlwmCommand::GetAttr(attr) => format!("{self}\t{attr}"), HlwmCommand::SetAttr { path: attribute, new_value, } => format!("{self}\t{attribute}\t{new_value}"), HlwmCommand::Attr { path, new_value } => { format!( "{self}\t{path}\t{}", new_value .as_ref() .map(|val| val.to_string()) .unwrap_or_default() ) } HlwmCommand::NewAttr { path, attr } => { format!( "{self}\t{ty}\t{path}\t{attr}", ty = attr.type_string(), attr = attr.value_string().unwrap_or_default() ) } HlwmCommand::AttrType(attr) | HlwmCommand::RemoveAttr(attr) => { format!("{self}\t{attr}") } HlwmCommand::Set(setting) => format!("{self}\t{}", setting.to_command_string()), HlwmCommand::EmitHook(hook) => format!("{self}\t{}", hook.to_command_string()), HlwmCommand::Keybind(keybind) => { format!("{self}\t{}", keybind.to_command_string()) } HlwmCommand::Mousebind(mousebind) => format!("{self}\t{}", mousebind.to_string()), HlwmCommand::Keyunbind(key_unbind) => format!("{self}\t{key_unbind}"), HlwmCommand::MoveIndex { index, skip_visible, } => format!( "{self}\t{index}{}", if *skip_visible { "\t--skip-visible" } else { "" } ), HlwmCommand::JumpTo(win) => format!("{self}\t{win}"), HlwmCommand::AddTag(tag) => format!("{self}\t{tag}"), HlwmCommand::MergeTag { tag, target } => format!( "{self}\t{tag}\t{}", target.as_ref().map(|t| t.to_string()).unwrap_or_default() ), HlwmCommand::Focus(dir) | HlwmCommand::Shift(dir) => format!("{self}\t{dir}"), HlwmCommand::Split(split) => format!("{self}\t{}", split.to_command_string()), HlwmCommand::Fullscreen(option) => format!( "{self}\t{}", match option { ToggleBool::Bool(b) => match b { true => "on", false => "off", }, ToggleBool::Toggle => "toggle", } ), HlwmCommand::And { separator, commands, } | HlwmCommand::Or { separator, commands, } => format!( "{self}\t{separator}\t{}", commands .into_iter() .map(|c| c.to_command_string()) .collect::>() .join(&format!("\t{separator}\t")) ), HlwmCommand::Compare { attribute, operator, value, } => format!("{self}\t{attribute}\t{operator}\t{value}"), HlwmCommand::CycleLayout { delta, layouts } => { let mut command = self.to_string(); if let Some(delta) = delta { command = format!("{command}\t{delta}"); } if layouts.len() != 0 { command = format!( "{command}\t{}", layouts .into_iter() .map(|l| l.to_string()) .collect::>() .join("\t") ) } command } HlwmCommand::Resize { direction, fraction_delta, } => format!( "{self}\t{direction}\t{}", fraction_delta .as_ref() .map(|d| d.to_string()) .unwrap_or_default() ), HlwmCommand::TagStatus { monitor } => { format!("{self}\t{}", monitor.unwrap_or_default()) } HlwmCommand::Rule(rule) => format!("{self}\t{}", rule.to_command_string()), HlwmCommand::Get(setting) => format!("{self}\t{setting}"), HlwmCommand::UseIndex { index, skip_visible, } => { if *skip_visible { format!("{self}\t{index}\t--skip-visible") } else { format!("{self}\t{index}") } } HlwmCommand::UseTag(tag) | HlwmCommand::MoveTag(tag) => format!("{self}\t{tag}"), HlwmCommand::Try(cmd) | HlwmCommand::Silent(cmd) => { format!("{self}\t{}", cmd.to_command_string()) } }; if let Some(s) = cmd_string.strip_suffix('\t') { return s.to_string(); } cmd_string } } #[cfg(test)] mod test { use strum::IntoEnumIterator; use crate::hlwm::{ attribute::{Attribute, AttributeOption}, command::{FrameLayout, HlwmCommand, Index, Operator, Separator}, hlwmbool::ToggleBool, hook::Hook, key::{Key, KeyUnbind, MouseButton, Mousebind, MousebindAction}, rule::{Condition, Consequence, Rule, RuleOperator}, setting::{Setting, SettingName}, window::Window, Align, Direction, Tag, ToCommandString, }; use pretty_assertions::assert_eq; #[test] fn hlwm_command_string_and_back() { let commands = HlwmCommand::iter() .map(|cmd| match cmd { HlwmCommand::Quit | HlwmCommand::Version | HlwmCommand::Reload | HlwmCommand::Lock | HlwmCommand::Remove | HlwmCommand::Cycle | HlwmCommand::ListMonitors | HlwmCommand::ListRules | HlwmCommand::ListKeybinds | HlwmCommand::Unlock | HlwmCommand::True | HlwmCommand::False | HlwmCommand::Substitute | HlwmCommand::Mouseunbind => (cmd.clone(), cmd.to_string()), HlwmCommand::Echo(_) => ( HlwmCommand::Echo(vec!["Hello world!".into()]), "echo\tHello world!".into(), ), HlwmCommand::Close { window: _ } => ( HlwmCommand::Close { window: Some(Window::LastMinimized), }, "close\tlast-minimized".into(), ), HlwmCommand::Spawn { executable: _, args: _, } => ( HlwmCommand::Spawn { executable: "grep".into(), args: vec!["content".into()], }, "spawn\t\"grep\"\t\"content\"".into(), ), HlwmCommand::GetAttr(_) => ( HlwmCommand::GetAttr("my_attr".into()), "get_attr\tmy_attr".into(), ), HlwmCommand::SetAttr { path: _, new_value: _, } => ( HlwmCommand::SetAttr { path: "theme.color".into(), new_value: Attribute::Color("#000000".parse().unwrap()), }, "set_attr\ttheme.color\t#000000".into(), ), HlwmCommand::Attr { path: _, new_value: _, } => ( HlwmCommand::Attr { path: "my_attr".into(), new_value: Some(Attribute::String("hello".into())), }, "attr\tmy_attr\thello".into(), ), HlwmCommand::NewAttr { path: _, attr: _ } => ( HlwmCommand::NewAttr { path: "my_attr".into(), attr: AttributeOption::String(Some("hello".into())), }, "new_attr\tstring\tmy_attr\thello".into(), ), HlwmCommand::AttrType(_) => ( HlwmCommand::AttrType("my_attr".into()), "attr_type\tmy_attr".into(), ), HlwmCommand::RemoveAttr(_) => ( HlwmCommand::RemoveAttr("my_attr".into()), "remove_attr\tmy_attr".into(), ), HlwmCommand::Set(_) => ( HlwmCommand::Set(Setting::AutoDetectMonitors(ToggleBool::Toggle)), "set\tauto_detect_monitors\ttoggle".into(), ), HlwmCommand::EmitHook(_) => ( HlwmCommand::EmitHook(Hook::Reload), "emit_hook\treload".into(), ), HlwmCommand::Keybind(_) => ( HlwmCommand::Keybind("Mod4+1\treload".parse().unwrap()), "keybind\tMod4+1\treload".into(), ), HlwmCommand::Keyunbind(_) => ( HlwmCommand::Keyunbind(KeyUnbind::All), "keyunbind\t--all".into(), ), HlwmCommand::Mousebind(_) => ( HlwmCommand::Mousebind(Mousebind { keys: vec![Key::Mod4Super, Key::Mouse(MouseButton::Button1)], action: MousebindAction::Move, }), "mousebind\tMod4-Button1\tmove".into(), ), HlwmCommand::MoveIndex { index: _, skip_visible: _, } => ( HlwmCommand::MoveIndex { index: Index::Absolute(1), skip_visible: true, }, "move_index\t1\t--skip-visible".into(), ), HlwmCommand::JumpTo(_) => ( HlwmCommand::JumpTo(Window::LastMinimized), "jumpto\tlast-minimized".into(), ), HlwmCommand::AddTag(_) => ( HlwmCommand::AddTag("tag_name".into()), "add\ttag_name".into(), ), HlwmCommand::Focus(_) => { (HlwmCommand::Focus(Direction::Down), "focus\tdown".into()) } HlwmCommand::Shift(_) => (HlwmCommand::Shift(Direction::Up), "shift\tup".into()), HlwmCommand::Split(_) => ( HlwmCommand::Split(Align::Right(Some(0.5))), "split\tright\t0.5".into(), ), HlwmCommand::Fullscreen(_) => ( HlwmCommand::Fullscreen(ToggleBool::Toggle), "fullscreen\ttoggle".into(), ), HlwmCommand::MergeTag { tag: _, target: _ } => ( HlwmCommand::MergeTag { tag: "my_tag".into(), target: Some(Tag::Name("other_tag".into())), }, "merge_tag\tmy_tag\tother_tag".into(), ), HlwmCommand::CycleLayout { delta: _, layouts: _, } => ( HlwmCommand::CycleLayout { delta: Some(Index::Absolute(1)), layouts: vec![FrameLayout::Vertical, FrameLayout::Max, FrameLayout::Grid], }, "cycle_layout\t1\tvertical\tmax\tgrid".into(), ), HlwmCommand::Resize { direction: _, fraction_delta: _, } => ( HlwmCommand::Resize { direction: Direction::Down, fraction_delta: Some(Index::Absolute(0.5)), }, "resize\tdown\t0.5".into(), ), HlwmCommand::Watch => (HlwmCommand::Watch, "watch".into()), HlwmCommand::Or { separator: _, commands: _, } => ( HlwmCommand::Or { separator: Separator::Comma, commands: vec![HlwmCommand::Reload, HlwmCommand::Quit], }, "or\t,\treload\t,\tquit".into(), ), HlwmCommand::And { separator: _, commands: _, } => ( HlwmCommand::And { separator: Separator::Period, commands: vec![HlwmCommand::Reload, HlwmCommand::Quit], }, "and\t.\treload\t.\tquit".into(), ), HlwmCommand::Compare { attribute: _, operator: _, value: _, } => ( HlwmCommand::Compare { attribute: "my_attr".to_string(), operator: Operator::Equal, value: "my_value".to_string(), }, "compare\tmy_attr\t=\tmy_value".into(), ), HlwmCommand::TagStatus { monitor: _ } => ( HlwmCommand::TagStatus { monitor: Some(1) }, "tag_status\t1".into(), ), HlwmCommand::Rule(_) => ( HlwmCommand::Rule(Rule::new( Some(Condition::Class { operator: RuleOperator::Equal, value: "Netscape".to_string(), }), vec![Consequence::Tag("6".into()), Consequence::Focus(false)], None, None, )), "rule\t--class=Netscape\t--tag=6\t--focus=off".into(), ), HlwmCommand::Get(_) => ( HlwmCommand::Get(SettingName::AutoDetectMonitors), "get\tauto_detect_monitors".into(), ), HlwmCommand::UseIndex { index: _, skip_visible: _, } => ( HlwmCommand::UseIndex { index: Index::Absolute(1), skip_visible: true, }, "use_index\t1\t--skip-visible".into(), ), HlwmCommand::UseTag(_) => (HlwmCommand::UseTag("tag".into()), "use\ttag".into()), HlwmCommand::MoveTag(_) => (HlwmCommand::MoveTag("tag".into()), "move\ttag".into()), HlwmCommand::Try(_) => ( HlwmCommand::Try(Box::new(HlwmCommand::MergeTag { tag: "default".into(), target: None, })), "try\tmerge_tag\tdefault".into(), ), HlwmCommand::Silent(_) => ( HlwmCommand::Silent(Box::new(HlwmCommand::MergeTag { tag: "default".into(), target: None, })), "silent\tmerge_tag\tdefault".into(), ), }) .collect::>(); for (command, expected_string) in commands { let actual_string = command.to_command_string(); assert_eq!( expected_string, actual_string, "\n1.\n\tExpected [{expected_string}]\n\tGot [{actual_string}]" ); let actual: HlwmCommand = actual_string .parse() .expect(&format!("\n2.\n\tparsing string: [{actual_string}]")); assert_eq!( command, actual, "\n3.\n\tcomparing commands:\n\t\tleft: [{command:?}]\n\t\tright: [{actual:?}]" ) } } }