hlctl/src/hlwm/command.rs

1015 lines
38 KiB
Rust

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<String>),
/// Executes the autostart file.
Reload,
/// Closes the specified `window` gracefully or the focused window
/// if none is given explicitly
Close {
window: Option<Window>,
},
/// Spawns an `executable` with its `args`.
Spawn {
executable: String,
args: Vec<String>,
},
/// 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<Attribute>,
},
/// 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<i32>,
skip_visible: bool,
},
MoveIndex {
index: Index<i32>,
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<Tag>,
},
Cycle,
Focus(Direction),
Shift(Direction),
Split(Align),
Remove,
Fullscreen(ToggleBool),
CycleLayout {
delta: Option<Index<i32>>,
layouts: Vec<FrameLayout>,
},
Resize {
direction: Direction,
fraction_delta: Option<Index<f64>>,
},
Watch,
Or {
separator: Separator,
commands: Vec<HlwmCommand>,
},
And {
separator: Separator,
commands: Vec<HlwmCommand>,
},
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<Monitor>,
},
/// 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<HlwmCommand>),
/// executes the provided command, but discards its output and only returns its exit code.
Silent(Box<HlwmCommand>),
}
impl FromStr for Box<HlwmCommand> {
type Err = CommandParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Box::new(HlwmCommand::from_str(s)?))
}
}
impl Serialize for HlwmCommand {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_command_string().replace("\t", " "))
}
}
impl<'de> Deserialize<'de> for HlwmCommand {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<Self, Self::Err> {
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<String>) -> Result<Self, CommandParseError> {
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<FromStr>] => Close),
HlwmCommand::Spawn {
executable: _,
args: _,
} => {
parse!(executable: String, args: [Vec<String>] => 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::<Vec<_>>().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::<Vec<_>>();
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::<Vec<String>>();
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<FromStr>] => 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::<Result<_, _>>()
.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::<Result<_, _>>()
.map_err(|_| CommandParseError::BadArgument {
command: command.to_string(),
})?,
})
}
}
}
}
HlwmCommand::Resize {
direction: _,
fraction_delta: _,
} => parse!(direction: FromStr, fraction_delta: [Option<FromStr>] => 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<FromStr>] => 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<String> {
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<String>),
#[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::<Vec<String>>()
.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::<Vec<String>>()
.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::<Vec<String>>()
.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::<Vec<_>>();
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:?}]"
)
}
}
}