summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorAsko Nõmm <asko@nmm.ee>2026-04-29 20:45:04 +0300
committerAsko Nõmm <asko@nmm.ee>2026-04-29 20:45:04 +0300
commita6b024ffbc0052813d5cfd05fa2cd207d5b20c9b (patch)
treef0c9c606888e0d3de77c3a39c2df129eae5c0072 /src/main.rs
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
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs169
1 files changed, 169 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..cafcaec
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,169 @@
+use log::{error, info, warn};
+use notification_badge::{AppMap, BadgeEmitter, DbusMessage, DbusParser};
+use std::io::BufRead;
+use std::process::{Command, Stdio};
+use std::sync::{Arc, Mutex};
+use std::thread;
+use std::time::Duration;
+
+/// How often (seconds) to re-scan open windows and rebuild the app map.
+const DISCOVERY_INTERVAL: u64 = 30;
+
+fn main() {
+ env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
+ .format_timestamp(None)
+ .init();
+
+ let app_map = Arc::new(Mutex::new(AppMap::new()));
+
+ install_signal_handler(&app_map);
+ run_initial_discovery(&app_map);
+ spawn_discovery_thread(&app_map);
+ run_monitor_loop(&app_map);
+}
+
+fn install_signal_handler(app_map: &Arc<Mutex<AppMap>>) {
+ let app_map_sig = Arc::clone(app_map);
+ ctrlc::handle(move || {
+ info!("shutting down, clearing badges...");
+ let map = app_map_sig.lock().unwrap();
+ BadgeEmitter::clear_all(&map.all_desktop_ids());
+ std::process::exit(0);
+ });
+}
+
+fn run_initial_discovery(app_map: &Arc<Mutex<AppMap>>) {
+ let new_map = notification_badge::discover_apps();
+ let mut map = app_map.lock().unwrap();
+ if map.update_patterns(new_map) {
+ log_discovered(&map);
+ } else {
+ warn!("no apps discovered, will retry...");
+ }
+}
+
+fn spawn_discovery_thread(app_map: &Arc<Mutex<AppMap>>) {
+ let app_map_disc = Arc::clone(app_map);
+ thread::spawn(move || loop {
+ thread::sleep(Duration::from_secs(DISCOVERY_INTERVAL));
+ let new_map = notification_badge::discover_apps();
+ let mut map = app_map_disc.lock().unwrap();
+ if map.update_patterns(new_map) {
+ log_discovered(&map);
+ }
+ });
+}
+
+fn run_monitor_loop(app_map: &Arc<Mutex<AppMap>>) {
+ let mut monitor = Command::new("dbus-monitor")
+ .args(["--session"])
+ .stdout(Stdio::piped())
+ .stderr(Stdio::null())
+ .spawn()
+ .expect("failed to start dbus-monitor");
+
+ let stdout = monitor
+ .stdout
+ .take()
+ .expect("failed to capture dbus-monitor stdout");
+
+ let reader = std::io::BufReader::new(stdout);
+ let mut parser = DbusParser::new();
+
+ for line in reader.lines() {
+ let line = match line {
+ Ok(l) => l,
+ Err(e) => {
+ error!("read error: {e}");
+ break;
+ }
+ };
+
+ if let Some(msg) = parser.feed_line(&line) {
+ handle_message(msg, app_map);
+ }
+ }
+
+ if let Some(msg) = parser.flush() {
+ handle_message(msg, app_map);
+ }
+
+ error!("dbus-monitor exited unexpectedly");
+ let map = app_map.lock().unwrap();
+ BadgeEmitter::clear_all(&map.all_desktop_ids());
+}
+
+fn handle_message(msg: DbusMessage, app_map: &Arc<Mutex<AppMap>>) {
+ let mut map = app_map.lock().unwrap();
+
+ match msg {
+ DbusMessage::Notify {
+ serial,
+ app_name,
+ replaces_id,
+ desktop_entry_hint,
+ } => {
+ let hint_ref = desktop_entry_hint.as_deref().unwrap_or("");
+ if let Some(desktop_id) = map.match_app(&[&app_name, hint_ref]) {
+ map.record_notify(serial, replaces_id, &desktop_id);
+ }
+ }
+ DbusMessage::NotifyReply {
+ reply_serial,
+ notification_id,
+ } => {
+ if let Some((desktop_id, count)) = map.resolve_reply(reply_serial, notification_id) {
+ BadgeEmitter::emit(&desktop_id, count);
+ info!("+ [{notification_id}] {desktop_id} (count: {count})");
+ }
+ }
+ DbusMessage::NotificationClosed { notification_id } => {
+ if let Some((desktop_id, count)) = map.notification_closed(notification_id) {
+ BadgeEmitter::emit(&desktop_id, count);
+ info!("- [{notification_id}] {desktop_id} (count: {count})");
+ }
+ }
+ }
+}
+
+fn log_discovered(map: &AppMap) {
+ let mut apps: Vec<_> = map.patterns().values().collect();
+ apps.sort();
+ apps.dedup();
+ info!(
+ "discovered apps: [{}]",
+ apps.iter()
+ .map(|s| s.as_str())
+ .collect::<Vec<_>>()
+ .join(", ")
+ );
+}
+
+/// Minimal signal handler using libc.
+mod ctrlc {
+ pub fn handle<F: Fn() + Send + 'static>(handler: F) {
+ unsafe {
+ libc::signal(
+ libc::SIGINT,
+ signal_handler as *const () as libc::sighandler_t,
+ );
+ libc::signal(
+ libc::SIGTERM,
+ signal_handler as *const () as libc::sighandler_t,
+ );
+ }
+
+ HANDLER.lock().unwrap().replace(Box::new(move || handler()));
+ }
+
+ use std::sync::Mutex;
+ static HANDLER: Mutex<Option<Box<dyn Fn() + Send>>> = Mutex::new(None);
+
+ extern "C" fn signal_handler(_sig: libc::c_int) {
+ if let Ok(guard) = HANDLER.lock() {
+ if let Some(ref handler) = *guard {
+ handler();
+ }
+ }
+ }
+}