atagen

monolith

public
Star0
Watch0
509842c1b6dc0e9d48985cb5aa8d691a1b2301ee
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,
509842c1b6dcatagen2026-10-28 04:05 UTC43
    pub hide_cursor_on_type: bool,
45a383fca811atagen2026-10-17 11:31 UTC44
    /// The first window to land on or move to an empty tag nobody is
45a383fca811atagen2026-10-17 11:31 UTC45
    /// viewing brings that tag into view on its output.
47259c39793fatagen2026-10-17 10:13 UTC46
    pub view_tag_on_first_window: bool,
4444c779eb48atagen2026-10-16 12:07 UTC47
}
4444c779eb48atagen2026-10-16 12:07 UTC48
4444c779eb48atagen2026-10-16 12:07 UTC49
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
4444c779eb48atagen2026-10-16 12:07 UTC50
#[serde(rename_all = "lowercase")]
4444c779eb48atagen2026-10-16 12:07 UTC51
pub enum MoveMode {
4444c779eb48atagen2026-10-16 12:07 UTC52
    /// Trade places with it.
4444c779eb48atagen2026-10-16 12:07 UTC53
    #[default]
4444c779eb48atagen2026-10-16 12:07 UTC54
    Swap,
4444c779eb48atagen2026-10-16 12:07 UTC55
    /// Join its lane, stacking with it if it is alone.
4444c779eb48atagen2026-10-16 12:07 UTC56
    Merge,
fe835aed6c8datagen2026-10-16 10:05 UTC57
}
fe835aed6c8datagen2026-10-16 10:05 UTC58
fe835aed6c8datagen2026-10-16 10:05 UTC59
impl Default for General {
fe835aed6c8datagen2026-10-16 10:05 UTC60
    fn default() -> Self {
fe835aed6c8datagen2026-10-16 10:05 UTC61
        Self {
fe835aed6c8datagen2026-10-16 10:05 UTC62
            mod_key: Modifier::Super,
fe835aed6c8datagen2026-10-16 10:05 UTC63
            gaps: Gaps::default(),
86ef802692f5atagen2026-10-19 07:43 UTC64
            autotile: true,
4444c779eb48atagen2026-10-16 12:07 UTC65
            move_mode: MoveMode::Swap,
24fca1aad608atagen2026-10-17 09:56 UTC66
            focus_follows_mouse: false,
24fca1aad608atagen2026-10-17 09:56 UTC67
            mouse_follows_focus: false,
509842c1b6dcatagen2026-10-28 04:05 UTC68
            hide_cursor_on_type: false,
47259c39793fatagen2026-10-17 10:13 UTC69
            view_tag_on_first_window: true,
fe835aed6c8datagen2026-10-16 10:05 UTC70
        }
fe835aed6c8datagen2026-10-16 10:05 UTC71
    }
fe835aed6c8datagen2026-10-16 10:05 UTC72
}
fe835aed6c8datagen2026-10-16 10:05 UTC73
1df9b00ad0fdatagen2026-10-19 07:34 UTC74
#[derive(Deserialize, Debug, Clone, PartialEq)]
1df9b00ad0fdatagen2026-10-19 07:34 UTC75
#[serde(default, deny_unknown_fields)]
1df9b00ad0fdatagen2026-10-19 07:34 UTC76
pub struct Xwayland {
dd5b0783344datagen2026-10-19 10:51 UTC77
    /// Which X11 clients may read the Wayland selections and set them.
1df9b00ad0fdatagen2026-10-19 07:34 UTC78
    pub selection_access: SelectionAccess,
1df9b00ad0fdatagen2026-10-19 07:34 UTC79
}
1df9b00ad0fdatagen2026-10-19 07:34 UTC80
1df9b00ad0fdatagen2026-10-19 07:34 UTC81
impl Default for Xwayland {
1df9b00ad0fdatagen2026-10-19 07:34 UTC82
    fn default() -> Self {
1df9b00ad0fdatagen2026-10-19 07:34 UTC83
        Self {
1df9b00ad0fdatagen2026-10-19 07:34 UTC84
            selection_access: SelectionAccess::Focused,
1df9b00ad0fdatagen2026-10-19 07:34 UTC85
        }
1df9b00ad0fdatagen2026-10-19 07:34 UTC86
    }
1df9b00ad0fdatagen2026-10-19 07:34 UTC87
}
1df9b00ad0fdatagen2026-10-19 07:34 UTC88
1df9b00ad0fdatagen2026-10-19 07:34 UTC89
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1df9b00ad0fdatagen2026-10-19 07:34 UTC90
#[serde(rename_all = "lowercase")]
1df9b00ad0fdatagen2026-10-19 07:34 UTC91
pub enum SelectionAccess {
1df9b00ad0fdatagen2026-10-19 07:34 UTC92
    /// Only the X11 window holding keyboard focus.
1df9b00ad0fdatagen2026-10-19 07:34 UTC93
    Focused,
1df9b00ad0fdatagen2026-10-19 07:34 UTC94
    /// Every X11 client, which clipboard managers and headless tools need.
1df9b00ad0fdatagen2026-10-19 07:34 UTC95
    Any,
1df9b00ad0fdatagen2026-10-19 07:34 UTC96
}
1df9b00ad0fdatagen2026-10-19 07:34 UTC97
fe835aed6c8datagen2026-10-16 10:05 UTC98
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC99
#[serde(default, deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC100
pub struct Gaps {
fe835aed6c8datagen2026-10-16 10:05 UTC101
    pub inner: f64,
fe835aed6c8datagen2026-10-16 10:05 UTC102
    pub outer: f64,
fe835aed6c8datagen2026-10-16 10:05 UTC103
}
fe835aed6c8datagen2026-10-16 10:05 UTC104
fe835aed6c8datagen2026-10-16 10:05 UTC105
impl Default for Gaps {
fe835aed6c8datagen2026-10-16 10:05 UTC106
    fn default() -> Self {
fe835aed6c8datagen2026-10-16 10:05 UTC107
        Self {
fe835aed6c8datagen2026-10-16 10:05 UTC108
            inner: 8.0,
fe835aed6c8datagen2026-10-16 10:05 UTC109
            outer: 8.0,
fe835aed6c8datagen2026-10-16 10:05 UTC110
        }
fe835aed6c8datagen2026-10-16 10:05 UTC111
    }
fe835aed6c8datagen2026-10-16 10:05 UTC112
}
fe835aed6c8datagen2026-10-16 10:05 UTC113
8e6f96f0a32datagen2026-10-16 22:17 UTC114
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
9b87063809e1atagen2026-10-17 13:28 UTC115
#[serde(default)]
8e6f96f0a32datagen2026-10-16 22:17 UTC116
pub struct Outputs {
8e6f96f0a32datagen2026-10-16 22:17 UTC117
    pub vrr: Vrr,
fc167134b65datagen2026-10-19 08:09 UTC118
    /// Settings for one connector, by name, like `DP-1`, or `nested` for the
fc167134b65datagen2026-10-19 08:09 UTC119
    /// nested backend's window.
9b87063809e1atagen2026-10-17 13:28 UTC120
    #[serde(flatten)]
9b87063809e1atagen2026-10-17 13:28 UTC121
    pub named: std::collections::BTreeMap<String, OutputConfig>,
9b87063809e1atagen2026-10-17 13:28 UTC122
}
9b87063809e1atagen2026-10-17 13:28 UTC123
fc167134b65datagen2026-10-19 08:09 UTC124
/// Transform, position and scale follow a reload. A new mode waits for a
fc167134b65datagen2026-10-19 08:09 UTC125
/// restart or a replug, since it means a modeset.
9b87063809e1atagen2026-10-17 13:28 UTC126
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
9b87063809e1atagen2026-10-17 13:28 UTC127
#[serde(default, deny_unknown_fields)]
9b87063809e1atagen2026-10-17 13:28 UTC128
pub struct OutputConfig {
9b87063809e1atagen2026-10-17 13:28 UTC129
    pub transform: OutputTransform,
9b87063809e1atagen2026-10-17 13:28 UTC130
    /// Top left corner in the global layout. Outputs without one are placed
9b87063809e1atagen2026-10-17 13:28 UTC131
    /// to the right of the others.
9b87063809e1atagen2026-10-17 13:28 UTC132
    pub position: Option<[i32; 2]>,
9b87063809e1atagen2026-10-17 13:28 UTC133
    /// `WIDTHxHEIGHT` or `WIDTHxHEIGHT@HZ`. Without a rate the highest one
9b87063809e1atagen2026-10-17 13:28 UTC134
    /// for that size wins.
9b87063809e1atagen2026-10-17 13:28 UTC135
    pub mode: Option<String>,
fc167134b65datagen2026-10-19 08:09 UTC136
    /// How many physical pixels make a logical one, `0.5` to `4.0` in steps of
fc167134b65datagen2026-10-19 08:09 UTC137
    /// 1/120. Positions are in logical pixels.
fc167134b65datagen2026-10-19 08:09 UTC138
    pub scale: Option<f64>,
9b87063809e1atagen2026-10-17 13:28 UTC139
}
9b87063809e1atagen2026-10-17 13:28 UTC140
fc167134b65datagen2026-10-19 08:09 UTC141
impl OutputConfig {
fc167134b65datagen2026-10-19 08:09 UTC142
    pub fn scale(&self) -> f64 {
fc167134b65datagen2026-10-19 08:09 UTC143
        self.scale.unwrap_or(1.0)
fc167134b65datagen2026-10-19 08:09 UTC144
    }
fc167134b65datagen2026-10-19 08:09 UTC145
fc167134b65datagen2026-10-19 08:09 UTC146
    fn validate(&mut self, name: &str, w: &mut Vec<String>) {
fc167134b65datagen2026-10-19 08:09 UTC147
        let Some(s) = self.scale else {
fc167134b65datagen2026-10-19 08:09 UTC148
            return;
fc167134b65datagen2026-10-19 08:09 UTC149
        };
fc167134b65datagen2026-10-19 08:09 UTC150
        if !(MIN_SCALE..=MAX_SCALE).contains(&s) {
fc167134b65datagen2026-10-19 08:09 UTC151
            w.push(format!(
fc167134b65datagen2026-10-19 08:09 UTC152
                "{name}.scale: {s} is outside {MIN_SCALE} to {MAX_SCALE}, ignored"
fc167134b65datagen2026-10-19 08:09 UTC153
            ));
fc167134b65datagen2026-10-19 08:09 UTC154
            self.scale = None;
fc167134b65datagen2026-10-19 08:09 UTC155
        } else {
fc167134b65datagen2026-10-19 08:09 UTC156
            self.scale = Some((s * 120.0).round() / 120.0);
fc167134b65datagen2026-10-19 08:09 UTC157
        }
fc167134b65datagen2026-10-19 08:09 UTC158
    }
fc167134b65datagen2026-10-19 08:09 UTC159
}
fc167134b65datagen2026-10-19 08:09 UTC160
fc167134b65datagen2026-10-19 08:09 UTC161
const MIN_SCALE: f64 = 0.5;
fc167134b65datagen2026-10-19 08:09 UTC162
const MAX_SCALE: f64 = 4.0;
fc167134b65datagen2026-10-19 08:09 UTC163
9b87063809e1atagen2026-10-17 13:28 UTC164
/// Counterclockwise rotation, optionally after flipping horizontally.
9b87063809e1atagen2026-10-17 13:28 UTC165
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
9b87063809e1atagen2026-10-17 13:28 UTC166
#[serde(rename_all = "kebab-case")]
9b87063809e1atagen2026-10-17 13:28 UTC167
pub enum OutputTransform {
9b87063809e1atagen2026-10-17 13:28 UTC168
    #[default]
9b87063809e1atagen2026-10-17 13:28 UTC169
    Normal,
9b87063809e1atagen2026-10-17 13:28 UTC170
    #[serde(rename = "90")]
9b87063809e1atagen2026-10-17 13:28 UTC171
    Rotate90,
9b87063809e1atagen2026-10-17 13:28 UTC172
    #[serde(rename = "180")]
9b87063809e1atagen2026-10-17 13:28 UTC173
    Rotate180,
9b87063809e1atagen2026-10-17 13:28 UTC174
    #[serde(rename = "270")]
9b87063809e1atagen2026-10-17 13:28 UTC175
    Rotate270,
9b87063809e1atagen2026-10-17 13:28 UTC176
    Flipped,
9b87063809e1atagen2026-10-17 13:28 UTC177
    #[serde(rename = "flipped-90")]
9b87063809e1atagen2026-10-17 13:28 UTC178
    Flipped90,
9b87063809e1atagen2026-10-17 13:28 UTC179
    #[serde(rename = "flipped-180")]
9b87063809e1atagen2026-10-17 13:28 UTC180
    Flipped180,
9b87063809e1atagen2026-10-17 13:28 UTC181
    #[serde(rename = "flipped-270")]
9b87063809e1atagen2026-10-17 13:28 UTC182
    Flipped270,
8e6f96f0a32datagen2026-10-16 22:17 UTC183
}
8e6f96f0a32datagen2026-10-16 22:17 UTC184
8e6f96f0a32datagen2026-10-16 22:17 UTC185
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
8e6f96f0a32datagen2026-10-16 22:17 UTC186
#[serde(rename_all = "lowercase")]
8e6f96f0a32datagen2026-10-16 22:17 UTC187
pub enum Vrr {
8e6f96f0a32datagen2026-10-16 22:17 UTC188
    Off,
8e6f96f0a32datagen2026-10-16 22:17 UTC189
    On,
8e6f96f0a32datagen2026-10-16 22:17 UTC190
    /// Only while a fullscreen window is shown, where a game sets the pace.
8e6f96f0a32datagen2026-10-16 22:17 UTC191
    #[default]
8e6f96f0a32datagen2026-10-16 22:17 UTC192
    Fullscreen,
8e6f96f0a32datagen2026-10-16 22:17 UTC193
}
8e6f96f0a32datagen2026-10-16 22:17 UTC194
fe835aed6c8datagen2026-10-16 10:05 UTC195
#[derive(Deserialize, Debug, Clone, PartialEq)]
eacaaa02eca5atagen2026-10-22 05:38 UTC196
#[serde(default, deny_unknown_fields, rename_all = "kebab-case")]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC197
pub struct Decoration {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC198
    /// Border width in logical pixels, drawn in the gap around each window.
7ce6ae4e5a85atagen2026-10-16 12:16 UTC199
    pub border: u32,
47259c39793fatagen2026-10-17 10:13 UTC200
    pub radius: Corners,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC201
    pub focused: Color,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC202
    pub unfocused: Color,
1d62be4ff79catagen2026-10-19 07:37 UTC203
    pub urgent: Color,
eacaaa02eca5atagen2026-10-22 05:38 UTC204
    pub extended: Color,
eacaaa02eca5atagen2026-10-22 05:38 UTC205
    pub extended_focused: Color,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC206
}
7ce6ae4e5a85atagen2026-10-16 12:16 UTC207
47259c39793fatagen2026-10-17 10:13 UTC208
/// Corner radii in logical pixels, top left, top right, bottom right and
47259c39793fatagen2026-10-17 10:13 UTC209
/// bottom left. One number rounds every corner alike.
47259c39793fatagen2026-10-17 10:13 UTC210
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
47259c39793fatagen2026-10-17 10:13 UTC211
#[serde(from = "CornersToml")]
47259c39793fatagen2026-10-17 10:13 UTC212
pub struct Corners(pub [u32; 4]);
47259c39793fatagen2026-10-17 10:13 UTC213
47259c39793fatagen2026-10-17 10:13 UTC214
#[derive(Deserialize)]
8fb62aebf501atagen2026-10-17 10:25 UTC215
#[serde(
8fb62aebf501atagen2026-10-17 10:25 UTC216
    untagged,
8fb62aebf501atagen2026-10-17 10:25 UTC217
    expecting = "expected a number or [top left, top right, bottom right, bottom left]"
