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)]
24pub struct NotificationPreferences {
25 #[serde(default = "default_true")]
27 pub enabled: bool,
28 #[serde(default = "default_true")]
30 pub on_clarification: bool,
31 #[serde(default = "default_true")]
33 pub on_tool_approval: bool,
34 #[serde(default = "default_true")]
36 pub on_context_pressure: bool,
37 #[serde(default = "default_true")]
39 pub on_subagent_complete: bool,
40}
41
42impl Default for NotificationPreferences {
43 fn default() -> Self {
44 Self {
45 enabled: true,
46 on_clarification: true,
47 on_tool_approval: true,
48 on_context_pressure: true,
49 on_subagent_complete: true,
50 }
51 }
52}
53
54impl NotificationPreferences {
55 pub fn load(path: &Path) -> Self {
62 let contents = match std::fs::read_to_string(path) {
63 Ok(contents) => contents,
64 Err(_) => return Self::default(),
65 };
66 match serde_json::from_str(&contents) {
67 Ok(prefs) => prefs,
68 Err(err) => {
69 tracing::warn!(
70 path = %path.display(),
71 error = %err,
72 "failed to parse notification preferences; using defaults"
73 );
74 Self::default()
75 }
76 }
77 }
78
79 pub fn save(&self, path: &Path) -> std::io::Result<()> {
83 if let Some(parent) = path.parent() {
84 if !parent.as_os_str().is_empty() {
85 std::fs::create_dir_all(parent)?;
86 }
87 }
88 let json = serde_json::to_string_pretty(self)
89 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
90 std::fs::write(path, json)
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn default_is_all_true() {
100 let prefs = NotificationPreferences::default();
101 assert!(prefs.enabled);
102 assert!(prefs.on_clarification);
103 assert!(prefs.on_tool_approval);
104 assert!(prefs.on_context_pressure);
105 assert!(prefs.on_subagent_complete);
106 }
107
108 #[test]
109 fn save_load_roundtrip() {
110 let dir = tempfile::tempdir().unwrap();
111 let path = dir.path().join("nested").join("prefs.json");
112 let prefs = NotificationPreferences {
113 enabled: true,
114 on_clarification: false,
115 on_tool_approval: true,
116 on_context_pressure: false,
117 on_subagent_complete: true,
118 };
119 prefs.save(&path).unwrap();
120 let loaded = NotificationPreferences::load(&path);
121 assert_eq!(prefs, loaded);
122 }
123
124 #[test]
125 fn missing_file_yields_default() {
126 let dir = tempfile::tempdir().unwrap();
127 let path = dir.path().join("does-not-exist.json");
128 assert_eq!(
129 NotificationPreferences::load(&path),
130 NotificationPreferences::default()
131 );
132 }
133
134 #[test]
135 fn partial_json_fills_missing_with_true() {
136 let dir = tempfile::tempdir().unwrap();
137 let path = dir.path().join("partial.json");
138 std::fs::write(&path, r#"{"on_clarification": false}"#).unwrap();
139 let loaded = NotificationPreferences::load(&path);
140 assert!(!loaded.on_clarification);
141 assert!(loaded.enabled);
143 assert!(loaded.on_tool_approval);
144 assert!(loaded.on_context_pressure);
145 assert!(loaded.on_subagent_complete);
146 }
147}