brewery
notashelf /
0738ce3b6f70d7848889218f41239f396b523b4f

beer

public

A terminal worth pouring time into

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/beer.git
src/config.rsRust180 lines5.7 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 cursor: Cursor,
17
    pub scrollback: Scrollback,
18
    pub bell: Bell,
19
}
20
21
/// `[cursor]`: the default cursor presentation (DECSCUSR may override at runtime).
22
#[derive(Debug, Clone, Default, Deserialize)]
23
#[serde(default, rename_all = "kebab-case")]
24
pub struct Cursor {
25
    /// `block`, `beam`/`bar`, or `underline`.
26
    pub style: Option<String>,
27
    /// Whether the cursor blinks by default.
28
    pub blink: bool,
29
}
30
31
/// `[bell]`: what happens on `BEL` (0x07).
32
#[derive(Debug, Clone, Default, Deserialize)]
33
#[serde(default, rename_all = "kebab-case")]
34
pub struct Bell {
35
    /// Briefly flash the screen.
36
    pub visual: bool,
37
}
38
39
/// `[colors]`: foreground/background, the 16 base palette entries, and accents.
40
/// Each value is an X11 colour spec (`#rrggbb` or `rgb:rr/gg/bb`); unset entries
41
/// keep the built-in default.
42
#[derive(Debug, Clone, Default, Deserialize)]
43
#[serde(default, rename_all = "kebab-case")]
44
pub struct Colors {
45
    pub foreground: Option<String>,
46
    pub background: Option<String>,
47
    pub cursor: Option<String>,
48
    pub selection_foreground: Option<String>,
49
    pub selection_background: Option<String>,
50
    /// The eight regular palette entries (indices 0-7).
51
    pub regular: Option<Vec<String>>,
52
    /// The eight bright palette entries (indices 8-15).
53
    pub bright: Option<Vec<String>>,
54
    pub match_background: Option<String>,
55
    pub match_current_background: Option<String>,
56
    /// Background opacity, 0.0 (transparent) - 1.0 (opaque).
57
    pub alpha: Option<f32>,
58
    /// Render bold text with the bright palette variant.
59
    pub bold_as_bright: Option<bool>,
60
}
61
62
/// `[main]`: fonts, window geometry, padding, and the terminal name.
63
#[derive(Debug, Clone, Deserialize)]
64
#[serde(default, rename_all = "kebab-case")]
65
pub struct Main {
66
    /// Primary font family, resolved via fontconfig.
67
    pub font: String,
68
    /// Font size in pixels.
69
    pub font_size: u32,
70
    /// `TERM` value exported to the child shell.
71
    pub term: String,
72
    /// Initial size in character cells.
73
    pub initial_cols: u16,
74
    pub initial_rows: u16,
75
    /// Inner padding in pixels between the window edge and the cell grid.
76
    pub pad_x: u32,
77
    pub pad_y: u32,
78
    /// Characters that break a word for double-click selection. Empty/unset
79
    /// keeps the built-in default.
80
    pub word_delimiters: Option<String>,
81
}
82
83
impl Default for Main {
84
    fn default() -> Self {
85
        Self {
86
            font: "monospace".to_string(),
87
            font_size: 16,
88
            term: "beer".to_string(),
89
            initial_cols: 80,
90
            initial_rows: 24,
91
            pad_x: 2,
92
            pad_y: 2,
93
            word_delimiters: None,
94
        }
95
    }
96
}
97
98
/// `[scrollback]`: history retention.
99
#[derive(Debug, Clone, Deserialize)]
100
#[serde(default, rename_all = "kebab-case")]
101
pub struct Scrollback {
102
    /// Lines of history retained for the main screen.
103
    pub lines: usize,
104
}
105
106
impl Default for Scrollback {
107
    fn default() -> Self {
108
        Self { lines: 10_000 }
109
    }
110
}
111
112
impl Config {
113
    /// Load configuration from `explicit` if given, else the default path.
114
    /// Any read/parse failure logs a warning and returns defaults.
115
    pub fn load(explicit: Option<&Path>) -> Self {
116
        let Some(path) = explicit.map(Path::to_path_buf).or_else(default_path) else {
117
            return Self::default();
118
        };
119
        if !path.exists() {
120
            return Self::default();
121
        }
122
        match std::fs::read_to_string(&path) {
123
            Ok(text) => match toml::from_str(&text) {
124
                Ok(config) => {
125
                    tracing::info!("loaded config from {}", path.display());
126
                    config
127
                }
128
                Err(err) => {
129
                    tracing::warn!("config {}: {err}; using defaults", path.display());
130
                    Self::default()
131
                }
132
            },
133
            Err(err) => {
134
                tracing::warn!("read config {}: {err}; using defaults", path.display());
135
                Self::default()
136
            }
137
        }
138
    }
139
}
140
141
/// `$XDG_CONFIG_HOME/beer/beer.toml`, or `~/.config/beer/beer.toml`.
142
fn default_path() -> Option<PathBuf> {
143
    if let Some(dir) = std::env::var_os("XDG_CONFIG_HOME").filter(|s| !s.is_empty()) {
144
        return Some(PathBuf::from(dir).join("beer/beer.toml"));
145
    }
146
    let home = std::env::var_os("HOME")?;
147
    Some(PathBuf::from(home).join(".config/beer/beer.toml"))
148
}
149
150
#[cfg(test)]
151
mod tests {
152
    use super::*;
153
154
    #[test]
155
    fn defaults_are_sane() {
156
        let c = Config::default();
157
        assert_eq!(c.main.font, "monospace");
158
        assert_eq!(c.main.font_size, 16);
159
        assert_eq!(c.scrollback.lines, 10_000);
160
    }
161
162
    #[test]
163
    fn parses_partial_config_and_ignores_unknown() {
164
        let toml = r##"
165
            [main]
166
            font = "JetBrains Mono"
167
            font-size = 14
168
            unknown-key = "tolerated"
169
170
            [colors]
171
            background = "#000000"
172
        "##;
173
        let c: Config = toml::from_str(toml).unwrap();
174
        assert_eq!(c.main.font, "JetBrains Mono");
175
        assert_eq!(c.main.font_size, 14);
176
        // Unset keys keep defaults; unknown tables/keys are ignored.
177
        assert_eq!(c.main.term, "beer");
178
        assert_eq!(c.scrollback.lines, 10_000);
179
    }
180
}