Files
kadence/desktop/kadence_desktop/integrations/inhibit.py
T
2026-10-18 18:02:05 +03:00

107 lines
3.8 KiB
Python

"""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)