atagen

monolith

public
Star0
Watch0
605c44c8c5fc0b5e0455cf5570d97997c21fbdd7
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/config/src/lib.rsRust447 lines12.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 output: Outputs,
16
    pub decoration: Decoration,
17
    pub animation: Animation,
18
    pub keyboard: Keyboard,
19
    pub binds: Vec<Bind>,
20
    pub rules: Vec<Rule>,
21
    pub startup: Vec<Vec<String>>,
22
}
23
24
#[derive(Deserialize, Debug, Clone, PartialEq)]
25
#[serde(default, deny_unknown_fields)]
26
pub struct General {
27
    /// What `Mod` means in binds.
28
    #[serde(rename = "mod")]
29
    pub mod_key: Modifier,
30
    pub gaps: Gaps,
31
    /// What a window move does when it meets a neighbour.
32
    #[serde(rename = "move")]
33
    pub move_mode: MoveMode,
34
    pub focus_follows_mouse: bool,
35
    pub mouse_follows_focus: bool,
36
}
37
38
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
39
#[serde(rename_all = "lowercase")]
40
pub enum MoveMode {
41
    /// Trade places with it.
42
    #[default]
43
    Swap,
44
    /// Join its lane, stacking with it if it is alone.
45
    Merge,
46
}
47
48
impl Default for General {
49
    fn default() -> Self {
50
        Self {
51
            mod_key: Modifier::Super,
52
            gaps: Gaps::default(),
53
            move_mode: MoveMode::Swap,
54
            focus_follows_mouse: false,
55
            mouse_follows_focus: false,
56
        }
57
    }
58
}
59
60
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
61
#[serde(default, deny_unknown_fields)]
62
pub struct Gaps {
63
    pub inner: f64,
64
    pub outer: f64,
65
}
66
67
impl Default for Gaps {
68
    fn default() -> Self {
69
        Self {
70
            inner: 8.0,
71
            outer: 8.0,
72
        }
73
    }
74
}
75
76
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
77
#[serde(default, deny_unknown_fields)]
78
pub struct Outputs {
79
    pub vrr: Vrr,
80
}
81
82
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
83
#[serde(rename_all = "lowercase")]
84
pub enum Vrr {
85
    Off,
86
    On,
87
    /// Only while a fullscreen window is shown, where a game sets the pace.
88
    #[default]
89
    Fullscreen,
90
}
91
92
#[derive(Deserialize, Debug, Clone, PartialEq)]
93
#[serde(default, deny_unknown_fields)]
94
pub struct Decoration {
95
    /// Border width in logical pixels, drawn in the gap around each window.
96
    pub border: u32,
97
    pub focused: Color,
98
    pub unfocused: Color,
99
}
100
101
impl Default for Decoration {
102
    fn default() -> Self {
103
        Self {
104
            border: 2,
105
            focused: Color::rgb(0x88, 0xbf, 0xd1),
106
            unfocused: Color::rgb(0x3b, 0x42, 0x52),
107
        }
108
    }
109
}
110
111
/// Straight RGBA from `#rrggbb` or `#rrggbbaa`.
112
#[derive(Debug, Clone, Copy, PartialEq)]
113
pub struct Color(pub [f32; 4]);
114
115
impl Color {
116
    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
117
        Self([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
118
    }
119
}
120
121
impl<'de> Deserialize<'de> for Color {
122
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
123
        let s = String::deserialize(d)?;
124
        let hex = s.strip_prefix('#').unwrap_or(&s);
125
        let bad =
126
            || serde::de::Error::custom(format!("bad colour {s:?}, use #rrggbb or #rrggbbaa"));
127
        if !matches!(hex.len(), 6 | 8) || !hex.is_ascii() {
128
            return Err(bad());
129
        }
130
        let mut c = [1.0f32; 4];
131
        for (i, v) in c.iter_mut().enumerate().take(hex.len() / 2) {
132
            let byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| bad())?;
133
            *v = f32::from(byte) / 255.0;
134
        }
135
        Ok(Self(c))
136
    }
