Skip to main content

honk_core/
config.rs

1//! Tool configuration.
2//!
3//! A YAML config in `~/.config/honk/config.yaml` on Linux and macOS (or under
4//! `$XDG_CONFIG_HOME`), `%APPDATA%\honk\config.yaml` on Windows, holding the user's default honk:
5//! style, volume and number of honks. Command-line flags override it.
6
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::{Error, Result};
12
13/// Persisted configuration for the tool.
14#[derive(Debug, Default, Clone, Serialize, Deserialize)]
15#[serde(default)]
16pub struct Config {
17    /// Default horn style.
18    pub style: Option<crate::style::Style>,
19    /// Default volume, 0.0 to 1.0.
20    pub volume: Option<f32>,
21    /// Default number of honks, 1 to 10.
22    pub times: Option<u32>,
23}
24
25impl Config {
26    /// Return the path to the config file, creating no files.
27    ///
28    /// `HONK_CONFIG_DIR` overrides the directory (used by tests and power users).
29    /// Otherwise Linux and macOS use `$XDG_CONFIG_HOME/honk` or `~/.config/honk`,
30    /// the same rule as Ailloy; Windows uses `%APPDATA%\honk`.
31    pub fn config_path() -> Result<PathBuf> {
32        if let Some(dir) = std::env::var_os("HONK_CONFIG_DIR") {
33            return Ok(PathBuf::from(dir).join("config.yaml"));
34        }
35        let dir = crate::paths::config_dir().ok_or(Error::NoConfigDir)?;
36        Ok(dir.join("config.yaml"))
37    }
38
39    /// Load the config from disk. Returns [`Config::default`] if no file exists yet.
40    pub fn load() -> Result<Self> {
41        let path = Self::config_path()?;
42        if !path.exists() {
43            return Ok(Self::default());
44        }
45        let yaml = std::fs::read_to_string(&path)?;
46        Ok(serde_norway::from_str(&yaml)?)
47    }
48
49    /// Save the config to disk, creating the parent directory if needed.
50    pub fn save(&self) -> Result<()> {
51        let path = Self::config_path()?;
52        if let Some(parent) = path.parent() {
53            std::fs::create_dir_all(parent)?;
54        }
55        let yaml = serde_norway::to_string(self)?;
56        std::fs::write(&path, yaml)?;
57        Ok(())
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use crate::style::Style;
65
66    #[test]
67    fn round_trips_through_yaml() {
68        let cfg = Config {
69            style: Some(Style::Awooga),
70            volume: Some(0.5),
71            times: Some(3),
72        };
73        let yaml = serde_norway::to_string(&cfg).unwrap();
74        let parsed: Config = serde_norway::from_str(&yaml).unwrap();
75        assert_eq!(parsed.style, Some(Style::Awooga));
76        assert_eq!(parsed.volume, Some(0.5));
77        assert_eq!(parsed.times, Some(3));
78    }
79
80    #[test]
81    fn serialized_output_is_stable() {
82        // Pins the exact bytes `save` writes, so a YAML library change cannot
83        // silently alter existing config files.
84        let cfg = Config {
85            style: Some(Style::Car),
86            volume: Some(0.5),
87            times: Some(2),
88        };
89        assert_eq!(
90            serde_norway::to_string(&cfg).unwrap(),
91            "style: car\nvolume: 0.5\ntimes: 2\n"
92        );
93        assert_eq!(
94            serde_norway::to_string(&Config::default()).unwrap(),
95            "style: null\nvolume: null\ntimes: null\n"
96        );
97    }
98
99    #[test]
100    fn empty_yaml_uses_defaults() {
101        let parsed: Config = serde_norway::from_str("{}").unwrap();
102        assert!(parsed.style.is_none() && parsed.volume.is_none() && parsed.times.is_none());
103    }
104}