brewery
notashelf /
0c0da3d035ae9bfeec4bf921699f2bd07905f2ab

beer

public

A terminal worth pouring time into

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