atagen

monolith

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