137
}
138
139
#[derive(Deserialize, Debug, Clone, PartialEq)]
140
#[serde(default, deny_unknown_fields)]
141
pub struct Animation {
142
    pub enabled: bool,
143
    pub style: AnimationStyle,
144
    /// Every phase of a planned transition lasts this long.
145
    pub phase_ms: u64,
146
    /// A cubic bezier, as in CSS.
147
    pub curve: [f64; 4],
148
}
149
150
impl Default for Animation {
151
    fn default() -> Self {
152
        Self {
153
            enabled: true,
154
            style: AnimationStyle::Orthogonal,
155
            phase_ms: 160,
156
            curve: [0.45, 0.0, 0.2, 1.0],
157
        }
158
    }
159
}
160
161
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
162
#[serde(rename_all = "lowercase")]
163
pub enum AnimationStyle {
164
    Orthogonal,
165
    Direct,
166
}
167
168
#[derive(Deserialize, Debug, Clone, PartialEq)]
169
#[serde(default, deny_unknown_fields)]
170
pub struct Keyboard {
171
    pub rules: String,
172
    pub model: String,
173
    pub layout: String,
174
    pub variant: String,
175
    pub options: Option<String>,
176
    pub repeat_delay: i32,
177
    pub repeat_rate: i32,
178
}
179
180
impl Default for Keyboard {
181
    fn default() -> Self {
182
        Self {
183
            rules: String::new(),
184
            model: String::new(),
185
            layout: String::new(),
186
            variant: String::new(),
187
            options: None,
188
            repeat_delay: 200,
189
            repeat_rate: 25,
190
        }
191
    }
192
}
193
194
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
195
#[serde(rename_all = "lowercase")]
196
pub enum Modifier {
197
    Super,
198
    Alt,
199
    Ctrl,
200
    Shift,
201
}
202
203
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
204
pub struct Mods {
205
    pub logo: bool,
206
    pub alt: bool,
207
    pub ctrl: bool,
208
    pub shift: bool,
209
}
210
211
impl Mods {
212
    pub fn has(&self, m: Modifier) -> bool {
213
        match m {
214
            Modifier::Super => self.logo,
215
            Modifier::Alt => self.alt,
216
            Modifier::Ctrl => self.ctrl,
217
            Modifier::Shift => self.shift,
218
        }
219
    }
220
221
    fn set(&mut self, m: Modifier) {
222
        match m {
223
            Modifier::Super => self.logo = true,
224
            Modifier::Alt => self.alt = true,
225
            Modifier::Ctrl => self.ctrl = true,
226
            Modifier::Shift => self.shift = true,
227
        }
228
    }
229
}
230
231
#[derive(Debug, Clone, PartialEq)]
232
pub struct Bind {
233
    pub mods: Mods,
234
    /// An xkb keysym name, resolved by the compositor.
235
    pub key: String,
236
    pub command: Command,
237
}
238
239
#[derive(Debug, Clone)]
240
pub struct Rule {
241
    pub app_id: Option<Regex>,
242
    pub title: Option<Regex>,
243
    pub tag: Option<String>,
244
    pub output: Option<String>,
245
    pub floating: Option<bool>,
246
    pub sticky: Option<bool>,
247
    pub scratchpad: Option<String>,
248
}
249
250
/// Everything the matching rules say about a window, later rules winning.
251
#[derive(Debug, Clone, Default, PartialEq)]
252
pub struct Placement {
253
    pub tag: Option<String>,
254
    pub output: Option<String>,
255
    pub floating: Option<bool>,
256
    pub sticky: Option<bool>,
257
    pub scratchpad: Option<String>,
258
}
259
260
#[derive(Debug)]
261
pub enum Error {
262
    Io(PathBuf, std::io::Error),
263
    Toml(toml::de::Error),
264
    Bind(String, String),
265
    Regex(String, regex::Error),
266
}
267
268
impl fmt::Display for Error {
269
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270
        match self {
271
            Error::Io(p, e) => write!(f, "{}: {e}", p.display()),
272
            Error::Toml(e) => write!(f, "{e}"),
273
            Error::Bind(k, e) => write!(f, "bind {k:?}: {e}"),
274
            Error::Regex(r, e) => write!(f, "rule pattern {r:?}: {e}"),
275
        }
276
    }
