"""Desktop-local configuration. Only what is *about this machine* lives here — where the backend is, and which system integrations this box may use. Everything else (timer lengths, block lists, Telegram) is server-side, so the phone, the web UI and this app always agree. Stored as JSON in `$XDG_CONFIG_HOME/kadence/desktop.json`. """ from __future__ import annotations import json import os from dataclasses import asdict, dataclass, field from pathlib import Path def config_dir() -> Path: base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config") path = Path(base) / "kadence" path.mkdir(parents=True, exist_ok=True) return path CONFIG_PATH = config_dir() / "desktop.json" @dataclass class DesktopConfig: """What this machine needs to know.""" server_url: str = "http://localhost:8000" api_token: str = "" poll_seconds: int = 5 """How often to ask the backend for the timer state and the block policy.""" start_minimized: bool = False close_to_tray: bool = True view_only: bool = False """When true, the app runs in view-only mode with the timer turned off.""" # ---- the floating mini timer ------------------------------------------ mini_timer: bool = True """Allow the small always-on-top countdown window at all.""" mini_timer_auto: bool = True """Show it by itself while an interval runs and the main window is away.""" mini_timer_keep_above: bool = True """On KDE, install a KWin rule so it really floats above every window. Wayland gives an application no way to ask for "always on top", so this is the only honest route; see `integrations.keep_above`. Switching it off removes the rule again. """ mini_timer_corner: str = "bottom-right" """Where it appears: a screen corner, an edge centre, or `remember`. One of: top-left, top-centre, top-right, bottom-left, bottom-centre, bottom-right, centre, remember. `remember` puts it back where you last dragged it instead. """ mini_timer_margin: int = 24 """Gap between the mini timer and the screen edges, in pixels.""" mini_timer_x: int = -1 mini_timer_y: int = -1 """Where it was last dragged to. -1 = let the compositor place it.""" # ---- system integration (each one is a real desktop feature) ----------- tray_icon: bool = True notifications: bool = True notification_actions: bool = True """Buttons on the notification itself: "Start break", "Skip", "+5 min".""" inhibit_screensaver: bool = True """Keep the screen awake while a work interval runs.""" do_not_disturb: bool = True """Silence other apps' notifications during a work interval.""" taskbar_progress: bool = True """Progress on the dock/taskbar icon (Unity LauncherEntry — KDE honours it).""" pause_media: bool = True """Pause whatever is playing (MPRIS) when focus starts.""" pause_on_lock: bool = True """Pause the timer when the session locks, resume when you come back.""" pause_on_idle_minutes: int = 0 """Pause after this many idle minutes. 0 = never.""" dbus_service: bool = True """Expose org.kadence.Desktop so shortcuts and scripts can drive it.""" # ---- blocking (enforcement; the rules themselves are server-side) ------ enforce_blocking: bool = True block_hosts_file: bool = True block_apps: bool = True helper_path: str = "/usr/local/bin/kadence-helper" window: dict = field(default_factory=lambda: {"width": 1100, "height": 760}) @classmethod def load(cls) -> DesktopConfig: if not CONFIG_PATH.exists(): config = cls() config.save() return config try: data = json.loads(CONFIG_PATH.read_text("utf-8")) except (OSError, json.JSONDecodeError): return cls() known = {key: value for key, value in data.items() if key in cls.__dataclass_fields__} return cls(**known) def save(self) -> None: CONFIG_PATH.write_text(json.dumps(asdict(self), indent=2), "utf-8") def update(self, **changes: object) -> None: for key, value in changes.items(): if key in self.__dataclass_fields__: setattr(self, key, value) self.save()