1use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::{Error, Result};
12
13#[derive(Debug, Default, Clone, Serialize, Deserialize)]
15#[serde(default)]
16pub struct Config {
17 pub style: Option<crate::style::Style>,
19 pub volume: Option<f32>,
21 pub times: Option<u32>,
23}
24
25impl Config {
26 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 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 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 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}