bom_buddy/
config.rs

1use crate::cli::{Cli, Commands};
2use crate::client::{Client, ClientOptions};
3use crate::persistence::Database;
4use crate::radar::{Radar, RadarId, RadarImageOptions};
5use crate::util::remove_if_exists;
6use crate::{location::Location, logging::LoggingOptions};
7use anyhow::{anyhow, Result};
8use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
9use figment::providers::{Env, Format, Serialized, Yaml};
10use figment::Figment;
11use once_cell::sync::OnceCell;
12use serde::{Deserialize, Serialize};
13use std::fs;
14use std::path::PathBuf;
15use tracing::info;
16
17#[derive(Debug, Deserialize, Serialize)]
18pub struct Config {
19    #[serde(skip)]
20    pub config_path: PathBuf,
21    pub main: MainConfig,
22}
23
24impl Default for Config {
25    fn default() -> Self {
26        Self {
27            config_path: Self::default_path(),
28            main: MainConfig::default(),
29        }
30    }
31}
32
33#[derive(Debug, Deserialize, Serialize)]
34pub struct MainConfig {
35    pub db_path: PathBuf,
36    pub locations: Vec<String>,
37    pub logging: LoggingOptions,
38    pub client: ClientOptions,
39    pub radars: Vec<RadarConfig>,
40    pub current_fstring: String,
41}
42
43#[derive(Debug, Deserialize, Serialize)]
44pub struct RadarConfig {
45    pub id: RadarId,
46    pub name: String,
47    pub opts: RadarImageOptions,
48}
49
50impl Default for MainConfig {
51    fn default() -> Self {
52        Self {
53            db_path: Config::default_dirs().state.join("bom-buddy.db"),
54            logging: LoggingOptions::default(),
55            client: ClientOptions::default(),
56            radars: Vec::new(),
57            locations: Vec::new(),
58            current_fstring: "{icon} {temp} ({next_temp})".to_string(),
59        }
60    }
61}
62
63impl Config {
64    pub fn default_path() -> PathBuf {
65        Self::default_dirs().config.join("config.yml")
66    }
67
68    pub fn from_default_path() -> Result<Self> {
69        let config_path = Self::default_path();
70
71        let main = Figment::from(Serialized::defaults(MainConfig::default()))
72            .merge(Yaml::file(&config_path))
73            .merge(Env::prefixed("BOM_"))
74            .extract()?;
75
76        Ok(Self { config_path, main })
77    }
78
79    pub fn default_dirs() -> &'static DefaultDirs {
80        if let Some(defaults) = DEFAULT_DIRS.get() {
81            defaults
82        } else {
83            let strategy = choose_app_strategy(AppStrategyArgs {
84                top_level_domain: "org".to_string(),
85                author: "sublipri".to_string(),
86                app_name: "BOM Buddy".to_string(),
87            })
88            .unwrap();
89            let defaults = DefaultDirs {
90                home: strategy.home_dir().to_path_buf(),
91                config: strategy.config_dir(),
92                cache: strategy.cache_dir(),
93                data: strategy.data_dir(),
94                run: strategy.runtime_dir().unwrap_or(strategy.data_dir()),
95                state: strategy.state_dir().unwrap_or(strategy.data_dir()),
96            };
97            DEFAULT_DIRS.set(defaults).unwrap();
98            DEFAULT_DIRS.get().unwrap()
99        }
100    }
101
102    pub fn from_cli(args: &Cli) -> Result<Self> {
103        let config_path = if let Some(path) = &args.config_path {
104            path.to_owned()
105        } else {
106            Self::default_path().to_owned()
107        };
108        if let Some(Commands::Init(iargs)) = &args.command {
109            if iargs.force {
110                remove_if_exists(&config_path)?;
111            }
112        }
113
114        let main = Figment::from(Serialized::defaults(MainConfig::default()))
115            .merge(Yaml::file(&config_path))
116            .merge(Env::prefixed("BOM_"))
117            .merge(Serialized::defaults(args));
118
119        let main = match &args.command {
120            Some(Commands::Radar(rargs)) => {
121                let arg_opts = serde_json::to_value(rargs)?;
122                let mut radar_array: serde_json::Value = main.extract_inner("radars")?;
123                override_array_opts(&mut radar_array, &arg_opts);
124                main.merge(("radars", radar_array))
125            }
126            _ => main,
127        };
128
129        let mut main: MainConfig = main.extract()?;
130
131        if let Some(level) = args.log_level {
132            main.logging.console_level = level;
133        }
134        if let Some(level) = args.log_file_level {
135            main.logging.file_level = level;
136        }
137        if let Some(path) = &args.log_path {
138            main.logging.file_path = path.clone();
139        }
140        Ok(Config { config_path, main })
141    }
142
143    pub fn write_config_file(&self) -> Result<()> {
144        let yaml = serde_yaml::to_string(&self.main)?;
145        if let Some(parent) = self.config_path.parent() {
146            fs::create_dir_all(parent)?;
147        }
148        fs::write(&self.config_path, yaml)?;
149        Ok(())
150    }
151
152    pub fn get_database(&self) -> Result<Database> {
153        Database::from_path(self.main.db_path.clone())
154    }
155
156    pub fn get_client(&self) -> Client {
157        Client::new(self.main.client.clone())
158    }
159
160    pub fn add_location(&mut self, location: &Location) -> Result<()> {
161        if self.main.locations.contains(&location.id) {
162            return Err(anyhow!(
163                "{} already in {}",
164                location.id,
165                self.config_path.display()
166            ));
167        }
168        info!("Adding {} to {}", location.id, self.config_path.display());
169        self.main.locations.push(location.id.to_owned());
170        self.write_config_file()?;
171        Ok(())
172    }
173
174    pub fn add_radar(&mut self, radar: &Radar) -> Result<()> {
175        let radar_config = RadarConfig {
176            id: radar.id,
177            name: radar.full_name.clone(),
178            opts: RadarImageOptions::default(),
179        };
180        info!(
181            "Adding radar {} {} to {}",
182            radar_config.id,
183            &radar_config.name,
184            self.config_path.display()
185        );
186        self.main.radars.push(radar_config);
187        self.write_config_file()?;
188        Ok(())
189    }
190}
191
192static DEFAULT_DIRS: OnceCell<DefaultDirs> = OnceCell::new();
193
194#[derive(Debug, Deserialize, Serialize)]
195pub struct DefaultDirs {
196    pub home: PathBuf,
197    pub config: PathBuf,
198    pub cache: PathBuf,
199    pub data: PathBuf,
200    pub state: PathBuf,
201    pub run: PathBuf,
202}
203
204// A hacky way to allow different radars to have different options in the config file
205// that are still overwritten by CLI arguments
206fn override_array_opts(config_array: &mut serde_json::Value, arg_opts: &serde_json::Value) {
207    let config_array = config_array.as_array_mut().unwrap();
208    for element in &mut *config_array {
209        let Some(conf_opts) = element.get_mut("opts") else {
210            continue;
211        };
212        let Some(conf_opts) = conf_opts.as_object_mut() else {
213            continue;
214        };
215        for (key, arg_value) in arg_opts.as_object().unwrap() {
216            if let Some(conf_value) = conf_opts.get_mut(key) {
217                *conf_value = arg_value.clone();
218            }
219        }
220    }
221}