atagen

monolith

public
Star0
Watch0
140b53f6ef8471eeb8b42353c198c3f41283dc7c
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/config/src/lib.rsRust848 lines25.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 serde::de::DeserializeOwned;
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 input: Input,
20
    pub idle: Idle,
21
    pub binds: Vec<Bind>,
22
    pub rules: Vec<Rule>,
23
    pub startup: Vec<Vec<String>>,
24
}
25
26
#[derive(Deserialize, Debug, Clone, PartialEq)]
27
#[serde(default, deny_unknown_fields)]
28
pub struct General {
29
    /// What `Mod` means in binds.
30
    #[serde(rename = "mod")]
31
    pub mod_key: Modifier,
32
    pub gaps: Gaps,
33
    /// What a window move does when it meets a neighbour.
34
    #[serde(rename = "move")]
35
    pub move_mode: MoveMode,
36
    pub focus_follows_mouse: bool,
37
    pub mouse_follows_focus: bool,
38
    /// The first window to land on or move to an empty tag nobody is
39
    /// viewing brings that tag into view on its output.
40
    pub view_tag_on_first_window: bool,
41
}
42
43
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
44
#[serde(rename_all = "lowercase")]
45
pub enum MoveMode {
46
    /// Trade places with it.
47
    #[default]
48
    Swap,
49
    /// Join its lane, stacking with it if it is alone.
50
    Merge,
51
}
52
53
impl Default for General {
54
    fn default() -> Self {
55
        Self {
56
            mod_key: Modifier::Super,
57
            gaps: Gaps::default(),
58
            move_mode: MoveMode::Swap,
59
            focus_follows_mouse: false,
60
            mouse_follows_focus: false,
61
            view_tag_on_first_window: true,
62
        }
63
    }
64
}
65
66
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
67
#[serde(default, deny_unknown_fields)]
68
pub struct Gaps {
69
    pub inner: f64,
70
    pub outer: f64,
71
}
72
73
impl Default for Gaps {
74
    fn default() -> Self {
75
        Self {
76
            inner: 8.0,
77
            outer: 8.0,
78
        }
79
    }
80
}
81
82
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
83
#[serde(default)]
84
pub struct Outputs {
85
    pub vrr: Vrr,
86
    /// Settings for one connector, by name, like `DP-1`.
87
    #[serde(flatten)]
88
    pub named: std::collections::BTreeMap<String, OutputConfig>,
89
}
90
91
/// Transform and position follow a reload. A new mode waits for a restart
92
/// or a replug, since it means a modeset.
93
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
94
#[serde(default, deny_unknown_fields)]
95
pub struct OutputConfig {
96
    pub transform: OutputTransform,
97
    /// Top left corner in the global layout. Outputs without one are placed
98
    /// to the right of the others.
99
    pub position: Option<[i32; 2]>,
100
    /// `WIDTHxHEIGHT` or `WIDTHxHEIGHT@HZ`. Without a rate the highest one
101
    /// for that size wins.
102
    pub mode: Option<String>,
103
}
104
105
/// Counterclockwise rotation, optionally after flipping horizontally.
106
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
107
#[serde(rename_all = "kebab-case")]
108
pub enum OutputTransform {
109
    #[default]
110
    Normal,
111
    #[serde(rename = "90")]
112
    Rotate90,
113
    #[serde(rename = "180")]
114
    Rotate180,
115
    #[serde(rename = "270")]
116
    Rotate270,
117
    Flipped,
118
    #[serde(rename = "flipped-90")]
119
    Flipped90,
120
    #[serde(rename = "flipped-180")]
121
    Flipped180,
122
    #[serde(rename = "flipped-270")]
123
    Flipped270,
124
}
125
126
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
127
#[serde(rename_all = "lowercase")]
128
pub enum Vrr {
129
    Off,
130
    On,
131
    /// Only while a fullscreen window is shown, where a game sets the pace.
132
    #[default]
133
    Fullscreen,
134
}
135
136
#[derive(Deserialize, Debug, Clone, PartialEq)]
137
#[serde(default, deny_unknown_fields)]
138
pub struct Decoration {
139
    /// Border width in logical pixels, drawn in the gap around each window.
140
    pub border: u32,
141
    pub radius: Corners,
142
    pub focused: Color,
143
    pub unfocused: Color,
144
}
145
146
/// Corner radii in logical pixels, top left, top right, bottom right and
147
/// bottom left. One number rounds every corner alike.
148
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
149
#[serde(from = "CornersToml")]
150
pub struct Corners(pub [u32; 4]);
151
152
#[derive(Deserialize)]
153
#[serde(
154
    untagged,
