brewery
notashelf /
72ec651ff1dc4daa6a0146a6dd9c1b47c4d5c464

beer

public

A terminal worth pouring time into

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