brewery
notashelf /
2d319b7e7366d88340f4aea106d121fff60450f1

beer

public

A terminal worth pouring time into

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/beer.git
src/config.rsRust160 lines5.1 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
    /// Inner padding in pixels between the window edge and the cell grid.
56
    pub pad_x: u32,
57
    pub pad_y: u32,
58
    /// Characters that break a word for double-click selection. Empty/unset
59
    /// keeps the built-in default.
60
    pub word_delimiters: Option<String>,
61
}
62
63
impl Default for Main {
64
    fn default() -> Self {
65
        Self {
66
            font: "monospace".to_string(),
67
            font_size: 16,
68
            term: "beer".to_string(),
69
            initial_cols: 80,
70
            initial_rows: 24,
71
            pad_x: 2,
72
            pad_y: 2,
73
            word_delimiters: None,
74
        }
75
    }
76
}
77
78
/// `[scrollback]`: history retention.
79
#[derive(Debug, Clone, Deserialize)]
80
#[serde(default, rename_all = "kebab-case")]
81
pub struct Scrollback {
82
    /// Lines of history retained for the main screen.
83
    pub lines: usize,
84
}
85
86
impl Default for Scrollback {
87
    fn default() -> Self {
88
        Self { lines: 10_000 }
89
    }
90
}
91
92
impl Config {
93
    /// Load configuration from `explicit` if given, else the default path.
94
    /// Any read/parse failure logs a warning and returns defaults.
95
    pub fn load(explicit: Option<&Path>) -> Self {
96
        let Some(path) = explicit.map(Path::to_path_buf).or_else(default_path) else {
97
            return Self::default();
98
        };
99
        if !path.exists() {
100
            return Self::default();
101
        }
102
        match std::fs::read_to_string(&path) {
103
            Ok(text) => match toml::from_str(&text) {
104
                Ok(config) => {
105
                    tracing::info!("loaded config from {}", path.display());
106
                    config
107
                }
108
                Err(err) => {
109
                    tracing::warn!("config {}: {err}; using defaults", path.display());
110
                    Self::default()
111
                }
112
            },
113
            Err(err) => {
114
                tracing::warn!("read config {}: {err}; using defaults", path.display());
115
                Self::default()
116
            }
117
        }
118
    }
119
}
120
121
/// `$XDG_CONFIG_HOME/beer/beer.toml`, or `~/.config/beer/beer.toml`.
122
fn default_path() -> Option<PathBuf> {
123
    if let Some(dir) = std::env::var_os("XDG_CONFIG_HOME").filter(|s| !s.is_empty()) {
124
        return Some(PathBuf::from(dir).join("beer/beer.toml"));
125
    }
126
    let home = std::env::var_os("HOME")?;
127
    Some(PathBuf::from(home).join(".config/beer/beer.toml"))
128
}
129
130
#[cfg(test)]
131
mod tests {
132
    use super::*;
133
134
    #[test]
135
    fn defaults_are_sane() {
136
        let c = Config::default();
137
        assert_eq!(c.main.font, "monospace");
138
        assert_eq!(c.main.font_size, 16);
139
        assert_eq!(c.scrollback.lines, 10_000);
140
    }
141
142
    #[test]
143
    fn parses_partial_config_and_ignores_unknown() {
144
        let toml = r##"
145
            [main]
146
            font = "JetBrains Mono"
147
            font-size = 14
148
            unknown-key = "tolerated"
149
150
            [colors]
151
            background = "#000000"
152
        "##;
153
        let c: Config = toml::from_str(toml).unwrap();
154
        assert_eq!(c.main.font, "JetBrains Mono");
155
        assert_eq!(c.main.font_size, 14);
156
        // Unset keys keep defaults; unknown tables/keys are ignored.
157
        assert_eq!(c.main.term, "beer");
158
        assert_eq!(c.scrollback.lines, 10_000);
159
    }
160
}