atagen

monolith

public
Star0
Watch0
e2443dec4dcd5091452989ac02026bdaba0e764d
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,
fe835aed6c8datagen2026-10-16 10:05 UTC19
    pub binds: Vec<Bind>,
fe835aed6c8datagen2026-10-16 10:05 UTC20
    pub rules: Vec<Rule>,
fe835aed6c8datagen2026-10-16 10:05 UTC21
    pub startup: Vec<Vec<String>>,
fe835aed6c8datagen2026-10-16 10:05 UTC22
}
fe835aed6c8datagen2026-10-16 10:05 UTC23
fe835aed6c8datagen2026-10-16 10:05 UTC24
#[derive(Deserialize, Debug, Clone, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC25
#[serde(default, deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC26
pub struct General {
fe835aed6c8datagen2026-10-16 10:05 UTC27
    /// What `Mod` means in binds.
fe835aed6c8datagen2026-10-16 10:05 UTC28
    #[serde(rename = "mod")]
fe835aed6c8datagen2026-10-16 10:05 UTC29
    pub mod_key: Modifier,
fe835aed6c8datagen2026-10-16 10:05 UTC30
    pub gaps: Gaps,
4444c779eb48atagen2026-10-16 12:07 UTC31
    /// What a window move does when it meets a neighbour.
4444c779eb48atagen2026-10-16 12:07 UTC32
    #[serde(rename = "move")]
4444c779eb48atagen2026-10-16 12:07 UTC33
    pub move_mode: MoveMode,
24fca1aad608atagen2026-10-17 09:56 UTC34
    pub focus_follows_mouse: bool,
24fca1aad608atagen2026-10-17 09:56 UTC35
    pub mouse_follows_focus: bool,
45a383fca811atagen2026-10-17 11:31 UTC36
    /// The first window to land on or move to an empty tag nobody is
45a383fca811atagen2026-10-17 11:31 UTC37
    /// viewing brings that tag into view on its output.
47259c39793fatagen2026-10-17 10:13 UTC38
    pub view_tag_on_first_window: bool,
4444c779eb48atagen2026-10-16 12:07 UTC39
}
4444c779eb48atagen2026-10-16 12:07 UTC40
4444c779eb48atagen2026-10-16 12:07 UTC41
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
4444c779eb48atagen2026-10-16 12:07 UTC42
#[serde(rename_all = "lowercase")]
4444c779eb48atagen2026-10-16 12:07 UTC43
pub enum MoveMode {
4444c779eb48atagen2026-10-16 12:07 UTC44
    /// Trade places with it.
4444c779eb48atagen2026-10-16 12:07 UTC45
    #[default]
4444c779eb48atagen2026-10-16 12:07 UTC46
    Swap,
4444c779eb48atagen2026-10-16 12:07 UTC47
    /// Join its lane, stacking with it if it is alone.
4444c779eb48atagen2026-10-16 12:07 UTC48
    Merge,
fe835aed6c8datagen2026-10-16 10:05 UTC49
}
fe835aed6c8datagen2026-10-16 10:05 UTC50
fe835aed6c8datagen2026-10-16 10:05 UTC51
impl Default for General {
fe835aed6c8datagen2026-10-16 10:05 UTC52
    fn default() -> Self {
fe835aed6c8datagen2026-10-16 10:05 UTC53
        Self {
fe835aed6c8datagen2026-10-16 10:05 UTC54
            mod_key: Modifier::Super,
fe835aed6c8datagen2026-10-16 10:05 UTC55
            gaps: Gaps::default(),
4444c779eb48atagen2026-10-16 12:07 UTC56
            move_mode: MoveMode::Swap,
24fca1aad608atagen2026-10-17 09:56 UTC57
            focus_follows_mouse: false,
24fca1aad608atagen2026-10-17 09:56 UTC58
            mouse_follows_focus: false,
47259c39793fatagen2026-10-17 10:13 UTC59
            view_tag_on_first_window: true,
fe835aed6c8datagen2026-10-16 10:05 UTC60
        }
fe835aed6c8datagen2026-10-16 10:05 UTC61
    }
fe835aed6c8datagen2026-10-16 10:05 UTC62
}
fe835aed6c8datagen2026-10-16 10:05 UTC63
fe835aed6c8datagen2026-10-16 10:05 UTC64
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC65
#[serde(default, deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC66
pub struct Gaps {
fe835aed6c8datagen2026-10-16 10:05 UTC67
    pub inner: f64,
fe835aed6c8datagen2026-10-16 10:05 UTC68
    pub outer: f64,
fe835aed6c8datagen2026-10-16 10:05 UTC69
}
fe835aed6c8datagen2026-10-16 10:05 UTC70
fe835aed6c8datagen2026-10-16 10:05 UTC71
impl Default for Gaps {
fe835aed6c8datagen2026-10-16 10:05 UTC72
    fn default() -> Self {
fe835aed6c8datagen2026-10-16 10:05 UTC73
        Self {
fe835aed6c8datagen2026-10-16 10:05 UTC74
            inner: 8.0,
fe835aed6c8datagen2026-10-16 10:05 UTC75
            outer: 8.0,
fe835aed6c8datagen2026-10-16 10:05 UTC76
        }
fe835aed6c8datagen2026-10-16 10:05 UTC77
    }
fe835aed6c8datagen2026-10-16 10:05 UTC78
}
fe835aed6c8datagen2026-10-16 10:05 UTC79
8e6f96f0a32datagen2026-10-16 22:17 UTC80
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
9b87063809e1atagen2026-10-17 13:28 UTC81
#[serde(default)]
8e6f96f0a32datagen2026-10-16 22:17 UTC82
pub struct Outputs {
8e6f96f0a32datagen2026-10-16 22:17 UTC83
    pub vrr: Vrr,
9b87063809e1atagen2026-10-17 13:28 UTC84
    /// Settings for one connector, by name, like `DP-1`.
9b87063809e1atagen2026-10-17 13:28 UTC85
    #[serde(flatten)]
9b87063809e1atagen2026-10-17 13:28 UTC86
    pub named: std::collections::BTreeMap<String, OutputConfig>,
9b87063809e1atagen2026-10-17 13:28 UTC87
}
9b87063809e1atagen2026-10-17 13:28 UTC88
ea84a35c5001atagen2026-10-18 02:40 UTC89
/// Transform and position follow a reload. A new mode waits for a restart
ea84a35c5001atagen2026-10-18 02:40 UTC90
/// or a replug, since it means a modeset.
9b87063809e1atagen2026-10-17 13:28 UTC91
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
9b87063809e1atagen2026-10-17 13:28 UTC92
#[serde(default, deny_unknown_fields)]
9b87063809e1atagen2026-10-17 13:28 UTC93
pub struct OutputConfig {
9b87063809e1atagen2026-10-17 13:28 UTC94
    pub transform: OutputTransform,
9b87063809e1atagen2026-10-17 13:28 UTC95
    /// Top left corner in the global layout. Outputs without one are placed
9b87063809e1atagen2026-10-17 13:28 UTC96
    /// to the right of the others.
9b87063809e1atagen2026-10-17 13:28 UTC97
    pub position: Option<[i32; 2]>,
9b87063809e1atagen2026-10-17 13:28 UTC98
    /// `WIDTHxHEIGHT` or `WIDTHxHEIGHT@HZ`. Without a rate the highest one
9b87063809e1atagen2026-10-17 13:28 UTC99
    /// for that size wins.
9b87063809e1atagen2026-10-17 13:28 UTC100
    pub mode: Option<String>,
9b87063809e1atagen2026-10-17 13:28 UTC101
}
9b87063809e1atagen2026-10-17 13:28 UTC102
9b87063809e1atagen2026-10-17 13:28 UTC103
/// Counterclockwise rotation, optionally after flipping horizontally.
9b87063809e1atagen2026-10-17 13:28 UTC104
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
9b87063809e1atagen2026-10-17 13:28 UTC105
#[serde(rename_all = "kebab-case")]
9b87063809e1atagen2026-10-17 13:28 UTC106
pub enum OutputTransform {
9b87063809e1atagen2026-10-17 13:28 UTC107
    #[default]
9b87063809e1atagen2026-10-17 13:28 UTC108
    Normal,
9b87063809e1atagen2026-10-17 13:28 UTC109
    #[serde(rename = "90")]
9b87063809e1atagen2026-10-17 13:28 UTC110
    Rotate90,
9b87063809e1atagen2026-10-17 13:28 UTC111
    #[serde(rename = "180")]
9b87063809e1atagen2026-10-17 13:28 UTC112
    Rotate180,
9b87063809e1atagen2026-10-17 13:28 UTC113
    #[serde(rename = "270")]
9b87063809e1atagen2026-10-17 13:28 UTC114
    Rotate270,
9b87063809e1atagen2026-10-17 13:28 UTC115
    Flipped,
9b87063809e1atagen2026-10-17 13:28 UTC116
    #[serde(rename = "flipped-90")]
9b87063809e1atagen2026-10-17 13:28 UTC117
    Flipped90,
9b87063809e1atagen2026-10-17 13:28 UTC118
    #[serde(rename = "flipped-180")]
9b87063809e1atagen2026-10-17 13:28 UTC119
    Flipped180,
9b87063809e1atagen2026-10-17 13:28 UTC120
    #[serde(rename = "flipped-270")]
9b87063809e1atagen2026-10-17 13:28 UTC121
    Flipped270,
8e6f96f0a32datagen2026-10-16 22:17 UTC122
}
8e6f96f0a32datagen2026-10-16 22:17 UTC123
8e6f96f0a32datagen2026-10-16 22:17 UTC124
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
8e6f96f0a32datagen2026-10-16 22:17 UTC125
#[serde(rename_all = "lowercase")]
8e6f96f0a32datagen2026-10-16 22:17 UTC126
pub enum Vrr {
8e6f96f0a32datagen2026-10-16 22:17 UTC127
    Off,
8e6f96f0a32datagen2026-10-16 22:17 UTC128
    On,
8e6f96f0a32datagen2026-10-16 22:17 UTC129
    /// Only while a fullscreen window is shown, where a game sets the pace.
8e6f96f0a32datagen2026-10-16 22:17 UTC130
    #[default]
8e6f96f0a32datagen2026-10-16 22:17 UTC131
    Fullscreen,
8e6f96f0a32datagen2026-10-16 22:17 UTC132
}
8e6f96f0a32datagen2026-10-16 22:17 UTC133
fe835aed6c8datagen2026-10-16 10:05 UTC134
#[derive(Deserialize, Debug, Clone, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC135
#[serde(default, deny_unknown_fields)]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC136
pub struct Decoration {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC137
    /// Border width in logical pixels, drawn in the gap around each window.
7ce6ae4e5a85atagen2026-10-16 12:16 UTC138
    pub border: u32,
47259c39793fatagen2026-10-17 10:13 UTC139
    pub radius: Corners,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC140
    pub focused: Color,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC141
    pub unfocused: Color,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC142
}
7ce6ae4e5a85atagen2026-10-16 12:16 UTC143
47259c39793fatagen2026-10-17 10:13 UTC144
/// Corner radii in logical pixels, top left, top right, bottom right and
47259c39793fatagen2026-10-17 10:13 UTC145
/// bottom left. One number rounds every corner alike.
47259c39793fatagen2026-10-17 10:13 UTC146
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
47259c39793fatagen2026-10-17 10:13 UTC147
#[serde(from = "CornersToml")]
47259c39793fatagen2026-10-17 10:13 UTC148
pub struct Corners(pub [u32; 4]);
47259c39793fatagen2026-10-17 10:13 UTC149
47259c39793fatagen2026-10-17 10:13 UTC150
#[derive(Deserialize)]
8fb62aebf501atagen2026-10-17 10:25 UTC151
#[serde(
8fb62aebf501atagen2026-10-17 10:25 UTC152
    untagged,