155
    expecting = "expected a number or [top left, top right, bottom right, bottom left]"
156
)]
157
enum CornersToml {
158
    All(u32),
159
    Each([u32; 4]),
160
}
161
162
impl From<CornersToml> for Corners {
163
    fn from(c: CornersToml) -> Self {
164
        match c {
165
            CornersToml::All(r) => Self([r; 4]),
166
            CornersToml::Each(r) => Self(r),
167
        }
168
    }
169
}
170
171
impl Default for Decoration {
172
    fn default() -> Self {
173
        Self {
174
            border: 2,
175
            radius: Corners([0; 4]),
176
            focused: Color::rgb(0x88, 0xbf, 0xd1),
177
            unfocused: Color::rgb(0x3b, 0x42, 0x52),
178
        }
179
    }
180
}
181
182
/// Straight RGBA from `#rrggbb` or `#rrggbbaa`.
183
#[derive(Debug, Clone, Copy, PartialEq)]
184
pub struct Color(pub [f32; 4]);
185
186
impl Color {
187
    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
188
        Self([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
189
    }
190
}
191
192
impl<'de> Deserialize<'de> for Color {
193
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
194
        let s = String::deserialize(d)?;
195
        let hex = s.strip_prefix('#').unwrap_or(&s);
196
        let bad =
197
            || serde::de::Error::custom(format!("bad colour {s:?}, use #rrggbb or #rrggbbaa"));
198
        if !matches!(hex.len(), 6 | 8) || !hex.is_ascii() {
199
            return Err(bad());
200
        }
201
        let mut c = [1.0f32; 4];
202
        for (i, v) in c.iter_mut().enumerate().take(hex.len() / 2) {
203
            let byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| bad())?;
204
            *v = f32::from(byte) / 255.0;
205
        }
206
        Ok(Self(c))
207
    }
