atagen

monolith

public
Star0
Watch0
2e852b236c45cdc6cef7688d2d177ff3a268df77
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git

Blame: crates/config/src/lib.rs

fe835aed6c8datagen2026-10-16 10:05 UTC1
//! The TOML config. Parsing and validation only, the compositor applies it.
57f44a63d99fatagen2026-10-16 05:34 UTC2
fe835aed6c8datagen2026-10-16 10:05 UTC3
use monolith_ipc::Command;
fe835aed6c8datagen2026-10-16 10:05 UTC4
use regex::Regex;
fe835aed6c8datagen2026-10-16 10:05 UTC5
use serde::Deserialize;
8fb62aebf501atagen2026-10-17 10:25 UTC6
use serde::de::DeserializeOwned;
fe835aed6c8datagen2026-10-16 10:05 UTC7
use std::fmt;
fe835aed6c8datagen2026-10-16 10:05 UTC8
use std::path::{Path, PathBuf};
fe835aed6c8datagen2026-10-16 10:05 UTC9
fe835aed6c8datagen2026-10-16 10:05 UTC10
pub const DEFAULT: &str = include_str!("../default.toml");
fe835aed6c8datagen2026-10-16 10:05 UTC11
fe835aed6c8datagen2026-10-16 10:05 UTC12
#[derive(Debug, Clone)]
fe835aed6c8datagen2026-10-16 10:05 UTC13
pub struct Config {
fe835aed6c8datagen2026-10-16 10:05 UTC14
    pub general: General,
8e6f96f0a32datagen2026-10-16 22:17 UTC15
    pub output: Outputs,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC16
    pub decoration: Decoration,
2af1d8c55876atagen2026-10-16 11:05 UTC17
    pub animation: Animation,
fe835aed6c8datagen2026-10-16 10:05 UTC18
    pub keyboard: Keyboard,
9944e8618a62atagen2026-10-18 10:00 UTC19
    pub input: Input,
47b86d08d05aatagen2026-10-19 07:37 UTC20
    pub idle: Idle,
5b8422a225daatagen2026-10-19 07:45 UTC21
    pub lid: Lid,
1df9b00ad0fdatagen2026-10-19 07:34 UTC22
    pub xwayland: Xwayland,
fe835aed6c8datagen2026-10-16 10:05 UTC23
    pub binds: Vec<Bind>,
fe835aed6c8datagen2026-10-16 10:05 UTC24
    pub rules: Vec<Rule>,
fe835aed6c8datagen2026-10-16 10:05 UTC25
    pub startup: Vec<Vec<String>>,
fe835aed6c8datagen2026-10-16 10:05 UTC26
}
fe835aed6c8datagen2026-10-16 10:05 UTC27
fe835aed6c8datagen2026-10-16 10:05 UTC28
#[derive(Deserialize, Debug, Clone, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC29
#[serde(default, deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC30
pub struct General {
fe835aed6c8datagen2026-10-16 10:05 UTC31
    /// What `Mod` means in binds.
fe835aed6c8datagen2026-10-16 10:05 UTC32
    #[serde(rename = "mod")]
fe835aed6c8datagen2026-10-16 10:05 UTC33
    pub mod_key: Modifier,
fe835aed6c8datagen2026-10-16 10:05 UTC34
    pub gaps: Gaps,
86ef802692f5atagen2026-10-19 07:43 UTC35
    /// A new window splits the one it opens beside along that window's
86ef802692f5atagen2026-10-19 07:43 UTC36
    /// longer side, instead of joining its container.
86ef802692f5atagen2026-10-19 07:43 UTC37
    pub autotile: bool,
4444c779eb48atagen2026-10-16 12:07 UTC38
    /// What a window move does when it meets a neighbour.
4444c779eb48atagen2026-10-16 12:07 UTC39
    #[serde(rename = "move")]
4444c779eb48atagen2026-10-16 12:07 UTC40
    pub move_mode: MoveMode,
24fca1aad608atagen2026-10-17 09:56 UTC41
    pub focus_follows_mouse: bool,
24fca1aad608atagen2026-10-17 09:56 UTC42
    pub mouse_follows_focus: bool,
45a383fca811atagen2026-10-17 11:31 UTC43
    /// The first window to land on or move to an empty tag nobody is
45a383fca811atagen2026-10-17 11:31 UTC44
    /// viewing brings that tag into view on its output.
47259c39793fatagen2026-10-17 10:13 UTC45
    pub view_tag_on_first_window: bool,
4444c779eb48atagen2026-10-16 12:07 UTC46
}
4444c779eb48atagen2026-10-16 12:07 UTC47
4444c779eb48atagen2026-10-16 12:07 UTC48
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
4444c779eb48atagen2026-10-16 12:07 UTC49
#[serde(rename_all = "lowercase")]
4444c779eb48atagen2026-10-16 12:07 UTC50
pub enum MoveMode {
4444c779eb48atagen2026-10-16 12:07 UTC51
    /// Trade places with it.
4444c779eb48atagen2026-10-16 12:07 UTC52
    #[default]
4444c779eb48atagen2026-10-16 12:07 UTC53
    Swap,
4444c779eb48atagen2026-10-16 12:07 UTC54
    /// Join its lane, stacking with it if it is alone.
4444c779eb48atagen2026-10-16 12:07 UTC55
    Merge,
fe835aed6c8datagen2026-10-16 10:05 UTC56
}
fe835aed6c8datagen2026-10-16 10:05 UTC57
fe835aed6c8datagen2026-10-16 10:05 UTC58
impl Default for General {
fe835aed6c8datagen2026-10-16 10:05 UTC59
    fn default() -> Self {
fe835aed6c8datagen2026-10-16 10:05 UTC60
        Self {
fe835aed6c8datagen2026-10-16 10:05 UTC61
            mod_key: Modifier::Super,
fe835aed6c8datagen2026-10-16 10:05 UTC62
            gaps: Gaps::default(),
86ef802692f5atagen2026-10-19 07:43 UTC63
            autotile: true,
4444c779eb48atagen2026-10-16 12:07 UTC64
            move_mode: MoveMode::Swap,
24fca1aad608atagen2026-10-17 09:56 UTC65
            focus_follows_mouse: false,
24fca1aad608atagen2026-10-17 09:56 UTC66
            mouse_follows_focus: false,
47259c39793fatagen2026-10-17 10:13 UTC67
            view_tag_on_first_window: true,
fe835aed6c8datagen2026-10-16 10:05 UTC68
        }
fe835aed6c8datagen2026-10-16 10:05 UTC69
    }
fe835aed6c8datagen2026-10-16 10:05 UTC70
}
fe835aed6c8datagen2026-10-16 10:05 UTC71
1df9b00ad0fdatagen2026-10-19 07:34 UTC72
#[derive(Deserialize, Debug, Clone, PartialEq)]
1df9b00ad0fdatagen2026-10-19 07:34 UTC73
#[serde(default, deny_unknown_fields)]
1df9b00ad0fdatagen2026-10-19 07:34 UTC74
pub struct Xwayland {
1df9b00ad0fdatagen2026-10-19 07:34 UTC75
    /// Which X11 clients may read a Wayland-owned selection.
1df9b00ad0fdatagen2026-10-19 07:34 UTC76
    pub selection_access: SelectionAccess,
1df9b00ad0fdatagen2026-10-19 07:34 UTC77
}
1df9b00ad0fdatagen2026-10-19 07:34 UTC78
1df9b00ad0fdatagen2026-10-19 07:34 UTC79
impl Default for Xwayland {
1df9b00ad0fdatagen2026-10-19 07:34 UTC80
    fn default() -> Self {
1df9b00ad0fdatagen2026-10-19 07:34 UTC81
        Self {
1df9b00ad0fdatagen2026-10-19 07:34 UTC82
            selection_access: SelectionAccess::Focused,
1df9b00ad0fdatagen2026-10-19 07:34 UTC83
        }
1df9b00ad0fdatagen2026-10-19 07:34 UTC84
    }
1df9b00ad0fdatagen2026-10-19 07:34 UTC85
}
1df9b00ad0fdatagen2026-10-19 07:34 UTC86
1df9b00ad0fdatagen2026-10-19 07:34 UTC87
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1df9b00ad0fdatagen2026-10-19 07:34 UTC88
#[serde(rename_all = "lowercase")]
1df9b00ad0fdatagen2026-10-19 07:34 UTC89
pub enum SelectionAccess {
1df9b00ad0fdatagen2026-10-19 07:34 UTC90
    /// Only the X11 window holding keyboard focus.
1df9b00ad0fdatagen2026-10-19 07:34 UTC91
    Focused,
1df9b00ad0fdatagen2026-10-19 07:34 UTC92
    /// Every X11 client, which clipboard managers and headless tools need.
1df9b00ad0fdatagen2026-10-19 07:34 UTC93
    Any,
1df9b00ad0fdatagen2026-10-19 07:34 UTC94
}
1df9b00ad0fdatagen2026-10-19 07:34 UTC95
fe835aed6c8datagen2026-10-16 10:05 UTC96
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC97
#[serde(default, deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC98
pub struct Gaps {
fe835aed6c8datagen2026-10-16 10:05 UTC99
    pub inner: f64,
fe835aed6c8datagen2026-10-16 10:05 UTC100
    pub outer: f64,
fe835aed6c8datagen2026-10-16 10:05 UTC101
}
fe835aed6c8datagen2026-10-16 10:05 UTC102
fe835aed6c8datagen2026-10-16 10:05 UTC103
impl Default for Gaps {
fe835aed6c8datagen2026-10-16 10:05 UTC104
    fn default() -> Self {
fe835aed6c8datagen2026-10-16 10:05 UTC105
        Self {
fe835aed6c8datagen2026-10-16 10:05 UTC106
            inner: 8.0,
fe835aed6c8datagen2026-10-16 10:05 UTC107
            outer: 8.0,
fe835aed6c8datagen2026-10-16 10:05 UTC108
        }
fe835aed6c8datagen2026-10-16 10:05 UTC109
    }
fe835aed6c8datagen2026-10-16 10:05 UTC110
}
fe835aed6c8datagen2026-10-16 10:05 UTC111
8e6f96f0a32datagen2026-10-16 22:17 UTC112
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
9b87063809e1atagen2026-10-17 13:28 UTC113
#[serde(default)]
8e6f96f0a32datagen2026-10-16 22:17 UTC114
pub struct Outputs {
8e6f96f0a32datagen2026-10-16 22:17 UTC115
    pub vrr: Vrr,
fc167134b65datagen2026-10-19 08:09 UTC116
    /// Settings for one connector, by name, like `DP-1`, or `nested` for the
fc167134b65datagen2026-10-19 08:09 UTC117
    /// nested backend's window.
9b87063809e1atagen2026-10-17 13:28 UTC118
    #[serde(flatten)]
9b87063809e1atagen2026-10-17 13:28 UTC119
    pub named: std::collections::BTreeMap<String, OutputConfig>,
9b87063809e1atagen2026-10-17 13:28 UTC120
}
9b87063809e1atagen2026-10-17 13:28 UTC121
fc167134b65datagen2026-10-19 08:09 UTC122
/// Transform, position and scale follow a reload. A new mode waits for a
fc167134b65datagen2026-10-19 08:09 UTC123
/// restart or a replug, since it means a modeset.
9b87063809e1atagen2026-10-17 13:28 UTC124
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
9b87063809e1atagen2026-10-17 13:28 UTC125
#[serde(default, deny_unknown_fields)]
9b87063809e1atagen2026-10-17 13:28 UTC126
pub struct OutputConfig {
9b87063809e1atagen2026-10-17 13:28 UTC127
    pub transform: OutputTransform,
9b87063809e1atagen2026-10-17 13:28 UTC128
    /// Top left corner in the global layout. Outputs without one are placed
9b87063809e1atagen2026-10-17 13:28 UTC129
    /// to the right of the others.
9b87063809e1atagen2026-10-17 13:28 UTC130
    pub position: Option<[i32; 2]>,
9b87063809e1atagen2026-10-17 13:28 UTC131
    /// `WIDTHxHEIGHT` or `WIDTHxHEIGHT@HZ`. Without a rate the highest one
9b87063809e1atagen2026-10-17 13:28 UTC132
    /// for that size wins.
9b87063809e1atagen2026-10-17 13:28 UTC133
    pub mode: Option<String>,
fc167134b65datagen2026-10-19 08:09 UTC134
    /// How many physical pixels make a logical one, `0.5` to `4.0` in steps of
fc167134b65datagen2026-10-19 08:09 UTC135
    /// 1/120. Positions are in logical pixels.
fc167134b65datagen2026-10-19 08:09 UTC136
    pub scale: Option<f64>,
9b87063809e1atagen2026-10-17 13:28 UTC137
}
9b87063809e1atagen2026-10-17 13:28 UTC138
fc167134b65datagen2026-10-19 08:09 UTC139
impl OutputConfig {
fc167134b65datagen2026-10-19 08:09 UTC140
    pub fn scale(&self) -> f64 {
fc167134b65datagen2026-10-19 08:09 UTC141
        self.scale.unwrap_or(1.0)
fc167134b65datagen2026-10-19 08:09 UTC142
    }
fc167134b65datagen2026-10-19 08:09 UTC143
fc167134b65datagen2026-10-19 08:09 UTC144
    fn validate(&mut self, name: &str, w: &mut Vec<String>) {
fc167134b65datagen2026-10-19 08:09 UTC145
        let Some(s) = self.scale else {
fc167134b65datagen2026-10-19 08:09 UTC146
            return;
fc167134b65datagen2026-10-19 08:09 UTC147
        };
fc167134b65datagen2026-10-19 08:09 UTC148
        if !(MIN_SCALE..=MAX_SCALE).contains(&s) {
fc167134b65datagen2026-10-19 08:09 UTC149
            w.push(format!(
fc167134b65datagen2026-10-19 08:09 UTC150
                "{name}.scale: {s} is outside {MIN_SCALE} to {MAX_SCALE}, ignored"
fc167134b65datagen2026-10-19 08:09 UTC151
            ));
fc167134b65datagen2026-10-19 08:09 UTC152
            self.scale = None;
fc167134b65datagen2026-10-19 08:09 UTC153
        } else {
fc167134b65datagen2026-10-19 08:09 UTC154
            self.scale = Some((s * 120.0).round() / 120.0);
fc167134b65datagen2026-10-19 08:09 UTC155
        }
fc167134b65datagen2026-10-19 08:09 UTC156
    }
fc167134b65datagen2026-10-19 08:09 UTC157
}
fc167134b65datagen2026-10-19 08:09 UTC158
fc167134b65datagen2026-10-19 08:09 UTC159
const MIN_SCALE: f64 = 0.5;
fc167134b65datagen2026-10-19 08:09 UTC160
const MAX_SCALE: f64 = 4.0;
fc167134b65datagen2026-10-19 08:09 UTC161
9b87063809e1atagen2026-10-17 13:28 UTC162
/// Counterclockwise rotation, optionally after flipping horizontally.
9b87063809e1atagen2026-10-17 13:28 UTC163
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
9b87063809e1atagen2026-10-17 13:28 UTC164
#[serde(rename_all = "kebab-case")]
9b87063809e1atagen2026-10-17 13:28 UTC165
pub enum OutputTransform {
9b87063809e1atagen2026-10-17 13:28 UTC166
    #[default]
9b87063809e1atagen2026-10-17 13:28 UTC167
    Normal,
9b87063809e1atagen2026-10-17 13:28 UTC168
    #[serde(rename = "90")]
9b87063809e1atagen2026-10-17 13:28 UTC169
    Rotate90,
9b87063809e1atagen2026-10-17 13:28 UTC170
    #[serde(rename = "180")]
9b87063809e1atagen2026-10-17 13:28 UTC171
    Rotate180,
9b87063809e1atagen2026-10-17 13:28 UTC172
    #[serde(rename = "270")]
9b87063809e1atagen2026-10-17 13:28 UTC173
    Rotate270,
9b87063809e1atagen2026-10-17 13:28 UTC174
    Flipped,
9b87063809e1atagen2026-10-17 13:28 UTC175
    #[serde(rename = "flipped-90")]
9b87063809e1atagen2026-10-17 13:28 UTC176
    Flipped90,
9b87063809e1atagen2026-10-17 13:28 UTC177
    #[serde(rename = "flipped-180")]
9b87063809e1atagen2026-10-17 13:28 UTC178
    Flipped180,
9b87063809e1atagen2026-10-17 13:28 UTC179
    #[serde(rename = "flipped-270")]
9b87063809e1atagen2026-10-17 13:28 UTC180
    Flipped270,
8e6f96f0a32datagen2026-10-16 22:17 UTC181
}
8e6f96f0a32datagen2026-10-16 22:17 UTC182
8e6f96f0a32datagen2026-10-16 22:17 UTC183
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
8e6f96f0a32datagen2026-10-16 22:17 UTC184
#[serde(rename_all = "lowercase")]
8e6f96f0a32datagen2026-10-16 22:17 UTC185
pub enum Vrr {
8e6f96f0a32datagen2026-10-16 22:17 UTC186
    Off,
8e6f96f0a32datagen2026-10-16 22:17 UTC187
    On,
8e6f96f0a32datagen2026-10-16 22:17 UTC188
    /// Only while a fullscreen window is shown, where a game sets the pace.
8e6f96f0a32datagen2026-10-16 22:17 UTC189
    #[default]
8e6f96f0a32datagen2026-10-16 22:17 UTC190
    Fullscreen,
8e6f96f0a32datagen2026-10-16 22:17 UTC191
}
8e6f96f0a32datagen2026-10-16 22:17 UTC192
fe835aed6c8datagen2026-10-16 10:05 UTC193
#[derive(Deserialize, Debug, Clone, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC194
#[serde(default, deny_unknown_fields)]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC195
pub struct Decoration {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC196
    /// Border width in logical pixels, drawn in the gap around each window.
7ce6ae4e5a85atagen2026-10-16 12:16 UTC197
    pub border: u32,
47259c39793fatagen2026-10-17 10:13 UTC198
    pub radius: Corners,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC199
    pub focused: Color,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC200
    pub unfocused: Color,
1d62be4ff79catagen2026-10-19 07:37 UTC201
    pub urgent: Color,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC202
}
7ce6ae4e5a85atagen2026-10-16 12:16 UTC203
47259c39793fatagen2026-10-17 10:13 UTC204
/// Corner radii in logical pixels, top left, top right, bottom right and
47259c39793fatagen2026-10-17 10:13 UTC205
/// bottom left. One number rounds every corner alike.
47259c39793fatagen2026-10-17 10:13 UTC206
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
47259c39793fatagen2026-10-17 10:13 UTC207
#[serde(from = "CornersToml")]
47259c39793fatagen2026-10-17 10:13 UTC208
pub struct Corners(pub [u32; 4]);
47259c39793fatagen2026-10-17 10:13 UTC209
47259c39793fatagen2026-10-17 10:13 UTC210
#[derive(Deserialize)]
8fb62aebf501atagen2026-10-17 10:25 UTC211
#[serde(
8fb62aebf501atagen2026-10-17 10:25 UTC212
    untagged,
