lightfall-utils 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lightfall_utils/__init__.py +3 -0
- lightfall_utils/_version.py +24 -0
- lightfall_utils/ca/__init__.py +9 -0
- lightfall_utils/ca/context.py +100 -0
- lightfall_utils/ca/pv.py +307 -0
- lightfall_utils/caproto_shutdown.py +89 -0
- lightfall_utils/config/__init__.py +12 -0
- lightfall_utils/config/layers.py +317 -0
- lightfall_utils/config/manager.py +246 -0
- lightfall_utils/log_buffer.py +231 -0
- lightfall_utils/logging.py +235 -0
- lightfall_utils/py.typed +0 -0
- lightfall_utils/qt_affinity.py +92 -0
- lightfall_utils/theming/__init__.py +30 -0
- lightfall_utils/theming/builtin.py +737 -0
- lightfall_utils/theming/manager.py +1030 -0
- lightfall_utils/theming/provider.py +102 -0
- lightfall_utils/theming/registry.py +195 -0
- lightfall_utils/threads.py +1071 -0
- lightfall_utils-0.1.0.dist-info/METADATA +54 -0
- lightfall_utils-0.1.0.dist-info/RECORD +24 -0
- lightfall_utils-0.1.0.dist-info/WHEEL +4 -0
- lightfall_utils-0.1.0.dist-info/licenses/LEGAL.md +13 -0
- lightfall_utils-0.1.0.dist-info/licenses/LICENSE.md +33 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Theme token definitions and the theme-provider interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ThemeDefinition:
|
|
12
|
+
"""Color and style definitions for a theme.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
primary: Primary brand/accent color.
|
|
16
|
+
secondary: Secondary accent color.
|
|
17
|
+
success: Success/positive state color.
|
|
18
|
+
warning: Warning state color.
|
|
19
|
+
error: Error/danger state color.
|
|
20
|
+
info: Informational state color.
|
|
21
|
+
background: Main background color.
|
|
22
|
+
surface: Elevated surface color.
|
|
23
|
+
text: Primary text color.
|
|
24
|
+
text_secondary: Secondary/muted text color.
|
|
25
|
+
border: Border/divider color.
|
|
26
|
+
connected: Connected state color (defaults to success).
|
|
27
|
+
disconnected: Disconnected state color (defaults to error).
|
|
28
|
+
css_overrides: Optional CSS rules to append after base stylesheet.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
primary: str = "#2563eb" # Blue
|
|
32
|
+
secondary: str = "#7c3aed" # Purple
|
|
33
|
+
success: str = "#16a34a" # Green
|
|
34
|
+
warning: str = "#d97706" # Amber
|
|
35
|
+
error: str = "#dc2626" # Red
|
|
36
|
+
info: str = "#0891b2" # Cyan
|
|
37
|
+
|
|
38
|
+
background: str = "#ffffff"
|
|
39
|
+
surface: str = "#f3f4f6"
|
|
40
|
+
text: str = "#1f2937"
|
|
41
|
+
text_secondary: str = "#6b7280"
|
|
42
|
+
border: str = "#e5e7eb"
|
|
43
|
+
|
|
44
|
+
connected: str = ""
|
|
45
|
+
disconnected: str = ""
|
|
46
|
+
|
|
47
|
+
# Islands layout: "sea" is the visible gap behind floating panels.
|
|
48
|
+
# When empty, falls back to background (non-Islands themes unchanged).
|
|
49
|
+
sea: str = ""
|
|
50
|
+
|
|
51
|
+
css_overrides: str = ""
|
|
52
|
+
|
|
53
|
+
def __post_init__(self) -> None:
|
|
54
|
+
"""Set default state colors based on theme."""
|
|
55
|
+
if not self.connected:
|
|
56
|
+
self.connected = self.success
|
|
57
|
+
if not self.disconnected:
|
|
58
|
+
self.disconnected = self.error
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ThemeProvider(ABC):
|
|
62
|
+
"""A named theme that supplies a ThemeDefinition.
|
|
63
|
+
|
|
64
|
+
Framework-free: host applications may mix this into their own plugin
|
|
65
|
+
base classes (Lightfall mixes it with its PluginType).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def name(self) -> str:
|
|
71
|
+
"""Unique identifier (lowercase, no spaces, e.g. "slate")."""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def display_name(self) -> str:
|
|
77
|
+
"""Human-readable name shown in a theme selector."""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def is_dark(self) -> bool:
|
|
83
|
+
"""Whether this is a dark theme (used for system-theme detection)."""
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def get_theme_definition(self) -> ThemeDefinition:
|
|
88
|
+
"""Get the theme's color definitions."""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
def get_introspection_data(self) -> dict[str, Any]:
|
|
92
|
+
"""Machine-readable summary of this theme."""
|
|
93
|
+
definition = self.get_theme_definition()
|
|
94
|
+
return {
|
|
95
|
+
"type": getattr(self, "type_name", "theme"),
|
|
96
|
+
"name": self.name,
|
|
97
|
+
"display_name": self.display_name,
|
|
98
|
+
"is_dark": self.is_dark,
|
|
99
|
+
"has_css_overrides": bool(definition.css_overrides),
|
|
100
|
+
"class": self.__class__.__name__,
|
|
101
|
+
"module": self.__class__.__module__,
|
|
102
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Theme registry for managing registered theme plugins.
|
|
2
|
+
|
|
3
|
+
ThemeRegistry is a singleton that manages all registered theme plugins.
|
|
4
|
+
It provides lookup by name and returns available themes for the UI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import threading
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from lightfall_utils.logging import logger
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from lightfall_utils.theming.provider import ThemeDefinition, ThemeProvider
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ThemeRegistry:
|
|
19
|
+
"""Singleton registry for theme plugins.
|
|
20
|
+
|
|
21
|
+
The registry manages registered theme plugins and provides:
|
|
22
|
+
- Registration of theme plugins
|
|
23
|
+
- Lookup by theme name
|
|
24
|
+
- List of available themes for UI
|
|
25
|
+
- Default theme selection based on system preference
|
|
26
|
+
|
|
27
|
+
Example::
|
|
28
|
+
|
|
29
|
+
registry = ThemeRegistry.get_instance()
|
|
30
|
+
|
|
31
|
+
# Register a theme
|
|
32
|
+
registry.register(my_theme_plugin)
|
|
33
|
+
|
|
34
|
+
# Get a theme by name
|
|
35
|
+
theme = registry.get("slate")
|
|
36
|
+
|
|
37
|
+
# Get all themes for UI dropdown
|
|
38
|
+
themes = registry.get_all()
|
|
39
|
+
|
|
40
|
+
# Get default theme based on system
|
|
41
|
+
theme = registry.get_theme_for_system(is_dark=True)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
_instance: ThemeRegistry | None = None
|
|
45
|
+
_lock = threading.RLock()
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
"""Initialize the theme registry."""
|
|
49
|
+
self._themes: dict[str, ThemeProvider] = {}
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def get_instance(cls) -> ThemeRegistry:
|
|
53
|
+
"""Get the singleton ThemeRegistry instance."""
|
|
54
|
+
if cls._instance is None:
|
|
55
|
+
with cls._lock:
|
|
56
|
+
if cls._instance is None:
|
|
57
|
+
cls._instance = cls()
|
|
58
|
+
return cls._instance
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def reset(cls) -> None:
|
|
62
|
+
"""Reset the singleton instance (for testing)."""
|
|
63
|
+
with cls._lock:
|
|
64
|
+
cls._instance = None
|
|
65
|
+
|
|
66
|
+
def register(self, plugin: ThemeProvider) -> bool:
|
|
67
|
+
"""Register a theme plugin.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
plugin: The theme plugin to register.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if registered successfully, False if name already exists.
|
|
74
|
+
"""
|
|
75
|
+
name = plugin.name
|
|
76
|
+
if name in self._themes:
|
|
77
|
+
logger.warning("Theme '{}' already registered, skipping", name)
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
self._themes[name] = plugin
|
|
81
|
+
logger.debug("Registered theme: {} ({})", name, plugin.display_name)
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def unregister(self, name: str) -> bool:
|
|
85
|
+
"""Unregister a theme by name.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
name: Theme name to unregister.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if unregistered, False if not found.
|
|
92
|
+
"""
|
|
93
|
+
if name in self._themes:
|
|
94
|
+
del self._themes[name]
|
|
95
|
+
logger.debug("Unregistered theme: {}", name)
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def get(self, name: str) -> ThemeProvider | None:
|
|
100
|
+
"""Get a theme plugin by name.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
name: The theme name (e.g., "light", "slate").
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The theme plugin or None if not found.
|
|
107
|
+
"""
|
|
108
|
+
return self._themes.get(name)
|
|
109
|
+
|
|
110
|
+
def get_definition(self, name: str) -> ThemeDefinition | None:
|
|
111
|
+
"""Get a theme definition by name.
|
|
112
|
+
|
|
113
|
+
Convenience method that returns the ThemeDefinition directly.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
name: The theme name.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The theme definition or None if not found.
|
|
120
|
+
"""
|
|
121
|
+
plugin = self.get(name)
|
|
122
|
+
if plugin:
|
|
123
|
+
return plugin.get_theme_definition()
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
def get_all(self) -> list[ThemeProvider]:
|
|
127
|
+
"""Get all registered themes.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
List of theme plugins, sorted by display name.
|
|
131
|
+
"""
|
|
132
|
+
return sorted(self._themes.values(), key=lambda t: t.display_name)
|
|
133
|
+
|
|
134
|
+
def get_names(self) -> list[str]:
|
|
135
|
+
"""Get all registered theme names.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of theme names.
|
|
139
|
+
"""
|
|
140
|
+
return list(self._themes.keys())
|
|
141
|
+
|
|
142
|
+
def get_theme_for_system(self, is_dark: bool) -> ThemeProvider | None:
|
|
143
|
+
"""Get the default theme based on system preference.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
is_dark: True if system is in dark mode.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The first matching theme (dark or light), or None if no themes registered.
|
|
150
|
+
"""
|
|
151
|
+
# Find themes matching the requested mode
|
|
152
|
+
matching = [t for t in self._themes.values() if t.is_dark == is_dark]
|
|
153
|
+
|
|
154
|
+
if matching:
|
|
155
|
+
# Prefer "light" or "slate" as defaults, otherwise first match
|
|
156
|
+
for preferred in ("light", "slate"):
|
|
157
|
+
for theme in matching:
|
|
158
|
+
if theme.name == preferred:
|
|
159
|
+
return theme
|
|
160
|
+
return matching[0]
|
|
161
|
+
|
|
162
|
+
# Fall back to any theme
|
|
163
|
+
if self._themes:
|
|
164
|
+
return next(iter(self._themes.values()))
|
|
165
|
+
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def has_theme(self, name: str) -> bool:
|
|
169
|
+
"""Check if a theme is registered.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
name: Theme name to check.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
True if the theme exists.
|
|
176
|
+
"""
|
|
177
|
+
return name in self._themes
|
|
178
|
+
|
|
179
|
+
def get_introspection_data(self) -> dict[str, Any]:
|
|
180
|
+
"""Get introspection data for MCP tools.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Dictionary with registry information.
|
|
184
|
+
"""
|
|
185
|
+
return {
|
|
186
|
+
"registered_themes": [
|
|
187
|
+
{
|
|
188
|
+
"name": t.name,
|
|
189
|
+
"display_name": t.display_name,
|
|
190
|
+
"is_dark": t.is_dark,
|
|
191
|
+
}
|
|
192
|
+
for t in self.get_all()
|
|
193
|
+
],
|
|
194
|
+
"count": len(self._themes),
|
|
195
|
+
}
|