atagen

monolith

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