8fb62aebf501atagen2026-10-17 10:25 UTC213
    expecting = "expected a number or [top left, top right, bottom right, bottom left]"
8fb62aebf501atagen2026-10-17 10:25 UTC214
)]
47259c39793fatagen2026-10-17 10:13 UTC215
enum CornersToml {
47259c39793fatagen2026-10-17 10:13 UTC216
    All(u32),
47259c39793fatagen2026-10-17 10:13 UTC217
    Each([u32; 4]),
47259c39793fatagen2026-10-17 10:13 UTC218
}
47259c39793fatagen2026-10-17 10:13 UTC219
47259c39793fatagen2026-10-17 10:13 UTC220
impl From<CornersToml> for Corners {
47259c39793fatagen2026-10-17 10:13 UTC221
    fn from(c: CornersToml) -> Self {
47259c39793fatagen2026-10-17 10:13 UTC222
        match c {
47259c39793fatagen2026-10-17 10:13 UTC223
            CornersToml::All(r) => Self([r; 4]),
47259c39793fatagen2026-10-17 10:13 UTC224
            CornersToml::Each(r) => Self(r),
47259c39793fatagen2026-10-17 10:13 UTC225
        }
47259c39793fatagen2026-10-17 10:13 UTC226
    }
47259c39793fatagen2026-10-17 10:13 UTC227
}
47259c39793fatagen2026-10-17 10:13 UTC228
7ce6ae4e5a85atagen2026-10-16 12:16 UTC229
impl Default for Decoration {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC230
    fn default() -> Self {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC231
        Self {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC232
            border: 2,
47259c39793fatagen2026-10-17 10:13 UTC233
            radius: Corners([0; 4]),
b22c0511d5afatagen2026-10-17 03:48 UTC234
            focused: Color::rgb(0x88, 0xbf, 0xd1),
b22c0511d5afatagen2026-10-17 03:48 UTC235
            unfocused: Color::rgb(0x3b, 0x42, 0x52),
1d62be4ff79catagen2026-10-19 07:37 UTC236
            urgent: Color::rgb(0xeb, 0xcb, 0x8b),
7ce6ae4e5a85atagen2026-10-16 12:16 UTC237
        }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC238
    }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC239
}
7ce6ae4e5a85atagen2026-10-16 12:16 UTC240
7ce6ae4e5a85atagen2026-10-16 12:16 UTC241
/// Straight RGBA from `#rrggbb` or `#rrggbbaa`.
7ce6ae4e5a85atagen2026-10-16 12:16 UTC242
#[derive(Debug, Clone, Copy, PartialEq)]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC243
pub struct Color(pub [f32; 4]);
7ce6ae4e5a85atagen2026-10-16 12:16 UTC244
b22c0511d5afatagen2026-10-17 03:48 UTC245
impl Color {
b22c0511d5afatagen2026-10-17 03:48 UTC246
    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
b22c0511d5afatagen2026-10-17 03:48 UTC247
        Self([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
b22c0511d5afatagen2026-10-17 03:48 UTC248
    }
b22c0511d5afatagen2026-10-17 03:48 UTC249
}
b22c0511d5afatagen2026-10-17 03:48 UTC250
7ce6ae4e5a85atagen2026-10-16 12:16 UTC251
impl<'de> Deserialize<'de> for Color {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC252
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC253
        let s = String::deserialize(d)?;
7ce6ae4e5a85atagen2026-10-16 12:16 UTC254
        let hex = s.strip_prefix('#').unwrap_or(&s);
7ce6ae4e5a85atagen2026-10-16 12:16 UTC255
        let bad =
7ce6ae4e5a85atagen2026-10-16 12:16 UTC256
            || serde::de::Error::custom(format!("bad colour {s:?}, use #rrggbb or #rrggbbaa"));
7ce6ae4e5a85atagen2026-10-16 12:16 UTC257
        if !matches!(hex.len(), 6 | 8) || !hex.is_ascii() {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC258
            return Err(bad());
7ce6ae4e5a85atagen2026-10-16 12:16 UTC259
        }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC260
        let mut c = [1.0f32; 4];
7ce6ae4e5a85atagen2026-10-16 12:16 UTC261
        for (i, v) in c.iter_mut().enumerate().take(hex.len() / 2) {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC262
            let byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| bad())?;
7ce6ae4e5a85atagen2026-10-16 12:16 UTC263
            *v = f32::from(byte) / 255.0;
7ce6ae4e5a85atagen2026-10-16 12:16 UTC264
        }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC265
        Ok(Self(c))
7ce6ae4e5a85atagen2026-10-16 12:16 UTC266
    }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC267
}
7ce6ae4e5a85atagen2026-10-16 12:16 UTC268
7ce6ae4e5a85atagen2026-10-16 12:16 UTC269
#[derive(Deserialize, Debug, Clone, PartialEq)]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC270
#[serde(default, deny_unknown_fields)]
2af1d8c55876atagen2026-10-16 11:05 UTC271
pub struct Animation {
2af1d8c55876atagen2026-10-16 11:05 UTC272
    pub enabled: bool,
2af1d8c55876atagen2026-10-16 11:05 UTC273
    pub style: AnimationStyle,
2af1d8c55876atagen2026-10-16 11:05 UTC274
    /// Every phase of a planned transition lasts this long.
2af1d8c55876atagen2026-10-16 11:05 UTC275
    pub phase_ms: u64,
2af1d8c55876atagen2026-10-16 11:05 UTC276
    /// A cubic bezier, as in CSS.
2af1d8c55876atagen2026-10-16 11:05 UTC277
    pub curve: [f64; 4],
2af1d8c55876atagen2026-10-16 11:05 UTC278
}
2af1d8c55876atagen2026-10-16 11:05 UTC279
2af1d8c55876atagen2026-10-16 11:05 UTC280
impl Default for Animation {
2af1d8c55876atagen2026-10-16 11:05 UTC281
    fn default() -> Self {
2af1d8c55876atagen2026-10-16 11:05 UTC282
        Self {
2af1d8c55876atagen2026-10-16 11:05 UTC283
            enabled: true,
2af1d8c55876atagen2026-10-16 11:05 UTC284
            style: AnimationStyle::Orthogonal,
2af1d8c55876atagen2026-10-16 11:05 UTC285
            phase_ms: 160,
2af1d8c55876atagen2026-10-16 11:05 UTC286
            curve: [0.45, 0.0, 0.2, 1.0],
2af1d8c55876atagen2026-10-16 11:05 UTC287
        }
2af1d8c55876atagen2026-10-16 11:05 UTC288
    }
2af1d8c55876atagen2026-10-16 11:05 UTC289
}
2af1d8c55876atagen2026-10-16 11:05 UTC290
2af1d8c55876atagen2026-10-16 11:05 UTC291
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
2af1d8c55876atagen2026-10-16 11:05 UTC292
#[serde(rename_all = "lowercase")]
2af1d8c55876atagen2026-10-16 11:05 UTC293
pub enum AnimationStyle {
2af1d8c55876atagen2026-10-16 11:05 UTC294
    Orthogonal,
2af1d8c55876atagen2026-10-16 11:05 UTC295
    Direct,
2af1d8c55876atagen2026-10-16 11:05 UTC296
}
2af1d8c55876atagen2026-10-16 11:05 UTC297
47b86d08d05aatagen2026-10-19 07:37 UTC298
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
47b86d08d05aatagen2026-10-19 07:37 UTC299
#[serde(default, deny_unknown_fields)]
47b86d08d05aatagen2026-10-19 07:37 UTC300
pub struct Idle {
47b86d08d05aatagen2026-10-19 07:37 UTC301
    /// Seconds without input before every output is switched off, 0 for
47b86d08d05aatagen2026-10-19 07:37 UTC302
    /// never.
47b86d08d05aatagen2026-10-19 07:37 UTC303
    pub blank_after: u64,
47b86d08d05aatagen2026-10-19 07:37 UTC304
}
47b86d08d05aatagen2026-10-19 07:37 UTC305
5b8422a225daatagen2026-10-19 07:45 UTC306
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
5b8422a225daatagen2026-10-19 07:45 UTC307
#[serde(default, deny_unknown_fields)]
5b8422a225daatagen2026-10-19 07:45 UTC308
pub struct Lid {
5b8422a225daatagen2026-10-19 07:45 UTC309
    /// What closing the lid does while no other output is connected.
5b8422a225daatagen2026-10-19 07:45 UTC310
    pub close: LidAction,
5b8422a225daatagen2026-10-19 07:45 UTC311
}
5b8422a225daatagen2026-10-19 07:45 UTC312
5b8422a225daatagen2026-10-19 07:45 UTC313
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
5b8422a225daatagen2026-10-19 07:45 UTC314
#[serde(rename_all = "lowercase")]
5b8422a225daatagen2026-10-19 07:45 UTC315
pub enum LidAction {
5b8422a225daatagen2026-10-19 07:45 UTC316
    #[default]
5b8422a225daatagen2026-10-19 07:45 UTC317
    Suspend,
5b8422a225daatagen2026-10-19 07:45 UTC318
    Poweroff,
5b8422a225daatagen2026-10-19 07:45 UTC319
    /// Leave it to logind or whatever else watches the switch.
5b8422a225daatagen2026-10-19 07:45 UTC320
    Ignore,
5b8422a225daatagen2026-10-19 07:45 UTC321
}
5b8422a225daatagen2026-10-19 07:45 UTC322
2af1d8c55876atagen2026-10-16 11:05 UTC323
#[derive(Deserialize, Debug, Clone, PartialEq)]
2af1d8c55876atagen2026-10-16 11:05 UTC324
#[serde(default, deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC325
pub struct Keyboard {
fe835aed6c8datagen2026-10-16 10:05 UTC326
    pub rules: String,
fe835aed6c8datagen2026-10-16 10:05 UTC327
    pub model: String,
fe835aed6c8datagen2026-10-16 10:05 UTC328
    pub layout: String,
fe835aed6c8datagen2026-10-16 10:05 UTC329
    pub variant: String,
fe835aed6c8datagen2026-10-16 10:05 UTC330
    pub options: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC331
    pub repeat_delay: i32,
fe835aed6c8datagen2026-10-16 10:05 UTC332
    pub repeat_rate: i32,
fe835aed6c8datagen2026-10-16 10:05 UTC333
}
fe835aed6c8datagen2026-10-16 10:05 UTC334
fe835aed6c8datagen2026-10-16 10:05 UTC335
impl Default for Keyboard {
fe835aed6c8datagen2026-10-16 10:05 UTC336
    fn default() -> Self {
fe835aed6c8datagen2026-10-16 10:05 UTC337
        Self {
fe835aed6c8datagen2026-10-16 10:05 UTC338
            rules: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC339
            model: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC340
            layout: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC341
            variant: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC342
            options: None,
fe835aed6c8datagen2026-10-16 10:05 UTC343
            repeat_delay: 200,
fe835aed6c8datagen2026-10-16 10:05 UTC344
            repeat_rate: 25,
fe835aed6c8datagen2026-10-16 10:05 UTC345
        }
fe835aed6c8datagen2026-10-16 10:05 UTC346
    }
fe835aed6c8datagen2026-10-16 10:05 UTC347
}
fe835aed6c8datagen2026-10-16 10:05 UTC348
9944e8618a62atagen2026-10-18 10:00 UTC349
/// libinput settings for pointing devices. Unset options keep libinput's
9944e8618a62atagen2026-10-18 10:00 UTC350
/// default for the device.
9944e8618a62atagen2026-10-18 10:00 UTC351
#[derive(Debug, Clone, PartialEq, Default)]
9944e8618a62atagen2026-10-18 10:00 UTC352
pub struct Input {
9944e8618a62atagen2026-10-18 10:00 UTC353
    pub pointer: DeviceSettings,
9944e8618a62atagen2026-10-18 10:00 UTC354
    /// Devices that can tap, which libinput only offers on touchpads.
9944e8618a62atagen2026-10-18 10:00 UTC355
    pub touchpad: DeviceSettings,
9944e8618a62atagen2026-10-18 10:00 UTC356
    /// Overrides for devices by name, later entries winning.
9944e8618a62atagen2026-10-18 10:00 UTC357
    pub device: Vec<NamedDevice>,
9944e8618a62atagen2026-10-18 10:00 UTC358
}
9944e8618a62atagen2026-10-18 10:00 UTC359
9944e8618a62atagen2026-10-18 10:00 UTC360
#[derive(Debug, Clone, PartialEq)]
9944e8618a62atagen2026-10-18 10:00 UTC361
pub struct NamedDevice {
9944e8618a62atagen2026-10-18 10:00 UTC362
    pub name: String,
9944e8618a62atagen2026-10-18 10:00 UTC363
    pub settings: DeviceSettings,
9944e8618a62atagen2026-10-18 10:00 UTC364
}
9944e8618a62atagen2026-10-18 10:00 UTC365
9944e8618a62atagen2026-10-18 10:00 UTC366
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
9944e8618a62atagen2026-10-18 10:00 UTC367
#[serde(default, deny_unknown_fields)]
9944e8618a62atagen2026-10-18 10:00 UTC368
pub struct DeviceSettings {
9944e8618a62atagen2026-10-18 10:00 UTC369
    pub accel_profile: Option<AccelProfile>,
9944e8618a62atagen2026-10-18 10:00 UTC370
    pub accel_speed: Option<f64>,
9944e8618a62atagen2026-10-18 10:00 UTC371
    /// The custom profile's curve, pointer speed sampled every `accel_step`
9944e8618a62atagen2026-10-18 10:00 UTC372
    /// device units per millisecond.
9944e8618a62atagen2026-10-18 10:00 UTC373
    pub accel_points: Option<Vec<f64>>,
9944e8618a62atagen2026-10-18 10:00 UTC374
    pub accel_step: Option<f64>,
9944e8618a62atagen2026-10-18 10:00 UTC375
    pub natural_scroll: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC376
    pub left_handed: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC377
    pub tap: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC378
    pub tap_drag: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC379
    pub tap_drag_lock: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC380
    pub dwt: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC381
    pub middle_emulation: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC382
    pub scroll_method: Option<ScrollMethod>,
9944e8618a62atagen2026-10-18 10:00 UTC383
    pub scroll_button: Option<u32>,
9944e8618a62atagen2026-10-18 10:00 UTC384
    pub click_method: Option<ClickMethod>,
9944e8618a62atagen2026-10-18 10:00 UTC385
    /// Applied by the compositor, libinput has no such setting.
9944e8618a62atagen2026-10-18 10:00 UTC386
    pub scroll_factor: Option<f64>,
9944e8618a62atagen2026-10-18 10:00 UTC387
}
9944e8618a62atagen2026-10-18 10:00 UTC388
9944e8618a62atagen2026-10-18 10:00 UTC389
impl DeviceSettings {
9944e8618a62atagen2026-10-18 10:00 UTC390
    /// `self` with every option `over` sets replaced.
9944e8618a62atagen2026-10-18 10:00 UTC391
    pub fn overlay(&self, over: &Self) -> Self {
9944e8618a62atagen2026-10-18 10:00 UTC392
        Self {
9944e8618a62atagen2026-10-18 10:00 UTC393
            accel_profile: over.accel_profile.or(self.accel_profile),
9944e8618a62atagen2026-10-18 10:00 UTC394
            accel_speed: over.accel_speed.or(self.accel_speed),
9944e8618a62atagen2026-10-18 10:00 UTC395
            accel_points: over
9944e8618a62atagen2026-10-18 10:00 UTC396
                .accel_points
9944e8618a62atagen2026-10-18 10:00 UTC397
                .clone()
9944e8618a62atagen2026-10-18 10:00 UTC398
                .or_else(|| self.accel_points.clone()),
9944e8618a62atagen2026-10-18 10:00 UTC399
            accel_step: over.accel_step.or(self.accel_step),
9944e8618a62atagen2026-10-18 10:00 UTC400
            natural_scroll: over.natural_scroll.or(self.natural_scroll),
9944e8618a62atagen2026-10-18 10:00 UTC401
            left_handed: over.left_handed.or(self.left_handed),
9944e8618a62atagen2026-10-18 10:00 UTC402
            tap: over.tap.or(self.tap),
9944e8618a62atagen2026-10-18 10:00 UTC403
            tap_drag: over.tap_drag.or(self.tap_drag),
9944e8618a62atagen2026-10-18 10:00 UTC404
            tap_drag_lock: over.tap_drag_lock.or(self.tap_drag_lock),
9944e8618a62atagen2026-10-18 10:00 UTC405
            dwt: over.dwt.or(self.dwt),
9944e8618a62atagen2026-10-18 10:00 UTC406
            middle_emulation: over.middle_emulation.or(self.middle_emulation),
9944e8618a62atagen2026-10-18 10:00 UTC407
            scroll_method: over.scroll_method.or(self.scroll_method),
9944e8618a62atagen2026-10-18 10:00 UTC408
            scroll_button: over.scroll_button.or(self.scroll_button),
9944e8618a62atagen2026-10-18 10:00 UTC409
            click_method: over.click_method.or(self.click_method),
9944e8618a62atagen2026-10-18 10:00 UTC410
            scroll_factor: over.scroll_factor.or(self.scroll_factor),
9944e8618a62atagen2026-10-18 10:00 UTC411
        }
9944e8618a62atagen2026-10-18 10:00 UTC412
    }
9944e8618a62atagen2026-10-18 10:00 UTC413
9944e8618a62atagen2026-10-18 10:00 UTC414
    fn validate(&mut self, name: &str, w: &mut Vec<String>) {
9944e8618a62atagen2026-10-18 10:00 UTC415
        if let Some(s) = self.accel_speed
9944e8618a62atagen2026-10-18 10:00 UTC416
            && !(-1.0..=1.0).contains(&s)
9944e8618a62atagen2026-10-18 10:00 UTC417
        {
9944e8618a62atagen2026-10-18 10:00 UTC418
            w.push(format!(
9944e8618a62atagen2026-10-18 10:00 UTC419
                "{name}.accel_speed: {s} is outside -1 to 1, ignored"
9944e8618a62atagen2026-10-18 10:00 UTC420
            ));
9944e8618a62atagen2026-10-18 10:00 UTC421
            self.accel_speed = None;
9944e8618a62atagen2026-10-18 10:00 UTC422
        }
9944e8618a62atagen2026-10-18 10:00 UTC423
        if let Some(f) = self.scroll_factor
9944e8618a62atagen2026-10-18 10:00 UTC424
            && !(f.is_finite() && f >= 0.0)
9944e8618a62atagen2026-10-18 10:00 UTC425
        {
9944e8618a62atagen2026-10-18 10:00 UTC426
            w.push(format!(
9944e8618a62atagen2026-10-18 10:00 UTC427
                "{name}.scroll_factor: {f} must be zero or more, ignored"
9944e8618a62atagen2026-10-18 10:00 UTC428
            ));
9944e8618a62atagen2026-10-18 10:00 UTC429
            self.scroll_factor = None;
9944e8618a62atagen2026-10-18 10:00 UTC430
        }
9944e8618a62atagen2026-10-18 10:00 UTC431
        if self.accel_points.is_some() != self.accel_step.is_some() {
9944e8618a62atagen2026-10-18 10:00 UTC432
            w.push(format!(
9944e8618a62atagen2026-10-18 10:00 UTC433
                "{name}: accel_points needs accel_step and the other way round, both ignored"
9944e8618a62atagen2026-10-18 10:00 UTC434
            ));
9944e8618a62atagen2026-10-18 10:00 UTC435
            self.accel_points = None;
9944e8618a62atagen2026-10-18 10:00 UTC436
            self.accel_step = None;
9944e8618a62atagen2026-10-18 10:00 UTC437
        }
9944e8618a62atagen2026-10-18 10:00 UTC438
    }
9944e8618a62atagen2026-10-18 10:00 UTC439
}
9944e8618a62atagen2026-10-18 10:00 UTC440
9944e8618a62atagen2026-10-18 10:00 UTC441
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
9944e8618a62atagen2026-10-18 10:00 UTC442
#[serde(rename_all = "lowercase")]
9944e8618a62atagen2026-10-18 10:00 UTC443
pub enum AccelProfile {
9944e8618a62atagen2026-10-18 10:00 UTC444
    Flat,
9944e8618a62atagen2026-10-18 10:00 UTC445
    Adaptive,
9944e8618a62atagen2026-10-18 10:00 UTC446
    Custom,
9944e8618a62atagen2026-10-18 10:00 UTC447
}
9944e8618a62atagen2026-10-18 10:00 UTC448
9944e8618a62atagen2026-10-18 10:00 UTC449
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
9944e8618a62atagen2026-10-18 10:00 UTC450
#[serde(rename_all = "kebab-case")]
9944e8618a62atagen2026-10-18 10:00 UTC451
pub enum ScrollMethod {
9944e8618a62atagen2026-10-18 10:00 UTC452
    None,
9944e8618a62atagen2026-10-18 10:00 UTC453
    TwoFinger,
9944e8618a62atagen2026-10-18 10:00 UTC454
    Edge,
9944e8618a62atagen2026-10-18 10:00 UTC455
    OnButtonDown,
9944e8618a62atagen2026-10-18 10:00 UTC456
}
9944e8618a62atagen2026-10-18 10:00 UTC457
9944e8618a62atagen2026-10-18 10:00 UTC458
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
9944e8618a62atagen2026-10-18 10:00 UTC459
#[serde(rename_all = "kebab-case")]
9944e8618a62atagen2026-10-18 10:00 UTC460
pub enum ClickMethod {
9944e8618a62atagen2026-10-18 10:00 UTC461
    ButtonAreas,
9944e8618a62atagen2026-10-18 10:00 UTC462
    Clickfinger,
9944e8618a62atagen2026-10-18 10:00 UTC463
}
9944e8618a62atagen2026-10-18 10:00 UTC464
fe835aed6c8datagen2026-10-16 10:05 UTC465
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
fe835aed6c8datagen2026-10-16 10:05 UTC466
#[serde(rename_all = "lowercase")]
fe835aed6c8datagen2026-10-16 10:05 UTC467
pub enum Modifier {
fe835aed6c8datagen2026-10-16 10:05 UTC468
    Super,
fe835aed6c8datagen2026-10-16 10:05 UTC469
    Alt,
fe835aed6c8datagen2026-10-16 10:05 UTC470
    Ctrl,
fe835aed6c8datagen2026-10-16 10:05 UTC471
    Shift,
fe835aed6c8datagen2026-10-16 10:05 UTC472
}
fe835aed6c8datagen2026-10-16 10:05 UTC473
fe835aed6c8datagen2026-10-16 10:05 UTC474
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
fe835aed6c8datagen2026-10-16 10:05 UTC475
pub struct Mods {
fe835aed6c8datagen2026-10-16 10:05 UTC476
    pub logo: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC477
    pub alt: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC478
    pub ctrl: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC479
    pub shift: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC480
}
fe835aed6c8datagen2026-10-16 10:05 UTC481
fe835aed6c8datagen2026-10-16 10:05 UTC482
impl Mods {
b22c0511d5afatagen2026-10-17 03:48 UTC483
    pub fn has(&self, m: Modifier) -> bool {
b22c0511d5afatagen2026-10-17 03:48 UTC484
        match m {
b22c0511d5afatagen2026-10-17 03:48 UTC485
            Modifier::Super => self.logo,
b22c0511d5afatagen2026-10-17 03:48 UTC486
            Modifier::Alt => self.alt,
b22c0511d5afatagen2026-10-17 03:48 UTC487
            Modifier::Ctrl => self.ctrl,
b22c0511d5afatagen2026-10-17 03:48 UTC488
            Modifier::Shift => self.shift,
b22c0511d5afatagen2026-10-17 03:48 UTC489
        }
b22c0511d5afatagen2026-10-17 03:48 UTC490
    }
b22c0511d5afatagen2026-10-17 03:48 UTC491
fe835aed6c8datagen2026-10-16 10:05 UTC492
    fn set(&mut self, m: Modifier) {
fe835aed6c8datagen2026-10-16 10:05 UTC493
        match m {
fe835aed6c8datagen2026-10-16 10:05 UTC494
            Modifier::Super => self.logo = true,
fe835aed6c8datagen2026-10-16 10:05 UTC495
            Modifier::Alt => self.alt = true,
fe835aed6c8datagen2026-10-16 10:05 UTC496
            Modifier::Ctrl => self.ctrl = true,
fe835aed6c8datagen2026-10-16 10:05 UTC497
            Modifier::Shift => self.shift = true,
fe835aed6c8datagen2026-10-16 10:05 UTC498
        }
fe835aed6c8datagen2026-10-16 10:05 UTC499
    }
fe835aed6c8datagen2026-10-16 10:05 UTC500
}
fe835aed6c8datagen2026-10-16 10:05 UTC501
fe835aed6c8datagen2026-10-16 10:05 UTC502
#[derive(Debug, Clone, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC503
pub struct Bind {
fe835aed6c8datagen2026-10-16 10:05 UTC504
    pub mods: Mods,
fe835aed6c8datagen2026-10-16 10:05 UTC505
    /// An xkb keysym name, resolved by the compositor.
fe835aed6c8datagen2026-10-16 10:05 UTC506
    pub key: String,
fe835aed6c8datagen2026-10-16 10:05 UTC507
    pub command: Command,
fe835aed6c8datagen2026-10-16 10:05 UTC508
}
fe835aed6c8datagen2026-10-16 10:05 UTC509
fe835aed6c8datagen2026-10-16 10:05 UTC510
#[derive(Debug, Clone)]
fe835aed6c8datagen2026-10-16 10:05 UTC511
pub struct Rule {
fe835aed6c8datagen2026-10-16 10:05 UTC512
    pub app_id: Option<Regex>,
fe835aed6c8datagen2026-10-16 10:05 UTC513
    pub title: Option<Regex>,
fe835aed6c8datagen2026-10-16 10:05 UTC514
    pub tag: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC515
    pub output: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC516
    pub floating: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC517
    pub sticky: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC518
    pub scratchpad: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC519
}
fe835aed6c8datagen2026-10-16 10:05 UTC520
fe835aed6c8datagen2026-10-16 10:05 UTC521
/// Everything the matching rules say about a window, later rules winning.
fe835aed6c8datagen2026-10-16 10:05 UTC522
#[derive(Debug, Clone, Default, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC523
pub struct Placement {
fe835aed6c8datagen2026-10-16 10:05 UTC524
    pub tag: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC525
    pub output: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC526
    pub floating: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC527
    pub sticky: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC528
    pub scratchpad: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC529
}
fe835aed6c8datagen2026-10-16 10:05 UTC530
fe835aed6c8datagen2026-10-16 10:05 UTC531
#[derive(Debug)]
fe835aed6c8datagen2026-10-16 10:05 UTC532
pub enum Error {
fe835aed6c8datagen2026-10-16 10:05 UTC533
    Io(PathBuf, std::io::Error),
fe835aed6c8datagen2026-10-16 10:05 UTC534
    Toml(toml::de::Error),
fe835aed6c8datagen2026-10-16 10:05 UTC535
}
fe835aed6c8datagen2026-10-16 10:05 UTC536
fe835aed6c8datagen2026-10-16 10:05 UTC537
impl fmt::Display for Error {
fe835aed6c8datagen2026-10-16 10:05 UTC538
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fe835aed6c8datagen2026-10-16 10:05 UTC539
        match self {
fe835aed6c8datagen2026-10-16 10:05 UTC540
            Error::Io(p, e) => write!(f, "{}: {e}", p.display()),
fe835aed6c8datagen2026-10-16 10:05 UTC541
            Error::Toml(e) => write!(f, "{e}"),
fe835aed6c8datagen2026-10-16 10:05 UTC542
        }
fe835aed6c8datagen2026-10-16 10:05 UTC543
    }
