atagen

monolith

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