Coverage for src / local_deep_research / web / themes / __init__.py: 98%
77 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
1"""
2Theme System - Modular theme management with individual theme files.
4This module provides a centralized theme registry that:
5- Auto-discovers themes from CSS files with TOML frontmatter
6- Generates combined CSS for browser consumption
7- Provides theme metadata for UI dropdowns
8- Integrates with Flask templates via Jinja2 globals
10Usage:
11 from local_deep_research.web.themes import theme_registry
13 # Get all theme IDs
14 themes = theme_registry.get_theme_ids()
16 # Get theme metadata for JavaScript
17 metadata_json = theme_registry.get_metadata_json()
19 # Generate combined CSS
20 css = theme_registry.get_combined_css()
21"""
23import json
24from pathlib import Path
26from loguru import logger
27from markupsafe import Markup
29from .loader import ThemeLoader
30from .schema import GROUP_LABELS, ThemeMetadata
32# Package directory (where theme subdirectories live)
33THEMES_DIR = Path(__file__).parent
36class ThemeRegistry:
37 """Central registry for theme management.
39 This is a singleton class that manages all theme loading, caching,
40 and provides methods for Flask integration.
41 """
43 _instance: "ThemeRegistry | None" = None
45 def __new__(cls) -> "ThemeRegistry":
46 """Ensure singleton pattern."""
47 if cls._instance is None:
48 cls._instance = super().__new__(cls)
49 cls._instance._loader = ThemeLoader(THEMES_DIR)
50 cls._instance._initialized = False
51 return cls._instance
53 @property
54 def themes(self) -> dict[str, ThemeMetadata]:
55 """Get all loaded themes."""
56 return self._loader.load_all_themes()
58 def get_theme(self, theme_id: str) -> ThemeMetadata | None:
59 """Get a specific theme by ID.
61 Args:
62 theme_id: The theme identifier (e.g., "nord", "dracula")
64 Returns:
65 ThemeMetadata object or None if not found
66 """
67 return self.themes.get(theme_id)
69 def get_theme_ids(self) -> list[str]:
70 """Get sorted list of all theme IDs.
72 Returns:
73 List of theme ID strings
74 """
75 return sorted(self.themes.keys())
77 def get_themes_by_group(self) -> dict[str, list[ThemeMetadata]]:
78 """Get themes organized by group.
80 Returns:
81 Dictionary mapping group names to lists of ThemeMetadata
82 """
83 grouped: dict[str, list[ThemeMetadata]] = {}
84 for theme in self.themes.values():
85 grouped.setdefault(theme.group, []).append(theme)
86 # Sort themes within each group by label
87 for group in grouped:
88 grouped[group].sort(key=lambda t: t.label)
89 return grouped
91 def get_combined_css(self) -> str:
92 """Generate combined CSS from all theme files.
94 Strips frontmatter and concatenates all theme CSS.
96 Returns:
97 Combined CSS string
98 """
99 css_parts: list[str] = []
101 # Add header comment
102 css_parts.append(
103 "/* Auto-generated theme CSS - Do not edit directly */"
104 )
105 css_parts.append(
106 f"/* Generated from {len(self.themes)} theme files */\n"
107 )
109 # Process themes in group order for predictable output
110 group_order = ["core", "nature", "dev", "research", "other"]
111 for group in group_order:
112 group_themes = [t for t in self.themes.values() if t.group == group]
113 if not group_themes:
114 continue
116 css_parts.append(
117 f"\n/* === {GROUP_LABELS.get(group, group)} === */\n"
118 )
120 for theme in sorted(group_themes, key=lambda t: t.label):
121 if theme.css_path and theme.css_path.exists(): 121 ↛ 120line 121 didn't jump to line 120 because the condition on line 121 was always true
122 content = self._loader.get_css_content(theme)
123 if content: 123 ↛ 120line 123 didn't jump to line 120 because the condition on line 123 was always true
124 css_parts.append(f"/* Theme: {theme.label} */")
125 css_parts.append(content)
126 css_parts.append("") # Empty line between themes
128 return "\n".join(css_parts)
130 def get_themes_json(self) -> Markup:
131 """Get theme ID list as JSON for JavaScript.
133 Returns:
134 Markup-safe JSON array string
135 """
136 return Markup(json.dumps(self.get_theme_ids()))
138 def get_metadata_json(self) -> Markup:
139 """Get full theme metadata as JSON for JavaScript.
141 Returns:
142 Markup-safe JSON object string with theme metadata
143 """
144 metadata = {}
145 for theme_id, theme in self.themes.items():
146 metadata[theme_id] = theme.to_dict()
147 return Markup(json.dumps(metadata))
149 def get_settings_options(self) -> list[dict]:
150 """Generate options list for settings UI.
152 Returns:
153 List of {label, value} dicts suitable for default_settings.json format
154 """
155 options = []
156 for theme in self.themes.values():
157 options.append(
158 {
159 "label": theme.label,
160 "value": theme.id,
161 }
162 )
163 return sorted(options, key=lambda x: x["label"])
165 def get_grouped_settings_options(self) -> list[dict]:
166 """Generate grouped options for settings UI with optgroup support.
168 Returns:
169 List of {label, value, group} dicts
170 """
171 options = []
172 for theme in self.themes.values():
173 options.append(
174 {
175 "label": theme.label,
176 "value": theme.id,
177 "group": GROUP_LABELS.get(theme.group, theme.group),
178 }
179 )
180 return sorted(options, key=lambda x: (x["group"], x["label"]))
182 def is_valid_theme(self, theme_id: str) -> bool:
183 """Check if a theme ID is valid.
185 Args:
186 theme_id: The theme identifier to validate
188 Returns:
189 True if theme exists, False otherwise
190 """
191 return theme_id in self.themes
193 def clear_cache(self) -> None:
194 """Clear theme cache (useful for development/hot-reload)."""
195 self._loader.clear_cache()
196 logger.debug("Theme cache cleared")
199# Singleton instance
200theme_registry = ThemeRegistry()
203# Flask integration functions (for backward compatibility with theme_helper pattern)
204def get_themes() -> list[str]:
205 """Get list of available theme names.
207 Returns:
208 Sorted list of theme ID strings
209 """
210 return theme_registry.get_theme_ids()
213def get_themes_json() -> Markup:
214 """Get themes as JSON string for embedding in HTML.
216 Returns:
217 Markup-safe JSON array of theme IDs
218 """
219 return theme_registry.get_themes_json()
222def get_theme_metadata() -> Markup:
223 """Get full theme metadata as JSON for JavaScript.
225 Returns:
226 Markup-safe JSON object with theme metadata
227 """
228 return theme_registry.get_metadata_json()
231__all__ = [
232 "ThemeRegistry",
233 "ThemeMetadata",
234 "theme_registry",
235 "get_themes",
236 "get_themes_json",
237 "get_theme_metadata",
238 "THEMES_DIR",
239 "GROUP_LABELS",
240]