""" Modular Theme System for Markitect This module provides a dynamic theme loading system that loads themes from separate YAML files organized by scope (mode, ui, document, branding). Architecture: themes/ mode/ # Light/dark color schemes ui/ # Interface styling document/ # Document formatting branding/ # Company/personal styling Each theme file is a YAML file with metadata and properties. """ import os import yaml from pathlib import Path from typing import Dict, Any, List import logging logger = logging.getLogger(__name__) class ThemeRegistry: """Registry for dynamically loaded themes.""" def __init__(self): self._themes = {} self._loaded = False def load_themes(self) -> Dict[str, Dict[str, Any]]: """Load all themes from YAML files.""" if self._loaded: return self._themes themes_dir = Path(__file__).parent # Scan all scope directories for scope_dir in themes_dir.iterdir(): if scope_dir.is_dir() and not scope_dir.name.startswith('__'): scope = scope_dir.name self._load_scope_themes(scope_dir, scope) self._loaded = True return self._themes def _load_scope_themes(self, scope_dir: Path, scope: str) -> None: """Load themes from a specific scope directory.""" for theme_file in scope_dir.glob('*.yaml'): theme_name = theme_file.stem try: with open(theme_file, 'r', encoding='utf-8') as f: theme_data = yaml.safe_load(f) # Validate theme structure if not self._validate_theme(theme_data, theme_name, theme_file): continue # Store theme in registry self._themes[theme_name] = { 'scope': theme_data.get('scope', scope), 'properties': theme_data.get('properties', {}), 'metadata': { 'name': theme_data.get('name', theme_name), 'description': theme_data.get('description', ''), 'author': theme_data.get('author', ''), 'version': theme_data.get('version', '1.0.0'), 'file': str(theme_file) } } logger.debug(f"Loaded theme '{theme_name}' from {theme_file}") except yaml.YAMLError as e: logger.error(f"Failed to parse YAML in {theme_file}: {e}") except Exception as e: logger.error(f"Failed to load theme from {theme_file}: {e}") def _validate_theme(self, theme_data: Dict[str, Any], theme_name: str, theme_file: Path) -> bool: """Validate theme structure.""" if not isinstance(theme_data, dict): logger.error(f"Theme {theme_file} must be a dictionary") return False if 'properties' not in theme_data: logger.error(f"Theme {theme_file} missing 'properties' section") return False if not isinstance(theme_data['properties'], dict): logger.error(f"Theme {theme_file} 'properties' must be a dictionary") return False return True def get_theme(self, name: str) -> Dict[str, Any]: """Get a specific theme by name.""" if not self._loaded: self.load_themes() return self._themes.get(name) def list_themes(self, scope: str = None) -> List[str]: """List available theme names, optionally filtered by scope.""" if not self._loaded: self.load_themes() if scope: return [name for name, data in self._themes.items() if data.get('scope') == scope] return list(self._themes.keys()) def get_themes_by_scope(self, scope: str) -> Dict[str, Dict[str, Any]]: """Get all themes for a specific scope.""" if not self._loaded: self.load_themes() return {name: data for name, data in self._themes.items() if data.get('scope') == scope} # Global theme registry instance theme_registry = ThemeRegistry() def get_layered_themes() -> Dict[str, Dict[str, Any]]: """ Get all themes in the format expected by the existing system. Returns a dictionary compatible with the old LAYERED_THEMES structure. """ return theme_registry.load_themes() def get_theme(name: str) -> Dict[str, Any]: """Get a specific theme by name.""" return theme_registry.get_theme(name) def list_themes(scope: str = None) -> List[str]: """List available theme names.""" return theme_registry.list_themes(scope) def reload_themes() -> None: """Force reload of all themes.""" theme_registry._loaded = False theme_registry._themes.clear() theme_registry.load_themes() # Export main functions __all__ = ['get_layered_themes', 'get_theme', 'list_themes', 'reload_themes', 'theme_registry']