From a6b024ffbc0052813d5cfd05fa2cd207d5b20c9b Mon Sep 17 00:00:00 2001 From: Asko Nõmm Date: Wed, 29 Apr 2026 20:45:04 +0300 Subject: Initial commit: Rust notification badge daemon for KDE Plasma 6 Monitors D-Bus for desktop notifications and emits Unity LauncherEntry badge updates so KDE Plasma task manager shows notification counts. - Streaming dbus-monitor parser with app matching and lifecycle tracking - KWin window discovery for automatic desktop file detection - Systemd user service, Makefile install/uninstall targets - 38 tests (32 unit + 6 integration), 97% line coverage via cargo-llvm-cov - CodeScene code health: 10/10 on all source files --- src/badge.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/badge.rs (limited to 'src/badge.rs') diff --git a/src/badge.rs b/src/badge.rs new file mode 100644 index 0000000..4868570 --- /dev/null +++ b/src/badge.rs @@ -0,0 +1,79 @@ +use log::warn; + +/// Emits com.canonical.Unity.LauncherEntry.Update signals via gdbus. +pub struct BadgeEmitter; + +impl BadgeEmitter { + /// Emit a badge update for the given desktop_id with the specified count. + pub fn emit(desktop_id: &str, count: usize) { + Self::emit_with(desktop_id, count, crate::dbus_glue::emit_badge); + } + + /// Testable version that accepts a function for the actual D-Bus emission. + pub fn emit_with(desktop_id: &str, count: usize, emit_fn: F) + where + F: FnOnce(&str, &str) -> Result<(), String>, + { + let visible = if count > 0 { "true" } else { "false" }; + let urgent = visible; + let properties = format!( + "{{'count': , 'count-visible': <{visible}>, 'urgent': <{urgent}>}}" + ); + + if let Err(e) = emit_fn(desktop_id, &properties) { + warn!("badge error: {e}"); + } + } + + /// Clear badges for a list of desktop_ids (used on shutdown). + pub fn clear_all(desktop_ids: &[String]) { + for id in desktop_ids { + Self::emit(id, 0); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::cell::RefCell; + + #[test] + fn test_emit_with_count() { + let captured = RefCell::new(None); + + BadgeEmitter::emit_with("application://firefox.desktop", 3, |id, props| { + *captured.borrow_mut() = Some((id.to_string(), props.to_string())); + Ok(()) + }); + + let (id, props) = captured.borrow().clone().unwrap(); + assert_eq!(id, "application://firefox.desktop"); + assert!(props.contains("count': ")); + assert!(props.contains("count-visible': ")); + assert!(props.contains("urgent': ")); + } + + #[test] + fn test_emit_with_zero_count() { + let captured = RefCell::new(None); + + BadgeEmitter::emit_with("application://firefox.desktop", 0, |id, props| { + *captured.borrow_mut() = Some((id.to_string(), props.to_string())); + Ok(()) + }); + + let (_, props) = captured.borrow().clone().unwrap(); + assert!(props.contains("count': ")); + assert!(props.contains("count-visible': ")); + assert!(props.contains("urgent': ")); + } + + #[test] + fn test_emit_error_is_logged_not_panicked() { + // Should not panic on error + BadgeEmitter::emit_with("application://firefox.desktop", 1, |_, _| { + Err("dbus error".into()) + }); + } +} -- cgit v1.2.3