|
|
|
@@ -0,0 +1,421 @@
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
#
|
|
|
|
|
# Copyright (c) 2025 Stormux
|
|
|
|
|
#
|
|
|
|
|
# This library is free software; you can redistribute it and/or
|
|
|
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
|
|
|
# License as published by the Free Software Foundation; either
|
|
|
|
|
# version 2.1 of the License, or (at your option) any later version.
|
|
|
|
|
|
|
|
|
|
"""PluginManager plugin for Cthulhu - GUI interface for managing plugins."""
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
import configparser
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import gi
|
|
|
|
|
gi.require_version('Gtk', '3.0')
|
|
|
|
|
from gi.repository import Gtk, GLib
|
|
|
|
|
|
|
|
|
|
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
|
|
|
|
from cthulhu import debug
|
|
|
|
|
from cthulhu import settings_manager
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
_settingsManager = settings_manager.getManager()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PluginManager(Plugin):
|
|
|
|
|
"""Plugin that provides a GUI interface for managing other plugins."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
"""Initialize the PluginManager plugin."""
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
self._kb_binding = None
|
|
|
|
|
self._dialog = None
|
|
|
|
|
self._plugin_checkboxes = {}
|
|
|
|
|
|
|
|
|
|
logger.info("PluginManager plugin initialized")
|
|
|
|
|
|
|
|
|
|
@cthulhu_hookimpl
|
|
|
|
|
def activate(self, plugin=None):
|
|
|
|
|
"""Activate the PluginManager plugin."""
|
|
|
|
|
if plugin is not None and plugin is not self:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
logger.info("=== PluginManager plugin activation starting ===")
|
|
|
|
|
|
|
|
|
|
# Register keybinding for opening plugin manager (Cthulhu+Shift+P)
|
|
|
|
|
self._register_keybinding()
|
|
|
|
|
|
|
|
|
|
logger.info("PluginManager plugin activated successfully")
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to activate PluginManager plugin: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@cthulhu_hookimpl
|
|
|
|
|
def deactivate(self, plugin=None):
|
|
|
|
|
"""Deactivate the PluginManager plugin."""
|
|
|
|
|
if plugin is not None and plugin is not self:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
logger.info("=== PluginManager plugin deactivation starting ===")
|
|
|
|
|
|
|
|
|
|
# Close dialog if open
|
|
|
|
|
if self._dialog:
|
|
|
|
|
self._dialog.destroy()
|
|
|
|
|
self._dialog = None
|
|
|
|
|
|
|
|
|
|
logger.info("PluginManager plugin deactivated successfully")
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to deactivate PluginManager plugin: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def _register_keybinding(self):
|
|
|
|
|
"""Register the Cthulhu+Shift+P keybinding for opening plugin manager."""
|
|
|
|
|
try:
|
|
|
|
|
if not self.app:
|
|
|
|
|
logger.error("PluginManager: No app reference available for keybinding")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Register Cthulhu+Shift+P keybinding
|
|
|
|
|
gesture_string = "kb:cthulhu+shift+p"
|
|
|
|
|
description = "Open plugin manager"
|
|
|
|
|
|
|
|
|
|
self._kb_binding = self.registerGestureByString(
|
|
|
|
|
self._open_plugin_manager,
|
|
|
|
|
description,
|
|
|
|
|
gesture_string
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if self._kb_binding:
|
|
|
|
|
logger.info(f"PluginManager: Registered keybinding {gesture_string}")
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Registered keybinding {gesture_string}", True)
|
|
|
|
|
else:
|
|
|
|
|
logger.error(f"PluginManager: Failed to register keybinding {gesture_string}")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"PluginManager: Error registering keybinding: {e}")
|
|
|
|
|
|
|
|
|
|
def _open_plugin_manager(self, script, inputEvent=None):
|
|
|
|
|
"""Open the plugin manager dialog."""
|
|
|
|
|
try:
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, "PluginManager: Opening plugin manager dialog", True)
|
|
|
|
|
|
|
|
|
|
# Close existing dialog if open
|
|
|
|
|
if self._dialog:
|
|
|
|
|
self._dialog.destroy()
|
|
|
|
|
|
|
|
|
|
# Create new dialog
|
|
|
|
|
self._create_dialog()
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"PluginManager: Error opening plugin manager: {e}")
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error opening dialog: {e}", True)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def _create_dialog(self):
|
|
|
|
|
"""Create and show the plugin manager dialog."""
|
|
|
|
|
try:
|
|
|
|
|
# Create dialog window
|
|
|
|
|
self._dialog = Gtk.Dialog(
|
|
|
|
|
title="Cthulhu Plugin Manager",
|
|
|
|
|
parent=None,
|
|
|
|
|
flags=Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Set dialog properties
|
|
|
|
|
self._dialog.set_default_size(400, 300)
|
|
|
|
|
self._dialog.set_border_width(10)
|
|
|
|
|
|
|
|
|
|
# Add buttons
|
|
|
|
|
self._dialog.add_button("Close", Gtk.ResponseType.CLOSE)
|
|
|
|
|
|
|
|
|
|
# Create main content area
|
|
|
|
|
content_area = self._dialog.get_content_area()
|
|
|
|
|
|
|
|
|
|
# Add title label
|
|
|
|
|
title_label = Gtk.Label()
|
|
|
|
|
title_label.set_markup("<b>Available Plugins</b>")
|
|
|
|
|
title_label.set_halign(Gtk.Align.CENTER)
|
|
|
|
|
content_area.pack_start(title_label, False, False, 10)
|
|
|
|
|
|
|
|
|
|
# Add info label about PluginManager
|
|
|
|
|
info_label = Gtk.Label()
|
|
|
|
|
info_label.set_markup("<i>Note: PluginManager is always enabled and not shown here</i>")
|
|
|
|
|
info_label.set_halign(Gtk.Align.CENTER)
|
|
|
|
|
content_area.pack_start(info_label, False, False, 5)
|
|
|
|
|
|
|
|
|
|
# Create scrolled window for plugin list
|
|
|
|
|
scrolled = Gtk.ScrolledWindow()
|
|
|
|
|
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
|
|
|
|
scrolled.set_size_request(-1, 200)
|
|
|
|
|
|
|
|
|
|
# Create list box for plugins
|
|
|
|
|
self._plugin_listbox = Gtk.ListBox()
|
|
|
|
|
self._plugin_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
|
|
|
|
scrolled.add(self._plugin_listbox)
|
|
|
|
|
|
|
|
|
|
content_area.pack_start(scrolled, True, True, 0)
|
|
|
|
|
|
|
|
|
|
# Populate plugin list
|
|
|
|
|
self._populate_plugin_list()
|
|
|
|
|
|
|
|
|
|
# Connect signals
|
|
|
|
|
self._dialog.connect("response", self._on_dialog_response)
|
|
|
|
|
|
|
|
|
|
# Show dialog
|
|
|
|
|
self._dialog.show_all()
|
|
|
|
|
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, "PluginManager: Dialog created and shown", True)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"PluginManager: Error creating dialog: {e}")
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error creating dialog: {e}", True)
|
|
|
|
|
|
|
|
|
|
def _populate_plugin_list(self):
|
|
|
|
|
"""Populate the plugin list with available plugins."""
|
|
|
|
|
try:
|
|
|
|
|
# Get available plugins
|
|
|
|
|
available_plugins = self._discover_plugins()
|
|
|
|
|
|
|
|
|
|
# Get currently active plugins
|
|
|
|
|
active_plugins = _settingsManager.getSetting('activePlugins') or []
|
|
|
|
|
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Found {len(available_plugins)} plugins", True)
|
|
|
|
|
|
|
|
|
|
# Clear existing checkboxes
|
|
|
|
|
self._plugin_checkboxes.clear()
|
|
|
|
|
|
|
|
|
|
# Add each plugin as a checkbox (except PluginManager itself)
|
|
|
|
|
for plugin_name, plugin_info in sorted(available_plugins.items()):
|
|
|
|
|
# Skip PluginManager to prevent users from disabling plugin management
|
|
|
|
|
if plugin_name == "PluginManager":
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Create row container
|
|
|
|
|
row = Gtk.ListBoxRow()
|
|
|
|
|
row.set_activatable(False)
|
|
|
|
|
|
|
|
|
|
# Create horizontal box for checkbox and info
|
|
|
|
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
|
|
|
|
hbox.set_border_width(5)
|
|
|
|
|
|
|
|
|
|
# Create checkbox
|
|
|
|
|
checkbox = Gtk.CheckButton()
|
|
|
|
|
checkbox.set_active(plugin_name in active_plugins)
|
|
|
|
|
checkbox.connect("toggled", self._on_plugin_toggled, plugin_name)
|
|
|
|
|
|
|
|
|
|
# Create plugin info label
|
|
|
|
|
info_text = f"<b>{plugin_name}</b>"
|
|
|
|
|
if plugin_info.get('description'):
|
|
|
|
|
info_text += f"\n{plugin_info['description']}"
|
|
|
|
|
if plugin_info.get('version'):
|
|
|
|
|
info_text += f" (v{plugin_info['version']})"
|
|
|
|
|
|
|
|
|
|
label = Gtk.Label()
|
|
|
|
|
label.set_markup(info_text)
|
|
|
|
|
label.set_halign(Gtk.Align.START)
|
|
|
|
|
label.set_line_wrap(True)
|
|
|
|
|
|
|
|
|
|
# Pack widgets
|
|
|
|
|
hbox.pack_start(checkbox, False, False, 0)
|
|
|
|
|
hbox.pack_start(label, True, True, 0)
|
|
|
|
|
|
|
|
|
|
row.add(hbox)
|
|
|
|
|
self._plugin_listbox.add(row)
|
|
|
|
|
|
|
|
|
|
# Store checkbox reference
|
|
|
|
|
self._plugin_checkboxes[plugin_name] = checkbox
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"PluginManager: Error populating plugin list: {e}")
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error populating list: {e}", True)
|
|
|
|
|
|
|
|
|
|
def _discover_plugins(self):
|
|
|
|
|
"""Discover all available plugins."""
|
|
|
|
|
plugins = {}
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Get plugin system manager
|
|
|
|
|
from cthulhu import plugin_system_manager
|
|
|
|
|
|
|
|
|
|
# Use existing plugin manager to get plugins
|
|
|
|
|
if hasattr(plugin_system_manager, '_manager') and plugin_system_manager._manager:
|
|
|
|
|
manager = plugin_system_manager._manager
|
|
|
|
|
manager.rescanPlugins()
|
|
|
|
|
|
|
|
|
|
for plugin_info in manager.plugins:
|
|
|
|
|
plugin_name = plugin_info.get_module_name()
|
|
|
|
|
plugins[plugin_name] = {
|
|
|
|
|
'name': plugin_name,
|
|
|
|
|
'description': getattr(plugin_info, 'description', ''),
|
|
|
|
|
'version': getattr(plugin_info, 'version', ''),
|
|
|
|
|
'path': getattr(plugin_info, 'module_dir', '')
|
|
|
|
|
}
|
|
|
|
|
else:
|
|
|
|
|
# Fallback: manually scan plugin directories
|
|
|
|
|
plugins = self._manual_plugin_discovery()
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"PluginManager: Error in plugin discovery: {e}")
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Plugin discovery error: {e}", True)
|
|
|
|
|
# Fallback to manual discovery
|
|
|
|
|
plugins = self._manual_plugin_discovery()
|
|
|
|
|
|
|
|
|
|
return plugins
|
|
|
|
|
|
|
|
|
|
def _manual_plugin_discovery(self):
|
|
|
|
|
"""Manually discover plugins by scanning directories."""
|
|
|
|
|
plugins = {}
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Get plugin directories
|
|
|
|
|
from cthulhu.plugin_system_manager import PluginType
|
|
|
|
|
|
|
|
|
|
system_dir = PluginType.SYSTEM.get_root_dir()
|
|
|
|
|
user_dir = PluginType.USER.get_root_dir()
|
|
|
|
|
|
|
|
|
|
for plugin_dir in [system_dir, user_dir]:
|
|
|
|
|
if os.path.exists(plugin_dir):
|
|
|
|
|
self._scan_plugin_directory(plugin_dir, plugins)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"PluginManager: Error in manual discovery: {e}")
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Manual discovery error: {e}", True)
|
|
|
|
|
|
|
|
|
|
return plugins
|
|
|
|
|
|
|
|
|
|
def _scan_plugin_directory(self, directory, plugins):
|
|
|
|
|
"""Scan a directory for plugins."""
|
|
|
|
|
try:
|
|
|
|
|
for item in os.listdir(directory):
|
|
|
|
|
plugin_path = os.path.join(directory, item)
|
|
|
|
|
if os.path.isdir(plugin_path):
|
|
|
|
|
# Check for plugin.py and plugin.info
|
|
|
|
|
plugin_py = os.path.join(plugin_path, "plugin.py")
|
|
|
|
|
plugin_info_file = os.path.join(plugin_path, "plugin.info")
|
|
|
|
|
|
|
|
|
|
if os.path.exists(plugin_py):
|
|
|
|
|
plugin_info = {'name': item, 'description': '', 'version': ''}
|
|
|
|
|
|
|
|
|
|
# Try to read plugin.info
|
|
|
|
|
if os.path.exists(plugin_info_file):
|
|
|
|
|
try:
|
|
|
|
|
config = configparser.ConfigParser()
|
|
|
|
|
config.read(plugin_info_file)
|
|
|
|
|
|
|
|
|
|
# Handle both INI-style and simple key=value format
|
|
|
|
|
if config.sections():
|
|
|
|
|
# INI-style format
|
|
|
|
|
for section in config.sections():
|
|
|
|
|
for key, value in config[section].items():
|
|
|
|
|
if key.lower() in ['description', 'version']:
|
|
|
|
|
plugin_info[key.lower()] = value
|
|
|
|
|
else:
|
|
|
|
|
# Simple key=value format
|
|
|
|
|
with open(plugin_info_file, 'r') as f:
|
|
|
|
|
for line in f:
|
|
|
|
|
line = line.strip()
|
|
|
|
|
if '=' in line and not line.startswith('#'):
|
|
|
|
|
key, value = line.split('=', 1)
|
|
|
|
|
key = key.strip().lower()
|
|
|
|
|
value = value.strip()
|
|
|
|
|
if key in ['description', 'version']:
|
|
|
|
|
plugin_info[key] = value
|
|
|
|
|
except Exception as info_e:
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error reading {plugin_info_file}: {info_e}", True)
|
|
|
|
|
|
|
|
|
|
plugins[item] = plugin_info
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"PluginManager: Error scanning directory {directory}: {e}")
|
|
|
|
|
|
|
|
|
|
def _on_plugin_toggled(self, checkbox, plugin_name):
|
|
|
|
|
"""Handle plugin checkbox toggle."""
|
|
|
|
|
try:
|
|
|
|
|
is_active = checkbox.get_active()
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Plugin {plugin_name} toggled to {'active' if is_active else 'inactive'}", True)
|
|
|
|
|
|
|
|
|
|
# Get current active plugins
|
|
|
|
|
active_plugins = _settingsManager.getSetting('activePlugins') or []
|
|
|
|
|
active_plugins = list(active_plugins) # Make a copy
|
|
|
|
|
|
|
|
|
|
# Update the list
|
|
|
|
|
if is_active and plugin_name not in active_plugins:
|
|
|
|
|
active_plugins.append(plugin_name)
|
|
|
|
|
elif not is_active and plugin_name in active_plugins:
|
|
|
|
|
active_plugins.remove(plugin_name)
|
|
|
|
|
|
|
|
|
|
# Save updated settings
|
|
|
|
|
_settingsManager.setSetting('activePlugins', active_plugins)
|
|
|
|
|
|
|
|
|
|
# Save to disk using the backend directly
|
|
|
|
|
try:
|
|
|
|
|
# Get current general settings
|
|
|
|
|
current_general = _settingsManager.getGeneralSettings()
|
|
|
|
|
current_general['activePlugins'] = active_plugins
|
|
|
|
|
|
|
|
|
|
# Save using the backend
|
|
|
|
|
backend = _settingsManager._backend
|
|
|
|
|
if backend:
|
|
|
|
|
backend.saveDefaultSettings(
|
|
|
|
|
current_general,
|
|
|
|
|
_settingsManager.getPronunciations(),
|
|
|
|
|
_settingsManager.getKeybindings()
|
|
|
|
|
)
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, "PluginManager: Settings saved to backend", True)
|
|
|
|
|
else:
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, "PluginManager: No backend available for saving", True)
|
|
|
|
|
|
|
|
|
|
except Exception as save_e:
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error saving via backend: {save_e}", True)
|
|
|
|
|
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Updated active plugins: {active_plugins}", True)
|
|
|
|
|
|
|
|
|
|
# Show restart message
|
|
|
|
|
self._show_restart_message()
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"PluginManager: Error toggling plugin {plugin_name}: {e}")
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error toggling {plugin_name}: {e}", True)
|
|
|
|
|
|
|
|
|
|
def _show_restart_message(self):
|
|
|
|
|
"""Show a message that restart is needed to apply plugin changes."""
|
|
|
|
|
if not hasattr(self, '_restart_message_shown'):
|
|
|
|
|
try:
|
|
|
|
|
# Get the content area of the existing dialog
|
|
|
|
|
content_area = self._dialog.get_content_area()
|
|
|
|
|
|
|
|
|
|
# Add restart message if not already shown
|
|
|
|
|
restart_label = Gtk.Label()
|
|
|
|
|
restart_label.set_markup("<i>Restart Cthulhu to apply plugin changes</i>")
|
|
|
|
|
restart_label.set_halign(Gtk.Align.CENTER)
|
|
|
|
|
content_area.pack_end(restart_label, False, False, 10)
|
|
|
|
|
restart_label.show()
|
|
|
|
|
|
|
|
|
|
self._restart_message_shown = True
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, "PluginManager: Added restart message to dialog", True)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error showing restart message: {e}", True)
|
|
|
|
|
|
|
|
|
|
def _on_dialog_response(self, dialog, response_id):
|
|
|
|
|
"""Handle dialog response (close button clicked)."""
|
|
|
|
|
try:
|
|
|
|
|
if response_id == Gtk.ResponseType.CLOSE:
|
|
|
|
|
dialog.destroy()
|
|
|
|
|
self._dialog = None
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, "PluginManager: Dialog closed", True)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"PluginManager: Error handling dialog response: {e}")
|