//! The D-Bus watcher server: the daemon's public side of the KWin handshake. //! //! KWin calls *us*, not the other way round. The compositor has no way to push a //! focus event to an arbitrary subscriber, so the daemon owns a bus name and //! exports an interface that the loaded KWin script calls into. This module owns //! that name and turns those calls into [`FocusEvent`]s on a channel. //! //! # Owning the name is not optional //! //! Without the daemon holding `vu.valiq.blocker`, KWin's `callDBus` has nothing //! to resolve and every focus event is silently discarded. That is why //! [`serve`] must succeed *before* the script is loaded. //! //! # Fail-open //! //! If the daemon is not running, the KWin script's `callDBus` fails inside a //! signal handler and the compositor is left alone — no exception, no log spam. //! Blocking stops; the desktop keeps working. That is section 1.5.4's rule and //! it is why the script swallows its errors. use std::sync::mpsc::Sender; use zbus::blocking::{connection::Builder, Connection}; use super::kwin::{BUS_NAME, OBJECT_PATH}; use super::{parse_payload, FocusEvent}; /// Why the watcher could not start. #[derive(Debug)] pub enum WatcherError { /// No session bus, so there is nothing to own a name on. Almost always /// means the daemon was started outside a desktop session — over SSH, or /// from a system unit rather than a user one. NoSessionBus(String), /// The name could not be acquired, which normally means another instance is /// already running. NameTaken(String), /// Anything else zbus reports. Other(String), } impl std::fmt::Display for WatcherError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::NoSessionBus(e) => write!(f, "no session bus: {e}"), Self::NameTaken(e) => write!(f, "another watcher already owns {BUS_NAME}: {e}"), Self::Other(e) => write!(f, "watcher could not start: {e}"), } } } impl std::error::Error for WatcherError {} /// The object the KWin script calls into. /// /// One method, `Focused`, whose payload is the tab-separated window description /// built by `kwin::SCRIPT_JS`. An empty payload means focus was cleared. struct Watcher { sink: Sender, } #[zbus::interface(name = "vu.valiq.blocker.Watcher")] impl Watcher { /// Called by the KWin script on every focus change. /// /// Payload is tab-separated: resourceClass, resourceName, caption, /// internalId, pid — in the order `kwin::SCRIPT_JS` writes them. fn focused(&self, payload: &str) { let event = match parse_payload(payload) { Some(window) => FocusEvent::Activated(window), None => FocusEvent::Cleared, }; // A send failure means the daemon has stopped. Nothing to escalate: the // thread exits when the connection drops. let _ = self.sink.send(event); } } /// Acquire the bus name and export [`Watcher`], returning the live connection. /// /// The connection must be held for the export to stay registered, so the caller /// keeps it for the daemon's lifetime. /// /// Note on ordering: the daemon calls [`serve`] *before* /// [`super::kwin::KwinBackend::connect`]. Loading the script first would start a /// handler calling a name nobody owns, and every event would be lost. pub fn serve(sink: Sender) -> Result { Builder::session() .map_err(|e| WatcherError::NoSessionBus(e.to_string()))? .name(BUS_NAME) .map_err(|e| WatcherError::NameTaken(e.to_string()))? // `#[zbus::interface]` implements `Interface` for the struct, so the // instance itself is what gets registered — it holds the channel. .serve_at(OBJECT_PATH, Watcher { sink }) .map_err(|e| WatcherError::Other(e.to_string()))? .build() .map_err(|e| { // zbus cannot distinguish "name already owned" from other failures // at build time, so the message is what separates them. A name clash // is the common case and deserves the clearer variant. let rendered = e.to_string(); if rendered.contains("taken") || rendered.contains("already") { WatcherError::NameTaken(rendered) } else { WatcherError::Other(rendered) } }) } /// Drop the bus name, so a restarted daemon does not collide with a stale one. /// /// Called before exiting. Not cosmetic: systemd `Restart=always` can start the /// replacement before the old process is fully gone, and losing that race would /// leave the new instance wedged on a name its predecessor still holds. pub fn release(connection: &Connection) { let _ = connection.release_name(BUS_NAME); }