atagen

monolith

public
Star0
Watch0
4e5afcada08b43cdb086a1cf8a4562e47d03d154
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/config/src/lib.rsRust307 lines8.2 KB
1
//! The TOML config. Parsing and validation only, the compositor applies it.
2
3
use monolith_ipc::Command;
4
use regex::Regex;
5
use serde::Deserialize;
6
use std::collections::BTreeMap;
7
use std::fmt;
8
use std::path::{Path, PathBuf};
9
10
pub const DEFAULT: &str = include_str!("../default.toml");
11
12
#[derive(Debug, Clone)]
13
pub struct Config {
14
    pub general: General,
15
    pub keyboard: Keyboard,
16
    pub binds: Vec<Bind>,
17
    pub rules: Vec<Rule>,
18
    pub startup: Vec<Vec<String>>,
19
}
20
21
#[derive(Deserialize, Debug, Clone, PartialEq)]
22
#[serde(default, deny_unknown_fields)]
23
pub struct General {
24
    /// What `Mod` means in binds.
25
    #[serde(rename = "mod")]
26
    pub mod_key: Modifier,
27
    pub gaps: Gaps,
28
}
29
30
impl Default for General {
31
    fn default() -> Self {
32
        Self {
33
            mod_key: Modifier::Super,
34
            gaps: Gaps::default(),
35
        }
36
    }
37
}
38
39
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
40
#[serde(default, deny_unknown_fields)]
41
pub struct Gaps {
42
    pub inner: f64,
43
    pub outer: f64,
44
}
45
46
impl Default for Gaps {
47
    fn default() -> Self {
48
        Self {
49
            inner: 8.0,
50
            outer: 8.0,
51
        }
52
    }
53
}
54
55
#[derive(Deserialize, Debug, Clone, PartialEq)]
56
#[serde(default, deny_unknown_fields)]
57
pub struct Keyboard {
58
    pub rules: String,
59
    pub model: String,
60
    pub layout: String,
61
    pub variant: String,
62
    pub options: Option<String>,
63
    pub repeat_delay: i32,
64
    pub repeat_rate: i32,
65
}
66
67
impl Default for Keyboard {
68
    fn default() -> Self {
69
        Self {
70
            rules: String::new(),
71
            model: String::new(),
72
            layout: String::new(),
73
            variant: String::new(),
74
            options: None,
75
            repeat_delay: 200,
76
            repeat_rate: 25,
77
        }
78
    }
79
}
80
81
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
82
#[serde(rename_all = "lowercase")]
83
pub enum Modifier {
84
    Super,
85
    Alt,
86
    Ctrl,
87
    Shift,
88
}
89
90
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
91
pub struct Mods {
92
    pub logo: bool,
93
    pub alt: bool,
94
    pub ctrl: bool,
95
    pub shift: bool,
96
}
97
98
impl Mods {
99
    fn set(&mut self, m: Modifier) {
100
        match m {
101
            Modifier::Super => self.logo = true,
102
            Modifier::Alt => self.alt = true,
103
            Modifier::Ctrl => self.ctrl = true,
104
            Modifier::Shift => self.shift = true,
105
        }
106
    }
107
}
108
109
#[derive(Debug, Clone, PartialEq)]
110
pub struct Bind {
111
    pub mods: Mods,
112
    /// An xkb keysym name, resolved by the compositor.
113
    pub key: String,
114
    pub command: Command,
115
}
116
117
#[derive(Debug, Clone)]
118
pub struct Rule {
119
    pub app_id: Option<Regex>,
120
    pub title: Option<Regex>,
121
    pub tag: Option<String>,
122
    pub output: Option<String>,
123
    pub floating: Option<bool>,
124
    pub sticky: Option<bool>,
125
    pub scratchpad: Option<String>,
126
}
127
128
/// Everything the matching rules say about a window, later rules winning.
129
#[derive(Debug, Clone, Default, PartialEq)]
130
pub struct Placement {
131
    pub tag: Option<String>,
132
    pub output: Option<String>,
133
    pub floating: Option<bool>,
134
    pub sticky: Option<bool>,
135
    pub scratchpad: Option<String>,
136
}
137
138
#[derive(Debug)]
139
pub enum Error {
140
    Io(PathBuf, std::io::Error),
141
    Toml(toml::de::Error),
142
    Bind(String, String),
143
    Regex(String, regex::Error),
144
}
145
146
impl fmt::Display for Error {
147
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148
        match self {
149
            Error::Io(p, e) => write!(f, "{}: {e}", p.display()),
150
            Error::Toml(e) => write!(f, "{e}"),
151
            Error::Bind(k, e) => write!(f, "bind {k:?}: {e}"),
152
            Error::Regex(r, e) => write!(f, "rule pattern {r:?}: {e}"),
153
        }
154
    }
