"""Starting with the session, the way the desktop expects. Two mechanisms, because the two desktops disagree about which is canonical: * a `.desktop` file in `~/.config/autostart` — the XDG standard, honoured by KDE, GNOME, XFCE and friends; * a **systemd user unit**, which is what you actually want on a modern box: it restarts the app if it dies and keeps its logs in the journal. Either can be switched on alone; the app only ever touches files under the user's own config, never system paths. """ from __future__ import annotations import logging import os import shutil import subprocess import sys from pathlib import Path log = logging.getLogger(__name__) DESKTOP_FILE_NAME = "kadence.desktop" SERVICE_NAME = "kadence-desktop.service" def _autostart_dir() -> Path: base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config") path = Path(base) / "autostart" path.mkdir(parents=True, exist_ok=True) return path def _systemd_user_dir() -> Path: base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config") path = Path(base) / "systemd" / "user" path.mkdir(parents=True, exist_ok=True) return path def launch_command() -> str: """How to start this app again, whether installed or run from a checkout.""" executable = shutil.which("kadence-desktop") if executable: return executable return f"{sys.executable} -m kadence_desktop" # -------------------------------------------------------------------------- # XDG autostart # -------------------------------------------------------------------------- def autostart_enabled() -> bool: return (_autostart_dir() / DESKTOP_FILE_NAME).exists() def set_autostart(enabled: bool, *, minimized: bool = True) -> None: path = _autostart_dir() / DESKTOP_FILE_NAME if not enabled: path.unlink(missing_ok=True) return command = launch_command() + (" --minimized" if minimized else "") path.write_text( "\n".join( [ "[Desktop Entry]", "Type=Application", "Name=Kadence", "Comment=Pomodoro timer, task list and focus guard", f"Exec={command}", "Icon=kadence", "Terminal=false", "Categories=Utility;Office;", "X-GNOME-Autostart-enabled=true", "", ] ), "utf-8", ) log.info("autostart entry written to %s", path) # -------------------------------------------------------------------------- # systemd user service # -------------------------------------------------------------------------- def service_installed() -> bool: return (_systemd_user_dir() / SERVICE_NAME).exists() def service_enabled() -> bool: if not shutil.which("systemctl"): return False result = subprocess.run( ["systemctl", "--user", "is-enabled", SERVICE_NAME], capture_output=True, text=True, check=False, ) return result.stdout.strip() == "enabled" def install_service(*, enable: bool = True) -> str: """Write (and optionally enable) the user unit. Returns a status line.""" path = _systemd_user_dir() / SERVICE_NAME path.write_text( "\n".join( [ "[Unit]", "Description=Kadence desktop client", "PartOf=graphical-session.target", "After=graphical-session.target", "", "[Service]", "Type=simple", f"ExecStart={launch_command()} --minimized", "Restart=on-failure", "RestartSec=5", "", "[Install]", "WantedBy=graphical-session.target", "", ] ), "utf-8", ) if not shutil.which("systemctl"): return f"unit written to {path} (systemctl not found)" subprocess.run(["systemctl", "--user", "daemon-reload"], check=False) if enable: subprocess.run(["systemctl", "--user", "enable", SERVICE_NAME], check=False) return f"unit written to {path}" + (" and enabled" if enable else "") def remove_service() -> None: if shutil.which("systemctl"): subprocess.run(["systemctl", "--user", "disable", "--now", SERVICE_NAME], check=False) (_systemd_user_dir() / SERVICE_NAME).unlink(missing_ok=True) if shutil.which("systemctl"): subprocess.run(["systemctl", "--user", "daemon-reload"], check=False)