atagen

monolith

public
Star0
Watch0
24fca1aad608996015c80df3e8dff46c67700f55
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;
fe835aed6c8datagen2026-10-16 10:05 UTC6
use std::collections::BTreeMap;
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,
4444c779eb48atagen2026-10-16 12:07 UTC36
}
4444c779eb48atagen2026-10-16 12:07 UTC37
4444c779eb48atagen2026-10-16 12:07 UTC38
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
4444c779eb48atagen2026-10-16 12:07 UTC39
#[serde(rename_all = "lowercase")]
4444c779eb48atagen2026-10-16 12:07 UTC40
pub enum MoveMode {
4444c779eb48atagen2026-10-16 12:07 UTC41
    /// Trade places with it.
4444c779eb48atagen2026-10-16 12:07 UTC42
    #[default]
4444c779eb48atagen2026-10-16 12:07 UTC43
    Swap,
4444c779eb48atagen2026-10-16 12:07 UTC44
    /// Join its lane, stacking with it if it is alone.
4444c779eb48atagen2026-10-16 12:07 UTC45
    Merge,
fe835aed6c8datagen2026-10-16 10:05 UTC46
}
fe835aed6c8datagen2026-10-16 10:05 UTC47
fe835aed6c8datagen2026-10-16 10:05 UTC48
impl Default for General {
fe835aed6c8datagen2026-10-16 10:05 UTC49
    fn default() -> Self {
fe835aed6c8datagen2026-10-16 10:05 UTC50
        Self {
fe835aed6c8datagen2026-10-16 10:05 UTC51
            mod_key: Modifier::Super,
fe835aed6c8datagen2026-10-16 10:05 UTC52
            gaps: Gaps::default(),
4444c779eb48atagen2026-10-16 12:07 UTC53
            move_mode: MoveMode::Swap,
24fca1aad608atagen2026-10-17 09:56 UTC54
            focus_follows_mouse: false,
24fca1aad608atagen2026-10-17 09:56 UTC55
            mouse_follows_focus: false,
fe835aed6c8datagen2026-10-16 10:05 UTC56
        }
fe835aed6c8datagen2026-10-16 10:05 UTC57
    }
fe835aed6c8datagen2026-10-16 10:05 UTC58
}
fe835aed6c8datagen2026-10-16 10:05 UTC59
fe835aed6c8datagen2026-10-16 10:05 UTC60
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC61
#[serde(default, deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC62
pub struct Gaps {
fe835aed6c8datagen2026-10-16 10:05 UTC63
    pub inner: f64,
fe835aed6c8datagen2026-10-16 10:05 UTC64
    pub outer: f64,
fe835aed6c8datagen2026-10-16 10:05 UTC65
}
fe835aed6c8datagen2026-10-16 10:05 UTC66
fe835aed6c8datagen2026-10-16 10:05 UTC67
impl Default for Gaps {
fe835aed6c8datagen2026-10-16 10:05 UTC68
    fn default() -> Self {
fe835aed6c8datagen2026-10-16 10:05 UTC69
        Self {
fe835aed6c8datagen2026-10-16 10:05 UTC70
            inner: 8.0,
fe835aed6c8datagen2026-10-16 10:05 UTC71
            outer: 8.0,
fe835aed6c8datagen2026-10-16 10:05 UTC72
        }
fe835aed6c8datagen2026-10-16 10:05 UTC73
    }
fe835aed6c8datagen2026-10-16 10:05 UTC74
}
fe835aed6c8datagen2026-10-16 10:05 UTC75
8e6f96f0a32datagen2026-10-16 22:17 UTC76
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
8e6f96f0a32datagen2026-10-16 22:17 UTC77
#[serde(default, deny_unknown_fields)]
8e6f96f0a32datagen2026-10-16 22:17 UTC78
pub struct Outputs {
8e6f96f0a32datagen2026-10-16 22:17 UTC79
    pub vrr: Vrr,
8e6f96f0a32datagen2026-10-16 22:17 UTC80
}
8e6f96f0a32datagen2026-10-16 22:17 UTC81
8e6f96f0a32datagen2026-10-16 22:17 UTC82
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
8e6f96f0a32datagen2026-10-16 22:17 UTC83
#[serde(rename_all = "lowercase")]
8e6f96f0a32datagen2026-10-16 22:17 UTC84
pub enum Vrr {
8e6f96f0a32datagen2026-10-16 22:17 UTC85
    Off,
8e6f96f0a32datagen2026-10-16 22:17 UTC86
    On,
8e6f96f0a32datagen2026-10-16 22:17 UTC87
    /// Only while a fullscreen window is shown, where a game sets the pace.
8e6f96f0a32datagen2026-10-16 22:17 UTC88
    #[default]
8e6f96f0a32datagen2026-10-16 22:17 UTC89
    Fullscreen,
8e6f96f0a32datagen2026-10-16 22:17 UTC90
}
8e6f96f0a32datagen2026-10-16 22:17 UTC91
fe835aed6c8datagen2026-10-16 10:05 UTC92
#[derive(Deserialize, Debug, Clone, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC93
#[serde(default, deny_unknown_fields)]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC94
pub struct Decoration {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC95
    /// Border width in logical pixels, drawn in the gap around each window.
7ce6ae4e5a85atagen2026-10-16 12:16 UTC96
    pub border: u32,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC97
    pub focused: Color,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC98
    pub unfocused: Color,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC99
}
7ce6ae4e5a85atagen2026-10-16 12:16 UTC100
7ce6ae4e5a85atagen2026-10-16 12:16 UTC101
impl Default for Decoration {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC102
    fn default() -> Self {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC103
        Self {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC104
            border: 2,
b22c0511d5afatagen2026-10-17 03:48 UTC105
            focused: Color::rgb(0x88, 0xbf, 0xd1),
b22c0511d5afatagen2026-10-17 03:48 UTC106
            unfocused: Color::rgb(0x3b, 0x42, 0x52),
7ce6ae4e5a85atagen2026-10-16 12:16 UTC107
        }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC108
    }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC109
}
7ce6ae4e5a85atagen2026-10-16 12:16 UTC110
7ce6ae4e5a85atagen2026-10-16 12:16 UTC111
/// Straight RGBA from `#rrggbb` or `#rrggbbaa`.
7ce6ae4e5a85atagen2026-10-16 12:16 UTC112
#[derive(Debug, Clone, Copy, PartialEq)]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC113
pub struct Color(pub [f32; 4]);
7ce6ae4e5a85atagen2026-10-16 12:16 UTC114
b22c0511d5afatagen2026-10-17 03:48 UTC115
impl Color {
b22c0511d5afatagen2026-10-17 03:48 UTC116
    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
b22c0511d5afatagen2026-10-17 03:48 UTC117
        Self([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
b22c0511d5afatagen2026-10-17 03:48 UTC118
    }
b22c0511d5afatagen2026-10-17 03:48 UTC119
}
b22c0511d5afatagen2026-10-17 03:48 UTC120
7ce6ae4e5a85atagen2026-10-16 12:16 UTC121
impl<'de> Deserialize<'de> for Color {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC122
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC123
        let s = String::deserialize(d)?;
7ce6ae4e5a85atagen2026-10-16 12:16 UTC124
        let hex = s.strip_prefix('#').unwrap_or(&s);
7ce6ae4e5a85atagen2026-10-16 12:16 UTC125
        let bad =
7ce6ae4e5a85atagen2026-10-16 12:16 UTC126
            || serde::de::Error::custom(format!("bad colour {s:?}, use #rrggbb or #rrggbbaa"));
7ce6ae4e5a85atagen2026-10-16 12:16 UTC127
        if !matches!(hex.len(), 6 | 8) || !hex.is_ascii() {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC128
            return Err(bad());
7ce6ae4e5a85atagen2026-10-16 12:16 UTC129
        }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC130
        let mut c = [1.0f32; 4];
7ce6ae4e5a85atagen2026-10-16 12:16 UTC131
        for (i, v) in c.iter_mut().enumerate().take(hex.len() / 2) {
7ce6ae4e5a85atagen2026-10-16 12:16 UTC132
            let byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| bad())?;
7ce6ae4e5a85atagen2026-10-16 12:16 UTC133
            *v = f32::from(byte) / 255.0;
7ce6ae4e5a85atagen2026-10-16 12:16 UTC134
        }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC135
        Ok(Self(c))
7ce6ae4e5a85atagen2026-10-16 12:16 UTC136
    }
