brewery
notashelf /
c78687c0aea2db7643f6d11c38c26c28609ce053

beer

public

A terminal worth pouring time into

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/beer.git
src/config.rsRust155 lines4.9 KB
1
//! User configuration: a TOML file at `$XDG_CONFIG_HOME/beer/beer.toml`
2
//! deserialized into a typed [`Config`]. A missing file uses defaults; a
3
//! malformed one warns and falls back to defaults rather than failing to start.
4
5
use std::path::{Path, PathBuf};
6
7
use serde::Deserialize;
8
9
/// Top-level configuration. Unknown keys are ignored so a config written for a
10
/// newer beer still loads.
11
#[derive(Debug, Clone, Default, Deserialize)]
12
#[serde(default, rename_all = "kebab-case")]
13
pub struct Config {
14
    pub main: Main,
15
    pub colors: Colors,
16
    pub scrollback: Scrollback,
17
}
18
19
/// `[colors]`: foreground/background, the 16 base palette entries, and accents.
20
/// Each value is an X11 colour spec (`#rrggbb` or `rgb:rr/gg/bb`); unset entries
21
/// keep the built-in default.
22
#[derive(Debug, Clone, Default, Deserialize)]
23
#[serde(default, rename_all = "kebab-case")]
24
pub struct Colors {
25
    pub foreground: Option<String>,
26
    pub background: Option<String>,
27
    pub cursor: Option<String>,
28
    pub selection_foreground: Option<String>,
29
    pub selection_background: Option<String>,
30
    /// The eight regular palette entries (indices 0-7).
31
    pub regular: Option<Vec<String>>,
32
    /// The eight bright palette entries (indices 8-15).
33
    pub bright: Option<Vec<String>>,
34
    pub match_background: Option<String>,
35
    pub match_current_background: Option<String>,
36
    /// Background opacity, 0.0 (transparent) - 1.0 (opaque).
37
    pub alpha: Option<f32>,
38
    /// Render bold text with the bright palette variant.
39
    pub bold_as_bright: Option<bool>,
40
}
41
42
/// `[main]`: fonts, window geometry, padding, and the terminal name.
43
#[derive(Debug, Clone, Deserialize)]
44
#[serde(default, rename_all = "kebab-case")]
45
pub struct Main {
46
    /// Primary font family, resolved via fontconfig.
47
    pub font: String,
48
    /// Font size in pixels.
49
    pub font_size: u32,
50
    /// `TERM` value exported to the child shell.
51
    pub term: String,
52
    /// Initial size in character cells.
53
    pub initial_cols: u16,
54
    pub initial_rows: u16,
55
    /// Characters that break a word for double-click selection. Empty/unset
56
    /// keeps the built-in default.
57
    pub word_delimiters: Option<String>,
58
}
59
60
impl Default for Main {
61
    fn default() -> Self {
62
        Self {
63
            font: "monospace".to_string(),
64
            font_size: 16,
65
            term: "beer".to_string(),
66
            initial_cols: 80,
67
            initial_rows: 24,
68
            word_delimiters: None,
69
        }
70
    }
71
}
72
73
/// `[scrollback]`: history retention.
74
#[derive(Debug, Clone, Deserialize)]
75
#[serde(default, rename_all = "kebab-case")]
76
pub struct Scrollback {
77
    /// Lines of history retained for the main screen.
78
    pub lines: usize,
79
}
80
81
impl Default for Scrollback {
82
    fn default() -> Self {
83
        Self { lines: 10_000 }
84
    }
85
}
86
87
impl Config {
88
    /// Load configuration from `explicit` if given, else the default path.
89
    /// Any read/parse failure logs a warning and returns defaults.
90
    pub fn load(explicit: Option<&Path>) -> Self {
91
        let Some(path) = explicit.map(Path::to_path_buf).or_else(default_path) else {
92
            return Self::default();
93
        };
94
        if !path.exists() {
95
            return Self::default();
96
        }
97
        match std::fs::read_to_string(&path) {
98
            Ok(text) => match toml::from_str(&text) {
99
                Ok(config) => {
100
                    tracing::info!("loaded config from {}", path.display());
101
                    config
102
                }
103
                Err(err) => {
104
                    tracing::warn!("config {}: {err}; using defaults", path.display());
105
                    Self::default()
106
                }
107
            },
108
            Err(err) => {
109
                tracing::warn!("read config {}: {err}; using defaults", path.display());
110
                Self::default()
111
            }
112
        }
113
    }
114
}
115
116
/// `$XDG_CONFIG_HOME/beer/beer.toml`, or `~/.config/beer/beer.toml`.
117
fn default_path() -> Option<PathBuf> {
118
    if let Some(dir) = std::env::var_os("XDG_CONFIG_HOME").filter(|s| !s.is_empty()) {
119
        return Some(PathBuf::from(dir).join("beer/beer.toml"));
120
    }
121
    let home = std::env::var_os("HOME")?;
122
    Some(PathBuf::from(home).join(".config/beer/beer.toml"))
123
}
124
125
#[cfg(test)]
126
mod tests {
127
    use super::*;
128
129
    #[test]
130
    fn defaults_are_sane() {
131
        let c = Config::default();
132
        assert_eq!(c.main.font, "monospace");
133
        assert_eq!(c.main.font_size, 16);
134
        assert_eq!(c.scrollback.lines, 10_000);
135
    }
136
137
    #[test]
138
    fn parses_partial_config_and_ignores_unknown() {
139
        let toml = r##"
140
            [main]
141
            font = "JetBrains Mono"
142
            font-size = 14
143
            unknown-key = "tolerated"
144
145
            [colors]
146
            background = "#000000"
147
        "##;
148
        let c: Config = toml::from_str(toml).unwrap();
149
        assert_eq!(c.main.font, "JetBrains Mono");
150
        assert_eq!(c.main.font_size, 14);
151
        // Unset keys keep defaults; unknown tables/keys are ignored.
152
        assert_eq!(c.main.term, "beer");
153
        assert_eq!(c.scrollback.lines, 10_000);
154
    }
155
}