"""Themes de l'application : palette d'interface (QSS) + palette du terminal. Trois themes sont fournis (clair par defaut, bleu nuit, noir). Ils partagent le meme gabarit `resources/theme.qss`, dont les $variables sont remplacees par la palette choisie : une seule feuille de style a maintenir au lieu de trois quasi identiques. """ from __future__ import annotations import json from pathlib import Path from string import Template from PySide6.QtCore import Qt from PySide6.QtGui import QIcon, QPixmap, QPainter, QColor, QFont, QLinearGradient from PySide6.QtWidgets import QApplication from app.paths import RESOURCES_DIR, DATA_DIR # noqa: E402 (chemins valables aussi en .exe) STYLESHEET_PATH = RESOURCES_DIR / "theme.qss" SETTINGS_FILE = DATA_DIR / "settings.json" THEME_LIGHT = "light" THEME_MIDNIGHT = "midnight" THEME_BLACK = "black" DEFAULT_THEME = THEME_LIGHT # --------------------------------------------------------------------------- # Palettes d'interface # --------------------------------------------------------------------------- UI_PALETTES: dict[str, dict[str, str]] = { THEME_LIGHT: { "window": "#f4f6f9", "base": "#ffffff", "panel": "#eef1f6", "elevated": "#e4e9f0", "input": "#ffffff", "hover": "#dfe6f2", "pressed": "#d2dae8", "border": "#d3dae4", "border_strong": "#b9c3d1", "text": "#1c2430", "text_dim": "#5c6875", "text_disabled": "#a3adba", "accent": "#2f6fed", "accent_hover": "#1f5ed6", "on_accent": "#ffffff", "scroll": "#c3ccd9", "scroll_hover": "#a7b3c4", }, THEME_MIDNIGHT: { "window": "#0a1220", "base": "#0c1526", "panel": "#0e1a30", "elevated": "#1c2c47", "input": "#101c30", "hover": "#172542", "pressed": "#16233b", "border": "#1c2c47", "border_strong": "#2a3c5c", "text": "#e6ecf6", "text_dim": "#93a4c2", "text_disabled": "#55627c", "accent": "#2f6fed", "accent_hover": "#4f8cff", "on_accent": "#ffffff", "scroll": "#2a3c5c", "scroll_hover": "#3a527d", }, THEME_BLACK: { "window": "#000000", "base": "#0a0a0a", "panel": "#141414", "elevated": "#1f1f1f", "input": "#141414", "hover": "#262626", "pressed": "#0f0f0f", "border": "#2b2b2b", "border_strong": "#3d3d3d", "text": "#e8e8e8", "text_dim": "#9a9a9a", "text_disabled": "#5a5a5a", "accent": "#4a9eff", "accent_hover": "#6fb3ff", "on_accent": "#000000", "scroll": "#3a3a3a", "scroll_hover": "#525252", }, } # --------------------------------------------------------------------------- # Palettes du terminal (16 couleurs ANSI + defauts + curseur) # --------------------------------------------------------------------------- TERMINAL_PALETTES: dict[str, dict] = { THEME_LIGHT: { "fg": "#1c2430", "bg": "#ffffff", "cursor": (47, 111, 237, 110), "ansi": { "black": "#1c2430", "red": "#c0392b", "green": "#2e7d32", "brown": "#a06800", "blue": "#1f5ed6", "magenta": "#8e24aa", "cyan": "#00838f", "white": "#5c6875", "brightblack": "#8a94a3", "brightred": "#e04b3a", "brightgreen": "#3ca142", "brightbrown": "#c98a00", "brightblue": "#2f6fed", "brightmagenta": "#a944c7", "brightcyan": "#0aa2b0", "brightwhite": "#1c2430", }, }, THEME_MIDNIGHT: { "fg": "#e6ecf6", "bg": "#0b1220", "cursor": (79, 140, 255, 140), "ansi": { "black": "#0b1220", "red": "#ff6b81", "green": "#8ce99a", "brown": "#f2c94c", "blue": "#4f8cff", "magenta": "#c792ea", "cyan": "#63d4e0", "white": "#dbe4f5", "brightblack": "#3a4a68", "brightred": "#ff8a9b", "brightgreen": "#a9e6b5", "brightbrown": "#f5d778", "brightblue": "#7aa9ff", "brightmagenta": "#dcb3f0", "brightcyan": "#8fe3ec", "brightwhite": "#ffffff", }, }, THEME_BLACK: { "fg": "#e8e8e8", "bg": "#000000", "cursor": (74, 158, 255, 140), "ansi": { "black": "#000000", "red": "#e05561", "green": "#8cc265", "brown": "#d18f52", "blue": "#4aa5f0", "magenta": "#c162de", "cyan": "#42b3c2", "white": "#d0d0d0", "brightblack": "#5a5a5a", "brightred": "#ff6b78", "brightgreen": "#a5e075", "brightbrown": "#e5a663", "brightblue": "#6fb3ff", "brightmagenta": "#d178ee", "brightcyan": "#5fc9d8", "brightwhite": "#ffffff", }, }, } THEME_LABELS = { THEME_LIGHT: "Clair", THEME_MIDNIGHT: "Bleu nuit", THEME_BLACK: "Noir", } _current_theme = DEFAULT_THEME # --------------------------------------------------------------------------- # Preference persistante # --------------------------------------------------------------------------- def load_saved_theme() -> str: try: data = json.loads(SETTINGS_FILE.read_text(encoding="utf-8")) theme = data.get("theme") if theme in UI_PALETTES: return theme except (OSError, json.JSONDecodeError): pass return DEFAULT_THEME def save_theme(theme: str) -> None: DATA_DIR.mkdir(parents=True, exist_ok=True) data = {} try: data = json.loads(SETTINGS_FILE.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): pass data["theme"] = theme try: SETTINGS_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8") except OSError: pass def current_theme() -> str: return _current_theme def terminal_palette(theme: str | None = None) -> dict: return TERMINAL_PALETTES.get(theme or _current_theme, TERMINAL_PALETTES[DEFAULT_THEME]) # --------------------------------------------------------------------------- # Application du theme # --------------------------------------------------------------------------- def apply_theme(app: QApplication, theme: str = DEFAULT_THEME) -> None: """Bascule sur le style Fusion (necessaire pour que la QSS s'applique uniformement, y compris aux widgets stylises nativement par Windows) puis construit la feuille de style a partir de la palette demandee.""" global _current_theme if theme not in UI_PALETTES: theme = DEFAULT_THEME _current_theme = theme app.setStyle("Fusion") try: template = Template(STYLESHEET_PATH.read_text(encoding="utf-8")) app.setStyleSheet(template.safe_substitute(UI_PALETTES[theme])) except OSError: pass def build_app_icon() -> QIcon: """Genere une icone simple (pas de fichier externe requis) : carre + prompt '>_'""" size = 64 pixmap = QPixmap(size, size) pixmap.fill(Qt.transparent) painter = QPainter(pixmap) painter.setRenderHint(QPainter.Antialiasing) gradient = QLinearGradient(0, 0, size, size) gradient.setColorAt(0, QColor("#182a4a")) gradient.setColorAt(1, QColor("#0a1220")) painter.setBrush(gradient) painter.setPen(QColor("#2a3c5c")) painter.drawRoundedRect(2, 2, size - 4, size - 4, 14, 14) font = QFont("Consolas") font.setBold(True) font.setPointSize(24) painter.setFont(font) painter.setPen(QColor("#4f8cff")) painter.drawText(pixmap.rect(), Qt.AlignCenter, ">_") painter.end() return QIcon(pixmap)