use std::path::Path;
use serde::{Deserialize, Serialize};
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NotificationPreferences {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_true")]
pub on_clarification: bool,
#[serde(default = "default_true")]
pub on_tool_approval: bool,
#[serde(default = "default_true")]
pub on_context_pressure: bool,
#[serde(default = "default_true")]
pub on_subagent_complete: bool,
#[serde(default = "default_true")]
pub on_background_task_complete: bool,
#[serde(default = "default_true")]
pub on_run_complete: bool,
#[serde(default = "default_true")]
pub on_run_failed: bool,
}
impl Default for NotificationPreferences {
fn default() -> Self {
Self {
enabled: true,
on_clarification: true,
on_tool_approval: true,
on_context_pressure: true,
on_subagent_complete: true,
on_background_task_complete: true,
on_run_complete: true,
on_run_failed: true,
}
}
}
impl NotificationPreferences {
pub fn load(path: &Path) -> Self {
let contents = match std::fs::read_to_string(path) {
Ok(contents) => contents,
Err(_) => return Self::default(),
};
match serde_json::from_str(&contents) {
Ok(prefs) => prefs,
Err(err) => {
tracing::warn!(
path = %path.display(),
error = %err,
"failed to parse notification preferences; using defaults"
);
Self::default()
}
}
}
pub fn save(&self, path: &Path) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)?;
}
}
let json = serde_json::to_string_pretty(self)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
std::fs::write(path, json)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_all_true() {
let prefs = NotificationPreferences::default();
assert!(prefs.enabled);
assert!(prefs.on_clarification);
assert!(prefs.on_tool_approval);
assert!(prefs.on_context_pressure);
assert!(prefs.on_subagent_complete);
assert!(prefs.on_background_task_complete);
assert!(prefs.on_run_complete);
assert!(prefs.on_run_failed);
}
#[test]
fn save_load_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nested").join("prefs.json");
let prefs = NotificationPreferences {
enabled: true,
on_clarification: false,
on_tool_approval: true,
on_context_pressure: false,
on_subagent_complete: true,
on_background_task_complete: false,
on_run_complete: false,
on_run_failed: true,
};
prefs.save(&path).unwrap();
let loaded = NotificationPreferences::load(&path);
assert_eq!(prefs, loaded);
}
#[test]
fn missing_file_yields_default() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("does-not-exist.json");
assert_eq!(
NotificationPreferences::load(&path),
NotificationPreferences::default()
);
}
#[test]
fn partial_json_fills_missing_with_true() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("partial.json");
std::fs::write(&path, r#"{"on_clarification": false}"#).unwrap();
let loaded = NotificationPreferences::load(&path);
assert!(!loaded.on_clarification);
assert!(loaded.enabled);
assert!(loaded.on_tool_approval);
assert!(loaded.on_context_pressure);
assert!(loaded.on_subagent_complete);
assert!(loaded.on_background_task_complete);
assert!(loaded.on_run_complete);
assert!(loaded.on_run_failed);
}
#[test]
fn new_keys_missing_from_an_older_clients_json_default_to_true() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("old-client.json");
std::fs::write(
&path,
r#"{
"enabled": true,
"on_clarification": true,
"on_tool_approval": true,
"on_context_pressure": true,
"on_subagent_complete": true,
"on_background_task_complete": true
}"#,
)
.unwrap();
let loaded = NotificationPreferences::load(&path);
assert!(loaded.on_run_complete);
assert!(loaded.on_run_failed);
}
}