Skip to main content

honk_core/
config.rs

1//! Tool configuration.
2//!
3//! A YAML config stored in the platform config directory
4//! (`~/.config/honk/config.yaml` on Linux) 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    /// `HONK_CONFIG_DIR` overrides the directory (used by tests and power users).
28    pub fn config_path() -> Result<PathBuf> {
29        if let Some(dir) = std::env::var_os("HONK_CONFIG_DIR") {
30            return Ok(PathBuf::from(dir).join("config.yaml"));
31        }
32        let dir = dirs::config_dir().ok_or(Error::NoConfigDir)?;
33        Ok(dir.join("honk").join("config.yaml"))
34    }
35
36    /// Load the config from disk. Returns [`Config::default`] if no file exists yet.
37    pub fn load() -> Result<Self> {
38        let path = Self::config_path()?;
39        if !path.exists() {
40            return Ok(Self::default());
41        }
42        let yaml = std::fs::read_to_string(&path)?;
43        Ok(serde_norway::from_str(&yaml)?)
44    }
45
46    /// Save the config to disk, creating the parent directory if needed.
47    pub fn save(&self) -> Result<()> {
48        let path = Self::config_path()?;
49        if let Some(parent) = path.parent() {
50            std::fs::create_dir_all(parent)?;
51        }
52        let yaml = serde_norway::to_string(self)?;
53        std::fs::write(&path, yaml)?;
54        Ok(())
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::style::Style;
62
63    #[test]
64    fn round_trips_through_yaml() {
65        let cfg = Config {
66            style: Some(Style::Awooga),
67            volume: Some(0.5),
68            times: Some(3),
69        };
70        let yaml = serde_norway::to_string(&cfg).unwrap();
71        let parsed: Config = serde_norway::from_str(&yaml).unwrap();
72        assert_eq!(parsed.style, Some(Style::Awooga));
73        assert_eq!(parsed.volume, Some(0.5));
74        assert_eq!(parsed.times, Some(3));
75    }
76
77    #[test]
78    fn serialized_output_is_stable() {
79        // Pins the exact bytes `save` writes, so a YAML library change cannot
80        // silently alter existing config files.
81        let cfg = Config {
82            style: Some(Style::Car),
83            volume: Some(0.5),
84            times: Some(2),
85        };
86        assert_eq!(
87            serde_norway::to_string(&cfg).unwrap(),
88            "style: car\nvolume: 0.5\ntimes: 2\n"
89        );
90        assert_eq!(
91            serde_norway::to_string(&Config::default()).unwrap(),
92            "style: null\nvolume: null\ntimes: null\n"
93        );
94    }
95
96    #[test]
97    fn empty_yaml_uses_defaults() {
98        let parsed: Config = serde_norway::from_str("{}").unwrap();
99        assert!(parsed.style.is_none() && parsed.volume.is_none() && parsed.times.is_none());
100    }
101}