8fb62aebf501atagen2026-10-17 10:25 UTC218
)]
47259c39793fatagen2026-10-17 10:13 UTC219
enum CornersToml {
47259c39793fatagen2026-10-17 10:13 UTC220
    All(u32),
47259c39793fatagen2026-10-17 10:13 UTC221
    Each([u32; 4]),
47259c39793fatagen2026-10-17 10:13 UTC222
}
47259c39793fatagen2026-10-17 10:13 UTC223
47259c39793fatagen2026-10-17 10:13 UTC224
impl From<CornersToml> for Corners {
47259c39793fatagen2026-10-17 10:13 UTC225
    fn from(c: CornersToml) -> Self {
47259c39793fatagen2026-10-17 10:13 UTC226
        match c {
47259c39793fatagen2026-10-17 10:13 UTC227
            CornersToml::All(r) => Self([r; 4]),
47259c39793fatagen2026-10-17 10:13 UTC228
            CornersToml::Each(r) => Self(r),
47259c39793fatagen2026-10-17 10:13 UTC229
        }
47259c39793fatagen2026-10-17 10:13 UTC230
    }
47259c39793fatagen2026-10-17 10:13 UTC231
}
47259c39793fatagen2026-10-17 10:13 UTC232
7ce6ae4e5a85atagen2026-10-16 12:16 UTC233
impl Default for Decoration {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC234
    fn default() -> Self {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC235
        Self {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC236
            border: 2,
47259c39793fatagen2026-10-17 10:13 UTC237
            radius: Corners([0; 4]),
b22c0511d5afatagen2026-10-17 03:48 UTC238
            focused: Color::rgb(0x88, 0xbf, 0xd1),
b22c0511d5afatagen2026-10-17 03:48 UTC239
            unfocused: Color::rgb(0x3b, 0x42, 0x52),
1d62be4ff79catagen2026-10-19 07:37 UTC240
            urgent: Color::rgb(0xeb, 0xcb, 0x8b),
eacaaa02eca5atagen2026-10-22 05:38 UTC241
            extended: Color::rgb(0x5e, 0x4b, 0x6e),
eacaaa02eca5atagen2026-10-22 05:38 UTC242
            extended_focused: Color::rgb(0xb4, 0x8e, 0xad),
7ce6ae4e5a85atagen2026-10-16 12:16 UTC243
        }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC244
    }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC245
}
7ce6ae4e5a85atagen2026-10-16 12:16 UTC246
7ce6ae4e5a85atagen2026-10-16 12:16 UTC247
/// Straight RGBA from `#rrggbb` or `#rrggbbaa`.
7ce6ae4e5a85atagen2026-10-16 12:16 UTC248
#[derive(Debug, Clone, Copy, PartialEq)]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC249
pub struct Color(pub [f32; 4]);
7ce6ae4e5a85atagen2026-10-16 12:16 UTC250
b22c0511d5afatagen2026-10-17 03:48 UTC251
impl Color {
b22c0511d5afatagen2026-10-17 03:48 UTC252
    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
b22c0511d5afatagen2026-10-17 03:48 UTC253
        Self([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
b22c0511d5afatagen2026-10-17 03:48 UTC254
    }
b22c0511d5afatagen2026-10-17 03:48 UTC255
}
b22c0511d5afatagen2026-10-17 03:48 UTC256
7ce6ae4e5a85atagen2026-10-16 12:16 UTC257
impl<'de> Deserialize<'de> for Color {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC258
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC259
        let s = String::deserialize(d)?;
7ce6ae4e5a85atagen2026-10-16 12:16 UTC260
        let hex = s.strip_prefix('#').unwrap_or(&s);
7ce6ae4e5a85atagen2026-10-16 12:16 UTC261
        let bad =
7ce6ae4e5a85atagen2026-10-16 12:16 UTC262
            || serde::de::Error::custom(format!("bad colour {s:?}, use #rrggbb or #rrggbbaa"));
7ce6ae4e5a85atagen2026-10-16 12:16 UTC263
        if !matches!(hex.len(), 6 | 8) || !hex.is_ascii() {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC264
            return Err(bad());
7ce6ae4e5a85atagen2026-10-16 12:16 UTC265
        }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC266
        let mut c = [1.0f32; 4];
7ce6ae4e5a85atagen2026-10-16 12:16 UTC267
        for (i, v) in c.iter_mut().enumerate().take(hex.len() / 2) {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC268
            let byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| bad())?;
7ce6ae4e5a85atagen2026-10-16 12:16 UTC269
            *v = f32::from(byte) / 255.0;
7ce6ae4e5a85atagen2026-10-16 12:16 UTC270
        }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC271
        Ok(Self(c))
7ce6ae4e5a85atagen2026-10-16 12:16 UTC272
    }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC273
}
7ce6ae4e5a85atagen2026-10-16 12:16 UTC274
7ce6ae4e5a85atagen2026-10-16 12:16 UTC275
#[derive(Deserialize, Debug, Clone, PartialEq)]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC276
#[serde(default, deny_unknown_fields)]
2af1d8c55876atagen2026-10-16 11:05 UTC277
pub struct Animation {
2af1d8c55876atagen2026-10-16 11:05 UTC278
    pub enabled: bool,
2af1d8c55876atagen2026-10-16 11:05 UTC279
    pub style: AnimationStyle,
2af1d8c55876atagen2026-10-16 11:05 UTC280
    /// Every phase of a planned transition lasts this long.
2af1d8c55876atagen2026-10-16 11:05 UTC281
    pub phase_ms: u64,
2af1d8c55876atagen2026-10-16 11:05 UTC282
    /// A cubic bezier, as in CSS.
2af1d8c55876atagen2026-10-16 11:05 UTC283
    pub curve: [f64; 4],
2af1d8c55876atagen2026-10-16 11:05 UTC284
}
2af1d8c55876atagen2026-10-16 11:05 UTC285
2af1d8c55876atagen2026-10-16 11:05 UTC286
impl Default for Animation {
2af1d8c55876atagen2026-10-16 11:05 UTC287
    fn default() -> Self {
2af1d8c55876atagen2026-10-16 11:05 UTC288
        Self {
2af1d8c55876atagen2026-10-16 11:05 UTC289
            enabled: true,
2af1d8c55876atagen2026-10-16 11:05 UTC290
            style: AnimationStyle::Orthogonal,
2af1d8c55876atagen2026-10-16 11:05 UTC291
            phase_ms: 160,
2af1d8c55876atagen2026-10-16 11:05 UTC292
            curve: [0.45, 0.0, 0.2, 1.0],
2af1d8c55876atagen2026-10-16 11:05 UTC293
        }
2af1d8c55876atagen2026-10-16 11:05 UTC294
    }
2af1d8c55876atagen2026-10-16 11:05 UTC295
}
2af1d8c55876atagen2026-10-16 11:05 UTC296
2af1d8c55876atagen2026-10-16 11:05 UTC297
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
2af1d8c55876atagen2026-10-16 11:05 UTC298
#[serde(rename_all = "lowercase")]
2af1d8c55876atagen2026-10-16 11:05 UTC299
pub enum AnimationStyle {
2af1d8c55876atagen2026-10-16 11:05 UTC300
    Orthogonal,
2af1d8c55876atagen2026-10-16 11:05 UTC301
    Direct,
2af1d8c55876atagen2026-10-16 11:05 UTC302
}
2af1d8c55876atagen2026-10-16 11:05 UTC303
47b86d08d05aatagen2026-10-19 07:37 UTC304
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
47b86d08d05aatagen2026-10-19 07:37 UTC305
#[serde(default, deny_unknown_fields)]
47b86d08d05aatagen2026-10-19 07:37 UTC306
pub struct Idle {
47b86d08d05aatagen2026-10-19 07:37 UTC307
    /// Seconds without input before every output is switched off, 0 for
47b86d08d05aatagen2026-10-19 07:37 UTC308
    /// never.
47b86d08d05aatagen2026-10-19 07:37 UTC309
    pub blank_after: u64,
47b86d08d05aatagen2026-10-19 07:37 UTC310
}
47b86d08d05aatagen2026-10-19 07:37 UTC311
5b8422a225daatagen2026-10-19 07:45 UTC312
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
5b8422a225daatagen2026-10-19 07:45 UTC313
#[serde(default, deny_unknown_fields)]
5b8422a225daatagen2026-10-19 07:45 UTC314
pub struct Lid {
5b8422a225daatagen2026-10-19 07:45 UTC315
    /// What closing the lid does while no other output is connected.
5b8422a225daatagen2026-10-19 07:45 UTC316
    pub close: LidAction,
5b8422a225daatagen2026-10-19 07:45 UTC317
}
5b8422a225daatagen2026-10-19 07:45 UTC318
5b8422a225daatagen2026-10-19 07:45 UTC319
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
5b8422a225daatagen2026-10-19 07:45 UTC320
#[serde(rename_all = "lowercase")]
5b8422a225daatagen2026-10-19 07:45 UTC321
pub enum LidAction {
5b8422a225daatagen2026-10-19 07:45 UTC322
    #[default]
5b8422a225daatagen2026-10-19 07:45 UTC323
    Suspend,
5b8422a225daatagen2026-10-19 07:45 UTC324
    Poweroff,
5b8422a225daatagen2026-10-19 07:45 UTC325
    /// Leave it to logind or whatever else watches the switch.
5b8422a225daatagen2026-10-19 07:45 UTC326
    Ignore,
5b8422a225daatagen2026-10-19 07:45 UTC327
}
5b8422a225daatagen2026-10-19 07:45 UTC328
2af1d8c55876atagen2026-10-16 11:05 UTC329
#[derive(Deserialize, Debug, Clone, PartialEq)]
2af1d8c55876atagen2026-10-16 11:05 UTC330
#[serde(default, deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC331
pub struct Keyboard {
fe835aed6c8datagen2026-10-16 10:05 UTC332
    pub rules: String,
fe835aed6c8datagen2026-10-16 10:05 UTC333
    pub model: String,
fe835aed6c8datagen2026-10-16 10:05 UTC334
    pub layout: String,
fe835aed6c8datagen2026-10-16 10:05 UTC335
    pub variant: String,
fe835aed6c8datagen2026-10-16 10:05 UTC336
    pub options: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC337
    pub repeat_delay: i32,
fe835aed6c8datagen2026-10-16 10:05 UTC338
    pub repeat_rate: i32,
fe835aed6c8datagen2026-10-16 10:05 UTC339
}
fe835aed6c8datagen2026-10-16 10:05 UTC340
fe835aed6c8datagen2026-10-16 10:05 UTC341
impl Default for Keyboard {
fe835aed6c8datagen2026-10-16 10:05 UTC342
    fn default() -> Self {
fe835aed6c8datagen2026-10-16 10:05 UTC343
        Self {
fe835aed6c8datagen2026-10-16 10:05 UTC344
            rules: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC345
            model: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC346
            layout: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC347
            variant: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC348
            options: None,
fe835aed6c8datagen2026-10-16 10:05 UTC349
            repeat_delay: 200,
fe835aed6c8datagen2026-10-16 10:05 UTC350
            repeat_rate: 25,
fe835aed6c8datagen2026-10-16 10:05 UTC351
        }
fe835aed6c8datagen2026-10-16 10:05 UTC352
    }
fe835aed6c8datagen2026-10-16 10:05 UTC353
}
fe835aed6c8datagen2026-10-16 10:05 UTC354
9944e8618a62atagen2026-10-18 10:00 UTC355
/// libinput settings for pointing devices. Unset options keep libinput's
9944e8618a62atagen2026-10-18 10:00 UTC356
/// default for the device.
9944e8618a62atagen2026-10-18 10:00 UTC357
#[derive(Debug, Clone, PartialEq, Default)]
9944e8618a62atagen2026-10-18 10:00 UTC358
pub struct Input {
9944e8618a62atagen2026-10-18 10:00 UTC359
    pub pointer: DeviceSettings,
9944e8618a62atagen2026-10-18 10:00 UTC360
    /// Devices that can tap, which libinput only offers on touchpads.
9944e8618a62atagen2026-10-18 10:00 UTC361
    pub touchpad: DeviceSettings,
9944e8618a62atagen2026-10-18 10:00 UTC362
    /// Overrides for devices by name, later entries winning.
9944e8618a62atagen2026-10-18 10:00 UTC363
    pub device: Vec<NamedDevice>,
9944e8618a62atagen2026-10-18 10:00 UTC364
}
9944e8618a62atagen2026-10-18 10:00 UTC365
9944e8618a62atagen2026-10-18 10:00 UTC366
#[derive(Debug, Clone, PartialEq)]
9944e8618a62atagen2026-10-18 10:00 UTC367
pub struct NamedDevice {
9944e8618a62atagen2026-10-18 10:00 UTC368
    pub name: String,
9944e8618a62atagen2026-10-18 10:00 UTC369
    pub settings: DeviceSettings,
9944e8618a62atagen2026-10-18 10:00 UTC370
}
9944e8618a62atagen2026-10-18 10:00 UTC371
9944e8618a62atagen2026-10-18 10:00 UTC372
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
9944e8618a62atagen2026-10-18 10:00 UTC373
#[serde(default, deny_unknown_fields)]
9944e8618a62atagen2026-10-18 10:00 UTC374
pub struct DeviceSettings {
9944e8618a62atagen2026-10-18 10:00 UTC375
    pub accel_profile: Option<AccelProfile>,
9944e8618a62atagen2026-10-18 10:00 UTC376
    pub accel_speed: Option<f64>,
9944e8618a62atagen2026-10-18 10:00 UTC377
    /// The custom profile's curve, pointer speed sampled every `accel_step`
9944e8618a62atagen2026-10-18 10:00 UTC378
    /// device units per millisecond.
9944e8618a62atagen2026-10-18 10:00 UTC379
    pub accel_points: Option<Vec<f64>>,
9944e8618a62atagen2026-10-18 10:00 UTC380
    pub accel_step: Option<f64>,
9944e8618a62atagen2026-10-18 10:00 UTC381
    pub natural_scroll: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC382
    pub left_handed: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC383
    pub tap: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC384
    pub tap_drag: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC385
    pub tap_drag_lock: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC386
    pub dwt: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC387
    pub middle_emulation: Option<bool>,
9944e8618a62atagen2026-10-18 10:00 UTC388
    pub scroll_method: Option<ScrollMethod>,
9944e8618a62atagen2026-10-18 10:00 UTC389
    pub scroll_button: Option<u32>,
9944e8618a62atagen2026-10-18 10:00 UTC390
    pub click_method: Option<ClickMethod>,
9944e8618a62atagen2026-10-18 10:00 UTC391
    /// Applied by the compositor, libinput has no such setting.
9944e8618a62atagen2026-10-18 10:00 UTC392
    pub scroll_factor: Option<f64>,
9944e8618a62atagen2026-10-18 10:00 UTC393
}
9944e8618a62atagen2026-10-18 10:00 UTC394
9944e8618a62atagen2026-10-18 10:00 UTC395
impl DeviceSettings {
9944e8618a62atagen2026-10-18 10:00 UTC396
    /// `self` with every option `over` sets replaced.
9944e8618a62atagen2026-10-18 10:00 UTC397
    pub fn overlay(&self, over: &Self) -> Self {
9944e8618a62atagen2026-10-18 10:00 UTC398
        Self {
9944e8618a62atagen2026-10-18 10:00 UTC399
            accel_profile: over.accel_profile.or(self.accel_profile),
9944e8618a62atagen2026-10-18 10:00 UTC400
            accel_speed: over.accel_speed.or(self.accel_speed),
9944e8618a62atagen2026-10-18 10:00 UTC401
            accel_points: over
9944e8618a62atagen2026-10-18 10:00 UTC402
                .accel_points
9944e8618a62atagen2026-10-18 10:00 UTC403
                .clone()
9944e8618a62atagen2026-10-18 10:00 UTC404
                .or_else(|| self.accel_points.clone()),
9944e8618a62atagen2026-10-18 10:00 UTC405
            accel_step: over.accel_step.or(self.accel_step),
9944e8618a62atagen2026-10-18 10:00 UTC406
            natural_scroll: over.natural_scroll.or(self.natural_scroll),
9944e8618a62atagen2026-10-18 10:00 UTC407
            left_handed: over.left_handed.or(self.left_handed),
9944e8618a62atagen2026-10-18 10:00 UTC408
            tap: over.tap.or(self.tap),
9944e8618a62atagen2026-10-18 10:00 UTC409
            tap_drag: over.tap_drag.or(self.tap_drag),
9944e8618a62atagen2026-10-18 10:00 UTC410
            tap_drag_lock: over.tap_drag_lock.or(self.tap_drag_lock),
9944e8618a62atagen2026-10-18 10:00 UTC411
            dwt: over.dwt.or(self.dwt),
9944e8618a62atagen2026-10-18 10:00 UTC412
            middle_emulation: over.middle_emulation.or(self.middle_emulation),
9944e8618a62atagen2026-10-18 10:00 UTC413
            scroll_method: over.scroll_method.or(self.scroll_method),
9944e8618a62atagen2026-10-18 10:00 UTC414
            scroll_button: over.scroll_button.or(self.scroll_button),
9944e8618a62atagen2026-10-18 10:00 UTC415
            click_method: over.click_method.or(self.click_method),
9944e8618a62atagen2026-10-18 10:00 UTC416
            scroll_factor: over.scroll_factor.or(self.scroll_factor),
9944e8618a62atagen2026-10-18 10:00 UTC417
        }
9944e8618a62atagen2026-10-18 10:00 UTC418
    }
9944e8618a62atagen2026-10-18 10:00 UTC419
9944e8618a62atagen2026-10-18 10:00 UTC420
    fn validate(&mut self, name: &str, w: &mut Vec<String>) {
9944e8618a62atagen2026-10-18 10:00 UTC421
        if let Some(s) = self.accel_speed
9944e8618a62atagen2026-10-18 10:00 UTC422
            && !(-1.0..=1.0).contains(&s)
9944e8618a62atagen2026-10-18 10:00 UTC423
        {
9944e8618a62atagen2026-10-18 10:00 UTC424
            w.push(format!(
9944e8618a62atagen2026-10-18 10:00 UTC425
                "{name}.accel_speed: {s} is outside -1 to 1, ignored"
9944e8618a62atagen2026-10-18 10:00 UTC426
            ));
9944e8618a62atagen2026-10-18 10:00 UTC427
            self.accel_speed = None;
9944e8618a62atagen2026-10-18 10:00 UTC428
        }
9944e8618a62atagen2026-10-18 10:00 UTC429
        if let Some(f) = self.scroll_factor
9944e8618a62atagen2026-10-18 10:00 UTC430
            && !(f.is_finite() && f >= 0.0)
9944e8618a62atagen2026-10-18 10:00 UTC431
        {
9944e8618a62atagen2026-10-18 10:00 UTC432
            w.push(format!(
9944e8618a62atagen2026-10-18 10:00 UTC433
                "{name}.scroll_factor: {f} must be zero or more, ignored"
9944e8618a62atagen2026-10-18 10:00 UTC434
            ));
9944e8618a62atagen2026-10-18 10:00 UTC435
            self.scroll_factor = None;
9944e8618a62atagen2026-10-18 10:00 UTC436
        }
9944e8618a62atagen2026-10-18 10:00 UTC437
        if self.accel_points.is_some() != self.accel_step.is_some() {
9944e8618a62atagen2026-10-18 10:00 UTC438
            w.push(format!(
9944e8618a62atagen2026-10-18 10:00 UTC439
                "{name}: accel_points needs accel_step and the other way round, both ignored"
9944e8618a62atagen2026-10-18 10:00 UTC440
            ));
9944e8618a62atagen2026-10-18 10:00 UTC441
            self.accel_points = None;
9944e8618a62atagen2026-10-18 10:00 UTC442
            self.accel_step = None;
9944e8618a62atagen2026-10-18 10:00 UTC443
        }
9944e8618a62atagen2026-10-18 10:00 UTC444
    }
9944e8618a62atagen2026-10-18 10:00 UTC445
}
9944e8618a62atagen2026-10-18 10:00 UTC446
9944e8618a62atagen2026-10-18 10:00 UTC447
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
9944e8618a62atagen2026-10-18 10:00 UTC448
#[serde(rename_all = "lowercase")]
9944e8618a62atagen2026-10-18 10:00 UTC449
pub enum AccelProfile {
9944e8618a62atagen2026-10-18 10:00 UTC450
    Flat,
9944e8618a62atagen2026-10-18 10:00 UTC451
    Adaptive,
9944e8618a62atagen2026-10-18 10:00 UTC452
    Custom,
9944e8618a62atagen2026-10-18 10:00 UTC453
}
9944e8618a62atagen2026-10-18 10:00 UTC454
9944e8618a62atagen2026-10-18 10:00 UTC455
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
9944e8618a62atagen2026-10-18 10:00 UTC456
#[serde(rename_all = "kebab-case")]
9944e8618a62atagen2026-10-18 10:00 UTC457
pub enum ScrollMethod {
9944e8618a62atagen2026-10-18 10:00 UTC458
    None,
9944e8618a62atagen2026-10-18 10:00 UTC459
    TwoFinger,
9944e8618a62atagen2026-10-18 10:00 UTC460
    Edge,
9944e8618a62atagen2026-10-18 10:00 UTC461
    OnButtonDown,
9944e8618a62atagen2026-10-18 10:00 UTC462
}
9944e8618a62atagen2026-10-18 10:00 UTC463
9944e8618a62atagen2026-10-18 10:00 UTC464
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
9944e8618a62atagen2026-10-18 10:00 UTC465
#[serde(rename_all = "kebab-case")]
9944e8618a62atagen2026-10-18 10:00 UTC466
pub enum ClickMethod {
9944e8618a62atagen2026-10-18 10:00 UTC467
    ButtonAreas,
9944e8618a62atagen2026-10-18 10:00 UTC468
    Clickfinger,
9944e8618a62atagen2026-10-18 10:00 UTC469
}
9944e8618a62atagen2026-10-18 10:00 UTC470
fe835aed6c8datagen2026-10-16 10:05 UTC471
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
fe835aed6c8datagen2026-10-16 10:05 UTC472
#[serde(rename_all = "lowercase")]
fe835aed6c8datagen2026-10-16 10:05 UTC473
pub enum Modifier {
fe835aed6c8datagen2026-10-16 10:05 UTC474
    Super,
fe835aed6c8datagen2026-10-16 10:05 UTC475
    Alt,
fe835aed6c8datagen2026-10-16 10:05 UTC476
    Ctrl,
fe835aed6c8datagen2026-10-16 10:05 UTC477
    Shift,
fe835aed6c8datagen2026-10-16 10:05 UTC478
}
fe835aed6c8datagen2026-10-16 10:05 UTC479
fe835aed6c8datagen2026-10-16 10:05 UTC480
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
fe835aed6c8datagen2026-10-16 10:05 UTC481
pub struct Mods {
fe835aed6c8datagen2026-10-16 10:05 UTC482
    pub logo: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC483
    pub alt: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC484
    pub ctrl: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC485
    pub shift: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC486
}
fe835aed6c8datagen2026-10-16 10:05 UTC487
fe835aed6c8datagen2026-10-16 10:05 UTC488
impl Mods {
b22c0511d5afatagen2026-10-17 03:48 UTC489
    pub fn has(&self, m: Modifier) -> bool {
b22c0511d5afatagen2026-10-17 03:48 UTC490
        match m {
b22c0511d5afatagen2026-10-17 03:48 UTC491
            Modifier::Super => self.logo,
b22c0511d5afatagen2026-10-17 03:48 UTC492
            Modifier::Alt => self.alt,
b22c0511d5afatagen2026-10-17 03:48 UTC493
            Modifier::Ctrl => self.ctrl,
b22c0511d5afatagen2026-10-17 03:48 UTC494
            Modifier::Shift => self.shift,
b22c0511d5afatagen2026-10-17 03:48 UTC495
        }
b22c0511d5afatagen2026-10-17 03:48 UTC496
    }
b22c0511d5afatagen2026-10-17 03:48 UTC497
fe835aed6c8datagen2026-10-16 10:05 UTC498
    fn set(&mut self, m: Modifier) {
fe835aed6c8datagen2026-10-16 10:05 UTC499
        match m {
fe835aed6c8datagen2026-10-16 10:05 UTC500
            Modifier::Super => self.logo = true,
fe835aed6c8datagen2026-10-16 10:05 UTC501
            Modifier::Alt => self.alt = true,
fe835aed6c8datagen2026-10-16 10:05 UTC502
            Modifier::Ctrl => self.ctrl = true,
fe835aed6c8datagen2026-10-16 10:05 UTC503
            Modifier::Shift => self.shift = true,
fe835aed6c8datagen2026-10-16 10:05 UTC504
        }
fe835aed6c8datagen2026-10-16 10:05 UTC505
    }
fe835aed6c8datagen2026-10-16 10:05 UTC506
}
fe835aed6c8datagen2026-10-16 10:05 UTC507
fe835aed6c8datagen2026-10-16 10:05 UTC508
#[derive(Debug, Clone, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC509
pub struct Bind {
fe835aed6c8datagen2026-10-16 10:05 UTC510
    pub mods: Mods,
fe835aed6c8datagen2026-10-16 10:05 UTC511
    /// An xkb keysym name, resolved by the compositor.
fe835aed6c8datagen2026-10-16 10:05 UTC512
    pub key: String,
fe835aed6c8datagen2026-10-16 10:05 UTC513
    pub command: Command,
fe835aed6c8datagen2026-10-16 10:05 UTC514
}
fe835aed6c8datagen2026-10-16 10:05 UTC515
fe835aed6c8datagen2026-10-16 10:05 UTC516
#[derive(Debug, Clone)]
fe835aed6c8datagen2026-10-16 10:05 UTC517
pub struct Rule {
f4c93ddeb08fatagen2026-10-19 08:41 UTC518
    pub when: Matcher,
f4c93ddeb08fatagen2026-10-19 08:41 UTC519
    pub set: Placement,
f4c93ddeb08fatagen2026-10-19 08:41 UTC520
}
f4c93ddeb08fatagen2026-10-19 08:41 UTC521
f4c93ddeb08fatagen2026-10-19 08:41 UTC522
/// What a rule asks of a window. Every matcher given must hit.
f4c93ddeb08fatagen2026-10-19 08:41 UTC523
#[derive(Debug, Clone, Default)]
f4c93ddeb08fatagen2026-10-19 08:41 UTC524
pub struct Matcher {
fe835aed6c8datagen2026-10-16 10:05 UTC525
    pub app_id: Option<Regex>,
fe835aed6c8datagen2026-10-16 10:05 UTC526
    pub title: Option<Regex>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC527
    pub initial_title: Option<Regex>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC528
    pub class: Option<Regex>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC529
    pub instance: Option<Regex>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC530
    pub xwayland: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC531
    pub dialog: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC532
    pub parent_app_id: Option<Regex>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC533
}
f4c93ddeb08fatagen2026-10-19 08:41 UTC534
f4c93ddeb08fatagen2026-10-19 08:41 UTC535
/// What the rules can see of a window. `class` and `instance` are None for
f4c93ddeb08fatagen2026-10-19 08:41 UTC536
/// windows that are not X11, and `parent_app_id` for those without a parent.
f4c93ddeb08fatagen2026-10-19 08:41 UTC537
#[derive(Debug, Clone, Copy, Default)]
f4c93ddeb08fatagen2026-10-19 08:41 UTC538
pub struct Subject<'a> {
f4c93ddeb08fatagen2026-10-19 08:41 UTC539
    pub app_id: &'a str,
