2024-02-29 20:36:38 +00:00
|
|
|
|
use std::{
|
|
|
|
|
borrow::BorrowMut,
|
2024-03-01 19:50:32 +00:00
|
|
|
|
convert::Infallible,
|
2024-02-29 20:36:38 +00:00
|
|
|
|
fmt::{Display, Write},
|
|
|
|
|
str::FromStr,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use serde::{de::Expected, Deserialize, Serialize};
|
|
|
|
|
use strum::IntoEnumIterator;
|
|
|
|
|
|
2024-03-02 21:18:21 +00:00
|
|
|
|
use crate::split;
|
|
|
|
|
|
2024-03-06 19:49:15 +00:00
|
|
|
|
use super::{
|
|
|
|
|
hlwmbool,
|
|
|
|
|
hook::Hook,
|
|
|
|
|
parser::{FromCommandArgs, FromStrings, ParseError},
|
|
|
|
|
ToCommandString,
|
|
|
|
|
};
|
2024-02-29 20:36:38 +00:00
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
|
|
pub struct Rule {
|
|
|
|
|
/// Rule labels default to an incremental index.
|
|
|
|
|
/// These default labels are unique, unless you assign a different
|
|
|
|
|
/// rule a custom integer LABEL. Default labels can be captured
|
|
|
|
|
/// with the printlabel flag.
|
|
|
|
|
label: Option<String>,
|
|
|
|
|
flag: Option<Flag>,
|
|
|
|
|
/// If each condition of this rule matches against this client,
|
|
|
|
|
/// then every [Consequence] is executed.
|
|
|
|
|
/// (If there are no conditions given, then this rule is executed for each client)
|
|
|
|
|
condition: Option<Condition>,
|
|
|
|
|
consequences: Vec<Consequence>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Rule {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
label: Default::default(),
|
|
|
|
|
flag: Default::default(),
|
|
|
|
|
condition: Some(Condition::FixedSize),
|
|
|
|
|
consequences: vec![Consequence::Focus(true)],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ToCommandString for Rule {
|
|
|
|
|
fn to_command_string(&self) -> String {
|
|
|
|
|
let mut args = Vec::with_capacity(4);
|
|
|
|
|
if let Some(label) = self.label.as_ref() {
|
|
|
|
|
args.push(format!("--label={}", label));
|
|
|
|
|
}
|
|
|
|
|
if let Some(flag) = self.flag.as_ref() {
|
|
|
|
|
args.push(format!("{flag}"));
|
|
|
|
|
}
|
|
|
|
|
if let Some(cond) = self.condition.as_ref() {
|
|
|
|
|
args.push(cond.to_command_string());
|
|
|
|
|
}
|
|
|
|
|
args.push(
|
|
|
|
|
(&self.consequences)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|c| format!("--{}", c.to_command_string()))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join("\t"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
args.join("\t")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Rule {
|
|
|
|
|
pub fn new(
|
|
|
|
|
condition: Option<Condition>,
|
|
|
|
|
consequences: Vec<Consequence>,
|
|
|
|
|
label: Option<String>,
|
|
|
|
|
flag: Option<Flag>,
|
|
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
label,
|
|
|
|
|
flag,
|
|
|
|
|
condition,
|
|
|
|
|
consequences,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-06 19:49:15 +00:00
|
|
|
|
impl FromStrings for Rule {
|
|
|
|
|
fn from_strings<I: Iterator<Item = String>>(s: I) -> Result<Self, ParseError> {
|
2024-02-29 20:36:38 +00:00
|
|
|
|
let mut condition: Option<Condition> = None;
|
|
|
|
|
let mut consequences = vec![];
|
|
|
|
|
let mut label: Option<String> = None;
|
|
|
|
|
let mut flag: Option<Flag> = None;
|
2024-03-06 19:49:15 +00:00
|
|
|
|
let mut args = s.map(|part| part.strip_prefix("--").unwrap_or(part.as_str()).to_string());
|
2024-02-29 20:36:38 +00:00
|
|
|
|
|
2024-03-06 19:49:15 +00:00
|
|
|
|
let mut original = Vec::new(); // For InvalidValue error
|
2024-02-29 20:36:38 +00:00
|
|
|
|
while let Some(arg) = args.next() {
|
2024-03-06 19:49:15 +00:00
|
|
|
|
original.push(arg.clone());
|
2024-03-01 21:48:25 +00:00
|
|
|
|
if label.is_none() {
|
|
|
|
|
if let Some((name, value)) = arg.split_once('=') {
|
|
|
|
|
if name.trim() == "label" {
|
|
|
|
|
label = Some(value.trim().to_string());
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-29 20:36:38 +00:00
|
|
|
|
if flag.is_none() {
|
|
|
|
|
if let Ok(flag_res) = Flag::from_str(&arg) {
|
|
|
|
|
flag = Some(flag_res);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if condition.is_none() {
|
|
|
|
|
if let Ok(cond) = Condition::from_str(&arg) {
|
|
|
|
|
condition = Some(cond);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Ok(cons) = Consequence::from_str(&arg) {
|
|
|
|
|
consequences.push(cons);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if consequences.is_empty() {
|
2024-03-06 19:49:15 +00:00
|
|
|
|
return Err(ParseError::InvalidValue {
|
|
|
|
|
value: original.join(" "),
|
|
|
|
|
expected: "condition and/or consequences",
|
|
|
|
|
});
|
2024-02-29 20:36:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
label,
|
|
|
|
|
flag,
|
|
|
|
|
condition: condition,
|
|
|
|
|
consequences: consequences,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Serialize for Rule {
|
|
|
|
|
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 Rule {
|
|
|
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
|
|
where
|
|
|
|
|
D: serde::Deserializer<'de>,
|
|
|
|
|
{
|
|
|
|
|
pub struct Expect;
|
|
|
|
|
impl Expected for Expect {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
|
|
|
write!(f, "a valid herbstluftwm rule")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let str_val: String = Deserialize::deserialize(deserializer)?;
|
2024-03-06 19:49:15 +00:00
|
|
|
|
let strings = split::tab_or_space(&str_val);
|
2024-02-29 20:36:38 +00:00
|
|
|
|
|
2024-03-06 19:49:15 +00:00
|
|
|
|
Ok(Self::from_strings(strings.into_iter()).map_err(|_| {
|
2024-02-29 20:36:38 +00:00
|
|
|
|
serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &Expect)
|
|
|
|
|
})?)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, strum::EnumIter)]
|
|
|
|
|
pub enum RuleOperator {
|
|
|
|
|
/// ~ matches if client’s property is matched by the regex value.
|
|
|
|
|
Regex,
|
|
|
|
|
/// = matches if client’s property string is equal to value.
|
|
|
|
|
Equal,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RuleOperator {
|
|
|
|
|
pub const fn char(&self) -> char {
|
|
|
|
|
match self {
|
|
|
|
|
RuleOperator::Regex => '~',
|
|
|
|
|
RuleOperator::Equal => '=',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub const fn match_set() -> [char; 2] {
|
|
|
|
|
[Self::Regex.char(), Self::Equal.char()]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TryFrom<char> for RuleOperator {
|
2024-03-06 19:49:15 +00:00
|
|
|
|
type Error = ParseError;
|
2024-02-29 20:36:38 +00:00
|
|
|
|
|
|
|
|
|
fn try_from(value: char) -> Result<Self, Self::Error> {
|
|
|
|
|
Self::iter()
|
|
|
|
|
.find(|i| i.char() == value)
|
2024-03-06 19:49:15 +00:00
|
|
|
|
.ok_or(ParseError::InvalidCommand(value.to_string()))
|
2024-02-29 20:36:38 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for RuleOperator {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::Equal
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Display for RuleOperator {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
f.write_char(self.char())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FromStr for RuleOperator {
|
2024-03-06 19:49:15 +00:00
|
|
|
|
type Err = ParseError;
|
2024-02-29 20:36:38 +00:00
|
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
2024-03-06 19:49:15 +00:00
|
|
|
|
s.chars().next().ok_or(ParseError::Empty)?.try_into()
|
2024-02-29 20:36:38 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, strum::Display, strum::EnumIter)]
|
|
|
|
|
#[strum(serialize_all = "lowercase")]
|
|
|
|
|
pub enum Condition {
|
|
|
|
|
/// the first entry in client’s WM_CLASS.
|
|
|
|
|
Instance {
|
|
|
|
|
operator: RuleOperator,
|
|
|
|
|
value: String,
|
|
|
|
|
},
|
|
|
|
|
/// the second entry in client’s WM_CLASS.
|
|
|
|
|
Class {
|
|
|
|
|
operator: RuleOperator,
|
|
|
|
|
value: String,
|
|
|
|
|
},
|
|
|
|
|
/// client’s window title.
|
|
|
|
|
Title {
|
|
|
|
|
operator: RuleOperator,
|
|
|
|
|
value: String,
|
|
|
|
|
},
|
|
|
|
|
/// the client’s process id (Warning: the pid is not available for every client.
|
|
|
|
|
/// This only matches if the client sets _NET_WM_PID to the pid itself).
|
|
|
|
|
Pid {
|
|
|
|
|
operator: RuleOperator,
|
|
|
|
|
value: String,
|
|
|
|
|
},
|
|
|
|
|
/// this client’s process group id. Since the pgid of a window is derived
|
|
|
|
|
/// from its pid the same restrictions apply as above.
|
|
|
|
|
Pgid {
|
|
|
|
|
operator: RuleOperator,
|
|
|
|
|
value: String,
|
|
|
|
|
},
|
|
|
|
|
/// matches if the age of the rule measured in seconds does not exceed value.
|
|
|
|
|
/// This condition only can be used with the = operator. If maxage already is
|
|
|
|
|
/// exceeded (and never will match again), then this rule is removed.
|
|
|
|
|
/// (With this you can build rules that only live for a certain time.)
|
|
|
|
|
MaxAge {
|
|
|
|
|
operator: RuleOperator,
|
|
|
|
|
value: String,
|
|
|
|
|
},
|
|
|
|
|
/// matches the _NET_WM_WINDOW_TYPE property of a window.
|
|
|
|
|
/// If _NET_WM_WINDOW_TYPE has multiple entries, then only the first entry is used here.
|
|
|
|
|
WindowType {
|
|
|
|
|
operator: RuleOperator,
|
|
|
|
|
value: String,
|
|
|
|
|
},
|
|
|
|
|
/// matches the WM_WINDOW_ROLE property of a window if it is set by the window.
|
|
|
|
|
WindowRole {
|
|
|
|
|
operator: RuleOperator,
|
|
|
|
|
value: String,
|
|
|
|
|
},
|
|
|
|
|
/// matches if the window does not allow being resized (i.e. if the minimum
|
|
|
|
|
/// size matches the maximum size). This condition does not take a parameter.
|
|
|
|
|
FixedSize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ToCommandString for Condition {
|
|
|
|
|
fn to_command_string(&self) -> String {
|
|
|
|
|
match self {
|
|
|
|
|
Condition::Instance { operator, value }
|
|
|
|
|
| Condition::Class { operator, value }
|
|
|
|
|
| Condition::Title { operator, value }
|
|
|
|
|
| Condition::Pid { operator, value }
|
|
|
|
|
| Condition::Pgid { operator, value }
|
|
|
|
|
| Condition::MaxAge { operator, value }
|
|
|
|
|
| Condition::WindowType { operator, value }
|
|
|
|
|
| Condition::WindowRole { operator, value } => format!("--{self}{operator}{value}"),
|
|
|
|
|
// Note: There might be a bug where if you use --fixedsize
|
|
|
|
|
// herbstclient treats it as if fixedsize requires an argument.
|
|
|
|
|
//
|
|
|
|
|
// To deal with this on our end, we omit the -- prefix for fixedsize
|
|
|
|
|
Condition::FixedSize => self.to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FromStr for Condition {
|
2024-03-06 19:49:15 +00:00
|
|
|
|
type Err = ParseError;
|
2024-02-29 20:36:38 +00:00
|
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
|
// Handle the case for fixedsize first so that we can treat the rest
|
|
|
|
|
// of the variants as having arguments
|
2024-03-01 21:48:25 +00:00
|
|
|
|
//
|
|
|
|
|
// Also, sometimes `fixedsize=0` could come back from `herbstclient list_rules`
|
|
|
|
|
if s.starts_with(&Self::FixedSize.to_string()) {
|
2024-02-29 20:36:38 +00:00
|
|
|
|
return Ok(Self::FixedSize);
|
|
|
|
|
}
|
|
|
|
|
let ((name, match_val), match_char) =
|
|
|
|
|
match split::on_first_match(s, &RuleOperator::match_set()) {
|
|
|
|
|
Some(parts) => parts,
|
2024-03-06 19:49:15 +00:00
|
|
|
|
None => return Err(ParseError::Empty),
|
2024-02-29 20:36:38 +00:00
|
|
|
|
};
|
|
|
|
|
let mut prop = Self::iter()
|
|
|
|
|
.find(|i| i.to_string() == name)
|
2024-03-06 19:49:15 +00:00
|
|
|
|
.ok_or(ParseError::InvalidCommand(s.to_string()))?;
|
2024-02-29 20:36:38 +00:00
|
|
|
|
|
|
|
|
|
match prop.borrow_mut() {
|
|
|
|
|
Condition::Instance { operator, value }
|
|
|
|
|
| Condition::Class { operator, value }
|
|
|
|
|
| Condition::Title { operator, value }
|
|
|
|
|
| Condition::Pid { operator, value }
|
|
|
|
|
| Condition::Pgid { operator, value }
|
|
|
|
|
| Condition::MaxAge { operator, value }
|
|
|
|
|
| Condition::WindowType { operator, value }
|
|
|
|
|
| Condition::WindowRole { operator, value } => {
|
|
|
|
|
*operator = match_char.try_into()?;
|
|
|
|
|
*value = match_val;
|
|
|
|
|
}
|
|
|
|
|
// Should be handled at the top of the function. If it's here that's not a valid
|
|
|
|
|
// use of fixedsize.
|
2024-03-06 19:49:15 +00:00
|
|
|
|
Condition::FixedSize => {
|
|
|
|
|
return Err(ParseError::InvalidValue {
|
|
|
|
|
value: s.to_string(),
|
|
|
|
|
expected: "[BUG] Should be handled at the top of the function",
|
|
|
|
|
})
|
|
|
|
|
}
|
2024-02-29 20:36:38 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(prop)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, strum::Display, strum::EnumIter)]
|
|
|
|
|
#[strum(serialize_all = "lowercase")]
|
|
|
|
|
pub enum Consequence {
|
|
|
|
|
/// moves the client to tag value.
|
|
|
|
|
Tag(String),
|
|
|
|
|
/// moves the client to the tag on monitor VALUE.
|
|
|
|
|
/// If the tag consequence was also specified,
|
|
|
|
|
/// and switchtag is set for the client, move the client to that tag,
|
|
|
|
|
/// then display that tag on monitor VALUE. If the tag consequence was specified,
|
|
|
|
|
/// but switchtag was not, ignore this consequence.
|
|
|
|
|
Monitor(String),
|
|
|
|
|
/// decides whether the client gets the input focus in its tag. The default is off.
|
|
|
|
|
Focus(bool),
|
|
|
|
|
/// if focus is activated and the client is put to a not focused tag,
|
|
|
|
|
/// then switchtag tells whether the client’s tag will be shown or not.
|
|
|
|
|
/// If the tag is shown on any monitor but is not focused, the client’s tag
|
|
|
|
|
/// only is brought to the current monitor if swap_monitors_to_get_tag is activated.
|
|
|
|
|
SwitchTag(bool),
|
|
|
|
|
/// decides whether the client will be managed or not. The default is on.
|
|
|
|
|
Manage(bool),
|
|
|
|
|
/// moves the window to a specified index in the tree.
|
|
|
|
|
Index(i32),
|
|
|
|
|
/// sets the floating state of the client.
|
|
|
|
|
Floating(bool),
|
|
|
|
|
/// sets the pseudotile state of the client.
|
|
|
|
|
PseudoTile(bool),
|
|
|
|
|
/// sets the sticky attribute of the client.
|
|
|
|
|
Sticky(bool),
|
|
|
|
|
/// sets whether the window state (the fullscreen state and the demands attention flag)
|
|
|
|
|
/// can be changed by the application via ewmh itself. This does not affect the
|
|
|
|
|
/// initial fullscreen state requested by the window.
|
|
|
|
|
EwmhRequests(bool),
|
|
|
|
|
/// sets whether hlwm should let the client know about EMWH changes
|
|
|
|
|
/// (currently only the fullscreen state). If this is set, applications do
|
|
|
|
|
/// not change to their fullscreen-mode while still being fullscreen.
|
|
|
|
|
EwmhNotify(bool),
|
|
|
|
|
/// sets the fullscreen flag of the client.
|
|
|
|
|
Fullscreen(bool),
|
|
|
|
|
/// emits the custom hook rule VALUE WINID when this rule is triggered
|
|
|
|
|
/// by a new window with the id WINID. This consequence can be used multiple times,
|
|
|
|
|
/// which will cause a hook to be emitted for each occurrence of a hook consequence.
|
|
|
|
|
Hook(Hook),
|
|
|
|
|
/// sets the keymask for a client.
|
|
|
|
|
/// A regular expression that is matched against the string representation of
|
|
|
|
|
/// all key bindings (as they are printed by list_keybinds).
|
|
|
|
|
/// While this client is focused, only bindings that match the expression will be active.
|
|
|
|
|
/// Any other bindings will be disabled. The default keymask is an empty string (),
|
|
|
|
|
/// which does not disable any keybinding
|
|
|
|
|
KeyMask(String),
|
|
|
|
|
/// sets a regex that determines which key bindings are inactive
|
|
|
|
|
/// for a client.
|
|
|
|
|
/// A regular expression that describes which keybindings are inactive while
|
|
|
|
|
/// the client is focused. If a key combination is pressed and its string
|
|
|
|
|
/// representation (as given by list_keybinds) matches the regex,
|
|
|
|
|
/// then the key press is propagated to the client
|
|
|
|
|
KeysInactive(String),
|
|
|
|
|
/// changes the floating position of a window
|
|
|
|
|
FloatPlacement(FloatPlacement),
|
|
|
|
|
/// Sets the client’s floating_geometry attribute.
|
|
|
|
|
/// The VALUE is a rectangle, interpreted relatively to the monitor.
|
|
|
|
|
/// If floatplacement is also specified for the client (possibly by another rule),
|
|
|
|
|
/// then only the size of the floating_geometry is used.
|
|
|
|
|
/// In order to force the position from the geometry, it is necessary to add
|
|
|
|
|
/// floatplacement=none.
|
|
|
|
|
FloatingGeometry { x: u32, y: u32 },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ToCommandString for Consequence {
|
|
|
|
|
fn to_command_string(&self) -> String {
|
|
|
|
|
match self {
|
|
|
|
|
Consequence::Focus(value)
|
|
|
|
|
| Consequence::SwitchTag(value)
|
|
|
|
|
| Consequence::Manage(value)
|
|
|
|
|
| Consequence::Floating(value)
|
|
|
|
|
| Consequence::PseudoTile(value)
|
|
|
|
|
| Consequence::Sticky(value)
|
|
|
|
|
| Consequence::EwmhRequests(value)
|
|
|
|
|
| Consequence::EwmhNotify(value)
|
|
|
|
|
| Consequence::Fullscreen(value) => vec![
|
|
|
|
|
self.to_string(),
|
|
|
|
|
match value {
|
|
|
|
|
true => "on".into(),
|
|
|
|
|
false => "off".into(),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
Consequence::Tag(value)
|
|
|
|
|
| Consequence::Monitor(value)
|
|
|
|
|
| Consequence::KeyMask(value)
|
|
|
|
|
| Consequence::KeysInactive(value) => vec![self.to_string(), value.to_string()],
|
|
|
|
|
Consequence::Index(value) => vec![self.to_string(), value.to_string()],
|
|
|
|
|
Consequence::Hook(value) => vec![self.to_string(), value.to_string()],
|
|
|
|
|
Consequence::FloatPlacement(value) => vec![self.to_string(), value.to_string()],
|
|
|
|
|
Consequence::FloatingGeometry { x, y } => vec![self.to_string(), format!("{x}x{y}")],
|
|
|
|
|
}
|
|
|
|
|
.join("=")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FromStr for Consequence {
|
2024-03-06 19:49:15 +00:00
|
|
|
|
type Err = ParseError;
|
2024-02-29 20:36:38 +00:00
|
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
|
let parts = split::tab_or_space(s);
|
|
|
|
|
let mut parts = parts.into_iter();
|
|
|
|
|
let name = parts.next().unwrap();
|
|
|
|
|
let (name, value_str) = if name.contains('=') {
|
|
|
|
|
let parts = name.split('=').collect::<Vec<_>>();
|
|
|
|
|
if parts.len() != 2 {
|
2024-03-06 19:49:15 +00:00
|
|
|
|
return Err(ParseError::InvalidCommand(s.to_string()));
|
2024-02-29 20:36:38 +00:00
|
|
|
|
}
|
|
|
|
|
let mut parts = parts.into_iter();
|
|
|
|
|
(
|
|
|
|
|
parts.next().unwrap().to_string(),
|
|
|
|
|
parts.next().unwrap().to_string(),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
match parts.next() {
|
|
|
|
|
Some(op) => {
|
|
|
|
|
if op != "=" {
|
2024-03-06 19:49:15 +00:00
|
|
|
|
return Err(ParseError::InvalidCommand(s.to_string()));
|
2024-02-29 20:36:38 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2024-03-06 19:49:15 +00:00
|
|
|
|
None => return Err(ParseError::InvalidCommand(s.to_string())),
|
2024-02-29 20:36:38 +00:00
|
|
|
|
};
|
|
|
|
|
let value = parts.collect::<Vec<_>>().join("\t");
|
|
|
|
|
(name, value)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut cons = Self::iter()
|
|
|
|
|
.find(|i| i.to_string() == name)
|
2024-03-06 19:49:15 +00:00
|
|
|
|
.ok_or(ParseError::InvalidCommand(s.to_string()))?;
|
2024-02-29 20:36:38 +00:00
|
|
|
|
match cons.borrow_mut() {
|
|
|
|
|
Consequence::Focus(value)
|
|
|
|
|
| Consequence::SwitchTag(value)
|
|
|
|
|
| Consequence::Floating(value)
|
|
|
|
|
| Consequence::PseudoTile(value)
|
|
|
|
|
| Consequence::Sticky(value)
|
|
|
|
|
| Consequence::EwmhRequests(value)
|
|
|
|
|
| Consequence::EwmhNotify(value)
|
|
|
|
|
| Consequence::Fullscreen(value)
|
|
|
|
|
| Consequence::Manage(value) => {
|
|
|
|
|
*value = hlwmbool::from_hlwm_string(&value_str)?;
|
|
|
|
|
}
|
|
|
|
|
Consequence::Tag(value)
|
|
|
|
|
| Consequence::Monitor(value)
|
|
|
|
|
| Consequence::KeyMask(value)
|
|
|
|
|
| Consequence::KeysInactive(value) => *value = value_str,
|
|
|
|
|
Consequence::Index(value) => *value = i32::from_str(&value_str)?,
|
|
|
|
|
Consequence::Hook(value) => {
|
2024-03-06 19:49:15 +00:00
|
|
|
|
*value = Hook::from_command_args(&name, [value_str].into_iter())?;
|
2024-02-29 20:36:38 +00:00
|
|
|
|
}
|
|
|
|
|
Consequence::FloatPlacement(value) => *value = FloatPlacement::from_str(&value_str)?,
|
|
|
|
|
Consequence::FloatingGeometry { x, y } => {
|
|
|
|
|
let mut values = value_str.split('=');
|
2024-03-06 19:49:15 +00:00
|
|
|
|
*x = u32::from_str(values.next().ok_or(ParseError::ValueMissing)?)?;
|
|
|
|
|
*y = u32::from_str(values.next().ok_or(ParseError::ValueMissing)?)?;
|
2024-02-29 20:36:38 +00:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(cons)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, strum::Display, strum::EnumIter)]
|
|
|
|
|
#[strum(serialize_all = "lowercase")]
|
|
|
|
|
pub enum FloatPlacement {
|
|
|
|
|
/// does not change the placement at all
|
|
|
|
|
None,
|
|
|
|
|
/// centers the window on the monitor
|
|
|
|
|
Center,
|
|
|
|
|
/// tries to place it with as little overlap to other floating windows as possible.
|
|
|
|
|
/// If there are multiple options with the least overlap, then the position with
|
|
|
|
|
/// the least overlap to tiling windows is chosen
|
|
|
|
|
Smart,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FromStr for FloatPlacement {
|
2024-03-06 19:49:15 +00:00
|
|
|
|
type Err = ParseError;
|
2024-02-29 20:36:38 +00:00
|
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
|
Self::iter()
|
|
|
|
|
.find(|i| i.to_string() == s)
|
2024-03-06 19:49:15 +00:00
|
|
|
|
.ok_or(ParseError::InvalidCommand(s.to_string()))
|
2024-02-29 20:36:38 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for FloatPlacement {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::Smart
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, strum::EnumIter)]
|
|
|
|
|
pub enum Flag {
|
|
|
|
|
/// negates the next CONDITION.
|
|
|
|
|
Not,
|
|
|
|
|
/// only apply this rule once (and delete it afterwards).
|
|
|
|
|
Once,
|
|
|
|
|
/// prints the label of the newly created rule to stdout.
|
|
|
|
|
PrintLabel,
|
|
|
|
|
/// prepend the rule to the list of rules instead of appending it.
|
|
|
|
|
/// So its consequences may be overwritten by already existing rules.
|
|
|
|
|
Prepend,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Display for Flag {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
match self {
|
|
|
|
|
Flag::Not => f.write_str("not"),
|
|
|
|
|
Flag::Once => f.write_str("once"),
|
|
|
|
|
Flag::PrintLabel => f.write_str("printlabel"),
|
|
|
|
|
Flag::Prepend => f.write_str("prepend"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FromStr for Flag {
|
2024-03-06 19:49:15 +00:00
|
|
|
|
type Err = ParseError;
|
2024-02-29 20:36:38 +00:00
|
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
|
match s {
|
|
|
|
|
"!" | "not" => Ok(Self::Not),
|
|
|
|
|
"once" => Ok(Self::Once),
|
|
|
|
|
"printlabel" => Ok(Self::PrintLabel),
|
|
|
|
|
"prepend" => Ok(Self::Prepend),
|
2024-03-06 19:49:15 +00:00
|
|
|
|
_ => Err(ParseError::InvalidCommand(s.to_string())),
|
2024-02-29 20:36:38 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[macro_export]
|
|
|
|
|
macro_rules! rule {
|
|
|
|
|
($consequence:expr) => {
|
|
|
|
|
crate::hlwm::rule::Rule::new(None, vec![$consequence], None, None)
|
|
|
|
|
};
|
|
|
|
|
($condition:expr, $consequence:expr) => {
|
|
|
|
|
crate::hlwm::rule::Rule::new(Some($condition), vec![$consequence], None, None)
|
|
|
|
|
};
|
|
|
|
|
($flag:tt: $condition:expr, $consequence:expr) => {
|
|
|
|
|
crate::hlwm::rule::Rule::new(
|
|
|
|
|
Some($condition),
|
|
|
|
|
vec![$consequence],
|
|
|
|
|
None,
|
|
|
|
|
Some(rule!(Flag $flag)),
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
($label:literal => $consequence:expr) => {
|
|
|
|
|
crate::hlwm::rule::Rule::new(None, vec![$consequence], Some($label.into()), None)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
($label:literal => $condition:expr, $consequence:expr) => {
|
|
|
|
|
crate::hlwm::rule::Rule::new(Some($condition), vec![$consequence], Some($label.into()), None)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
($label:literal => $flag:tt: $condition:expr, $consequence:expr) => {
|
|
|
|
|
crate::hlwm::rule::Rule::new(Some($condition), vec![$consequence], Some($label.into()), Some(rule!(Flag $flag)))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(Flag Not) => {
|
|
|
|
|
crate::hlwm::rule::Flag::Not
|
|
|
|
|
};
|
|
|
|
|
(Flag Once) => {
|
|
|
|
|
crate::hlwm::rule::Flag::Once
|
|
|
|
|
};
|
|
|
|
|
(Flag PrintLabel) => {
|
|
|
|
|
crate::hlwm::rule::Flag::PrintLabel
|
|
|
|
|
};
|
|
|
|
|
(Flag Prepend) => {
|
|
|
|
|
crate::hlwm::rule::Flag::Prepend
|
|
|
|
|
};
|
|
|
|
|
(Flag $other:tt) => {
|
|
|
|
|
compile_error!("flag can be one of: Not, Once, PrintLabel, Prepend")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-01 19:50:32 +00:00
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
|
|
|
pub enum Unrule {
|
|
|
|
|
All,
|
|
|
|
|
Rule(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Display for Unrule {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
match self {
|
|
|
|
|
Unrule::All => f.write_str("--all"),
|
|
|
|
|
Unrule::Rule(rule) => f.write_str(&rule),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Unrule {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Unrule::All
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FromStr for Unrule {
|
|
|
|
|
type Err = Infallible;
|
|
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
|
match s {
|
|
|
|
|
"--all" | "-F" => Ok(Self::All),
|
|
|
|
|
_ => Ok(Self::Rule(s.to_string())),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-29 20:36:38 +00:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod test {
|
2024-03-01 21:48:25 +00:00
|
|
|
|
use std::str::FromStr;
|
|
|
|
|
|
2024-02-29 20:36:38 +00:00
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
2024-03-06 19:49:15 +00:00
|
|
|
|
use crate::{
|
|
|
|
|
hlwm::{parser::FromStrings, rule::FloatPlacement},
|
|
|
|
|
split,
|
|
|
|
|
};
|
2024-03-01 21:48:25 +00:00
|
|
|
|
use pretty_assertions::assert_eq;
|
|
|
|
|
|
2024-02-29 20:36:38 +00:00
|
|
|
|
use super::{Condition, Consequence, Flag, Rule, RuleOperator};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
|
|
|
pub struct RuleWrapper {
|
|
|
|
|
rule: Rule,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn rule_serialize_deserialize() {
|
2024-03-01 21:48:25 +00:00
|
|
|
|
for rule in [
|
|
|
|
|
Rule::new(
|
|
|
|
|
Some(Condition::Class {
|
|
|
|
|
operator: RuleOperator::Equal,
|
|
|
|
|
value: "Netscape".into(),
|
|
|
|
|
}),
|
|
|
|
|
vec![Consequence::Tag(1.to_string())],
|
|
|
|
|
Some("label".into()),
|
|
|
|
|
Some(Flag::Not),
|
|
|
|
|
),
|
|
|
|
|
Rule::new(
|
|
|
|
|
Some(Condition::FixedSize),
|
|
|
|
|
vec![Consequence::Floating(true)],
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
),
|
|
|
|
|
] {
|
2024-02-29 20:36:38 +00:00
|
|
|
|
let serialized = toml::to_string_pretty(&RuleWrapper { rule: rule.clone() })
|
|
|
|
|
.expect("serializing rule");
|
|
|
|
|
let deserialized: RuleWrapper =
|
|
|
|
|
toml::from_str(&serialized).expect("deserializing rule");
|
|
|
|
|
assert_eq!(rule, deserialized.rule);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-03-01 21:48:25 +00:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn rules_from_list_rules_parse() {
|
|
|
|
|
const INPUT: &str = r#"label=0 windowtype~_NET_WM_WINDOW_TYPE_(DIALOG|UTILITY|SPLASH) floating=on
|
|
|
|
|
label=1 focus=on
|
|
|
|
|
label=2 floatplacement=smart
|
|
|
|
|
label=3 windowtype~_NET_WM_WINDOW_TYPE_(DIALOG) floating=on
|
|
|
|
|
label=4 windowtype~_NET_WM_WINDOW_TYPE_(NOTIFICATION|DOCK|DESKTOP) manage=off
|
|
|
|
|
label=5 fixedsize=0 floating=true"#;
|
|
|
|
|
|
|
|
|
|
let parsed = INPUT
|
|
|
|
|
.split('\n')
|
2024-03-06 19:49:15 +00:00
|
|
|
|
.map(|l| Rule::from_strings(split::tab_or_space(l).into_iter()))
|
2024-03-01 21:48:25 +00:00
|
|
|
|
.collect::<Result<Vec<_>, _>>()
|
|
|
|
|
.expect("parsing error");
|
|
|
|
|
|
|
|
|
|
let expected = [
|
|
|
|
|
Rule::new(
|
|
|
|
|
Some(Condition::WindowType {
|
|
|
|
|
operator: RuleOperator::Regex,
|
|
|
|
|
value: "_NET_WM_WINDOW_TYPE_(DIALOG|UTILITY|SPLASH)".into(),
|
|
|
|
|
}),
|
|
|
|
|
vec![Consequence::Floating(true)],
|
|
|
|
|
Some("0".into()),
|
|
|
|
|
None,
|
|
|
|
|
),
|
|
|
|
|
Rule::new(None, vec![Consequence::Focus(true)], Some("1".into()), None),
|
|
|
|
|
Rule::new(
|
|
|
|
|
None,
|
|
|
|
|
vec![Consequence::FloatPlacement(FloatPlacement::Smart)],
|
|
|
|
|
Some("2".into()),
|
|
|
|
|
None,
|
|
|
|
|
),
|
|
|
|
|
Rule::new(
|
|
|
|
|
Some(Condition::WindowType {
|
|
|
|
|
operator: RuleOperator::Regex,
|
|
|
|
|
value: "_NET_WM_WINDOW_TYPE_(DIALOG)".into(),
|
|
|
|
|
}),
|
|
|
|
|
vec![Consequence::Floating(true)],
|
|
|
|
|
Some("3".to_string()),
|
|
|
|
|
None,
|
|
|
|
|
),
|
|
|
|
|
Rule::new(
|
|
|
|
|
Some(Condition::WindowType {
|
|
|
|
|
operator: RuleOperator::Regex,
|
|
|
|
|
value: "_NET_WM_WINDOW_TYPE_(NOTIFICATION|DOCK|DESKTOP)".into(),
|
|
|
|
|
}),
|
|
|
|
|
vec![Consequence::Manage(false)],
|
|
|
|
|
Some("4".to_string()),
|
|
|
|
|
None,
|
|
|
|
|
),
|
|
|
|
|
Rule::new(
|
|
|
|
|
Some(Condition::FixedSize),
|
|
|
|
|
vec![Consequence::Floating(true)],
|
|
|
|
|
Some("5".into()),
|
|
|
|
|
None,
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
parsed
|
|
|
|
|
.into_iter()
|
|
|
|
|
.zip(expected)
|
|
|
|
|
.for_each(|(parsed, expected)| {
|
|
|
|
|
assert_eq!(expected, parsed);
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-02-29 20:36:38 +00:00
|
|
|
|
}
|