208
}
209
210
#[derive(Deserialize, Debug, Clone, PartialEq)]
211
#[serde(default, deny_unknown_fields)]
212
pub struct Animation {
213
    pub enabled: bool,
214
    pub style: AnimationStyle,
215
    /// Every phase of a planned transition lasts this long.
216
    pub phase_ms: u64,
217
    /// A cubic bezier, as in CSS.
218
    pub curve: [f64; 4],
219
}
220
221
impl Default for Animation {
222
    fn default() -> Self {
223
        Self {
224
            enabled: true,
225
            style: AnimationStyle::Orthogonal,
226
            phase_ms: 160,
227
            curve: [0.45, 0.0, 0.2, 1.0],
228
        }
229
    }
230
}
231
232
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
233
#[serde(rename_all = "lowercase")]
234
pub enum AnimationStyle {
235
    Orthogonal,
236
    Direct,
237
}
238
239
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
240
#[serde(default, deny_unknown_fields)]
241
pub struct Idle {
242
    /// Seconds without input before every output is switched off, 0 for
243
    /// never.
244
    pub blank_after: u64,
245
}
246
247
#[derive(Deserialize, Debug, Clone, PartialEq)]
248
#[serde(default, deny_unknown_fields)]
249
pub struct Keyboard {
250
    pub rules: String,
251
    pub model: String,
252
    pub layout: String,
253
    pub variant: String,
254
    pub options: Option<String>,
255
    pub repeat_delay: i32,
256
    pub repeat_rate: i32,
257
}
258
259
impl Default for Keyboard {
260
    fn default() -> Self {
261
        Self {
262
            rules: String::new(),
263
            model: String::new(),
264
            layout: String::new(),
265
            variant: String::new(),
266
            options: None,
267
            repeat_delay: 200,
268
            repeat_rate: 25,
269
        }
270
    }
271
}
272
273
/// libinput settings for pointing devices. Unset options keep libinput's
274
/// default for the device.
275
#[derive(Debug, Clone, PartialEq, Default)]
276
pub struct Input {
277
    pub pointer: DeviceSettings,
278
    /// Devices that can tap, which libinput only offers on touchpads.
279
    pub touchpad: DeviceSettings,
280
    /// Overrides for devices by name, later entries winning.
281
    pub device: Vec<NamedDevice>,
282
}
283
284
#[derive(Debug, Clone, PartialEq)]
285
pub struct NamedDevice {
286
    pub name: String,
287
    pub settings: DeviceSettings,
288
}
289
290
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
291
#[serde(default, deny_unknown_fields)]
292
pub struct DeviceSettings {
293
    pub accel_profile: Option<AccelProfile>,
294
    pub accel_speed: Option<f64>,
295
    /// The custom profile's curve, pointer speed sampled every `accel_step`
296
    /// device units per millisecond.
297
    pub accel_points: Option<Vec<f64>>,
298
    pub accel_step: Option<f64>,
299
    pub natural_scroll: Option<bool>,
300
    pub left_handed: Option<bool>,
301
    pub tap: Option<bool>,
302
    pub tap_drag: Option<bool>,
303
    pub tap_drag_lock: Option<bool>,
304
    pub dwt: Option<bool>,
305
    pub middle_emulation: Option<bool>,
306
    pub scroll_method: Option<ScrollMethod>,
307
    pub scroll_button: Option<u32>,
308
    pub click_method: Option<ClickMethod>,
309
    /// Applied by the compositor, libinput has no such setting.
310
    pub scroll_factor: Option<f64>,
311
}
312
313
impl DeviceSettings {
314
    /// `self` with every option `over` sets replaced.
315
    pub fn overlay(&self, over: &Self) -> Self {
316
        Self {
317
            accel_profile: over.accel_profile.or(self.accel_profile),
318
            accel_speed: over.accel_speed.or(self.accel_speed),
319
            accel_points: over
320
                .accel_points
321
                .clone()
322
                .or_else(|| self.accel_points.clone()),
323
            accel_step: over.accel_step.or(self.accel_step),
324
            natural_scroll: over.natural_scroll.or(self.natural_scroll),
325
            left_handed: over.left_handed.or(self.left_handed),
326
            tap: over.tap.or(self.tap),
327
            tap_drag: over.tap_drag.or(self.tap_drag),
328
            tap_drag_lock: over.tap_drag_lock.or(self.tap_drag_lock),
329
            dwt: over.dwt.or(self.dwt),
330
            middle_emulation: over.middle_emulation.or(self.middle_emulation),
331
            scroll_method: over.scroll_method.or(self.scroll_method),
332
            scroll_button: over.scroll_button.or(self.scroll_button),
333
            click_method: over.click_method.or(self.click_method),
334
            scroll_factor: over.scroll_factor.or(self.scroll_factor),
335
        }
336
    }
337
338
    fn validate(&mut self, name: &str, w: &mut Vec<String>) {
339
        if let Some(s) = self.accel_speed
340
            && !(-1.0..=1.0).contains(&s)
341
        {
342
            w.push(format!(
343
                "{name}.accel_speed: {s} is outside -1 to 1, ignored"
344
            ));
345
            self.accel_speed = None;
346
        }
347
        if let Some(f) = self.scroll_factor
348
            && !(f.is_finite() && f >= 0.0)
349
        {
350
            w.push(format!(
351
                "{name}.scroll_factor: {f} must be zero or more, ignored"
352
            ));
353
            self.scroll_factor = None;
354
        }
355
        if self.accel_points.is_some() != self.accel_step.is_some() {
356
            w.push(format!(
357
                "{name}: accel_points needs accel_step and the other way round, both ignored"
358
            ));
359
            self.accel_points = None;
360
            self.accel_step = None;
361
        }
362
    }
363
}
364
365
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
366
#[serde(rename_all = "lowercase")]
367
pub enum AccelProfile {
368
    Flat,
369
    Adaptive,
370
    Custom,
371
}
372
373
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
374
#[serde(rename_all = "kebab-case")]
375
pub enum ScrollMethod {
376
    None,
377
    TwoFinger,
378
    Edge,
379
    OnButtonDown,
380
}
381
382
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
383
#[serde(rename_all = "kebab-case")]
384
pub enum ClickMethod {
385
    ButtonAreas,
386
    Clickfinger,
387
}
388
389
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
390
#[serde(rename_all = "lowercase")]
391
pub enum Modifier {
392
    Super,
393
    Alt,
394
    Ctrl,
395
    Shift,
396
}
397
398
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
399
pub struct Mods {
400
    pub logo: bool,
401
    pub alt: bool,
402
    pub ctrl: bool,
403
    pub shift: bool,
404
}
405
406
impl Mods {
407
    pub fn has(&self, m: Modifier) -> bool {
408
        match m {
409
            Modifier::Super => self.logo,
410
            Modifier::Alt => self.alt,
411
            Modifier::Ctrl => self.ctrl,
412
            Modifier::Shift => self.shift,
413
        }
414
    }
415
416
    fn set(&mut self, m: Modifier) {
417
        match m {
418
            Modifier::Super => self.logo = true,
419
            Modifier::Alt => self.alt = true,
420
            Modifier::Ctrl => self.ctrl = true,
421
            Modifier::Shift => self.shift = true,
422
        }
423
    }
424
}
425
426
#[derive(Debug, Clone, PartialEq)]
427
pub struct Bind {
428
    pub mods: Mods,
429
    /// An xkb keysym name, resolved by the compositor.
430
    pub key: String,
431
    pub command: Command,
432
}
433
434
#[derive(Debug, Clone)]
435
pub struct Rule {
436
    pub app_id: Option<Regex>,
437
    pub title: Option<Regex>,
438
    pub tag: Option<String>,
439
    pub output: Option<String>,
440
    pub floating: Option<bool>,
441
    pub sticky: Option<bool>,
442
    pub scratchpad: Option<String>,
443
}
444
445
/// Everything the matching rules say about a window, later rules winning.
446
#[derive(Debug, Clone, Default, PartialEq)]
447
pub struct Placement {
448
    pub tag: Option<String>,
449
    pub output: Option<String>,
450
    pub floating: Option<bool>,
451
    pub sticky: Option<bool>,
452
    pub scratchpad: Option<String>,
453
}
454
455
#[derive(Debug)]
456
pub enum Error {
457
    Io(PathBuf, std::io::Error),
458
    Toml(toml::de::Error),
459
}
460
461
impl fmt::Display for Error {
462
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463
        match self {
464
            Error::Io(p, e) => write!(f, "{}: {e}", p.display()),
465
            Error::Toml(e) => write!(f, "{e}"),
466
        }
467
    }
