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
204fn 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}