@@ -236,6 +236,125 @@ fn reload_garbar(child: &std::process::Child) { |
| 236 | 236 | libc::kill(child.id() as i32, libc::SIGHUP); |
| 237 | 237 | } |
| 238 | 238 | } |
| 239 | + |
| 240 | +// ============================================================================ |
| 241 | +// garnotify integration - auto-spawn notification daemon |
| 242 | +// ============================================================================ |
| 243 | + |
| 244 | +/// Get garnotify socket path |
| 245 | +fn garnotify_socket_path() -> String { |
| 246 | + std::env::var("XDG_RUNTIME_DIR") |
| 247 | + .map(|dir| format!("{}/garnotify.sock", dir)) |
| 248 | + .unwrap_or_else(|_| "/tmp/garnotify.sock".to_string()) |
| 249 | +} |
| 250 | + |
| 251 | +/// Check if garnotify is healthy (socket exists) |
| 252 | +fn is_garnotify_healthy() -> bool { |
| 253 | + std::path::Path::new(&garnotify_socket_path()).exists() |
| 254 | +} |
| 255 | + |
| 256 | +/// Spawn garnotify notification daemon |
| 257 | +fn spawn_garnotify() -> Option<std::process::Child> { |
| 258 | + tracing::info!("Spawning garnotify..."); |
| 259 | + |
| 260 | + // Try to find garnotify in PATH or common locations |
| 261 | + let garnotify_cmd = which_garnotify().unwrap_or_else(|| "garnotify".to_string()); |
| 262 | + |
| 263 | + // Inherit DISPLAY from current environment |
| 264 | + let x_display = std::env::var("DISPLAY").unwrap_or_else(|_| ":0".to_string()); |
| 265 | + |
| 266 | + match Command::new(&garnotify_cmd) |
| 267 | + .arg("daemon") |
| 268 | + .env("DISPLAY", &x_display) |
| 269 | + .spawn() |
| 270 | + { |
| 271 | + Ok(child) => { |
| 272 | + tracing::info!("garnotify started (PID {}), DISPLAY={}", child.id(), x_display); |
| 273 | + |
| 274 | + // Wait briefly and verify garnotify becomes healthy |
| 275 | + for attempt in 1..=10 { |
| 276 | + std::thread::sleep(std::time::Duration::from_millis(200)); |
| 277 | + if is_garnotify_healthy() { |
| 278 | + tracing::info!("garnotify socket ready after {}ms", attempt * 200); |
| 279 | + return Some(child); |
| 280 | + } |
| 281 | + } |
| 282 | + |
| 283 | + // Socket never appeared - might still be starting up |
| 284 | + tracing::warn!("garnotify socket not ready after 2s, may still be starting"); |
| 285 | + Some(child) |
| 286 | + } |
| 287 | + Err(e) => { |
| 288 | + tracing::error!("Failed to spawn garnotify: {}", e); |
| 289 | + tracing::info!("Hint: Ensure garnotify is installed and in PATH"); |
| 290 | + None |
| 291 | + } |
| 292 | + } |
| 293 | +} |
| 294 | + |
| 295 | +/// Find garnotify executable |
| 296 | +fn which_garnotify() -> Option<String> { |
| 297 | + // Check if garnotify is in PATH |
| 298 | + if Command::new("which") |
| 299 | + .arg("garnotify") |
| 300 | + .output() |
| 301 | + .map(|o| o.status.success()) |
| 302 | + .unwrap_or(false) |
| 303 | + { |
| 304 | + return Some("garnotify".to_string()); |
| 305 | + } |
| 306 | + |
| 307 | + // Check common cargo install location |
| 308 | + if let Ok(home) = std::env::var("HOME") { |
| 309 | + let cargo_bin = format!("{}/.cargo/bin/garnotify", home); |
| 310 | + if std::path::Path::new(&cargo_bin).exists() { |
| 311 | + return Some(cargo_bin); |
| 312 | + } |
| 313 | + } |
| 314 | + |
| 315 | + // Check /usr/local/bin |
| 316 | + if std::path::Path::new("/usr/local/bin/garnotify").exists() { |
| 317 | + return Some("/usr/local/bin/garnotify".to_string()); |
| 318 | + } |
| 319 | + |
| 320 | + None |
| 321 | +} |
| 322 | + |
| 323 | +/// Stop garnotify gracefully by sending SIGTERM. |
| 324 | +fn stop_garnotify(child: &mut std::process::Child) { |
| 325 | + tracing::info!("Stopping garnotify (PID {})...", child.id()); |
| 326 | + |
| 327 | + // Send SIGTERM for graceful shutdown |
| 328 | + unsafe { |
| 329 | + libc::kill(child.id() as i32, libc::SIGTERM); |
| 330 | + } |
| 331 | + |
| 332 | + // Wait briefly for it to exit |
| 333 | + match child.try_wait() { |
| 334 | + Ok(Some(status)) => { |
| 335 | + tracing::info!("garnotify exited with status: {}", status); |
| 336 | + } |
| 337 | + Ok(None) => { |
| 338 | + std::thread::sleep(std::time::Duration::from_millis(100)); |
| 339 | + match child.try_wait() { |
| 340 | + Ok(Some(status)) => { |
| 341 | + tracing::info!("garnotify exited with status: {}", status); |
| 342 | + } |
| 343 | + Ok(None) => { |
| 344 | + tracing::warn!("garnotify did not exit gracefully, sending SIGKILL"); |
| 345 | + let _ = child.kill(); |
| 346 | + } |
| 347 | + Err(e) => { |
| 348 | + tracing::warn!("Error waiting for garnotify: {}", e); |
| 349 | + } |
| 350 | + } |
| 351 | + } |
| 352 | + Err(e) => { |
| 353 | + tracing::warn!("Error checking garnotify status: {}", e); |
| 354 | + } |
| 355 | + } |
| 356 | +} |
| 357 | + |
| 239 | 358 | use x11rb::protocol::xproto::{ |
| 240 | 359 | ButtonPressEvent, ButtonReleaseEvent, ClientMessageEvent, ConfigureRequestEvent, |
| 241 | 360 | ConfigureWindowAux, ConnectionExt, DestroyNotifyEvent, EnterNotifyEvent, EventMask, |
@@ -2503,6 +2622,11 @@ impl WindowManager { |
| 2503 | 2622 | self.garbar_process = spawn_garbar(); |
| 2504 | 2623 | } |
| 2505 | 2624 | |
| 2625 | + // Spawn garnotify if gar.notification is configured |
| 2626 | + if self.config.notification_enabled { |
| 2627 | + self.garnotify_process = spawn_garnotify(); |
| 2628 | + } |
| 2629 | + |
| 2506 | 2630 | while self.running { |
| 2507 | 2631 | // Handle X11 events (non-blocking poll) |
| 2508 | 2632 | while let Some(event) = self.conn.conn.poll_for_event()? { |
@@ -2539,6 +2663,12 @@ impl WindowManager { |
| 2539 | 2663 | } |
| 2540 | 2664 | self.garbar_process = None; |
| 2541 | 2665 | |
| 2666 | + // Stop garnotify if it was spawned |
| 2667 | + if let Some(ref mut child) = self.garnotify_process { |
| 2668 | + stop_garnotify(child); |
| 2669 | + } |
| 2670 | + self.garnotify_process = None; |
| 2671 | + |
| 2542 | 2672 | // Kill picom to prevent compositor effects from bleeding into the greeter |
| 2543 | 2673 | tracing::info!("Killing picom..."); |
| 2544 | 2674 | let _ = std::process::Command::new("pkill") |