Skip to main content

ralph/notification/
config.rs

1//! Notification configuration resolution.
2//!
3//! Responsibilities:
4//! - Define CLI override and runtime configuration types for notifications.
5//! - Merge config-file values with CLI overrides into a resolved runtime config.
6//!
7//! Does NOT handle:
8//! - Notification display or sound playback.
9//! - UI activity detection beyond suppression flags.
10//!
11//! Invariants:
12//! - Runtime config defaults remain explicit and stable.
13//! - CLI override precedence is preserved over stored configuration.
14
15/// CLI overrides for notification settings.
16/// Fields are `Option<bool>` to distinguish "not set" from explicit false.
17#[derive(Debug, Clone, Default)]
18pub struct NotificationOverrides {
19    /// Override notify_on_complete from CLI.
20    pub notify_on_complete: Option<bool>,
21    /// Override notify_on_fail from CLI.
22    pub notify_on_fail: Option<bool>,
23    /// Override sound_enabled from CLI.
24    pub notify_sound: Option<bool>,
25}
26
27/// Configuration for desktop notifications.
28#[derive(Debug, Clone, Default)]
29pub struct NotificationConfig {
30    /// Enable desktop notifications on task completion (legacy field).
31    pub enabled: bool,
32    /// Enable desktop notifications on task completion.
33    pub notify_on_complete: bool,
34    /// Enable desktop notifications on task failure.
35    pub notify_on_fail: bool,
36    /// Enable desktop notifications when loop mode completes.
37    pub notify_on_loop_complete: bool,
38    /// Suppress notifications when a foreground UI client is active.
39    pub suppress_when_active: bool,
40    /// Enable sound alerts with notifications.
41    pub sound_enabled: bool,
42    /// Custom sound file path (platform-specific format).
43    /// If not set, uses platform default sounds.
44    pub sound_path: Option<String>,
45    /// Notification timeout in milliseconds (default: 8000).
46    pub timeout_ms: u32,
47}
48
49impl NotificationConfig {
50    /// Create a new config with sensible defaults.
51    pub fn new() -> Self {
52        Self {
53            enabled: true,
54            notify_on_complete: true,
55            notify_on_fail: true,
56            notify_on_loop_complete: true,
57            suppress_when_active: true,
58            sound_enabled: false,
59            sound_path: None,
60            timeout_ms: 8000,
61        }
62    }
63
64    /// Check if notifications should be suppressed based on UI state.
65    pub fn should_suppress(&self, ui_active: bool) -> bool {
66        if !self.enabled {
67            return true;
68        }
69        ui_active && self.suppress_when_active
70    }
71}
72
73/// Build a runtime NotificationConfig from config and CLI overrides.
74///
75/// Precedence: CLI overrides > config values > defaults.
76pub fn build_notification_config(
77    config: &crate::contracts::NotificationConfig,
78    overrides: &NotificationOverrides,
79) -> NotificationConfig {
80    let notify_on_complete = overrides
81        .notify_on_complete
82        .or(config.notify_on_complete)
83        .unwrap_or(true);
84    let notify_on_fail = overrides
85        .notify_on_fail
86        .or(config.notify_on_fail)
87        .unwrap_or(true);
88    let notify_on_loop_complete = config.notify_on_loop_complete.unwrap_or(true);
89
90    NotificationConfig {
91        enabled: notify_on_complete || notify_on_fail || notify_on_loop_complete,
92        notify_on_complete,
93        notify_on_fail,
94        notify_on_loop_complete,
95        suppress_when_active: config.suppress_when_active.unwrap_or(true),
96        sound_enabled: overrides
97            .notify_sound
98            .or(config.sound_enabled)
99            .unwrap_or(false),
100        sound_path: config.sound_path.clone(),
101        timeout_ms: config.timeout_ms.unwrap_or(8000),
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn build_notification_config_uses_defaults() {
111        let config = crate::contracts::NotificationConfig::default();
112        let overrides = NotificationOverrides::default();
113        let result = build_notification_config(&config, &overrides);
114
115        assert!(result.enabled);
116        assert!(result.notify_on_complete);
117        assert!(result.notify_on_fail);
118        assert!(result.notify_on_loop_complete);
119        assert!(result.suppress_when_active);
120        assert!(!result.sound_enabled);
121        assert!(result.sound_path.is_none());
122        assert_eq!(result.timeout_ms, 8000);
123    }
124
125    #[test]
126    fn build_notification_config_overrides_take_precedence() {
127        let config = crate::contracts::NotificationConfig {
128            notify_on_complete: Some(false),
129            notify_on_fail: Some(false),
130            sound_enabled: Some(false),
131            ..Default::default()
132        };
133        let overrides = NotificationOverrides {
134            notify_on_complete: Some(true),
135            notify_on_fail: Some(true),
136            notify_sound: Some(true),
137        };
138        let result = build_notification_config(&config, &overrides);
139
140        assert!(result.notify_on_complete);
141        assert!(result.notify_on_fail);
142        assert!(result.sound_enabled);
143    }
144
145    #[test]
146    fn build_notification_config_config_used_when_no_override() {
147        let config = crate::contracts::NotificationConfig {
148            notify_on_complete: Some(false),
149            notify_on_fail: Some(true),
150            suppress_when_active: Some(false),
151            timeout_ms: Some(5000),
152            sound_path: Some("/path/to/sound.wav".to_string()),
153            ..Default::default()
154        };
155        let overrides = NotificationOverrides::default();
156        let result = build_notification_config(&config, &overrides);
157
158        assert!(!result.notify_on_complete);
159        assert!(result.notify_on_fail);
160        assert!(!result.suppress_when_active);
161        assert_eq!(result.timeout_ms, 5000);
162        assert_eq!(result.sound_path, Some("/path/to/sound.wav".to_string()));
163    }
164
165    #[test]
166    fn build_notification_config_enabled_computed_correctly() {
167        let config = crate::contracts::NotificationConfig {
168            notify_on_complete: Some(false),
169            notify_on_fail: Some(false),
170            notify_on_loop_complete: Some(false),
171            ..Default::default()
172        };
173        let overrides = NotificationOverrides::default();
174        let result = build_notification_config(&config, &overrides);
175        assert!(!result.enabled);
176
177        let config = crate::contracts::NotificationConfig {
178            notify_on_complete: Some(true),
179            notify_on_fail: Some(false),
180            notify_on_loop_complete: Some(false),
181            ..Default::default()
182        };
183        let result = build_notification_config(&config, &overrides);
184        assert!(result.enabled);
185    }
186}