fe835aed6c8datagen2026-10-16 10:05 UTC544
}
fe835aed6c8datagen2026-10-16 10:05 UTC545
fe835aed6c8datagen2026-10-16 10:05 UTC546
impl std::error::Error for Error {}
fe835aed6c8datagen2026-10-16 10:05 UTC547
fe835aed6c8datagen2026-10-16 10:05 UTC548
#[derive(Deserialize)]
fe835aed6c8datagen2026-10-16 10:05 UTC549
#[serde(untagged)]
fe835aed6c8datagen2026-10-16 10:05 UTC550
enum Words {
fe835aed6c8datagen2026-10-16 10:05 UTC551
    Line(String),
fe835aed6c8datagen2026-10-16 10:05 UTC552
    List(Vec<String>),
fe835aed6c8datagen2026-10-16 10:05 UTC553
}
fe835aed6c8datagen2026-10-16 10:05 UTC554
fe835aed6c8datagen2026-10-16 10:05 UTC555
#[derive(Deserialize)]
fe835aed6c8datagen2026-10-16 10:05 UTC556
#[serde(deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC557
struct RawRule {
fe835aed6c8datagen2026-10-16 10:05 UTC558
    app_id: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC559
    title: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC560
    tag: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC561
    output: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC562
    floating: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC563
    sticky: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC564
    scratchpad: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC565
}
fe835aed6c8datagen2026-10-16 10:05 UTC566
fe835aed6c8datagen2026-10-16 10:05 UTC567
impl Config {
fe835aed6c8datagen2026-10-16 10:05 UTC568
    /// `$MONOLITH_CONFIG`, else `$XDG_CONFIG_HOME/monolith/config.toml`.
fe835aed6c8datagen2026-10-16 10:05 UTC569
    pub fn path() -> Option<PathBuf> {
fe835aed6c8datagen2026-10-16 10:05 UTC570
        if let Some(p) = std::env::var_os("MONOLITH_CONFIG") {
fe835aed6c8datagen2026-10-16 10:05 UTC571
            return Some(p.into());
fe835aed6c8datagen2026-10-16 10:05 UTC572
        }
fe835aed6c8datagen2026-10-16 10:05 UTC573
        let base = std::env::var_os("XDG_CONFIG_HOME")
fe835aed6c8datagen2026-10-16 10:05 UTC574
            .map(PathBuf::from)
fe835aed6c8datagen2026-10-16 10:05 UTC575
            .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
fe835aed6c8datagen2026-10-16 10:05 UTC576
        Some(base.join("monolith/config.toml"))
fe835aed6c8datagen2026-10-16 10:05 UTC577
    }
fe835aed6c8datagen2026-10-16 10:05 UTC578
fe835aed6c8datagen2026-10-16 10:05 UTC579
    /// Loads `path`, or the built-in default when it does not exist.
8fb62aebf501atagen2026-10-17 10:25 UTC580
    pub fn load(path: Option<&Path>) -> Result<Loaded, Error> {
fe835aed6c8datagen2026-10-16 10:05 UTC581
        match path {
fe835aed6c8datagen2026-10-16 10:05 UTC582
            Some(p) => match std::fs::read_to_string(p) {
fe835aed6c8datagen2026-10-16 10:05 UTC583
                Ok(text) => Self::parse(&text),
fe835aed6c8datagen2026-10-16 10:05 UTC584
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::parse(DEFAULT),
fe835aed6c8datagen2026-10-16 10:05 UTC585
                Err(e) => Err(Error::Io(p.to_owned(), e)),
fe835aed6c8datagen2026-10-16 10:05 UTC586
            },
fe835aed6c8datagen2026-10-16 10:05 UTC587
            None => Self::parse(DEFAULT),
fe835aed6c8datagen2026-10-16 10:05 UTC588
        }
fe835aed6c8datagen2026-10-16 10:05 UTC589
    }
fe835aed6c8datagen2026-10-16 10:05 UTC590
a1e55dd5ed07atagen2026-10-19 08:36 UTC591
    /// Only TOML too broken to repair line by line fails. A bad option, bind
a1e55dd5ed07atagen2026-10-19 08:36 UTC592
    /// or rule is left out with a warning, so one typo does not throw away the
a1e55dd5ed07atagen2026-10-19 08:36 UTC593
    /// rest of the file.
8fb62aebf501atagen2026-10-17 10:25 UTC594
    pub fn parse(text: &str) -> Result<Loaded, Error> {
8fb62aebf501atagen2026-10-17 10:25 UTC595
        let mut warnings = Vec::new();
a1e55dd5ed07atagen2026-10-19 08:36 UTC596
        let mut table = lenient_table(text, &mut warnings)?;
8fb62aebf501atagen2026-10-17 10:25 UTC597
        let w = &mut warnings;
8fb62aebf501atagen2026-10-17 10:25 UTC598
        let general: General = section("general", table.remove("general"), w);
8fb62aebf501atagen2026-10-17 10:25 UTC599
        let keyboard = section("keyboard", table.remove("keyboard"), w);
9944e8618a62atagen2026-10-18 10:00 UTC600
        let input = input(table.remove("input"), w);
47b86d08d05aatagen2026-10-19 07:37 UTC601
        let idle = section("idle", table.remove("idle"), w);
5b8422a225daatagen2026-10-19 07:45 UTC602
        let lid = section("lid", table.remove("lid"), w);
8fb62aebf501atagen2026-10-17 10:25 UTC603
        let animation = section("animation", table.remove("animation"), w);
1df9b00ad0fdatagen2026-10-19 07:34 UTC604
        let xwayland = section("xwayland", table.remove("xwayland"), w);
8fb62aebf501atagen2026-10-17 10:25 UTC605
        let decoration = section("decoration", table.remove("decoration"), w);
fc167134b65datagen2026-10-19 08:09 UTC606
        let mut output: Outputs = section("output", table.remove("output"), w);
fc167134b65datagen2026-10-19 08:09 UTC607
        for name in output
fc167134b65datagen2026-10-19 08:09 UTC608
            .named
fc167134b65datagen2026-10-19 08:09 UTC609
            .keys()
fc167134b65datagen2026-10-19 08:09 UTC610
            .filter(|n| *n != "nested" && !connector_like(n))
fc167134b65datagen2026-10-19 08:09 UTC611
        {
0f513a2420c0atagen2026-10-18 02:40 UTC612
            w.push(format!(
0f513a2420c0atagen2026-10-18 02:40 UTC613
                "output.{name}: not a connector name like DP-1 or HDMI-A-1, matches nothing"
0f513a2420c0atagen2026-10-18 02:40 UTC614
            ));
0f513a2420c0atagen2026-10-18 02:40 UTC615
        }
fc167134b65datagen2026-10-19 08:09 UTC616
        for (name, config) in &mut output.named {
fc167134b65datagen2026-10-19 08:09 UTC617
            config.validate(&format!("output.{name}"), w);
fc167134b65datagen2026-10-19 08:09 UTC618
        }
8fb62aebf501atagen2026-10-17 10:25 UTC619
        let startup = items("startup", table.remove("startup"), w);
8fb62aebf501atagen2026-10-17 10:25 UTC620
        let mut binds = Vec::new();
8fb62aebf501atagen2026-10-17 10:25 UTC621
        if let Some(value) = table.remove("binds") {
8fb62aebf501atagen2026-10-17 10:25 UTC622
            let entries: toml::Table = entry("binds", value, w).unwrap_or_default();
8fb62aebf501atagen2026-10-17 10:25 UTC623
            for (combo, value) in entries {
8fb62aebf501atagen2026-10-17 10:25 UTC624
                let name = format!("binds.{combo:?}");
8fb62aebf501atagen2026-10-17 10:25 UTC625
                let bind = entry::<Words>(&name, value, w)
8fb62aebf501atagen2026-10-17 10:25 UTC626
                    .map(|words| parse_bind(&combo, words, general.mod_key));
8fb62aebf501atagen2026-10-17 10:25 UTC627
                match bind {
8fb62aebf501atagen2026-10-17 10:25 UTC628
                    Some(Ok(b)) => binds.push(b),
8fb62aebf501atagen2026-10-17 10:25 UTC629
                    Some(Err(e)) => w.push(format!("{name}: {e}, left out")),
8fb62aebf501atagen2026-10-17 10:25 UTC630
                    None => {}
8fb62aebf501atagen2026-10-17 10:25 UTC631
                }
8fb62aebf501atagen2026-10-17 10:25 UTC632
            }
8fb62aebf501atagen2026-10-17 10:25 UTC633
        }
8fb62aebf501atagen2026-10-17 10:25 UTC634
        let regex = |p: Option<String>| p.map(|p| Regex::new(&p)).transpose();
8fb62aebf501atagen2026-10-17 10:25 UTC635
        let mut rules = Vec::new();
8fb62aebf501atagen2026-10-17 10:25 UTC636
        for (i, raw) in items::<RawRule>("rules", table.remove("rules"), w)
fe835aed6c8datagen2026-10-16 10:05 UTC637
            .into_iter()
8fb62aebf501atagen2026-10-17 10:25 UTC638
            .enumerate()
8fb62aebf501atagen2026-10-17 10:25 UTC639
        {
8fb62aebf501atagen2026-10-17 10:25 UTC640
            match (regex(raw.app_id), regex(raw.title)) {
8fb62aebf501atagen2026-10-17 10:25 UTC641
                (Ok(app_id), Ok(title)) => rules.push(Rule {
8fb62aebf501atagen2026-10-17 10:25 UTC642
                    app_id,
8fb62aebf501atagen2026-10-17 10:25 UTC643
                    title,
8fb62aebf501atagen2026-10-17 10:25 UTC644
                    tag: raw.tag,
8fb62aebf501atagen2026-10-17 10:25 UTC645
                    output: raw.output,
8fb62aebf501atagen2026-10-17 10:25 UTC646
                    floating: raw.floating,
8fb62aebf501atagen2026-10-17 10:25 UTC647
                    sticky: raw.sticky,
8fb62aebf501atagen2026-10-17 10:25 UTC648
                    scratchpad: raw.scratchpad,
8fb62aebf501atagen2026-10-17 10:25 UTC649
                }),
8fb62aebf501atagen2026-10-17 10:25 UTC650
                (Err(e), _) | (_, Err(e)) => {
8fb62aebf501atagen2026-10-17 10:25 UTC651
                    let e = e
8fb62aebf501atagen2026-10-17 10:25 UTC652
                        .to_string()
8fb62aebf501atagen2026-10-17 10:25 UTC653
                        .split_whitespace()
8fb62aebf501atagen2026-10-17 10:25 UTC654
                        .collect::<Vec<_>>()
8fb62aebf501atagen2026-10-17 10:25 UTC655
                        .join(" ");
8fb62aebf501atagen2026-10-17 10:25 UTC656
                    w.push(format!("rules[{i}]: {e}, left out"));
8fb62aebf501atagen2026-10-17 10:25 UTC657
                }
8fb62aebf501atagen2026-10-17 10:25 UTC658
            }
8fb62aebf501atagen2026-10-17 10:25 UTC659
        }
8fb62aebf501atagen2026-10-17 10:25 UTC660
        for key in table.keys() {
8fb62aebf501atagen2026-10-17 10:25 UTC661
            w.push(format!("unknown section [{key}], ignored"));
8fb62aebf501atagen2026-10-17 10:25 UTC662
        }
8fb62aebf501atagen2026-10-17 10:25 UTC663
        let config = Self {
8fb62aebf501atagen2026-10-17 10:25 UTC664
            general,
8fb62aebf501atagen2026-10-17 10:25 UTC665
            animation,
8fb62aebf501atagen2026-10-17 10:25 UTC666
            decoration,
8fb62aebf501atagen2026-10-17 10:25 UTC667
            output,
8fb62aebf501atagen2026-10-17 10:25 UTC668
            keyboard,
9944e8618a62atagen2026-10-18 10:00 UTC669
            input,
47b86d08d05aatagen2026-10-19 07:37 UTC670
            idle,
5b8422a225daatagen2026-10-19 07:45 UTC671
            lid,
1df9b00ad0fdatagen2026-10-19 07:34 UTC672
            xwayland,
fe835aed6c8datagen2026-10-16 10:05 UTC673
            binds,
fe835aed6c8datagen2026-10-16 10:05 UTC674
            rules,
8fb62aebf501atagen2026-10-17 10:25 UTC675
            startup,
8fb62aebf501atagen2026-10-17 10:25 UTC676
        };
8fb62aebf501atagen2026-10-17 10:25 UTC677
        Ok(Loaded { config, warnings })
fe835aed6c8datagen2026-10-16 10:05 UTC678
    }
fe835aed6c8datagen2026-10-16 10:05 UTC679
fe835aed6c8datagen2026-10-16 10:05 UTC680
    pub fn placement(&self, app_id: &str, title: &str) -> Placement {
fe835aed6c8datagen2026-10-16 10:05 UTC681
        let mut p = Placement::default();
fe835aed6c8datagen2026-10-16 10:05 UTC682
        let hit = |re: &Option<Regex>, s: &str| re.as_ref().is_none_or(|re| re.is_match(s));
fe835aed6c8datagen2026-10-16 10:05 UTC683
        for r in &self.rules {
fe835aed6c8datagen2026-10-16 10:05 UTC684
            if !hit(&r.app_id, app_id) || !hit(&r.title, title) {
fe835aed6c8datagen2026-10-16 10:05 UTC685
                continue;
fe835aed6c8datagen2026-10-16 10:05 UTC686
            }
fe835aed6c8datagen2026-10-16 10:05 UTC687
            p.tag = r.tag.clone().or(p.tag);
fe835aed6c8datagen2026-10-16 10:05 UTC688
            p.output = r.output.clone().or(p.output);
fe835aed6c8datagen2026-10-16 10:05 UTC689
            p.floating = r.floating.or(p.floating);
fe835aed6c8datagen2026-10-16 10:05 UTC690
            p.sticky = r.sticky.or(p.sticky);
fe835aed6c8datagen2026-10-16 10:05 UTC691
            p.scratchpad = r.scratchpad.clone().or(p.scratchpad);
fe835aed6c8datagen2026-10-16 10:05 UTC692
        }
fe835aed6c8datagen2026-10-16 10:05 UTC693
        p
fe835aed6c8datagen2026-10-16 10:05 UTC694
    }
fe835aed6c8datagen2026-10-16 10:05 UTC695
}
fe835aed6c8datagen2026-10-16 10:05 UTC696
8fb62aebf501atagen2026-10-17 10:25 UTC697
fn parse_bind(combo: &str, words: Words, mod_key: Modifier) -> Result<Bind, String> {
fe835aed6c8datagen2026-10-16 10:05 UTC698
    let parts: Vec<&str> = combo.split('+').map(str::trim).collect();
fe835aed6c8datagen2026-10-16 10:05 UTC699
    let Some((&key, mods)) = parts.split_last().filter(|(k, _)| !k.is_empty()) else {
8fb62aebf501atagen2026-10-17 10:25 UTC700
        return Err("no key".into());
fe835aed6c8datagen2026-10-16 10:05 UTC701
    };
fe835aed6c8datagen2026-10-16 10:05 UTC702
    let mut set = Mods::default();
fe835aed6c8datagen2026-10-16 10:05 UTC703
    for m in mods {
fe835aed6c8datagen2026-10-16 10:05 UTC704
        let m = match m.to_ascii_lowercase().as_str() {
fe835aed6c8datagen2026-10-16 10:05 UTC705
            "mod" => mod_key,
fe835aed6c8datagen2026-10-16 10:05 UTC706
            "super" | "logo" | "win" => Modifier::Super,
fe835aed6c8datagen2026-10-16 10:05 UTC707
            "alt" => Modifier::Alt,
fe835aed6c8datagen2026-10-16 10:05 UTC708
            "ctrl" | "control" => Modifier::Ctrl,
fe835aed6c8datagen2026-10-16 10:05 UTC709
            "shift" => Modifier::Shift,
8fb62aebf501atagen2026-10-17 10:25 UTC710
            other => return Err(format!("unknown modifier {other}")),
fe835aed6c8datagen2026-10-16 10:05 UTC711
        };
fe835aed6c8datagen2026-10-16 10:05 UTC712
        set.set(m);
fe835aed6c8datagen2026-10-16 10:05 UTC713
    }
fe835aed6c8datagen2026-10-16 10:05 UTC714
    let words: Vec<String> = match words {
fe835aed6c8datagen2026-10-16 10:05 UTC715
        Words::Line(s) => s.split_whitespace().map(str::to_owned).collect(),
fe835aed6c8datagen2026-10-16 10:05 UTC716
        Words::List(v) => v,
fe835aed6c8datagen2026-10-16 10:05 UTC717
    };
8fb62aebf501atagen2026-10-17 10:25 UTC718
    let command = Command::parse(&words).map_err(|e| e.0)?;
fe835aed6c8datagen2026-10-16 10:05 UTC719
    Ok(Bind {
fe835aed6c8datagen2026-10-16 10:05 UTC720
        mods: set,
fe835aed6c8datagen2026-10-16 10:05 UTC721
        key: key.to_owned(),
fe835aed6c8datagen2026-10-16 10:05 UTC722
        command,
fe835aed6c8datagen2026-10-16 10:05 UTC723
    })
fe835aed6c8datagen2026-10-16 10:05 UTC724
}
fe835aed6c8datagen2026-10-16 10:05 UTC725
8fb62aebf501atagen2026-10-17 10:25 UTC726
/// A config and what had to be left out of it.
8fb62aebf501atagen2026-10-17 10:25 UTC727
pub struct Loaded {
8fb62aebf501atagen2026-10-17 10:25 UTC728
    pub config: Config,
8fb62aebf501atagen2026-10-17 10:25 UTC729
    pub warnings: Vec<String>,
8fb62aebf501atagen2026-10-17 10:25 UTC730
}
8fb62aebf501atagen2026-10-17 10:25 UTC731
a1e55dd5ed07atagen2026-10-19 08:36 UTC732
/// Parses TOML, dropping each line the parser rejects and trying again, so a
a1e55dd5ed07atagen2026-10-19 08:36 UTC733
/// syntax error costs its own line rather than the whole file. An unclosed
a1e55dd5ed07atagen2026-10-19 08:36 UTC734
/// table or array is reported lines after the mistake, so the culprit is
a1e55dd5ed07atagen2026-10-19 08:36 UTC735
/// the nearby line whose removal clears that error, or else gets furthest.
a1e55dd5ed07atagen2026-10-19 08:36 UTC736
fn lenient_table(text: &str, w: &mut Vec<String>) -> Result<toml::Table, Error> {
a1e55dd5ed07atagen2026-10-19 08:36 UTC737
    const MAX_BAD_LINES: usize = 32;
a1e55dd5ed07atagen2026-10-19 08:36 UTC738
    const LOOK_BACK: usize = 4;
a1e55dd5ed07atagen2026-10-19 08:36 UTC739
    let mut lines: Vec<(usize, &str)> = text.lines().enumerate().map(|(i, l)| (i + 1, l)).collect();
a1e55dd5ed07atagen2026-10-19 08:36 UTC740
    let join =
a1e55dd5ed07atagen2026-10-19 08:36 UTC741
        |lines: &[(usize, &str)]| lines.iter().map(|(_, l)| *l).collect::<Vec<_>>().join("\n");
a1e55dd5ed07atagen2026-10-19 08:36 UTC742
    let failing_line = |lines: &[(usize, &str)], e: &toml::de::Error| -> Option<usize> {
a1e55dd5ed07atagen2026-10-19 08:36 UTC743
        let at = e.span()?.start;
a1e55dd5ed07atagen2026-10-19 08:36 UTC744
        let text = join(lines);
a1e55dd5ed07atagen2026-10-19 08:36 UTC745
        let index = text[..at.min(text.len())].matches('\n').count();
a1e55dd5ed07atagen2026-10-19 08:36 UTC746
        Some(index.min(lines.len().saturating_sub(1)))
a1e55dd5ed07atagen2026-10-19 08:36 UTC747
    };
a1e55dd5ed07atagen2026-10-19 08:36 UTC748
    for _ in 0..MAX_BAD_LINES {
a1e55dd5ed07atagen2026-10-19 08:36 UTC749
        let e = match toml::from_str(&join(&lines)) {
a1e55dd5ed07atagen2026-10-19 08:36 UTC750
            Ok(table) => return Ok(table),
a1e55dd5ed07atagen2026-10-19 08:36 UTC751
            Err(e) => e,
a1e55dd5ed07atagen2026-10-19 08:36 UTC752
        };
a1e55dd5ed07atagen2026-10-19 08:36 UTC753
        let Some(at) = failing_line(&lines, &e) else {
a1e55dd5ed07atagen2026-10-19 08:36 UTC754
            return Err(Error::Toml(e));
a1e55dd5ed07atagen2026-10-19 08:36 UTC755
        };
a1e55dd5ed07atagen2026-10-19 08:36 UTC756
        let progress = |drop: usize| {
a1e55dd5ed07atagen2026-10-19 08:36 UTC757
            let mut rest = lines.clone();
a1e55dd5ed07atagen2026-10-19 08:36 UTC758
            rest.remove(drop);
a1e55dd5ed07atagen2026-10-19 08:36 UTC759
            match toml::from_str::<toml::Table>(&join(&rest)) {
a1e55dd5ed07atagen2026-10-19 08:36 UTC760
                Ok(_) => (true, usize::MAX),
a1e55dd5ed07atagen2026-10-19 08:36 UTC761
                Err(next) => (
a1e55dd5ed07atagen2026-10-19 08:36 UTC762
                    next.message() != e.message(),
a1e55dd5ed07atagen2026-10-19 08:36 UTC763
                    failing_line(&rest, &next).map_or(0, |i| rest[i].0),
a1e55dd5ed07atagen2026-10-19 08:36 UTC764
                ),
a1e55dd5ed07atagen2026-10-19 08:36 UTC765
            }
a1e55dd5ed07atagen2026-10-19 08:36 UTC766
        };
a1e55dd5ed07atagen2026-10-19 08:36 UTC767
        let culprit = (at.saturating_sub(LOOK_BACK)..=at)
a1e55dd5ed07atagen2026-10-19 08:36 UTC768
            .filter(|&i| !lines[i].1.trim().is_empty())
a1e55dd5ed07atagen2026-10-19 08:36 UTC769
            .max_by_key(|&i| progress(i))
a1e55dd5ed07atagen2026-10-19 08:36 UTC770
            .unwrap_or(at);
a1e55dd5ed07atagen2026-10-19 08:36 UTC771
        let (line, _) = lines.remove(culprit);
a1e55dd5ed07atagen2026-10-19 08:36 UTC772
        w.push(format!("line {line}: {}, left out", e.message().trim()));
a1e55dd5ed07atagen2026-10-19 08:36 UTC773
    }
a1e55dd5ed07atagen2026-10-19 08:36 UTC774
    toml::from_str(&join(&lines)).map_err(Error::Toml)
a1e55dd5ed07atagen2026-10-19 08:36 UTC775
}
a1e55dd5ed07atagen2026-10-19 08:36 UTC776
8fb62aebf501atagen2026-10-17 10:25 UTC777
fn entry<T: DeserializeOwned>(name: &str, value: toml::Value, w: &mut Vec<String>) -> Option<T> {
8fb62aebf501atagen2026-10-17 10:25 UTC778
    value
8fb62aebf501atagen2026-10-17 10:25 UTC779
        .try_into()
8fb62aebf501atagen2026-10-17 10:25 UTC780
        .map_err(|e| w.push(format!("{name}: {}, left out", one_line(&e))))
8fb62aebf501atagen2026-10-17 10:25 UTC781
        .ok()
8fb62aebf501atagen2026-10-17 10:25 UTC782
}
8fb62aebf501atagen2026-10-17 10:25 UTC783
9944e8618a62atagen2026-10-18 10:00 UTC784
fn input(value: Option<toml::Value>, w: &mut Vec<String>) -> Input {
9944e8618a62atagen2026-10-18 10:00 UTC785
    let mut table = match value {
9944e8618a62atagen2026-10-18 10:00 UTC786
        None => toml::Table::new(),
9944e8618a62atagen2026-10-18 10:00 UTC787
        Some(toml::Value::Table(t)) => t,
9944e8618a62atagen2026-10-18 10:00 UTC788
        Some(_) => {
9944e8618a62atagen2026-10-18 10:00 UTC789
            w.push("[input] is not a table, using the defaults".into());
9944e8618a62atagen2026-10-18 10:00 UTC790
            toml::Table::new()
9944e8618a62atagen2026-10-18 10:00 UTC791
        }
9944e8618a62atagen2026-10-18 10:00 UTC792
    };
9944e8618a62atagen2026-10-18 10:00 UTC793
    let mut input = Input {
9944e8618a62atagen2026-10-18 10:00 UTC794
        pointer: section("input.pointer", table.remove("pointer"), w),
9944e8618a62atagen2026-10-18 10:00 UTC795
        touchpad: section("input.touchpad", table.remove("touchpad"), w),
9944e8618a62atagen2026-10-18 10:00 UTC796
        device: Vec::new(),
9944e8618a62atagen2026-10-18 10:00 UTC797
    };
9944e8618a62atagen2026-10-18 10:00 UTC798
    for (i, mut entry) in items::<toml::Table>("input.device", table.remove("device"), w)
9944e8618a62atagen2026-10-18 10:00 UTC799
        .into_iter()
9944e8618a62atagen2026-10-18 10:00 UTC800
        .enumerate()
9944e8618a62atagen2026-10-18 10:00 UTC801
    {
9944e8618a62atagen2026-10-18 10:00 UTC802
        let name = format!("input.device[{i}]");
9944e8618a62atagen2026-10-18 10:00 UTC803
        let Some(toml::Value::String(device)) = entry.remove("name") else {
9944e8618a62atagen2026-10-18 10:00 UTC804
            w.push(format!("{name}: no name, left out"));
9944e8618a62atagen2026-10-18 10:00 UTC805
            continue;
9944e8618a62atagen2026-10-18 10:00 UTC806
        };
9944e8618a62atagen2026-10-18 10:00 UTC807
        input.device.push(NamedDevice {
9944e8618a62atagen2026-10-18 10:00 UTC808
            name: device,
9944e8618a62atagen2026-10-18 10:00 UTC809
            settings: section(&name, Some(toml::Value::Table(entry)), w),
9944e8618a62atagen2026-10-18 10:00 UTC810
        });
9944e8618a62atagen2026-10-18 10:00 UTC811
    }
9944e8618a62atagen2026-10-18 10:00 UTC812
    for key in table.keys() {
9944e8618a62atagen2026-10-18 10:00 UTC813
        w.push(format!("input.{key}: unknown option, ignored"));
9944e8618a62atagen2026-10-18 10:00 UTC814
    }
9944e8618a62atagen2026-10-18 10:00 UTC815
    input.pointer.validate("input.pointer", w);
9944e8618a62atagen2026-10-18 10:00 UTC816
    input.touchpad.validate("input.touchpad", w);
9944e8618a62atagen2026-10-18 10:00 UTC817
    for (i, d) in input.device.iter_mut().enumerate() {
9944e8618a62atagen2026-10-18 10:00 UTC818
        d.settings.validate(&format!("input.device[{i}]"), w);
9944e8618a62atagen2026-10-18 10:00 UTC819
    }
9944e8618a62atagen2026-10-18 10:00 UTC820
    input
9944e8618a62atagen2026-10-18 10:00 UTC821
}
9944e8618a62atagen2026-10-18 10:00 UTC822
0f513a2420c0atagen2026-10-18 02:40 UTC823
fn connector_like(name: &str) -> bool {
0f513a2420c0atagen2026-10-18 02:40 UTC824
    name.rsplit_once('-').is_some_and(|(kind, n)| {
0f513a2420c0atagen2026-10-18 02:40 UTC825
        !kind.is_empty() && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())
0f513a2420c0atagen2026-10-18 02:40 UTC826
    })
