248 lines
8.9 KiB
Python
248 lines
8.9 KiB
Python
"""
|
|
EU-Utility - Plugin Manager
|
|
|
|
Handles discovery, loading, and lifecycle of plugins.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import importlib
|
|
import importlib.util
|
|
from pathlib import Path
|
|
from typing import Dict, List, Type, Optional
|
|
|
|
from core.base_plugin import BasePlugin
|
|
|
|
|
|
class PluginManager:
|
|
"""Manages loading and lifecycle of plugins."""
|
|
|
|
PLUGIN_DIRS = [
|
|
"plugins", # Built-in plugins
|
|
"user_plugins", # User-installed plugins
|
|
]
|
|
|
|
def __init__(self, overlay_window):
|
|
self.overlay = overlay_window
|
|
self.plugins: Dict[str, BasePlugin] = {}
|
|
self.plugin_classes: Dict[str, Type[BasePlugin]] = {}
|
|
self.config = self._load_config()
|
|
|
|
# Add plugin dirs to path
|
|
for plugin_dir in self.PLUGIN_DIRS:
|
|
path = Path(plugin_dir).absolute()
|
|
if path.exists() and str(path) not in sys.path:
|
|
sys.path.insert(0, str(path))
|
|
|
|
def _load_config(self) -> dict:
|
|
"""Load plugin configuration."""
|
|
config_path = Path("config/plugins.json")
|
|
if config_path.exists():
|
|
try:
|
|
return json.loads(config_path.read_text())
|
|
except json.JSONDecodeError:
|
|
pass
|
|
# Default: no plugins enabled - all disabled by default
|
|
return {"enabled": [], "settings": {}}
|
|
|
|
def save_config(self) -> None:
|
|
"""Save plugin configuration."""
|
|
config_path = Path("config/plugins.json")
|
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
config_path.write_text(json.dumps(self.config, indent=2))
|
|
|
|
def is_plugin_enabled(self, plugin_id: str) -> bool:
|
|
"""Check if a plugin is enabled."""
|
|
# Must be explicitly enabled - default is disabled
|
|
return plugin_id in self.config.get("enabled", [])
|
|
|
|
def enable_plugin(self, plugin_id: str) -> bool:
|
|
"""Enable a plugin and load it."""
|
|
if plugin_id in self.config.get("enabled", []):
|
|
return True
|
|
|
|
# Add to enabled list
|
|
if "enabled" not in self.config:
|
|
self.config["enabled"] = []
|
|
self.config["enabled"].append(plugin_id)
|
|
self.save_config()
|
|
|
|
# Load the plugin
|
|
plugin_class = self.plugin_classes.get(plugin_id)
|
|
if plugin_class:
|
|
return self.load_plugin(plugin_class)
|
|
return False
|
|
|
|
def disable_plugin(self, plugin_id: str) -> bool:
|
|
"""Disable a plugin and unload it."""
|
|
if "enabled" in self.config and plugin_id in self.config["enabled"]:
|
|
self.config["enabled"].remove(plugin_id)
|
|
self.save_config()
|
|
|
|
# Unload if loaded
|
|
if plugin_id in self.plugins:
|
|
self.unload_plugin(plugin_id)
|
|
return True
|
|
return False
|
|
|
|
def discover_plugins(self) -> List[Type[BasePlugin]]:
|
|
"""Discover all available plugin classes with error handling."""
|
|
discovered = []
|
|
|
|
for plugin_dir in self.PLUGIN_DIRS:
|
|
base_path = Path(plugin_dir)
|
|
if not base_path.exists():
|
|
continue
|
|
|
|
# Find plugin folders
|
|
for item in base_path.iterdir():
|
|
if not item.is_dir():
|
|
continue
|
|
if item.name.startswith("__"):
|
|
continue
|
|
if item.name == "__pycache__":
|
|
continue
|
|
|
|
plugin_file = item / "plugin.py"
|
|
|
|
if not plugin_file.exists():
|
|
continue
|
|
|
|
try:
|
|
# Load the plugin module
|
|
module_name = f"{plugin_dir}.{item.name}.plugin"
|
|
|
|
if module_name in sys.modules:
|
|
module = sys.modules[module_name]
|
|
else:
|
|
spec = importlib.util.spec_from_file_location(
|
|
module_name, plugin_file
|
|
)
|
|
if not spec or not spec.loader:
|
|
continue
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[module_name] = module
|
|
spec.loader.exec_module(module)
|
|
|
|
# Find plugin class
|
|
for attr_name in dir(module):
|
|
attr = getattr(module, attr_name)
|
|
if (isinstance(attr, type) and
|
|
issubclass(attr, BasePlugin) and
|
|
attr != BasePlugin and
|
|
not attr.__name__.startswith("Base")):
|
|
discovered.append(attr)
|
|
break
|
|
|
|
except Exception as e:
|
|
print(f"[PluginManager] Failed to discover {item.name}: {e}")
|
|
continue
|
|
|
|
return discovered
|
|
|
|
def load_plugin(self, plugin_class: Type[BasePlugin]) -> bool:
|
|
"""Instantiate and initialize a plugin with error handling."""
|
|
try:
|
|
plugin_id = f"{plugin_class.__module__}.{plugin_class.__name__}"
|
|
|
|
# Check if already loaded
|
|
if plugin_id in self.plugins:
|
|
return True
|
|
|
|
# Check if disabled
|
|
if plugin_id in self.config.get("disabled", []):
|
|
print(f"[PluginManager] Skipping disabled plugin: {plugin_class.name}")
|
|
return False
|
|
|
|
# Get plugin config
|
|
plugin_config = self.config.get("settings", {}).get(plugin_id, {})
|
|
|
|
# Create instance
|
|
try:
|
|
instance = plugin_class(self.overlay, plugin_config)
|
|
except Exception as e:
|
|
print(f"[PluginManager] Failed to create {plugin_class.name}: {e}")
|
|
return False
|
|
|
|
# Initialize with error handling
|
|
try:
|
|
instance.initialize()
|
|
except Exception as e:
|
|
print(f"[PluginManager] Failed to initialize {plugin_class.name}: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return False
|
|
|
|
# Store
|
|
self.plugins[plugin_id] = instance
|
|
self.plugin_classes[plugin_id] = plugin_class
|
|
|
|
print(f"[PluginManager] ✓ Loaded: {instance.name} v{instance.version}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"[PluginManager] Failed to load {plugin_class.__name__}: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return False
|
|
|
|
def load_all_plugins(self) -> None:
|
|
"""Load only enabled plugins."""
|
|
discovered = self.discover_plugins()
|
|
|
|
for plugin_class in discovered:
|
|
plugin_id = f"{plugin_class.__module__}.{plugin_class.__name__}"
|
|
|
|
# Only load if explicitly enabled
|
|
if self.is_plugin_enabled(plugin_id):
|
|
self.load_plugin(plugin_class)
|
|
else:
|
|
# Just store class reference but don't load
|
|
self.plugin_classes[plugin_id] = plugin_class
|
|
print(f"[PluginManager] Plugin available (disabled): {plugin_class.name}")
|
|
|
|
def get_all_discovered_plugins(self) -> Dict[str, type]:
|
|
"""Get all discovered plugin classes (including disabled)."""
|
|
return self.plugin_classes.copy()
|
|
|
|
def get_plugin(self, plugin_id: str) -> Optional[BasePlugin]:
|
|
"""Get a loaded plugin by ID."""
|
|
return self.plugins.get(plugin_id)
|
|
|
|
def get_plugin_ui(self, plugin_id: str):
|
|
"""Get UI widget for a plugin."""
|
|
plugin = self.plugins.get(plugin_id)
|
|
if plugin:
|
|
return plugin.get_ui()
|
|
return None
|
|
|
|
def get_all_plugins(self) -> Dict[str, BasePlugin]:
|
|
"""Get all loaded plugins."""
|
|
return self.plugins.copy()
|
|
|
|
def unload_plugin(self, plugin_id: str) -> None:
|
|
"""Shutdown and unload a plugin."""
|
|
if plugin_id in self.plugins:
|
|
try:
|
|
self.plugins[plugin_id].shutdown()
|
|
except Exception as e:
|
|
print(f"Error shutting down {plugin_id}: {e}")
|
|
del self.plugins[plugin_id]
|
|
|
|
def shutdown_all(self) -> None:
|
|
"""Shutdown all plugins."""
|
|
for plugin_id in list(self.plugins.keys()):
|
|
self.unload_plugin(plugin_id)
|
|
|
|
def trigger_hotkey(self, hotkey: str) -> bool:
|
|
"""Trigger plugin by hotkey. Returns True if handled."""
|
|
for plugin_id, plugin in self.plugins.items():
|
|
if plugin.hotkey == hotkey and plugin.enabled:
|
|
try:
|
|
plugin.on_hotkey()
|
|
return True
|
|
except Exception as e:
|
|
print(f"Error triggering hotkey for {plugin_id}: {e}")
|
|
return False
|