468
}
469
470
impl std::error::Error for Error {}
471
472
#[derive(Deserialize)]
473
#[serde(untagged)]
474
enum Words {
475
    Line(String),
476
    List(Vec<String>),
477
}
478
479
#[derive(Deserialize)]
480
#[serde(deny_unknown_fields)]
481
struct RawRule {
482
    app_id: Option<String>,
483
    title: Option<String>,
484
    tag: Option<String>,
485
    output: Option<String>,
486
    floating: Option<bool>,
487
    sticky: Option<bool>,
488
    scratchpad: Option<String>,
489
}
490
491
impl Config {
492
    /// `$MONOLITH_CONFIG`, else `$XDG_CONFIG_HOME/monolith/config.toml`.
493
    pub fn path() -> Option<PathBuf> {
494
        if let Some(p) = std::env::var_os("MONOLITH_CONFIG") {
495
            return Some(p.into());
496
        }
497
        let base = std::env::var_os("XDG_CONFIG_HOME")
498
            .map(PathBuf::from)
499
            .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
500
        Some(base.join("monolith/config.toml"))
501
    }
502
503
    /// Loads `path`, or the built-in default when it does not exist.
504
    pub fn load(path: Option<&Path>) -> Result<Loaded, Error> {
505
        match path {
506
            Some(p) => match std::fs::read_to_string(p) {
507
                Ok(text) => Self::parse(&text),
508
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::parse(DEFAULT),
509
                Err(e) => Err(Error::Io(p.to_owned(), e)),
510
            },
511
            None => Self::parse(DEFAULT),
512
        }
513
    }
514
515
    /// Only broken TOML fails. A bad option, bind or rule is left out with a
516
    /// warning, so one typo does not throw away the rest of the file.
517
    pub fn parse(text: &str) -> Result<Loaded, Error> {
518
        let mut table: toml::Table = toml::from_str(text).map_err(Error::Toml)?;
519
        let mut warnings = Vec::new();
520
        let w = &mut warnings;
521
        let general: General = section("general", table.remove("general"), w);
522
        let keyboard = section("keyboard", table.remove("keyboard"), w);
523
        let input = input(table.remove("input"), w);
524
        let idle = section("idle", table.remove("idle"), w);
525
        let animation = section("animation", table.remove("animation"), w);
526
        let decoration = section("decoration", table.remove("decoration"), w);
527
        let output: Outputs = section("output", table.remove("output"), w);
528
        for name in output.named.keys().filter(|n| !connector_like(n)) {
529
            w.push(format!(
530
                "output.{name}: not a connector name like DP-1 or HDMI-A-1, matches nothing"
531
            ));
532
        }
533
        let startup = items("startup", table.remove("startup"), w);
534
        let mut binds = Vec::new();
535
        if let Some(value) = table.remove("binds") {
536
            let entries: toml::Table = entry("binds", value, w).unwrap_or_default();
537
            for (combo, value) in entries {
538
                let name = format!("binds.{combo:?}");
539
                let bind = entry::<Words>(&name, value, w)
540
                    .map(|words| parse_bind(&combo, words, general.mod_key));
541
                match bind {
542
                    Some(Ok(b)) => binds.push(b),
543
                    Some(Err(e)) => w.push(format!("{name}: {e}, left out")),
544
                    None => {}
545
                }
546
            }
547
        }
548
        let regex = |p: Option<String>| p.map(|p| Regex::new(&p)).transpose();
549
        let mut rules = Vec::new();
550
        for (i, raw) in items::<RawRule>("rules", table.remove("rules"), w)
551
            .into_iter()
552
            .enumerate()
553
        {
554
            match (regex(raw.app_id), regex(raw.title)) {
555
                (Ok(app_id), Ok(title)) => rules.push(Rule {
556
                    app_id,
557
                    title,
558
                    tag: raw.tag,
559
                    output: raw.output,
560
                    floating: raw.floating,
561
                    sticky: raw.sticky,
562
                    scratchpad: raw.scratchpad,
563
                }),
564
                (Err(e), _) | (_, Err(e)) => {
565
                    let e = e
566
                        .to_string()
567
                        .split_whitespace()
568
                        .collect::<Vec<_>>()
569
                        .join(" ");
570
                    w.push(format!("rules[{i}]: {e}, left out"));
571
                }
572
            }
573
        }
574
        for key in table.keys() {
575
            w.push(format!("unknown section [{key}], ignored"));
576
        }
577
        let config = Self {
578
            general,
579
            animation,
580
            decoration,
581
            output,
582
            keyboard,
583
            input,
584
            idle,
585
            binds,
586
            rules,
587
            startup,
588
        };
589
        Ok(Loaded { config, warnings })
590
    }
591
592
    pub fn placement(&self, app_id: &str, title: &str) -> Placement {
593
        let mut p = Placement::default();
594
        let hit = |re: &Option<Regex>, s: &str| re.as_ref().is_none_or(|re| re.is_match(s));
595
        for r in &self.rules {
596
            if !hit(&r.app_id, app_id) || !hit(&r.title, title) {
597
                continue;
598
            }
599
            p.tag = r.tag.clone().or(p.tag);
600
            p.output = r.output.clone().or(p.output);
601
            p.floating = r.floating.or(p.floating);
602
            p.sticky = r.sticky.or(p.sticky);
603
            p.scratchpad = r.scratchpad.clone().or(p.scratchpad);
604
        }
605
        p
606
    }
607
}
608
609
fn parse_bind(combo: &str, words: Words, mod_key: Modifier) -> Result<Bind, String> {
610
    let parts: Vec<&str> = combo.split('+').map(str::trim).collect();
611
    let Some((&key, mods)) = parts.split_last().filter(|(k, _)| !k.is_empty()) else {
612
        return Err("no key".into());
613
    };
614
    let mut set = Mods::default();
615
    for m in mods {
616
        let m = match m.to_ascii_lowercase().as_str() {
617
            "mod" => mod_key,
618
            "super" | "logo" | "win" => Modifier::Super,
619
            "alt" => Modifier::Alt,
620
            "ctrl" | "control" => Modifier::Ctrl,
621
            "shift" => Modifier::Shift,
622
            other => return Err(format!("unknown modifier {other}")),
623
        };
624
        set.set(m);
625
    }
626
    let words: Vec<String> = match words {
627
        Words::Line(s) => s.split_whitespace().map(str::to_owned).collect(),
628
        Words::List(v) => v,
629
    };
630
    let command = Command::parse(&words).map_err(|e| e.0)?;
631
    Ok(Bind {
632
        mods: set,
633
        key: key.to_owned(),
634
        command,
635
    })
636
}
637
638
/// A config and what had to be left out of it.
639
pub struct Loaded {
640
    pub config: Config,
641
    pub warnings: Vec<String>,
642
}
643
644
fn entry<T: DeserializeOwned>(name: &str, value: toml::Value, w: &mut Vec<String>) -> Option<T> {
645
    value
646
        .try_into()
647
        .map_err(|e| w.push(format!("{name}: {}, left out", one_line(&e))))
648
        .ok()
649
}
650
651
fn input(value: Option<toml::Value>, w: &mut Vec<String>) -> Input {
652
    let mut table = match value {
653
        None => toml::Table::new(),
654
        Some(toml::Value::Table(t)) => t,
655
        Some(_) => {
656
            w.push("[input] is not a table, using the defaults".into());
657
            toml::Table::new()
658
        }
659
    };
660
    let mut input = Input {
661
        pointer: section("input.pointer", table.remove("pointer"), w),
662
        touchpad: section("input.touchpad", table.remove("touchpad"), w),
663
        device: Vec::new(),
664
    };
665
    for (i, mut entry) in items::<toml::Table>("input.device", table.remove("device"), w)
666
        .into_iter()
667
        .enumerate()
668
    {
669
        let name = format!("input.device[{i}]");
670
        let Some(toml::Value::String(device)) = entry.remove("name") else {
671
            w.push(format!("{name}: no name, left out"));
672
            continue;
673
        };
674
        input.device.push(NamedDevice {
675
            name: device,
676
            settings: section(&name, Some(toml::Value::Table(entry)), w),
677
        });
678
    }
679
    for key in table.keys() {
680
        w.push(format!("input.{key}: unknown option, ignored"));
681
    }
682
    input.pointer.validate("input.pointer", w);
683
    input.touchpad.validate("input.touchpad", w);
684
    for (i, d) in input.device.iter_mut().enumerate() {
685
        d.settings.validate(&format!("input.device[{i}]"), w);
686
    }
687
    input
688
}
689
690
fn connector_like(name: &str) -> bool {
691
    name.rsplit_once('-').is_some_and(|(kind, n)| {
692
        !kind.is_empty() && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())
693
    })