f4c93ddeb08fatagen2026-10-19 08:41 UTC540
    pub title: &'a str,
f4c93ddeb08fatagen2026-10-19 08:41 UTC541
    pub initial_title: &'a str,
f4c93ddeb08fatagen2026-10-19 08:41 UTC542
    pub class: Option<&'a str>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC543
    pub instance: Option<&'a str>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC544
    pub xwayland: bool,
f4c93ddeb08fatagen2026-10-19 08:41 UTC545
    pub dialog: bool,
f4c93ddeb08fatagen2026-10-19 08:41 UTC546
    pub parent_app_id: Option<&'a str>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC547
}
f4c93ddeb08fatagen2026-10-19 08:41 UTC548
f4c93ddeb08fatagen2026-10-19 08:41 UTC549
impl Matcher {
f4c93ddeb08fatagen2026-10-19 08:41 UTC550
    fn hits(&self, s: &Subject) -> bool {
f4c93ddeb08fatagen2026-10-19 08:41 UTC551
        fn re(re: &Option<Regex>, s: Option<&str>) -> bool {
f4c93ddeb08fatagen2026-10-19 08:41 UTC552
            re.as_ref()
f4c93ddeb08fatagen2026-10-19 08:41 UTC553
                .is_none_or(|re| s.is_some_and(|s| re.is_match(s)))
f4c93ddeb08fatagen2026-10-19 08:41 UTC554
        }
f4c93ddeb08fatagen2026-10-19 08:41 UTC555
        re(&self.app_id, Some(s.app_id))
f4c93ddeb08fatagen2026-10-19 08:41 UTC556
            && re(&self.title, Some(s.title))
f4c93ddeb08fatagen2026-10-19 08:41 UTC557
            && re(&self.initial_title, Some(s.initial_title))
f4c93ddeb08fatagen2026-10-19 08:41 UTC558
            && re(&self.class, s.class)
f4c93ddeb08fatagen2026-10-19 08:41 UTC559
            && re(&self.instance, s.instance)
f4c93ddeb08fatagen2026-10-19 08:41 UTC560
            && re(&self.parent_app_id, s.parent_app_id)
f4c93ddeb08fatagen2026-10-19 08:41 UTC561
            && self.xwayland.is_none_or(|x| x == s.xwayland)
f4c93ddeb08fatagen2026-10-19 08:41 UTC562
            && self.dialog.is_none_or(|d| d == s.dialog)
f4c93ddeb08fatagen2026-10-19 08:41 UTC563
    }
f4c93ddeb08fatagen2026-10-19 08:41 UTC564
}
f4c93ddeb08fatagen2026-10-19 08:41 UTC565
f4c93ddeb08fatagen2026-10-19 08:41 UTC566
/// Where a rule puts a floating window.
f4c93ddeb08fatagen2026-10-19 08:41 UTC567
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
f4c93ddeb08fatagen2026-10-19 08:41 UTC568
pub enum At {
f4c93ddeb08fatagen2026-10-19 08:41 UTC569
    /// From the top left of the output's area.
f4c93ddeb08fatagen2026-10-19 08:41 UTC570
    Position(i32, i32),
f4c93ddeb08fatagen2026-10-19 08:41 UTC571
    Center,
f4c93ddeb08fatagen2026-10-19 08:41 UTC572
}
f4c93ddeb08fatagen2026-10-19 08:41 UTC573
f4c93ddeb08fatagen2026-10-19 08:41 UTC574
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
f4c93ddeb08fatagen2026-10-19 08:41 UTC575
#[serde(rename_all = "lowercase")]
f4c93ddeb08fatagen2026-10-19 08:41 UTC576
pub enum IdleInhibit {
f4c93ddeb08fatagen2026-10-19 08:41 UTC577
    /// While the window is on screen.
f4c93ddeb08fatagen2026-10-19 08:41 UTC578
    Visible,
f4c93ddeb08fatagen2026-10-19 08:41 UTC579
    Fullscreen,
fe835aed6c8datagen2026-10-16 10:05 UTC580
}
fe835aed6c8datagen2026-10-16 10:05 UTC581
fe835aed6c8datagen2026-10-16 10:05 UTC582
/// Everything the matching rules say about a window, later rules winning.
fe835aed6c8datagen2026-10-16 10:05 UTC583
#[derive(Debug, Clone, Default, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC584
pub struct Placement {
fe835aed6c8datagen2026-10-16 10:05 UTC585
    pub tag: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC586
    pub output: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC587
    pub floating: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC588
    pub sticky: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC589
    pub scratchpad: Option<String>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC590
    pub size: Option<(u32, u32)>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC591
    pub at: Option<At>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC592
    /// A zero axis puts no lower bound on it.
f4c93ddeb08fatagen2026-10-19 08:41 UTC593
    pub min_size: Option<(u32, u32)>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC594
    /// A zero axis puts no upper bound on it.
f4c93ddeb08fatagen2026-10-19 08:41 UTC595
    pub max_size: Option<(u32, u32)>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC596
    pub fullscreen: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC597
    pub focus: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC598
    pub opacity: Option<f32>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC599
    pub idle_inhibit: Option<IdleInhibit>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC600
    pub vrr: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC601
    /// Inert until tearing presentation exists.
f4c93ddeb08fatagen2026-10-19 08:41 UTC602
    pub tearing: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC603
}
f4c93ddeb08fatagen2026-10-19 08:41 UTC604
f4c93ddeb08fatagen2026-10-19 08:41 UTC605
impl Placement {
f4c93ddeb08fatagen2026-10-19 08:41 UTC606
    fn over(&mut self, r: &Placement) {
f4c93ddeb08fatagen2026-10-19 08:41 UTC607
        self.tag = r.tag.clone().or(self.tag.take());
f4c93ddeb08fatagen2026-10-19 08:41 UTC608
        self.output = r.output.clone().or(self.output.take());
f4c93ddeb08fatagen2026-10-19 08:41 UTC609
        self.floating = r.floating.or(self.floating);
f4c93ddeb08fatagen2026-10-19 08:41 UTC610
        self.sticky = r.sticky.or(self.sticky);
f4c93ddeb08fatagen2026-10-19 08:41 UTC611
        self.scratchpad = r.scratchpad.clone().or(self.scratchpad.take());
f4c93ddeb08fatagen2026-10-19 08:41 UTC612
        self.size = r.size.or(self.size);
f4c93ddeb08fatagen2026-10-19 08:41 UTC613
        self.at = r.at.or(self.at);
f4c93ddeb08fatagen2026-10-19 08:41 UTC614
        self.min_size = r.min_size.or(self.min_size);
f4c93ddeb08fatagen2026-10-19 08:41 UTC615
        self.max_size = r.max_size.or(self.max_size);
f4c93ddeb08fatagen2026-10-19 08:41 UTC616
        self.fullscreen = r.fullscreen.or(self.fullscreen);
f4c93ddeb08fatagen2026-10-19 08:41 UTC617
        self.focus = r.focus.or(self.focus);
f4c93ddeb08fatagen2026-10-19 08:41 UTC618
        self.opacity = r.opacity.or(self.opacity);
f4c93ddeb08fatagen2026-10-19 08:41 UTC619
        self.idle_inhibit = r.idle_inhibit.or(self.idle_inhibit);
f4c93ddeb08fatagen2026-10-19 08:41 UTC620
        self.vrr = r.vrr.or(self.vrr);
f4c93ddeb08fatagen2026-10-19 08:41 UTC621
        self.tearing = r.tearing.or(self.tearing);
f4c93ddeb08fatagen2026-10-19 08:41 UTC622
    }
f4c93ddeb08fatagen2026-10-19 08:41 UTC623
f4c93ddeb08fatagen2026-10-19 08:41 UTC624
    /// Bounds `(w, h)` by `min_size` and `max_size`.
f4c93ddeb08fatagen2026-10-19 08:41 UTC625
    pub fn clamp_size(&self, w: f64, h: f64) -> (f64, f64) {
f4c93ddeb08fatagen2026-10-19 08:41 UTC626
        let axis = |v: f64, min: Option<u32>, max: Option<u32>| {
f4c93ddeb08fatagen2026-10-19 08:41 UTC627
            let v = min.map_or(v, |m| v.max(f64::from(m)));
f4c93ddeb08fatagen2026-10-19 08:41 UTC628
            max.filter(|&m| m > 0).map_or(v, |m| v.min(f64::from(m)))
f4c93ddeb08fatagen2026-10-19 08:41 UTC629
        };
f4c93ddeb08fatagen2026-10-19 08:41 UTC630
        let (min, max) = (self.min_size, self.max_size);
f4c93ddeb08fatagen2026-10-19 08:41 UTC631
        (
f4c93ddeb08fatagen2026-10-19 08:41 UTC632
            axis(w, min.map(|s| s.0), max.map(|s| s.0)),
f4c93ddeb08fatagen2026-10-19 08:41 UTC633
            axis(h, min.map(|s| s.1), max.map(|s| s.1)),
f4c93ddeb08fatagen2026-10-19 08:41 UTC634
        )
f4c93ddeb08fatagen2026-10-19 08:41 UTC635
    }
fe835aed6c8datagen2026-10-16 10:05 UTC636
}
fe835aed6c8datagen2026-10-16 10:05 UTC637
fe835aed6c8datagen2026-10-16 10:05 UTC638
#[derive(Debug)]
fe835aed6c8datagen2026-10-16 10:05 UTC639
pub enum Error {
fe835aed6c8datagen2026-10-16 10:05 UTC640
    Io(PathBuf, std::io::Error),
fe835aed6c8datagen2026-10-16 10:05 UTC641
    Toml(toml::de::Error),
fe835aed6c8datagen2026-10-16 10:05 UTC642
}
fe835aed6c8datagen2026-10-16 10:05 UTC643
fe835aed6c8datagen2026-10-16 10:05 UTC644
impl fmt::Display for Error {
fe835aed6c8datagen2026-10-16 10:05 UTC645
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fe835aed6c8datagen2026-10-16 10:05 UTC646
        match self {
fe835aed6c8datagen2026-10-16 10:05 UTC647
            Error::Io(p, e) => write!(f, "{}: {e}", p.display()),
fe835aed6c8datagen2026-10-16 10:05 UTC648
            Error::Toml(e) => write!(f, "{e}"),
fe835aed6c8datagen2026-10-16 10:05 UTC649
        }
fe835aed6c8datagen2026-10-16 10:05 UTC650
    }
