use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::{AppReceivers, DndState}; use futures::StreamExt; use tokio::sync::watch; use tracing::{debug, error, info}; use zbus::{Connection, fdo::PropertiesProxy, proxy}; pub struct DndModule; impl WaybarModule for DndModule { async fn run( &self, config: &Config, state: &AppReceivers, args: &[&str], ) -> Result { let action = args.first().unwrap_or(&"show"); if *action == "toggle" { let connection = Connection::session() .await .map_err(|e| crate::error::FluxoError::Module { module: "dnd", message: format!("DBus connection failed: {}", e), })?; // Try toggling SwayNC if let Ok(proxy) = SwayncControlProxy::new(&connection).await { if let Ok(is_dnd) = proxy.dnd().await { let _ = proxy.set_dnd(!is_dnd).await; return Ok(WaybarOutput::default()); } } // Try toggling Dunst if let Ok(proxy) = DunstControlProxy::new(&connection).await { if let Ok(is_paused) = proxy.paused().await { let _ = proxy.set_paused(!is_paused).await; return Ok(WaybarOutput::default()); } } return Err(crate::error::FluxoError::Module { module: "dnd", message: "No supported notification daemon found to toggle".to_string(), }); } let is_dnd = state.dnd.borrow().is_dnd; if is_dnd { Ok(WaybarOutput { text: config.dnd.format_dnd.clone(), tooltip: Some("Do Not Disturb: On".to_string()), class: Some("dnd".to_string()), percentage: None, }) } else { Ok(WaybarOutput { text: config.dnd.format_normal.clone(), tooltip: Some("Do Not Disturb: Off".to_string()), class: Some("normal".to_string()), percentage: None, }) } } } pub struct DndDaemon; #[proxy( interface = "org.erikreider.swaync.control", default_service = "org.erikreider.swaync.control", default_path = "/org/erikreider/swaync/control" )] trait SwayncControl { #[zbus(property)] fn dnd(&self) -> zbus::Result; #[zbus(property)] fn set_dnd(&self, value: bool) -> zbus::Result<()>; } #[proxy( interface = "org.dunstproject.cmd0", default_service = "org.freedesktop.Notifications", default_path = "/org/freedesktop/Notifications" )] trait DunstControl { #[zbus(property)] fn paused(&self) -> zbus::Result; #[zbus(property)] fn set_paused(&self, value: bool) -> zbus::Result<()>; } impl DndDaemon { pub fn new() -> Self { Self } pub fn start(&self, tx: watch::Sender) { tokio::spawn(async move { loop { if let Err(e) = Self::listen_loop(&tx).await { error!("DND listener error: {}", e); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; } } }); } async fn listen_loop(tx: &watch::Sender) -> anyhow::Result<()> { let connection = Connection::session().await?; info!("Connected to D-Bus for DND monitoring"); // Try SwayNC if let Ok(proxy) = SwayncControlProxy::new(&connection).await { if let Ok(is_dnd) = proxy.dnd().await { debug!("Found SwayNC, using it for DND state."); let _ = tx.send(DndState { is_dnd }); if let Ok(props_proxy) = PropertiesProxy::builder(&connection) .destination("org.erikreider.swaync.control")? .path("/org/erikreider/swaync/control")? .build() .await { let mut stream = props_proxy.receive_properties_changed().await?; while let Some(signal) = stream.next().await { let args = signal.args()?; if args.interface_name == "org.erikreider.swaync.control" && let Some(val) = args.changed_properties.get("dnd") && let Ok(is_dnd) = bool::try_from(val) { let _ = tx.send(DndState { is_dnd }); } } } } } // Try Dunst if let Ok(proxy) = DunstControlProxy::new(&connection).await { if let Ok(is_dnd) = proxy.paused().await { debug!("Found Dunst, using it for DND state."); let _ = tx.send(DndState { is_dnd }); if let Ok(props_proxy) = PropertiesProxy::builder(&connection) .destination("org.freedesktop.Notifications")? .path("/org/freedesktop/Notifications")? .build() .await { let mut stream = props_proxy.receive_properties_changed().await?; while let Some(signal) = stream.next().await { let args = signal.args()?; if args.interface_name == "org.dunstproject.cmd0" && let Some(val) = args.changed_properties.get("paused") && let Ok(is_dnd) = bool::try_from(val) { let _ = tx.send(DndState { is_dnd }); } } } } } Err(anyhow::anyhow!("DND stream ended or daemon not found")) } }