123 lines
3.9 KiB
Python
123 lines
3.9 KiB
Python
"""Kadence's own D-Bus API: `org.kadence.Desktop`.
|
|
|
|
This is what makes the app scriptable and bindable. KDE's System Settings →
|
|
Shortcuts → Custom Shortcuts can call a D-Bus method directly, so a global
|
|
hotkey for "start a pomodoro" needs no X11 key-grabbing hack:
|
|
|
|
qdbus org.kadence.Desktop /Timer org.kadence.Timer.StartWork
|
|
|
|
Owning the name also gives single-instance behaviour for free: a second launch
|
|
sees the name is taken, calls `Raise()` on the first, and exits.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Callable
|
|
|
|
from PyQt6.QtCore import QObject, pyqtSlot
|
|
from PyQt6.QtDBus import QDBusConnection
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
SERVICE_NAME = "org.kadence.Desktop"
|
|
OBJECT_PATH = "/Timer"
|
|
INTERFACE = "org.kadence.Timer"
|
|
|
|
|
|
class TimerDBusService(QObject):
|
|
"""The exported object. Every slot is a verb you can bind to a key."""
|
|
|
|
def __init__(self, actions: dict[str, Callable[[], object]], parent: QObject | None = None) -> None:
|
|
super().__init__(parent)
|
|
self._actions = actions
|
|
|
|
def _run(self, name: str) -> str:
|
|
action = self._actions.get(name)
|
|
if action is None:
|
|
return f"unknown action: {name}"
|
|
try:
|
|
result = action()
|
|
return str(result) if result is not None else "ok"
|
|
except Exception as exc: # pragma: no cover - never let a caller crash us
|
|
log.exception("D-Bus action %s failed", name)
|
|
return f"error: {exc}"
|
|
|
|
@pyqtSlot(result=str)
|
|
def StartWork(self) -> str: # noqa: N802 - D-Bus methods are PascalCase
|
|
"""Start a focus interval."""
|
|
return self._run("start_work")
|
|
|
|
@pyqtSlot(result=str)
|
|
def StartBreak(self) -> str: # noqa: N802
|
|
return self._run("start_break")
|
|
|
|
@pyqtSlot(result=str)
|
|
def Toggle(self) -> str: # noqa: N802
|
|
"""Start if idle, pause if running, resume if paused — one hotkey."""
|
|
return self._run("toggle")
|
|
|
|
@pyqtSlot(result=str)
|
|
def Pause(self) -> str: # noqa: N802
|
|
return self._run("pause")
|
|
|
|
@pyqtSlot(result=str)
|
|
def Resume(self) -> str: # noqa: N802
|
|
return self._run("resume")
|
|
|
|
@pyqtSlot(result=str)
|
|
def Complete(self) -> str: # noqa: N802
|
|
return self._run("complete")
|
|
|
|
@pyqtSlot(result=str)
|
|
def Stop(self) -> str: # noqa: N802
|
|
return self._run("stop")
|
|
|
|
@pyqtSlot(result=str)
|
|
def Extend(self) -> str: # noqa: N802
|
|
return self._run("extend")
|
|
|
|
@pyqtSlot(result=str)
|
|
def Interrupt(self) -> str: # noqa: N802
|
|
"""Count a distraction without stopping the clock."""
|
|
return self._run("interrupt")
|
|
|
|
@pyqtSlot(result=str)
|
|
def Status(self) -> str: # noqa: N802
|
|
"""One line: what is running and how much is left."""
|
|
return self._run("status")
|
|
|
|
@pyqtSlot(result=str)
|
|
def SendReport(self) -> str: # noqa: N802
|
|
return self._run("send_report")
|
|
|
|
@pyqtSlot(result=str)
|
|
def Raise(self) -> str: # noqa: N802
|
|
"""Show the window (used by the second instance)."""
|
|
return self._run("raise_window")
|
|
|
|
|
|
def register(service: TimerDBusService) -> bool:
|
|
"""Own the name and export the object. False means another instance has it."""
|
|
bus = QDBusConnection.sessionBus()
|
|
if not bus.registerService(SERVICE_NAME):
|
|
return False
|
|
bus.registerObject(
|
|
OBJECT_PATH,
|
|
service,
|
|
QDBusConnection.RegisterOption.ExportAllSlots | QDBusConnection.RegisterOption.ExportAllInvokables,
|
|
)
|
|
log.info("D-Bus service %s%s is up", SERVICE_NAME, OBJECT_PATH)
|
|
return True
|
|
|
|
|
|
def call_existing(method: str = "Raise") -> bool:
|
|
"""Poke the instance that already owns the name."""
|
|
from PyQt6.QtDBus import QDBusInterface
|
|
|
|
interface = QDBusInterface(SERVICE_NAME, OBJECT_PATH, INTERFACE, QDBusConnection.sessionBus())
|
|
if not interface.isValid():
|
|
return False
|
|
interface.call(method)
|
|
return True
|