@@ -5,7 +5,7 @@ |
| 5 | 5 | |
| 6 | 6 | use anyhow::{Context, Result}; |
| 7 | 7 | use serde::{Deserialize, Serialize}; |
| 8 | | -use std::io::{BufRead, BufReader, Write}; |
| 8 | +use std::io::{BufRead, BufReader, Read, Write}; |
| 9 | 9 | use std::os::unix::net::{UnixListener, UnixStream}; |
| 10 | 10 | use std::path::PathBuf; |
| 11 | 11 | use std::sync::mpsc::{self, Receiver, Sender}; |
@@ -55,10 +55,45 @@ pub enum Command { |
| 55 | 55 | Reload, |
| 56 | 56 | /// Get daemon status |
| 57 | 57 | Status, |
| 58 | + /// Subscribe to notification events (keeps connection open) |
| 59 | + Subscribe, |
| 58 | 60 | /// Quit the daemon |
| 59 | 61 | Quit, |
| 60 | 62 | } |
| 61 | 63 | |
| 64 | +/// Notification events sent to subscribers |
| 65 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 66 | +#[serde(tag = "event", rename_all = "snake_case")] |
| 67 | +pub enum Event { |
| 68 | + /// New notification created |
| 69 | + NotificationNew { |
| 70 | + id: u32, |
| 71 | + app_name: String, |
| 72 | + summary: String, |
| 73 | + body: String, |
| 74 | + urgency: String, |
| 75 | + }, |
| 76 | + /// Notification closed |
| 77 | + NotificationClosed { |
| 78 | + id: u32, |
| 79 | + reason: String, |
| 80 | + }, |
| 81 | + /// Notification updated/replaced |
| 82 | + NotificationUpdated { |
| 83 | + id: u32, |
| 84 | + summary: String, |
| 85 | + body: String, |
| 86 | + }, |
| 87 | + /// DND state changed |
| 88 | + PausedChanged { |
| 89 | + paused: bool, |
| 90 | + }, |
| 91 | + /// Count changed (for bar widgets) |
| 92 | + CountChanged { |
| 93 | + count: usize, |
| 94 | + }, |
| 95 | +} |
| 96 | + |
| 62 | 97 | /// IPC response |
| 63 | 98 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 64 | 99 | pub struct Response { |
@@ -109,11 +144,28 @@ pub struct IpcRequest { |
| 109 | 144 | pub response_tx: std::sync::mpsc::Sender<Response>, |
| 110 | 145 | } |
| 111 | 146 | |
| 147 | +/// A subscriber connection for streaming events |
| 148 | +pub struct Subscriber { |
| 149 | + stream: UnixStream, |
| 150 | +} |
| 151 | + |
| 152 | +impl Subscriber { |
| 153 | + /// Send an event to this subscriber |
| 154 | + pub fn send(&mut self, event: &Event) -> Result<()> { |
| 155 | + let json = serde_json::to_string(event)?; |
| 156 | + writeln!(self.stream, "{}", json)?; |
| 157 | + self.stream.flush()?; |
| 158 | + Ok(()) |
| 159 | + } |
| 160 | +} |
| 161 | + |
| 112 | 162 | /// IPC server for the daemon |
| 113 | 163 | pub struct IpcServer { |
| 114 | 164 | socket_path: PathBuf, |
| 115 | 165 | listener: Option<UnixListener>, |
| 116 | 166 | tx: Sender<IpcRequest>, |
| 167 | + /// Active event subscribers |
| 168 | + subscribers: std::sync::Arc<std::sync::Mutex<Vec<Subscriber>>>, |
| 117 | 169 | } |
| 118 | 170 | |
| 119 | 171 | impl IpcServer { |
@@ -124,10 +176,18 @@ impl IpcServer { |
| 124 | 176 | socket_path: socket_path(), |
| 125 | 177 | listener: None, |
| 126 | 178 | tx, |
| 179 | + subscribers: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), |
| 127 | 180 | }; |
| 128 | 181 | (server, rx) |
| 129 | 182 | } |
| 130 | 183 | |
| 184 | + /// Get a handle for broadcasting events to subscribers |
| 185 | + pub fn broadcaster(&self) -> EventBroadcaster { |
| 186 | + EventBroadcaster { |
| 187 | + subscribers: self.subscribers.clone(), |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 131 | 191 | /// Start listening for connections |
| 132 | 192 | pub fn start(&mut self) -> Result<()> { |
| 133 | 193 | // Remove stale socket |
@@ -141,6 +201,7 @@ impl IpcServer { |
| 141 | 201 | info!("IPC server listening on {}", self.socket_path.display()); |
| 142 | 202 | |
| 143 | 203 | let tx = self.tx.clone(); |
| 204 | + let subscribers = self.subscribers.clone(); |
| 144 | 205 | self.listener = Some(listener.try_clone()?); |
| 145 | 206 | |
| 146 | 207 | // Spawn listener thread |
@@ -149,8 +210,9 @@ impl IpcServer { |
| 149 | 210 | match stream { |
| 150 | 211 | Ok(stream) => { |
| 151 | 212 | let tx = tx.clone(); |
| 213 | + let subscribers = subscribers.clone(); |
| 152 | 214 | thread::spawn(move || { |
| 153 | | - if let Err(e) = handle_client(stream, tx) { |
| 215 | + if let Err(e) = handle_client(stream, tx, subscribers) { |
| 154 | 216 | error!("Client error: {}", e); |
| 155 | 217 | } |
| 156 | 218 | }); |
@@ -180,8 +242,47 @@ impl Drop for IpcServer { |
| 180 | 242 | } |
| 181 | 243 | } |
| 182 | 244 | |
| 245 | +/// Handle for broadcasting events to all subscribers |
| 246 | +#[derive(Clone)] |
| 247 | +pub struct EventBroadcaster { |
| 248 | + subscribers: std::sync::Arc<std::sync::Mutex<Vec<Subscriber>>>, |
| 249 | +} |
| 250 | + |
| 251 | +impl EventBroadcaster { |
| 252 | + /// Broadcast an event to all subscribers, removing dead connections |
| 253 | + pub fn broadcast(&self, event: &Event) { |
| 254 | + let mut subs = match self.subscribers.lock() { |
| 255 | + Ok(s) => s, |
| 256 | + Err(_) => return, |
| 257 | + }; |
| 258 | + |
| 259 | + // Send to all, track failures |
| 260 | + let mut failed = Vec::new(); |
| 261 | + for (i, sub) in subs.iter_mut().enumerate() { |
| 262 | + if sub.send(event).is_err() { |
| 263 | + failed.push(i); |
| 264 | + } |
| 265 | + } |
| 266 | + |
| 267 | + // Remove failed subscribers (in reverse to preserve indices) |
| 268 | + for i in failed.into_iter().rev() { |
| 269 | + subs.remove(i); |
| 270 | + debug!("Removed dead subscriber"); |
| 271 | + } |
| 272 | + } |
| 273 | + |
| 274 | + /// Get current subscriber count |
| 275 | + pub fn subscriber_count(&self) -> usize { |
| 276 | + self.subscribers.lock().map(|s| s.len()).unwrap_or(0) |
| 277 | + } |
| 278 | +} |
| 279 | + |
| 183 | 280 | /// Handle a client connection |
| 184 | | -fn handle_client(mut stream: UnixStream, tx: Sender<IpcRequest>) -> Result<()> { |
| 281 | +fn handle_client( |
| 282 | + mut stream: UnixStream, |
| 283 | + tx: Sender<IpcRequest>, |
| 284 | + subscribers: std::sync::Arc<std::sync::Mutex<Vec<Subscriber>>>, |
| 285 | +) -> Result<()> { |
| 185 | 286 | let reader = BufReader::new(stream.try_clone()?); |
| 186 | 287 | |
| 187 | 288 | for line in reader.lines() { |
@@ -189,6 +290,40 @@ fn handle_client(mut stream: UnixStream, tx: Sender<IpcRequest>) -> Result<()> { |
| 189 | 290 | debug!("Received: {}", line); |
| 190 | 291 | |
| 191 | 292 | let response = match serde_json::from_str::<Command>(&line) { |
| 293 | + Ok(Command::Subscribe) => { |
| 294 | + // Send acknowledgment then add to subscribers |
| 295 | + let response = Response::ok_with_message("Subscribed to notification events"); |
| 296 | + let response_json = serde_json::to_string(&response)?; |
| 297 | + writeln!(stream, "{}", response_json)?; |
| 298 | + stream.flush()?; |
| 299 | + |
| 300 | + // Add to subscribers list |
| 301 | + if let Ok(mut subs) = subscribers.lock() { |
| 302 | + let sub_stream = stream.try_clone()?; |
| 303 | + subs.push(Subscriber { stream: sub_stream }); |
| 304 | + info!("New subscriber connected, total: {}", subs.len()); |
| 305 | + } |
| 306 | + |
| 307 | + // Keep connection open - block on read until client disconnects |
| 308 | + let mut buf = [0u8; 1]; |
| 309 | + loop { |
| 310 | + match stream.read(&mut buf) { |
| 311 | + Ok(0) => { |
| 312 | + debug!("Subscriber disconnected"); |
| 313 | + break; |
| 314 | + } |
| 315 | + Err(_) => { |
| 316 | + debug!("Subscriber connection error"); |
| 317 | + break; |
| 318 | + } |
| 319 | + Ok(_) => { |
| 320 | + // Ignore any data from subscriber (they should only read) |
| 321 | + } |
| 322 | + } |
| 323 | + } |
| 324 | + |
| 325 | + return Ok(()); |
| 326 | + } |
| 192 | 327 | Ok(cmd) => { |
| 193 | 328 | // Create response channel |
| 194 | 329 | let (response_tx, response_rx) = mpsc::channel(); |