0f513a2420c0atagen2026-10-18 02:40 UTC827
}
0f513a2420c0atagen2026-10-18 02:40 UTC828
8fb62aebf501atagen2026-10-17 10:25 UTC829
/// A section with every option that does not fit left at its default.
8fb62aebf501atagen2026-10-17 10:25 UTC830
fn section<T: DeserializeOwned + Default>(
8fb62aebf501atagen2026-10-17 10:25 UTC831
    name: &str,
8fb62aebf501atagen2026-10-17 10:25 UTC832
    value: Option<toml::Value>,
8fb62aebf501atagen2026-10-17 10:25 UTC833
    w: &mut Vec<String>,
8fb62aebf501atagen2026-10-17 10:25 UTC834
) -> T {
8fb62aebf501atagen2026-10-17 10:25 UTC835
    let Some(value) = value else {
8fb62aebf501atagen2026-10-17 10:25 UTC836
        return T::default();
8fb62aebf501atagen2026-10-17 10:25 UTC837
    };
8fb62aebf501atagen2026-10-17 10:25 UTC838
    if let Ok(t) = value.clone().try_into() {
8fb62aebf501atagen2026-10-17 10:25 UTC839
        return t;
8fb62aebf501atagen2026-10-17 10:25 UTC840
    }
8fb62aebf501atagen2026-10-17 10:25 UTC841
    let toml::Value::Table(options) = value else {
8fb62aebf501atagen2026-10-17 10:25 UTC842
        w.push(format!("[{name}] is not a table, using the defaults"));
8fb62aebf501atagen2026-10-17 10:25 UTC843
        return T::default();
8fb62aebf501atagen2026-10-17 10:25 UTC844
    };
8fb62aebf501atagen2026-10-17 10:25 UTC845
    let mut kept = toml::Table::new();
8fb62aebf501atagen2026-10-17 10:25 UTC846
    for (key, v) in options {
8fb62aebf501atagen2026-10-17 10:25 UTC847
        let alone = toml::Table::from_iter([(key.clone(), v.clone())]);
8fb62aebf501atagen2026-10-17 10:25 UTC848
        match toml::Value::Table(alone).try_into::<T>() {
8fb62aebf501atagen2026-10-17 10:25 UTC849
            Ok(_) => {
8fb62aebf501atagen2026-10-17 10:25 UTC850
                kept.insert(key, v);
8fb62aebf501atagen2026-10-17 10:25 UTC851
            }
8fb62aebf501atagen2026-10-17 10:25 UTC852
            Err(e) => {
8fb62aebf501atagen2026-10-17 10:25 UTC853
                let e = one_line(&e);
8fb62aebf501atagen2026-10-17 10:25 UTC854
                let fallback = if e.starts_with("unknown field") {
8fb62aebf501atagen2026-10-17 10:25 UTC855
                    "ignored"
8fb62aebf501atagen2026-10-17 10:25 UTC856
                } else {
8fb62aebf501atagen2026-10-17 10:25 UTC857
                    "using the default"
8fb62aebf501atagen2026-10-17 10:25 UTC858
                };
8fb62aebf501atagen2026-10-17 10:25 UTC859
                w.push(format!("{name}.{key}: {e}, {fallback}"));
8fb62aebf501atagen2026-10-17 10:25 UTC860
            }
8fb62aebf501atagen2026-10-17 10:25 UTC861
        }
8fb62aebf501atagen2026-10-17 10:25 UTC862
    }
8fb62aebf501atagen2026-10-17 10:25 UTC863
    toml::Value::Table(kept).try_into().unwrap_or_default()
8fb62aebf501atagen2026-10-17 10:25 UTC864
}
8fb62aebf501atagen2026-10-17 10:25 UTC865
8fb62aebf501atagen2026-10-17 10:25 UTC866
/// An array with the elements that do not fit left out.
8fb62aebf501atagen2026-10-17 10:25 UTC867
fn items<T: DeserializeOwned>(
8fb62aebf501atagen2026-10-17 10:25 UTC868
    name: &str,
8fb62aebf501atagen2026-10-17 10:25 UTC869
    value: Option<toml::Value>,
8fb62aebf501atagen2026-10-17 10:25 UTC870
    w: &mut Vec<String>,
8fb62aebf501atagen2026-10-17 10:25 UTC871
) -> Vec<T> {
8fb62aebf501atagen2026-10-17 10:25 UTC872
    match value {
8fb62aebf501atagen2026-10-17 10:25 UTC873
        None => Vec::new(),
8fb62aebf501atagen2026-10-17 10:25 UTC874
        Some(toml::Value::Array(a)) => a
8fb62aebf501atagen2026-10-17 10:25 UTC875
            .into_iter()
8fb62aebf501atagen2026-10-17 10:25 UTC876
            .enumerate()
8fb62aebf501atagen2026-10-17 10:25 UTC877
            .filter_map(|(i, v)| entry(&format!("{name}[{i}]"), v, w))
8fb62aebf501atagen2026-10-17 10:25 UTC878
            .collect(),
8fb62aebf501atagen2026-10-17 10:25 UTC879
        Some(_) => {
8fb62aebf501atagen2026-10-17 10:25 UTC880
            w.push(format!("{name} is not an array, left out"));
8fb62aebf501atagen2026-10-17 10:25 UTC881
            Vec::new()
8fb62aebf501atagen2026-10-17 10:25 UTC882
        }
8fb62aebf501atagen2026-10-17 10:25 UTC883
    }
8fb62aebf501atagen2026-10-17 10:25 UTC884
}
8fb62aebf501atagen2026-10-17 10:25 UTC885
8fb62aebf501atagen2026-10-17 10:25 UTC886
fn one_line(e: &toml::de::Error) -> String {
8fb62aebf501atagen2026-10-17 10:25 UTC887
    e.message().trim().replace('\n', " ")
8fb62aebf501atagen2026-10-17 10:25 UTC888
}
fe835aed6c8datagen2026-10-16 10:05 UTC889
#[cfg(test)]
fe835aed6c8datagen2026-10-16 10:05 UTC890
mod tests {
b22c0511d5afatagen2026-10-17 03:48 UTC891
    use super::*;
b22c0511d5afatagen2026-10-17 03:48 UTC892
b22c0511d5afatagen2026-10-17 03:48 UTC893
    /// default.toml documents the defaults for users, and the Default impls
b22c0511d5afatagen2026-10-17 03:48 UTC894
    /// apply them to sections a user config leaves out. They must agree.
fe835aed6c8datagen2026-10-16 10:05 UTC895
    #[test]
b22c0511d5afatagen2026-10-17 03:48 UTC896
    fn default_toml_states_the_defaults() {
8fb62aebf501atagen2026-10-17 10:25 UTC897
        let loaded = Config::parse(DEFAULT).unwrap();
8fb62aebf501atagen2026-10-17 10:25 UTC898
        assert_eq!(loaded.warnings, Vec::<String>::new());
8fb62aebf501atagen2026-10-17 10:25 UTC899
        let c = loaded.config;
b22c0511d5afatagen2026-10-17 03:48 UTC900
        assert_eq!(c.general, General::default());
b22c0511d5afatagen2026-10-17 03:48 UTC901
        assert_eq!(c.output, Outputs::default());
b22c0511d5afatagen2026-10-17 03:48 UTC902
        assert_eq!(c.decoration, Decoration::default());
b22c0511d5afatagen2026-10-17 03:48 UTC903
        assert_eq!(c.animation, Animation::default());
b22c0511d5afatagen2026-10-17 03:48 UTC904
        assert_eq!(c.keyboard, Keyboard::default());
9944e8618a62atagen2026-10-18 10:00 UTC905
        assert_eq!(c.input, Input::default());
47b86d08d05aatagen2026-10-19 07:37 UTC906
        assert_eq!(c.idle, Idle::default());
5b8422a225daatagen2026-10-19 07:45 UTC907
        assert_eq!(c.lid, Lid::default());
1df9b00ad0fdatagen2026-10-19 07:34 UTC908
        assert_eq!(c.xwayland, Xwayland::default());
fe835aed6c8datagen2026-10-16 10:05 UTC909
        assert!(c.binds.len() > 40);
fe835aed6c8datagen2026-10-16 10:05 UTC910
    }
8fb62aebf501atagen2026-10-17 10:25 UTC911
8fb62aebf501atagen2026-10-17 10:25 UTC912
    #[test]
fc167134b65datagen2026-10-19 08:09 UTC913
    fn output_scale_is_snapped_and_range_checked() {
fc167134b65datagen2026-10-19 08:09 UTC914
        let loaded = Config::parse(
fc167134b65datagen2026-10-19 08:09 UTC915
            r#"
fc167134b65datagen2026-10-19 08:09 UTC916
            [output.DP-1]
fc167134b65datagen2026-10-19 08:09 UTC917
            scale = 1.5
fc167134b65datagen2026-10-19 08:09 UTC918
            [output.DP-2]
fc167134b65datagen2026-10-19 08:09 UTC919
            scale = 1.3333
fc167134b65datagen2026-10-19 08:09 UTC920
            [output.DP-3]
fc167134b65datagen2026-10-19 08:09 UTC921
            scale = 9.0
fc167134b65datagen2026-10-19 08:09 UTC922
            [output.nested]
fc167134b65datagen2026-10-19 08:09 UTC923
            scale = 2.0
fc167134b65datagen2026-10-19 08:09 UTC924
            "#,
fc167134b65datagen2026-10-19 08:09 UTC925
        )
fc167134b65datagen2026-10-19 08:09 UTC926
        .unwrap();
fc167134b65datagen2026-10-19 08:09 UTC927
        let named = &loaded.config.output.named;
fc167134b65datagen2026-10-19 08:09 UTC928
        assert_eq!(named["DP-1"].scale(), 1.5);
fc167134b65datagen2026-10-19 08:09 UTC929
        assert_eq!(named["DP-2"].scale(), 160.0 / 120.0);
fc167134b65datagen2026-10-19 08:09 UTC930
        assert_eq!(named["DP-3"].scale(), 1.0);
fc167134b65datagen2026-10-19 08:09 UTC931
        assert_eq!(loaded.warnings.len(), 1, "{:#?}", loaded.warnings);
fc167134b65datagen2026-10-19 08:09 UTC932
    }
fc167134b65datagen2026-10-19 08:09 UTC933
fc167134b65datagen2026-10-19 08:09 UTC934
    #[test]
8fb62aebf501atagen2026-10-17 10:25 UTC935
    fn a_bad_option_costs_only_itself() {
8fb62aebf501atagen2026-10-17 10:25 UTC936
        let loaded = Config::parse(
8fb62aebf501atagen2026-10-17 10:25 UTC937
            r##"
8fb62aebf501atagen2026-10-17 10:25 UTC938
            [general]
8fb62aebf501atagen2026-10-17 10:25 UTC939
            gaps = { inner = 3, outer = 3 }
8fb62aebf501atagen2026-10-17 10:25 UTC940
            focus_follows_mouse = "yes"
8fb62aebf501atagen2026-10-17 10:25 UTC941
            typo = 1
8fb62aebf501atagen2026-10-17 10:25 UTC942
            [decoration]
8fb62aebf501atagen2026-10-17 10:25 UTC943
            radius = [1, 2, 3, 4]
8fb62aebf501atagen2026-10-17 10:25 UTC944
            focused = "#zzzzzz"
8fb62aebf501atagen2026-10-17 10:25 UTC945
            [binds]
3b180fd69834atagen2026-10-17 10:40 UTC946
            "Mod+Return" = "spawn beer"
8fb62aebf501atagen2026-10-17 10:25 UTC947
            "Hyper+x" = "close"
9b87063809e1atagen2026-10-17 13:28 UTC948
            [output.DP-1]
9b87063809e1atagen2026-10-17 13:28 UTC949
            transform = "90"
9b87063809e1atagen2026-10-17 13:28 UTC950
            position = [0, 0]
9b87063809e1atagen2026-10-17 13:28 UTC951
            [output.DP-2]
9b87063809e1atagen2026-10-17 13:28 UTC952
            transform = "sideways"
0f513a2420c0atagen2026-10-18 02:40 UTC953
            [output.DP1]
8fb62aebf501atagen2026-10-17 10:25 UTC954
            [[rules]]
8fb62aebf501atagen2026-10-17 10:25 UTC955
            app_id = "("
8fb62aebf501atagen2026-10-17 10:25 UTC956
            [[rules]]
8fb62aebf501atagen2026-10-17 10:25 UTC957
            app_id = "^mpv$"
8fb62aebf501atagen2026-10-17 10:25 UTC958
            floating = true
8fb62aebf501atagen2026-10-17 10:25 UTC959
            "##,
8fb62aebf501atagen2026-10-17 10:25 UTC960
        )
8fb62aebf501atagen2026-10-17 10:25 UTC961
        .unwrap();
8fb62aebf501atagen2026-10-17 10:25 UTC962
        let c = loaded.config;
8fb62aebf501atagen2026-10-17 10:25 UTC963
        assert_eq!(c.general.gaps.inner, 3.0);
8fb62aebf501atagen2026-10-17 10:25 UTC964
        assert!(!c.general.focus_follows_mouse);
8fb62aebf501atagen2026-10-17 10:25 UTC965
        assert_eq!(c.decoration.radius, Corners([1, 2, 3, 4]));
8fb62aebf501atagen2026-10-17 10:25 UTC966
        assert_eq!(c.decoration.focused, Decoration::default().focused);
8fb62aebf501atagen2026-10-17 10:25 UTC967
        assert_eq!((c.binds.len(), c.rules.len()), (1, 1));
9b87063809e1atagen2026-10-17 13:28 UTC968
        assert_eq!(c.output.named["DP-1"].transform, OutputTransform::Rotate90);
9b87063809e1atagen2026-10-17 13:28 UTC969
        assert!(!c.output.named.contains_key("DP-2"));
0f513a2420c0atagen2026-10-18 02:40 UTC970
        assert_eq!(loaded.warnings.len(), 7, "{:#?}", loaded.warnings);
8fb62aebf501atagen2026-10-17 10:25 UTC971
    }
9944e8618a62atagen2026-10-18 10:00 UTC972
9944e8618a62atagen2026-10-18 10:00 UTC973
    #[test]
a1e55dd5ed07atagen2026-10-19 08:36 UTC974
    fn broken_toml_costs_only_its_line() {
a1e55dd5ed07atagen2026-10-19 08:36 UTC975
        let loaded = Config::parse(
a1e55dd5ed07atagen2026-10-19 08:36 UTC976
            r##"
a1e55dd5ed07atagen2026-10-19 08:36 UTC977
            [general]
a1e55dd5ed07atagen2026-10-19 08:36 UTC978
            gaps = { inner = 3, outer = 3
a1e55dd5ed07atagen2026-10-19 08:36 UTC979
            focus_follows_mouse = true
a1e55dd5ed07atagen2026-10-19 08:36 UTC980
            focus_follows_mouse = false
a1e55dd5ed07atagen2026-10-19 08:36 UTC981
            [binds]
a1e55dd5ed07atagen2026-10-19 08:36 UTC982
            "Mod+Return" = "spawn beer
a1e55dd5ed07atagen2026-10-19 08:36 UTC983
            "Mod+q" = "close"
a1e55dd5ed07atagen2026-10-19 08:36 UTC984
            "##,
a1e55dd5ed07atagen2026-10-19 08:36 UTC985
        )
a1e55dd5ed07atagen2026-10-19 08:36 UTC986
        .unwrap();
a1e55dd5ed07atagen2026-10-19 08:36 UTC987
        let c = loaded.config;
a1e55dd5ed07atagen2026-10-19 08:36 UTC988
        assert_eq!(c.general.gaps, General::default().gaps);
a1e55dd5ed07atagen2026-10-19 08:36 UTC989
        assert_eq!(c.binds.len(), 1);
a1e55dd5ed07atagen2026-10-19 08:36 UTC990
        assert_eq!(loaded.warnings.len(), 3, "{:#?}", loaded.warnings);
a1e55dd5ed07atagen2026-10-19 08:36 UTC991
        assert!(
a1e55dd5ed07atagen2026-10-19 08:36 UTC992
            loaded.warnings[0].starts_with("line 3:"),
a1e55dd5ed07atagen2026-10-19 08:36 UTC993
            "{:#?}",
a1e55dd5ed07atagen2026-10-19 08:36 UTC994
            loaded.warnings
a1e55dd5ed07atagen2026-10-19 08:36 UTC995
        );
a1e55dd5ed07atagen2026-10-19 08:36 UTC996
    }
a1e55dd5ed07atagen2026-10-19 08:36 UTC997
a1e55dd5ed07atagen2026-10-19 08:36 UTC998
    #[test]
9944e8618a62atagen2026-10-18 10:00 UTC999
    fn a_bad_device_option_costs_only_itself() {
9944e8618a62atagen2026-10-18 10:00 UTC1000
        let loaded = Config::parse(
9944e8618a62atagen2026-10-18 10:00 UTC1001
            r#"
9944e8618a62atagen2026-10-18 10:00 UTC1002
            [input.pointer]
9944e8618a62atagen2026-10-18 10:00 UTC1003
            accel_speed = 2.0
9944e8618a62atagen2026-10-18 10:00 UTC1004
            scroll_method = "two-finger"
9944e8618a62atagen2026-10-18 10:00 UTC1005
            [[input.device]]
9944e8618a62atagen2026-10-18 10:00 UTC1006
            name = "Mouse"
9944e8618a62atagen2026-10-18 10:00 UTC1007
            accel_profile = "flat"
9944e8618a62atagen2026-10-18 10:00 UTC1008
            [[input.device]]
9944e8618a62atagen2026-10-18 10:00 UTC1009
            name = "Typo"
9944e8618a62atagen2026-10-18 10:00 UTC1010
            tapp = true
9944e8618a62atagen2026-10-18 10:00 UTC1011
            [[input.device]]
9944e8618a62atagen2026-10-18 10:00 UTC1012
            name = "Half"
9944e8618a62atagen2026-10-18 10:00 UTC1013
            accel_points = [0.0, 1.0]
9944e8618a62atagen2026-10-18 10:00 UTC1014
            "#,
9944e8618a62atagen2026-10-18 10:00 UTC1015
        )
9944e8618a62atagen2026-10-18 10:00 UTC1016
        .unwrap();
9944e8618a62atagen2026-10-18 10:00 UTC1017
        let input = loaded.config.input;
9944e8618a62atagen2026-10-18 10:00 UTC1018
        assert_eq!(input.pointer.accel_speed, None);
9944e8618a62atagen2026-10-18 10:00 UTC1019
        assert_eq!(input.pointer.scroll_method, Some(ScrollMethod::TwoFinger));
9944e8618a62atagen2026-10-18 10:00 UTC1020
        let names: Vec<_> = input.device.iter().map(|d| d.name.as_str()).collect();
9944e8618a62atagen2026-10-18 10:00 UTC1021
        assert_eq!(names, ["Mouse", "Typo", "Half"]);
9944e8618a62atagen2026-10-18 10:00 UTC1022
        assert_eq!(
9944e8618a62atagen2026-10-18 10:00 UTC1023
            input.device[0].settings.accel_profile,
9944e8618a62atagen2026-10-18 10:00 UTC1024
            Some(AccelProfile::Flat)
9944e8618a62atagen2026-10-18 10:00 UTC1025
        );
9944e8618a62atagen2026-10-18 10:00 UTC1026
        assert_eq!(input.device[1].settings, DeviceSettings::default());
9944e8618a62atagen2026-10-18 10:00 UTC1027
        assert_eq!(input.device[2].settings.accel_points, None);
9944e8618a62atagen2026-10-18 10:00 UTC1028
        assert_eq!(loaded.warnings.len(), 3, "{:#?}", loaded.warnings);
9944e8618a62atagen2026-10-18 10:00 UTC1029
    }
fe835aed6c8datagen2026-10-16 10:05 UTC1030
}