fe835aed6c8datagen2026-10-16 10:05 UTC651
}
fe835aed6c8datagen2026-10-16 10:05 UTC652
fe835aed6c8datagen2026-10-16 10:05 UTC653
impl std::error::Error for Error {}
fe835aed6c8datagen2026-10-16 10:05 UTC654
fe835aed6c8datagen2026-10-16 10:05 UTC655
#[derive(Deserialize)]
fe835aed6c8datagen2026-10-16 10:05 UTC656
#[serde(untagged)]
fe835aed6c8datagen2026-10-16 10:05 UTC657
enum Words {
fe835aed6c8datagen2026-10-16 10:05 UTC658
    Line(String),
fe835aed6c8datagen2026-10-16 10:05 UTC659
    List(Vec<String>),
fe835aed6c8datagen2026-10-16 10:05 UTC660
}
fe835aed6c8datagen2026-10-16 10:05 UTC661
fe835aed6c8datagen2026-10-16 10:05 UTC662
#[derive(Deserialize)]
fe835aed6c8datagen2026-10-16 10:05 UTC663
#[serde(deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC664
struct RawRule {
fe835aed6c8datagen2026-10-16 10:05 UTC665
    app_id: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC666
    title: Option<String>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC667
    initial_title: Option<String>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC668
    class: Option<String>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC669
    instance: Option<String>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC670
    xwayland: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC671
    dialog: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC672
    parent_app_id: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC673
    tag: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC674
    output: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC675
    floating: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC676
    sticky: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC677
    scratchpad: Option<String>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC678
    size: Option<[u32; 2]>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC679
    position: Option<[i32; 2]>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC680
    center: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC681
    min_size: Option<[u32; 2]>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC682
    max_size: Option<[u32; 2]>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC683
    fullscreen: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC684
    focus: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC685
    opacity: Option<f32>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC686
    idle_inhibit: Option<IdleInhibit>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC687
    vrr: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC688
    tearing: Option<bool>,
f4c93ddeb08fatagen2026-10-19 08:41 UTC689
}
f4c93ddeb08fatagen2026-10-19 08:41 UTC690
f4c93ddeb08fatagen2026-10-19 08:41 UTC691
impl RawRule {
f4c93ddeb08fatagen2026-10-19 08:41 UTC692
    /// A pattern that does not compile drops the rule, since without it the
f4c93ddeb08fatagen2026-10-19 08:41 UTC693
    /// rule would hit every window. A bad effect only drops itself.
f4c93ddeb08fatagen2026-10-19 08:41 UTC694
    fn build(self, name: &str, warn: &mut Vec<String>) -> Result<Rule, regex::Error> {
f4c93ddeb08fatagen2026-10-19 08:41 UTC695
        let regex = |p: Option<String>| p.map(|p| Regex::new(&p)).transpose();
f4c93ddeb08fatagen2026-10-19 08:41 UTC696
        let when = Matcher {
f4c93ddeb08fatagen2026-10-19 08:41 UTC697
            app_id: regex(self.app_id)?,
f4c93ddeb08fatagen2026-10-19 08:41 UTC698
            title: regex(self.title)?,
f4c93ddeb08fatagen2026-10-19 08:41 UTC699
            initial_title: regex(self.initial_title)?,
f4c93ddeb08fatagen2026-10-19 08:41 UTC700
            class: regex(self.class)?,
f4c93ddeb08fatagen2026-10-19 08:41 UTC701
            instance: regex(self.instance)?,
f4c93ddeb08fatagen2026-10-19 08:41 UTC702
            xwayland: self.xwayland,
f4c93ddeb08fatagen2026-10-19 08:41 UTC703
            dialog: self.dialog,
f4c93ddeb08fatagen2026-10-19 08:41 UTC704
            parent_app_id: regex(self.parent_app_id)?,
f4c93ddeb08fatagen2026-10-19 08:41 UTC705
        };
f4c93ddeb08fatagen2026-10-19 08:41 UTC706
        let mut set = Placement {
f4c93ddeb08fatagen2026-10-19 08:41 UTC707
            tag: self.tag,
f4c93ddeb08fatagen2026-10-19 08:41 UTC708
            output: self.output,
f4c93ddeb08fatagen2026-10-19 08:41 UTC709
            floating: self.floating,
f4c93ddeb08fatagen2026-10-19 08:41 UTC710
            sticky: self.sticky,
f4c93ddeb08fatagen2026-10-19 08:41 UTC711
            scratchpad: self.scratchpad,
f4c93ddeb08fatagen2026-10-19 08:41 UTC712
            size: self.size.map(|[a, b]| (a, b)),
f4c93ddeb08fatagen2026-10-19 08:41 UTC713
            at: self.position.map(|[x, y]| At::Position(x, y)),
f4c93ddeb08fatagen2026-10-19 08:41 UTC714
            min_size: self.min_size.map(|[a, b]| (a, b)),
f4c93ddeb08fatagen2026-10-19 08:41 UTC715
            max_size: self.max_size.map(|[a, b]| (a, b)),
f4c93ddeb08fatagen2026-10-19 08:41 UTC716
            fullscreen: self.fullscreen,
f4c93ddeb08fatagen2026-10-19 08:41 UTC717
            focus: self.focus,
f4c93ddeb08fatagen2026-10-19 08:41 UTC718
            opacity: self.opacity,
f4c93ddeb08fatagen2026-10-19 08:41 UTC719
            idle_inhibit: self.idle_inhibit,
f4c93ddeb08fatagen2026-10-19 08:41 UTC720
            vrr: self.vrr,
f4c93ddeb08fatagen2026-10-19 08:41 UTC721
            tearing: self.tearing,
f4c93ddeb08fatagen2026-10-19 08:41 UTC722
        };
f4c93ddeb08fatagen2026-10-19 08:41 UTC723
        if self.center == Some(true) {
f4c93ddeb08fatagen2026-10-19 08:41 UTC724
            if set.at.is_some() {
f4c93ddeb08fatagen2026-10-19 08:41 UTC725
                warn.push(format!("{name}: position and center both set, center wins"));
f4c93ddeb08fatagen2026-10-19 08:41 UTC726
            }
f4c93ddeb08fatagen2026-10-19 08:41 UTC727
            set.at = Some(At::Center);
f4c93ddeb08fatagen2026-10-19 08:41 UTC728
        }
f4c93ddeb08fatagen2026-10-19 08:41 UTC729
        if set.size.is_some_and(|(w, h)| w == 0 || h == 0) {
f4c93ddeb08fatagen2026-10-19 08:41 UTC730
            warn.push(format!("{name}.size: must be above zero, ignored"));
f4c93ddeb08fatagen2026-10-19 08:41 UTC731
            set.size = None;
f4c93ddeb08fatagen2026-10-19 08:41 UTC732
        }
f4c93ddeb08fatagen2026-10-19 08:41 UTC733
        if let (Some(min), Some(max)) = (set.min_size, set.max_size)
f4c93ddeb08fatagen2026-10-19 08:41 UTC734
            && [(min.0, max.0), (min.1, max.1)]
f4c93ddeb08fatagen2026-10-19 08:41 UTC735
                .iter()
f4c93ddeb08fatagen2026-10-19 08:41 UTC736
                .any(|&(lo, hi)| hi > 0 && lo > hi)
f4c93ddeb08fatagen2026-10-19 08:41 UTC737
        {
f4c93ddeb08fatagen2026-10-19 08:41 UTC738
            warn.push(format!(
f4c93ddeb08fatagen2026-10-19 08:41 UTC739
                "{name}: min_size is larger than max_size, both ignored"
f4c93ddeb08fatagen2026-10-19 08:41 UTC740
            ));
f4c93ddeb08fatagen2026-10-19 08:41 UTC741
            set.min_size = None;
f4c93ddeb08fatagen2026-10-19 08:41 UTC742
            set.max_size = None;
f4c93ddeb08fatagen2026-10-19 08:41 UTC743
        }
f4c93ddeb08fatagen2026-10-19 08:41 UTC744
        if let Some(o) = set.opacity
f4c93ddeb08fatagen2026-10-19 08:41 UTC745
            && !(0.0..=1.0).contains(&o)
f4c93ddeb08fatagen2026-10-19 08:41 UTC746
        {
f4c93ddeb08fatagen2026-10-19 08:41 UTC747
            warn.push(format!("{name}.opacity: {o} is outside 0 to 1, ignored"));
f4c93ddeb08fatagen2026-10-19 08:41 UTC748
            set.opacity = None;
f4c93ddeb08fatagen2026-10-19 08:41 UTC749
        }
f4c93ddeb08fatagen2026-10-19 08:41 UTC750
        Ok(Rule { when, set })
f4c93ddeb08fatagen2026-10-19 08:41 UTC751
    }
fe835aed6c8datagen2026-10-16 10:05 UTC752
}
fe835aed6c8datagen2026-10-16 10:05 UTC753
fe835aed6c8datagen2026-10-16 10:05 UTC754
impl Config {
fe835aed6c8datagen2026-10-16 10:05 UTC755
    /// `$MONOLITH_CONFIG`, else `$XDG_CONFIG_HOME/monolith/config.toml`.
fe835aed6c8datagen2026-10-16 10:05 UTC756
    pub fn path() -> Option<PathBuf> {
fe835aed6c8datagen2026-10-16 10:05 UTC757
        if let Some(p) = std::env::var_os("MONOLITH_CONFIG") {
fe835aed6c8datagen2026-10-16 10:05 UTC758
            return Some(p.into());
fe835aed6c8datagen2026-10-16 10:05 UTC759
        }
fe835aed6c8datagen2026-10-16 10:05 UTC760
        let base = std::env::var_os("XDG_CONFIG_HOME")
fe835aed6c8datagen2026-10-16 10:05 UTC761
            .map(PathBuf::from)
fe835aed6c8datagen2026-10-16 10:05 UTC762
            .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
fe835aed6c8datagen2026-10-16 10:05 UTC763
        Some(base.join("monolith/config.toml"))
fe835aed6c8datagen2026-10-16 10:05 UTC764
    }
fe835aed6c8datagen2026-10-16 10:05 UTC765
fe835aed6c8datagen2026-10-16 10:05 UTC766
    /// Loads `path`, or the built-in default when it does not exist.
8fb62aebf501atagen2026-10-17 10:25 UTC767
    pub fn load(path: Option<&Path>) -> Result<Loaded, Error> {
fe835aed6c8datagen2026-10-16 10:05 UTC768
        match path {
fe835aed6c8datagen2026-10-16 10:05 UTC769
            Some(p) => match std::fs::read_to_string(p) {
fe835aed6c8datagen2026-10-16 10:05 UTC770
                Ok(text) => Self::parse(&text),
fe835aed6c8datagen2026-10-16 10:05 UTC771
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::parse(DEFAULT),
fe835aed6c8datagen2026-10-16 10:05 UTC772
                Err(e) => Err(Error::Io(p.to_owned(), e)),
fe835aed6c8datagen2026-10-16 10:05 UTC773
            },
fe835aed6c8datagen2026-10-16 10:05 UTC774
            None => Self::parse(DEFAULT),
fe835aed6c8datagen2026-10-16 10:05 UTC775
        }
fe835aed6c8datagen2026-10-16 10:05 UTC776
    }
fe835aed6c8datagen2026-10-16 10:05 UTC777
a1e55dd5ed07atagen2026-10-19 08:36 UTC778
    /// Only TOML too broken to repair line by line fails. A bad option, bind
a1e55dd5ed07atagen2026-10-19 08:36 UTC779
    /// or rule is left out with a warning, so one typo does not throw away the
a1e55dd5ed07atagen2026-10-19 08:36 UTC780
    /// rest of the file.
8fb62aebf501atagen2026-10-17 10:25 UTC781
    pub fn parse(text: &str) -> Result<Loaded, Error> {
8fb62aebf501atagen2026-10-17 10:25 UTC782
        let mut warnings = Vec::new();
a1e55dd5ed07atagen2026-10-19 08:36 UTC783
        let mut table = lenient_table(text, &mut warnings)?;
8fb62aebf501atagen2026-10-17 10:25 UTC784
        let w = &mut warnings;
8fb62aebf501atagen2026-10-17 10:25 UTC785
        let general: General = section("general", table.remove("general"), w);
8fb62aebf501atagen2026-10-17 10:25 UTC786
        let keyboard = section("keyboard", table.remove("keyboard"), w);
9944e8618a62atagen2026-10-18 10:00 UTC787
        let input = input(table.remove("input"), w);
47b86d08d05aatagen2026-10-19 07:37 UTC788
        let idle = section("idle", table.remove("idle"), w);
5b8422a225daatagen2026-10-19 07:45 UTC789
        let lid = section("lid", table.remove("lid"), w);
8fb62aebf501atagen2026-10-17 10:25 UTC790
        let animation = section("animation", table.remove("animation"), w);
1df9b00ad0fdatagen2026-10-19 07:34 UTC791
        let xwayland = section("xwayland", table.remove("xwayland"), w);
8fb62aebf501atagen2026-10-17 10:25 UTC792
        let decoration = section("decoration", table.remove("decoration"), w);
fc167134b65datagen2026-10-19 08:09 UTC793
        let mut output: Outputs = section("output", table.remove("output"), w);
fc167134b65datagen2026-10-19 08:09 UTC794
        for name in output
fc167134b65datagen2026-10-19 08:09 UTC795
            .named
fc167134b65datagen2026-10-19 08:09 UTC796
            .keys()
fc167134b65datagen2026-10-19 08:09 UTC797
            .filter(|n| *n != "nested" && !connector_like(n))
fc167134b65datagen2026-10-19 08:09 UTC798
        {
0f513a2420c0atagen2026-10-18 02:40 UTC799
            w.push(format!(
0f513a2420c0atagen2026-10-18 02:40 UTC800
                "output.{name}: not a connector name like DP-1 or HDMI-A-1, matches nothing"
0f513a2420c0atagen2026-10-18 02:40 UTC801
            ));
0f513a2420c0atagen2026-10-18 02:40 UTC802
        }
fc167134b65datagen2026-10-19 08:09 UTC803
        for (name, config) in &mut output.named {
fc167134b65datagen2026-10-19 08:09 UTC804
            config.validate(&format!("output.{name}"), w);
fc167134b65datagen2026-10-19 08:09 UTC805
        }
8fb62aebf501atagen2026-10-17 10:25 UTC806
        let startup = items("startup", table.remove("startup"), w);
8fb62aebf501atagen2026-10-17 10:25 UTC807
        let mut binds = Vec::new();
8fb62aebf501atagen2026-10-17 10:25 UTC808
        if let Some(value) = table.remove("binds") {
8fb62aebf501atagen2026-10-17 10:25 UTC809
            let entries: toml::Table = entry("binds", value, w).unwrap_or_default();
8fb62aebf501atagen2026-10-17 10:25 UTC810
            for (combo, value) in entries {
8fb62aebf501atagen2026-10-17 10:25 UTC811
                let name = format!("binds.{combo:?}");
8fb62aebf501atagen2026-10-17 10:25 UTC812
                let bind = entry::<Words>(&name, value, w)
8fb62aebf501atagen2026-10-17 10:25 UTC813
                    .map(|words| parse_bind(&combo, words, general.mod_key));
8fb62aebf501atagen2026-10-17 10:25 UTC814
                match bind {
8fb62aebf501atagen2026-10-17 10:25 UTC815
                    Some(Ok(b)) => binds.push(b),
8fb62aebf501atagen2026-10-17 10:25 UTC816
                    Some(Err(e)) => w.push(format!("{name}: {e}, left out")),
8fb62aebf501atagen2026-10-17 10:25 UTC817
                    None => {}
8fb62aebf501atagen2026-10-17 10:25 UTC818
                }
8fb62aebf501atagen2026-10-17 10:25 UTC819
            }
8fb62aebf501atagen2026-10-17 10:25 UTC820
        }
8fb62aebf501atagen2026-10-17 10:25 UTC821
        let mut rules = Vec::new();
8fb62aebf501atagen2026-10-17 10:25 UTC822
        for (i, raw) in items::<RawRule>("rules", table.remove("rules"), w)
fe835aed6c8datagen2026-10-16 10:05 UTC823
            .into_iter()
8fb62aebf501atagen2026-10-17 10:25 UTC824
            .enumerate()
8fb62aebf501atagen2026-10-17 10:25 UTC825
        {
f4c93ddeb08fatagen2026-10-19 08:41 UTC826
            let name = format!("rules[{i}]");
f4c93ddeb08fatagen2026-10-19 08:41 UTC827
            match raw.build(&name, w) {
f4c93ddeb08fatagen2026-10-19 08:41 UTC828
                Ok(rule) => rules.push(rule),
f4c93ddeb08fatagen2026-10-19 08:41 UTC829
                Err(e) => {
8fb62aebf501atagen2026-10-17 10:25 UTC830
                    let e = e
8fb62aebf501atagen2026-10-17 10:25 UTC831
                        .to_string()
8fb62aebf501atagen2026-10-17 10:25 UTC832
                        .split_whitespace()
8fb62aebf501atagen2026-10-17 10:25 UTC833
                        .collect::<Vec<_>>()
8fb62aebf501atagen2026-10-17 10:25 UTC834
                        .join(" ");
f4c93ddeb08fatagen2026-10-19 08:41 UTC835
                    w.push(format!("{name}: {e}, left out"));
8fb62aebf501atagen2026-10-17 10:25 UTC836
                }
8fb62aebf501atagen2026-10-17 10:25 UTC837
            }
8fb62aebf501atagen2026-10-17 10:25 UTC838
        }
8fb62aebf501atagen2026-10-17 10:25 UTC839
        for key in table.keys() {
8fb62aebf501atagen2026-10-17 10:25 UTC840
            w.push(format!("unknown section [{key}], ignored"));
8fb62aebf501atagen2026-10-17 10:25 UTC841
        }
8fb62aebf501atagen2026-10-17 10:25 UTC842
        let config = Self {
8fb62aebf501atagen2026-10-17 10:25 UTC843
            general,
8fb62aebf501atagen2026-10-17 10:25 UTC844
            animation,
8fb62aebf501atagen2026-10-17 10:25 UTC845
            decoration,
8fb62aebf501atagen2026-10-17 10:25 UTC846
            output,
8fb62aebf501atagen2026-10-17 10:25 UTC847
            keyboard,
9944e8618a62atagen2026-10-18 10:00 UTC848
            input,
47b86d08d05aatagen2026-10-19 07:37 UTC849
            idle,
5b8422a225daatagen2026-10-19 07:45 UTC850
            lid,
1df9b00ad0fdatagen2026-10-19 07:34 UTC851
            xwayland,
fe835aed6c8datagen2026-10-16 10:05 UTC852
            binds,
fe835aed6c8datagen2026-10-16 10:05 UTC853
            rules,
8fb62aebf501atagen2026-10-17 10:25 UTC854
            startup,
8fb62aebf501atagen2026-10-17 10:25 UTC855
        };
8fb62aebf501atagen2026-10-17 10:25 UTC856
        Ok(Loaded { config, warnings })
fe835aed6c8datagen2026-10-16 10:05 UTC857
    }
fe835aed6c8datagen2026-10-16 10:05 UTC858
f4c93ddeb08fatagen2026-10-19 08:41 UTC859
    pub fn placement(&self, window: &Subject) -> Placement {
fe835aed6c8datagen2026-10-16 10:05 UTC860
        let mut p = Placement::default();
f4c93ddeb08fatagen2026-10-19 08:41 UTC861
        for r in self.rules.iter().filter(|r| r.when.hits(window)) {
f4c93ddeb08fatagen2026-10-19 08:41 UTC862
            p.over(&r.set);
fe835aed6c8datagen2026-10-16 10:05 UTC863
        }
fe835aed6c8datagen2026-10-16 10:05 UTC864
        p
fe835aed6c8datagen2026-10-16 10:05 UTC865
    }
fe835aed6c8datagen2026-10-16 10:05 UTC866
}
fe835aed6c8datagen2026-10-16 10:05 UTC867
8fb62aebf501atagen2026-10-17 10:25 UTC868
fn parse_bind(combo: &str, words: Words, mod_key: Modifier) -> Result<Bind, String> {
fe835aed6c8datagen2026-10-16 10:05 UTC869
    let parts: Vec<&str> = combo.split('+').map(str::trim).collect();
fe835aed6c8datagen2026-10-16 10:05 UTC870
    let Some((&key, mods)) = parts.split_last().filter(|(k, _)| !k.is_empty()) else {
8fb62aebf501atagen2026-10-17 10:25 UTC871
        return Err("no key".into());
fe835aed6c8datagen2026-10-16 10:05 UTC872
    };
fe835aed6c8datagen2026-10-16 10:05 UTC873
    let mut set = Mods::default();
fe835aed6c8datagen2026-10-16 10:05 UTC874
    for m in mods {
fe835aed6c8datagen2026-10-16 10:05 UTC875
        let m = match m.to_ascii_lowercase().as_str() {
fe835aed6c8datagen2026-10-16 10:05 UTC876
            "mod" => mod_key,
fe835aed6c8datagen2026-10-16 10:05 UTC877
            "super" | "logo" | "win" => Modifier::Super,
fe835aed6c8datagen2026-10-16 10:05 UTC878
            "alt" => Modifier::Alt,
fe835aed6c8datagen2026-10-16 10:05 UTC879
            "ctrl" | "control" => Modifier::Ctrl,
fe835aed6c8datagen2026-10-16 10:05 UTC880
            "shift" => Modifier::Shift,
8fb62aebf501atagen2026-10-17 10:25 UTC881
            other => return Err(format!("unknown modifier {other}")),
fe835aed6c8datagen2026-10-16 10:05 UTC882
        };
fe835aed6c8datagen2026-10-16 10:05 UTC883
        set.set(m);
fe835aed6c8datagen2026-10-16 10:05 UTC884
    }
fe835aed6c8datagen2026-10-16 10:05 UTC885
    let words: Vec<String> = match words {
fe835aed6c8datagen2026-10-16 10:05 UTC886
        Words::Line(s) => s.split_whitespace().map(str::to_owned).collect(),
fe835aed6c8datagen2026-10-16 10:05 UTC887
        Words::List(v) => v,
fe835aed6c8datagen2026-10-16 10:05 UTC888
    };
8fb62aebf501atagen2026-10-17 10:25 UTC889
    let command = Command::parse(&words).map_err(|e| e.0)?;
fe835aed6c8datagen2026-10-16 10:05 UTC890
    Ok(Bind {
fe835aed6c8datagen2026-10-16 10:05 UTC891
        mods: set,
fe835aed6c8datagen2026-10-16 10:05 UTC892
        key: key.to_owned(),
fe835aed6c8datagen2026-10-16 10:05 UTC893
        command,
fe835aed6c8datagen2026-10-16 10:05 UTC894
    })
fe835aed6c8datagen2026-10-16 10:05 UTC895
}
fe835aed6c8datagen2026-10-16 10:05 UTC896
8fb62aebf501atagen2026-10-17 10:25 UTC897
/// A config and what had to be left out of it.
8fb62aebf501atagen2026-10-17 10:25 UTC898
pub struct Loaded {
8fb62aebf501atagen2026-10-17 10:25 UTC899
    pub config: Config,
8fb62aebf501atagen2026-10-17 10:25 UTC900
    pub warnings: Vec<String>,
8fb62aebf501atagen2026-10-17 10:25 UTC901
}
8fb62aebf501atagen2026-10-17 10:25 UTC902
a1e55dd5ed07atagen2026-10-19 08:36 UTC903
/// Parses TOML, dropping each line the parser rejects and trying again, so a
a1e55dd5ed07atagen2026-10-19 08:36 UTC904
/// syntax error costs its own line rather than the whole file. An unclosed
a1e55dd5ed07atagen2026-10-19 08:36 UTC905
/// table or array is reported lines after the mistake, so the culprit is
a1e55dd5ed07atagen2026-10-19 08:36 UTC906
/// the nearby line whose removal clears that error, or else gets furthest.
92fbde37d644atagen2026-10-19 10:38 UTC907
/// A table header is never dropped alone, since its keys would then join the
92fbde37d644atagen2026-10-19 10:38 UTC908
/// table above and change what they mean. It takes its keys with it.
a1e55dd5ed07atagen2026-10-19 08:36 UTC909
fn lenient_table(text: &str, w: &mut Vec<String>) -> Result<toml::Table, Error> {
a1e55dd5ed07atagen2026-10-19 08:36 UTC910
    const MAX_BAD_LINES: usize = 32;
a1e55dd5ed07atagen2026-10-19 08:36 UTC911
    const LOOK_BACK: usize = 4;
a1e55dd5ed07atagen2026-10-19 08:36 UTC912
    let mut lines: Vec<(usize, &str)> = text.lines().enumerate().map(|(i, l)| (i + 1, l)).collect();
a1e55dd5ed07atagen2026-10-19 08:36 UTC913
    let join =
a1e55dd5ed07atagen2026-10-19 08:36 UTC914
        |lines: &[(usize, &str)]| lines.iter().map(|(_, l)| *l).collect::<Vec<_>>().join("\n");
a1e55dd5ed07atagen2026-10-19 08:36 UTC915
    let failing_line = |lines: &[(usize, &str)], e: &toml::de::Error| -> Option<usize> {
a1e55dd5ed07atagen2026-10-19 08:36 UTC916
        let at = e.span()?.start;
a1e55dd5ed07atagen2026-10-19 08:36 UTC917
        let text = join(lines);
a1e55dd5ed07atagen2026-10-19 08:36 UTC918
        let index = text[..at.min(text.len())].matches('\n').count();
a1e55dd5ed07atagen2026-10-19 08:36 UTC919
        Some(index.min(lines.len().saturating_sub(1)))
a1e55dd5ed07atagen2026-10-19 08:36 UTC920
    };
a1e55dd5ed07atagen2026-10-19 08:36 UTC921
    for _ in 0..MAX_BAD_LINES {
a1e55dd5ed07atagen2026-10-19 08:36 UTC922
        let e = match toml::from_str(&join(&lines)) {
a1e55dd5ed07atagen2026-10-19 08:36 UTC923
            Ok(table) => return Ok(table),
a1e55dd5ed07atagen2026-10-19 08:36 UTC924
            Err(e) => e,
a1e55dd5ed07atagen2026-10-19 08:36 UTC925
        };
a1e55dd5ed07atagen2026-10-19 08:36 UTC926
        let Some(at) = failing_line(&lines, &e) else {
a1e55dd5ed07atagen2026-10-19 08:36 UTC927
            return Err(Error::Toml(e));
a1e55dd5ed07atagen2026-10-19 08:36 UTC928
        };
a1e55dd5ed07atagen2026-10-19 08:36 UTC929
        let progress = |drop: usize| {
a1e55dd5ed07atagen2026-10-19 08:36 UTC930
            let mut rest = lines.clone();
a1e55dd5ed07atagen2026-10-19 08:36 UTC931
            rest.remove(drop);
a1e55dd5ed07atagen2026-10-19 08:36 UTC932
            match toml::from_str::<toml::Table>(&join(&rest)) {
a1e55dd5ed07atagen2026-10-19 08:36 UTC933
                Ok(_) => (true, usize::MAX),
a1e55dd5ed07atagen2026-10-19 08:36 UTC934
                Err(next) => (
a1e55dd5ed07atagen2026-10-19 08:36 UTC935
                    next.message() != e.message(),
a1e55dd5ed07atagen2026-10-19 08:36 UTC936
                    failing_line(&rest, &next).map_or(0, |i| rest[i].0),
a1e55dd5ed07atagen2026-10-19 08:36 UTC937
                ),
a1e55dd5ed07atagen2026-10-19 08:36 UTC938
            }
a1e55dd5ed07atagen2026-10-19 08:36 UTC939
        };
a1e55dd5ed07atagen2026-10-19 08:36 UTC940
        let culprit = (at.saturating_sub(LOOK_BACK)..=at)
a1e55dd5ed07atagen2026-10-19 08:36 UTC941
            .filter(|&i| !lines[i].1.trim().is_empty())
92fbde37d644atagen2026-10-19 10:38 UTC942
            .max_by_key(|&i| (progress(i), !is_header(lines[i].1)))
a1e55dd5ed07atagen2026-10-19 08:36 UTC943
            .unwrap_or(at);
92fbde37d644atagen2026-10-19 10:38 UTC944
        let (line, text) = lines[culprit];
92fbde37d644atagen2026-10-19 10:38 UTC945
        if is_header(text) {
92fbde37d644atagen2026-10-19 10:38 UTC946
            let end = (culprit + 1..lines.len())
92fbde37d644atagen2026-10-19 10:38 UTC947
                .find(|&i| is_header(lines[i].1))
92fbde37d644atagen2026-10-19 10:38 UTC948
                .unwrap_or(lines.len());
92fbde37d644atagen2026-10-19 10:38 UTC949
            lines.drain(culprit..end);
92fbde37d644atagen2026-10-19 10:38 UTC950
            let name = text.split('#').next().unwrap_or(text).trim();
92fbde37d644atagen2026-10-19 10:38 UTC951
            w.push(format!(
92fbde37d644atagen2026-10-19 10:38 UTC952
                "line {line}: {}, section {name} left out",
92fbde37d644atagen2026-10-19 10:38 UTC953
                e.message().trim()
92fbde37d644atagen2026-10-19 10:38 UTC954
            ));
92fbde37d644atagen2026-10-19 10:38 UTC955
        } else {
92fbde37d644atagen2026-10-19 10:38 UTC956
            lines.remove(culprit);
92fbde37d644atagen2026-10-19 10:38 UTC957
            w.push(format!("line {line}: {}, left out", e.message().trim()));
92fbde37d644atagen2026-10-19 10:38 UTC958
        }
a1e55dd5ed07atagen2026-10-19 08:36 UTC959
    }
a1e55dd5ed07atagen2026-10-19 08:36 UTC960
    toml::from_str(&join(&lines)).map_err(Error::Toml)
a1e55dd5ed07atagen2026-10-19 08:36 UTC961
}
a1e55dd5ed07atagen2026-10-19 08:36 UTC962
92fbde37d644atagen2026-10-19 10:38 UTC963
fn is_header(line: &str) -> bool {
92fbde37d644atagen2026-10-19 10:38 UTC964
    let line = line.split('#').next().unwrap_or_default().trim_start();
92fbde37d644atagen2026-10-19 10:38 UTC965
    line.starts_with('[') && !line.contains(['=', ',', '{'])
92fbde37d644atagen2026-10-19 10:38 UTC966
}
92fbde37d644atagen2026-10-19 10:38 UTC967
8fb62aebf501atagen2026-10-17 10:25 UTC968
fn entry<T: DeserializeOwned>(name: &str, value: toml::Value, w: &mut Vec<String>) -> Option<T> {
8fb62aebf501atagen2026-10-17 10:25 UTC969
    value
8fb62aebf501atagen2026-10-17 10:25 UTC970
        .try_into()
8fb62aebf501atagen2026-10-17 10:25 UTC971
        .map_err(|e| w.push(format!("{name}: {}, left out", one_line(&e))))
8fb62aebf501atagen2026-10-17 10:25 UTC972
        .ok()
8fb62aebf501atagen2026-10-17 10:25 UTC973
}
8fb62aebf501atagen2026-10-17 10:25 UTC974
9944e8618a62atagen2026-10-18 10:00 UTC975
fn input(value: Option<toml::Value>, w: &mut Vec<String>) -> Input {
9944e8618a62atagen2026-10-18 10:00 UTC976
    let mut table = match value {
9944e8618a62atagen2026-10-18 10:00 UTC977
        None => toml::Table::new(),
9944e8618a62atagen2026-10-18 10:00 UTC978
        Some(toml::Value::Table(t)) => t,
9944e8618a62atagen2026-10-18 10:00 UTC979
        Some(_) => {
9944e8618a62atagen2026-10-18 10:00 UTC980
            w.push("[input] is not a table, using the defaults".into());
9944e8618a62atagen2026-10-18 10:00 UTC981
            toml::Table::new()
9944e8618a62atagen2026-10-18 10:00 UTC982
        }
9944e8618a62atagen2026-10-18 10:00 UTC983
    };
9944e8618a62atagen2026-10-18 10:00 UTC984
    let mut input = Input {
9944e8618a62atagen2026-10-18 10:00 UTC985
        pointer: section("input.pointer", table.remove("pointer"), w),
9944e8618a62atagen2026-10-18 10:00 UTC986
        touchpad: section("input.touchpad", table.remove("touchpad"), w),
9944e8618a62atagen2026-10-18 10:00 UTC987
        device: Vec::new(),
9944e8618a62atagen2026-10-18 10:00 UTC988
    };
9944e8618a62atagen2026-10-18 10:00 UTC989
    for (i, mut entry) in items::<toml::Table>("input.device", table.remove("device"), w)
9944e8618a62atagen2026-10-18 10:00 UTC990
        .into_iter()
9944e8618a62atagen2026-10-18 10:00 UTC991
        .enumerate()
9944e8618a62atagen2026-10-18 10:00 UTC992
    {
9944e8618a62atagen2026-10-18 10:00 UTC993
        let name = format!("input.device[{i}]");
9944e8618a62atagen2026-10-18 10:00 UTC994
        let Some(toml::Value::String(device)) = entry.remove("name") else {
9944e8618a62atagen2026-10-18 10:00 UTC995
            w.push(format!("{name}: no name, left out"));
9944e8618a62atagen2026-10-18 10:00 UTC996
            continue;
9944e8618a62atagen2026-10-18 10:00 UTC997
        };
9944e8618a62atagen2026-10-18 10:00 UTC998
        input.device.push(NamedDevice {
9944e8618a62atagen2026-10-18 10:00 UTC999
            name: device,
9944e8618a62atagen2026-10-18 10:00 UTC1000
            settings: section(&name, Some(toml::Value::Table(entry)), w),
9944e8618a62atagen2026-10-18 10:00 UTC1001
        });
9944e8618a62atagen2026-10-18 10:00 UTC1002
    }
9944e8618a62atagen2026-10-18 10:00 UTC1003
    for key in table.keys() {
9944e8618a62atagen2026-10-18 10:00 UTC1004
        w.push(format!("input.{key}: unknown option, ignored"));
9944e8618a62atagen2026-10-18 10:00 UTC1005
    }
9944e8618a62atagen2026-10-18 10:00 UTC1006
    input.pointer.validate("input.pointer", w);
9944e8618a62atagen2026-10-18 10:00 UTC1007
    input.touchpad.validate("input.touchpad", w);
9944e8618a62atagen2026-10-18 10:00 UTC1008
    for (i, d) in input.device.iter_mut().enumerate() {
9944e8618a62atagen2026-10-18 10:00 UTC1009
        d.settings.validate(&format!("input.device[{i}]"), w);
9944e8618a62atagen2026-10-18 10:00 UTC1010
    }
9944e8618a62atagen2026-10-18 10:00 UTC1011
    input
9944e8618a62atagen2026-10-18 10:00 UTC1012
}
9944e8618a62atagen2026-10-18 10:00 UTC1013
0f513a2420c0atagen2026-10-18 02:40 UTC1014
fn connector_like(name: &str) -> bool {
0f513a2420c0atagen2026-10-18 02:40 UTC1015
    name.rsplit_once('-').is_some_and(|(kind, n)| {
0f513a2420c0atagen2026-10-18 02:40 UTC1016
        !kind.is_empty() && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())
0f513a2420c0atagen2026-10-18 02:40 UTC1017
    })
