reovim_plugin_notification/
state.rs

1//! Notification state management
2//!
3//! Provides thread-safe storage for notifications and progress.
4
5use std::{
6    collections::{HashMap, VecDeque},
7    sync::{Arc, RwLock},
8};
9
10use crate::{
11    notification::{Notification, NotificationLevel},
12    progress::{ProgressBarConfig, ProgressNotification},
13    style::NotificationStyles,
14};
15
16/// Position for notification display
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum NotificationPosition {
19    /// Top-right corner
20    TopRight,
21    /// Top-left corner
22    TopLeft,
23    /// Bottom-right corner (default - less intrusive for coding)
24    #[default]
25    BottomRight,
26    /// Bottom-left corner
27    BottomLeft,
28    /// Top-center
29    TopCenter,
30    /// Bottom-center
31    BottomCenter,
32}
33
34/// Configuration for notifications
35#[derive(Debug, Clone)]
36pub struct NotificationConfig {
37    /// Default display duration in milliseconds
38    pub default_duration_ms: u64,
39    /// Maximum number of notifications to display
40    pub max_notifications: usize,
41    /// Position on screen
42    pub position: NotificationPosition,
43    /// Progress bar configuration
44    pub progress_bar: ProgressBarConfig,
45}
46
47impl Default for NotificationConfig {
48    fn default() -> Self {
49        Self {
50            default_duration_ms: 3000,
51            max_notifications: 5,
52            position: NotificationPosition::BottomRight,
53            progress_bar: ProgressBarConfig::default(),
54        }
55    }
56}
57
58/// Inner state for notification manager
59#[derive(Default)]
60struct NotificationManagerInner {
61    notifications: VecDeque<Notification>,
62    progress: HashMap<String, ProgressNotification>,
63    config: NotificationConfig,
64    styles: NotificationStyles,
65}
66
67/// Thread-safe notification manager
68pub struct SharedNotificationManager {
69    inner: RwLock<NotificationManagerInner>,
70}
71
72impl Default for SharedNotificationManager {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78impl SharedNotificationManager {
79    /// Create a new notification manager
80    #[must_use]
81    pub fn new() -> Self {
82        Self {
83            inner: RwLock::new(NotificationManagerInner::default()),
84        }
85    }
86
87    /// Show a notification
88    ///
89    /// # Panics
90    ///
91    /// Panics if the internal lock is poisoned.
92    #[allow(clippy::significant_drop_tightening)]
93    pub fn show(
94        &self,
95        level: NotificationLevel,
96        message: String,
97        duration_ms: Option<u64>,
98        source: Option<String>,
99    ) {
100        let mut inner = self.inner.write().unwrap();
101        let duration = duration_ms.unwrap_or(inner.config.default_duration_ms);
102        let max = inner.config.max_notifications;
103
104        let mut notification = Notification::new(level, message).with_duration(duration);
105        if let Some(src) = source {
106            notification = notification.with_source(src);
107        }
108
109        inner.notifications.push_front(notification);
110
111        // Trim to max
112        while inner.notifications.len() > max {
113            inner.notifications.pop_back();
114        }
115    }
116
117    /// Dismiss a notification by ID
118    ///
119    /// # Panics
120    ///
121    /// Panics if the internal lock is poisoned.
122    pub fn dismiss(&self, id: &str) {
123        let mut inner = self.inner.write().unwrap();
124        inner.notifications.retain(|n| n.id != id);
125    }
126
127    /// Dismiss all notifications
128    ///
129    /// # Panics
130    ///
131    /// Panics if the internal lock is poisoned.
132    pub fn dismiss_all(&self) {
133        let mut inner = self.inner.write().unwrap();
134        inner.notifications.clear();
135    }
136
137    /// Update a progress notification
138    ///
139    /// # Panics
140    ///
141    /// Panics if the internal lock is poisoned.
142    pub fn update_progress(
143        &self,
144        id: String,
145        title: String,
146        source: String,
147        progress: Option<u8>,
148        detail: Option<String>,
149    ) {
150        let mut inner = self.inner.write().unwrap();
151
152        let mut notification = ProgressNotification::new(&id, title, source);
153        if let Some(pct) = progress {
154            notification = notification.with_progress(pct);
155        }
156        if let Some(det) = detail {
157            notification = notification.with_detail(det);
158        }
159
160        inner.progress.insert(id, notification);
161    }
162
163    /// Complete a progress notification
164    ///
165    /// # Panics
166    ///
167    /// Panics if the internal lock is poisoned.
168    #[allow(clippy::significant_drop_tightening)]
169    pub fn complete_progress(&self, id: &str, message: Option<String>) {
170        let mut inner = self.inner.write().unwrap();
171        let max = inner.config.max_notifications;
172
173        inner.progress.remove(id);
174
175        // Optionally show a completion toast
176        if let Some(msg) = message {
177            let notification =
178                Notification::new(NotificationLevel::Success, msg).with_duration(2000);
179            inner.notifications.push_front(notification);
180
181            // Trim to max
182            while inner.notifications.len() > max {
183                inner.notifications.pop_back();
184            }
185        }
186    }
187
188    /// Remove expired notifications
189    ///
190    /// # Panics
191    ///
192    /// Panics if the internal lock is poisoned.
193    pub fn cleanup_expired(&self) {
194        let mut inner = self.inner.write().unwrap();
195        inner.notifications.retain(|n| !n.is_expired());
196    }
197
198    /// Get the current configuration
199    ///
200    /// # Panics
201    ///
202    /// Panics if the internal lock is poisoned.
203    #[must_use]
204    pub fn config(&self) -> NotificationConfig {
205        self.inner.read().unwrap().config.clone()
206    }
207
208    /// Get the current styles
209    ///
210    /// # Panics
211    ///
212    /// Panics if the internal lock is poisoned.
213    #[must_use]
214    pub fn styles(&self) -> NotificationStyles {
215        self.inner.read().unwrap().styles.clone()
216    }
217
218    /// Get the current notifications
219    ///
220    /// # Panics
221    ///
222    /// Panics if the internal lock is poisoned.
223    #[must_use]
224    pub fn notifications(&self) -> Vec<Notification> {
225        self.inner
226            .read()
227            .unwrap()
228            .notifications
229            .iter()
230            .cloned()
231            .collect()
232    }
233
234    /// Get the current progress notifications
235    ///
236    /// # Panics
237    ///
238    /// Panics if the internal lock is poisoned.
239    #[must_use]
240    pub fn progress_items(&self) -> Vec<ProgressNotification> {
241        self.inner
242            .read()
243            .unwrap()
244            .progress
245            .values()
246            .cloned()
247            .collect()
248    }
249
250    /// Check if there are any visible notifications or progress items
251    ///
252    /// # Panics
253    ///
254    /// Panics if the internal lock is poisoned.
255    #[must_use]
256    pub fn has_visible(&self) -> bool {
257        let inner = self.inner.read().unwrap();
258        !inner.notifications.is_empty() || !inner.progress.is_empty()
259    }
260
261    /// Set the position
262    ///
263    /// # Panics
264    ///
265    /// Panics if the internal lock is poisoned.
266    pub fn set_position(&self, position: NotificationPosition) {
267        self.inner.write().unwrap().config.position = position;
268    }
269
270    /// Set the default duration
271    ///
272    /// # Panics
273    ///
274    /// Panics if the internal lock is poisoned.
275    pub fn set_default_duration(&self, duration_ms: u64) {
276        self.inner.write().unwrap().config.default_duration_ms = duration_ms;
277    }
278
279    /// Set the max notifications
280    ///
281    /// # Panics
282    ///
283    /// Panics if the internal lock is poisoned.
284    #[allow(clippy::significant_drop_tightening)]
285    pub fn set_max_notifications(&self, max: usize) {
286        let mut inner = self.inner.write().unwrap();
287        inner.config.max_notifications = max;
288        // Trim if needed
289        while inner.notifications.len() > max {
290            inner.notifications.pop_back();
291        }
292    }
293}
294
295/// Wrapper type for `Arc<SharedNotificationManager>`
296pub type NotificationManagerHandle = Arc<SharedNotificationManager>;