759 lines
25 KiB
Rust
759 lines
25 KiB
Rust
use std::{
|
||
borrow::BorrowMut,
|
||
convert::Infallible,
|
||
fmt::{Display, Write},
|
||
str::FromStr,
|
||
};
|
||
|
||
use serde::{de::Expected, Deserialize, Serialize};
|
||
use strum::IntoEnumIterator;
|
||
|
||
use crate::split;
|
||
|
||
use super::{
|
||
hlwmbool,
|
||
hook::Hook,
|
||
parser::{FromCommandArgs, FromStrings, ParseError},
|
||
ToCommandString,
|
||
};
|
||
|
||
#[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,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl FromStrings for Rule {
|
||
fn from_strings<I: Iterator<Item = String>>(s: I) -> Result<Self, ParseError> {
|
||
let mut condition: Option<Condition> = None;
|
||
let mut consequences = vec![];
|
||
let mut label: Option<String> = None;
|
||
let mut flag: Option<Flag> = None;
|
||
let mut args = s.map(|part| part.strip_prefix("--").unwrap_or(part.as_str()).to_string());
|
||
|
||
let mut original = Vec::new(); // For InvalidValue error
|
||
while let Some(arg) = args.next() {
|
||
original.push(arg.clone());
|
||
if label.is_none() {
|
||
if let Some((name, value)) = arg.split_once('=') {
|
||
if name.trim() == "label" {
|
||
label = Some(value.trim().to_string());
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
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() {
|
||
return Err(ParseError::InvalidValue {
|
||
value: original.join(" "),
|
||
expected: "condition and/or consequences",
|
||
});
|
||
}
|
||
|
||
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)?;
|
||
let strings = split::tab_or_space(&str_val);
|
||
|
||
Ok(Self::from_strings(strings.into_iter()).map_err(|_| {
|
||
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 {
|
||
type Error = ParseError;
|
||
|
||
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||
Self::iter()
|
||
.find(|i| i.char() == value)
|
||
.ok_or(ParseError::InvalidCommand(value.to_string()))
|
||
}
|
||
}
|
||
|
||
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 {
|
||
type Err = ParseError;
|
||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
s.chars().next().ok_or(ParseError::Empty)?.try_into()
|
||
}
|
||
}
|
||
|
||
#[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 {
|
||
type Err = ParseError;
|
||
|
||
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
|
||
//
|
||
// Also, sometimes `fixedsize=0` could come back from `herbstclient list_rules`
|
||
if s.starts_with(&Self::FixedSize.to_string()) {
|
||
return Ok(Self::FixedSize);
|
||
}
|
||
let ((name, match_val), match_char) =
|
||
match split::on_first_match(s, &RuleOperator::match_set()) {
|
||
Some(parts) => parts,
|
||
None => return Err(ParseError::Empty),
|
||
};
|
||
let mut prop = Self::iter()
|
||
.find(|i| i.to_string() == name)
|
||
.ok_or(ParseError::InvalidCommand(s.to_string()))?;
|
||
|
||
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.
|
||
Condition::FixedSize => {
|
||
return Err(ParseError::InvalidValue {
|
||
value: s.to_string(),
|
||
expected: "[BUG] Should be handled at the top of the function",
|
||
})
|
||
}
|
||
};
|
||
|
||
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 {
|
||
type Err = ParseError;
|
||
|
||
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 {
|
||
return Err(ParseError::InvalidCommand(s.to_string()));
|
||
}
|
||
let mut parts = parts.into_iter();
|
||
(
|
||
parts.next().unwrap().to_string(),
|
||
parts.next().unwrap().to_string(),
|
||
)
|
||
} else {
|
||
match parts.next() {
|
||
Some(op) => {
|
||
if op != "=" {
|
||
return Err(ParseError::InvalidCommand(s.to_string()));
|
||
}
|
||
}
|
||
None => return Err(ParseError::InvalidCommand(s.to_string())),
|
||
};
|
||
let value = parts.collect::<Vec<_>>().join("\t");
|
||
(name, value)
|
||
};
|
||
|
||
let mut cons = Self::iter()
|
||
.find(|i| i.to_string() == name)
|
||
.ok_or(ParseError::InvalidCommand(s.to_string()))?;
|
||
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) => {
|
||
*value = Hook::from_command_args(&name, [value_str].into_iter())?;
|
||
}
|
||
Consequence::FloatPlacement(value) => *value = FloatPlacement::from_str(&value_str)?,
|
||
Consequence::FloatingGeometry { x, y } => {
|
||
let mut values = value_str.split('=');
|
||
*x = u32::from_str(values.next().ok_or(ParseError::ValueMissing)?)?;
|
||
*y = u32::from_str(values.next().ok_or(ParseError::ValueMissing)?)?;
|
||
}
|
||
};
|
||
|
||
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 {
|
||
type Err = ParseError;
|
||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
Self::iter()
|
||
.find(|i| i.to_string() == s)
|
||
.ok_or(ParseError::InvalidCommand(s.to_string()))
|
||
}
|
||
}
|
||
|
||
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 {
|
||
type Err = ParseError;
|
||
|
||
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),
|
||
_ => Err(ParseError::InvalidCommand(s.to_string())),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[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")
|
||
}
|
||
}
|
||
|
||
#[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())),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod test {
|
||
use std::str::FromStr;
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
use crate::{
|
||
hlwm::{parser::FromStrings, rule::FloatPlacement},
|
||
split,
|
||
};
|
||
use pretty_assertions::assert_eq;
|
||
|
||
use super::{Condition, Consequence, Flag, Rule, RuleOperator};
|
||
|
||
#[derive(Debug, Deserialize, Serialize)]
|
||
pub struct RuleWrapper {
|
||
rule: Rule,
|
||
}
|
||
|
||
#[test]
|
||
fn rule_serialize_deserialize() {
|
||
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,
|
||
),
|
||
] {
|
||
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);
|
||
}
|
||
}
|
||
|
||
#[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')
|
||
.map(|l| Rule::from_strings(split::tab_or_space(l).into_iter()))
|
||
.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);
|
||
});
|
||
}
|
||
}
|