atagen

monolith

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