1use anyhow::{Context, Result};
2use directories::ProjectDirs;
3use serde::{Deserialize, Serialize};
4use serde_aux::prelude::bool_true;
5use std::collections::HashSet;
6use std::fs;
7use std::path::PathBuf;
8
9use crate::lighthouse::Lighthouse;
10
11pub fn config_local_dir() -> Result<PathBuf> {
19 let proj = ProjectDirs::from("io", "atomicflag", "Lighthouse Manager")
20 .ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))?;
21 let dir = proj.config_local_dir();
22 fs::create_dir_all(dir).context("Failed to create config directory")?;
24 Ok(dir.to_path_buf())
25}
26
27fn config_path() -> Result<PathBuf> {
29 let dir = config_local_dir()?;
30 Ok(dir.join("settings.jsonc"))
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Autostart {
36 pub cooldown_secs: u64,
39 pub last_turned_off_at: Option<u64>,
41}
42
43impl Default for Autostart {
44 fn default() -> Self {
45 Self {
46 cooldown_secs: 600,
47 last_turned_off_at: None,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct AppSettings {
55 pub version: u32,
56 pub lighthouses: Vec<Lighthouse>,
57 #[serde(default)]
58 pub autostart: Autostart,
59 #[serde(default = "bool_true")]
62 pub parallel_power: bool,
63}
64
65impl Default for AppSettings {
66 fn default() -> Self {
67 Self {
68 version: 1,
69 lighthouses: Vec::new(),
70 autostart: Autostart::default(),
71 parallel_power: true,
72 }
73 }
74}
75
76fn load_at(path: &PathBuf) -> Result<AppSettings> {
78 if !path.exists() {
79 return Ok(AppSettings::default());
80 }
81 let content = fs::read_to_string(path).context("Failed to read settings")?;
82 let settings: AppSettings =
83 jsonc_parser::parse_to_serde_value(&content, &jsonc_parser::ParseOptions::default())
84 .context("Failed to parse settings JSONC")?;
85 Ok(settings)
86}
87
88fn save_at(path: &PathBuf, settings: &AppSettings) -> Result<()> {
90 let content = serde_json::to_string_pretty(settings).context("Failed to serialize settings")?;
91 fs::write(path, content).context("Failed to write settings")?;
92 Ok(())
93}
94
95pub fn load() -> Result<AppSettings> {
101 let path = config_path()?;
102 load_at(&path)
103}
104
105pub fn save(settings: &AppSettings) -> Result<()> {
115 let path = config_path()?;
116 save_at(&path, settings)
117}
118
119pub fn add_new(settings: &mut AppSettings, discovered: &[Lighthouse]) -> usize {
124 let existing: HashSet<String> = settings
125 .lighthouses
126 .iter()
127 .map(|l| l.address.clone())
128 .collect();
129
130 let new_lhs: Vec<Lighthouse> = discovered
131 .iter()
132 .filter(|lh| !existing.contains(&lh.address))
133 .cloned()
134 .collect();
135 let count = new_lhs.len();
136 settings.lighthouses.extend(new_lhs);
137 count
138}
139
140#[must_use]
142pub fn managed_lighthouses(settings: &AppSettings) -> Vec<&Lighthouse> {
143 settings.lighthouses.iter().filter(|l| l.managed).collect()
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use std::path::PathBuf;
150
151 fn test_settings_path() -> (PathBuf, tempfile::TempDir) {
152 let dir = tempfile::tempdir().unwrap();
153 let path = dir.path().join("settings.jsonc");
154 (path, dir)
155 }
156
157 #[test]
158 fn test_load_empty_settings() {
159 let (path, _guard) = test_settings_path();
160 let settings = load_at(&path).unwrap();
161 assert!(settings.lighthouses.is_empty());
162 assert_eq!(settings.version, 1);
163 }
164
165 #[test]
166 fn test_save_and_load() {
167 let (path, _guard) = test_settings_path();
168
169 let settings = AppSettings {
170 version: 1,
171 lighthouses: vec![Lighthouse {
172 name: "LHB-0A1B2C3D".into(),
173 address: "AA:BB:CC:DD:EE:FF".into(),
174 id: None,
175 managed: true,
176 }],
177 ..Default::default()
178 };
179 save_at(&path, &settings).unwrap();
180
181 let loaded = load_at(&path).unwrap();
182 assert_eq!(loaded.lighthouses.len(), 1);
183 assert_eq!(loaded.lighthouses[0].name, "LHB-0A1B2C3D");
184 assert!(loaded.lighthouses[0].managed);
185 }
186
187 #[test]
188 fn test_add_new_deduplication() {
189 let (path, _guard) = test_settings_path();
190
191 let mut settings = AppSettings {
192 version: 1,
193 lighthouses: vec![Lighthouse {
194 name: "HTC BS-AABBCCDD".into(),
195 address: "AA:BB:CC:DD:EE:FF".into(),
196 id: Some("AABBCCDD".into()),
197 managed: true,
198 }],
199 ..Default::default()
200 };
201
202 let discovered = vec![
204 Lighthouse {
205 name: "HTC BS-AABBCCDD-NEW".into(), address: "AA:BB:CC:DD:EE:FF".into(),
207 id: Some("AABBCCDD2".into()),
208 managed: true,
209 },
210 Lighthouse {
211 name: "LHB-0A1B2C3D".into(),
212 address: "11:22:33:44:55:66".into(),
213 id: None,
214 managed: true,
215 },
216 ];
217
218 let count = add_new(&mut settings, &discovered);
219 assert_eq!(count, 1); assert_eq!(settings.lighthouses.len(), 2);
221 assert_eq!(settings.lighthouses[0].name, "HTC BS-AABBCCDD");
223
224 save_at(&path, &settings).ok();
225 }
226
227 #[test]
228 fn test_newly_discovered_are_unmanaged() {
229 let (path, _guard) = test_settings_path();
230
231 let mut settings = AppSettings::default();
232 let discovered = vec![Lighthouse {
233 name: "LHB-0A1B2C3D".into(),
234 address: "AA:BB:CC:DD:EE:FF".into(),
235 id: None,
236 managed: false, }];
238
239 add_new(&mut settings, &discovered);
240
241 assert!(!settings.lighthouses[0].managed);
242
243 save_at(&path, &settings).ok();
244 }
245
246 #[test]
247 fn test_managed_lighthouses_filter() {
248 let settings = AppSettings {
249 version: 1,
250 lighthouses: vec![
251 Lighthouse {
252 name: "LHB-0000".into(),
253 address: "AA:00".into(),
254 id: None,
255 managed: true,
256 },
257 Lighthouse {
258 name: "HTC BS-1111".into(),
259 address: "BB:00".into(),
260 id: Some("1111".into()),
261 managed: false,
262 },
263 Lighthouse {
264 name: "LHB-2222".into(),
265 address: "CC:00".into(),
266 id: None,
267 managed: true,
268 },
269 ],
270 ..Default::default()
271 };
272
273 let managed = managed_lighthouses(&settings);
274 assert_eq!(managed.len(), 2);
275 assert_eq!(managed[0].name, "LHB-0000");
276 assert_eq!(managed[1].name, "LHB-2222");
277 }
278
279 #[test]
280 fn test_serde_roundtrip() {
281 let settings = AppSettings {
282 version: 1,
283 lighthouses: vec![
284 Lighthouse {
285 name: "HTC BS-AABBCCDD".into(),
286 address: "AA:BB:CC:DD:EE:FF".into(),
287 id: Some("AABBCCDD".into()),
288 managed: true,
289 },
290 Lighthouse {
291 name: "LHB-0A1B2C3D".into(),
292 address: "11:22:33:44:55:66".into(),
293 id: None,
294 managed: false,
295 },
296 ],
297 ..Default::default()
298 };
299
300 let json = serde_json::to_string_pretty(&settings).unwrap();
301 let restored: AppSettings =
302 jsonc_parser::parse_to_serde_value(&json, &jsonc_parser::ParseOptions::default())
303 .unwrap();
304 assert_eq!(restored.version, 1);
305 assert_eq!(restored.lighthouses.len(), 2);
306 }
307}