8fb62aebf501atagen2026-10-17 10:25 UTC153
    expecting = "expected a number or [top left, top right, bottom right, bottom left]"
8fb62aebf501atagen2026-10-17 10:25 UTC154
)]
47259c39793fatagen2026-10-17 10:13 UTC155
enum CornersToml {
47259c39793fatagen2026-10-17 10:13 UTC156
    All(u32),
47259c39793fatagen2026-10-17 10:13 UTC157
    Each([u32; 4]),
47259c39793fatagen2026-10-17 10:13 UTC158
}
47259c39793fatagen2026-10-17 10:13 UTC159
47259c39793fatagen2026-10-17 10:13 UTC160
impl From<CornersToml> for Corners {
47259c39793fatagen2026-10-17 10:13 UTC161
    fn from(c: CornersToml) -> Self {
47259c39793fatagen2026-10-17 10:13 UTC162
        match c {
47259c39793fatagen2026-10-17 10:13 UTC163
            CornersToml::All(r) => Self([r; 4]),
47259c39793fatagen2026-10-17 10:13 UTC164
            CornersToml::Each(r) => Self(r),
47259c39793fatagen2026-10-17 10:13 UTC165
        }
47259c39793fatagen2026-10-17 10:13 UTC166
    }
47259c39793fatagen2026-10-17 10:13 UTC167
}
47259c39793fatagen2026-10-17 10:13 UTC168
7ce6ae4e5a85atagen2026-10-16 12:16 UTC169
impl Default for Decoration {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC170
    fn default() -> Self {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC171
        Self {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC172
            border: 2,
47259c39793fatagen2026-10-17 10:13 UTC173
            radius: Corners([0; 4]),
b22c0511d5afatagen2026-10-17 03:48 UTC174
            focused: Color::rgb(0x88, 0xbf, 0xd1),
b22c0511d5afatagen2026-10-17 03:48 UTC175
            unfocused: Color::rgb(0x3b, 0x42, 0x52),
7ce6ae4e5a85atagen2026-10-16 12:16 UTC176
        }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC177
    }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC178
}
7ce6ae4e5a85atagen2026-10-16 12:16 UTC179
7ce6ae4e5a85atagen2026-10-16 12:16 UTC180
/// Straight RGBA from `#rrggbb` or `#rrggbbaa`.
7ce6ae4e5a85atagen2026-10-16 12:16 UTC181
#[derive(Debug, Clone, Copy, PartialEq)]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC182
pub struct Color(pub [f32; 4]);
7ce6ae4e5a85atagen2026-10-16 12:16 UTC183
b22c0511d5afatagen2026-10-17 03:48 UTC184
impl Color {
b22c0511d5afatagen2026-10-17 03:48 UTC185
    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
b22c0511d5afatagen2026-10-17 03:48 UTC186
        Self([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
b22c0511d5afatagen2026-10-17 03:48 UTC187
    }
b22c0511d5afatagen2026-10-17 03:48 UTC188
}
b22c0511d5afatagen2026-10-17 03:48 UTC189
7ce6ae4e5a85atagen2026-10-16 12:16 UTC190
impl<'de> Deserialize<'de> for Color {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC191
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC192
        let s = String::deserialize(d)?;
7ce6ae4e5a85atagen2026-10-16 12:16 UTC193
        let hex = s.strip_prefix('#').unwrap_or(&s);
7ce6ae4e5a85atagen2026-10-16 12:16 UTC194
        let bad =
7ce6ae4e5a85atagen2026-10-16 12:16 UTC195
            || serde::de::Error::custom(format!("bad colour {s:?}, use #rrggbb or #rrggbbaa"));
7ce6ae4e5a85atagen2026-10-16 12:16 UTC196
        if !matches!(hex.len(), 6 | 8) || !hex.is_ascii() {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC197
            return Err(bad());
7ce6ae4e5a85atagen2026-10-16 12:16 UTC198
        }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC199
        let mut c = [1.0f32; 4];
7ce6ae4e5a85atagen2026-10-16 12:16 UTC200
        for (i, v) in c.iter_mut().enumerate().take(hex.len() / 2) {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC201
            let byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| bad())?;
7ce6ae4e5a85atagen2026-10-16 12:16 UTC202
            *v = f32::from(byte) / 255.0;
7ce6ae4e5a85atagen2026-10-16 12:16 UTC203
        }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC204
        Ok(Self(c))
7ce6ae4e5a85atagen2026-10-16 12:16 UTC205
    }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC206
}
7ce6ae4e5a85atagen2026-10-16 12:16 UTC207
7ce6ae4e5a85atagen2026-10-16 12:16 UTC208
#[derive(Deserialize, Debug, Clone, PartialEq)]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC209
#[serde(default, deny_unknown_fields)]
2af1d8c55876atagen2026-10-16 11:05 UTC210
pub struct Animation {
2af1d8c55876atagen2026-10-16 11:05 UTC211
    pub enabled: bool,
2af1d8c55876atagen2026-10-16 11:05 UTC212
    pub style: AnimationStyle,
2af1d8c55876atagen2026-10-16 11:05 UTC213
    /// Every phase of a planned transition lasts this long.
2af1d8c55876atagen2026-10-16 11:05 UTC214
    pub phase_ms: u64,
2af1d8c55876atagen2026-10-16 11:05 UTC215
    /// A cubic bezier, as in CSS.
2af1d8c55876atagen2026-10-16 11:05 UTC216
    pub curve: [f64; 4],
2af1d8c55876atagen2026-10-16 11:05 UTC217
}
2af1d8c55876atagen2026-10-16 11:05 UTC218
2af1d8c55876atagen2026-10-16 11:05 UTC219
impl Default for Animation {
2af1d8c55876atagen2026-10-16 11:05 UTC220
    fn default() -> Self {
2af1d8c55876atagen2026-10-16 11:05 UTC221
        Self {
2af1d8c55876atagen2026-10-16 11:05 UTC222
            enabled: true,
2af1d8c55876atagen2026-10-16 11:05 UTC223
            style: AnimationStyle::Orthogonal,
2af1d8c55876atagen2026-10-16 11:05 UTC224
            phase_ms: 160,
2af1d8c55876atagen2026-10-16 11:05 UTC225
            curve: [0.45, 0.0, 0.2, 1.0],
2af1d8c55876atagen2026-10-16 11:05 UTC226
        }
2af1d8c55876atagen2026-10-16 11:05 UTC227
    }
2af1d8c55876atagen2026-10-16 11:05 UTC228
}
2af1d8c55876atagen2026-10-16 11:05 UTC229
2af1d8c55876atagen2026-10-16 11:05 UTC230
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
2af1d8c55876atagen2026-10-16 11:05 UTC231
#[serde(rename_all = "lowercase")]
2af1d8c55876atagen2026-10-16 11:05 UTC232
pub enum AnimationStyle {
2af1d8c55876atagen2026-10-16 11:05 UTC233
    Orthogonal,
2af1d8c55876atagen2026-10-16 11:05 UTC234
    Direct,
2af1d8c55876atagen2026-10-16 11:05 UTC235
}
2af1d8c55876atagen2026-10-16 11:05 UTC236
2af1d8c55876atagen2026-10-16 11:05 UTC237
#[derive(Deserialize, Debug, Clone, PartialEq)]
2af1d8c55876atagen2026-10-16 11:05 UTC238
#[serde(default, deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC239
pub struct Keyboard {
fe835aed6c8datagen2026-10-16 10:05 UTC240
    pub rules: String,
fe835aed6c8datagen2026-10-16 10:05 UTC241
    pub model: String,
fe835aed6c8datagen2026-10-16 10:05 UTC242
    pub layout: String,
fe835aed6c8datagen2026-10-16 10:05 UTC243
    pub variant: String,
fe835aed6c8datagen2026-10-16 10:05 UTC244
    pub options: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC245
    pub repeat_delay: i32,
fe835aed6c8datagen2026-10-16 10:05 UTC246
    pub repeat_rate: i32,
fe835aed6c8datagen2026-10-16 10:05 UTC247
}
fe835aed6c8datagen2026-10-16 10:05 UTC248
fe835aed6c8datagen2026-10-16 10:05 UTC249
impl Default for Keyboard {
fe835aed6c8datagen2026-10-16 10:05 UTC250
    fn default() -> Self {
fe835aed6c8datagen2026-10-16 10:05 UTC251
        Self {
fe835aed6c8datagen2026-10-16 10:05 UTC252
            rules: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC253
            model: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC254
            layout: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC255
            variant: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC256
            options: None,
fe835aed6c8datagen2026-10-16 10:05 UTC257
            repeat_delay: 200,
fe835aed6c8datagen2026-10-16 10:05 UTC258
            repeat_rate: 25,
fe835aed6c8datagen2026-10-16 10:05 UTC259
        }
fe835aed6c8datagen2026-10-16 10:05 UTC260
    }
fe835aed6c8datagen2026-10-16 10:05 UTC261
}
fe835aed6c8datagen2026-10-16 10:05 UTC262
fe835aed6c8datagen2026-10-16 10:05 UTC263
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
fe835aed6c8datagen2026-10-16 10:05 UTC264
#[serde(rename_all = "lowercase")]
fe835aed6c8datagen2026-10-16 10:05 UTC265
pub enum Modifier {
fe835aed6c8datagen2026-10-16 10:05 UTC266
    Super,
fe835aed6c8datagen2026-10-16 10:05 UTC267
    Alt,
fe835aed6c8datagen2026-10-16 10:05 UTC268
    Ctrl,
fe835aed6c8datagen2026-10-16 10:05 UTC269
    Shift,
fe835aed6c8datagen2026-10-16 10:05 UTC270
}
fe835aed6c8datagen2026-10-16 10:05 UTC271
fe835aed6c8datagen2026-10-16 10:05 UTC272
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
fe835aed6c8datagen2026-10-16 10:05 UTC273
pub struct Mods {
fe835aed6c8datagen2026-10-16 10:05 UTC274
    pub logo: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC275
    pub alt: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC276
    pub ctrl: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC277
    pub shift: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC278
}
fe835aed6c8datagen2026-10-16 10:05 UTC279
fe835aed6c8datagen2026-10-16 10:05 UTC280
impl Mods {
b22c0511d5afatagen2026-10-17 03:48 UTC281
    pub fn has(&self, m: Modifier) -> bool {
b22c0511d5afatagen2026-10-17 03:48 UTC282
        match m {
b22c0511d5afatagen2026-10-17 03:48 UTC283
            Modifier::Super => self.logo,
b22c0511d5afatagen2026-10-17 03:48 UTC284
            Modifier::Alt => self.alt,
b22c0511d5afatagen2026-10-17 03:48 UTC285
            Modifier::Ctrl => self.ctrl,
b22c0511d5afatagen2026-10-17 03:48 UTC286
            Modifier::Shift => self.shift,
b22c0511d5afatagen2026-10-17 03:48 UTC287
        }
b22c0511d5afatagen2026-10-17 03:48 UTC288
    }
b22c0511d5afatagen2026-10-17 03:48 UTC289
fe835aed6c8datagen2026-10-16 10:05 UTC290
    fn set(&mut self, m: Modifier) {
fe835aed6c8datagen2026-10-16 10:05 UTC291
        match m {
fe835aed6c8datagen2026-10-16 10:05 UTC292
            Modifier::Super => self.logo = true,
fe835aed6c8datagen2026-10-16 10:05 UTC293
            Modifier::Alt => self.alt = true,
fe835aed6c8datagen2026-10-16 10:05 UTC294
            Modifier::Ctrl => self.ctrl = true,
fe835aed6c8datagen2026-10-16 10:05 UTC295
            Modifier::Shift => self.shift = true,
fe835aed6c8datagen2026-10-16 10:05 UTC296
        }
fe835aed6c8datagen2026-10-16 10:05 UTC297
    }
fe835aed6c8datagen2026-10-16 10:05 UTC298
}
fe835aed6c8datagen2026-10-16 10:05 UTC299
fe835aed6c8datagen2026-10-16 10:05 UTC300
#[derive(Debug, Clone, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC301
pub struct Bind {
fe835aed6c8datagen2026-10-16 10:05 UTC302
    pub mods: Mods,
fe835aed6c8datagen2026-10-16 10:05 UTC303
    /// An xkb keysym name, resolved by the compositor.
fe835aed6c8datagen2026-10-16 10:05 UTC304
    pub key: String,
fe835aed6c8datagen2026-10-16 10:05 UTC305
    pub command: Command,
fe835aed6c8datagen2026-10-16 10:05 UTC306
}
fe835aed6c8datagen2026-10-16 10:05 UTC307
fe835aed6c8datagen2026-10-16 10:05 UTC308
#[derive(Debug, Clone)]
fe835aed6c8datagen2026-10-16 10:05 UTC309
pub struct Rule {
fe835aed6c8datagen2026-10-16 10:05 UTC310
    pub app_id: Option<Regex>,
fe835aed6c8datagen2026-10-16 10:05 UTC311
    pub title: Option<Regex>,
fe835aed6c8datagen2026-10-16 10:05 UTC312
    pub tag: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC313
    pub output: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC314
    pub floating: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC315
    pub sticky: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC316
    pub scratchpad: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC317
}
fe835aed6c8datagen2026-10-16 10:05 UTC318
fe835aed6c8datagen2026-10-16 10:05 UTC319
/// Everything the matching rules say about a window, later rules winning.
fe835aed6c8datagen2026-10-16 10:05 UTC320
#[derive(Debug, Clone, Default, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC321
pub struct Placement {
fe835aed6c8datagen2026-10-16 10:05 UTC322
    pub tag: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC323
    pub output: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC324
    pub floating: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC325
    pub sticky: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC326
    pub scratchpad: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC327
}
fe835aed6c8datagen2026-10-16 10:05 UTC328
fe835aed6c8datagen2026-10-16 10:05 UTC329
#[derive(Debug)]
fe835aed6c8datagen2026-10-16 10:05 UTC330
pub enum Error {
fe835aed6c8datagen2026-10-16 10:05 UTC331
    Io(PathBuf, std::io::Error),
fe835aed6c8datagen2026-10-16 10:05 UTC332
    Toml(toml::de::Error),
fe835aed6c8datagen2026-10-16 10:05 UTC333
}
fe835aed6c8datagen2026-10-16 10:05 UTC334
fe835aed6c8datagen2026-10-16 10:05 UTC335
impl fmt::Display for Error {
fe835aed6c8datagen2026-10-16 10:05 UTC336
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fe835aed6c8datagen2026-10-16 10:05 UTC337
        match self {
fe835aed6c8datagen2026-10-16 10:05 UTC338
            Error::Io(p, e) => write!(f, "{}: {e}", p.display()),
fe835aed6c8datagen2026-10-16 10:05 UTC339
            Error::Toml(e) => write!(f, "{e}"),
fe835aed6c8datagen2026-10-16 10:05 UTC340
        }
fe835aed6c8datagen2026-10-16 10:05 UTC341
    }
