1178 lines
44 KiB
Rust
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);
|
|
}
|
|
}
|