atagen

monolith

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