"""Two things a focus app owes you: the screen stays on, and the world shuts up. * **Screensaver inhibit** — `org.freedesktop.ScreenSaver.Inhibit`, honoured by KDE, GNOME and anything XDG-ish. Without it a 50-minute interval spent reading ends with a lock screen. * **Do Not Disturb** — Plasma (and any daemon implementing the spec's optional `Inhibit`) silences other apps' banners while you focus. GNOME has no such method, so there we flip the `show-banners` gsetting and put it back after. Both are strictly paired: every inhibit is released on exit, including a crash, because the cookie dies with the D-Bus connection. """ from __future__ import annotations import logging import shutil import subprocess from PyQt6.QtDBus import QDBusConnection, QDBusInterface log = logging.getLogger(__name__) _SCREENSAVER = ("org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver") _NOTIFICATIONS = ("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications") class ScreensaverInhibitor: """Keeps the session awake for as long as `hold()` is in effect.""" def __init__(self, app_name: str = "Kadence") -> None: self._app_name = app_name self._interface = QDBusInterface(*_SCREENSAVER, QDBusConnection.sessionBus()) self._cookie: int | None = None @property def active(self) -> bool: return self._cookie is not None def hold(self, reason: str = "Focus interval in progress") -> None: if self._cookie is not None or not self._interface.isValid(): return reply = self._interface.call("Inhibit", self._app_name, reason) arguments = reply.arguments() if arguments: self._cookie = int(arguments[0]) log.debug("screensaver inhibited (cookie %s)", self._cookie) def release(self) -> None: if self._cookie is None: return self._interface.call("UnInhibit", self._cookie) log.debug("screensaver released") self._cookie = None class DoNotDisturb: """Silences other applications' notifications during focus.""" def __init__(self, app_name: str = "Kadence") -> None: self._app_name = app_name self._interface = QDBusInterface(*_NOTIFICATIONS, QDBusConnection.sessionBus()) self._cookie: int | None = None self._gnome_restored = True @property def active(self) -> bool: return self._cookie is not None or not self._gnome_restored def hold(self, reason: str = "Focus interval in progress") -> None: if self.active: return if self._interface.isValid(): reply = self._interface.call("Inhibit", self._app_name, reason, {}) arguments = reply.arguments() if arguments and not reply.errorName(): self._cookie = int(arguments[0]) return self._gnome_dnd(True) def release(self) -> None: if self._cookie is not None: self._interface.call("UnInhibit", self._cookie) self._cookie = None if not self._gnome_restored: self._gnome_dnd(False) def _gnome_dnd(self, enable: bool) -> None: """GNOME fallback: toggle the banner gsetting, and remember to undo it.""" if not shutil.which("gsettings"): return try: subprocess.run( [ "gsettings", "set", "org.gnome.desktop.notifications", "show-banners", "false" if enable else "true", ], check=False, timeout=5, ) self._gnome_restored = not enable except (OSError, subprocess.SubprocessError) as exc: # pragma: no cover log.debug("gsettings DND failed: %s", exc)