fe835aed6c8datagen2026-10-16 10:05 UTC342
}
fe835aed6c8datagen2026-10-16 10:05 UTC343
fe835aed6c8datagen2026-10-16 10:05 UTC344
impl std::error::Error for Error {}
fe835aed6c8datagen2026-10-16 10:05 UTC345
fe835aed6c8datagen2026-10-16 10:05 UTC346
#[derive(Deserialize)]
fe835aed6c8datagen2026-10-16 10:05 UTC347
#[serde(untagged)]
fe835aed6c8datagen2026-10-16 10:05 UTC348
enum Words {
fe835aed6c8datagen2026-10-16 10:05 UTC349
    Line(String),
fe835aed6c8datagen2026-10-16 10:05 UTC350
    List(Vec<String>),
fe835aed6c8datagen2026-10-16 10:05 UTC351
}
fe835aed6c8datagen2026-10-16 10:05 UTC352
fe835aed6c8datagen2026-10-16 10:05 UTC353
#[derive(Deserialize)]
fe835aed6c8datagen2026-10-16 10:05 UTC354
#[serde(deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC355
struct RawRule {
fe835aed6c8datagen2026-10-16 10:05 UTC356
    app_id: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC357
    title: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC358
    tag: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC359
    output: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC360
    floating: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC361
    sticky: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC362
    scratchpad: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC363
}
fe835aed6c8datagen2026-10-16 10:05 UTC364
fe835aed6c8datagen2026-10-16 10:05 UTC365
impl Config {
fe835aed6c8datagen2026-10-16 10:05 UTC366
    /// `$MONOLITH_CONFIG`, else `$XDG_CONFIG_HOME/monolith/config.toml`.
fe835aed6c8datagen2026-10-16 10:05 UTC367
    pub fn path() -> Option<PathBuf> {
fe835aed6c8datagen2026-10-16 10:05 UTC368
        if let Some(p) = std::env::var_os("MONOLITH_CONFIG") {
fe835aed6c8datagen2026-10-16 10:05 UTC369
            return Some(p.into());
fe835aed6c8datagen2026-10-16 10:05 UTC370
        }
fe835aed6c8datagen2026-10-16 10:05 UTC371
        let base = std::env::var_os("XDG_CONFIG_HOME")
fe835aed6c8datagen2026-10-16 10:05 UTC372
            .map(PathBuf::from)
fe835aed6c8datagen2026-10-16 10:05 UTC373
            .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
fe835aed6c8datagen2026-10-16 10:05 UTC374
        Some(base.join("monolith/config.toml"))
fe835aed6c8datagen2026-10-16 10:05 UTC375
    }
fe835aed6c8datagen2026-10-16 10:05 UTC376
fe835aed6c8datagen2026-10-16 10:05 UTC377
    /// Loads `path`, or the built-in default when it does not exist.
8fb62aebf501atagen2026-10-17 10:25 UTC378
    pub fn load(path: Option<&Path>) -> Result<Loaded, Error> {
fe835aed6c8datagen2026-10-16 10:05 UTC379
        match path {
fe835aed6c8datagen2026-10-16 10:05 UTC380
            Some(p) => match std::fs::read_to_string(p) {
fe835aed6c8datagen2026-10-16 10:05 UTC381
                Ok(text) => Self::parse(&text),
fe835aed6c8datagen2026-10-16 10:05 UTC382
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::parse(DEFAULT),
fe835aed6c8datagen2026-10-16 10:05 UTC383
                Err(e) => Err(Error::Io(p.to_owned(), e)),
fe835aed6c8datagen2026-10-16 10:05 UTC384
            },
fe835aed6c8datagen2026-10-16 10:05 UTC385
            None => Self::parse(DEFAULT),
fe835aed6c8datagen2026-10-16 10:05 UTC386
        }
fe835aed6c8datagen2026-10-16 10:05 UTC387
    }
fe835aed6c8datagen2026-10-16 10:05 UTC388
8fb62aebf501atagen2026-10-17 10:25 UTC389
    /// Only broken TOML fails. A bad option, bind or rule is left out with a
8fb62aebf501atagen2026-10-17 10:25 UTC390
    /// warning, so one typo does not throw away the rest of the file.
8fb62aebf501atagen2026-10-17 10:25 UTC391
    pub fn parse(text: &str) -> Result<Loaded, Error> {
8fb62aebf501atagen2026-10-17 10:25 UTC392
        let mut table: toml::Table = toml::from_str(text).map_err(Error::Toml)?;
8fb62aebf501atagen2026-10-17 10:25 UTC393
        let mut warnings = Vec::new();
8fb62aebf501atagen2026-10-17 10:25 UTC394
        let w = &mut warnings;
8fb62aebf501atagen2026-10-17 10:25 UTC395
        let general: General = section("general", table.remove("general"), w);
8fb62aebf501atagen2026-10-17 10:25 UTC396
        let keyboard = section("keyboard", table.remove("keyboard"), w);
8fb62aebf501atagen2026-10-17 10:25 UTC397
        let animation = section("animation", table.remove("animation"), w);
8fb62aebf501atagen2026-10-17 10:25 UTC398
        let decoration = section("decoration", table.remove("decoration"), w);
0f513a2420c0atagen2026-10-18 02:40 UTC399
        let output: Outputs = section("output", table.remove("output"), w);
0f513a2420c0atagen2026-10-18 02:40 UTC400
        for name in output.named.keys().filter(|n| !connector_like(n)) {
0f513a2420c0atagen2026-10-18 02:40 UTC401
            w.push(format!(
0f513a2420c0atagen2026-10-18 02:40 UTC402
                "output.{name}: not a connector name like DP-1 or HDMI-A-1, matches nothing"
0f513a2420c0atagen2026-10-18 02:40 UTC403
            ));
0f513a2420c0atagen2026-10-18 02:40 UTC404
        }
8fb62aebf501atagen2026-10-17 10:25 UTC405
        let startup = items("startup", table.remove("startup"), w);
8fb62aebf501atagen2026-10-17 10:25 UTC406
        let mut binds = Vec::new();
8fb62aebf501atagen2026-10-17 10:25 UTC407
        if let Some(value) = table.remove("binds") {
8fb62aebf501atagen2026-10-17 10:25 UTC408
            let entries: toml::Table = entry("binds", value, w).unwrap_or_default();
8fb62aebf501atagen2026-10-17 10:25 UTC409
            for (combo, value) in entries {
8fb62aebf501atagen2026-10-17 10:25 UTC410
                let name = format!("binds.{combo:?}");
8fb62aebf501atagen2026-10-17 10:25 UTC411
                let bind = entry::<Words>(&name, value, w)
8fb62aebf501atagen2026-10-17 10:25 UTC412
                    .map(|words| parse_bind(&combo, words, general.mod_key));
8fb62aebf501atagen2026-10-17 10:25 UTC413
                match bind {
8fb62aebf501atagen2026-10-17 10:25 UTC414
                    Some(Ok(b)) => binds.push(b),
8fb62aebf501atagen2026-10-17 10:25 UTC415
                    Some(Err(e)) => w.push(format!("{name}: {e}, left out")),
8fb62aebf501atagen2026-10-17 10:25 UTC416
                    None => {}
8fb62aebf501atagen2026-10-17 10:25 UTC417
                }
8fb62aebf501atagen2026-10-17 10:25 UTC418
            }
8fb62aebf501atagen2026-10-17 10:25 UTC419
        }
8fb62aebf501atagen2026-10-17 10:25 UTC420
        let regex = |p: Option<String>| p.map(|p| Regex::new(&p)).transpose();
8fb62aebf501atagen2026-10-17 10:25 UTC421
        let mut rules = Vec::new();
8fb62aebf501atagen2026-10-17 10:25 UTC422
        for (i, raw) in items::<RawRule>("rules", table.remove("rules"), w)
fe835aed6c8datagen2026-10-16 10:05 UTC423
            .into_iter()
8fb62aebf501atagen2026-10-17 10:25 UTC424
            .enumerate()
8fb62aebf501atagen2026-10-17 10:25 UTC425
        {
8fb62aebf501atagen2026-10-17 10:25 UTC426
            match (regex(raw.app_id), regex(raw.title)) {
8fb62aebf501atagen2026-10-17 10:25 UTC427
                (Ok(app_id), Ok(title)) => rules.push(Rule {
8fb62aebf501atagen2026-10-17 10:25 UTC428
                    app_id,
8fb62aebf501atagen2026-10-17 10:25 UTC429
                    title,
8fb62aebf501atagen2026-10-17 10:25 UTC430
                    tag: raw.tag,
8fb62aebf501atagen2026-10-17 10:25 UTC431
                    output: raw.output,
8fb62aebf501atagen2026-10-17 10:25 UTC432
                    floating: raw.floating,
8fb62aebf501atagen2026-10-17 10:25 UTC433
                    sticky: raw.sticky,
8fb62aebf501atagen2026-10-17 10:25 UTC434
                    scratchpad: raw.scratchpad,
8fb62aebf501atagen2026-10-17 10:25 UTC435
                }),
8fb62aebf501atagen2026-10-17 10:25 UTC436
                (Err(e), _) | (_, Err(e)) => {
8fb62aebf501atagen2026-10-17 10:25 UTC437
                    let e = e
8fb62aebf501atagen2026-10-17 10:25 UTC438
                        .to_string()
8fb62aebf501atagen2026-10-17 10:25 UTC439
                        .split_whitespace()
8fb62aebf501atagen2026-10-17 10:25 UTC440
                        .collect::<Vec<_>>()
8fb62aebf501atagen2026-10-17 10:25 UTC441
                        .join(" ");
8fb62aebf501atagen2026-10-17 10:25 UTC442
                    w.push(format!("rules[{i}]: {e}, left out"));
8fb62aebf501atagen2026-10-17 10:25 UTC443
                }
8fb62aebf501atagen2026-10-17 10:25 UTC444
            }
8fb62aebf501atagen2026-10-17 10:25 UTC445
        }
8fb62aebf501atagen2026-10-17 10:25 UTC446
        for key in table.keys() {
8fb62aebf501atagen2026-10-17 10:25 UTC447
            w.push(format!("unknown section [{key}], ignored"));
8fb62aebf501atagen2026-10-17 10:25 UTC448
        }
8fb62aebf501atagen2026-10-17 10:25 UTC449
        let config = Self {
8fb62aebf501atagen2026-10-17 10:25 UTC450
            general,
8fb62aebf501atagen2026-10-17 10:25 UTC451
            animation,
8fb62aebf501atagen2026-10-17 10:25 UTC452
            decoration,
8fb62aebf501atagen2026-10-17 10:25 UTC453
            output,
8fb62aebf501atagen2026-10-17 10:25 UTC454
            keyboard,
fe835aed6c8datagen2026-10-16 10:05 UTC455
            binds,
fe835aed6c8datagen2026-10-16 10:05 UTC456
            rules,
8fb62aebf501atagen2026-10-17 10:25 UTC457
            startup,
8fb62aebf501atagen2026-10-17 10:25 UTC458
        };
8fb62aebf501atagen2026-10-17 10:25 UTC459
        Ok(Loaded { config, warnings })
fe835aed6c8datagen2026-10-16 10:05 UTC460
    }
fe835aed6c8datagen2026-10-16 10:05 UTC461
fe835aed6c8datagen2026-10-16 10:05 UTC462
    pub fn placement(&self, app_id: &str, title: &str) -> Placement {
fe835aed6c8datagen2026-10-16 10:05 UTC463
        let mut p = Placement::default();
fe835aed6c8datagen2026-10-16 10:05 UTC464
        let hit = |re: &Option<Regex>, s: &str| re.as_ref().is_none_or(|re| re.is_match(s));
fe835aed6c8datagen2026-10-16 10:05 UTC465
        for r in &self.rules {
fe835aed6c8datagen2026-10-16 10:05 UTC466
            if !hit(&r.app_id, app_id) || !hit(&r.title, title) {
fe835aed6c8datagen2026-10-16 10:05 UTC467
                continue;
fe835aed6c8datagen2026-10-16 10:05 UTC468
            }
fe835aed6c8datagen2026-10-16 10:05 UTC469
            p.tag = r.tag.clone().or(p.tag);
fe835aed6c8datagen2026-10-16 10:05 UTC470
            p.output = r.output.clone().or(p.output);
fe835aed6c8datagen2026-10-16 10:05 UTC471
            p.floating = r.floating.or(p.floating);
fe835aed6c8datagen2026-10-16 10:05 UTC472
            p.sticky = r.sticky.or(p.sticky);
fe835aed6c8datagen2026-10-16 10:05 UTC473
            p.scratchpad = r.scratchpad.clone().or(p.scratchpad);
fe835aed6c8datagen2026-10-16 10:05 UTC474
        }
fe835aed6c8datagen2026-10-16 10:05 UTC475
        p
fe835aed6c8datagen2026-10-16 10:05 UTC476
    }
fe835aed6c8datagen2026-10-16 10:05 UTC477
}
fe835aed6c8datagen2026-10-16 10:05 UTC478
8fb62aebf501atagen2026-10-17 10:25 UTC479
fn parse_bind(combo: &str, words: Words, mod_key: Modifier) -> Result<Bind, String> {
fe835aed6c8datagen2026-10-16 10:05 UTC480
    let parts: Vec<&str> = combo.split('+').map(str::trim).collect();
fe835aed6c8datagen2026-10-16 10:05 UTC481
    let Some((&key, mods)) = parts.split_last().filter(|(k, _)| !k.is_empty()) else {
8fb62aebf501atagen2026-10-17 10:25 UTC482
        return Err("no key".into());
fe835aed6c8datagen2026-10-16 10:05 UTC483
    };
fe835aed6c8datagen2026-10-16 10:05 UTC484
    let mut set = Mods::default();
fe835aed6c8datagen2026-10-16 10:05 UTC485
    for m in mods {
fe835aed6c8datagen2026-10-16 10:05 UTC486
        let m = match m.to_ascii_lowercase().as_str() {
fe835aed6c8datagen2026-10-16 10:05 UTC487
            "mod" => mod_key,
fe835aed6c8datagen2026-10-16 10:05 UTC488
            "super" | "logo" | "win" => Modifier::Super,
fe835aed6c8datagen2026-10-16 10:05 UTC489
            "alt" => Modifier::Alt,
fe835aed6c8datagen2026-10-16 10:05 UTC490
            "ctrl" | "control" => Modifier::Ctrl,
fe835aed6c8datagen2026-10-16 10:05 UTC491
            "shift" => Modifier::Shift,
8fb62aebf501atagen2026-10-17 10:25 UTC492
            other => return Err(format!("unknown modifier {other}")),
fe835aed6c8datagen2026-10-16 10:05 UTC493
        };
fe835aed6c8datagen2026-10-16 10:05 UTC494
        set.set(m);
fe835aed6c8datagen2026-10-16 10:05 UTC495
    }
fe835aed6c8datagen2026-10-16 10:05 UTC496
    let words: Vec<String> = match words {
fe835aed6c8datagen2026-10-16 10:05 UTC497
        Words::Line(s) => s.split_whitespace().map(str::to_owned).collect(),
fe835aed6c8datagen2026-10-16 10:05 UTC498
        Words::List(v) => v,
fe835aed6c8datagen2026-10-16 10:05 UTC499
    };
8fb62aebf501atagen2026-10-17 10:25 UTC500
    let command = Command::parse(&words).map_err(|e| e.0)?;
fe835aed6c8datagen2026-10-16 10:05 UTC501
    Ok(Bind {
fe835aed6c8datagen2026-10-16 10:05 UTC502
        mods: set,
fe835aed6c8datagen2026-10-16 10:05 UTC503
        key: key.to_owned(),
fe835aed6c8datagen2026-10-16 10:05 UTC504
        command,
fe835aed6c8datagen2026-10-16 10:05 UTC505
    })
fe835aed6c8datagen2026-10-16 10:05 UTC506
}
fe835aed6c8datagen2026-10-16 10:05 UTC507
8fb62aebf501atagen2026-10-17 10:25 UTC508
/// A config and what had to be left out of it.
8fb62aebf501atagen2026-10-17 10:25 UTC509
pub struct Loaded {
8fb62aebf501atagen2026-10-17 10:25 UTC510
    pub config: Config,
8fb62aebf501atagen2026-10-17 10:25 UTC511
    pub warnings: Vec<String>,
8fb62aebf501atagen2026-10-17 10:25 UTC512
}
8fb62aebf501atagen2026-10-17 10:25 UTC513
8fb62aebf501atagen2026-10-17 10:25 UTC514
fn entry<T: DeserializeOwned>(name: &str, value: toml::Value, w: &mut Vec<String>) -> Option<T> {
8fb62aebf501atagen2026-10-17 10:25 UTC515
    value
8fb62aebf501atagen2026-10-17 10:25 UTC516
        .try_into()
8fb62aebf501atagen2026-10-17 10:25 UTC517
        .map_err(|e| w.push(format!("{name}: {}, left out", one_line(&e))))
8fb62aebf501atagen2026-10-17 10:25 UTC518
        .ok()
8fb62aebf501atagen2026-10-17 10:25 UTC519
}
8fb62aebf501atagen2026-10-17 10:25 UTC520
0f513a2420c0atagen2026-10-18 02:40 UTC521
fn connector_like(name: &str) -> bool {
0f513a2420c0atagen2026-10-18 02:40 UTC522
    name.rsplit_once('-').is_some_and(|(kind, n)| {
0f513a2420c0atagen2026-10-18 02:40 UTC523
        !kind.is_empty() && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())
0f513a2420c0atagen2026-10-18 02:40 UTC524
    })
