107 lines
3.8 KiB
Python
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)
|