1179 lines
44 KiB
Rust
1179 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, 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(#[from] FromUtf8Error),
|
|
#[error("parsing error: [{0}]")]
|
|
StringParseError(#[from] ParseError),
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|