diff --git a/build-local.sh b/build-local.sh index 58f96e5..28c34f5 100755 --- a/build-local.sh +++ b/build-local.sh @@ -70,6 +70,18 @@ make install || { echo -e "${GREEN}=== Build Complete ===${NC}" echo -e "${GREEN}Cthulhu installed to: ${LOCAL_PREFIX}${NC}" echo -e "${GREEN}Binary location: ${LOCAL_PREFIX}/bin/cthulhu${NC}" + +# Ensure UI files are installed (fallback in case make install missed them) +echo -e "${YELLOW}Ensuring UI files are installed...${NC}" +mkdir -p "${LOCAL_PREFIX}/share/cthulhu/ui" +if [[ ! -f "${LOCAL_PREFIX}/share/cthulhu/ui/cthulhu-setup.ui" ]]; then + cp src/cthulhu/cthulhu-setup.ui "${LOCAL_PREFIX}/share/cthulhu/ui/" 2>/dev/null || true +fi +if [[ ! -f "${LOCAL_PREFIX}/share/cthulhu/ui/cthulhu-find.ui" ]]; then + cp src/cthulhu/cthulhu-find.ui "${LOCAL_PREFIX}/share/cthulhu/ui/" 2>/dev/null || true +fi +echo -e "${GREEN}UI files confirmed.${NC}" + echo "" echo -e "${YELLOW}To run local Cthulhu:${NC}" echo -e " ${LOCAL_PREFIX}/bin/cthulhu" diff --git a/clean-local.sh b/clean-local.sh index 2efe478..7a03e34 100755 --- a/clean-local.sh +++ b/clean-local.sh @@ -57,15 +57,18 @@ uninstall_local() { # Remove Python modules rm -rf "${LOCAL_PREFIX}/lib/python"*/site-packages/cthulhu* - # Remove data files but preserve user settings + # Remove data files but preserve user settings and UI files if [[ -d "${LOCAL_PREFIX}/share/cthulhu" ]]; then - # Backup user settings if they exist + # Backup user settings and UI files if they exist if [[ -f "${LOCAL_PREFIX}/share/cthulhu/user-settings.conf" ]]; then cp "${LOCAL_PREFIX}/share/cthulhu/user-settings.conf" /tmp/cthulhu-user-settings.backup fi if [[ -f "${LOCAL_PREFIX}/share/cthulhu/cthulhu-customizations.py" ]]; then cp "${LOCAL_PREFIX}/share/cthulhu/cthulhu-customizations.py" /tmp/cthulhu-customizations.backup fi + if [[ -d "${LOCAL_PREFIX}/share/cthulhu/ui" ]]; then + cp -r "${LOCAL_PREFIX}/share/cthulhu/ui" /tmp/cthulhu-ui.backup + fi # Remove the directory rm -rf "${LOCAL_PREFIX}/share/cthulhu" @@ -78,6 +81,9 @@ uninstall_local() { if [[ -f /tmp/cthulhu-customizations.backup ]]; then mv /tmp/cthulhu-customizations.backup "${LOCAL_PREFIX}/share/cthulhu/cthulhu-customizations.py" fi + if [[ -d /tmp/cthulhu-ui.backup ]]; then + mv /tmp/cthulhu-ui.backup "${LOCAL_PREFIX}/share/cthulhu/ui" + fi fi # Remove docs diff --git a/configure.ac b/configure.ac index 88db710..cb631d5 100644 --- a/configure.ac +++ b/configure.ac @@ -138,6 +138,7 @@ src/cthulhu/plugins/HelloCthulhu/Makefile src/cthulhu/plugins/Clipboard/Makefile src/cthulhu/plugins/DisplayVersion/Makefile src/cthulhu/plugins/IndentationAudio/Makefile +src/cthulhu/plugins/PluginManager/Makefile src/cthulhu/plugins/hello_world/Makefile src/cthulhu/plugins/self_voice/Makefile src/cthulhu/plugins/SimplePluginSystem/Makefile diff --git a/src/cthulhu/plugins/Makefile.am b/src/cthulhu/plugins/Makefile.am index cbbb653..e311901 100644 --- a/src/cthulhu/plugins/Makefile.am +++ b/src/cthulhu/plugins/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = Clipboard DisplayVersion IndentationAudio hello_world self_voice ByeCthulhu HelloCthulhu SimplePluginSystem +SUBDIRS = Clipboard DisplayVersion IndentationAudio PluginManager hello_world self_voice ByeCthulhu HelloCthulhu SimplePluginSystem cthulhu_pythondir=$(pkgpythondir)/plugins diff --git a/src/cthulhu/plugins/PluginManager/Makefile.am b/src/cthulhu/plugins/PluginManager/Makefile.am new file mode 100644 index 0000000..c0db4f1 --- /dev/null +++ b/src/cthulhu/plugins/PluginManager/Makefile.am @@ -0,0 +1,6 @@ +cthulhu_python_PYTHON = \ + __init__.py \ + plugin.info \ + plugin.py + +cthulhu_pythondir=$(pkgpythondir)/plugins/PluginManager \ No newline at end of file diff --git a/src/cthulhu/plugins/PluginManager/__init__.py b/src/cthulhu/plugins/PluginManager/__init__.py new file mode 100644 index 0000000..a5dbd0a --- /dev/null +++ b/src/cthulhu/plugins/PluginManager/__init__.py @@ -0,0 +1,14 @@ +#!/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 package.""" + +from .plugin import PluginManager + +__all__ = ['PluginManager'] \ No newline at end of file diff --git a/src/cthulhu/plugins/PluginManager/plugin.info b/src/cthulhu/plugins/PluginManager/plugin.info new file mode 100644 index 0000000..115f79e --- /dev/null +++ b/src/cthulhu/plugins/PluginManager/plugin.info @@ -0,0 +1,8 @@ +name = PluginManager +version = 1.0.0 +description = GUI interface for managing Cthulhu plugins - enable/disable plugins with checkboxes +authors = Stormux +website = https://git.stormux.org/storm/cthulhu +copyright = Copyright 2025 +builtin = false +hidden = false \ No newline at end of file diff --git a/src/cthulhu/plugins/PluginManager/plugin.py b/src/cthulhu/plugins/PluginManager/plugin.py new file mode 100644 index 0000000..33b0d8e --- /dev/null +++ b/src/cthulhu/plugins/PluginManager/plugin.py @@ -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("Available Plugins") + 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("Note: PluginManager is always enabled and not shown here") + 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"{plugin_name}" + 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("Restart Cthulhu to apply plugin changes") + 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}") \ No newline at end of file diff --git a/src/cthulhu/settings.py b/src/cthulhu/settings.py index 8d1a1f4..0c4e1a8 100644 --- a/src/cthulhu/settings.py +++ b/src/cthulhu/settings.py @@ -413,4 +413,4 @@ presentChatRoomLast = False presentLiveRegionFromInactiveTab = False # Plugins -activePlugins = ['Clipboard', 'DisplayVersion', 'ByeCthulhu', 'HelloCthulhu', 'hello_world', 'self_voice', 'SimplePluginSystem'] +activePlugins = ['DisplayVersion', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu']