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