atagen

monolith

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