kollabor 0.4.9__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.
- core/__init__.py +18 -0
- core/application.py +578 -0
- core/cli.py +193 -0
- core/commands/__init__.py +43 -0
- core/commands/executor.py +277 -0
- core/commands/menu_renderer.py +319 -0
- core/commands/parser.py +186 -0
- core/commands/registry.py +331 -0
- core/commands/system_commands.py +479 -0
- core/config/__init__.py +7 -0
- core/config/llm_task_config.py +110 -0
- core/config/loader.py +501 -0
- core/config/manager.py +112 -0
- core/config/plugin_config_manager.py +346 -0
- core/config/plugin_schema.py +424 -0
- core/config/service.py +399 -0
- core/effects/__init__.py +1 -0
- core/events/__init__.py +12 -0
- core/events/bus.py +129 -0
- core/events/executor.py +154 -0
- core/events/models.py +258 -0
- core/events/processor.py +176 -0
- core/events/registry.py +289 -0
- core/fullscreen/__init__.py +19 -0
- core/fullscreen/command_integration.py +290 -0
- core/fullscreen/components/__init__.py +12 -0
- core/fullscreen/components/animation.py +258 -0
- core/fullscreen/components/drawing.py +160 -0
- core/fullscreen/components/matrix_components.py +177 -0
- core/fullscreen/manager.py +302 -0
- core/fullscreen/plugin.py +204 -0
- core/fullscreen/renderer.py +282 -0
- core/fullscreen/session.py +324 -0
- core/io/__init__.py +52 -0
- core/io/buffer_manager.py +362 -0
- core/io/config_status_view.py +272 -0
- core/io/core_status_views.py +410 -0
- core/io/input_errors.py +313 -0
- core/io/input_handler.py +2655 -0
- core/io/input_mode_manager.py +402 -0
- core/io/key_parser.py +344 -0
- core/io/layout.py +587 -0
- core/io/message_coordinator.py +204 -0
- core/io/message_renderer.py +601 -0
- core/io/modal_interaction_handler.py +315 -0
- core/io/raw_input_processor.py +946 -0
- core/io/status_renderer.py +845 -0
- core/io/terminal_renderer.py +586 -0
- core/io/terminal_state.py +551 -0
- core/io/visual_effects.py +734 -0
- core/llm/__init__.py +26 -0
- core/llm/api_communication_service.py +863 -0
- core/llm/conversation_logger.py +473 -0
- core/llm/conversation_manager.py +414 -0
- core/llm/file_operations_executor.py +1401 -0
- core/llm/hook_system.py +402 -0
- core/llm/llm_service.py +1629 -0
- core/llm/mcp_integration.py +386 -0
- core/llm/message_display_service.py +450 -0
- core/llm/model_router.py +214 -0
- core/llm/plugin_sdk.py +396 -0
- core/llm/response_parser.py +848 -0
- core/llm/response_processor.py +364 -0
- core/llm/tool_executor.py +520 -0
- core/logging/__init__.py +19 -0
- core/logging/setup.py +208 -0
- core/models/__init__.py +5 -0
- core/models/base.py +23 -0
- core/plugins/__init__.py +13 -0
- core/plugins/collector.py +212 -0
- core/plugins/discovery.py +386 -0
- core/plugins/factory.py +263 -0
- core/plugins/registry.py +152 -0
- core/storage/__init__.py +5 -0
- core/storage/state_manager.py +84 -0
- core/ui/__init__.py +6 -0
- core/ui/config_merger.py +176 -0
- core/ui/config_widgets.py +369 -0
- core/ui/live_modal_renderer.py +276 -0
- core/ui/modal_actions.py +162 -0
- core/ui/modal_overlay_renderer.py +373 -0
- core/ui/modal_renderer.py +591 -0
- core/ui/modal_state_manager.py +443 -0
- core/ui/widget_integration.py +222 -0
- core/ui/widgets/__init__.py +27 -0
- core/ui/widgets/base_widget.py +136 -0
- core/ui/widgets/checkbox.py +85 -0
- core/ui/widgets/dropdown.py +140 -0
- core/ui/widgets/label.py +78 -0
- core/ui/widgets/slider.py +185 -0
- core/ui/widgets/text_input.py +224 -0
- core/utils/__init__.py +11 -0
- core/utils/config_utils.py +656 -0
- core/utils/dict_utils.py +212 -0
- core/utils/error_utils.py +275 -0
- core/utils/key_reader.py +171 -0
- core/utils/plugin_utils.py +267 -0
- core/utils/prompt_renderer.py +151 -0
- kollabor-0.4.9.dist-info/METADATA +298 -0
- kollabor-0.4.9.dist-info/RECORD +128 -0
- kollabor-0.4.9.dist-info/WHEEL +5 -0
- kollabor-0.4.9.dist-info/entry_points.txt +2 -0
- kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
- kollabor-0.4.9.dist-info/top_level.txt +4 -0
- kollabor_cli_main.py +20 -0
- plugins/__init__.py +1 -0
- plugins/enhanced_input/__init__.py +18 -0
- plugins/enhanced_input/box_renderer.py +103 -0
- plugins/enhanced_input/box_styles.py +142 -0
- plugins/enhanced_input/color_engine.py +165 -0
- plugins/enhanced_input/config.py +150 -0
- plugins/enhanced_input/cursor_manager.py +72 -0
- plugins/enhanced_input/geometry.py +81 -0
- plugins/enhanced_input/state.py +130 -0
- plugins/enhanced_input/text_processor.py +115 -0
- plugins/enhanced_input_plugin.py +385 -0
- plugins/fullscreen/__init__.py +9 -0
- plugins/fullscreen/example_plugin.py +327 -0
- plugins/fullscreen/matrix_plugin.py +132 -0
- plugins/hook_monitoring_plugin.py +1299 -0
- plugins/query_enhancer_plugin.py +350 -0
- plugins/save_conversation_plugin.py +502 -0
- plugins/system_commands_plugin.py +93 -0
- plugins/tmux_plugin.py +795 -0
- plugins/workflow_enforcement_plugin.py +629 -0
- system_prompt/default.md +1286 -0
- system_prompt/default_win.md +265 -0
- system_prompt/example_with_trender.md +47 -0
core/events/registry.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Hook registry for managing hook registration and lifecycle."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from .models import EventType, Hook, HookStatus
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HookRegistry:
|
|
13
|
+
"""Manages hook registration, organization, and status tracking.
|
|
14
|
+
|
|
15
|
+
This class is responsible for maintaining the registry of hooks,
|
|
16
|
+
organizing them by event type and priority, and tracking their status.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
"""Initialize the hook registry."""
|
|
21
|
+
self.hooks: Dict[EventType, List[Hook]] = defaultdict(list)
|
|
22
|
+
self.hook_status: Dict[str, HookStatus] = {}
|
|
23
|
+
self.hook_metadata: Dict[str, Dict[str, Any]] = {}
|
|
24
|
+
logger.info("HookRegistry initialized")
|
|
25
|
+
|
|
26
|
+
def register_hook(self, hook: Hook) -> bool:
|
|
27
|
+
"""Register a hook with the registry.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
hook: The hook to register.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
True if registration successful, False otherwise.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
hook_key = f"{hook.plugin_name}.{hook.name}"
|
|
37
|
+
|
|
38
|
+
# Check for duplicate registration
|
|
39
|
+
if hook_key in self.hook_status:
|
|
40
|
+
logger.warning(f"Hook {hook_key} already registered, updating registration")
|
|
41
|
+
self._remove_hook_from_lists(hook_key)
|
|
42
|
+
|
|
43
|
+
# Add hook to appropriate event type list
|
|
44
|
+
self.hooks[hook.event_type].append(hook)
|
|
45
|
+
|
|
46
|
+
# Sort hooks by priority (highest first)
|
|
47
|
+
self.hooks[hook.event_type].sort(key=lambda h: h.priority, reverse=True)
|
|
48
|
+
|
|
49
|
+
# Track hook status and metadata
|
|
50
|
+
self.hook_status[hook_key] = HookStatus.PENDING
|
|
51
|
+
self.hook_metadata[hook_key] = {
|
|
52
|
+
"event_type": hook.event_type.value,
|
|
53
|
+
"priority": hook.priority,
|
|
54
|
+
"plugin_name": hook.plugin_name,
|
|
55
|
+
"enabled": hook.enabled,
|
|
56
|
+
"timeout": hook.timeout,
|
|
57
|
+
"error_action": hook.error_action,
|
|
58
|
+
"registration_order": len(self.hook_status)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
logger.debug(f"Registered hook: {hook_key} for {hook.event_type.value} (priority: {hook.priority})")
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Failed to register hook {hook.plugin_name}.{hook.name}: {e}")
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def unregister_hook(self, plugin_name: str, hook_name: str) -> bool:
|
|
69
|
+
"""Unregister a hook from the registry.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
plugin_name: Name of the plugin that owns the hook.
|
|
73
|
+
hook_name: Name of the hook.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if unregistration successful, False otherwise.
|
|
77
|
+
"""
|
|
78
|
+
hook_key = f"{plugin_name}.{hook_name}"
|
|
79
|
+
|
|
80
|
+
if hook_key not in self.hook_status:
|
|
81
|
+
logger.warning(f"Hook {hook_key} not found for unregistration")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Remove from hook lists
|
|
86
|
+
removed = self._remove_hook_from_lists(hook_key)
|
|
87
|
+
|
|
88
|
+
if removed:
|
|
89
|
+
# Clean up status and metadata
|
|
90
|
+
del self.hook_status[hook_key]
|
|
91
|
+
del self.hook_metadata[hook_key]
|
|
92
|
+
logger.info(f"Unregistered hook: {hook_key}")
|
|
93
|
+
return True
|
|
94
|
+
else:
|
|
95
|
+
logger.warning(f"Hook {hook_key} not found in any event type lists")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Failed to unregister hook {hook_key}: {e}")
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def get_hooks_for_event(self, event_type: EventType) -> List[Hook]:
|
|
103
|
+
"""Get all hooks registered for a specific event type.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
event_type: The event type to get hooks for.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of hooks sorted by priority (highest first).
|
|
110
|
+
"""
|
|
111
|
+
hooks = self.hooks.get(event_type, [])
|
|
112
|
+
|
|
113
|
+
# Filter out disabled hooks
|
|
114
|
+
enabled_hooks = [hook for hook in hooks if hook.enabled]
|
|
115
|
+
|
|
116
|
+
#logger.debug(f"Retrieved {len(enabled_hooks)} enabled hooks for {event_type.value}")
|
|
117
|
+
return enabled_hooks
|
|
118
|
+
|
|
119
|
+
def update_hook_status(self, hook_key: str, status: str) -> bool:
|
|
120
|
+
"""Update the status of a registered hook.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
hook_key: Key identifying the hook (plugin_name.hook_name).
|
|
124
|
+
status: New status for the hook.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if update successful, False otherwise.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
if hook_key not in self.hook_status:
|
|
131
|
+
logger.warning(f"Cannot update status for unknown hook: {hook_key}")
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
# Convert string status to HookStatus enum
|
|
135
|
+
status_mapping = {
|
|
136
|
+
"pending": HookStatus.PENDING,
|
|
137
|
+
"working": HookStatus.WORKING,
|
|
138
|
+
"completed": HookStatus.COMPLETED,
|
|
139
|
+
"failed": HookStatus.FAILED,
|
|
140
|
+
"timeout": HookStatus.TIMEOUT
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if status not in status_mapping:
|
|
144
|
+
logger.error(f"Invalid status '{status}' for hook {hook_key}")
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
old_status = self.hook_status[hook_key]
|
|
148
|
+
self.hook_status[hook_key] = status_mapping[status]
|
|
149
|
+
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error(f"Failed to update status for hook {hook_key}: {e}")
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
def get_hook_status_summary(self) -> Dict[str, Any]:
|
|
157
|
+
"""Get a summary of all hook statuses.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Dictionary with hook status statistics and details.
|
|
161
|
+
"""
|
|
162
|
+
status_counts = defaultdict(int)
|
|
163
|
+
hook_details = {}
|
|
164
|
+
|
|
165
|
+
for hook_key, status in self.hook_status.items():
|
|
166
|
+
status_counts[status.value] += 1
|
|
167
|
+
|
|
168
|
+
metadata = self.hook_metadata.get(hook_key, {})
|
|
169
|
+
hook_details[hook_key] = {
|
|
170
|
+
"status": status.value,
|
|
171
|
+
"event_type": metadata.get("event_type", "unknown"),
|
|
172
|
+
"priority": metadata.get("priority", 0),
|
|
173
|
+
"enabled": metadata.get("enabled", True),
|
|
174
|
+
"plugin_name": metadata.get("plugin_name", "unknown")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"total_hooks": len(self.hook_status),
|
|
179
|
+
"status_counts": dict(status_counts),
|
|
180
|
+
"hook_details": hook_details
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
def enable_hook(self, plugin_name: str, hook_name: str) -> bool:
|
|
184
|
+
"""Enable a registered hook.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
plugin_name: Name of the plugin that owns the hook.
|
|
188
|
+
hook_name: Name of the hook.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
True if hook was enabled, False otherwise.
|
|
192
|
+
"""
|
|
193
|
+
return self._set_hook_enabled(plugin_name, hook_name, True)
|
|
194
|
+
|
|
195
|
+
def disable_hook(self, plugin_name: str, hook_name: str) -> bool:
|
|
196
|
+
"""Disable a registered hook.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
plugin_name: Name of the plugin that owns the hook.
|
|
200
|
+
hook_name: Name of the hook.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
True if hook was disabled, False otherwise.
|
|
204
|
+
"""
|
|
205
|
+
return self._set_hook_enabled(plugin_name, hook_name, False)
|
|
206
|
+
|
|
207
|
+
def _set_hook_enabled(self, plugin_name: str, hook_name: str, enabled: bool) -> bool:
|
|
208
|
+
"""Set the enabled state of a hook.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
plugin_name: Name of the plugin that owns the hook.
|
|
212
|
+
hook_name: Name of the hook.
|
|
213
|
+
enabled: Whether to enable or disable the hook.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
True if state was changed, False otherwise.
|
|
217
|
+
"""
|
|
218
|
+
hook_key = f"{plugin_name}.{hook_name}"
|
|
219
|
+
|
|
220
|
+
# Find the hook in the registry
|
|
221
|
+
hook_found = False
|
|
222
|
+
for event_type, hooks in self.hooks.items():
|
|
223
|
+
for hook in hooks:
|
|
224
|
+
if f"{hook.plugin_name}.{hook.name}" == hook_key:
|
|
225
|
+
hook.enabled = enabled
|
|
226
|
+
hook_found = True
|
|
227
|
+
|
|
228
|
+
# Update metadata
|
|
229
|
+
if hook_key in self.hook_metadata:
|
|
230
|
+
self.hook_metadata[hook_key]["enabled"] = enabled
|
|
231
|
+
|
|
232
|
+
action = "enabled" if enabled else "disabled"
|
|
233
|
+
logger.info(f"Hook {hook_key} {action}")
|
|
234
|
+
break
|
|
235
|
+
if hook_found:
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
if not hook_found:
|
|
239
|
+
logger.warning(f"Hook {hook_key} not found for enable/disable")
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
return True
|
|
243
|
+
|
|
244
|
+
def _remove_hook_from_lists(self, hook_key: str) -> bool:
|
|
245
|
+
"""Remove a hook from all event type lists.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
hook_key: Key identifying the hook to remove.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
True if hook was found and removed, False otherwise.
|
|
252
|
+
"""
|
|
253
|
+
removed = False
|
|
254
|
+
|
|
255
|
+
for event_type, hooks in self.hooks.items():
|
|
256
|
+
# Find and remove hook from this event type's list
|
|
257
|
+
for i, hook in enumerate(hooks):
|
|
258
|
+
if f"{hook.plugin_name}.{hook.name}" == hook_key:
|
|
259
|
+
del hooks[i]
|
|
260
|
+
removed = True
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
return removed
|
|
264
|
+
|
|
265
|
+
def get_registry_stats(self) -> Dict[str, Any]:
|
|
266
|
+
"""Get comprehensive registry statistics.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Dictionary with detailed registry statistics.
|
|
270
|
+
"""
|
|
271
|
+
event_type_counts = {}
|
|
272
|
+
priority_distribution = defaultdict(int)
|
|
273
|
+
plugin_counts = defaultdict(int)
|
|
274
|
+
|
|
275
|
+
for event_type, hooks in self.hooks.items():
|
|
276
|
+
event_type_counts[event_type.value] = len(hooks)
|
|
277
|
+
|
|
278
|
+
for hook in hooks:
|
|
279
|
+
priority_distribution[hook.priority] += 1
|
|
280
|
+
plugin_counts[hook.plugin_name] += 1
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
"total_hooks": len(self.hook_status),
|
|
284
|
+
"event_types": len(self.hooks),
|
|
285
|
+
"hooks_per_event_type": event_type_counts,
|
|
286
|
+
"priority_distribution": dict(priority_distribution),
|
|
287
|
+
"hooks_per_plugin": dict(plugin_counts),
|
|
288
|
+
"status_summary": self.get_hook_status_summary()
|
|
289
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Full-screen plugin framework for terminal takeover.
|
|
2
|
+
|
|
3
|
+
This module provides a reusable framework for plugins to take complete
|
|
4
|
+
terminal control using the proven modal system architecture.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .manager import FullScreenManager
|
|
8
|
+
from .renderer import FullScreenRenderer
|
|
9
|
+
from .plugin import FullScreenPlugin
|
|
10
|
+
from .session import FullScreenSession
|
|
11
|
+
from .command_integration import FullScreenCommandIntegrator
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"FullScreenManager",
|
|
15
|
+
"FullScreenRenderer",
|
|
16
|
+
"FullScreenPlugin",
|
|
17
|
+
"FullScreenSession",
|
|
18
|
+
"FullScreenCommandIntegrator"
|
|
19
|
+
]
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Fullscreen plugin command integration system.
|
|
2
|
+
|
|
3
|
+
This module handles automatic discovery and registration of slash commands
|
|
4
|
+
for fullscreen plugins, enabling dynamic plugin-to-command mapping.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Optional, Type
|
|
10
|
+
import importlib.util
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from ..events.models import CommandDefinition, CommandMode, CommandCategory
|
|
14
|
+
from ..commands.registry import SlashCommandRegistry
|
|
15
|
+
from .plugin import FullScreenPlugin
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FullScreenCommandIntegrator:
|
|
21
|
+
"""Integrates fullscreen plugins with the slash command system.
|
|
22
|
+
|
|
23
|
+
This class:
|
|
24
|
+
- Discovers fullscreen plugins in plugins/fullscreen/
|
|
25
|
+
- Auto-registers slash commands based on plugin metadata
|
|
26
|
+
- Handles dynamic plugin loading/unloading
|
|
27
|
+
- Maps commands to plugin execution
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, command_registry: SlashCommandRegistry, event_bus):
|
|
31
|
+
"""Initialize the fullscreen command integrator.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
command_registry: Slash command registry for registration
|
|
35
|
+
event_bus: Event bus for communication
|
|
36
|
+
"""
|
|
37
|
+
self.command_registry = command_registry
|
|
38
|
+
self.event_bus = event_bus
|
|
39
|
+
self.registered_plugins: Dict[str, Type[FullScreenPlugin]] = {}
|
|
40
|
+
self.plugin_instances: Dict[str, FullScreenPlugin] = {}
|
|
41
|
+
self._fullscreen_manager = None
|
|
42
|
+
|
|
43
|
+
logger.info("FullScreen command integrator initialized")
|
|
44
|
+
|
|
45
|
+
def discover_and_register_plugins(self, plugins_dir: Path) -> int:
|
|
46
|
+
"""Discover and register all fullscreen plugins.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
plugins_dir: Base plugins directory
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Number of plugins registered
|
|
53
|
+
"""
|
|
54
|
+
fullscreen_dir = plugins_dir / "fullscreen"
|
|
55
|
+
if not fullscreen_dir.exists():
|
|
56
|
+
logger.info("No fullscreen plugins directory found")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
registered_count = 0
|
|
60
|
+
|
|
61
|
+
# Scan for Python files in fullscreen directory
|
|
62
|
+
for plugin_file in fullscreen_dir.glob("*.py"):
|
|
63
|
+
if plugin_file.name.startswith("__"):
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
plugin_class = self._load_plugin_class(plugin_file)
|
|
68
|
+
if plugin_class:
|
|
69
|
+
if self._register_plugin_commands(plugin_class):
|
|
70
|
+
registered_count += 1
|
|
71
|
+
logger.info(f"Registered commands for plugin: {plugin_class.__name__}")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Failed to load plugin {plugin_file.name}: {e}")
|
|
74
|
+
|
|
75
|
+
logger.info(f"Discovered and registered {registered_count} fullscreen plugins")
|
|
76
|
+
return registered_count
|
|
77
|
+
|
|
78
|
+
def _load_plugin_class(self, plugin_file: Path) -> Optional[Type[FullScreenPlugin]]:
|
|
79
|
+
"""Load a plugin class from a Python file.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
plugin_file: Path to the plugin Python file
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Plugin class or None if not found/invalid
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
# Create module spec and load
|
|
89
|
+
module_name = f"plugins.fullscreen.{plugin_file.stem}"
|
|
90
|
+
spec = importlib.util.spec_from_file_location(module_name, plugin_file)
|
|
91
|
+
if not spec or not spec.loader:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
module = importlib.util.module_from_spec(spec)
|
|
95
|
+
sys.modules[module_name] = module
|
|
96
|
+
spec.loader.exec_module(module)
|
|
97
|
+
|
|
98
|
+
# Find FullScreenPlugin subclass
|
|
99
|
+
for attr_name in dir(module):
|
|
100
|
+
attr = getattr(module, attr_name)
|
|
101
|
+
if (isinstance(attr, type) and
|
|
102
|
+
issubclass(attr, FullScreenPlugin) and
|
|
103
|
+
attr != FullScreenPlugin):
|
|
104
|
+
return attr
|
|
105
|
+
|
|
106
|
+
logger.warning(f"No FullScreenPlugin subclass found in {plugin_file.name}")
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Error loading plugin class from {plugin_file}: {e}")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
def _register_plugin_commands(self, plugin_class: Type[FullScreenPlugin]) -> bool:
|
|
114
|
+
"""Register slash commands for a plugin class.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
plugin_class: The plugin class to register commands for
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True if registration successful
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
# Create temporary instance to get metadata
|
|
124
|
+
temp_instance = plugin_class()
|
|
125
|
+
metadata = temp_instance.metadata
|
|
126
|
+
|
|
127
|
+
if not metadata:
|
|
128
|
+
logger.warning(f"Plugin {plugin_class.__name__} has no metadata")
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Store plugin class for later instantiation
|
|
132
|
+
self.registered_plugins[metadata.name] = plugin_class
|
|
133
|
+
|
|
134
|
+
# Register primary command (plugin name)
|
|
135
|
+
primary_command = CommandDefinition(
|
|
136
|
+
name=metadata.name,
|
|
137
|
+
aliases=metadata.aliases or [],
|
|
138
|
+
description=metadata.description,
|
|
139
|
+
category=CommandCategory.SYSTEM,
|
|
140
|
+
mode=CommandMode.INSTANT,
|
|
141
|
+
handler=self._create_plugin_handler(metadata.name),
|
|
142
|
+
icon=metadata.icon,
|
|
143
|
+
plugin_name="fullscreen_integrator"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
success = self.command_registry.register_command(primary_command)
|
|
147
|
+
if not success:
|
|
148
|
+
logger.error(f"Failed to register primary command for {metadata.name}")
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
# Register alias commands if any
|
|
152
|
+
for alias in (metadata.aliases or []):
|
|
153
|
+
alias_command = CommandDefinition(
|
|
154
|
+
name=alias,
|
|
155
|
+
aliases=[],
|
|
156
|
+
description=f"{metadata.description} (alias)",
|
|
157
|
+
category=CommandCategory.SYSTEM,
|
|
158
|
+
mode=CommandMode.INSTANT,
|
|
159
|
+
handler=self._create_plugin_handler(metadata.name),
|
|
160
|
+
icon=metadata.icon,
|
|
161
|
+
plugin_name="fullscreen_integrator"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
alias_success = self.command_registry.register_command(alias_command)
|
|
165
|
+
if alias_success:
|
|
166
|
+
logger.debug(f"Registered alias command: {alias} -> {metadata.name}")
|
|
167
|
+
else:
|
|
168
|
+
logger.warning(f"Failed to register alias command: {alias}")
|
|
169
|
+
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(f"Error registering commands for {plugin_class.__name__}: {e}")
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
def _create_plugin_handler(self, plugin_name: str):
|
|
177
|
+
"""Create a command handler for a specific plugin.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
plugin_name: Name of the plugin to handle
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Async command handler function
|
|
184
|
+
"""
|
|
185
|
+
async def handler(command):
|
|
186
|
+
"""Handle command execution for fullscreen plugin."""
|
|
187
|
+
try:
|
|
188
|
+
# Get or create fullscreen manager
|
|
189
|
+
if not self._fullscreen_manager:
|
|
190
|
+
from . import FullScreenManager
|
|
191
|
+
self._fullscreen_manager = FullScreenManager(self.event_bus, None)
|
|
192
|
+
|
|
193
|
+
# Get or create plugin instance
|
|
194
|
+
if plugin_name not in self.plugin_instances:
|
|
195
|
+
plugin_class = self.registered_plugins.get(plugin_name)
|
|
196
|
+
if not plugin_class:
|
|
197
|
+
raise ValueError(f"Plugin class not found: {plugin_name}")
|
|
198
|
+
|
|
199
|
+
plugin_instance = plugin_class()
|
|
200
|
+
self.plugin_instances[plugin_name] = plugin_instance
|
|
201
|
+
self._fullscreen_manager.register_plugin(plugin_instance)
|
|
202
|
+
logger.debug(f"Created and registered plugin instance: {plugin_name}")
|
|
203
|
+
|
|
204
|
+
# Launch the plugin
|
|
205
|
+
success = await self._fullscreen_manager.launch_plugin(plugin_name)
|
|
206
|
+
|
|
207
|
+
if success:
|
|
208
|
+
from ..events.models import CommandResult
|
|
209
|
+
return CommandResult(
|
|
210
|
+
success=True,
|
|
211
|
+
message="", # No message to avoid display artifacts
|
|
212
|
+
display_type="success"
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
from ..events.models import CommandResult
|
|
216
|
+
return CommandResult(
|
|
217
|
+
success=False,
|
|
218
|
+
message=f"Failed to launch {plugin_name} plugin",
|
|
219
|
+
display_type="error"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logger.error(f"Error executing plugin {plugin_name}: {e}")
|
|
224
|
+
from ..events.models import CommandResult
|
|
225
|
+
return CommandResult(
|
|
226
|
+
success=False,
|
|
227
|
+
message=f"Plugin error: {str(e)}",
|
|
228
|
+
display_type="error"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return handler
|
|
232
|
+
|
|
233
|
+
def unregister_plugin(self, plugin_name: str) -> bool:
|
|
234
|
+
"""Unregister a plugin and its commands.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
plugin_name: Name of plugin to unregister
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
True if successful
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
# Remove from our tracking
|
|
244
|
+
if plugin_name in self.registered_plugins:
|
|
245
|
+
plugin_class = self.registered_plugins[plugin_name]
|
|
246
|
+
temp_instance = plugin_class()
|
|
247
|
+
metadata = temp_instance.metadata
|
|
248
|
+
|
|
249
|
+
# Unregister all commands for this plugin
|
|
250
|
+
self.command_registry.unregister_command(metadata.name)
|
|
251
|
+
for alias in (metadata.aliases or []):
|
|
252
|
+
self.command_registry.unregister_command(alias)
|
|
253
|
+
|
|
254
|
+
del self.registered_plugins[plugin_name]
|
|
255
|
+
|
|
256
|
+
if plugin_name in self.plugin_instances:
|
|
257
|
+
del self.plugin_instances[plugin_name]
|
|
258
|
+
|
|
259
|
+
logger.info(f"Unregistered plugin: {plugin_name}")
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.error(f"Error unregistering plugin {plugin_name}: {e}")
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
def get_registered_plugins(self) -> List[str]:
|
|
269
|
+
"""Get list of registered plugin names.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of plugin names
|
|
273
|
+
"""
|
|
274
|
+
return list(self.registered_plugins.keys())
|
|
275
|
+
|
|
276
|
+
def reload_plugins(self, plugins_dir: Path) -> int:
|
|
277
|
+
"""Reload all fullscreen plugins from directory.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
plugins_dir: Base plugins directory
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Number of plugins reloaded
|
|
284
|
+
"""
|
|
285
|
+
# Unregister all current plugins
|
|
286
|
+
for plugin_name in list(self.registered_plugins.keys()):
|
|
287
|
+
self.unregister_plugin(plugin_name)
|
|
288
|
+
|
|
289
|
+
# Re-discover and register
|
|
290
|
+
return self.discover_and_register_plugins(plugins_dir)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Full-screen plugin components and utilities."""
|
|
2
|
+
|
|
3
|
+
from .drawing import DrawingPrimitives
|
|
4
|
+
from .animation import AnimationFramework
|
|
5
|
+
from .matrix_components import MatrixColumn, MatrixRenderer
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DrawingPrimitives",
|
|
9
|
+
"AnimationFramework",
|
|
10
|
+
"MatrixColumn",
|
|
11
|
+
"MatrixRenderer"
|
|
12
|
+
]
|