277
}
278
279
impl std::error::Error for Error {}
280
281
#[derive(Deserialize)]
282
#[serde(deny_unknown_fields)]
283
struct Raw {
284
    #[serde(default)]
285
    startup: Vec<Vec<String>>,
286
    #[serde(default)]
287
    general: General,
288
    #[serde(default)]
289
    keyboard: Keyboard,
290
    #[serde(default)]
291
    animation: Animation,
292
    #[serde(default)]
293
    decoration: Decoration,
294
    #[serde(default)]
295
    output: Outputs,
296
    #[serde(default)]
297
    binds: BTreeMap<String, Words>,
298
    #[serde(default)]
299
    rules: Vec<RawRule>,
300
}
301
302
#[derive(Deserialize)]
303
#[serde(untagged)]
304
enum Words {
305
    Line(String),
306
    List(Vec<String>),
307
}
308
309
#[derive(Deserialize)]
310
#[serde(deny_unknown_fields)]
311
struct RawRule {
312
    app_id: Option<String>,
313
    title: Option<String>,
314
    tag: Option<String>,
315
    output: Option<String>,
316
    floating: Option<bool>,
317
    sticky: Option<bool>,
318
    scratchpad: Option<String>,
319
}
320
321
impl Config {
322
    /// `$MONOLITH_CONFIG`, else `$XDG_CONFIG_HOME/monolith/config.toml`.
323
    pub fn path() -> Option<PathBuf> {
324
        if let Some(p) = std::env::var_os("MONOLITH_CONFIG") {
325
            return Some(p.into());
326
        }
327
        let base = std::env::var_os("XDG_CONFIG_HOME")
328
            .map(PathBuf::from)
329
            .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
330
        Some(base.join("monolith/config.toml"))
331
    }
332
333
    /// Loads `path`, or the built-in default when it does not exist.
334
    pub fn load(path: Option<&Path>) -> Result<Self, Error> {
335
        match path {
336
            Some(p) => match std::fs::read_to_string(p) {
337
                Ok(text) => Self::parse(&text),
338
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::parse(DEFAULT),
339
                Err(e) => Err(Error::Io(p.to_owned(), e)),
340
            },
341
            None => Self::parse(DEFAULT),
342
        }
343
    }
344
345
    pub fn parse(text: &str) -> Result<Self, Error> {
346
        let raw: Raw = toml::from_str(text).map_err(Error::Toml)?;
347
        let mod_key = raw.general.mod_key;
348
        let binds = raw
349
            .binds
350
            .into_iter()
351
            .map(|(combo, words)| parse_bind(&combo, words, mod_key))
352
            .collect::<Result<_, _>>()?;
353
        let regex = |p: Option<String>| {
354
            p.map(|p| Regex::new(&p).map_err(|e| Error::Regex(p, e)))
355
                .transpose()
356
        };
357
        let rules = raw
358
            .rules
359
            .into_iter()
360
            .map(|r| {
361
                Ok(Rule {
362
                    app_id: regex(r.app_id)?,
363
                    title: regex(r.title)?,
364
                    tag: r.tag,
365
                    output: r.output,
366
                    floating: r.floating,
367
                    sticky: r.sticky,
368
                    scratchpad: r.scratchpad,
369
                })
370
            })
371
            .collect::<Result<_, Error>>()?;
372
        Ok(Self {
373
            general: raw.general,
374
            animation: raw.animation,
375
            decoration: raw.decoration,
376
            output: raw.output,
377
            keyboard: raw.keyboard,
378
            binds,
379
            rules,
380
            startup: raw.startup,
381
        })
382
    }
383
384
    pub fn placement(&self, app_id: &str, title: &str) -> Placement {
385
        let mut p = Placement::default();
386
        let hit = |re: &Option<Regex>, s: &str| re.as_ref().is_none_or(|re| re.is_match(s));
387
        for r in &self.rules {
388
            if !hit(&r.app_id, app_id) || !hit(&r.title, title) {
389
                continue;
390
            }
391
            p.tag = r.tag.clone().or(p.tag);
392
            p.output = r.output.clone().or(p.output);
393
            p.floating = r.floating.or(p.floating);
394
            p.sticky = r.sticky.or(p.sticky);
395
            p.scratchpad = r.scratchpad.clone().or(p.scratchpad);
396
        }
397
        p
398
    }
399
}
400
401
fn parse_bind(combo: &str, words: Words, mod_key: Modifier) -> Result<Bind, Error> {
402
    let bad = |m: String| Error::Bind(combo.to_owned(), m);
403
    let parts: Vec<&str> = combo.split('+').map(str::trim).collect();
404
    let Some((&key, mods)) = parts.split_last().filter(|(k, _)| !k.is_empty()) else {
405
        return Err(bad("no key".into()));
406
    };
407
    let mut set = Mods::default();
408
    for m in mods {
409
        let m = match m.to_ascii_lowercase().as_str() {
410
            "mod" => mod_key,
411
            "super" | "logo" | "win" => Modifier::Super,
412
            "alt" => Modifier::Alt,
413
            "ctrl" | "control" => Modifier::Ctrl,
414
            "shift" => Modifier::Shift,
415
            other => return Err(bad(format!("unknown modifier {other}"))),
416
        };
417
        set.set(m);
418
    }
419
    let words: Vec<String> = match words {
420
        Words::Line(s) => s.split_whitespace().map(str::to_owned).collect(),
421
        Words::List(v) => v,
422
    };
423
    let command = Command::parse(&words).map_err(|e| bad(e.0))?;
424
    Ok(Bind {
425
        mods: set,
426
        key: key.to_owned(),
427
        command,
428
    })
429
}
430
431
#[cfg(test)]
432
mod tests {
433
    use super::*;
434
435
    /// default.toml documents the defaults for users, and the Default impls
436
    /// apply them to sections a user config leaves out. They must agree.
437
    #[test]
438
    fn default_toml_states_the_defaults() {
439
        let c = Config::parse(DEFAULT).unwrap();
440
        assert_eq!(c.general, General::default());
441
        assert_eq!(c.output, Outputs::default());
442
        assert_eq!(c.decoration, Decoration::default());
443
        assert_eq!(c.animation, Animation::default());
444
        assert_eq!(c.keyboard, Keyboard::default());
445
        assert!(c.binds.len() > 40);
446
    }
447
}