- Refactored TimerPage component to improve layout and user experience. - Updated theme configuration to utilize design tokens for better maintainability. - Introduced new tokens for colors and typography to align with design principles. - Simplified state management and improved timer functionality. - Enhanced accessibility and responsiveness of UI elements.
130 lines
4.2 KiB
Python
130 lines
4.2 KiB
Python
"""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()
|