""" Theme palettes and QSS application. Each theme is a flat dict of color tokens. apply_qss() loads style_template.qss and substitutes @{key} tokens with the palette values. Supported themes ---------------- dark Catppuccin Frappé (dark blue-gray) light Catppuccin Latte (warm light) one_dark One Dark Pro (VS Code classic dark) nord Nord (arctic blue-gray dark) tokyo_night Tokyo Night (purple-dark) dracula Dracula (classic purple-dark) github_light GitHub Light (clean minimal light) """ from __future__ import annotations import re from pathlib import Path _QSS_DIR = Path(__file__).parent.parent.parent / "resources" # ── Palettes ────────────────────────────────────────────────────────────────── # Catppuccin Frappé — dark blue-gray _FRAPPÉ: dict = { "base": "#303446", "mantle": "#292c3c", "crust": "#232634", "surface0": "#414559", "surface1": "#51576d", "surface2": "#626880", "overlay": "#737994", "subtext": "#a5adce", "text": "#c6d0f5", "blue": "#8caaee", "lavender": "#babbf1", "green": "#a6d189", "teal": "#81c8be", "sky": "#99d1db", "red": "#e78284", "peach": "#ef9f76", "yellow": "#e5c890", "mauve": "#ca9ee6", "red_hover": "#ea999c", "tab_hover": "#353849", "tree_hover": "#363a4f", "btn_pressed": "#363849", "gutter_bg": "#2a2d3e", "dirty_bg": "#3a2f25", "dirty_fg": "#ef9f76", "delete_bg": "#3a2530", "delete_fg": "#e78284", "syn_keyword": "#8caaee", "syn_type": "#ef9f76", "syn_function": "#a6d189", "syn_string": "#a6d189", "syn_number": "#ef9f76", "syn_comment": "#737994", "syn_operator": "#ca9ee6", "syn_ident": "#99d1db", "popup_bg": "#292c3c", "popup_border": "#51576d", "popup_selected": "#414559", "popup_hover": "#363a4f", } # Catppuccin Latte — warm light _LATTE: dict = { "base": "#eff1f5", "mantle": "#e6e9ef", "crust": "#dce0e8", "surface0": "#ccd0da", "surface1": "#bcc0cc", "surface2": "#acb0be", "overlay": "#9ca0b0", "subtext": "#6c6f85", "text": "#4c4f69", "blue": "#1e66f5", "lavender": "#7287fd", "green": "#40a02b", "teal": "#179299", "sky": "#04a5e5", "red": "#d20f39", "peach": "#fe640b", "yellow": "#df8e1d", "mauve": "#8839ef", "red_hover": "#da2b50", "tab_hover": "#d8dce8", "tree_hover": "#d8dce8", "btn_pressed": "#c8cdd9", "gutter_bg": "#dce0e8", "dirty_bg": "#fff3e6", "dirty_fg": "#fe640b", "delete_bg": "#ffe8ed", "delete_fg": "#d20f39", "syn_keyword": "#1e66f5", "syn_type": "#fe640b", "syn_function": "#40a02b", "syn_string": "#40a02b", "syn_number": "#fe640b", "syn_comment": "#9ca0b0", "syn_operator": "#8839ef", "syn_ident": "#04a5e5", "popup_bg": "#e6e9ef", "popup_border": "#bcc0cc", "popup_selected": "#ccd0da", "popup_hover": "#d8dce8", } # One Dark Pro — VS Code classic _ONE_DARK: dict = { "base": "#282c34", "mantle": "#21252b", "crust": "#1d2026", "surface0": "#2c313c", "surface1": "#3e4452", "surface2": "#4b5363", "overlay": "#5c6370", "subtext": "#828997", "text": "#abb2bf", "blue": "#61afef", "lavender": "#c678dd", "green": "#98c379", "teal": "#56b6c2", "sky": "#56b6c2", "red": "#e06c75", "peach": "#d19a66", "yellow": "#e5c07b", "mauve": "#c678dd", "red_hover": "#e8868f", "tab_hover": "#2e3440", "tree_hover": "#2c313c", "btn_pressed": "#282c34", "gutter_bg": "#1e2127", "dirty_bg": "#3a2f1e", "dirty_fg": "#d19a66", "delete_bg": "#3a1e20", "delete_fg": "#e06c75", "syn_keyword": "#c678dd", "syn_type": "#e5c07b", "syn_function": "#61afef", "syn_string": "#98c379", "syn_number": "#d19a66", "syn_comment": "#5c6370", "syn_operator": "#56b6c2", "syn_ident": "#abb2bf", "popup_bg": "#21252b", "popup_border": "#3e4452", "popup_selected": "#2c313c", "popup_hover": "#2e3440", } # Nord — arctic blue-gray _NORD: dict = { "base": "#2e3440", "mantle": "#292e3b", "crust": "#242831", "surface0": "#3b4252", "surface1": "#434c5e", "surface2": "#4c566a", "overlay": "#616e88", "subtext": "#d8dee9", "text": "#eceff4", "blue": "#88c0d0", "lavender": "#b48ead", "green": "#a3be8c", "teal": "#8fbcbb", "sky": "#81a1c1", "red": "#bf616a", "peach": "#d08770", "yellow": "#ebcb8b", "mauve": "#b48ead", "red_hover": "#ca737b", "tab_hover": "#333a47", "tree_hover": "#323844", "btn_pressed": "#2e3440", "gutter_bg": "#272c38", "dirty_bg": "#3a3320", "dirty_fg": "#d08770", "delete_bg": "#3a2730", "delete_fg": "#bf616a", "syn_keyword": "#81a1c1", "syn_type": "#ebcb8b", "syn_function": "#88c0d0", "syn_string": "#a3be8c", "syn_number": "#b48ead", "syn_comment": "#616e88", "syn_operator": "#8fbcbb", "syn_ident": "#eceff4", "popup_bg": "#292e3b", "popup_border": "#434c5e", "popup_selected": "#3b4252", "popup_hover": "#323844", } # Tokyo Night — deep purple-dark _TOKYO_NIGHT: dict = { "base": "#1a1b2e", "mantle": "#16161e", "crust": "#13131a", "surface0": "#24283b", "surface1": "#292e42", "surface2": "#2f3549", "overlay": "#565f89", "subtext": "#9aa5ce", "text": "#c0caf5", "blue": "#7aa2f7", "lavender": "#bb9af7", "green": "#9ece6a", "teal": "#73daca", "sky": "#7dcfff", "red": "#f7768e", "peach": "#ff9e64", "yellow": "#e0af68", "mauve": "#bb9af7", "red_hover": "#f98da0", "tab_hover": "#1e2030", "tree_hover": "#1e2030", "btn_pressed": "#1a1b2e", "gutter_bg": "#16161e", "dirty_bg": "#2a2216", "dirty_fg": "#ff9e64", "delete_bg": "#2a1620", "delete_fg": "#f7768e", "syn_keyword": "#bb9af7", "syn_type": "#e0af68", "syn_function": "#7aa2f7", "syn_string": "#9ece6a", "syn_number": "#ff9e64", "syn_comment": "#565f89", "syn_operator": "#73daca", "syn_ident": "#c0caf5", "popup_bg": "#16161e", "popup_border": "#292e42", "popup_selected": "#24283b", "popup_hover": "#1e2030", } # Dracula — classic purple-dark _DRACULA: dict = { "base": "#282a36", "mantle": "#21222c", "crust": "#191a21", "surface0": "#343746", "surface1": "#424450", "surface2": "#54555f", "overlay": "#6272a4", "subtext": "#bfbfbf", "text": "#f8f8f2", "blue": "#8be9fd", "lavender": "#bd93f9", "green": "#50fa7b", "teal": "#8be9fd", "sky": "#8be9fd", "red": "#ff5555", "peach": "#ffb86c", "yellow": "#f1fa8c", "mauve": "#bd93f9", "red_hover": "#ff7373", "tab_hover": "#2d2f3e", "tree_hover": "#2d2f3e", "btn_pressed": "#21222c", "gutter_bg": "#20212b", "dirty_bg": "#3a3020", "dirty_fg": "#ffb86c", "delete_bg": "#3a1e1e", "delete_fg": "#ff5555", "syn_keyword": "#ff79c6", "syn_type": "#ffb86c", "syn_function": "#50fa7b", "syn_string": "#f1fa8c", "syn_number": "#bd93f9", "syn_comment": "#6272a4", "syn_operator": "#ff79c6", "syn_ident": "#8be9fd", "popup_bg": "#21222c", "popup_border": "#424450", "popup_selected": "#343746", "popup_hover": "#2d2f3e", } # GitHub Light — clean minimal light _GITHUB_LIGHT: dict = { "base": "#ffffff", "mantle": "#f6f8fa", "crust": "#eaeef2", "surface0": "#f0f3f6", "surface1": "#d0d7de", "surface2": "#afb8c1", "overlay": "#8c959f", "subtext": "#57606a", "text": "#24292f", "blue": "#0969da", "lavender": "#8250df", "green": "#1a7f37", "teal": "#0a7bca", "sky": "#0550ae", "red": "#cf222e", "peach": "#bc4c00", "yellow": "#9a6700", "mauve": "#8250df", "red_hover": "#d93f47", "tab_hover": "#eaeef2", "tree_hover": "#eaeef2", "btn_pressed": "#e0e6ec", "gutter_bg": "#f0f3f6", "dirty_bg": "#fff8e1", "dirty_fg": "#bc4c00", "delete_bg": "#fde8e8", "delete_fg": "#cf222e", "syn_keyword": "#cf222e", "syn_type": "#953800", "syn_function": "#8250df", "syn_string": "#0a3069", "syn_number": "#0550ae", "syn_comment": "#8c959f", "syn_operator": "#cf222e", "syn_ident": "#0969da", "popup_bg": "#f6f8fa", "popup_border": "#d0d7de", "popup_selected": "#e8ecf0", "popup_hover": "#eaeef2", } # Registry: theme key → palette dict ALL_THEMES: dict[str, dict] = { "dark": _FRAPPÉ, "light": _LATTE, "one_dark": _ONE_DARK, "nord": _NORD, "tokyo_night": _TOKYO_NIGHT, "dracula": _DRACULA, "github_light": _GITHUB_LIGHT, } # Which theme keys are considered "dark" _DARK_KEYS = {"dark", "one_dark", "nord", "tokyo_night", "dracula"} # Kept for backward compat (imported elsewhere as DARK / LIGHT) DARK = _FRAPPÉ LIGHT = _LATTE # ── Public API ──────────────────────────────────────────────────────────────── def get_palette() -> dict: """Return the color dict for the currently active theme.""" from app.config.settings import get_settings name = get_settings().get("theme", "dark") return ALL_THEMES.get(name, _FRAPPÉ) def is_dark(theme: str | None = None) -> bool: from app.config.settings import get_settings if theme is None: theme = get_settings().get("theme", "dark") return theme in _DARK_KEYS def apply_qss(app, theme: str | None = None) -> None: """Load the QSS template, substitute palette tokens, and apply to *app*.""" from app.config.settings import get_settings if theme is None: theme = get_settings().get("theme", "dark") palette = ALL_THEMES.get(theme, _FRAPPÉ) template_path = _QSS_DIR / "style_template.qss" if template_path.exists(): template = template_path.read_text(encoding="utf-8") qss = re.sub(r"@\{(\w+)\}", lambda m: palette.get(m.group(1), m.group(0)), template) app.setStyleSheet(qss) return # Fallback: legacy per-file approach legacy = _QSS_DIR / ("style.qss" if is_dark(theme) else "style_light.qss") if legacy.exists(): app.setStyleSheet(legacy.read_text(encoding="utf-8")) def qss_path(theme: str) -> Path: """Legacy helper — kept so old call-sites don't break.""" return _QSS_DIR / ("style.qss" if is_dark(theme) else "style_light.qss")