0f513a2420c0atagen2026-10-18 02:40 UTC1018
}
0f513a2420c0atagen2026-10-18 02:40 UTC1019
8fb62aebf501atagen2026-10-17 10:25 UTC1020
/// A section with every option that does not fit left at its default.
8fb62aebf501atagen2026-10-17 10:25 UTC1021
fn section<T: DeserializeOwned + Default>(
8fb62aebf501atagen2026-10-17 10:25 UTC1022
    name: &str,
8fb62aebf501atagen2026-10-17 10:25 UTC1023
    value: Option<toml::Value>,
8fb62aebf501atagen2026-10-17 10:25 UTC1024
    w: &mut Vec<String>,
8fb62aebf501atagen2026-10-17 10:25 UTC1025
) -> T {
8fb62aebf501atagen2026-10-17 10:25 UTC1026
    let Some(value) = value else {
8fb62aebf501atagen2026-10-17 10:25 UTC1027
        return T::default();
8fb62aebf501atagen2026-10-17 10:25 UTC1028
    };
8fb62aebf501atagen2026-10-17 10:25 UTC1029
    if let Ok(t) = value.clone().try_into() {
8fb62aebf501atagen2026-10-17 10:25 UTC1030
        return t;
8fb62aebf501atagen2026-10-17 10:25 UTC1031
    }
8fb62aebf501atagen2026-10-17 10:25 UTC1032
    let toml::Value::Table(options) = value else {
8fb62aebf501atagen2026-10-17 10:25 UTC1033
        w.push(format!("[{name}] is not a table, using the defaults"));
8fb62aebf501atagen2026-10-17 10:25 UTC1034
        return T::default();
8fb62aebf501atagen2026-10-17 10:25 UTC1035
    };
8fb62aebf501atagen2026-10-17 10:25 UTC1036
    let mut kept = toml::Table::new();
8fb62aebf501atagen2026-10-17 10:25 UTC1037
    for (key, v) in options {
8fb62aebf501atagen2026-10-17 10:25 UTC1038
        let alone = toml::Table::from_iter([(key.clone(), v.clone())]);
8fb62aebf501atagen2026-10-17 10:25 UTC1039
        match toml::Value::Table(alone).try_into::<T>() {
8fb62aebf501atagen2026-10-17 10:25 UTC1040
            Ok(_) => {
8fb62aebf501atagen2026-10-17 10:25 UTC1041
                kept.insert(key, v);
8fb62aebf501atagen2026-10-17 10:25 UTC1042
            }
8fb62aebf501atagen2026-10-17 10:25 UTC1043
            Err(e) => {
8fb62aebf501atagen2026-10-17 10:25 UTC1044
                let e = one_line(&e);
8fb62aebf501atagen2026-10-17 10:25 UTC1045
                let fallback = if e.starts_with("unknown field") {
8fb62aebf501atagen2026-10-17 10:25 UTC1046
                    "ignored"
8fb62aebf501atagen2026-10-17 10:25 UTC1047
                } else {
8fb62aebf501atagen2026-10-17 10:25 UTC1048
                    "using the default"
8fb62aebf501atagen2026-10-17 10:25 UTC1049
                };
8fb62aebf501atagen2026-10-17 10:25 UTC1050
                w.push(format!("{name}.{key}: {e}, {fallback}"));
8fb62aebf501atagen2026-10-17 10:25 UTC1051
            }
8fb62aebf501atagen2026-10-17 10:25 UTC1052
        }
8fb62aebf501atagen2026-10-17 10:25 UTC1053
    }
8fb62aebf501atagen2026-10-17 10:25 UTC1054
    toml::Value::Table(kept).try_into().unwrap_or_default()
8fb62aebf501atagen2026-10-17 10:25 UTC1055
}
8fb62aebf501atagen2026-10-17 10:25 UTC1056
8fb62aebf501atagen2026-10-17 10:25 UTC1057
/// An array with the elements that do not fit left out.
8fb62aebf501atagen2026-10-17 10:25 UTC1058
fn items<T: DeserializeOwned>(
8fb62aebf501atagen2026-10-17 10:25 UTC1059
    name: &str,
8fb62aebf501atagen2026-10-17 10:25 UTC1060
    value: Option<toml::Value>,
8fb62aebf501atagen2026-10-17 10:25 UTC1061
    w: &mut Vec<String>,
8fb62aebf501atagen2026-10-17 10:25 UTC1062
) -> Vec<T> {
8fb62aebf501atagen2026-10-17 10:25 UTC1063
    match value {
8fb62aebf501atagen2026-10-17 10:25 UTC1064
        None => Vec::new(),
8fb62aebf501atagen2026-10-17 10:25 UTC1065
        Some(toml::Value::Array(a)) => a
8fb62aebf501atagen2026-10-17 10:25 UTC1066
            .into_iter()
8fb62aebf501atagen2026-10-17 10:25 UTC1067
            .enumerate()
8fb62aebf501atagen2026-10-17 10:25 UTC1068
            .filter_map(|(i, v)| entry(&format!("{name}[{i}]"), v, w))
8fb62aebf501atagen2026-10-17 10:25 UTC1069
            .collect(),
8fb62aebf501atagen2026-10-17 10:25 UTC1070
        Some(_) => {
8fb62aebf501atagen2026-10-17 10:25 UTC1071
            w.push(format!("{name} is not an array, left out"));
8fb62aebf501atagen2026-10-17 10:25 UTC1072
            Vec::new()
8fb62aebf501atagen2026-10-17 10:25 UTC1073
        }
8fb62aebf501atagen2026-10-17 10:25 UTC1074
    }
8fb62aebf501atagen2026-10-17 10:25 UTC1075
}
8fb62aebf501atagen2026-10-17 10:25 UTC1076
8fb62aebf501atagen2026-10-17 10:25 UTC1077
fn one_line(e: &toml::de::Error) -> String {
8fb62aebf501atagen2026-10-17 10:25 UTC1078
    e.message().trim().replace('\n', " ")
8fb62aebf501atagen2026-10-17 10:25 UTC1079
}
fe835aed6c8datagen2026-10-16 10:05 UTC1080
#[cfg(test)]
fe835aed6c8datagen2026-10-16 10:05 UTC1081
mod tests {
b22c0511d5afatagen2026-10-17 03:48 UTC1082
    use super::*;
b22c0511d5afatagen2026-10-17 03:48 UTC1083
b22c0511d5afatagen2026-10-17 03:48 UTC1084
    /// default.toml documents the defaults for users, and the Default impls
b22c0511d5afatagen2026-10-17 03:48 UTC1085
    /// apply them to sections a user config leaves out. They must agree.
fe835aed6c8datagen2026-10-16 10:05 UTC1086
    #[test]
b22c0511d5afatagen2026-10-17 03:48 UTC1087
    fn default_toml_states_the_defaults() {
8fb62aebf501atagen2026-10-17 10:25 UTC1088
        let loaded = Config::parse(DEFAULT).unwrap();
8fb62aebf501atagen2026-10-17 10:25 UTC1089
        assert_eq!(loaded.warnings, Vec::<String>::new());
8fb62aebf501atagen2026-10-17 10:25 UTC1090
        let c = loaded.config;
b22c0511d5afatagen2026-10-17 03:48 UTC1091
        assert_eq!(c.general, General::default());
b22c0511d5afatagen2026-10-17 03:48 UTC1092
        assert_eq!(c.output, Outputs::default());
b22c0511d5afatagen2026-10-17 03:48 UTC1093
        assert_eq!(c.decoration, Decoration::default());
b22c0511d5afatagen2026-10-17 03:48 UTC1094
        assert_eq!(c.animation, Animation::default());
b22c0511d5afatagen2026-10-17 03:48 UTC1095
        assert_eq!(c.keyboard, Keyboard::default());
9944e8618a62atagen2026-10-18 10:00 UTC1096
        assert_eq!(c.input, Input::default());
47b86d08d05aatagen2026-10-19 07:37 UTC1097
        assert_eq!(c.idle, Idle::default());
5b8422a225daatagen2026-10-19 07:45 UTC1098
        assert_eq!(c.lid, Lid::default());
1df9b00ad0fdatagen2026-10-19 07:34 UTC1099
        assert_eq!(c.xwayland, Xwayland::default());
fe835aed6c8datagen2026-10-16 10:05 UTC1100
        assert!(c.binds.len() > 40);
fe835aed6c8datagen2026-10-16 10:05 UTC1101
    }
8fb62aebf501atagen2026-10-17 10:25 UTC1102
8fb62aebf501atagen2026-10-17 10:25 UTC1103
    #[test]
fc167134b65datagen2026-10-19 08:09 UTC1104
    fn output_scale_is_snapped_and_range_checked() {
fc167134b65datagen2026-10-19 08:09 UTC1105
        let loaded = Config::parse(
fc167134b65datagen2026-10-19 08:09 UTC1106
            r#"
fc167134b65datagen2026-10-19 08:09 UTC1107
            [output.DP-1]
fc167134b65datagen2026-10-19 08:09 UTC1108
            scale = 1.5
fc167134b65datagen2026-10-19 08:09 UTC1109
            [output.DP-2]
fc167134b65datagen2026-10-19 08:09 UTC1110
            scale = 1.3333
fc167134b65datagen2026-10-19 08:09 UTC1111
            [output.DP-3]
fc167134b65datagen2026-10-19 08:09 UTC1112
            scale = 9.0
fc167134b65datagen2026-10-19 08:09 UTC1113
            [output.nested]
fc167134b65datagen2026-10-19 08:09 UTC1114
            scale = 2.0
fc167134b65datagen2026-10-19 08:09 UTC1115
            "#,
fc167134b65datagen2026-10-19 08:09 UTC1116
        )
fc167134b65datagen2026-10-19 08:09 UTC1117
        .unwrap();
fc167134b65datagen2026-10-19 08:09 UTC1118
        let named = &loaded.config.output.named;
fc167134b65datagen2026-10-19 08:09 UTC1119
        assert_eq!(named["DP-1"].scale(), 1.5);
fc167134b65datagen2026-10-19 08:09 UTC1120
        assert_eq!(named["DP-2"].scale(), 160.0 / 120.0);
fc167134b65datagen2026-10-19 08:09 UTC1121
        assert_eq!(named["DP-3"].scale(), 1.0);
fc167134b65datagen2026-10-19 08:09 UTC1122
        assert_eq!(loaded.warnings.len(), 1, "{:#?}", loaded.warnings);
fc167134b65datagen2026-10-19 08:09 UTC1123
    }
fc167134b65datagen2026-10-19 08:09 UTC1124
fc167134b65datagen2026-10-19 08:09 UTC1125
    #[test]
8fb62aebf501atagen2026-10-17 10:25 UTC1126
    fn a_bad_option_costs_only_itself() {
8fb62aebf501atagen2026-10-17 10:25 UTC1127
        let loaded = Config::parse(
8fb62aebf501atagen2026-10-17 10:25 UTC1128
            r##"
8fb62aebf501atagen2026-10-17 10:25 UTC1129
            [general]
8fb62aebf501atagen2026-10-17 10:25 UTC1130
            gaps = { inner = 3, outer = 3 }
8fb62aebf501atagen2026-10-17 10:25 UTC1131
            focus_follows_mouse = "yes"
8fb62aebf501atagen2026-10-17 10:25 UTC1132
            typo = 1
8fb62aebf501atagen2026-10-17 10:25 UTC1133
            [decoration]
8fb62aebf501atagen2026-10-17 10:25 UTC1134
            radius = [1, 2, 3, 4]
8fb62aebf501atagen2026-10-17 10:25 UTC1135
            focused = "#zzzzzz"
8fb62aebf501atagen2026-10-17 10:25 UTC1136
            [binds]
3b180fd69834atagen2026-10-17 10:40 UTC1137
            "Mod+Return" = "spawn beer"
8fb62aebf501atagen2026-10-17 10:25 UTC1138
            "Hyper+x" = "close"
9b87063809e1atagen2026-10-17 13:28 UTC1139
            [output.DP-1]
9b87063809e1atagen2026-10-17 13:28 UTC1140
            transform = "90"
9b87063809e1atagen2026-10-17 13:28 UTC1141
            position = [0, 0]
9b87063809e1atagen2026-10-17 13:28 UTC1142
            [output.DP-2]
9b87063809e1atagen2026-10-17 13:28 UTC1143
            transform = "sideways"
0f513a2420c0atagen2026-10-18 02:40 UTC1144
            [output.DP1]
8fb62aebf501atagen2026-10-17 10:25 UTC1145
            [[rules]]
8fb62aebf501atagen2026-10-17 10:25 UTC1146
            app_id = "("
8fb62aebf501atagen2026-10-17 10:25 UTC1147
            [[rules]]
8fb62aebf501atagen2026-10-17 10:25 UTC1148
            app_id = "^mpv$"
8fb62aebf501atagen2026-10-17 10:25 UTC1149
            floating = true
8fb62aebf501atagen2026-10-17 10:25 UTC1150
            "##,
8fb62aebf501atagen2026-10-17 10:25 UTC1151
        )
8fb62aebf501atagen2026-10-17 10:25 UTC1152
        .unwrap();
8fb62aebf501atagen2026-10-17 10:25 UTC1153
        let c = loaded.config;
8fb62aebf501atagen2026-10-17 10:25 UTC1154
        assert_eq!(c.general.gaps.inner, 3.0);
8fb62aebf501atagen2026-10-17 10:25 UTC1155
        assert!(!c.general.focus_follows_mouse);
8fb62aebf501atagen2026-10-17 10:25 UTC1156
        assert_eq!(c.decoration.radius, Corners([1, 2, 3, 4]));
8fb62aebf501atagen2026-10-17 10:25 UTC1157
        assert_eq!(c.decoration.focused, Decoration::default().focused);
8fb62aebf501atagen2026-10-17 10:25 UTC1158
        assert_eq!((c.binds.len(), c.rules.len()), (1, 1));
9b87063809e1atagen2026-10-17 13:28 UTC1159
        assert_eq!(c.output.named["DP-1"].transform, OutputTransform::Rotate90);
9b87063809e1atagen2026-10-17 13:28 UTC1160
        assert!(!c.output.named.contains_key("DP-2"));
0f513a2420c0atagen2026-10-18 02:40 UTC1161
        assert_eq!(loaded.warnings.len(), 7, "{:#?}", loaded.warnings);
8fb62aebf501atagen2026-10-17 10:25 UTC1162
    }
9944e8618a62atagen2026-10-18 10:00 UTC1163
9944e8618a62atagen2026-10-18 10:00 UTC1164
    #[test]
a1e55dd5ed07atagen2026-10-19 08:36 UTC1165
    fn broken_toml_costs_only_its_line() {
a1e55dd5ed07atagen2026-10-19 08:36 UTC1166
        let loaded = Config::parse(
a1e55dd5ed07atagen2026-10-19 08:36 UTC1167
            r##"
a1e55dd5ed07atagen2026-10-19 08:36 UTC1168
            [general]
a1e55dd5ed07atagen2026-10-19 08:36 UTC1169
            gaps = { inner = 3, outer = 3
a1e55dd5ed07atagen2026-10-19 08:36 UTC1170
            focus_follows_mouse = true
a1e55dd5ed07atagen2026-10-19 08:36 UTC1171
            focus_follows_mouse = false
a1e55dd5ed07atagen2026-10-19 08:36 UTC1172
            [binds]
a1e55dd5ed07atagen2026-10-19 08:36 UTC1173
            "Mod+Return" = "spawn beer
a1e55dd5ed07atagen2026-10-19 08:36 UTC1174
            "Mod+q" = "close"
a1e55dd5ed07atagen2026-10-19 08:36 UTC1175
            "##,
a1e55dd5ed07atagen2026-10-19 08:36 UTC1176
        )
a1e55dd5ed07atagen2026-10-19 08:36 UTC1177
        .unwrap();
a1e55dd5ed07atagen2026-10-19 08:36 UTC1178
        let c = loaded.config;
a1e55dd5ed07atagen2026-10-19 08:36 UTC1179
        assert_eq!(c.general.gaps, General::default().gaps);
a1e55dd5ed07atagen2026-10-19 08:36 UTC1180
        assert_eq!(c.binds.len(), 1);
a1e55dd5ed07atagen2026-10-19 08:36 UTC1181
        assert_eq!(loaded.warnings.len(), 3, "{:#?}", loaded.warnings);
a1e55dd5ed07atagen2026-10-19 08:36 UTC1182
        assert!(
a1e55dd5ed07atagen2026-10-19 08:36 UTC1183
            loaded.warnings[0].starts_with("line 3:"),
a1e55dd5ed07atagen2026-10-19 08:36 UTC1184
            "{:#?}",
a1e55dd5ed07atagen2026-10-19 08:36 UTC1185
            loaded.warnings
a1e55dd5ed07atagen2026-10-19 08:36 UTC1186
        );
a1e55dd5ed07atagen2026-10-19 08:36 UTC1187
    }
a1e55dd5ed07atagen2026-10-19 08:36 UTC1188
a1e55dd5ed07atagen2026-10-19 08:36 UTC1189
    #[test]
92fbde37d644atagen2026-10-19 10:38 UTC1190
    fn a_broken_table_header_takes_its_section_with_it() {
92fbde37d644atagen2026-10-19 10:38 UTC1191
        let loaded = Config::parse(
92fbde37d644atagen2026-10-19 10:38 UTC1192
            r##"
92fbde37d644atagen2026-10-19 10:38 UTC1193
            [general]
92fbde37d644atagen2026-10-19 10:38 UTC1194
            focus_follows_mouse = false
92fbde37d644atagen2026-10-19 10:38 UTC1195
            [binds
92fbde37d644atagen2026-10-19 10:38 UTC1196
            "Mod+q" = "close"
92fbde37d644atagen2026-10-19 10:38 UTC1197
            [decoration]
92fbde37d644atagen2026-10-19 10:38 UTC1198
            radius = [1, 2, 3, 4]
92fbde37d644atagen2026-10-19 10:38 UTC1199
            "##,
92fbde37d644atagen2026-10-19 10:38 UTC1200
        )
92fbde37d644atagen2026-10-19 10:38 UTC1201
        .unwrap();
92fbde37d644atagen2026-10-19 10:38 UTC1202
        let c = loaded.config;
92fbde37d644atagen2026-10-19 10:38 UTC1203
        assert!(!c.general.focus_follows_mouse);
92fbde37d644atagen2026-10-19 10:38 UTC1204
        assert_eq!(c.binds.len(), 0);
92fbde37d644atagen2026-10-19 10:38 UTC1205
        assert_eq!(c.decoration.radius, Corners([1, 2, 3, 4]));
92fbde37d644atagen2026-10-19 10:38 UTC1206
        assert_eq!(loaded.warnings.len(), 1, "{:#?}", loaded.warnings);
92fbde37d644atagen2026-10-19 10:38 UTC1207
        assert!(
92fbde37d644atagen2026-10-19 10:38 UTC1208
            loaded.warnings[0].contains("section [binds left out"),
92fbde37d644atagen2026-10-19 10:38 UTC1209
            "{:#?}",
92fbde37d644atagen2026-10-19 10:38 UTC1210
            loaded.warnings
92fbde37d644atagen2026-10-19 10:38 UTC1211
        );
92fbde37d644atagen2026-10-19 10:38 UTC1212
    }
92fbde37d644atagen2026-10-19 10:38 UTC1213
92fbde37d644atagen2026-10-19 10:38 UTC1214
    #[test]
92fbde37d644atagen2026-10-19 10:38 UTC1215
    fn a_broken_rule_header_does_not_feed_the_rule_above() {
92fbde37d644atagen2026-10-19 10:38 UTC1216
        let loaded = Config::parse(
92fbde37d644atagen2026-10-19 10:38 UTC1217
            r##"
92fbde37d644atagen2026-10-19 10:38 UTC1218
            [[rules]]
92fbde37d644atagen2026-10-19 10:38 UTC1219
            app_id = "^a$"
92fbde37d644atagen2026-10-19 10:38 UTC1220
            floating = true
92fbde37d644atagen2026-10-19 10:38 UTC1221
            [[rules]
92fbde37d644atagen2026-10-19 10:38 UTC1222
            app_id = "^b$"
92fbde37d644atagen2026-10-19 10:38 UTC1223
            fullscreen = true
92fbde37d644atagen2026-10-19 10:38 UTC1224
            [[rules]]
92fbde37d644atagen2026-10-19 10:38 UTC1225
            app_id = "^c$"
92fbde37d644atagen2026-10-19 10:38 UTC1226
            "##,
92fbde37d644atagen2026-10-19 10:38 UTC1227
        )
92fbde37d644atagen2026-10-19 10:38 UTC1228
        .unwrap();
92fbde37d644atagen2026-10-19 10:38 UTC1229
        let apps: Vec<_> = loaded
92fbde37d644atagen2026-10-19 10:38 UTC1230
            .config
92fbde37d644atagen2026-10-19 10:38 UTC1231
            .rules
92fbde37d644atagen2026-10-19 10:38 UTC1232
            .iter()
92fbde37d644atagen2026-10-19 10:38 UTC1233
            .map(|r| format!("{r:?}"))
92fbde37d644atagen2026-10-19 10:38 UTC1234
            .collect();
92fbde37d644atagen2026-10-19 10:38 UTC1235
        assert_eq!(apps.len(), 2, "{:#?} {:#?}", apps, loaded.warnings);
92fbde37d644atagen2026-10-19 10:38 UTC1236
        assert!(apps[0].contains("floating: Some(true)") && !apps[0].contains("fullscreen: Some"));
92fbde37d644atagen2026-10-19 10:38 UTC1237
        assert_eq!(loaded.warnings.len(), 1, "{:#?}", loaded.warnings);
92fbde37d644atagen2026-10-19 10:38 UTC1238
        assert!(loaded.warnings[0].contains("section [[rules] left out"));
92fbde37d644atagen2026-10-19 10:38 UTC1239
    }
92fbde37d644atagen2026-10-19 10:38 UTC1240
92fbde37d644atagen2026-10-19 10:38 UTC1241
    #[test]
9944e8618a62atagen2026-10-18 10:00 UTC1242
    fn a_bad_device_option_costs_only_itself() {
9944e8618a62atagen2026-10-18 10:00 UTC1243
        let loaded = Config::parse(
9944e8618a62atagen2026-10-18 10:00 UTC1244
            r#"
9944e8618a62atagen2026-10-18 10:00 UTC1245
            [input.pointer]
9944e8618a62atagen2026-10-18 10:00 UTC1246
            accel_speed = 2.0
9944e8618a62atagen2026-10-18 10:00 UTC1247
            scroll_method = "two-finger"
9944e8618a62atagen2026-10-18 10:00 UTC1248
            [[input.device]]
9944e8618a62atagen2026-10-18 10:00 UTC1249
            name = "Mouse"
9944e8618a62atagen2026-10-18 10:00 UTC1250
            accel_profile = "flat"
9944e8618a62atagen2026-10-18 10:00 UTC1251
            [[input.device]]
9944e8618a62atagen2026-10-18 10:00 UTC1252
            name = "Typo"
9944e8618a62atagen2026-10-18 10:00 UTC1253
            tapp = true
9944e8618a62atagen2026-10-18 10:00 UTC1254
            [[input.device]]
9944e8618a62atagen2026-10-18 10:00 UTC1255
            name = "Half"
9944e8618a62atagen2026-10-18 10:00 UTC1256
            accel_points = [0.0, 1.0]
9944e8618a62atagen2026-10-18 10:00 UTC1257
            "#,
9944e8618a62atagen2026-10-18 10:00 UTC1258
        )
9944e8618a62atagen2026-10-18 10:00 UTC1259
        .unwrap();
9944e8618a62atagen2026-10-18 10:00 UTC1260
        let input = loaded.config.input;
9944e8618a62atagen2026-10-18 10:00 UTC1261
        assert_eq!(input.pointer.accel_speed, None);
9944e8618a62atagen2026-10-18 10:00 UTC1262
        assert_eq!(input.pointer.scroll_method, Some(ScrollMethod::TwoFinger));
9944e8618a62atagen2026-10-18 10:00 UTC1263
        let names: Vec<_> = input.device.iter().map(|d| d.name.as_str()).collect();
9944e8618a62atagen2026-10-18 10:00 UTC1264
        assert_eq!(names, ["Mouse", "Typo", "Half"]);
9944e8618a62atagen2026-10-18 10:00 UTC1265
        assert_eq!(
9944e8618a62atagen2026-10-18 10:00 UTC1266
            input.device[0].settings.accel_profile,
9944e8618a62atagen2026-10-18 10:00 UTC1267
            Some(AccelProfile::Flat)
9944e8618a62atagen2026-10-18 10:00 UTC1268
        );
9944e8618a62atagen2026-10-18 10:00 UTC1269
        assert_eq!(input.device[1].settings, DeviceSettings::default());
9944e8618a62atagen2026-10-18 10:00 UTC1270
        assert_eq!(input.device[2].settings.accel_points, None);
9944e8618a62atagen2026-10-18 10:00 UTC1271
        assert_eq!(loaded.warnings.len(), 3, "{:#?}", loaded.warnings);
9944e8618a62atagen2026-10-18 10:00 UTC1272
    }
f4c93ddeb08fatagen2026-10-19 08:41 UTC1273
f4c93ddeb08fatagen2026-10-19 08:41 UTC1274
    fn subject<'a>(app_id: &'a str, title: &'a str) -> Subject<'a> {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1275
        Subject {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1276
            app_id,
f4c93ddeb08fatagen2026-10-19 08:41 UTC1277
            title,
f4c93ddeb08fatagen2026-10-19 08:41 UTC1278
            initial_title: title,
f4c93ddeb08fatagen2026-10-19 08:41 UTC1279
            ..Subject::default()
f4c93ddeb08fatagen2026-10-19 08:41 UTC1280
        }
f4c93ddeb08fatagen2026-10-19 08:41 UTC1281
    }
f4c93ddeb08fatagen2026-10-19 08:41 UTC1282
f4c93ddeb08fatagen2026-10-19 08:41 UTC1283
    #[test]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1284
    fn rules_match_every_given_matcher_and_later_ones_win() {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1285
        let loaded = Config::parse(
f4c93ddeb08fatagen2026-10-19 08:41 UTC1286
            r#"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1287
            [[rules]]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1288
            app_id = "^mpv$"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1289
            size = [800, 600]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1290
            opacity = 0.5
f4c93ddeb08fatagen2026-10-19 08:41 UTC1291
            idle_inhibit = "visible"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1292
            [[rules]]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1293
            title = "Picture"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1294
            opacity = 0.8
f4c93ddeb08fatagen2026-10-19 08:41 UTC1295
            focus = false
f4c93ddeb08fatagen2026-10-19 08:41 UTC1296
            vrr = true
f4c93ddeb08fatagen2026-10-19 08:41 UTC1297
            tearing = true
f4c93ddeb08fatagen2026-10-19 08:41 UTC1298
            [[rules]]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1299
            class = "^Steam$"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1300
            instance = "steam"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1301
            xwayland = true
f4c93ddeb08fatagen2026-10-19 08:41 UTC1302
            fullscreen = true
f4c93ddeb08fatagen2026-10-19 08:41 UTC1303
            [[rules]]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1304
            dialog = true
f4c93ddeb08fatagen2026-10-19 08:41 UTC1305
            parent_app_id = "^firefox$"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1306
            center = true
f4c93ddeb08fatagen2026-10-19 08:41 UTC1307
            min_size = [300, 0]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1308
            max_size = [0, 500]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1309
            "#,
f4c93ddeb08fatagen2026-10-19 08:41 UTC1310
        )
f4c93ddeb08fatagen2026-10-19 08:41 UTC1311
        .unwrap();
f4c93ddeb08fatagen2026-10-19 08:41 UTC1312
        assert_eq!(loaded.warnings, Vec::<String>::new());
f4c93ddeb08fatagen2026-10-19 08:41 UTC1313
        let c = loaded.config;
f4c93ddeb08fatagen2026-10-19 08:41 UTC1314
        let p = c.placement(&subject("mpv", "Picture in picture"));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1315
        assert_eq!(p.size, Some((800, 600)));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1316
        assert_eq!(p.opacity, Some(0.8));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1317
        assert_eq!(p.idle_inhibit, Some(IdleInhibit::Visible));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1318
        assert_eq!(
f4c93ddeb08fatagen2026-10-19 08:41 UTC1319
            (p.focus, p.vrr, p.tearing),
f4c93ddeb08fatagen2026-10-19 08:41 UTC1320
            (Some(false), Some(true), Some(true))
f4c93ddeb08fatagen2026-10-19 08:41 UTC1321
        );
f4c93ddeb08fatagen2026-10-19 08:41 UTC1322
        assert_eq!(c.placement(&subject("foot", "sh")), Placement::default());
f4c93ddeb08fatagen2026-10-19 08:41 UTC1323
f4c93ddeb08fatagen2026-10-19 08:41 UTC1324
        let x11 = Subject {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1325
            class: Some("Steam"),
f4c93ddeb08fatagen2026-10-19 08:41 UTC1326
            instance: Some("steam"),
f4c93ddeb08fatagen2026-10-19 08:41 UTC1327
            xwayland: true,
f4c93ddeb08fatagen2026-10-19 08:41 UTC1328
            ..subject("Steam", "Steam")
f4c93ddeb08fatagen2026-10-19 08:41 UTC1329
        };
f4c93ddeb08fatagen2026-10-19 08:41 UTC1330
        assert_eq!(c.placement(&x11).fullscreen, Some(true));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1331
        let wayland = Subject {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1332
            xwayland: false,
f4c93ddeb08fatagen2026-10-19 08:41 UTC1333
            ..subject("Steam", "Steam")
f4c93ddeb08fatagen2026-10-19 08:41 UTC1334
        };
f4c93ddeb08fatagen2026-10-19 08:41 UTC1335
        assert_eq!(c.placement(&wayland).fullscreen, None);
f4c93ddeb08fatagen2026-10-19 08:41 UTC1336
f4c93ddeb08fatagen2026-10-19 08:41 UTC1337
        let dialog = Subject {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1338
            dialog: true,
f4c93ddeb08fatagen2026-10-19 08:41 UTC1339
            parent_app_id: Some("firefox"),
f4c93ddeb08fatagen2026-10-19 08:41 UTC1340
            ..subject("firefox", "Save")
f4c93ddeb08fatagen2026-10-19 08:41 UTC1341
        };
f4c93ddeb08fatagen2026-10-19 08:41 UTC1342
        let p = c.placement(&dialog);
f4c93ddeb08fatagen2026-10-19 08:41 UTC1343
        assert_eq!(p.at, Some(At::Center));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1344
        assert_eq!(p.clamp_size(100.0, 900.0), (300.0, 500.0));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1345
        let orphan = Subject {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1346
            parent_app_id: None,
f4c93ddeb08fatagen2026-10-19 08:41 UTC1347
            ..dialog
f4c93ddeb08fatagen2026-10-19 08:41 UTC1348
        };
f4c93ddeb08fatagen2026-10-19 08:41 UTC1349
        assert_eq!(c.placement(&orphan), Placement::default());
f4c93ddeb08fatagen2026-10-19 08:41 UTC1350
    }
f4c93ddeb08fatagen2026-10-19 08:41 UTC1351
f4c93ddeb08fatagen2026-10-19 08:41 UTC1352
    #[test]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1353
    fn initial_title_holds_when_the_title_changes() {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1354
        let loaded = Config::parse(
f4c93ddeb08fatagen2026-10-19 08:41 UTC1355
            r#"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1356
            [[rules]]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1357
            initial_title = "^Untitled$"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1358
            floating = true
f4c93ddeb08fatagen2026-10-19 08:41 UTC1359
            "#,
f4c93ddeb08fatagen2026-10-19 08:41 UTC1360
        )
f4c93ddeb08fatagen2026-10-19 08:41 UTC1361
        .unwrap();
f4c93ddeb08fatagen2026-10-19 08:41 UTC1362
        let later = Subject {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1363
            title: "notes.txt",
f4c93ddeb08fatagen2026-10-19 08:41 UTC1364
            initial_title: "Untitled",
f4c93ddeb08fatagen2026-10-19 08:41 UTC1365
            ..subject("gedit", "")
f4c93ddeb08fatagen2026-10-19 08:41 UTC1366
        };
f4c93ddeb08fatagen2026-10-19 08:41 UTC1367
        assert_eq!(loaded.config.placement(&later).floating, Some(true));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1368
        let other = Subject {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1369
            initial_title: "notes.txt",
f4c93ddeb08fatagen2026-10-19 08:41 UTC1370
            ..later
f4c93ddeb08fatagen2026-10-19 08:41 UTC1371
        };
f4c93ddeb08fatagen2026-10-19 08:41 UTC1372
        assert_eq!(loaded.config.placement(&other).floating, None);
f4c93ddeb08fatagen2026-10-19 08:41 UTC1373
    }
f4c93ddeb08fatagen2026-10-19 08:41 UTC1374
f4c93ddeb08fatagen2026-10-19 08:41 UTC1375
    #[test]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1376
    fn bad_rule_values_are_warned_about_and_cost_little() {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1377
        let loaded = Config::parse(
f4c93ddeb08fatagen2026-10-19 08:41 UTC1378
            r#"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1379
            [[rules]]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1380
            app_id = "a"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1381
            opacity = 1.5
f4c93ddeb08fatagen2026-10-19 08:41 UTC1382
            size = [0, 100]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1383
            min_size = [500, 500]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1384
            max_size = [400, 0]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1385
            position = [10, 10]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1386
            center = true
f4c93ddeb08fatagen2026-10-19 08:41 UTC1387
            floating = true
f4c93ddeb08fatagen2026-10-19 08:41 UTC1388
            [[rules]]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1389
            app_id = "b"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1390
            idle_inhibit = "always"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1391
            [[rules]]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1392
            class = "("
f4c93ddeb08fatagen2026-10-19 08:41 UTC1393
            [[rules]]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1394
            parent_app_id = "["
f4c93ddeb08fatagen2026-10-19 08:41 UTC1395
            [[rules]]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1396
            app_id = "c"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1397
            size = [-1, 10]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1398
            [[rules]]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1399
            app_id = "d"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1400
            focus = "no"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1401
            [[rules]]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1402
            app_id = "e"
f4c93ddeb08fatagen2026-10-19 08:41 UTC1403
            vrr = false
f4c93ddeb08fatagen2026-10-19 08:41 UTC1404
            "#,
f4c93ddeb08fatagen2026-10-19 08:41 UTC1405
        )
f4c93ddeb08fatagen2026-10-19 08:41 UTC1406
        .unwrap();
f4c93ddeb08fatagen2026-10-19 08:41 UTC1407
        let c = loaded.config;
f4c93ddeb08fatagen2026-10-19 08:41 UTC1408
        assert_eq!(c.rules.len(), 2, "{:#?}", loaded.warnings);
f4c93ddeb08fatagen2026-10-19 08:41 UTC1409
        let a = c.placement(&subject("a", ""));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1410
        assert_eq!(a.floating, Some(true));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1411
        assert_eq!(
f4c93ddeb08fatagen2026-10-19 08:41 UTC1412
            (a.opacity, a.size, a.min_size, a.max_size),
f4c93ddeb08fatagen2026-10-19 08:41 UTC1413
            (None, None, None, None)
f4c93ddeb08fatagen2026-10-19 08:41 UTC1414
        );
f4c93ddeb08fatagen2026-10-19 08:41 UTC1415
        assert_eq!(a.at, Some(At::Center));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1416
        assert_eq!(c.placement(&subject("e", "")).vrr, Some(false));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1417
        assert_eq!(loaded.warnings.len(), 9, "{:#?}", loaded.warnings);
f4c93ddeb08fatagen2026-10-19 08:41 UTC1418
    }
f4c93ddeb08fatagen2026-10-19 08:41 UTC1419
f4c93ddeb08fatagen2026-10-19 08:41 UTC1420
    #[test]
075d167f7479atagen2026-10-19 08:49 UTC1421
    fn a_title_set_after_mapping_brings_its_rules_in() {
075d167f7479atagen2026-10-19 08:49 UTC1422
        let loaded = Config::parse(
075d167f7479atagen2026-10-19 08:49 UTC1423
            r#"
075d167f7479atagen2026-10-19 08:49 UTC1424
            [[rules]]
075d167f7479atagen2026-10-19 08:49 UTC1425
            app_id = "^gimp$"
075d167f7479atagen2026-10-19 08:49 UTC1426
            tag = "art"
075d167f7479atagen2026-10-19 08:49 UTC1427
            [[rules]]
075d167f7479atagen2026-10-19 08:49 UTC1428
            app_id = "^gimp$"
075d167f7479atagen2026-10-19 08:49 UTC1429
            title = "^Tool Options$"
075d167f7479atagen2026-10-19 08:49 UTC1430
            floating = true
075d167f7479atagen2026-10-19 08:49 UTC1431
            opacity = 0.9
075d167f7479atagen2026-10-19 08:49 UTC1432
            "#,
075d167f7479atagen2026-10-19 08:49 UTC1433
        )
075d167f7479atagen2026-10-19 08:49 UTC1434
        .unwrap();
075d167f7479atagen2026-10-19 08:49 UTC1435
        let before = loaded
075d167f7479atagen2026-10-19 08:49 UTC1436
            .config
075d167f7479atagen2026-10-19 08:49 UTC1437
            .placement(&subject("gimp", "GNU Image Manipulation Program"));
075d167f7479atagen2026-10-19 08:49 UTC1438
        let after = Subject {
075d167f7479atagen2026-10-19 08:49 UTC1439
            title: "Tool Options",
075d167f7479atagen2026-10-19 08:49 UTC1440
            initial_title: "GNU Image Manipulation Program",
075d167f7479atagen2026-10-19 08:49 UTC1441
            ..subject("gimp", "")
075d167f7479atagen2026-10-19 08:49 UTC1442
        };
075d167f7479atagen2026-10-19 08:49 UTC1443
        let after = loaded.config.placement(&after);
075d167f7479atagen2026-10-19 08:49 UTC1444
        assert_eq!((before.floating, before.opacity), (None, None));
075d167f7479atagen2026-10-19 08:49 UTC1445
        assert_eq!((after.floating, after.opacity), (Some(true), Some(0.9)));
075d167f7479atagen2026-10-19 08:49 UTC1446
        assert_eq!(before.tag, after.tag);
075d167f7479atagen2026-10-19 08:49 UTC1447
    }
075d167f7479atagen2026-10-19 08:49 UTC1448
075d167f7479atagen2026-10-19 08:49 UTC1449
    #[test]
f4c93ddeb08fatagen2026-10-19 08:41 UTC1450
    fn size_bounds_treat_zero_as_open() {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1451
        let p = Placement {
f4c93ddeb08fatagen2026-10-19 08:41 UTC1452
            min_size: Some((200, 0)),
f4c93ddeb08fatagen2026-10-19 08:41 UTC1453
            max_size: Some((0, 300)),
f4c93ddeb08fatagen2026-10-19 08:41 UTC1454
            ..Placement::default()
f4c93ddeb08fatagen2026-10-19 08:41 UTC1455
        };
f4c93ddeb08fatagen2026-10-19 08:41 UTC1456
        assert_eq!(p.clamp_size(50.0, 900.0), (200.0, 300.0));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1457
        assert_eq!(p.clamp_size(4000.0, 10.0), (4000.0, 10.0));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1458
        assert_eq!(Placement::default().clamp_size(7.0, 9.0), (7.0, 9.0));
f4c93ddeb08fatagen2026-10-19 08:41 UTC1459
    }
fe835aed6c8datagen2026-10-16 10:05 UTC1460
}