refactor: enhance TimerPage layout and functionality; update theme and tokens for improved design consistency

- 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.
This commit is contained in:
timka123 committed 2026-10-18 23:57:36 +03:00
1 parent 17a0a3c4c9
commit 77b26757bf
84 files changed
+4501 -2682

No files matched your search

@@ -0,0 +1,215 @@
"""Keeping the mini timer above everything else — on Wayland, for real.
Wayland has no "always on top" an ordinary application can ask for: the
`Qt.WindowStaysOnTopHint` flag is honoured on X11 and quietly ignored by a
Wayland compositor. KWin does have the mechanism, but it lives in *window
rules*, so the only honest way to float above other applications on KDE is to
install one — which is what this module does, reversibly and with a backup.
The rule matches this one window (title `Kadence mini`, app id `kadence`) and
forces: keep above, skip the task bar, skip the switcher, skip the pager. KWin
is asked to re-read its configuration over D-Bus, so it takes effect at once
without logging out.
Everywhere else this is a no-op and the Qt flag does the job.
"""
from __future__ import annotations
import configparser
import logging
import os
import shutil
import uuid
from pathlib import Path
from PyQt6.QtDBus import QDBusConnection, QDBusInterface
log = logging.getLogger(__name__)
DESCRIPTION = "Kadence mini timer"
WINDOW_TITLE = "Kadence mini"
WINDOW_CLASS = "kadence"
# KWin's rule-policy codes: 2 is "Force".
_FORCE = "2"
# KWin's string-match codes: 1 is "Exact Match", 2 is "Substring Match".
_EXACT = "1"
_SUBSTRING = "2"
# Matched on the window title, as a **substring**.
#
# Two traps here, both found by asking KWin what it actually sees:
# * Qt appends the application's display name, so the window KWin knows is
# "Kadence mini — Kadence", and an exact match on "Kadence mini" never
# fired;
# * the app id depends on how the app was started (`kadence` from the desktop
# entry, `python3` from a bare `python3 -m ...`), so requiring the class as
# well would make the rule stop matching for some launches.
RULE = {
"Description": DESCRIPTION,
"above": "true",
"aboverule": _FORCE,
"skiptaskbar": "true",
"skiptaskbarrule": _FORCE,
"skipswitcher": "true",
"skipswitcherrule": _FORCE,
"skippager": "true",
"skippagerrule": _FORCE,
"title": WINDOW_TITLE,
"titlematch": _SUBSTRING,
"wmclassmatch": "0",
}
def rules_path() -> Path:
base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
return Path(base) / "kwinrulesrc"
def supported() -> bool:
"""Only KDE has this mechanism; elsewhere the Qt flag is all there is."""
desktop = os.environ.get("XDG_CURRENT_DESKTOP", "")
return "KDE" in desktop.upper()
def _parser() -> configparser.RawConfigParser:
parser = configparser.RawConfigParser(strict=False)
# KDE keys are case-sensitive ("Description" must not become "description").
parser.optionxform = str # type: ignore[assignment]
return parser
def _load() -> configparser.RawConfigParser:
parser = _parser()
path = rules_path()
if path.exists():
parser.read(path, encoding="utf-8")
if not parser.has_section("General"):
parser.add_section("General")
return parser
def _our_section(parser: configparser.RawConfigParser) -> str | None:
for section in parser.sections():
if section == "General":
continue
if parser.get(section, "Description", fallback="") == DESCRIPTION:
return section
return None
def _save(parser: configparser.RawConfigParser) -> None:
path = rules_path()
path.parent.mkdir(parents=True, exist_ok=True)
# One backup, before the first time we ever touch the file.
backup = path.with_suffix(path.suffix + ".kadence-backup")
if path.exists() and not backup.exists():
shutil.copy2(path, backup)
temp = path.with_suffix(path.suffix + ".kadence-tmp")
with temp.open("w", encoding="utf-8") as handle:
parser.write(handle, space_around_delimiters=False)
temp.replace(path)
def _reconfigure() -> None:
"""Ask KWin to re-read its rules; without it the change waits for a relogin."""
interface = QDBusInterface("org.kde.KWin", "/KWin", "org.kde.KWin", QDBusConnection.sessionBus())
if interface.isValid():
interface.call("reconfigure")
def _wanted(position: tuple[int, int] | None) -> dict[str, str]:
"""The rule as it should be, with the forced position when there is one."""
rule = dict(RULE)
if position is not None:
# KWin stores a forced position as "x,y" — this is what places the
# window on Wayland, where a client may not place itself.
rule["position"] = f"{position[0]},{position[1]}"
rule["positionrule"] = _FORCE
return rule
def installed() -> bool:
try:
return _our_section(_load()) is not None
except (configparser.Error, OSError):
return False
def up_to_date(position: tuple[int, int] | None = None) -> bool:
"""Whether the installed rule matches what this version would write.
An older build wrote an exact title match that never fired; without this
check an upgrade would leave the broken rule in place forever, because the
rule *exists*. The position is part of the comparison, so moving the mini
timer to another corner rewrites the rule — and nothing else does.
"""
try:
parser = _load()
section = _our_section(parser)
if section is None:
return False
wanted = _wanted(position)
if any(parser.get(section, key, fallback=None) != value for key, value in wanted.items()):
return False
# A position left over from a previous corner must not linger.
if position is None and parser.has_option(section, "position"):
return False
return True
except (configparser.Error, OSError):
return False
def apply(position: tuple[int, int] | None = None) -> tuple[bool, str]:
"""Install (or refresh) the rule, optionally forcing a position."""
if not supported():
return False, "Keeping a window above others this way needs KWin (KDE)."
try:
parser = _load()
section = _our_section(parser) or str(uuid.uuid4())
if not parser.has_section(section):
parser.add_section(section)
for key in ("position", "positionrule"):
parser.remove_option(section, key)
for key, value in _wanted(position).items():
parser.set(section, key, value)
listed = [item for item in parser.get("General", "rules", fallback="").split(",") if item]
if section not in listed:
listed.append(section)
parser.set("General", "rules", ",".join(listed))
parser.set("General", "count", str(len(listed)))
_save(parser)
except (configparser.Error, OSError) as exc:
log.warning("could not write the KWin rule: %s", exc)
return False, f"Could not write {rules_path()}: {exc}"
_reconfigure()
if position is not None:
return True, "KWin keeps the mini timer above other windows, in the corner you chose."
return True, "KWin will now keep the mini timer above other windows."
def remove() -> tuple[bool, str]:
"""Take the rule back out, leaving the rest of the file alone."""
try:
parser = _load()
section = _our_section(parser)
if section is None:
return True, "No KWin rule to remove."
parser.remove_section(section)
listed = [
item
for item in parser.get("General", "rules", fallback="").split(",")
if item and item != section
]
parser.set("General", "rules", ",".join(listed))
parser.set("General", "count", str(len(listed)))
_save(parser)
except (configparser.Error, OSError) as exc:
return False, f"Could not update {rules_path()}: {exc}"
_reconfigure()
return True, "Removed the KWin rule; the mini timer floats only as far as Qt can ask."