Plugin manager added. No more hand editing the settings file to enable and disable plugins. Hopefully less breakage. Updates to the local build and clean files for installing test builds.

This commit is contained in:
Storm Dragon
2025-08-02 05:11:56 -04:00
parent cb20579625
commit 06894693b0
9 changed files with 472 additions and 4 deletions

View File

@ -70,6 +70,18 @@ make install || {
echo -e "${GREEN}=== Build Complete ===${NC}" echo -e "${GREEN}=== Build Complete ===${NC}"
echo -e "${GREEN}Cthulhu installed to: ${LOCAL_PREFIX}${NC}" echo -e "${GREEN}Cthulhu installed to: ${LOCAL_PREFIX}${NC}"
echo -e "${GREEN}Binary location: ${LOCAL_PREFIX}/bin/cthulhu${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 ""
echo -e "${YELLOW}To run local Cthulhu:${NC}" echo -e "${YELLOW}To run local Cthulhu:${NC}"
echo -e " ${LOCAL_PREFIX}/bin/cthulhu" echo -e " ${LOCAL_PREFIX}/bin/cthulhu"

View File

@ -57,15 +57,18 @@ uninstall_local() {
# Remove Python modules # Remove Python modules
rm -rf "${LOCAL_PREFIX}/lib/python"*/site-packages/cthulhu* 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 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 if [[ -f "${LOCAL_PREFIX}/share/cthulhu/user-settings.conf" ]]; then
cp "${LOCAL_PREFIX}/share/cthulhu/user-settings.conf" /tmp/cthulhu-user-settings.backup cp "${LOCAL_PREFIX}/share/cthulhu/user-settings.conf" /tmp/cthulhu-user-settings.backup
fi fi
if [[ -f "${LOCAL_PREFIX}/share/cthulhu/cthulhu-customizations.py" ]]; then if [[ -f "${LOCAL_PREFIX}/share/cthulhu/cthulhu-customizations.py" ]]; then
cp "${LOCAL_PREFIX}/share/cthulhu/cthulhu-customizations.py" /tmp/cthulhu-customizations.backup cp "${LOCAL_PREFIX}/share/cthulhu/cthulhu-customizations.py" /tmp/cthulhu-customizations.backup
fi fi
if [[ -d "${LOCAL_PREFIX}/share/cthulhu/ui" ]]; then
cp -r "${LOCAL_PREFIX}/share/cthulhu/ui" /tmp/cthulhu-ui.backup
fi
# Remove the directory # Remove the directory
rm -rf "${LOCAL_PREFIX}/share/cthulhu" rm -rf "${LOCAL_PREFIX}/share/cthulhu"
@ -78,6 +81,9 @@ uninstall_local() {
if [[ -f /tmp/cthulhu-customizations.backup ]]; then if [[ -f /tmp/cthulhu-customizations.backup ]]; then
mv /tmp/cthulhu-customizations.backup "${LOCAL_PREFIX}/share/cthulhu/cthulhu-customizations.py" mv /tmp/cthulhu-customizations.backup "${LOCAL_PREFIX}/share/cthulhu/cthulhu-customizations.py"
fi fi
if [[ -d /tmp/cthulhu-ui.backup ]]; then
mv /tmp/cthulhu-ui.backup "${LOCAL_PREFIX}/share/cthulhu/ui"
fi
fi fi
# Remove docs # Remove docs

View File

@ -138,6 +138,7 @@ src/cthulhu/plugins/HelloCthulhu/Makefile
src/cthulhu/plugins/Clipboard/Makefile src/cthulhu/plugins/Clipboard/Makefile
src/cthulhu/plugins/DisplayVersion/Makefile src/cthulhu/plugins/DisplayVersion/Makefile
src/cthulhu/plugins/IndentationAudio/Makefile src/cthulhu/plugins/IndentationAudio/Makefile
src/cthulhu/plugins/PluginManager/Makefile
src/cthulhu/plugins/hello_world/Makefile src/cthulhu/plugins/hello_world/Makefile
src/cthulhu/plugins/self_voice/Makefile src/cthulhu/plugins/self_voice/Makefile
src/cthulhu/plugins/SimplePluginSystem/Makefile src/cthulhu/plugins/SimplePluginSystem/Makefile

View File

@ -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 cthulhu_pythondir=$(pkgpythondir)/plugins

View File

@ -0,0 +1,6 @@
cthulhu_python_PYTHON = \
__init__.py \
plugin.info \
plugin.py
cthulhu_pythondir=$(pkgpythondir)/plugins/PluginManager

View File

@ -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']

View File

@ -0,0 +1,8 @@
name = PluginManager
version = 1.0.0
description = GUI interface for managing Cthulhu plugins - enable/disable plugins with checkboxes
authors = Stormux <storm_dragon@stormux.org>
website = https://git.stormux.org/storm/cthulhu
copyright = Copyright 2025
builtin = false
hidden = false

View File

@ -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}")

View File

@ -413,4 +413,4 @@ presentChatRoomLast = False
presentLiveRegionFromInactiveTab = False presentLiveRegionFromInactiveTab = False
# Plugins # Plugins
activePlugins = ['Clipboard', 'DisplayVersion', 'ByeCthulhu', 'HelloCthulhu', 'hello_world', 'self_voice', 'SimplePluginSystem'] activePlugins = ['DisplayVersion', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu']