667 lines
23 KiB
Rust
667 lines
23 KiB
Rust
use std::{
|
||
borrow::BorrowMut,
|
||
fmt::{Display, Write},
|
||
str::FromStr,
|
||
};
|
||
|
||
use serde::{de::Expected, Deserialize, Serialize};
|
||
use strum::IntoEnumIterator;
|
||
|
||
use super::{hlwmbool, hook::Hook, split, StringParseError, 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 FromStr for Rule {
|
||
type Err = StringParseError;
|
||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
let parts = split::tab_or_space(s);
|
||
if parts.is_empty() {
|
||
return Err(StringParseError::InvalidLength(0, "parse rule"));
|
||
}
|
||
let mut condition: Option<Condition> = None;
|
||
let mut consequences = vec![];
|
||
let mut label: Option<String> = None;
|
||
let mut flag: Option<Flag> = None;
|
||
|
||
for arg in (&parts)
|
||
.into_iter()
|
||
.map(|a| a.strip_prefix("--"))
|
||
.filter(|a| a.is_some())
|
||
{
|
||
let arg = arg.unwrap();
|
||
if condition.is_none() && arg == Condition::FixedSize.to_string() {
|
||
condition = Some(Condition::FixedSize);
|
||
continue;
|
||
}
|
||
let parts = arg.split(['=', '~']).collect::<Vec<_>>();
|
||
if parts.len() != 2 {
|
||
return Err(StringParseError::InvalidLength(parts.len(), "rule=parts"));
|
||
}
|
||
let mut parts = parts.into_iter();
|
||
let (name, value) = (parts.next().unwrap(), parts.next().unwrap());
|
||
|
||
if name == "label" && label.is_none() {
|
||
label = Some(value.to_string());
|
||
continue;
|
||
}
|
||
|
||
if condition.is_none() && Condition::iter().any(|prop| prop.to_string() == name) {
|
||
condition = Some(match Condition::from_str(arg) {
|
||
Ok(arg) => arg,
|
||
Err(err) => panic!("<<condition>>\n\n{err}\n\n\n"),
|
||
});
|
||
continue;
|
||
}
|
||
|
||
if Consequence::iter().any(|cons| cons.to_string() == name) {
|
||
consequences.push(match Consequence::from_str(arg) {
|
||
Ok(arg) => arg,
|
||
Err(err) => panic!("<<consequence>>\n\n{err}\n\n\n"),
|
||
});
|
||
continue;
|
||
}
|
||
}
|
||
let mut args = parts.into_iter().filter(|a| !a.starts_with("--"));
|
||
while let Some(arg) = args.next() {
|
||
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(StringParseError::RequiredArgMissing(
|
||
"condition and/or consequences".into(),
|
||
));
|
||
}
|
||
|
||
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)?;
|
||
|
||
Ok(Self::from_str(&str_val).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 = StringParseError;
|
||
|
||
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||
Self::iter()
|
||
.find(|i| i.char() == value)
|
||
.ok_or(StringParseError::UnknownValue)
|
||
}
|
||
}
|
||
|
||
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 = StringParseError;
|
||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
s.chars()
|
||
.next()
|
||
.ok_or(StringParseError::InvalidLength(s.len(), "rule operator"))?
|
||
.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 = StringParseError;
|
||
|
||
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
|
||
if s == 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(StringParseError::InvalidLength(1, "property")),
|
||
};
|
||
let mut prop = Self::iter()
|
||
.find(|i| i.to_string() == name)
|
||
.ok_or(StringParseError::UnknownValue)?;
|
||
|
||
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(StringParseError::UnknownValue),
|
||
};
|
||
|
||
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 = StringParseError;
|
||
|
||
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(StringParseError::UnknownValue);
|
||
}
|
||
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(StringParseError::UnknownValue);
|
||
}
|
||
}
|
||
None => return Err(StringParseError::UnknownValue),
|
||
};
|
||
let value = parts.collect::<Vec<_>>().join("\t");
|
||
(name, value)
|
||
};
|
||
|
||
let mut cons = Self::iter()
|
||
.find(|i| i.to_string() == name)
|
||
.ok_or(StringParseError::UnknownValue)?;
|
||
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_str(&value_str).map_err(|_| StringParseError::UnknownValue)?
|
||
}
|
||
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(StringParseError::UnknownValue)?)
|
||
.map_err(|_| StringParseError::UnknownValue)?;
|
||
*y = u32::from_str(values.next().ok_or(StringParseError::UnknownValue)?)
|
||
.map_err(|_| StringParseError::UnknownValue)?;
|
||
}
|
||
};
|
||
|
||
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 = StringParseError;
|
||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
Self::iter()
|
||
.find(|i| i.to_string() == s)
|
||
.ok_or(StringParseError::UnknownValue)
|
||
}
|
||
}
|
||
|
||
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 = StringParseError;
|
||
|
||
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(StringParseError::UnknownValue),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[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")
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod test {
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
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),
|
||
)] {
|
||
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);
|
||
}
|
||
}
|
||
}
|