atagen

monolith

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