"""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 from string import Template from PySide6.QtCore import Qt from PySide6.QtGui import QColor, QFont, QIcon, QLinearGradient, QPainter, QPixmap from PySide6.QtWidgets import QApplication from app.app_settings import get_setting, set_setting from app.paths import RESOURCES_DIR # noqa: E402 (chemins valables aussi en .exe) STYLESHEET_PATH = RESOURCES_DIR / "theme.qss" 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), "selection": (47, 111, 237, 70), "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), "selection": (79, 140, 255, 90), "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), "selection": (74, 158, 255, 90), "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: theme = get_setting("theme") return theme if theme in UI_PALETTES else DEFAULT_THEME def save_theme(theme: str) -> None: set_setting("theme", theme) DEFAULT_FONT_SIZE = 10 def load_font_size() -> int: try: size = int(get_setting("font_size", DEFAULT_FONT_SIZE)) except (TypeError, ValueError): return DEFAULT_FONT_SIZE return size if 6 <= size <= 32 else DEFAULT_FONT_SIZE def save_font_size(size: int) -> None: set_setting("font_size", size) 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)