Skip to main content

max / audiofiles

3.5 KB · 117 lines History Blame Raw
1 //! User preferences persisted to the config directory.
2 //!
3 //! Lives at `config_dir/preferences.json`. Only options that need to survive
4 //! across launches and that the user can change at runtime belong here.
5 //! Build-time settings stay in `synckit.toml` / env vars.
6
7 use std::path::{Path, PathBuf};
8
9 use serde::{Deserialize, Serialize};
10
11 const PREFERENCES_FILENAME: &str = "preferences.json";
12
13 fn default_check_for_updates() -> bool {
14 true
15 }
16
17 #[derive(Debug, Clone, Serialize, Deserialize)]
18 pub struct Preferences {
19 /// Whether the OTA update checker is allowed to contact makenot.work.
20 /// Defaults to `true`. Users on metered networks or in privacy-sensitive
21 /// environments can disable from the About box.
22 #[serde(default = "default_check_for_updates")]
23 pub check_for_updates: bool,
24 }
25
26 impl Default for Preferences {
27 fn default() -> Self {
28 Self {
29 check_for_updates: default_check_for_updates(),
30 }
31 }
32 }
33
34 fn path_for(config_dir: &Path) -> PathBuf {
35 config_dir.join(PREFERENCES_FILENAME)
36 }
37
38 impl Preferences {
39 pub fn load(config_dir: &Path) -> Self {
40 let path = path_for(config_dir);
41 match std::fs::read_to_string(&path) {
42 Ok(s) => serde_json::from_str(&s).unwrap_or_else(|e| {
43 tracing::warn!("Failed to parse {}: {e}; using defaults", path.display());
44 Preferences::default()
45 }),
46 Err(_) => Preferences::default(),
47 }
48 }
49
50 pub fn save(&self, config_dir: &Path) {
51 let path = path_for(config_dir);
52 match serde_json::to_string_pretty(self) {
53 Ok(s) => {
54 if let Err(e) = std::fs::write(&path, s) {
55 tracing::warn!("Failed to write {}: {e}", path.display());
56 }
57 }
58 Err(e) => tracing::warn!("Failed to serialise preferences: {e}"),
59 }
60 }
61 }
62
63 #[cfg(test)]
64 mod tests {
65 use super::*;
66
67 #[test]
68 fn default_enables_update_checks() {
69 let p = Preferences::default();
70 assert!(p.check_for_updates);
71 }
72
73 #[test]
74 fn missing_file_returns_defaults() {
75 let dir = tempfile::tempdir().unwrap();
76 let p = Preferences::load(dir.path());
77 assert!(p.check_for_updates);
78 }
79
80 #[test]
81 fn save_then_load_roundtrip() {
82 let dir = tempfile::tempdir().unwrap();
83 let p = Preferences { check_for_updates: false };
84 p.save(dir.path());
85 let loaded = Preferences::load(dir.path());
86 assert!(!loaded.check_for_updates);
87 }
88
89 #[test]
90 fn unknown_field_in_file_does_not_break_load() {
91 let dir = tempfile::tempdir().unwrap();
92 std::fs::write(
93 dir.path().join(PREFERENCES_FILENAME),
94 r#"{"check_for_updates": false, "future_field": 42}"#,
95 )
96 .unwrap();
97 let loaded = Preferences::load(dir.path());
98 assert!(!loaded.check_for_updates);
99 }
100
101 #[test]
102 fn missing_check_for_updates_field_defaults_to_true() {
103 let dir = tempfile::tempdir().unwrap();
104 std::fs::write(dir.path().join(PREFERENCES_FILENAME), r#"{}"#).unwrap();
105 let loaded = Preferences::load(dir.path());
106 assert!(loaded.check_for_updates);
107 }
108
109 #[test]
110 fn corrupt_file_falls_back_to_defaults() {
111 let dir = tempfile::tempdir().unwrap();
112 std::fs::write(dir.path().join(PREFERENCES_FILENAME), "{not json").unwrap();
113 let loaded = Preferences::load(dir.path());
114 assert!(loaded.check_for_updates);
115 }
116 }
117