bamboo_notification/
preferences.rs1use std::path::Path;
8
9use serde::{Deserialize, Serialize};
10
11fn default_true() -> bool {
12 true
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37pub struct NotificationPreferences {
38 #[serde(default = "default_true")]
40 pub enabled: bool,
41 #[serde(default = "default_true")]
43 pub on_clarification: bool,
44 #[serde(default = "default_true")]
46 pub on_tool_approval: bool,
47 #[serde(default = "default_true")]
49 pub on_context_pressure: bool,
50 #[serde(default = "default_true")]
52 pub on_subagent_complete: bool,
53 #[serde(default = "default_true")]
55 pub on_background_task_complete: bool,
56 #[serde(default = "default_true")]
58 pub on_run_complete: bool,
59 #[serde(default = "default_true")]
61 pub on_run_failed: bool,
62}
63
64impl Default for NotificationPreferences {
65 fn default() -> Self {
66 Self {
67 enabled: true,
68 on_clarification: true,
69 on_tool_approval: true,
70 on_context_pressure: true,
71 on_subagent_complete: true,
72 on_background_task_complete: true,
73 on_run_complete: true,
74 on_run_failed: true,
75 }
76 }
77}
78
79impl NotificationPreferences {
80 pub fn load(path: &Path) -> Self {
87 let contents = match std::fs::read_to_string(path) {
88 Ok(contents) => contents,
89 Err(_) => return Self::default(),
90 };
91 match serde_json::from_str(&contents) {
92 Ok(prefs) => prefs,
93 Err(err) => {
94 tracing::warn!(
95 path = %path.display(),
96 error = %err,
97 "failed to parse notification preferences; using defaults"
98 );
99 Self::default()
100 }
101 }
102 }
103
104 pub fn save(&self, path: &Path) -> std::io::Result<()> {
108 if let Some(parent) = path.parent() {
109 if !parent.as_os_str().is_empty() {
110 std::fs::create_dir_all(parent)?;
111 }
112 }
113 let json = serde_json::to_string_pretty(self)
114 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
115 std::fs::write(path, json)
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn default_is_all_true() {
125 let prefs = NotificationPreferences::default();
126 assert!(prefs.enabled);
127 assert!(prefs.on_clarification);
128 assert!(prefs.on_tool_approval);
129 assert!(prefs.on_context_pressure);
130 assert!(prefs.on_subagent_complete);
131 assert!(prefs.on_background_task_complete);
132 assert!(prefs.on_run_complete);
133 assert!(prefs.on_run_failed);
134 }
135
136 #[test]
137 fn save_load_roundtrip() {
138 let dir = tempfile::tempdir().unwrap();
139 let path = dir.path().join("nested").join("prefs.json");
140 let prefs = NotificationPreferences {
141 enabled: true,
142 on_clarification: false,
143 on_tool_approval: true,
144 on_context_pressure: false,
145 on_subagent_complete: true,
146 on_background_task_complete: false,
147 on_run_complete: false,
148 on_run_failed: true,
149 };
150 prefs.save(&path).unwrap();
151 let loaded = NotificationPreferences::load(&path);
152 assert_eq!(prefs, loaded);
153 }
154
155 #[test]
156 fn missing_file_yields_default() {
157 let dir = tempfile::tempdir().unwrap();
158 let path = dir.path().join("does-not-exist.json");
159 assert_eq!(
160 NotificationPreferences::load(&path),
161 NotificationPreferences::default()
162 );
163 }
164
165 #[test]
166 fn partial_json_fills_missing_with_true() {
167 let dir = tempfile::tempdir().unwrap();
168 let path = dir.path().join("partial.json");
169 std::fs::write(&path, r#"{"on_clarification": false}"#).unwrap();
170 let loaded = NotificationPreferences::load(&path);
171 assert!(!loaded.on_clarification);
172 assert!(loaded.enabled);
174 assert!(loaded.on_tool_approval);
175 assert!(loaded.on_context_pressure);
176 assert!(loaded.on_subagent_complete);
177 assert!(loaded.on_background_task_complete);
178 assert!(loaded.on_run_complete);
179 assert!(loaded.on_run_failed);
180 }
181
182 #[test]
183 fn new_keys_missing_from_an_older_clients_json_default_to_true() {
184 let dir = tempfile::tempdir().unwrap();
187 let path = dir.path().join("old-client.json");
188 std::fs::write(
189 &path,
190 r#"{
191 "enabled": true,
192 "on_clarification": true,
193 "on_tool_approval": true,
194 "on_context_pressure": true,
195 "on_subagent_complete": true,
196 "on_background_task_complete": true
197 }"#,
198 )
199 .unwrap();
200 let loaded = NotificationPreferences::load(&path);
201 assert!(loaded.on_run_complete);
202 assert!(loaded.on_run_failed);
203 }
204}