use std::{ borrow::BorrowMut, io, process::{self, ExitStatus}, str::FromStr, string::FromUtf8Error, }; use strum::IntoEnumIterator; use serde::{de::Expected, Deserialize, Serialize}; use thiserror::Error; use crate::{ hlwm::{ parser::{either::Either, ParseError}, Client, }, split, }; use super::{ and_or_command::AndOrCommands, attribute::{Attribute, AttributeError, AttributeOption, AttributeType}, hlwmbool::ToggleBool, hook::Hook, key::{KeyUnbind, Keybind, Mousebind}, pad::Pad, parser::{self, ArgParser, Flip, FromCommandArgs, FromStrings, ToOption}, rule::{Rule, Unrule}, setting::{FrameLayout, Setting, SettingName}, window::Window, Align, Direction, Index, Monitor, Operator, Separator, TagSelect, 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), /// Replaces all exact occurrences of `identifier` in `command` and its `args` by the value of the `attribute`. /// Note that the `command` also is replaced by the attribute value if it equals `identifier`. /// The replaced command with its arguments then is executed Substitute { identifier: String, attribute_path: String, command: Box, }, 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), /// Prints the rectangle of the specified monitor in the format: X Y W H /// If no monitor is given, then the current monitor is used. /// /// If `without_pad` is supplied, then the remaining rect without the pad around this /// monitor is printed. MonitorRect { monitor: Option, without_pad: bool, }, Pad { monitor: Monitor, pad: Pad, }, /// For each child of the given `object` the `command` is called with its `args`, where the `identifier` is replaced by the path of the child #[strum(serialize = "foreach")] ForEach { /// do not print duplicates (some objects can be reached via multiple paths, such as `clients.focus`) unique: bool, /// print `object` and all its children of arbitrary depth in breadth-first search order. This implicitly activates `unique` recursive: bool, /// consider children whose name match the specified REGEX filter_name: Option, identifier: String, object: String, command: Box, }, #[strum(serialize = "sprintf")] Sprintf(Vec), /// Removes all rules either with the given label, or all of them /// (See [Unrule]) Unrule(Unrule), } impl FromStr for Box { type Err = ParseError; 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 { ParseError(ParseError), } impl Expected for Expect { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Expect::ParseError(err) => f.write_str(&err.to_string()), } } } let str_val: String = Deserialize::deserialize(deserializer)?; ArgParser::from_strings(split::tab_or_space(&str_val).into_iter()) .collect_command("hlwm_command") .map_err(|err| { serde::de::Error::invalid_value( serde::de::Unexpected::Str(&str_val), &Expect::ParseError(err), ) }) } } impl FromStr for HlwmCommand { type Err = ParseError; fn from_str(s: &str) -> Result { ArgParser::from_strings(split::tab_or_space(s).into_iter()).collect_command("hlwm_command") } } impl Default for HlwmCommand { fn default() -> Self { HlwmCommand::Quit } } #[derive(Debug, Clone, Error)] pub enum CommandParseError { #[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(String), #[error("parsing error: [{0}]")] StringParseError(#[from] ParseError), } impl From for CommandParseError { fn from(value: FromUtf8Error) -> Self { Self::StringUtf8Error(value.to_string()) } } 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}") } } 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 execute(self) -> Result { Client::new().execute(self) } pub fn execute_str(self) -> Result { Ok(String::from_utf8(self.execute()?.stdout)?) } } impl FromCommandArgs for HlwmCommand { fn from_command_args, I: Iterator>( cmd_name: &str, args: I, ) -> Result { let mut command = HlwmCommand::iter() .find(|cmd| cmd.to_string() == cmd_name) .ok_or(parser::ParseError::InvalidCommand(cmd_name.to_string()))?; // Since HlwmCommand will often be parsed by its constituent commands (such as keybind) // just passing in `args.map(|s| s.into())` results in the typechecker overflowing its // recursion limit. So, to get around this, HlwmCommand will collect whatever type I // is into a `Vec`, which makes this loop not infinite. // // There really should be a better error for this. I'm lucky I figured it out fast. let mut parser = ArgParser::from_strings(args.map(|s| s.into()).collect::>().into_iter()); match command.borrow_mut() { HlwmCommand::Quit | HlwmCommand::Lock | HlwmCommand::Cycle | HlwmCommand::Watch | HlwmCommand::Reload | HlwmCommand::Remove | HlwmCommand::Unlock | HlwmCommand::Version | HlwmCommand::ListRules | HlwmCommand::False | HlwmCommand::True | HlwmCommand::Mouseunbind | HlwmCommand::ListMonitors | HlwmCommand::ListKeybinds => (), HlwmCommand::Echo(arg) => *arg = vec![parser.collect::>().join(" ")], HlwmCommand::Close { window } => { *window = parser.optional_next_from_str("close(window)")? } HlwmCommand::Spawn { executable, args } => { *executable = parser.must_string("spawn(executable)")?; *args = parser.collect(); } HlwmCommand::GetAttr(attr) => *attr = parser.must_string("get_attr")?, HlwmCommand::SetAttr { path, new_value } => { *path = parser.must_string("set_attr(path)")?; *new_value = parser.collect_from_strings_hint(path.as_str(), "set_attr(new_value)")?; } HlwmCommand::Attr { path, new_value } => { *path = parser.must_string("attr(path)")?; *new_value = parser .collect::>() .to_option() .map(|t| { let value = t.join(" "); Attribute::new(AttributeType::get_type_or_guess(&path, &value), &value) }) .flip()?; } HlwmCommand::NewAttr { path, attr } => { let attr_type: AttributeType = parser.next_from_str("new_attr(attr_type)")?; *path = parser.must_string("new_attr(path)")?; let value = parser.collect::>().to_option().map(|v| v.join(" ")); *attr = AttributeOption::new(attr_type, value.as_ref())?; } HlwmCommand::AttrType(path) | HlwmCommand::RemoveAttr(path) => { *path = parser.must_string(cmd_name)? } HlwmCommand::Set(setting) => *setting = parser.collect_command(cmd_name)?, HlwmCommand::EmitHook(hook) => *hook = parser.collect_command(cmd_name)?, HlwmCommand::Keybind(keybind) => *keybind = parser.collect_from_strings(cmd_name)?, HlwmCommand::Keyunbind(keyunbind) => *keyunbind = parser.next_from_str(cmd_name)?, HlwmCommand::Mousebind(mouseunbind) => { *mouseunbind = parser.collect_from_strings(cmd_name)? } HlwmCommand::JumpTo(win) => *win = parser.next_from_str(cmd_name)?, HlwmCommand::AddTag(tag) => *tag = parser.must_string(cmd_name)?, HlwmCommand::MergeTag { tag, target } => { *tag = parser.must_string("merge_tag(tag)")?; *target = parser.optional_next_from_str("merge_tag(target)")?; } HlwmCommand::Focus(dir) | HlwmCommand::Shift(dir) => { *dir = parser.next_from_str(cmd_name)? } HlwmCommand::Split(align) => *align = parser.collect_from_strings(cmd_name)?, HlwmCommand::Fullscreen(set) => *set = parser.next_from_str(cmd_name)?, HlwmCommand::CycleLayout { delta, layouts } => { let cycle_res = parser.try_first( |s| Index::::from_str(s), |s| Ok(FrameLayout::from_str(s)?), cmd_name, ); match cycle_res { Ok(res) => match res { Either::Left(idx) => { *delta = Some(idx); *layouts = parser.collect_from_str("cycle_layout(layouts)")?; } Either::Right(layout) => { *delta = None; *layouts = [layout] .into_iter() .chain(parser.collect_from_str("cycle_layout(layouts)")?) .collect(); } }, Err(err) => { if let ParseError::Empty = err { *delta = None; *layouts = vec![]; } else { return Err(err); } } } } HlwmCommand::Resize { direction, fraction_delta, } => { *direction = parser.next_from_str("resize(direction)")?; *fraction_delta = parser.optional_next_from_str("resize(fraction_delta)")?; } HlwmCommand::Or { separator, commands, } | HlwmCommand::And { separator, commands, } => { *separator = parser.next_from_str(&format!("{cmd_name}(separator)"))?; let commands_wrap: AndOrCommands = parser .collect_from_strings_hint(*separator, &format!("{cmd_name}(commands)"))?; *commands = commands_wrap.commands(); } HlwmCommand::Compare { attribute, operator, value, } => { *attribute = parser.must_string("compare(attribute)")?; *operator = parser.next_from_str("compare(operator)")?; *value = parser.must_string("compare(value)")?; } HlwmCommand::TagStatus { monitor } => { *monitor = parser.optional_next_from_str(cmd_name)?; } HlwmCommand::Rule(rule) => { *rule = parser.collect_from_strings(cmd_name)?; } HlwmCommand::Get(set) => *set = parser.next_from_str(cmd_name)?, HlwmCommand::UseIndex { index, skip_visible, } | HlwmCommand::MoveIndex { index, skip_visible, } => { let (args, skip) = parser.collect_strings_with_flag("--skip-visible"); *skip_visible = skip; *index = ArgParser::from_strings(args.into_iter()).next_from_str(cmd_name)?; } HlwmCommand::MoveTag(tag) | HlwmCommand::UseTag(tag) => { *tag = parser.must_string(cmd_name)? } HlwmCommand::Try(hlcmd) | HlwmCommand::Silent(hlcmd) => { *hlcmd = Box::new(parser.collect_command(cmd_name)?); } HlwmCommand::MonitorRect { monitor, without_pad, } => { let (args, pad) = parser.collect_strings_with_flag("-p"); *without_pad = pad; *monitor = args.first().map(|s| u32::from_str(s)).flip()?; } HlwmCommand::Pad { monitor, pad } => { *monitor = parser.next_from_str("pad(monitor)")?; *pad = parser.collect_from_strings("pad(pad)")?; } HlwmCommand::Substitute { identifier, attribute_path, command, } => { *identifier = parser.must_string("substitute(identifier)")?; *attribute_path = parser.must_string("substitute(attribute_path)")?; *command = Box::new(parser.collect_command("substitute(command)")?); } HlwmCommand::ForEach { unique, recursive, filter_name, identifier, object, command, } => { // Note: ForEach is still likely bugged due to this method of parsing the parts, as foreach may be nested let (normal, flags): (Vec<_>, Vec<_>) = parser.inner().partition(|c| { !c.starts_with("--unique") && !c.starts_with("--filter-name") && !c.starts_with("--recursive") }); let mut normal = ArgParser::from_strings(normal.into_iter()); for flag in flags { match flag.as_str() { "--unique" => *unique = true, "--recursive" => *recursive = true, other => { if let Some(name) = other.strip_prefix("--filter-name=") { *filter_name = Some(name.to_string()); } } } } *identifier = normal.must_string("for_each(identifier)")?; *object = normal.must_string("for_each(object)")?; *command = Box::new(normal.collect_command("for_each(command)")?); } HlwmCommand::Sprintf(s) => *s = parser.collect(), HlwmCommand::Unrule(unrule) => *unrule = parser.next_from_str(cmd_name)?, }; Ok(command) } } impl HlwmCommand { #[inline(always)] pub(crate) fn args(&self) -> Vec { self.to_command_string() .split('\t') .map(|a| a.to_string()) .collect() } } #[derive(Debug, Clone, Error)] pub enum CommandError { #[error("IO error")] IoError(String), #[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("parse error: {0}")] ParseError(#[from] ParseError), #[error("unexpected empty result")] Empty, #[error("invalid value")] Invalid, } impl From for CommandError { fn from(value: io::Error) -> Self { Self::IoError(value.to_string()) } } 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::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).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()) } HlwmCommand::MonitorRect { monitor, without_pad, } => { let mut parts = Vec::with_capacity(3); parts.push(self.to_string()); if let Some(monitor) = monitor { parts.push(monitor.to_string()); } if *without_pad { parts.push("-p".to_string()); } parts.join("\t") } HlwmCommand::Pad { monitor, pad } => format!("{self}\t{monitor}\t{pad}"), HlwmCommand::Substitute { identifier, attribute_path, command, } => format!( "{self}\t{identifier}\t{attribute_path}\t{}", command.to_command_string() ), HlwmCommand::ForEach { unique, recursive, filter_name, identifier, object, command, } => { let mut parts = Vec::with_capacity(7); parts.push(self.to_string()); parts.push(identifier.to_string()); parts.push(object.to_string()); if let Some(filter_name) = filter_name { parts.push(format!("--filter-name={filter_name}")); } if *unique { parts.push("--unique".into()); } if *recursive { parts.push("--recursive".into()); } parts.push(command.to_command_string()); parts.join("\t") } HlwmCommand::Sprintf(args) => [self.to_string()] .into_iter() .chain(args.into_iter().cloned()) .collect::>() .join("\t"), HlwmCommand::Unrule(unrule) => format!("{self}\t{unrule}"), }; if let Some(s) = cmd_string.strip_suffix('\t') { return s.to_string(); } cmd_string } } #[derive(Default)] struct ForEach { unique: bool, recursive: bool, filter_name: Option, identifier: String, object: String, command: HlwmCommand, } impl FromStrings for ForEach { fn from_strings>(s: I) -> Result { let mut for_each = Self::default(); let (normal, flags): (Vec<_>, Vec<_>) = s.partition(|c| !c.starts_with("--")); let mut normal = ArgParser::from_strings(normal.into_iter()); for flag in flags { match flag.as_str() { "--unique" => for_each.unique = true, "--recursive" => for_each.recursive = true, other => { if let Some(filter_name) = other.strip_prefix("--filter-name=") { for_each.filter_name = Some(filter_name.to_string()); } } } } for_each.identifier = normal.must_string("for_each(identifier)")?; for_each.object = normal.must_string("for_each(object)")?; for_each.command = normal.collect_command("for_each(command)")?; Ok(for_each) } } #[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, Keybind, MouseButton, Mousebind, MousebindAction}, pad::Pad, rule::{Condition, Consequence, Rule, RuleOperator}, setting::{Setting, SettingName}, window::Window, Align, Direction, TagSelect, 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::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\tgrep\tcontent".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(Keybind::new( [Key::Mod4Super, Key::Char('1')], HlwmCommand::Reload, )), "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(TagSelect::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(), ), HlwmCommand::MonitorRect { monitor: _, without_pad: _, } => ( HlwmCommand::MonitorRect { monitor: Some(1), without_pad: true, }, "monitor_rect\t1\t-p".into(), ), HlwmCommand::Pad { monitor: _, pad: _ } => ( HlwmCommand::Pad { monitor: 1, pad: Pad::UpRightDownLeft(2, 3, 4, 5), }, "pad\t1\t2\t3\t4\t5".into(), ), HlwmCommand::Substitute { identifier: _, attribute_path: _, command: _, } => ( HlwmCommand::Substitute { identifier: "MYTITLE".into(), attribute_path: "clients.focus.title".into(), command: Box::new(HlwmCommand::Echo(vec!["MYTITLE".to_string()])), }, "substitute\tMYTITLE\tclients.focus.title\techo\tMYTITLE".into(), ), HlwmCommand::ForEach { unique: _, recursive: _, filter_name: _, identifier: _, object: _, command: _, } => ( HlwmCommand::ForEach { unique: true, recursive: true, filter_name: Some(".+".into()), identifier: "CLIENT".into(), object: "clients.".into(), command: Box::new(HlwmCommand::Cycle), }, "foreach\tCLIENT\tclients.\t--filter-name=.+\t--unique\t--recursive\tcycle" .into(), ), HlwmCommand::Sprintf(_) => ( HlwmCommand::Sprintf( ["X", "tag=%s", "tags.focus.name", "rule", "once", "X"] .into_iter() .map(|s| s.to_string()) .collect(), ), "sprintf\tX\ttag=%s\ttags.focus.name\trule\tonce\tX".into(), ), HlwmCommand::Unrule(_) => ( HlwmCommand::Unrule(super::Unrule::All), "unrule\t--all".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:?}]" ) } } #[test] fn rule_fixedsize_not_omitted() { let cmd = HlwmCommand::Rule(Rule::new( Some(Condition::FixedSize), vec![Consequence::Floating(true)], None, None, )) .to_command_string(); assert_eq!("rule\tfixedsize\t--floating=on", cmd); } }