hlctl/src/hlwm/command.rs

1178 lines
44 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, split};
use super::{
attribute::{Attribute, AttributeError, AttributeOption},
hlwmbool::ToggleBool,
hook::Hook,
key::{KeyUnbind, Keybind, Mousebind},
pad::Pad,
rule::{Rule, Unrule},
setting::{FrameLayout, Setting, SettingName},
window::Window,
Align, Direction, Index, Monitor, Operator, Separator, StringParseError, 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<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),
/// 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<HlwmCommand>,
},
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<TagSelect>,
},
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>),
/// 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<Monitor>,
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<String>,
identifier: String,
object: String,
},
#[strum(serialize = "sprintf")]
Sprintf(Vec<String>),
/// Removes all rules either with the given label, or all of them
/// (See [Unrule])
Unrule(Unrule),
}
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::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: _,
} => {
parse!(skip_visible: [Flag("--skip-visible")], index: FromStr => MoveIndex)
}
HlwmCommand::UseIndex {
index: _,
skip_visible: _,
} => {
parse!(skip_visible: [Flag("--skip-visible")], index: FromStr => UseIndex)
}
HlwmCommand::UseTag(_) => parse!(String => UseTag),
HlwmCommand::MoveTag(_) => parse!(String => MoveTag),
HlwmCommand::Try(_) => parse!(FromStrAll => Try),
HlwmCommand::Silent(_) => parse!(FromStrAll => Silent),
HlwmCommand::MonitorRect {
monitor: _,
without_pad: _,
} => {
parse!(without_pad: [Flag("-p")], monitor: [Option<FromStr>] => MonitorRect)
}
HlwmCommand::Pad { monitor: _, pad: _ } => {
parse!(monitor: FromStr, pad: FromStrAll => Pad)
}
HlwmCommand::Substitute {
identifier: _,
attribute_path: _,
command: _,
} => {
parse!(identifier: String, attribute_path: String, command: FromStrAll => Substitute)
}
HlwmCommand::ForEach {
unique: _,
recursive: _,
filter_name: _,
identifier: _,
object: _,
} => {
let (params, args): (Vec<String>, Vec<String>) =
args.into_iter().partition(|a| a.starts_with("--"));
if args.len() < 2 {
return Err(CommandParseError::BadArgument {
command: command.to_string(),
});
}
let mut args = args.into_iter();
let (identifier, object) =
(args.next().unwrap(), args.collect::<Vec<_>>().join("\t"));
let mut unique = false;
let mut recursive = false;
let mut filter_name: Option<String> = None;
for param in params {
match param.as_str() {
"--unique" => unique = true,
"--recursive" => recursive = true,
other => {
if let Some(name) = other.strip_prefix("--filter-name=") {
filter_name = Some(name.to_string());
}
}
}
}
Ok(HlwmCommand::ForEach {
unique,
recursive,
filter_name,
identifier,
object,
})
}
HlwmCommand::Sprintf(_) => parse!([Vec<String>] => Sprintf),
HlwmCommand::Unrule(_) => parse!(FromStr => Unrule),
}?;
assert_eq!(command.to_string(), parsed_command.to_string());
Ok(parsed_command)
}
}
impl HlwmCommand {
#[inline(always)]
pub(crate) fn args(&self) -> Vec<String> {
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<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 From<io::Error> 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::<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())
}
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,
} => {
let mut parts = Vec::with_capacity(6);
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.join("\t")
}
HlwmCommand::Sprintf(args) => [self.to_string()]
.into_iter()
.chain(args.into_iter().cloned())
.collect::<Vec<_>>()
.join("\t"),
HlwmCommand::Unrule(unrule) => format!("{self}\t{unrule}"),
};
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},
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("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(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: _,
} => (
HlwmCommand::ForEach {
unique: true,
recursive: true,
filter_name: Some(".+".into()),
identifier: "CLIENT".into(),
object: "clients.".into(),
},
"foreach\tCLIENT\tclients.\t--filter-name=.+\t--unique\t--recursive".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::<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:?}]"
)
}
}
#[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);
}
}