atagen

monolith

public
Star0
Watch0
53111dd60132f6f8f83b508cc035f9509924e231
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/config/src/lib.rsRust419 lines11.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
}
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([0.53, 0.75, 0.82, 1.0]),
102
            unfocused: Color([0.23, 0.26, 0.32, 1.0]),
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<'de> Deserialize<'de> for Color {
112
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
113
        let s = String::deserialize(d)?;
114
        let hex = s.strip_prefix('#').unwrap_or(&s);
115
        let bad =
116
            || serde::de::Error::custom(format!("bad colour {s:?}, use #rrggbb or #rrggbbaa"));
117
        if !matches!(hex.len(), 6 | 8) || !hex.is_ascii() {
118
            return Err(bad());
119
        }
120
        let mut c = [1.0f32; 4];
121
        for (i, v) in c.iter_mut().enumerate().take(hex.len() / 2) {
122
            let byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| bad())?;
123
            *v = f32::from(byte) / 255.0;
124
        }
125
        Ok(Self(c))
126
    }
127
}
128
129
#[derive(Deserialize, Debug, Clone, PartialEq)]
130
#[serde(default, deny_unknown_fields)]
131
pub struct Animation {
132
    pub enabled: bool,
133
    pub style: AnimationStyle,
134
    /// Every phase of a planned transition lasts this long.
135
    pub phase_ms: u64,
136
    /// A cubic bezier, as in CSS.
137
    pub curve: [f64; 4],
138
}
139
140
impl Default for Animation {
141
    fn default() -> Self {
142
        Self {
143
            enabled: true,
144
            style: AnimationStyle::Orthogonal,
145
            phase_ms: 160,
146
            curve: [0.45, 0.0, 0.2, 1.0],
147
        }
148
    }
149
}
150
151
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
152
#[serde(rename_all = "lowercase")]
153
pub enum AnimationStyle {
154
    Orthogonal,
155
    Direct,
156
}
157
158
#[derive(Deserialize, Debug, Clone, PartialEq)]
159
#[serde(default, deny_unknown_fields)]
160
pub struct Keyboard {
161
    pub rules: String,
162
    pub model: String,
163
    pub layout: String,
164
    pub variant: String,
165
    pub options: Option<String>,
166
    pub repeat_delay: i32,
167
    pub repeat_rate: i32,
168
}
169
170
impl Default for Keyboard {
171
    fn default() -> Self {
172
        Self {
173
            rules: String::new(),
174
            model: String::new(),
175
            layout: String::new(),
176
            variant: String::new(),
177
            options: None,
178
            repeat_delay: 200,
179
            repeat_rate: 25,
180
        }
181
    }
182
}
183
184
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
185
#[serde(rename_all = "lowercase")]
186
pub enum Modifier {
187
    Super,
188
    Alt,
189
    Ctrl,
190
    Shift,
191
}
192
193
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
194
pub struct Mods {
195
    pub logo: bool,
196
    pub alt: bool,
197
    pub ctrl: bool,
198
    pub shift: bool,
199
}
200
201
impl Mods {
202
    fn set(&mut self, m: Modifier) {
203
        match m {
204
            Modifier::Super => self.logo = true,
205
            Modifier::Alt => self.alt = true,
206
            Modifier::Ctrl => self.ctrl = true,
207
            Modifier::Shift => self.shift = true,
208
        }
209
    }
210
}
211
212
#[derive(Debug, Clone, PartialEq)]
213
pub struct Bind {
214
    pub mods: Mods,
215
    /// An xkb keysym name, resolved by the compositor.
216
    pub key: String,
217
    pub command: Command,
218
}
219
220
#[derive(Debug, Clone)]
221
pub struct Rule {
222
    pub app_id: Option<Regex>,
223
    pub title: Option<Regex>,
224
    pub tag: Option<String>,
225
    pub output: Option<String>,
226
    pub floating: Option<bool>,
227
    pub sticky: Option<bool>,
228
    pub scratchpad: Option<String>,
229
}
230
231
/// Everything the matching rules say about a window, later rules winning.
232
#[derive(Debug, Clone, Default, PartialEq)]
233
pub struct Placement {
234
    pub tag: Option<String>,
235
    pub output: Option<String>,
236
    pub floating: Option<bool>,
237
    pub sticky: Option<bool>,
238
    pub scratchpad: Option<String>,
239
}
240
241
#[derive(Debug)]
242
pub enum Error {
243
    Io(PathBuf, std::io::Error),
244
    Toml(toml::de::Error),
245
    Bind(String, String),
246
    Regex(String, regex::Error),
247
}
248
249
impl fmt::Display for Error {
250
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251
        match self {
252
            Error::Io(p, e) => write!(f, "{}: {e}", p.display()),
253
            Error::Toml(e) => write!(f, "{e}"),
254
            Error::Bind(k, e) => write!(f, "bind {k:?}: {e}"),
255
            Error::Regex(r, e) => write!(f, "rule pattern {r:?}: {e}"),
256
        }
257
    }