694
}
695
696
/// A section with every option that does not fit left at its default.
697
fn section<T: DeserializeOwned + Default>(
698
    name: &str,
699
    value: Option<toml::Value>,
700
    w: &mut Vec<String>,
701
) -> T {
702
    let Some(value) = value else {
703
        return T::default();
704
    };
705
    if let Ok(t) = value.clone().try_into() {
706
        return t;
707
    }
708
    let toml::Value::Table(options) = value else {
709
        w.push(format!("[{name}] is not a table, using the defaults"));
710
        return T::default();
711
    };
712
    let mut kept = toml::Table::new();
713
    for (key, v) in options {
714
        let alone = toml::Table::from_iter([(key.clone(), v.clone())]);
715
        match toml::Value::Table(alone).try_into::<T>() {
716
            Ok(_) => {
717
                kept.insert(key, v);
718
            }
719
            Err(e) => {
720
                let e = one_line(&e);
721
                let fallback = if e.starts_with("unknown field") {
722
                    "ignored"
723
                } else {
724
                    "using the default"
725
                };
726
                w.push(format!("{name}.{key}: {e}, {fallback}"));
727
            }
728
        }
729
    }
730
    toml::Value::Table(kept).try_into().unwrap_or_default()
731
}
732
733
/// An array with the elements that do not fit left out.
734
fn items<T: DeserializeOwned>(
735
    name: &str,
736
    value: Option<toml::Value>,
737
    w: &mut Vec<String>,
738
) -> Vec<T> {
739
    match value {
740
        None => Vec::new(),
741
        Some(toml::Value::Array(a)) => a
742
            .into_iter()
743
            .enumerate()
744
            .filter_map(|(i, v)| entry(&format!("{name}[{i}]"), v, w))
745
            .collect(),
746
        Some(_) => {
747
            w.push(format!("{name} is not an array, left out"));
748
            Vec::new()
749
        }
750
    }
751
}
752
753
fn one_line(e: &toml::de::Error) -> String {
754
    e.message().trim().replace('\n', " ")
755
}
756
#[cfg(test)]
757
mod tests {
758
    use super::*;
759
760
    /// default.toml documents the defaults for users, and the Default impls
761
    /// apply them to sections a user config leaves out. They must agree.
762
    #[test]
763
    fn default_toml_states_the_defaults() {
764
        let loaded = Config::parse(DEFAULT).unwrap();
765
        assert_eq!(loaded.warnings, Vec::<String>::new());
766
        let c = loaded.config;
767
        assert_eq!(c.general, General::default());
768
        assert_eq!(c.output, Outputs::default());
769
        assert_eq!(c.decoration, Decoration::default());
770
        assert_eq!(c.animation, Animation::default());
771
        assert_eq!(c.keyboard, Keyboard::default());
772
        assert_eq!(c.input, Input::default());
773
        assert_eq!(c.idle, Idle::default());
774
        assert!(c.binds.len() > 40);
775
    }
776
777
    #[test]
778
    fn a_bad_option_costs_only_itself() {
779
        let loaded = Config::parse(
780
            r##"
781
            [general]
782
            gaps = { inner = 3, outer = 3 }
783
            focus_follows_mouse = "yes"
784
            typo = 1
785
            [decoration]
786
            radius = [1, 2, 3, 4]
787
            focused = "#zzzzzz"
788
            [binds]
789
            "Mod+Return" = "spawn beer"
790
            "Hyper+x" = "close"
791
            [output.DP-1]
792
            transform = "90"
793
            position = [0, 0]
794
            [output.DP-2]
795
            transform = "sideways"
796
            [output.DP1]
797
            [[rules]]
798
            app_id = "("
799
            [[rules]]
800
            app_id = "^mpv$"
801
            floating = true
802
            "##,
803
        )
804
        .unwrap();
805
        let c = loaded.config;
806
        assert_eq!(c.general.gaps.inner, 3.0);
807
        assert!(!c.general.focus_follows_mouse);
808
        assert_eq!(c.decoration.radius, Corners([1, 2, 3, 4]));
809
        assert_eq!(c.decoration.focused, Decoration::default().focused);
810
        assert_eq!((c.binds.len(), c.rules.len()), (1, 1));
811
        assert_eq!(c.output.named["DP-1"].transform, OutputTransform::Rotate90);
812
        assert!(!c.output.named.contains_key("DP-2"));
813
        assert_eq!(loaded.warnings.len(), 7, "{:#?}", loaded.warnings);
814
    }
815
816
    #[test]
817
    fn a_bad_device_option_costs_only_itself() {
818
        let loaded = Config::parse(
819
            r#"
820
            [input.pointer]
821
            accel_speed = 2.0
822
            scroll_method = "two-finger"
823
            [[input.device]]
824
            name = "Mouse"
825
            accel_profile = "flat"
826
            [[input.device]]
827
            name = "Typo"
828
            tapp = true
829
            [[input.device]]
830
            name = "Half"
831
            accel_points = [0.0, 1.0]
832
            "#,
833
        )
834
        .unwrap();
835
        let input = loaded.config.input;
836
        assert_eq!(input.pointer.accel_speed, None);
837
        assert_eq!(input.pointer.scroll_method, Some(ScrollMethod::TwoFinger));
838
        let names: Vec<_> = input.device.iter().map(|d| d.name.as_str()).collect();
839
        assert_eq!(names, ["Mouse", "Typo", "Half"]);
840
        assert_eq!(
841
            input.device[0].settings.accel_profile,
842
            Some(AccelProfile::Flat)
843
        );
844
        assert_eq!(input.device[1].settings, DeviceSettings::default());
845
        assert_eq!(input.device[2].settings.accel_points, None);
846
        assert_eq!(loaded.warnings.len(), 3, "{:#?}", loaded.warnings);
847
    }
848
}