hlctl/src/hlwm/command.rs

1185 lines
44 KiB
Rust

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<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,
command: Box<HlwmCommand>,
},
#[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 = ParseError;
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 {
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<Self, Self::Err> {
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<FromUtf8Error> 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<process::Output, CommandError> {
Client::new().execute(self)
}
pub fn execute_str(self) -> Result<String, CommandError> {
Ok(String::from_utf8(self.execute()?.stdout)?)
}
}
impl FromCommandArgs for HlwmCommand {
fn from_command_args<S: Into<String>, I: Iterator<Item = S>>(
cmd_name: &str,
args: I,
) -> Result<Self, super::parser::ParseError> {
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<String>`, 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::<Vec<_>>().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::<Vec<_>>().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::<Vec<_>>()
.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::<Vec<_>>().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::<i32>::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<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("parse error: {0}")]
ParseError(#[from] ParseError),
#[error("unexpected empty result")]
Empty,
#[error("invalid value")]
Invalid,
}
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,
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::<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
}
}
#[derive(Default)]
struct ForEach {
unique: bool,
recursive: bool,
filter_name: Option<String>,
identifier: String,
object: String,
command: HlwmCommand,
}
impl FromStrings for ForEach {
fn from_strings<I: Iterator<Item = String>>(s: I) -> Result<Self, ParseError> {
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::<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);
}
}