atagen

monolith

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