""" 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