srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/rules.rs
blob: 4d881a509a3cab01265d7877dc291845802717fc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
use crate::geometry::Rect;
use crate::window::Window;
use crate::workspace::WorkspaceId;

/// Match criteria for a [`WindowRule`]. A matcher with every field `None`
/// matches nothing (an accidental `srd.rule({}, {...})` in config should be a
/// silent no-op, not "apply to every window").
#[derive(Debug, Clone, Default)]
pub struct WindowMatch {
    /// Case-insensitive substring match against `Window::title`.
    pub title_contains: Option<String>,
    /// Case-insensitive exact match against `Window::app_id` (X11 `WM_CLASS`
    /// / Wayland `app_id`).
    pub class: Option<String>,
}

impl WindowMatch {
    pub fn is_empty(&self) -> bool {
        self.title_contains.is_none() && self.class.is_none()
    }

    pub fn matches(&self, window: &Window) -> bool {
        if self.is_empty() {
            return false;
        }
        if let Some(t) = &self.title_contains {
            if !window.title.to_lowercase().contains(&t.to_lowercase()) {
                return false;
            }
        }
        if let Some(c) = &self.class {
            if !window.app_id.eq_ignore_ascii_case(c) {
                return false;
            }
        }
        true
    }
}

/// Actions applied once, when a matching window is first added.
#[derive(Debug, Clone, Default)]
pub struct WindowRuleActions {
    pub floating: Option<bool>,
    pub maximized: Option<bool>,
    pub workspace: Option<WorkspaceId>,
    pub geometry: Option<Rect>,
    pub decorated: Option<bool>,
    pub border_color: Option<(u8, u8, u8)>,
    pub border_width: Option<u32>,
}

#[derive(Debug, Clone, Default)]
pub struct WindowRule {
    pub matcher: WindowMatch,
    pub actions: WindowRuleActions,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn empty_matcher_matches_nothing() {
        let w = Window::new(1, "anything");
        assert!(!WindowMatch::default().matches(&w));
    }

    #[test]
    fn title_match_is_case_insensitive_substring() {
        let w = Window::new(1, "Mozilla Firefox");
        let m = WindowMatch { title_contains: Some("firefox".into()), class: None };
        assert!(m.matches(&w));
    }

    #[test]
    fn class_match_is_case_insensitive_exact() {
        let mut w = Window::new(1, "");
        w.app_id = "Firefox".into();
        let m = WindowMatch { title_contains: None, class: Some("firefox".into()) };
        assert!(m.matches(&w));
        let mut w2 = Window::new(2, "");
        w2.app_id = "firefoxx".into();
        assert!(!m.matches(&w2));
    }
}