7ce6ae4e5a85atagen2026-10-16 12:16 UTC137
}
7ce6ae4e5a85atagen2026-10-16 12:16 UTC138
7ce6ae4e5a85atagen2026-10-16 12:16 UTC139
#[derive(Deserialize, Debug, Clone, PartialEq)]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC140
#[serde(default, deny_unknown_fields)]
2af1d8c55876atagen2026-10-16 11:05 UTC141
pub struct Animation {
2af1d8c55876atagen2026-10-16 11:05 UTC142
    pub enabled: bool,
2af1d8c55876atagen2026-10-16 11:05 UTC143
    pub style: AnimationStyle,
2af1d8c55876atagen2026-10-16 11:05 UTC144
    /// Every phase of a planned transition lasts this long.
2af1d8c55876atagen2026-10-16 11:05 UTC145
    pub phase_ms: u64,
2af1d8c55876atagen2026-10-16 11:05 UTC146
    /// A cubic bezier, as in CSS.
2af1d8c55876atagen2026-10-16 11:05 UTC147
    pub curve: [f64; 4],
2af1d8c55876atagen2026-10-16 11:05 UTC148
}
2af1d8c55876atagen2026-10-16 11:05 UTC149
2af1d8c55876atagen2026-10-16 11:05 UTC150
impl Default for Animation {
2af1d8c55876atagen2026-10-16 11:05 UTC151
    fn default() -> Self {
2af1d8c55876atagen2026-10-16 11:05 UTC152
        Self {
2af1d8c55876atagen2026-10-16 11:05 UTC153
            enabled: true,
2af1d8c55876atagen2026-10-16 11:05 UTC154
            style: AnimationStyle::Orthogonal,
2af1d8c55876atagen2026-10-16 11:05 UTC155
            phase_ms: 160,
2af1d8c55876atagen2026-10-16 11:05 UTC156
            curve: [0.45, 0.0, 0.2, 1.0],
2af1d8c55876atagen2026-10-16 11:05 UTC157
        }
2af1d8c55876atagen2026-10-16 11:05 UTC158
    }
2af1d8c55876atagen2026-10-16 11:05 UTC159
}
2af1d8c55876atagen2026-10-16 11:05 UTC160
2af1d8c55876atagen2026-10-16 11:05 UTC161
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
2af1d8c55876atagen2026-10-16 11:05 UTC162
#[serde(rename_all = "lowercase")]
2af1d8c55876atagen2026-10-16 11:05 UTC163
pub enum AnimationStyle {
2af1d8c55876atagen2026-10-16 11:05 UTC164
    Orthogonal,
2af1d8c55876atagen2026-10-16 11:05 UTC165
    Direct,
2af1d8c55876atagen2026-10-16 11:05 UTC166
}
2af1d8c55876atagen2026-10-16 11:05 UTC167
2af1d8c55876atagen2026-10-16 11:05 UTC168
#[derive(Deserialize, Debug, Clone, PartialEq)]
2af1d8c55876atagen2026-10-16 11:05 UTC169
#[serde(default, deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC170
pub struct Keyboard {
fe835aed6c8datagen2026-10-16 10:05 UTC171
    pub rules: String,
fe835aed6c8datagen2026-10-16 10:05 UTC172
    pub model: String,
fe835aed6c8datagen2026-10-16 10:05 UTC173
    pub layout: String,
fe835aed6c8datagen2026-10-16 10:05 UTC174
    pub variant: String,
fe835aed6c8datagen2026-10-16 10:05 UTC175
    pub options: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC176
    pub repeat_delay: i32,
fe835aed6c8datagen2026-10-16 10:05 UTC177
    pub repeat_rate: i32,
fe835aed6c8datagen2026-10-16 10:05 UTC178
}
fe835aed6c8datagen2026-10-16 10:05 UTC179
fe835aed6c8datagen2026-10-16 10:05 UTC180
impl Default for Keyboard {
fe835aed6c8datagen2026-10-16 10:05 UTC181
    fn default() -> Self {
fe835aed6c8datagen2026-10-16 10:05 UTC182
        Self {
fe835aed6c8datagen2026-10-16 10:05 UTC183
            rules: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC184
            model: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC185
            layout: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC186
            variant: String::new(),
fe835aed6c8datagen2026-10-16 10:05 UTC187
            options: None,
fe835aed6c8datagen2026-10-16 10:05 UTC188
            repeat_delay: 200,
fe835aed6c8datagen2026-10-16 10:05 UTC189
            repeat_rate: 25,
fe835aed6c8datagen2026-10-16 10:05 UTC190
        }
fe835aed6c8datagen2026-10-16 10:05 UTC191
    }
fe835aed6c8datagen2026-10-16 10:05 UTC192
}
fe835aed6c8datagen2026-10-16 10:05 UTC193
fe835aed6c8datagen2026-10-16 10:05 UTC194
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
fe835aed6c8datagen2026-10-16 10:05 UTC195
#[serde(rename_all = "lowercase")]
fe835aed6c8datagen2026-10-16 10:05 UTC196
pub enum Modifier {
fe835aed6c8datagen2026-10-16 10:05 UTC197
    Super,
fe835aed6c8datagen2026-10-16 10:05 UTC198
    Alt,
fe835aed6c8datagen2026-10-16 10:05 UTC199
    Ctrl,
fe835aed6c8datagen2026-10-16 10:05 UTC200
    Shift,
fe835aed6c8datagen2026-10-16 10:05 UTC201
}
fe835aed6c8datagen2026-10-16 10:05 UTC202
fe835aed6c8datagen2026-10-16 10:05 UTC203
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
fe835aed6c8datagen2026-10-16 10:05 UTC204
pub struct Mods {
fe835aed6c8datagen2026-10-16 10:05 UTC205
    pub logo: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC206
    pub alt: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC207
    pub ctrl: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC208
    pub shift: bool,
fe835aed6c8datagen2026-10-16 10:05 UTC209
}
fe835aed6c8datagen2026-10-16 10:05 UTC210
fe835aed6c8datagen2026-10-16 10:05 UTC211
impl Mods {
b22c0511d5afatagen2026-10-17 03:48 UTC212
    pub fn has(&self, m: Modifier) -> bool {
b22c0511d5afatagen2026-10-17 03:48 UTC213
        match m {
b22c0511d5afatagen2026-10-17 03:48 UTC214
            Modifier::Super => self.logo,
b22c0511d5afatagen2026-10-17 03:48 UTC215
            Modifier::Alt => self.alt,
b22c0511d5afatagen2026-10-17 03:48 UTC216
            Modifier::Ctrl => self.ctrl,
b22c0511d5afatagen2026-10-17 03:48 UTC217
            Modifier::Shift => self.shift,
b22c0511d5afatagen2026-10-17 03:48 UTC218
        }
b22c0511d5afatagen2026-10-17 03:48 UTC219
    }
b22c0511d5afatagen2026-10-17 03:48 UTC220
fe835aed6c8datagen2026-10-16 10:05 UTC221
    fn set(&mut self, m: Modifier) {
fe835aed6c8datagen2026-10-16 10:05 UTC222
        match m {
fe835aed6c8datagen2026-10-16 10:05 UTC223
            Modifier::Super => self.logo = true,
fe835aed6c8datagen2026-10-16 10:05 UTC224
            Modifier::Alt => self.alt = true,
fe835aed6c8datagen2026-10-16 10:05 UTC225
            Modifier::Ctrl => self.ctrl = true,
fe835aed6c8datagen2026-10-16 10:05 UTC226
            Modifier::Shift => self.shift = true,
fe835aed6c8datagen2026-10-16 10:05 UTC227
        }
fe835aed6c8datagen2026-10-16 10:05 UTC228
    }
fe835aed6c8datagen2026-10-16 10:05 UTC229
}
fe835aed6c8datagen2026-10-16 10:05 UTC230
fe835aed6c8datagen2026-10-16 10:05 UTC231
#[derive(Debug, Clone, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC232
pub struct Bind {
fe835aed6c8datagen2026-10-16 10:05 UTC233
    pub mods: Mods,
fe835aed6c8datagen2026-10-16 10:05 UTC234
    /// An xkb keysym name, resolved by the compositor.
fe835aed6c8datagen2026-10-16 10:05 UTC235
    pub key: String,
fe835aed6c8datagen2026-10-16 10:05 UTC236
    pub command: Command,
fe835aed6c8datagen2026-10-16 10:05 UTC237
}
fe835aed6c8datagen2026-10-16 10:05 UTC238
fe835aed6c8datagen2026-10-16 10:05 UTC239
#[derive(Debug, Clone)]
fe835aed6c8datagen2026-10-16 10:05 UTC240
pub struct Rule {
fe835aed6c8datagen2026-10-16 10:05 UTC241
    pub app_id: Option<Regex>,
fe835aed6c8datagen2026-10-16 10:05 UTC242
    pub title: Option<Regex>,
fe835aed6c8datagen2026-10-16 10:05 UTC243
    pub tag: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC244
    pub output: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC245
    pub floating: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC246
    pub sticky: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC247
    pub scratchpad: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC248
}
fe835aed6c8datagen2026-10-16 10:05 UTC249
fe835aed6c8datagen2026-10-16 10:05 UTC250
/// Everything the matching rules say about a window, later rules winning.
fe835aed6c8datagen2026-10-16 10:05 UTC251
#[derive(Debug, Clone, Default, PartialEq)]
fe835aed6c8datagen2026-10-16 10:05 UTC252
pub struct Placement {
fe835aed6c8datagen2026-10-16 10:05 UTC253
    pub tag: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC254
    pub output: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC255
    pub floating: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC256
    pub sticky: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC257
    pub scratchpad: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC258
}
fe835aed6c8datagen2026-10-16 10:05 UTC259
fe835aed6c8datagen2026-10-16 10:05 UTC260
#[derive(Debug)]
fe835aed6c8datagen2026-10-16 10:05 UTC261
pub enum Error {
fe835aed6c8datagen2026-10-16 10:05 UTC262
    Io(PathBuf, std::io::Error),
fe835aed6c8datagen2026-10-16 10:05 UTC263
    Toml(toml::de::Error),
fe835aed6c8datagen2026-10-16 10:05 UTC264
    Bind(String, String),
fe835aed6c8datagen2026-10-16 10:05 UTC265
    Regex(String, regex::Error),
fe835aed6c8datagen2026-10-16 10:05 UTC266
}
fe835aed6c8datagen2026-10-16 10:05 UTC267
fe835aed6c8datagen2026-10-16 10:05 UTC268
impl fmt::Display for Error {
fe835aed6c8datagen2026-10-16 10:05 UTC269
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fe835aed6c8datagen2026-10-16 10:05 UTC270
        match self {
fe835aed6c8datagen2026-10-16 10:05 UTC271
            Error::Io(p, e) => write!(f, "{}: {e}", p.display()),
fe835aed6c8datagen2026-10-16 10:05 UTC272
            Error::Toml(e) => write!(f, "{e}"),
fe835aed6c8datagen2026-10-16 10:05 UTC273
            Error::Bind(k, e) => write!(f, "bind {k:?}: {e}"),
fe835aed6c8datagen2026-10-16 10:05 UTC274
            Error::Regex(r, e) => write!(f, "rule pattern {r:?}: {e}"),
fe835aed6c8datagen2026-10-16 10:05 UTC275
        }
fe835aed6c8datagen2026-10-16 10:05 UTC276
    }