258
}
259
260
impl std::error::Error for Error {}
261
262
#[derive(Deserialize)]
263
#[serde(deny_unknown_fields)]
264
struct Raw {
265
    #[serde(default)]
266
    startup: Vec<Vec<String>>,
267
    #[serde(default)]
268
    general: General,
269
    #[serde(default)]
270
    keyboard: Keyboard,
271
    #[serde(default)]
272
    animation: Animation,
273
    #[serde(default)]
274
    decoration: Decoration,
275
    #[serde(default)]
276
    output: Outputs,
277
    #[serde(default)]
278
    binds: BTreeMap<String, Words>,
279
    #[serde(default)]
280
    rules: Vec<RawRule>,
281
}
282
283
#[derive(Deserialize)]
284
#[serde(untagged)]
285
enum Words {
286
    Line(String),
287
    List(Vec<String>),
288
}
289
290
#[derive(Deserialize)]
291
#[serde(deny_unknown_fields)]
292
struct RawRule {
293
    app_id: Option<String>,
294
    title: Option<String>,
295
    tag: Option<String>,
296
    output: Option<String>,
297
    floating: Option<bool>,
298
    sticky: Option<bool>,
299
    scratchpad: Option<String>,
300
}
301
302
impl Config {
303
    /// `$MONOLITH_CONFIG`, else `$XDG_CONFIG_HOME/monolith/config.toml`.
304
    pub fn path() -> Option<PathBuf> {
305
        if let Some(p) = std::env::var_os("MONOLITH_CONFIG") {
306
            return Some(p.into());
307
        }
308
        let base = std::env::var_os("XDG_CONFIG_HOME")
309
            .map(PathBuf::from)
310
            .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
311
        Some(base.join("monolith/config.toml"))
312
    }
313
314
    /// Loads `path`, or the built-in default when it does not exist.
315
    pub fn load(path: Option<&Path>) -> Result<Self, Error> {
316
        match path {
317
            Some(p) => match std::fs::read_to_string(p) {
318
                Ok(text) => Self::parse(&text),
319
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::parse(DEFAULT),
320
                Err(e) => Err(Error::Io(p.to_owned(), e)),
321
            },
322
            None => Self::parse(DEFAULT),
323
        }
324
    }
325
326
    pub fn parse(text: &str) -> Result<Self, Error> {
327
        let raw: Raw = toml::from_str(text).map_err(Error::Toml)?;
328
        let mod_key = raw.general.mod_key;
329
        let binds = raw
330
            .binds
331
            .into_iter()
332
            .map(|(combo, words)| parse_bind(&combo, words, mod_key))
333
            .collect::<Result<_, _>>()?;
334
        let regex = |p: Option<String>| {
335
            p.map(|p| Regex::new(&p).map_err(|e| Error::Regex(p, e)))
336
                .transpose()
337
        };
338
        let rules = raw
339
            .rules
340
            .into_iter()
341
            .map(|r| {
342
                Ok(Rule {
343
                    app_id: regex(r.app_id)?,
344
                    title: regex(r.title)?,
345
                    tag: r.tag,
346
                    output: r.output,
347
                    floating: r.floating,
348
                    sticky: r.sticky,
349
                    scratchpad: r.scratchpad,
350
                })
351
            })
352
            .collect::<Result<_, Error>>()?;
353
        Ok(Self {
354
            general: raw.general,
355
            animation: raw.animation,
356
            decoration: raw.decoration,
357
            output: raw.output,
358
            keyboard: raw.keyboard,
359
            binds,
360
            rules,
361
            startup: raw.startup,
362
        })
363
    }
364
365
    pub fn placement(&self, app_id: &str, title: &str) -> Placement {
366
        let mut p = Placement::default();
367
        let hit = |re: &Option<Regex>, s: &str| re.as_ref().is_none_or(|re| re.is_match(s));
368
        for r in &self.rules {
369
            if !hit(&r.app_id, app_id) || !hit(&r.title, title) {
370
                continue;
371
            }
372
            p.tag = r.tag.clone().or(p.tag);
373
            p.output = r.output.clone().or(p.output);
374
            p.floating = r.floating.or(p.floating);
375
            p.sticky = r.sticky.or(p.sticky);
376
            p.scratchpad = r.scratchpad.clone().or(p.scratchpad);
377
        }
378
        p
379
    }
380
}
381
382
fn parse_bind(combo: &str, words: Words, mod_key: Modifier) -> Result<Bind, Error> {
383
    let bad = |m: String| Error::Bind(combo.to_owned(), m);
384
    let parts: Vec<&str> = combo.split('+').map(str::trim).collect();
385
    let Some((&key, mods)) = parts.split_last().filter(|(k, _)| !k.is_empty()) else {
386
        return Err(bad("no key".into()));
387
    };
388
    let mut set = Mods::default();
389
    for m in mods {
390
        let m = match m.to_ascii_lowercase().as_str() {
391
            "mod" => mod_key,
392
            "super" | "logo" | "win" => Modifier::Super,
393
            "alt" => Modifier::Alt,
394
            "ctrl" | "control" => Modifier::Ctrl,
395
            "shift" => Modifier::Shift,
396
            other => return Err(bad(format!("unknown modifier {other}"))),
397
        };
398
        set.set(m);
399
    }
400
    let words: Vec<String> = match words {
401
        Words::Line(s) => s.split_whitespace().map(str::to_owned).collect(),
402
        Words::List(v) => v,
403
    };
404
    let command = Command::parse(&words).map_err(|e| bad(e.0))?;
405
    Ok(Bind {
406
        mods: set,
407
        key: key.to_owned(),
408
        command,
409
    })
410
}
411
412
#[cfg(test)]
413
mod tests {
414
    #[test]
415
    fn the_default_config_parses() {
416
        let c = super::Config::parse(super::DEFAULT).unwrap();
417
        assert!(c.binds.len() > 40);
418
    }
419
}