155
}
156
157
impl std::error::Error for Error {}
158
159
#[derive(Deserialize)]
160
#[serde(deny_unknown_fields)]
161
struct Raw {
162
    #[serde(default)]
163
    startup: Vec<Vec<String>>,
164
    #[serde(default)]
165
    general: General,
166
    #[serde(default)]
167
    keyboard: Keyboard,
168
    #[serde(default)]
169
    binds: BTreeMap<String, Words>,
170
    #[serde(default)]
171
    rules: Vec<RawRule>,
172
}
173
174
#[derive(Deserialize)]
175
#[serde(untagged)]
176
enum Words {
177
    Line(String),
178
    List(Vec<String>),
179
}
180
181
#[derive(Deserialize)]
182
#[serde(deny_unknown_fields)]
183
struct RawRule {
184
    app_id: Option<String>,
185
    title: Option<String>,
186
    tag: Option<String>,
187
    output: Option<String>,
188
    floating: Option<bool>,
189
    sticky: Option<bool>,
190
    scratchpad: Option<String>,
191
}
192
193
impl Config {
194
    /// `$MONOLITH_CONFIG`, else `$XDG_CONFIG_HOME/monolith/config.toml`.
195
    pub fn path() -> Option<PathBuf> {
196
        if let Some(p) = std::env::var_os("MONOLITH_CONFIG") {
197
            return Some(p.into());
198
        }
199
        let base = std::env::var_os("XDG_CONFIG_HOME")
200
            .map(PathBuf::from)
201
            .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
202
        Some(base.join("monolith/config.toml"))
203
    }
204
205
    /// Loads `path`, or the built-in default when it does not exist.
206
    pub fn load(path: Option<&Path>) -> Result<Self, Error> {
207
        match path {
208
            Some(p) => match std::fs::read_to_string(p) {
209
                Ok(text) => Self::parse(&text),
210
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::parse(DEFAULT),
211
                Err(e) => Err(Error::Io(p.to_owned(), e)),
212
            },
213
            None => Self::parse(DEFAULT),
214
        }
215
    }
216
217
    pub fn parse(text: &str) -> Result<Self, Error> {
218
        let raw: Raw = toml::from_str(text).map_err(Error::Toml)?;
219
        let mod_key = raw.general.mod_key;
220
        let binds = raw
221
            .binds
222
            .into_iter()
223
            .map(|(combo, words)| parse_bind(&combo, words, mod_key))
224
            .collect::<Result<_, _>>()?;
225
        let regex = |p: Option<String>| {
226
            p.map(|p| Regex::new(&p).map_err(|e| Error::Regex(p, e)))
227
                .transpose()
228
        };
229
        let rules = raw
230
            .rules
231
            .into_iter()
232
            .map(|r| {
233
                Ok(Rule {
234
                    app_id: regex(r.app_id)?,
235
                    title: regex(r.title)?,
236
                    tag: r.tag,
237
                    output: r.output,
238
                    floating: r.floating,
239
                    sticky: r.sticky,
240
                    scratchpad: r.scratchpad,
241
                })
242
            })
243
            .collect::<Result<_, Error>>()?;
244
        Ok(Self {
245
            general: raw.general,
246
            keyboard: raw.keyboard,
247
            binds,
248
            rules,
249
            startup: raw.startup,
250
        })
251
    }
252
253
    pub fn placement(&self, app_id: &str, title: &str) -> Placement {
254
        let mut p = Placement::default();
255
        let hit = |re: &Option<Regex>, s: &str| re.as_ref().is_none_or(|re| re.is_match(s));
256
        for r in &self.rules {
257
            if !hit(&r.app_id, app_id) || !hit(&r.title, title) {
258
                continue;
259
            }
260
            p.tag = r.tag.clone().or(p.tag);
261
            p.output = r.output.clone().or(p.output);
262
            p.floating = r.floating.or(p.floating);
263
            p.sticky = r.sticky.or(p.sticky);
264
            p.scratchpad = r.scratchpad.clone().or(p.scratchpad);
265
        }
266
        p
267
    }
268
}
269
270
fn parse_bind(combo: &str, words: Words, mod_key: Modifier) -> Result<Bind, Error> {
271
    let bad = |m: String| Error::Bind(combo.to_owned(), m);
272
    let parts: Vec<&str> = combo.split('+').map(str::trim).collect();
273
    let Some((&key, mods)) = parts.split_last().filter(|(k, _)| !k.is_empty()) else {
274
        return Err(bad("no key".into()));
275
    };
276
    let mut set = Mods::default();
277
    for m in mods {
278
        let m = match m.to_ascii_lowercase().as_str() {
279
            "mod" => mod_key,
280
            "super" | "logo" | "win" => Modifier::Super,
281
            "alt" => Modifier::Alt,
282
            "ctrl" | "control" => Modifier::Ctrl,
283
            "shift" => Modifier::Shift,
284
            other => return Err(bad(format!("unknown modifier {other}"))),
285
        };
286
        set.set(m);
287
    }
288
    let words: Vec<String> = match words {
289
        Words::Line(s) => s.split_whitespace().map(str::to_owned).collect(),
290
        Words::List(v) => v,
291
    };
292
    let command = Command::parse(&words).map_err(|e| bad(e.0))?;
293
    Ok(Bind {
294
        mods: set,
295
        key: key.to_owned(),
296
        command,
297
    })
298
}
299
300
#[cfg(test)]
301
mod tests {
302
    #[test]
303
    fn the_default_config_parses() {
304
        let c = super::Config::parse(super::DEFAULT).unwrap();
305
        assert!(c.binds.len() > 40);
306
    }
307
}