0f513a2420c0atagen2026-10-18 02:40 UTC525
}
0f513a2420c0atagen2026-10-18 02:40 UTC526
8fb62aebf501atagen2026-10-17 10:25 UTC527
/// A section with every option that does not fit left at its default.
8fb62aebf501atagen2026-10-17 10:25 UTC528
fn section<T: DeserializeOwned + Default>(
8fb62aebf501atagen2026-10-17 10:25 UTC529
    name: &str,
8fb62aebf501atagen2026-10-17 10:25 UTC530
    value: Option<toml::Value>,
8fb62aebf501atagen2026-10-17 10:25 UTC531
    w: &mut Vec<String>,
8fb62aebf501atagen2026-10-17 10:25 UTC532
) -> T {
8fb62aebf501atagen2026-10-17 10:25 UTC533
    let Some(value) = value else {
8fb62aebf501atagen2026-10-17 10:25 UTC534
        return T::default();
8fb62aebf501atagen2026-10-17 10:25 UTC535
    };
8fb62aebf501atagen2026-10-17 10:25 UTC536
    if let Ok(t) = value.clone().try_into() {
8fb62aebf501atagen2026-10-17 10:25 UTC537
        return t;
8fb62aebf501atagen2026-10-17 10:25 UTC538
    }
8fb62aebf501atagen2026-10-17 10:25 UTC539
    let toml::Value::Table(options) = value else {
8fb62aebf501atagen2026-10-17 10:25 UTC540
        w.push(format!("[{name}] is not a table, using the defaults"));
8fb62aebf501atagen2026-10-17 10:25 UTC541
        return T::default();
8fb62aebf501atagen2026-10-17 10:25 UTC542
    };
8fb62aebf501atagen2026-10-17 10:25 UTC543
    let mut kept = toml::Table::new();
8fb62aebf501atagen2026-10-17 10:25 UTC544
    for (key, v) in options {
8fb62aebf501atagen2026-10-17 10:25 UTC545
        let alone = toml::Table::from_iter([(key.clone(), v.clone())]);
8fb62aebf501atagen2026-10-17 10:25 UTC546
        match toml::Value::Table(alone).try_into::<T>() {
8fb62aebf501atagen2026-10-17 10:25 UTC547
            Ok(_) => {
8fb62aebf501atagen2026-10-17 10:25 UTC548
                kept.insert(key, v);
8fb62aebf501atagen2026-10-17 10:25 UTC549
            }
8fb62aebf501atagen2026-10-17 10:25 UTC550
            Err(e) => {
8fb62aebf501atagen2026-10-17 10:25 UTC551
                let e = one_line(&e);
8fb62aebf501atagen2026-10-17 10:25 UTC552
                let fallback = if e.starts_with("unknown field") {
8fb62aebf501atagen2026-10-17 10:25 UTC553
                    "ignored"
8fb62aebf501atagen2026-10-17 10:25 UTC554
                } else {
8fb62aebf501atagen2026-10-17 10:25 UTC555
                    "using the default"
8fb62aebf501atagen2026-10-17 10:25 UTC556
                };
8fb62aebf501atagen2026-10-17 10:25 UTC557
                w.push(format!("{name}.{key}: {e}, {fallback}"));
8fb62aebf501atagen2026-10-17 10:25 UTC558
            }
8fb62aebf501atagen2026-10-17 10:25 UTC559
        }
8fb62aebf501atagen2026-10-17 10:25 UTC560
    }
8fb62aebf501atagen2026-10-17 10:25 UTC561
    toml::Value::Table(kept).try_into().unwrap_or_default()
8fb62aebf501atagen2026-10-17 10:25 UTC562
}
8fb62aebf501atagen2026-10-17 10:25 UTC563
8fb62aebf501atagen2026-10-17 10:25 UTC564
/// An array with the elements that do not fit left out.
8fb62aebf501atagen2026-10-17 10:25 UTC565
fn items<T: DeserializeOwned>(
8fb62aebf501atagen2026-10-17 10:25 UTC566
    name: &str,
8fb62aebf501atagen2026-10-17 10:25 UTC567
    value: Option<toml::Value>,
8fb62aebf501atagen2026-10-17 10:25 UTC568
    w: &mut Vec<String>,
8fb62aebf501atagen2026-10-17 10:25 UTC569
) -> Vec<T> {
8fb62aebf501atagen2026-10-17 10:25 UTC570
    match value {
8fb62aebf501atagen2026-10-17 10:25 UTC571
        None => Vec::new(),
8fb62aebf501atagen2026-10-17 10:25 UTC572
        Some(toml::Value::Array(a)) => a
8fb62aebf501atagen2026-10-17 10:25 UTC573
            .into_iter()
8fb62aebf501atagen2026-10-17 10:25 UTC574
            .enumerate()
8fb62aebf501atagen2026-10-17 10:25 UTC575
            .filter_map(|(i, v)| entry(&format!("{name}[{i}]"), v, w))
8fb62aebf501atagen2026-10-17 10:25 UTC576
            .collect(),
8fb62aebf501atagen2026-10-17 10:25 UTC577
        Some(_) => {
8fb62aebf501atagen2026-10-17 10:25 UTC578
            w.push(format!("{name} is not an array, left out"));
8fb62aebf501atagen2026-10-17 10:25 UTC579
            Vec::new()
8fb62aebf501atagen2026-10-17 10:25 UTC580
        }
8fb62aebf501atagen2026-10-17 10:25 UTC581
    }
8fb62aebf501atagen2026-10-17 10:25 UTC582
}
8fb62aebf501atagen2026-10-17 10:25 UTC583
8fb62aebf501atagen2026-10-17 10:25 UTC584
fn one_line(e: &toml::de::Error) -> String {
8fb62aebf501atagen2026-10-17 10:25 UTC585
    e.message().trim().replace('\n', " ")
8fb62aebf501atagen2026-10-17 10:25 UTC586
}
fe835aed6c8datagen2026-10-16 10:05 UTC587
#[cfg(test)]
fe835aed6c8datagen2026-10-16 10:05 UTC588
mod tests {
b22c0511d5afatagen2026-10-17 03:48 UTC589
    use super::*;
b22c0511d5afatagen2026-10-17 03:48 UTC590
b22c0511d5afatagen2026-10-17 03:48 UTC591
    /// default.toml documents the defaults for users, and the Default impls
b22c0511d5afatagen2026-10-17 03:48 UTC592
    /// apply them to sections a user config leaves out. They must agree.
fe835aed6c8datagen2026-10-16 10:05 UTC593
    #[test]
b22c0511d5afatagen2026-10-17 03:48 UTC594
    fn default_toml_states_the_defaults() {
8fb62aebf501atagen2026-10-17 10:25 UTC595
        let loaded = Config::parse(DEFAULT).unwrap();
8fb62aebf501atagen2026-10-17 10:25 UTC596
        assert_eq!(loaded.warnings, Vec::<String>::new());
8fb62aebf501atagen2026-10-17 10:25 UTC597
        let c = loaded.config;
b22c0511d5afatagen2026-10-17 03:48 UTC598
        assert_eq!(c.general, General::default());
b22c0511d5afatagen2026-10-17 03:48 UTC599
        assert_eq!(c.output, Outputs::default());
b22c0511d5afatagen2026-10-17 03:48 UTC600
        assert_eq!(c.decoration, Decoration::default());
b22c0511d5afatagen2026-10-17 03:48 UTC601
        assert_eq!(c.animation, Animation::default());
b22c0511d5afatagen2026-10-17 03:48 UTC602
        assert_eq!(c.keyboard, Keyboard::default());
fe835aed6c8datagen2026-10-16 10:05 UTC603
        assert!(c.binds.len() > 40);
fe835aed6c8datagen2026-10-16 10:05 UTC604
    }
8fb62aebf501atagen2026-10-17 10:25 UTC605
8fb62aebf501atagen2026-10-17 10:25 UTC606
    #[test]
8fb62aebf501atagen2026-10-17 10:25 UTC607
    fn a_bad_option_costs_only_itself() {
8fb62aebf501atagen2026-10-17 10:25 UTC608
        let loaded = Config::parse(
8fb62aebf501atagen2026-10-17 10:25 UTC609
            r##"
8fb62aebf501atagen2026-10-17 10:25 UTC610
            [general]
8fb62aebf501atagen2026-10-17 10:25 UTC611
            gaps = { inner = 3, outer = 3 }
8fb62aebf501atagen2026-10-17 10:25 UTC612
            focus_follows_mouse = "yes"
8fb62aebf501atagen2026-10-17 10:25 UTC613
            typo = 1
8fb62aebf501atagen2026-10-17 10:25 UTC614
            [decoration]
8fb62aebf501atagen2026-10-17 10:25 UTC615
            radius = [1, 2, 3, 4]
8fb62aebf501atagen2026-10-17 10:25 UTC616
            focused = "#zzzzzz"
8fb62aebf501atagen2026-10-17 10:25 UTC617
            [binds]
3b180fd69834atagen2026-10-17 10:40 UTC618
            "Mod+Return" = "spawn beer"
8fb62aebf501atagen2026-10-17 10:25 UTC619
            "Hyper+x" = "close"
9b87063809e1atagen2026-10-17 13:28 UTC620
            [output.DP-1]
9b87063809e1atagen2026-10-17 13:28 UTC621
            transform = "90"
9b87063809e1atagen2026-10-17 13:28 UTC622
            position = [0, 0]
9b87063809e1atagen2026-10-17 13:28 UTC623
            [output.DP-2]
9b87063809e1atagen2026-10-17 13:28 UTC624
            transform = "sideways"
0f513a2420c0atagen2026-10-18 02:40 UTC625
            [output.DP1]
8fb62aebf501atagen2026-10-17 10:25 UTC626
            [[rules]]
8fb62aebf501atagen2026-10-17 10:25 UTC627
            app_id = "("
8fb62aebf501atagen2026-10-17 10:25 UTC628
            [[rules]]
8fb62aebf501atagen2026-10-17 10:25 UTC629
            app_id = "^mpv$"
8fb62aebf501atagen2026-10-17 10:25 UTC630
            floating = true
8fb62aebf501atagen2026-10-17 10:25 UTC631
            "##,
8fb62aebf501atagen2026-10-17 10:25 UTC632
        )
8fb62aebf501atagen2026-10-17 10:25 UTC633
        .unwrap();
8fb62aebf501atagen2026-10-17 10:25 UTC634
        let c = loaded.config;
8fb62aebf501atagen2026-10-17 10:25 UTC635
        assert_eq!(c.general.gaps.inner, 3.0);
8fb62aebf501atagen2026-10-17 10:25 UTC636
        assert!(!c.general.focus_follows_mouse);
8fb62aebf501atagen2026-10-17 10:25 UTC637
        assert_eq!(c.decoration.radius, Corners([1, 2, 3, 4]));
8fb62aebf501atagen2026-10-17 10:25 UTC638
        assert_eq!(c.decoration.focused, Decoration::default().focused);
8fb62aebf501atagen2026-10-17 10:25 UTC639
        assert_eq!((c.binds.len(), c.rules.len()), (1, 1));
9b87063809e1atagen2026-10-17 13:28 UTC640
        assert_eq!(c.output.named["DP-1"].transform, OutputTransform::Rotate90);
9b87063809e1atagen2026-10-17 13:28 UTC641
        assert!(!c.output.named.contains_key("DP-2"));
0f513a2420c0atagen2026-10-18 02:40 UTC642
        assert_eq!(loaded.warnings.len(), 7, "{:#?}", loaded.warnings);
8fb62aebf501atagen2026-10-17 10:25 UTC643
    }
fe835aed6c8datagen2026-10-16 10:05 UTC644
}