"""Tema claro/oscuro siguiendo el esquema de color del sistema. Usa el estilo Fusion (consistente entre plataformas) y aplica una paleta clara u oscura según ``QStyleHints.colorScheme()``. Los widgets que no fuerzan colores propios (consola de log, fondos) siguen automáticamente esta paleta. """ from __future__ import annotations import subprocess from collections.abc import Callable from PySide6.QtCore import Qt from PySide6.QtGui import QColor, QPalette from PySide6.QtWidgets import QApplication # Modos de tema seleccionables por el usuario. THEME_SYSTEM = "system" THEME_LIGHT = "light" THEME_DARK = "dark" THEME_MODES = (THEME_SYSTEM, THEME_LIGHT, THEME_DARK) def _portal_is_dark() -> bool | None: """Consulta el portal XDG (org.freedesktop.appearance color-scheme). Devuelve True (1=dark), False (2=light) o None (0=sin preferencia / no disponible). Es la fuente más fiable en Linux cuando Qt no detecta el esquema. """ try: out = subprocess.run( [ "gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.Read", "org.freedesktop.appearance", "color-scheme", ], capture_output=True, text=True, timeout=2, ) except (OSError, subprocess.SubprocessError): return None if out.returncode != 0: return None if "uint32 1" in out.stdout: return True if "uint32 2" in out.stdout: return False return None def _gsettings_is_dark() -> bool | None: """Respaldo: gsettings color-scheme de GNOME ('prefer-dark').""" try: out = subprocess.run( ["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"], capture_output=True, text=True, timeout=2, ) except (OSError, subprocess.SubprocessError): return None if out.returncode != 0: return None return "dark" in out.stdout.lower() def system_is_dark(app: QApplication) -> bool: """True si el sistema está en modo oscuro. Prioriza el aviso de Qt; si Qt dice claro (o no soporta la API), consulta el portal XDG y gsettings, que en sesiones xcb suelen ser más fiables que Qt. """ hints = app.styleHints() if hasattr(hints, "colorScheme"): try: if hints.colorScheme() == Qt.ColorScheme.Dark: return True except Exception: # noqa: BLE001 - APIs viejas/raras: caer al fallback pass for probe in (_portal_is_dark, _gsettings_is_dark): val = probe() if val is not None: return val return app.palette().color(QPalette.Window).lightness() < 128 def _dark_palette() -> QPalette: p = QPalette() window = QColor(0x2b, 0x2b, 0x2b) base = QColor(0x1e, 0x1e, 0x1e) alt = QColor(0x35, 0x35, 0x35) text = QColor(0xdc, 0xdc, 0xdc) disabled = QColor(0x7f, 0x7f, 0x7f) highlight = QColor(0x2a, 0x82, 0xda) p.setColor(QPalette.Window, window) p.setColor(QPalette.WindowText, text) p.setColor(QPalette.Base, base) p.setColor(QPalette.AlternateBase, alt) p.setColor(QPalette.ToolTipBase, window) p.setColor(QPalette.ToolTipText, text) p.setColor(QPalette.Text, text) p.setColor(QPalette.Button, alt) p.setColor(QPalette.ButtonText, text) p.setColor(QPalette.BrightText, Qt.red) p.setColor(QPalette.Link, highlight) p.setColor(QPalette.Highlight, highlight) p.setColor(QPalette.HighlightedText, Qt.black) p.setColor(QPalette.PlaceholderText, disabled) for role in (QPalette.WindowText, QPalette.Text, QPalette.ButtonText): p.setColor(QPalette.Disabled, role, disabled) return p def resolve_is_dark(app: QApplication, mode: str) -> bool: """Decide si pintar oscuro según el modo elegido (system/light/dark).""" if mode == THEME_DARK: return True if mode == THEME_LIGHT: return False return system_is_dark(app) # THEME_SYSTEM (o valor desconocido) def apply_theme(app: QApplication, mode: str = THEME_SYSTEM) -> None: """Aplica estilo Fusion + paleta clara/oscura según el modo elegido.""" app.setStyle("Fusion") if resolve_is_dark(app, mode): app.setPalette(_dark_palette()) else: app.setPalette(app.style().standardPalette()) def watch_system_theme(app: QApplication, should_follow: Callable[[], bool]) -> None: """Re-aplica el tema al cambiar el esquema del sistema, solo si seguimos al sistema. ``should_follow`` se consulta en cada cambio: devuelve True cuando el modo activo es 'system' (si el usuario ha forzado claro/oscuro, ignoramos el cambio del SO). """ hints = app.styleHints() if hasattr(hints, "colorSchemeChanged"): hints.colorSchemeChanged.connect( lambda _scheme: apply_theme(app, THEME_SYSTEM) if should_follow() else None )