fe835aed6c8datagen2026-10-16 10:05 UTC277
}
fe835aed6c8datagen2026-10-16 10:05 UTC278
fe835aed6c8datagen2026-10-16 10:05 UTC279
impl std::error::Error for Error {}
fe835aed6c8datagen2026-10-16 10:05 UTC280
fe835aed6c8datagen2026-10-16 10:05 UTC281
#[derive(Deserialize)]
fe835aed6c8datagen2026-10-16 10:05 UTC282
#[serde(deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC283
struct Raw {
fe835aed6c8datagen2026-10-16 10:05 UTC284
    #[serde(default)]
fe835aed6c8datagen2026-10-16 10:05 UTC285
    startup: Vec<Vec<String>>,
fe835aed6c8datagen2026-10-16 10:05 UTC286
    #[serde(default)]
fe835aed6c8datagen2026-10-16 10:05 UTC287
    general: General,
fe835aed6c8datagen2026-10-16 10:05 UTC288
    #[serde(default)]
fe835aed6c8datagen2026-10-16 10:05 UTC289
    keyboard: Keyboard,
fe835aed6c8datagen2026-10-16 10:05 UTC290
    #[serde(default)]
2af1d8c55876atagen2026-10-16 11:05 UTC291
    animation: Animation,
2af1d8c55876atagen2026-10-16 11:05 UTC292
    #[serde(default)]
7ce6ae4e5a85atagen2026-10-16 12:16 UTC293
    decoration: Decoration,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC294
    #[serde(default)]
8e6f96f0a32datagen2026-10-16 22:17 UTC295
    output: Outputs,
8e6f96f0a32datagen2026-10-16 22:17 UTC296
    #[serde(default)]
fe835aed6c8datagen2026-10-16 10:05 UTC297
    binds: BTreeMap<String, Words>,
fe835aed6c8datagen2026-10-16 10:05 UTC298
    #[serde(default)]
fe835aed6c8datagen2026-10-16 10:05 UTC299
    rules: Vec<RawRule>,
fe835aed6c8datagen2026-10-16 10:05 UTC300
}
fe835aed6c8datagen2026-10-16 10:05 UTC301
fe835aed6c8datagen2026-10-16 10:05 UTC302
#[derive(Deserialize)]
fe835aed6c8datagen2026-10-16 10:05 UTC303
#[serde(untagged)]
fe835aed6c8datagen2026-10-16 10:05 UTC304
enum Words {
fe835aed6c8datagen2026-10-16 10:05 UTC305
    Line(String),
fe835aed6c8datagen2026-10-16 10:05 UTC306
    List(Vec<String>),
fe835aed6c8datagen2026-10-16 10:05 UTC307
}
fe835aed6c8datagen2026-10-16 10:05 UTC308
fe835aed6c8datagen2026-10-16 10:05 UTC309
#[derive(Deserialize)]
fe835aed6c8datagen2026-10-16 10:05 UTC310
#[serde(deny_unknown_fields)]
fe835aed6c8datagen2026-10-16 10:05 UTC311
struct RawRule {
fe835aed6c8datagen2026-10-16 10:05 UTC312
    app_id: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC313
    title: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC314
    tag: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC315
    output: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC316
    floating: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC317
    sticky: Option<bool>,
fe835aed6c8datagen2026-10-16 10:05 UTC318
    scratchpad: Option<String>,
fe835aed6c8datagen2026-10-16 10:05 UTC319
}
fe835aed6c8datagen2026-10-16 10:05 UTC320
fe835aed6c8datagen2026-10-16 10:05 UTC321
impl Config {
fe835aed6c8datagen2026-10-16 10:05 UTC322
    /// `$MONOLITH_CONFIG`, else `$XDG_CONFIG_HOME/monolith/config.toml`.
fe835aed6c8datagen2026-10-16 10:05 UTC323
    pub fn path() -> Option<PathBuf> {
fe835aed6c8datagen2026-10-16 10:05 UTC324
        if let Some(p) = std::env::var_os("MONOLITH_CONFIG") {
fe835aed6c8datagen2026-10-16 10:05 UTC325
            return Some(p.into());
fe835aed6c8datagen2026-10-16 10:05 UTC326
        }
fe835aed6c8datagen2026-10-16 10:05 UTC327
        let base = std::env::var_os("XDG_CONFIG_HOME")
fe835aed6c8datagen2026-10-16 10:05 UTC328
            .map(PathBuf::from)
fe835aed6c8datagen2026-10-16 10:05 UTC329
            .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
fe835aed6c8datagen2026-10-16 10:05 UTC330
        Some(base.join("monolith/config.toml"))
fe835aed6c8datagen2026-10-16 10:05 UTC331
    }
fe835aed6c8datagen2026-10-16 10:05 UTC332
fe835aed6c8datagen2026-10-16 10:05 UTC333
    /// Loads `path`, or the built-in default when it does not exist.
fe835aed6c8datagen2026-10-16 10:05 UTC334
    pub fn load(path: Option<&Path>) -> Result<Self, Error> {
fe835aed6c8datagen2026-10-16 10:05 UTC335
        match path {
fe835aed6c8datagen2026-10-16 10:05 UTC336
            Some(p) => match std::fs::read_to_string(p) {
fe835aed6c8datagen2026-10-16 10:05 UTC337
                Ok(text) => Self::parse(&text),
fe835aed6c8datagen2026-10-16 10:05 UTC338
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::parse(DEFAULT),
fe835aed6c8datagen2026-10-16 10:05 UTC339
                Err(e) => Err(Error::Io(p.to_owned(), e)),
fe835aed6c8datagen2026-10-16 10:05 UTC340
            },
fe835aed6c8datagen2026-10-16 10:05 UTC341
            None => Self::parse(DEFAULT),
fe835aed6c8datagen2026-10-16 10:05 UTC342
        }
fe835aed6c8datagen2026-10-16 10:05 UTC343
    }
fe835aed6c8datagen2026-10-16 10:05 UTC344
fe835aed6c8datagen2026-10-16 10:05 UTC345
    pub fn parse(text: &str) -> Result<Self, Error> {
fe835aed6c8datagen2026-10-16 10:05 UTC346
        let raw: Raw = toml::from_str(text).map_err(Error::Toml)?;
fe835aed6c8datagen2026-10-16 10:05 UTC347
        let mod_key = raw.general.mod_key;
fe835aed6c8datagen2026-10-16 10:05 UTC348
        let binds = raw
fe835aed6c8datagen2026-10-16 10:05 UTC349
            .binds
fe835aed6c8datagen2026-10-16 10:05 UTC350
            .into_iter()
fe835aed6c8datagen2026-10-16 10:05 UTC351
            .map(|(combo, words)| parse_bind(&combo, words, mod_key))
fe835aed6c8datagen2026-10-16 10:05 UTC352
            .collect::<Result<_, _>>()?;
fe835aed6c8datagen2026-10-16 10:05 UTC353
        let regex = |p: Option<String>| {
fe835aed6c8datagen2026-10-16 10:05 UTC354
            p.map(|p| Regex::new(&p).map_err(|e| Error::Regex(p, e)))
fe835aed6c8datagen2026-10-16 10:05 UTC355
                .transpose()
fe835aed6c8datagen2026-10-16 10:05 UTC356
        };
fe835aed6c8datagen2026-10-16 10:05 UTC357
        let rules = raw
fe835aed6c8datagen2026-10-16 10:05 UTC358
            .rules
fe835aed6c8datagen2026-10-16 10:05 UTC359
            .into_iter()
fe835aed6c8datagen2026-10-16 10:05 UTC360
            .map(|r| {
fe835aed6c8datagen2026-10-16 10:05 UTC361
                Ok(Rule {
fe835aed6c8datagen2026-10-16 10:05 UTC362
                    app_id: regex(r.app_id)?,
fe835aed6c8datagen2026-10-16 10:05 UTC363
                    title: regex(r.title)?,
fe835aed6c8datagen2026-10-16 10:05 UTC364
                    tag: r.tag,
fe835aed6c8datagen2026-10-16 10:05 UTC365
                    output: r.output,
fe835aed6c8datagen2026-10-16 10:05 UTC366
                    floating: r.floating,
fe835aed6c8datagen2026-10-16 10:05 UTC367
                    sticky: r.sticky,
fe835aed6c8datagen2026-10-16 10:05 UTC368
                    scratchpad: r.scratchpad,
fe835aed6c8datagen2026-10-16 10:05 UTC369
                })
fe835aed6c8datagen2026-10-16 10:05 UTC370
            })
fe835aed6c8datagen2026-10-16 10:05 UTC371
            .collect::<Result<_, Error>>()?;
fe835aed6c8datagen2026-10-16 10:05 UTC372
        Ok(Self {
fe835aed6c8datagen2026-10-16 10:05 UTC373
            general: raw.general,
2af1d8c55876atagen2026-10-16 11:05 UTC374
            animation: raw.animation,
7ce6ae4e5a85atagen2026-10-16 12:16 UTC375
            decoration: raw.decoration,
8e6f96f0a32datagen2026-10-16 22:17 UTC376
            output: raw.output,
fe835aed6c8datagen2026-10-16 10:05 UTC377
            keyboard: raw.keyboard,
fe835aed6c8datagen2026-10-16 10:05 UTC378
            binds,
fe835aed6c8datagen2026-10-16 10:05 UTC379
            rules,
fe835aed6c8datagen2026-10-16 10:05 UTC380
            startup: raw.startup,
fe835aed6c8datagen2026-10-16 10:05 UTC381
        })
fe835aed6c8datagen2026-10-16 10:05 UTC382
    }
fe835aed6c8datagen2026-10-16 10:05 UTC383
fe835aed6c8datagen2026-10-16 10:05 UTC384
    pub fn placement(&self, app_id: &str, title: &str) -> Placement {
fe835aed6c8datagen2026-10-16 10:05 UTC385
        let mut p = Placement::default();
fe835aed6c8datagen2026-10-16 10:05 UTC386
        let hit = |re: &Option<Regex>, s: &str| re.as_ref().is_none_or(|re| re.is_match(s));
fe835aed6c8datagen2026-10-16 10:05 UTC387
        for r in &self.rules {
fe835aed6c8datagen2026-10-16 10:05 UTC388
            if !hit(&r.app_id, app_id) || !hit(&r.title, title) {
fe835aed6c8datagen2026-10-16 10:05 UTC389
                continue;
fe835aed6c8datagen2026-10-16 10:05 UTC390
            }
fe835aed6c8datagen2026-10-16 10:05 UTC391
            p.tag = r.tag.clone().or(p.tag);
fe835aed6c8datagen2026-10-16 10:05 UTC392
            p.output = r.output.clone().or(p.output);
fe835aed6c8datagen2026-10-16 10:05 UTC393
            p.floating = r.floating.or(p.floating);
fe835aed6c8datagen2026-10-16 10:05 UTC394
            p.sticky = r.sticky.or(p.sticky);
fe835aed6c8datagen2026-10-16 10:05 UTC395
            p.scratchpad = r.scratchpad.clone().or(p.scratchpad);
fe835aed6c8datagen2026-10-16 10:05 UTC396
        }
fe835aed6c8datagen2026-10-16 10:05 UTC397
        p
fe835aed6c8datagen2026-10-16 10:05 UTC398
    }
fe835aed6c8datagen2026-10-16 10:05 UTC399
}
fe835aed6c8datagen2026-10-16 10:05 UTC400
fe835aed6c8datagen2026-10-16 10:05 UTC401
fn parse_bind(combo: &str, words: Words, mod_key: Modifier) -> Result<Bind, Error> {
fe835aed6c8datagen2026-10-16 10:05 UTC402
    let bad = |m: String| Error::Bind(combo.to_owned(), m);
fe835aed6c8datagen2026-10-16 10:05 UTC403
    let parts: Vec<&str> = combo.split('+').map(str::trim).collect();
fe835aed6c8datagen2026-10-16 10:05 UTC404
    let Some((&key, mods)) = parts.split_last().filter(|(k, _)| !k.is_empty()) else {
fe835aed6c8datagen2026-10-16 10:05 UTC405
        return Err(bad("no key".into()));
fe835aed6c8datagen2026-10-16 10:05 UTC406
    };
fe835aed6c8datagen2026-10-16 10:05 UTC407
    let mut set = Mods::default();
fe835aed6c8datagen2026-10-16 10:05 UTC408
    for m in mods {
fe835aed6c8datagen2026-10-16 10:05 UTC409
        let m = match m.to_ascii_lowercase().as_str() {
fe835aed6c8datagen2026-10-16 10:05 UTC410
            "mod" => mod_key,
fe835aed6c8datagen2026-10-16 10:05 UTC411
            "super" | "logo" | "win" => Modifier::Super,
fe835aed6c8datagen2026-10-16 10:05 UTC412
            "alt" => Modifier::Alt,
fe835aed6c8datagen2026-10-16 10:05 UTC413
            "ctrl" | "control" => Modifier::Ctrl,
fe835aed6c8datagen2026-10-16 10:05 UTC414
            "shift" => Modifier::Shift,
fe835aed6c8datagen2026-10-16 10:05 UTC415
            other => return Err(bad(format!("unknown modifier {other}"))),
fe835aed6c8datagen2026-10-16 10:05 UTC416
        };
fe835aed6c8datagen2026-10-16 10:05 UTC417
        set.set(m);
fe835aed6c8datagen2026-10-16 10:05 UTC418
    }
fe835aed6c8datagen2026-10-16 10:05 UTC419
    let words: Vec<String> = match words {
fe835aed6c8datagen2026-10-16 10:05 UTC420
        Words::Line(s) => s.split_whitespace().map(str::to_owned).collect(),
fe835aed6c8datagen2026-10-16 10:05 UTC421
        Words::List(v) => v,
fe835aed6c8datagen2026-10-16 10:05 UTC422
    };
fe835aed6c8datagen2026-10-16 10:05 UTC423
    let command = Command::parse(&words).map_err(|e| bad(e.0))?;
fe835aed6c8datagen2026-10-16 10:05 UTC424
    Ok(Bind {
fe835aed6c8datagen2026-10-16 10:05 UTC425
        mods: set,
fe835aed6c8datagen2026-10-16 10:05 UTC426
        key: key.to_owned(),
fe835aed6c8datagen2026-10-16 10:05 UTC427
        command,
fe835aed6c8datagen2026-10-16 10:05 UTC428
    })
fe835aed6c8datagen2026-10-16 10:05 UTC429
}
fe835aed6c8datagen2026-10-16 10:05 UTC430
fe835aed6c8datagen2026-10-16 10:05 UTC431
#[cfg(test)]
fe835aed6c8datagen2026-10-16 10:05 UTC432
mod tests {
b22c0511d5afatagen2026-10-17 03:48 UTC433
    use super::*;
b22c0511d5afatagen2026-10-17 03:48 UTC434
b22c0511d5afatagen2026-10-17 03:48 UTC435
    /// default.toml documents the defaults for users, and the Default impls
b22c0511d5afatagen2026-10-17 03:48 UTC436
    /// apply them to sections a user config leaves out. They must agree.
fe835aed6c8datagen2026-10-16 10:05 UTC437
    #[test]
b22c0511d5afatagen2026-10-17 03:48 UTC438
    fn default_toml_states_the_defaults() {
b22c0511d5afatagen2026-10-17 03:48 UTC439
        let c = Config::parse(DEFAULT).unwrap();
b22c0511d5afatagen2026-10-17 03:48 UTC440
        assert_eq!(c.general, General::default());
b22c0511d5afatagen2026-10-17 03:48 UTC441
        assert_eq!(c.output, Outputs::default());
b22c0511d5afatagen2026-10-17 03:48 UTC442
        assert_eq!(c.decoration, Decoration::default());
b22c0511d5afatagen2026-10-17 03:48 UTC443
        assert_eq!(c.animation, Animation::default());
b22c0511d5afatagen2026-10-17 03:48 UTC444
        assert_eq!(c.keyboard, Keyboard::default());
fe835aed6c8datagen2026-10-16 10:05 UTC445
        assert!(c.binds.len() > 40);
fe835aed6c8datagen2026-10-16 10:05 UTC446
    }
fe835aed6c8datagen2026-10-16 10:05 UTC447
}