"""Knowing when you walked away. Two signals, both from the session itself rather than from guesswork: * **logind** emits `Lock`/`Unlock` on your session object, and `PrepareForSleep` when the machine suspends. Locking the screen is the clearest possible "I am not at this desk" — the timer pauses, and resumes when you come back. * **Idle time** comes from KDE's `org.kde.screensaver.GetSessionIdleTime` or GNOME's `org.gnome.Mutter.IdleMonitor`, so an interval you forgot to stop does not quietly bank half an hour of "focus" you spent in the kitchen. """ from __future__ import annotations import logging import os from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot from PyQt6.QtDBus import QDBusConnection, QDBusInterface, QDBusMessage log = logging.getLogger(__name__) _LOGIND = "org.freedesktop.login1" _MANAGER_PATH = "/org/freedesktop/login1" _MANAGER_IFACE = "org.freedesktop.login1.Manager" _SESSION_IFACE = "org.freedesktop.login1.Session" class SessionWatcher(QObject): """Emits lock/unlock/sleep, and answers "how long have they been idle?".""" locked = pyqtSignal() unlocked = pyqtSignal() sleeping = pyqtSignal() woke = pyqtSignal() def __init__(self, parent: QObject | None = None) -> None: super().__init__(parent) self._system_bus = QDBusConnection.systemBus() self._session_bus = QDBusConnection.sessionBus() self._session_path = self._find_session_path() if self._session_path: for signal_name, slot in (("Lock", self._on_lock), ("Unlock", self._on_unlock)): self._system_bus.connect(_LOGIND, self._session_path, _SESSION_IFACE, signal_name, slot) log.debug("watching logind session %s", self._session_path) self._system_bus.connect(_LOGIND, _MANAGER_PATH, _MANAGER_IFACE, "PrepareForSleep", self._on_sleep) # Plasma's own lock signal, for setups where logind's does not arrive. self._session_bus.connect( "org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver", "ActiveChanged", self._on_screensaver, ) def _find_session_path(self) -> str: """Ask logind for *this* session's object path.""" manager = QDBusInterface(_LOGIND, _MANAGER_PATH, _MANAGER_IFACE, self._system_bus) if not manager.isValid(): return "" session_id = os.environ.get("XDG_SESSION_ID", "") if session_id: reply = manager.call("GetSession", session_id) arguments = reply.arguments() if arguments and not reply.errorName(): return str(arguments[0].path() if hasattr(arguments[0], "path") else arguments[0]) reply = manager.call("GetSessionByPID", os.getpid()) arguments = reply.arguments() if arguments and not reply.errorName(): value = arguments[0] return str(value.path() if hasattr(value, "path") else value) return "" def idle_seconds(self) -> int: """Seconds since the last input, or -1 when nothing can tell us.""" kde = QDBusInterface( "org.freedesktop.ScreenSaver", "/ScreenSaver", "org.kde.screensaver", self._session_bus ) if kde.isValid(): reply = kde.call("GetSessionIdleTime") arguments = reply.arguments() if arguments and not reply.errorName(): return int(arguments[0]) // 1000 mutter = QDBusInterface( "org.gnome.Mutter.IdleMonitor", "/org/gnome/Mutter/IdleMonitor/Core", "org.gnome.Mutter.IdleMonitor", self._session_bus, ) if mutter.isValid(): reply = mutter.call("GetIdletime") arguments = reply.arguments() if arguments and not reply.errorName(): return int(arguments[0]) // 1000 return -1 @pyqtSlot(QDBusMessage) def _on_lock(self, _message: QDBusMessage) -> None: self.locked.emit() @pyqtSlot(QDBusMessage) def _on_unlock(self, _message: QDBusMessage) -> None: self.unlocked.emit() @pyqtSlot(QDBusMessage) def _on_sleep(self, message: QDBusMessage) -> None: arguments = message.arguments() going_to_sleep = bool(arguments[0]) if arguments else False (self.sleeping if going_to_sleep else self.woke).emit() @pyqtSlot(QDBusMessage) def _on_screensaver(self, message: QDBusMessage) -> None: arguments = message.arguments() if not arguments: return (self.locked if bool(arguments[0]) else self.unlocked).emit()