-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathui.py
More file actions
443 lines (357 loc) · 14.3 KB
/
Copy pathui.py
File metadata and controls
443 lines (357 loc) · 14.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
"""
UI Plugin SDK
==============
SDK for creating custom UI extension plugins.
UI plugins can:
- Add custom components to the Electron frontend
- Extend sidebar navigation with new items
- Add settings panels
- Create dashboard widgets
- Provide IPC handlers for frontend-backend communication
"""
from __future__ import annotations
import logging
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any
from ..base import PluginBase, PluginMetadata, PluginType
logger = logging.getLogger(__name__)
class UIExtensionPoint(str, Enum):
"""
Extension points where UI plugins can inject components.
These define where in the Electron frontend a plugin can add UI elements.
"""
SIDEBAR = "sidebar" # Add items to sidebar navigation
SETTINGS = "settings" # Add panels to settings page
DASHBOARD = "dashboard" # Add widgets to dashboard
TOOLBAR = "toolbar" # Add buttons to main toolbar
CONTEXT_MENU = "context_menu" # Add items to context menus
STATUS_BAR = "status_bar" # Add items to status bar
@dataclass
class UIComponentDefinition:
"""
Definition of a UI component provided by a plugin.
This describes a React component that should be loaded and rendered
in the Electron frontend at a specific extension point.
Attributes:
id: Unique identifier for this component
extension_point: Where to render this component
title: Display title for the component
icon: Optional icon name (from icon library)
component_path: Path to the React component file (relative to plugin directory)
route: Optional route path for navigation (e.g., "/my-plugin")
order: Optional display order (lower numbers appear first)
props: Optional default props to pass to the component
"""
id: str
extension_point: UIExtensionPoint
title: str
icon: str | None = None
component_path: str | None = None
route: str | None = None
order: int = 100
props: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for serialization."""
return {
"id": self.id,
"extension_point": self.extension_point.value
if isinstance(self.extension_point, UIExtensionPoint)
else self.extension_point,
"title": self.title,
"icon": self.icon,
"component_path": self.component_path,
"route": self.route,
"order": self.order,
"props": self.props,
}
@dataclass
class UIContext:
"""
Context information provided to UI plugins.
This context is passed to plugin hooks to provide information about
the current frontend state, project, and plugin configuration.
Attributes:
project_dir: Root directory of the project being worked on
plugin_dir: Directory containing the plugin files
config: Optional plugin-specific configuration
state: Optional persistent state data for this plugin
metadata: Optional additional metadata as key-value pairs
"""
project_dir: Path
plugin_dir: Path
config: dict[str, Any] = field(default_factory=dict)
state: dict[str, Any] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
@property
def project_name(self) -> str:
"""Get the project directory name."""
return self.project_dir.name
@property
def plugin_name(self) -> str:
"""Get the plugin directory name."""
return self.plugin_dir.name
def get_config(self, key: str, default: Any = None) -> Any:
"""
Get a configuration value by key.
Args:
key: Configuration key to look up
default: Default value if key not found
Returns:
Configuration value or default
"""
return self.config.get(key, default)
def get_state(self, key: str, default: Any = None) -> Any:
"""
Get a state value by key.
Args:
key: State key to look up
default: Default value if key not found
Returns:
State value or default
"""
return self.state.get(key, default)
def set_state(self, key: str, value: Any) -> None:
"""
Set a state value.
Args:
key: State key to set
value: Value to store
"""
self.state[key] = value
class UIPlugin(PluginBase):
"""
Base class for UI extension plugins.
UI plugins extend the Electron frontend by providing React components,
IPC handlers, and frontend-backend communication channels.
Key capabilities:
- Register UI components at extension points
- Provide IPC handlers for frontend communication
- Manage plugin-specific state and configuration
- Access project and plugin directories
Example:
```python
class MyUIPlugin(UIPlugin):
def __init__(self, metadata: PluginMetadata):
super().__init__(metadata)
self.data_cache = {}
def on_load(self):
# Validate plugin structure
if not (self.plugin_dir / "components").exists():
raise ValueError("Plugin missing components directory")
logger.info("MyUIPlugin loaded")
def on_enable(self):
# Register UI components and IPC handlers
logger.info("MyUIPlugin enabled")
def on_disable(self):
# Unregister components and handlers
logger.info("MyUIPlugin disabled")
def on_unload(self):
# Cleanup
self.data_cache.clear()
def get_ui_components(self, context: UIContext) -> list[UIComponentDefinition]:
# Define UI components to inject
return [
UIComponentDefinition(
id="my-dashboard-widget",
extension_point=UIExtensionPoint.DASHBOARD,
title="My Widget",
icon="chart-bar",
component_path="components/DashboardWidget.tsx",
order=10,
),
UIComponentDefinition(
id="my-settings-panel",
extension_point=UIExtensionPoint.SETTINGS,
title="My Plugin Settings",
icon="cog",
component_path="components/SettingsPanel.tsx",
route="/settings/my-plugin",
order=20,
),
]
def register_ipc_handlers(self, context: UIContext) -> dict[str, Callable]:
# Register IPC handlers for frontend communication
def get_data(event, request_id: str):
# Handle frontend request
return {"status": "success", "data": self.data_cache}
def update_data(event, key: str, value: Any):
# Update plugin data
self.data_cache[key] = value
return {"status": "success"}
# Return mapping of IPC channel names to handler functions
return {
f"{self.name}:get-data": get_data,
f"{self.name}:update-data": update_data,
}
def get_frontend_assets(self, context: UIContext) -> list[str]:
# Return paths to frontend assets (CSS, JS bundles)
return [
str(self.plugin_dir / "assets" / "styles.css"),
str(self.plugin_dir / "dist" / "bundle.js"),
]
```
"""
def __init__(self, metadata: PluginMetadata):
"""
Initialize UI plugin.
Args:
metadata: Plugin metadata from plugin.json
Raises:
ValueError: If plugin_type is not UI
"""
if metadata.plugin_type != PluginType.UI:
raise ValueError(
f"UIPlugin requires plugin_type=UI, got {metadata.plugin_type}"
)
super().__init__(metadata)
self.plugin_dir: Path | None = None
self._ipc_handlers: dict[str, Callable] = {}
logger.debug(f"UIPlugin initialized: {metadata.name}")
def set_plugin_dir(self, plugin_dir: Path) -> None:
"""
Set the plugin directory.
This is called by the plugin loader after instantiation to provide
the plugin with its installation directory.
Args:
plugin_dir: Path to the plugin directory
"""
self.plugin_dir = plugin_dir
def get_ui_components(self, context: UIContext) -> list[UIComponentDefinition]:
"""
Get UI components provided by this plugin.
Override this method to declare which UI components your plugin provides
and where they should be rendered in the Electron frontend.
Args:
context: UI context with project and plugin information
Returns:
List of UI component definitions
Example:
```python
def get_ui_components(self, context: UIContext) -> list[UIComponentDefinition]:
return [
UIComponentDefinition(
id="my-sidebar-item",
extension_point=UIExtensionPoint.SIDEBAR,
title="My Tool",
icon="puzzle",
route="/my-tool",
order=50,
),
]
```
"""
return []
def register_ipc_handlers(self, context: UIContext) -> dict[str, Callable]:
"""
Register IPC handlers for frontend-backend communication.
Override this method to provide IPC handlers that the frontend can call.
Each handler receives the IPC event and any arguments passed from the frontend.
Handler functions should:
- Accept (event, *args, **kwargs) parameters
- Return JSON-serializable data or None
- Handle errors gracefully
- Be lightweight (offload heavy work to background threads)
Args:
context: UI context with project and plugin information
Returns:
Dictionary mapping IPC channel names to handler functions
Example:
```python
def register_ipc_handlers(self, context: UIContext) -> dict[str, Callable]:
def handle_action(event, action_name: str, params: dict):
# Process action
result = self.perform_action(action_name, params)
return {"success": True, "result": result}
return {
f"{self.name}:perform-action": handle_action,
}
```
Note:
IPC channel names should be prefixed with your plugin name to avoid
conflicts with other plugins (e.g., "my-plugin:do-something").
"""
return {}
def get_frontend_assets(self, context: UIContext) -> list[str]:
"""
Get paths to frontend assets (CSS, JS, images) for this plugin.
Override this method to provide paths to static assets that should be
loaded in the frontend. Paths should be absolute or relative to the
plugin directory.
Args:
context: UI context with project and plugin information
Returns:
List of asset file paths
Example:
```python
def get_frontend_assets(self, context: UIContext) -> list[str]:
return [
str(self.plugin_dir / "assets" / "styles.css"),
str(self.plugin_dir / "dist" / "main.js"),
]
```
"""
return []
def on_frontend_ready(self, context: UIContext) -> None:
"""
Called when the Electron frontend has loaded and is ready.
Override this to:
- Initialize frontend state
- Send initial data to frontend
- Start background tasks that update UI
Args:
context: UI context with project and plugin information
"""
pass
def on_window_focus(self, context: UIContext, focused: bool) -> None:
"""
Called when the Electron window gains or loses focus.
Override this to:
- Pause/resume background updates
- Sync state when window regains focus
- Update UI based on focus state
Args:
context: UI context with project and plugin information
focused: True if window gained focus, False if lost focus
"""
pass
def save_state(self, context: UIContext) -> None:
"""
Save plugin state to disk.
This is a helper method for persisting UI state across sessions.
State is saved to the plugin directory.
Args:
context: UI context with state to save
"""
import json
if not self.plugin_dir:
logger.warning(f"Cannot save state for {self.name}: plugin_dir not set")
return
state_file = self.plugin_dir / ".state.json"
try:
with open(state_file, "w", encoding="utf-8") as f:
json.dump(context.state, f, indent=2)
except (OSError, UnicodeEncodeError) as e:
logger.error(f"Failed to save UI plugin state: {e}")
def load_state(self, context: UIContext) -> None:
"""
Load plugin state from disk.
This is a helper method for loading persisted UI state.
Args:
context: UI context to populate with loaded state
"""
import json
if not self.plugin_dir:
logger.warning(f"Cannot load state for {self.name}: plugin_dir not set")
return
state_file = self.plugin_dir / ".state.json"
if not state_file.exists():
return
try:
with open(state_file, encoding="utf-8") as f:
loaded_state = json.load(f)
context.state.update(loaded_state)
except (OSError, json.JSONDecodeError, UnicodeDecodeError) as e:
logger.error(f"Failed to load UI plugin state: {e}")