Download theme.py from smlflg/ClaudeCodePanel: direct link, hf CLI and curl.
- Browser
- Download file 14.5 kB
-
https://huggingface.co/smlflg/ClaudeCodePanel/resolve/main/theme.py
- Command line
-
hf download hf://smlflg/ClaudeCodePanel/theme.py
-
curl -L -o theme.py https://huggingface.co/smlflg/ClaudeCodePanel/resolve/main/theme.py
14.5 kB
| #!/usr/bin/env python3 | |
| """Theme detection + CSS generation for Claude Code Control Panel. | |
| Supports COSMIC Desktop light/dark switching. | |
| Detection: COSMIC is_dark config → GTK Settings fallback. | |
| Palettes: Catppuccin Mocha (dark) / Catppuccin Latte (light). | |
| """ | |
| from pathlib import Path | |
| import gi | |
| gi.require_version("Gtk", "3.0") | |
| from gi.repository import Gio, GLib, Gtk | |
| # --------------------------------------------------------------------------- | |
| # COSMIC config path | |
| # --------------------------------------------------------------------------- | |
| _COSMIC_IS_DARK = Path.home() / ".config/cosmic/com.system76.CosmicTheme.Mode/v1/is_dark" | |
| # --------------------------------------------------------------------------- | |
| # Palettes | |
| # --------------------------------------------------------------------------- | |
| DARK = { | |
| "name": "dark", | |
| "bg": "#1e1e2e", | |
| "mantle": "#181825", | |
| "card": "#313244", | |
| "border": "#45475a", | |
| "text": "#cdd6f4", | |
| "subtext1": "#bac2de", | |
| "dim": "#6c7086", | |
| "overlay": "#a6adc8", | |
| "accent": "#89b4fa", | |
| "mauve": "#cba6f7", | |
| "lavender": "#b4befe", | |
| "green": "#a6e3a1", | |
| "yellow": "#f9e2af", | |
| "red": "#f38ba8", | |
| "teal": "#94e2d5", | |
| "peach": "#fab387", | |
| # Derived | |
| "accent_hover_bg": "rgba(137,180,250,0.2)", | |
| "mauve_hover_bg": "rgba(203,166,247,0.2)", | |
| "green_hover_bg": "rgba(166,227,161,0.2)", | |
| "yellow_hover_bg": "rgba(249,226,175,0.2)", | |
| "resume_text": "#1e1e2e", | |
| } | |
| LIGHT = { | |
| "name": "light", | |
| "bg": "#eff1f5", | |
| "mantle": "#e6e9ef", | |
| "card": "#dce0e8", | |
| "border": "#ccd0da", | |
| "text": "#4c4f69", | |
| "subtext1": "#5c5f77", | |
| "dim": "#7c7f93", | |
| "overlay": "#8c8fa1", | |
| "accent": "#1e66f5", | |
| "mauve": "#8839ef", | |
| "lavender": "#7287fd", | |
| "green": "#1a7f12", | |
| "yellow": "#c65d06", | |
| "red": "#d20f39", | |
| "teal": "#127a80", | |
| "peach": "#e64d00", | |
| # Derived | |
| "accent_hover_bg": "rgba(30,102,245,0.12)", | |
| "mauve_hover_bg": "rgba(136,57,239,0.12)", | |
| "green_hover_bg": "rgba(64,160,43,0.12)", | |
| "yellow_hover_bg": "rgba(223,142,29,0.12)", | |
| "resume_text": "#eff1f5", | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Detection | |
| # --------------------------------------------------------------------------- | |
| def is_dark_mode() -> bool: | |
| """Detect dark mode: COSMIC config first, GTK Settings fallback.""" | |
| try: | |
| content = _COSMIC_IS_DARK.read_text().strip().lower() | |
| return content == "true" | |
| except (OSError, ValueError): | |
| pass | |
| settings = Gtk.Settings.get_default() | |
| if settings is None: | |
| return True # safe fallback | |
| return settings.get_property("gtk-application-prefer-dark-theme") | |
| def get_palette() -> dict[str, str]: | |
| """Return the active color palette.""" | |
| return DARK if is_dark_mode() else LIGHT | |
| def hex_to_pango_rgb(hex_color: str) -> tuple[int, int, int]: | |
| """Convert '#rrggbb' to Pango 16-bit (r, g, b) tuple.""" | |
| return ( | |
| int(hex_color[1:3], 16) * 257, | |
| int(hex_color[3:5], 16) * 257, | |
| int(hex_color[5:7], 16) * 257, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # CSS generation | |
| # --------------------------------------------------------------------------- | |
| def build_css(p: dict[str, str] | None = None) -> bytes: | |
| """Generate panel CSS from a palette dict. Auto-detects if p is None.""" | |
| if p is None: | |
| p = get_palette() | |
| return f""" | |
| window {{ | |
| background-color: {p["bg"]}; | |
| }} | |
| .panel-header {{ | |
| font-size: 18px; | |
| font-weight: bold; | |
| color: {p["text"]}; | |
| }} | |
| .stat-value {{ | |
| font-size: 22px; | |
| font-weight: bold; | |
| color: {p["accent"]}; | |
| }} | |
| .stat-label {{ | |
| font-size: 11px; | |
| color: {p["overlay"]}; | |
| }} | |
| .shortcut-btn {{ | |
| padding: 8px 12px; | |
| border-radius: 8px; | |
| background: {p["card"]}; | |
| color: {p["text"]}; | |
| border: 1px solid {p["border"]}; | |
| min-width: 90px; | |
| }} | |
| .shortcut-btn:hover {{ | |
| background: {p["border"]}; | |
| }} | |
| .shortcut-service {{ border-color: {p["accent"]}; }} | |
| .shortcut-service:hover {{ border-color: {p["accent"]}; background: {p["accent_hover_bg"]}; }} | |
| .shortcut-docs {{ border-color: {p["yellow"]}; }} | |
| .shortcut-docs:hover {{ border-color: {p["yellow"]}; background: {p["yellow_hover_bg"]}; }} | |
| .shortcut-folder {{ border-color: {p["green"]}; }} | |
| .shortcut-folder:hover {{ border-color: {p["green"]}; background: {p["green_hover_bg"]}; }} | |
| .shortcut-config {{ border-color: {p["mauve"]}; }} | |
| .shortcut-config:hover {{ border-color: {p["mauve"]}; background: {p["mauve_hover_bg"]}; }} | |
| .session-row {{ | |
| padding: 8px 10px; | |
| border-radius: 8px; | |
| background: transparent; | |
| border-left: 3px solid {p["mauve"]}; | |
| margin: 3px 0; | |
| }} | |
| .session-row:hover {{ | |
| background: {p["card"]}; | |
| }} | |
| .session-row-working {{ | |
| border-left: 3px solid {p["yellow"]}; | |
| background: {p["yellow_hover_bg"]}; | |
| }} | |
| .session-row-working:hover {{ | |
| background: {p["yellow_hover_bg"]}; | |
| }} | |
| .session-row-ready {{ | |
| border-left: 3px solid {p["green"]}; | |
| background: {p["green_hover_bg"]}; | |
| }} | |
| .session-row-ready:hover {{ | |
| background: {p["green_hover_bg"]}; | |
| }} | |
| .session-working-badge {{ | |
| background: {p["yellow_hover_bg"]}; | |
| color: {p["yellow"]}; | |
| border-radius: 12px; | |
| padding: 2px 8px; | |
| font-size: 10px; | |
| font-weight: bold; | |
| }} | |
| .session-ready-badge {{ | |
| background: {p["green_hover_bg"]}; | |
| color: {p["green"]}; | |
| border-radius: 12px; | |
| padding: 2px 8px; | |
| font-size: 10px; | |
| font-weight: bold; | |
| }} | |
| .session-project {{ | |
| font-size: 13px; | |
| font-weight: bold; | |
| color: {p["text"]}; | |
| }} | |
| .session-preview {{ | |
| font-size: 11px; | |
| color: {p["subtext1"]}; | |
| }} | |
| .session-meta {{ | |
| font-size: 10px; | |
| color: {p["dim"]}; | |
| letter-spacing: 0.02em; | |
| }} | |
| .session-resume {{ | |
| padding: 4px 14px; | |
| border-radius: 6px; | |
| background: {p["accent"]}; | |
| color: {p["resume_text"]}; | |
| font-weight: bold; | |
| font-size: 11px; | |
| border: none; | |
| }} | |
| .session-resume:hover {{ | |
| background: {p["lavender"]}; | |
| }} | |
| .session-stats {{ | |
| border-radius: 20px; | |
| background: {p["card"]}; | |
| padding: 6px 14px; | |
| color: {p["teal"]}; | |
| font-size: 11px; | |
| letter-spacing: 0.02em; | |
| }} | |
| .section-frame {{ | |
| border: 1px solid {p["border"]}; | |
| border-radius: 8px; | |
| padding: 10px; | |
| }} | |
| .section-title {{ | |
| font-weight: bold; | |
| color: {p["text"]}; | |
| font-size: 13px; | |
| }} | |
| .monitor-value {{ | |
| font-family: monospace; | |
| color: {p["green"]}; | |
| }} | |
| .cost-value {{ | |
| font-family: monospace; | |
| font-size: 20px; | |
| font-weight: bold; | |
| color: {p["yellow"]}; | |
| }} | |
| .status-bar {{ | |
| background: {p["mantle"]}; | |
| padding: 6px 12px; | |
| }} | |
| .status-saved {{ | |
| color: {p["green"]}; | |
| font-weight: bold; | |
| }} | |
| .status-error {{ | |
| color: {p["red"]}; | |
| font-weight: bold; | |
| }} | |
| notebook header {{ | |
| background: {p["mantle"]}; | |
| }} | |
| notebook tab {{ | |
| padding: 8px 16px; | |
| color: {p["overlay"]}; | |
| }} | |
| notebook tab:checked {{ | |
| color: {p["accent"]}; | |
| border-bottom: 2px solid {p["accent"]}; | |
| }} | |
| .watcher-dot-critical {{ color: {p["red"]}; font-size: 16px; }} | |
| .watcher-dot-warning {{ color: {p["yellow"]}; font-size: 16px; }} | |
| .watcher-dot-info {{ color: {p["accent"]}; font-size: 16px; }} | |
| .watcher-dot-inactive {{ color: {p["dim"]}; font-size: 16px; }} | |
| .watcher-name {{ font-family: monospace; font-size: 11px; color: {p["subtext1"]}; }} | |
| .phase-badge {{ | |
| font-family: monospace; | |
| font-size: 12px; | |
| font-weight: bold; | |
| padding: 4px 10px; | |
| border-radius: 12px; | |
| color: {p["text"]}; | |
| }} | |
| .phase-badge-exploration {{ background: rgba(137,180,250,0.25); color: {p["accent"]}; }} | |
| .phase-badge-implementation {{ background: rgba(166,227,161,0.25); color: {p["green"]}; }} | |
| .phase-badge-testing {{ background: rgba(249,226,175,0.25); color: {p["yellow"]}; }} | |
| .phase-badge-debugging {{ background: rgba(243,139,168,0.25); color: {p["red"]}; }} | |
| .signal-card {{ | |
| border-radius: 8px; | |
| background: {p["card"]}; | |
| border: 1px solid {p["border"]}; | |
| padding: 12px; | |
| }} | |
| .cost-value {{ | |
| font-family: monospace; | |
| font-size: 20px; | |
| font-weight: bold; | |
| color: {p["yellow"]}; | |
| }} | |
| .mon-stat-value {{ | |
| font-family: monospace; | |
| font-size: 16px; | |
| font-weight: bold; | |
| color: {p["accent"]}; | |
| }} | |
| .stat-sublabel {{ | |
| font-size: 11px; | |
| color: {p["subtext1"]}; | |
| }} | |
| .sc-card {{ | |
| border-radius: 8px; | |
| background: {p["card"]}; | |
| border: 1px solid {p["border"]}; | |
| padding: 10px; | |
| margin: 4px 0; | |
| }} | |
| .sc-badge-mastered {{ | |
| background: {p["green_hover_bg"]}; | |
| color: {p["green"]}; | |
| border-radius: 12px; | |
| padding: 2px 8px; | |
| font-size: 10px; | |
| font-weight: bold; | |
| }} | |
| .sc-badge-learning {{ | |
| background: {p["yellow_hover_bg"]}; | |
| color: {p["yellow"]}; | |
| border-radius: 12px; | |
| padding: 2px 8px; | |
| font-size: 10px; | |
| font-weight: bold; | |
| }} | |
| .sc-badge-unused {{ | |
| background: rgba(243,139,168,0.2); | |
| color: {p["red"]}; | |
| border-radius: 12px; | |
| padding: 2px 8px; | |
| font-size: 10px; | |
| font-weight: bold; | |
| }} | |
| .sc-learn-card {{ | |
| border-left: 3px solid {p["red"]}; | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| background: {p["card"]}; | |
| margin: 3px 0; | |
| }} | |
| .sc-learn-card:hover {{ | |
| background: {p["border"]}; | |
| }} | |
| /* --- Log Viewer --- */ | |
| .log-toolbar {{ | |
| background: {p["mantle"]}; | |
| border-radius: 8px; | |
| padding: 6px 10px; | |
| }} | |
| .log-card {{ | |
| background: {p["card"]}; | |
| border-radius: 6px; | |
| border: 1px solid {p["border"]}; | |
| padding: 6px 10px; | |
| margin: 1px 0; | |
| }} | |
| .log-card:hover {{ | |
| background: {p["border"]}; | |
| }} | |
| .log-card-tool {{ border-left: 3px solid {p["green"]}; }} | |
| .log-card-llm {{ border-left: 3px solid {p["accent"]}; }} | |
| .log-card-error {{ border-left: 3px solid {p["red"]}; }} | |
| .log-card-hook {{ border-left: 3px solid {p["mauve"]}; }} | |
| .log-time {{ | |
| font-family: monospace; | |
| font-size: 10px; | |
| color: {p["dim"]}; | |
| }} | |
| .log-tool-name {{ | |
| font-family: monospace; | |
| font-size: 11px; | |
| font-weight: bold; | |
| color: {p["text"]}; | |
| }} | |
| .log-detail {{ | |
| font-size: 11px; | |
| color: {p["subtext1"]}; | |
| }} | |
| .log-cost {{ | |
| font-family: monospace; | |
| font-size: 11px; | |
| color: {p["yellow"]}; | |
| }} | |
| .log-status-ok {{ color: {p["green"]}; }} | |
| .log-status-error {{ color: {p["red"]}; }} | |
| .log-status-neutral {{ color: {p["dim"]}; }} | |
| .log-stats {{ | |
| font-size: 11px; | |
| color: {p["overlay"]}; | |
| }} | |
| /* --- Swarm Tab --- */ | |
| .swarm-toolbar {{ | |
| background: {p["mantle"]}; | |
| border-radius: 8px; | |
| padding: 6px 10px; | |
| }} | |
| .swarm-btn {{ | |
| background: {p["card"]}; | |
| border-radius: 6px; | |
| border: 1px solid {p["border"]}; | |
| padding: 4px 12px; | |
| color: {p["text"]}; | |
| }} | |
| .swarm-btn:hover {{ | |
| background: {p["border"]}; | |
| }} | |
| .swarm-status-connected {{ | |
| color: {p["green"]}; | |
| font-weight: bold; | |
| }} | |
| .swarm-status-disconnected {{ | |
| color: {p["red"]}; | |
| font-weight: bold; | |
| }} | |
| .swarm-url {{ | |
| font-family: monospace; | |
| font-size: 11px; | |
| color: {p["dim"]}; | |
| }} | |
| /* --- Swarm Native Tab --- */ | |
| .swarm-task-row {{ | |
| padding: 4px 8px; | |
| border-radius: 6px; | |
| background: transparent; | |
| margin: 1px 0; | |
| }} | |
| .swarm-task-row:hover {{ | |
| background: {p["card"]}; | |
| }} | |
| .swarm-badge-progress {{ | |
| color: {p["accent"]}; | |
| font-weight: bold; | |
| font-size: 11px; | |
| }} | |
| .swarm-badge-completed {{ | |
| color: {p["green"]}; | |
| font-weight: bold; | |
| font-size: 11px; | |
| }} | |
| .swarm-badge-pending {{ | |
| color: {p["dim"]}; | |
| font-weight: bold; | |
| font-size: 11px; | |
| }} | |
| .swarm-message {{ | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| background: transparent; | |
| margin: 1px 0; | |
| }} | |
| .swarm-message:hover {{ | |
| background: {p["card"]}; | |
| }} | |
| .session-snippet {{ | |
| font-family: monospace; | |
| font-size: 10px; | |
| color: {p["subtext1"]}; | |
| padding: 2px 0; | |
| }} | |
| .session-match-badge {{ | |
| background: {p["accent_hover_bg"]}; | |
| color: {p["accent"]}; | |
| border-radius: 12px; | |
| padding: 2px 8px; | |
| font-size: 10px; | |
| font-weight: bold; | |
| }} | |
| .session-row-search {{ | |
| border-left: 3px solid {p["accent"]}; | |
| }} | |
| /* --- Monitor Tab Enhancements --- */ | |
| .mon-tool-bar {{ | |
| min-height: 8px; | |
| }} | |
| .mon-tool-bar trough {{ | |
| min-height: 8px; | |
| background: {p["border"]}; | |
| border-radius: 4px; | |
| }} | |
| .mon-tool-bar progress {{ | |
| min-height: 8px; | |
| background: {p["accent"]}; | |
| border-radius: 4px; | |
| }} | |
| .mon-expander {{ | |
| color: {p["subtext1"]}; | |
| font-size: 11px; | |
| }} | |
| .mon-expander title {{ | |
| color: {p["subtext1"]}; | |
| }} | |
| """.encode() | |
| # --------------------------------------------------------------------------- | |
| # Theme watcher | |
| # --------------------------------------------------------------------------- | |
| _css_provider: Gtk.CssProvider | None = None | |
| _file_monitor = None # prevent GC | |
| def apply_theme(provider: Gtk.CssProvider | None = None) -> None: | |
| """(Re-)apply CSS based on current theme. Stores provider for reuse.""" | |
| global _css_provider | |
| if provider is not None: | |
| _css_provider = provider | |
| if _css_provider is None: | |
| return | |
| _css_provider.load_from_data(build_css()) | |
| def setup_theme_watcher(provider: Gtk.CssProvider) -> None: | |
| """Watch for COSMIC dark/light changes and re-apply CSS automatically.""" | |
| global _css_provider, _file_monitor | |
| _css_provider = provider | |
| # Watch COSMIC config file (or parent dir if file doesn't exist yet) | |
| if _COSMIC_IS_DARK.exists(): | |
| gio_file = Gio.File.new_for_path(str(_COSMIC_IS_DARK)) | |
| try: | |
| _file_monitor = gio_file.monitor_file(Gio.FileMonitorFlags.NONE, None) | |
| _file_monitor.connect("changed", _on_cosmic_theme_changed) | |
| except Exception: | |
| pass | |
| elif _COSMIC_IS_DARK.parent.exists(): | |
| # Watch parent dir so we detect when the file is created | |
| gio_dir = Gio.File.new_for_path(str(_COSMIC_IS_DARK.parent)) | |
| try: | |
| _file_monitor = gio_dir.monitor_directory(Gio.FileMonitorFlags.NONE, None) | |
| _file_monitor.connect("changed", _on_cosmic_theme_changed) | |
| except Exception: | |
| pass | |
| # Also watch GTK settings as fallback | |
| settings = Gtk.Settings.get_default() | |
| if settings: | |
| settings.connect("notify::gtk-application-prefer-dark-theme", _on_gtk_theme_changed) | |
| settings.connect("notify::gtk-theme-name", _on_gtk_theme_changed) | |
| def _on_cosmic_theme_changed(_monitor, _file, _other, event_type) -> None: | |
| if event_type in (Gio.FileMonitorEvent.CHANGES_DONE_HINT, Gio.FileMonitorEvent.CREATED): | |
| GLib.idle_add(apply_theme) | |
| def _on_gtk_theme_changed(*_args) -> None: | |
| GLib.idle_add(apply_theme) | |