brewery
notashelf /
53924d381aa89c53ab1e3b2881c1da27f8cbe098

beer

public

A terminal worth pouring time into

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