Intial redesign of plugins manager. This is likely to be buggy.
This commit is contained in:
@@ -38,6 +38,7 @@ gi.require_version("Gtk", "3.0")
|
|||||||
from gi.repository import Atspi
|
from gi.repository import Atspi
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import threading
|
||||||
from gi.repository import Gdk
|
from gi.repository import Gdk
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
@@ -134,6 +135,19 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
self.planeCellRendererText = None
|
self.planeCellRendererText = None
|
||||||
self.pronunciationModel = None
|
self.pronunciationModel = None
|
||||||
self.pronunciationView = None
|
self.pronunciationView = None
|
||||||
|
self._plugin_checkboxes = {}
|
||||||
|
self._plugin_listbox = None
|
||||||
|
self._plugin_sources = []
|
||||||
|
self._plugin_sources_entry = None
|
||||||
|
self._plugin_sources_listbox = None
|
||||||
|
self._plugin_sources_original = []
|
||||||
|
self._available_plugins = set()
|
||||||
|
self._plugin_canonical_map = {}
|
||||||
|
self._plugin_group_map = {}
|
||||||
|
self._plugin_update_button = None
|
||||||
|
self._plugin_update_progress = None
|
||||||
|
self._plugin_update_status = None
|
||||||
|
self._plugin_update_in_progress = False
|
||||||
self.screenHeight = None
|
self.screenHeight = None
|
||||||
self.screenWidth = None
|
self.screenWidth = None
|
||||||
self.speechFamiliesChoice = None
|
self.speechFamiliesChoice = None
|
||||||
@@ -365,6 +379,11 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
self._isInitialSetup = \
|
self._isInitialSetup = \
|
||||||
not os.path.exists(_settingsManager.getPrefsDir())
|
not os.path.exists(_settingsManager.getPrefsDir())
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._initPluginsPage()
|
||||||
|
except Exception as e:
|
||||||
|
debug.printMessage(debug.LEVEL_WARNING, f"PREFERENCES DIALOG: Plugin page init failed: {e}", True)
|
||||||
|
|
||||||
appPage = self.script.getAppPreferencesGUI()
|
appPage = self.script.getAppPreferencesGUI()
|
||||||
if appPage:
|
if appPage:
|
||||||
label = Gtk.Label(label=AXObject.get_name(self.script.app))
|
label = Gtk.Label(label=AXObject.get_name(self.script.app))
|
||||||
@@ -373,6 +392,347 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
self._initGUIState()
|
self._initGUIState()
|
||||||
self._initSoundThemeState()
|
self._initSoundThemeState()
|
||||||
|
|
||||||
|
def _initPluginsPage(self):
|
||||||
|
self._plugin_sources = list(self.prefsDict.get("pluginSources", settings.pluginSources) or [])
|
||||||
|
self._plugin_sources_original = list(self._plugin_sources)
|
||||||
|
|
||||||
|
pluginsPage = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||||
|
pluginsPage.set_border_width(12)
|
||||||
|
|
||||||
|
infoLabel = Gtk.Label(label="Enable or disable plugins and manage plugin sources.")
|
||||||
|
infoLabel.set_line_wrap(True)
|
||||||
|
infoLabel.set_halign(Gtk.Align.START)
|
||||||
|
pluginsPage.pack_start(infoLabel, False, False, 0)
|
||||||
|
|
||||||
|
pluginsFrame = Gtk.Frame(label="Plugins")
|
||||||
|
try:
|
||||||
|
pluginsFrame.set_label_align(0.0, 0.5)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
pluginsFrame.set_label_xalign(0.0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
pluginsFrame.set_shadow_type(Gtk.ShadowType.NONE)
|
||||||
|
pluginsFrameBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||||
|
pluginsFrameBox.set_border_width(6)
|
||||||
|
pluginsFrame.add(pluginsFrameBox)
|
||||||
|
|
||||||
|
pluginsScrolled = Gtk.ScrolledWindow()
|
||||||
|
pluginsScrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||||
|
pluginsScrolled.set_size_request(-1, 200)
|
||||||
|
self._plugin_listbox = Gtk.ListBox()
|
||||||
|
self._plugin_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||||
|
pluginsScrolled.add(self._plugin_listbox)
|
||||||
|
pluginsFrameBox.pack_start(pluginsScrolled, True, True, 0)
|
||||||
|
|
||||||
|
pluginsPage.pack_start(pluginsFrame, True, True, 0)
|
||||||
|
|
||||||
|
sourcesFrame = Gtk.Frame(label="Plugin Sources")
|
||||||
|
try:
|
||||||
|
sourcesFrame.set_label_align(0.0, 0.5)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
sourcesFrame.set_label_xalign(0.0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
sourcesFrame.set_shadow_type(Gtk.ShadowType.NONE)
|
||||||
|
sourcesFrameBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||||
|
sourcesFrameBox.set_border_width(6)
|
||||||
|
sourcesFrame.add(sourcesFrameBox)
|
||||||
|
|
||||||
|
sourcesGrid = Gtk.Grid(row_spacing=6, column_spacing=6)
|
||||||
|
sourcesLabel = Gtk.Label(label="_Source URL:")
|
||||||
|
sourcesLabel.set_use_underline(True)
|
||||||
|
sourcesLabel.set_halign(Gtk.Align.START)
|
||||||
|
self._plugin_sources_entry = Gtk.Entry()
|
||||||
|
self._plugin_sources_entry.set_placeholder_text("https://example.com/repo.git")
|
||||||
|
sourcesLabel.set_mnemonic_widget(self._plugin_sources_entry)
|
||||||
|
addButton = Gtk.Button(label="Add Source")
|
||||||
|
addButton.connect("clicked", self._on_add_plugin_source)
|
||||||
|
sourcesGrid.attach(sourcesLabel, 0, 0, 1, 1)
|
||||||
|
sourcesGrid.attach(self._plugin_sources_entry, 1, 0, 1, 1)
|
||||||
|
sourcesGrid.attach(addButton, 2, 0, 1, 1)
|
||||||
|
sourcesFrameBox.pack_start(sourcesGrid, False, False, 0)
|
||||||
|
|
||||||
|
updateRow = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||||
|
self._plugin_update_button = Gtk.Button(label="Update Plugins")
|
||||||
|
self._plugin_update_button.connect("clicked", self._on_update_plugins_clicked)
|
||||||
|
self._plugin_update_progress = Gtk.ProgressBar()
|
||||||
|
self._plugin_update_progress.set_show_text(True)
|
||||||
|
self._plugin_update_progress.set_text("Idle")
|
||||||
|
updateRow.pack_start(self._plugin_update_button, False, False, 0)
|
||||||
|
updateRow.pack_start(self._plugin_update_progress, True, True, 0)
|
||||||
|
sourcesFrameBox.pack_start(updateRow, False, False, 0)
|
||||||
|
|
||||||
|
sourcesScrolled = Gtk.ScrolledWindow()
|
||||||
|
sourcesScrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||||
|
sourcesScrolled.set_size_request(-1, 120)
|
||||||
|
self._plugin_sources_listbox = Gtk.ListBox()
|
||||||
|
self._plugin_sources_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||||
|
sourcesScrolled.add(self._plugin_sources_listbox)
|
||||||
|
sourcesFrameBox.pack_start(sourcesScrolled, True, True, 0)
|
||||||
|
|
||||||
|
sourcesNote = Gtk.Label(label="Use Update Plugins to install or update sources, then Apply or OK to save settings.")
|
||||||
|
sourcesNote.set_line_wrap(True)
|
||||||
|
sourcesNote.set_halign(Gtk.Align.START)
|
||||||
|
sourcesFrameBox.pack_start(sourcesNote, False, False, 0)
|
||||||
|
|
||||||
|
self._plugin_update_status = Gtk.Label(label="")
|
||||||
|
self._plugin_update_status.set_line_wrap(True)
|
||||||
|
self._plugin_update_status.set_halign(Gtk.Align.START)
|
||||||
|
sourcesFrameBox.pack_start(self._plugin_update_status, False, False, 0)
|
||||||
|
|
||||||
|
pluginsPage.pack_start(sourcesFrame, True, True, 0)
|
||||||
|
|
||||||
|
notebook = self.get_widget("notebook")
|
||||||
|
notebook.append_page(pluginsPage, Gtk.Label(label="Plugins"))
|
||||||
|
|
||||||
|
self._populate_plugin_list()
|
||||||
|
self._populate_plugin_sources_list()
|
||||||
|
pluginsPage.show_all()
|
||||||
|
|
||||||
|
def _clear_listbox(self, listbox):
|
||||||
|
for child in listbox.get_children():
|
||||||
|
listbox.remove(child)
|
||||||
|
|
||||||
|
def _populate_plugin_list(self):
|
||||||
|
if not self._plugin_listbox:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._clear_listbox(self._plugin_listbox)
|
||||||
|
self._plugin_checkboxes.clear()
|
||||||
|
self._available_plugins = set()
|
||||||
|
self._plugin_canonical_map = {}
|
||||||
|
self._plugin_group_map = {}
|
||||||
|
|
||||||
|
manager = cthulhu.cthulhuApp.getPluginSystemManager()
|
||||||
|
if manager:
|
||||||
|
try:
|
||||||
|
manager.rescanPlugins()
|
||||||
|
except Exception as e:
|
||||||
|
debug.printMessage(debug.LEVEL_WARNING, f"PREFERENCES DIALOG: Plugin rescan failed: {e}", True)
|
||||||
|
manager = None
|
||||||
|
|
||||||
|
active_plugins = list(self.prefsDict.get("activePlugins", settings.activePlugins) or [])
|
||||||
|
active_plugins_lower = {name.lower() for name in active_plugins}
|
||||||
|
|
||||||
|
plugin_infos = manager.plugins if manager else []
|
||||||
|
self._available_plugins = {info.get_module_name() for info in plugin_infos}
|
||||||
|
canonical_counts = {}
|
||||||
|
canonical_builtins = {}
|
||||||
|
for info in plugin_infos:
|
||||||
|
canonical = info.get_canonical_name()
|
||||||
|
canonical_counts[canonical] = canonical_counts.get(canonical, 0) + 1
|
||||||
|
if info.builtin:
|
||||||
|
canonical_builtins[canonical] = True
|
||||||
|
for plugin_info in sorted(plugin_infos, key=lambda item: (item.get_name() or item.get_module_name()).lower()):
|
||||||
|
plugin_name = plugin_info.get_module_name()
|
||||||
|
canonical_name = plugin_info.get_canonical_name()
|
||||||
|
if plugin_info.hidden or canonical_name == "PluginManager":
|
||||||
|
continue
|
||||||
|
|
||||||
|
row = Gtk.ListBoxRow()
|
||||||
|
row.set_activatable(False)
|
||||||
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||||
|
hbox.set_border_width(5)
|
||||||
|
|
||||||
|
checkbox = Gtk.CheckButton()
|
||||||
|
is_active = (
|
||||||
|
plugin_name in active_plugins
|
||||||
|
or plugin_name.lower() in active_plugins_lower
|
||||||
|
or (plugin_info.preferred_alias and (canonical_name in active_plugins or canonical_name.lower() in active_plugins_lower))
|
||||||
|
or plugin_info.builtin
|
||||||
|
)
|
||||||
|
if canonical_builtins.get(canonical_name) and not plugin_info.builtin:
|
||||||
|
is_active = False
|
||||||
|
checkbox.set_active(is_active)
|
||||||
|
if plugin_info.builtin:
|
||||||
|
checkbox.set_sensitive(False)
|
||||||
|
elif canonical_builtins.get(canonical_name):
|
||||||
|
checkbox.set_sensitive(False)
|
||||||
|
checkbox.connect("toggled", self._on_plugin_checkbox_toggled, plugin_name)
|
||||||
|
|
||||||
|
display_name = GLib.markup_escape_text(plugin_info.get_name() or plugin_name)
|
||||||
|
info_text = f"<b>{display_name}</b>"
|
||||||
|
description = plugin_info.get_description()
|
||||||
|
if description:
|
||||||
|
info_text += f"\n{GLib.markup_escape_text(description)}"
|
||||||
|
version = plugin_info.get_version()
|
||||||
|
if version:
|
||||||
|
info_text += f" (v{GLib.markup_escape_text(version)})"
|
||||||
|
if canonical_counts.get(canonical_name, 0) > 1:
|
||||||
|
info_text += f"\nSource: {GLib.markup_escape_text(plugin_info.get_source_label())}"
|
||||||
|
if canonical_builtins.get(canonical_name) and not plugin_info.builtin:
|
||||||
|
info_text += "\nDisabled because a builtin plugin uses this name."
|
||||||
|
|
||||||
|
label = Gtk.Label()
|
||||||
|
label.set_markup(info_text)
|
||||||
|
label.set_halign(Gtk.Align.START)
|
||||||
|
label.set_line_wrap(True)
|
||||||
|
|
||||||
|
hbox.pack_start(checkbox, False, False, 0)
|
||||||
|
hbox.pack_start(label, True, True, 0)
|
||||||
|
row.add(hbox)
|
||||||
|
self._plugin_listbox.add(row)
|
||||||
|
self._plugin_checkboxes[plugin_name] = checkbox
|
||||||
|
self._plugin_canonical_map[plugin_name] = canonical_name
|
||||||
|
self._plugin_group_map.setdefault(canonical_name, []).append(plugin_name)
|
||||||
|
|
||||||
|
self._plugin_listbox.show_all()
|
||||||
|
except Exception as e:
|
||||||
|
debug.printMessage(debug.LEVEL_WARNING, f"PREFERENCES DIALOG: Plugin list build failed: {e}", True)
|
||||||
|
|
||||||
|
def _populate_plugin_sources_list(self):
|
||||||
|
if not self._plugin_sources_listbox:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._clear_listbox(self._plugin_sources_listbox)
|
||||||
|
|
||||||
|
for source in self._plugin_sources:
|
||||||
|
row = Gtk.ListBoxRow()
|
||||||
|
row.set_activatable(False)
|
||||||
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||||
|
hbox.set_border_width(5)
|
||||||
|
|
||||||
|
label = Gtk.Label(label=source)
|
||||||
|
label.set_halign(Gtk.Align.START)
|
||||||
|
label.set_line_wrap(True)
|
||||||
|
|
||||||
|
remove_button = Gtk.Button(label="Remove")
|
||||||
|
remove_button.connect("clicked", self._on_remove_plugin_source, source, row)
|
||||||
|
|
||||||
|
hbox.pack_start(label, True, True, 0)
|
||||||
|
hbox.pack_start(remove_button, False, False, 0)
|
||||||
|
row.add(hbox)
|
||||||
|
self._plugin_sources_listbox.add(row)
|
||||||
|
|
||||||
|
self._plugin_sources_listbox.show_all()
|
||||||
|
|
||||||
|
def _on_add_plugin_source(self, widget):
|
||||||
|
if not self._plugin_sources_entry:
|
||||||
|
return
|
||||||
|
source = self._plugin_sources_entry.get_text().strip()
|
||||||
|
if not source:
|
||||||
|
return
|
||||||
|
if source in self._plugin_sources:
|
||||||
|
return
|
||||||
|
self._plugin_sources.append(source)
|
||||||
|
self._plugin_sources_entry.set_text("")
|
||||||
|
self._populate_plugin_sources_list()
|
||||||
|
|
||||||
|
def _on_remove_plugin_source(self, widget, source, row):
|
||||||
|
if source in self._plugin_sources:
|
||||||
|
self._plugin_sources.remove(source)
|
||||||
|
if self._plugin_sources_listbox and row:
|
||||||
|
self._plugin_sources_listbox.remove(row)
|
||||||
|
|
||||||
|
def _on_update_plugins_clicked(self, widget):
|
||||||
|
if self._plugin_update_in_progress:
|
||||||
|
return
|
||||||
|
|
||||||
|
sources = list(self._plugin_sources)
|
||||||
|
if not sources:
|
||||||
|
self._set_plugin_update_status("No sources to update.", done=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._plugin_update_in_progress = True
|
||||||
|
if self._plugin_update_button:
|
||||||
|
self._plugin_update_button.set_sensitive(False)
|
||||||
|
self._set_plugin_update_progress(0, len(sources), "Starting updates...")
|
||||||
|
|
||||||
|
def _run_updates():
|
||||||
|
manager = cthulhu.cthulhuApp.getPluginSystemManager()
|
||||||
|
if not manager:
|
||||||
|
GLib.idle_add(self._finish_plugin_updates, "Plugin manager unavailable.")
|
||||||
|
return
|
||||||
|
|
||||||
|
def _progress_callback(index, total, message):
|
||||||
|
GLib.idle_add(self._set_plugin_update_progress, index, total, message)
|
||||||
|
|
||||||
|
manager.syncPluginSources(sources, progress_callback=_progress_callback)
|
||||||
|
try:
|
||||||
|
manager.rescanPlugins()
|
||||||
|
except Exception as e:
|
||||||
|
GLib.idle_add(self._finish_plugin_updates, f"Update finished with errors: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
GLib.idle_add(self._finish_plugin_updates, "Update complete.")
|
||||||
|
|
||||||
|
thread = threading.Thread(target=_run_updates, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def _set_plugin_update_progress(self, index, total, message):
|
||||||
|
if not self._plugin_update_progress:
|
||||||
|
return
|
||||||
|
fraction = 0.0 if total <= 0 else min(1.0, float(index) / float(total))
|
||||||
|
self._plugin_update_progress.set_fraction(fraction)
|
||||||
|
self._plugin_update_progress.set_text(message)
|
||||||
|
self._set_plugin_update_status(message)
|
||||||
|
|
||||||
|
def _set_plugin_update_status(self, message, done=False):
|
||||||
|
if self._plugin_update_status:
|
||||||
|
self._plugin_update_status.set_text(message)
|
||||||
|
if done and self._plugin_update_progress:
|
||||||
|
self._plugin_update_progress.set_fraction(0.0)
|
||||||
|
self._plugin_update_progress.set_text("Idle")
|
||||||
|
|
||||||
|
def _finish_plugin_updates(self, message):
|
||||||
|
self._plugin_update_in_progress = False
|
||||||
|
if self._plugin_update_button:
|
||||||
|
self._plugin_update_button.set_sensitive(True)
|
||||||
|
if message:
|
||||||
|
self._set_plugin_update_status(message)
|
||||||
|
self._populate_plugin_list()
|
||||||
|
|
||||||
|
def _on_plugin_checkbox_toggled(self, checkbox, plugin_name):
|
||||||
|
if not checkbox.get_active():
|
||||||
|
return
|
||||||
|
canonical_name = self._plugin_canonical_map.get(plugin_name)
|
||||||
|
if not canonical_name:
|
||||||
|
return
|
||||||
|
for other_name in self._plugin_group_map.get(canonical_name, []):
|
||||||
|
if other_name == plugin_name:
|
||||||
|
continue
|
||||||
|
other_checkbox = self._plugin_checkboxes.get(other_name)
|
||||||
|
if other_checkbox and other_checkbox.get_active():
|
||||||
|
other_checkbox.set_active(False)
|
||||||
|
|
||||||
|
def _get_active_plugins_from_ui(self):
|
||||||
|
existing_plugins = list(self.prefsDict.get("activePlugins", settings.activePlugins) or [])
|
||||||
|
preserved_plugins = [
|
||||||
|
name for name in existing_plugins
|
||||||
|
if name not in self._plugin_checkboxes and name in self._available_plugins
|
||||||
|
]
|
||||||
|
selected_plugins = []
|
||||||
|
for canonical_name, plugin_names in self._plugin_group_map.items():
|
||||||
|
active_in_group = [
|
||||||
|
name for name in plugin_names
|
||||||
|
if self._plugin_checkboxes.get(name) and self._plugin_checkboxes[name].get_active()
|
||||||
|
]
|
||||||
|
if active_in_group:
|
||||||
|
selected_plugins.append(active_in_group[-1])
|
||||||
|
return preserved_plugins + selected_plugins
|
||||||
|
|
||||||
|
def _apply_plugin_changes(self):
|
||||||
|
active_plugins = self._get_active_plugins_from_ui()
|
||||||
|
plugin_sources = list(self._plugin_sources)
|
||||||
|
|
||||||
|
self.prefsDict["activePlugins"] = active_plugins
|
||||||
|
self.prefsDict["pluginSources"] = plugin_sources
|
||||||
|
|
||||||
|
removed_sources = [source for source in self._plugin_sources_original if source not in plugin_sources]
|
||||||
|
manager = cthulhu.cthulhuApp.getPluginSystemManager()
|
||||||
|
if manager:
|
||||||
|
try:
|
||||||
|
manager.removePluginSources(removed_sources)
|
||||||
|
manager.rescanPlugins()
|
||||||
|
except Exception as e:
|
||||||
|
debug.printMessage(debug.LEVEL_WARNING, f"PREFERENCES DIALOG: Plugin sync failed: {e}", True)
|
||||||
|
|
||||||
|
self._plugin_sources_original = list(plugin_sources)
|
||||||
|
self._populate_plugin_list()
|
||||||
|
|
||||||
def _getACSSForVoiceType(self, voiceType):
|
def _getACSSForVoiceType(self, voiceType):
|
||||||
"""Return the ACSS value for the given voice type.
|
"""Return the ACSS value for the given voice type.
|
||||||
|
|
||||||
@@ -3683,6 +4043,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
self.prefsDict['startingProfile'] = startingProfile
|
self.prefsDict['startingProfile'] = startingProfile
|
||||||
_settingsManager.setStartingProfile(startingProfile)
|
_settingsManager.setStartingProfile(startingProfile)
|
||||||
|
|
||||||
|
self._apply_plugin_changes()
|
||||||
self.writeUserPreferences()
|
self.writeUserPreferences()
|
||||||
cthulhu.loadUserSettings(self.script)
|
cthulhu.loadUserSettings(self.script)
|
||||||
braille.checkBrailleSetting()
|
braille.checkBrailleSetting()
|
||||||
|
|||||||
@@ -8,11 +8,15 @@
|
|||||||
|
|
||||||
"""Plugin System Manager for Cthulhu using pluggy."""
|
"""Plugin System Manager for Cthulhu using pluggy."""
|
||||||
|
|
||||||
import os
|
|
||||||
import inspect
|
|
||||||
import importlib.util
|
|
||||||
import logging
|
|
||||||
import configparser
|
import configparser
|
||||||
|
import hashlib
|
||||||
|
import importlib.util
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
# Import pluggy if available
|
# Import pluggy if available
|
||||||
@@ -54,11 +58,15 @@ class PluginType(IntEnum):
|
|||||||
class PluginInfo:
|
class PluginInfo:
|
||||||
"""Information about a plugin."""
|
"""Information about a plugin."""
|
||||||
|
|
||||||
def __init__(self, name, module_name, module_dir, metadata=None):
|
def __init__(self, name, module_name, module_dir, metadata=None, canonical_name=None, source_id=None, origin=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.module_name = module_name
|
self.module_name = module_name
|
||||||
self.module_dir = module_dir
|
self.module_dir = module_dir
|
||||||
self.metadata = metadata or {}
|
self.metadata = metadata or {}
|
||||||
|
self.canonical_name = canonical_name or module_name
|
||||||
|
self.source_id = source_id or "unknown"
|
||||||
|
self.origin = origin or "unknown"
|
||||||
|
self.preferred_alias = False
|
||||||
self.builtin = False
|
self.builtin = False
|
||||||
self.hidden = False
|
self.hidden = False
|
||||||
self.module = None
|
self.module = None
|
||||||
@@ -68,6 +76,9 @@ class PluginInfo:
|
|||||||
def get_module_name(self):
|
def get_module_name(self):
|
||||||
return self.module_name
|
return self.module_name
|
||||||
|
|
||||||
|
def get_canonical_name(self):
|
||||||
|
return self.canonical_name
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
return self.metadata.get('name', self.name)
|
return self.metadata.get('name', self.name)
|
||||||
|
|
||||||
@@ -77,6 +88,17 @@ class PluginInfo:
|
|||||||
def get_description(self):
|
def get_description(self):
|
||||||
return self.metadata.get('description', '')
|
return self.metadata.get('description', '')
|
||||||
|
|
||||||
|
def get_source_id(self):
|
||||||
|
return self.source_id
|
||||||
|
|
||||||
|
def get_origin(self):
|
||||||
|
return self.origin
|
||||||
|
|
||||||
|
def get_source_label(self):
|
||||||
|
if self.origin == "sources":
|
||||||
|
return self.source_id
|
||||||
|
return self.origin
|
||||||
|
|
||||||
def get_module_dir(self):
|
def get_module_dir(self):
|
||||||
return self.module_dir
|
return self.module_dir
|
||||||
|
|
||||||
@@ -117,6 +139,7 @@ class PluginSystemManager:
|
|||||||
|
|
||||||
# Plugin storage
|
# Plugin storage
|
||||||
self._plugins = {} # module_name -> PluginInfo
|
self._plugins = {} # module_name -> PluginInfo
|
||||||
|
self._plugin_name_index = {} # canonical_name -> [module_name]
|
||||||
self._active_plugins = []
|
self._active_plugins = []
|
||||||
|
|
||||||
# Create plugin directories
|
# Create plugin directories
|
||||||
@@ -310,6 +333,53 @@ class PluginSystemManager:
|
|||||||
"""Ensure plugin directories exist."""
|
"""Ensure plugin directories exist."""
|
||||||
os.makedirs(PluginType.SYSTEM.get_root_dir(), exist_ok=True)
|
os.makedirs(PluginType.SYSTEM.get_root_dir(), exist_ok=True)
|
||||||
os.makedirs(PluginType.USER.get_root_dir(), exist_ok=True)
|
os.makedirs(PluginType.USER.get_root_dir(), exist_ok=True)
|
||||||
|
os.makedirs(self._get_plugin_sources_root(), exist_ok=True)
|
||||||
|
|
||||||
|
def _get_plugin_sources_root(self):
|
||||||
|
return os.path.expanduser('~/.local/share/cthulhu/plugin-sources')
|
||||||
|
|
||||||
|
def _get_additional_plugin_dirs(self):
|
||||||
|
return [os.path.expanduser('~/.local/share/plugins')]
|
||||||
|
|
||||||
|
def _path_under_root(self, path, root):
|
||||||
|
try:
|
||||||
|
return os.path.commonpath([os.path.abspath(path), os.path.abspath(root)]) == os.path.abspath(root)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _sanitize_source_id(self, source_id):
|
||||||
|
return re.sub(r'[^a-zA-Z0-9._-]+', '-', source_id).strip('-') or "source"
|
||||||
|
|
||||||
|
def _get_origin_info(self, plugin_dir):
|
||||||
|
system_root = PluginType.SYSTEM.get_root_dir()
|
||||||
|
user_root = PluginType.USER.get_root_dir()
|
||||||
|
local_root = os.path.expanduser('~/.local/share/plugins')
|
||||||
|
sources_root = self._get_plugin_sources_root()
|
||||||
|
|
||||||
|
if self._path_under_root(plugin_dir, system_root):
|
||||||
|
return ("system", "system")
|
||||||
|
if self._path_under_root(plugin_dir, user_root):
|
||||||
|
return ("user", "user")
|
||||||
|
if self._path_under_root(plugin_dir, local_root):
|
||||||
|
return ("local", "local")
|
||||||
|
if self._path_under_root(plugin_dir, sources_root):
|
||||||
|
rel_path = os.path.relpath(plugin_dir, sources_root)
|
||||||
|
source_id = rel_path.split(os.sep, 1)[0]
|
||||||
|
return ("sources", self._sanitize_source_id(source_id))
|
||||||
|
|
||||||
|
return ("unknown", "unknown")
|
||||||
|
|
||||||
|
def _make_unique_plugin_id(self, canonical_name, source_id):
|
||||||
|
base_id = canonical_name
|
||||||
|
if canonical_name in self._plugin_name_index:
|
||||||
|
base_id = f"{canonical_name}@{self._sanitize_source_id(source_id)}"
|
||||||
|
|
||||||
|
unique_id = base_id
|
||||||
|
suffix = 2
|
||||||
|
while unique_id in self._plugins:
|
||||||
|
unique_id = f"{base_id}-{suffix}"
|
||||||
|
suffix += 1
|
||||||
|
return unique_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plugins(self):
|
def plugins(self):
|
||||||
@@ -323,10 +393,15 @@ class PluginSystemManager:
|
|||||||
"""Scan for plugins in the plugin directories."""
|
"""Scan for plugins in the plugin directories."""
|
||||||
old_plugins = self._plugins.copy()
|
old_plugins = self._plugins.copy()
|
||||||
self._plugins = {}
|
self._plugins = {}
|
||||||
|
self._plugin_name_index = {}
|
||||||
|
|
||||||
# Scan system and user plugins
|
# Scan system and user plugins
|
||||||
self._scan_plugins_in_directory(PluginType.SYSTEM.get_root_dir())
|
self._scan_plugins_in_directory(PluginType.SYSTEM.get_root_dir())
|
||||||
self._scan_plugins_in_directory(PluginType.USER.get_root_dir())
|
self._scan_plugins_in_directory(PluginType.USER.get_root_dir(), max_depth=1)
|
||||||
|
for plugin_dir in self._get_additional_plugin_dirs():
|
||||||
|
if os.path.isdir(plugin_dir):
|
||||||
|
self._scan_plugins_in_directory(plugin_dir, max_depth=1)
|
||||||
|
self._scan_plugins_in_directory(self._get_plugin_sources_root(), max_depth=1)
|
||||||
|
|
||||||
# Preserve state for already loaded plugins
|
# Preserve state for already loaded plugins
|
||||||
for name, old_info in old_plugins.items():
|
for name, old_info in old_plugins.items():
|
||||||
@@ -335,52 +410,157 @@ class PluginSystemManager:
|
|||||||
self._plugins[name].instance = old_info.instance
|
self._plugins[name].instance = old_info.instance
|
||||||
self._plugins[name].module = old_info.module
|
self._plugins[name].module = old_info.module
|
||||||
|
|
||||||
def _scan_plugins_in_directory(self, directory):
|
def _scan_plugins_in_directory(self, directory, max_depth=0, _depth=0):
|
||||||
"""Scan for plugins in a directory."""
|
"""Scan for plugins in a directory."""
|
||||||
if not os.path.exists(directory) or not os.path.isdir(directory):
|
if not os.path.exists(directory) or not os.path.isdir(directory):
|
||||||
logger.warning(f"Plugin directory not found or not a directory: {directory}")
|
if _depth == 0:
|
||||||
|
logger.warning(f"Plugin directory not found or not a directory: {directory}")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(f"Scanning for plugins in directory: {directory}")
|
if _depth == 0:
|
||||||
for item in os.listdir(directory):
|
logger.info(f"Scanning for plugins in directory: {directory}")
|
||||||
|
|
||||||
|
for item in sorted(os.listdir(directory)):
|
||||||
|
if item.startswith('.'):
|
||||||
|
continue
|
||||||
plugin_dir = os.path.join(directory, item)
|
plugin_dir = os.path.join(directory, item)
|
||||||
if not os.path.isdir(plugin_dir):
|
if not os.path.isdir(plugin_dir):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check for the traditional structure first (plugin.py & plugin.info)
|
if self._register_plugin_from_directory(plugin_dir):
|
||||||
plugin_file = os.path.join(plugin_dir, "plugin.py")
|
continue
|
||||||
metadata_file = os.path.join(plugin_dir, "plugin.info")
|
|
||||||
|
|
||||||
# Fall back to [PluginName].py if plugin.py doesn't exist
|
if _depth < max_depth:
|
||||||
if not os.path.isfile(plugin_file):
|
self._scan_plugins_in_directory(plugin_dir, max_depth=max_depth, _depth=_depth + 1)
|
||||||
alternative_plugin_file = os.path.join(plugin_dir, f"{item}.py")
|
elif max_depth == 0:
|
||||||
if os.path.isfile(alternative_plugin_file):
|
|
||||||
plugin_file = alternative_plugin_file
|
|
||||||
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
|
|
||||||
|
|
||||||
# Check if we have any valid plugin file
|
|
||||||
if os.path.isfile(plugin_file):
|
|
||||||
# Extract plugin info
|
|
||||||
module_name = os.path.basename(plugin_dir)
|
|
||||||
logger.info(f"Found plugin: {module_name} in {plugin_dir}")
|
|
||||||
metadata = self._load_plugin_metadata(metadata_file)
|
|
||||||
|
|
||||||
plugin_info = PluginInfo(
|
|
||||||
metadata.get('name', module_name),
|
|
||||||
module_name,
|
|
||||||
plugin_dir,
|
|
||||||
metadata
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if it's a built-in or hidden plugin
|
|
||||||
plugin_info.builtin = metadata.get('builtin', 'false').lower() == 'true'
|
|
||||||
plugin_info.hidden = metadata.get('hidden', 'false').lower() == 'true'
|
|
||||||
|
|
||||||
logger.info(f"Adding plugin to registry: {module_name}")
|
|
||||||
self._plugins[module_name] = plugin_info
|
|
||||||
else:
|
|
||||||
logger.warning(f"No plugin file found in directory: {plugin_dir}")
|
logger.warning(f"No plugin file found in directory: {plugin_dir}")
|
||||||
|
|
||||||
|
def _register_plugin_from_directory(self, plugin_dir):
|
||||||
|
item = os.path.basename(plugin_dir)
|
||||||
|
plugin_file = os.path.join(plugin_dir, "plugin.py")
|
||||||
|
metadata_file = os.path.join(plugin_dir, "plugin.info")
|
||||||
|
|
||||||
|
if not os.path.isfile(plugin_file):
|
||||||
|
alternative_plugin_file = os.path.join(plugin_dir, f"{item}.py")
|
||||||
|
if os.path.isfile(alternative_plugin_file):
|
||||||
|
plugin_file = alternative_plugin_file
|
||||||
|
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
|
||||||
|
|
||||||
|
if not os.path.isfile(plugin_file):
|
||||||
|
return False
|
||||||
|
|
||||||
|
canonical_name = os.path.basename(plugin_dir)
|
||||||
|
origin, source_id = self._get_origin_info(plugin_dir)
|
||||||
|
module_name = self._make_unique_plugin_id(canonical_name, source_id)
|
||||||
|
if canonical_name != module_name:
|
||||||
|
logger.warning(
|
||||||
|
"Duplicate plugin name detected: "
|
||||||
|
f"{canonical_name} (origin={origin}, source={source_id}). "
|
||||||
|
f"Registering as {module_name}."
|
||||||
|
)
|
||||||
|
logger.info(f"Found plugin: {canonical_name} in {plugin_dir}")
|
||||||
|
metadata = self._load_plugin_metadata(metadata_file)
|
||||||
|
|
||||||
|
plugin_info = PluginInfo(
|
||||||
|
metadata.get('name', canonical_name),
|
||||||
|
module_name,
|
||||||
|
plugin_dir,
|
||||||
|
metadata,
|
||||||
|
canonical_name=canonical_name,
|
||||||
|
source_id=source_id,
|
||||||
|
origin=origin,
|
||||||
|
)
|
||||||
|
|
||||||
|
plugin_info.builtin = metadata.get('builtin', 'false').lower() == 'true'
|
||||||
|
plugin_info.hidden = metadata.get('hidden', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
if canonical_name not in self._plugin_name_index:
|
||||||
|
plugin_info.preferred_alias = True
|
||||||
|
self._plugin_name_index[canonical_name] = []
|
||||||
|
self._plugin_name_index[canonical_name].append(module_name)
|
||||||
|
|
||||||
|
logger.info(f"Adding plugin to registry: {module_name}")
|
||||||
|
self._plugins[module_name] = plugin_info
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _plugin_source_dir(self, source_url):
|
||||||
|
base_name = source_url.rstrip('/').split('/')[-1] or 'plugin-source'
|
||||||
|
if base_name.endswith('.git'):
|
||||||
|
base_name = base_name[:-4]
|
||||||
|
base_name = re.sub(r'[^a-zA-Z0-9._-]+', '-', base_name).strip('-') or 'plugin-source'
|
||||||
|
digest = hashlib.sha1(source_url.encode('utf-8')).hexdigest()[:8]
|
||||||
|
return os.path.join(self._get_plugin_sources_root(), f"{base_name}-{digest}")
|
||||||
|
|
||||||
|
def syncPluginSources(self, sources, progress_callback=None):
|
||||||
|
"""Clone or update plugin source repositories."""
|
||||||
|
if not sources:
|
||||||
|
return
|
||||||
|
|
||||||
|
git_path = shutil.which("git")
|
||||||
|
if not git_path:
|
||||||
|
logger.error("Git not available; cannot sync plugin sources.")
|
||||||
|
return
|
||||||
|
|
||||||
|
os.makedirs(self._get_plugin_sources_root(), exist_ok=True)
|
||||||
|
total = len(sources)
|
||||||
|
for index, source in enumerate(sources, start=1):
|
||||||
|
source = source.strip()
|
||||||
|
if not source:
|
||||||
|
continue
|
||||||
|
dest_dir = self._plugin_source_dir(source)
|
||||||
|
if os.path.isdir(dest_dir):
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[git_path, "-C", dest_dir, "pull", "--ff-only"],
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
logger.info(f"Updated plugin source: {source}")
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(index, total, f"Updated {source}")
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
logger.error(f"Failed to update plugin source {source}: {exc.stderr.decode('utf-8', 'ignore')}")
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(index, total, f"Failed to update {source}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[git_path, "clone", source, dest_dir],
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
logger.info(f"Installed plugin source: {source}")
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(index, total, f"Installed {source}")
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
logger.error(f"Failed to clone plugin source {source}: {exc.stderr.decode('utf-8', 'ignore')}")
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(index, total, f"Failed to clone {source}")
|
||||||
|
|
||||||
|
def removePluginSources(self, sources):
|
||||||
|
"""Remove plugin source repositories."""
|
||||||
|
if not sources:
|
||||||
|
return
|
||||||
|
|
||||||
|
sources_root = self._get_plugin_sources_root()
|
||||||
|
for source in sources:
|
||||||
|
source = source.strip()
|
||||||
|
if not source:
|
||||||
|
continue
|
||||||
|
dest_dir = self._plugin_source_dir(source)
|
||||||
|
if not os.path.isdir(dest_dir):
|
||||||
|
continue
|
||||||
|
if os.path.commonpath([dest_dir, sources_root]) != sources_root:
|
||||||
|
logger.warning(f"Skipping removal outside plugin sources root: {dest_dir}")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
shutil.rmtree(dest_dir)
|
||||||
|
logger.info(f"Removed plugin source: {source}")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"Failed to remove plugin source {source}: {exc}")
|
||||||
|
|
||||||
def _load_plugin_metadata(self, metadata_file):
|
def _load_plugin_metadata(self, metadata_file):
|
||||||
"""Load plugin metadata from a file."""
|
"""Load plugin metadata from a file."""
|
||||||
metadata = {}
|
metadata = {}
|
||||||
@@ -448,10 +628,71 @@ class PluginSystemManager:
|
|||||||
logger.info("No plugins found, rescanning...")
|
logger.info("No plugins found, rescanning...")
|
||||||
self.rescanPlugins()
|
self.rescanPlugins()
|
||||||
|
|
||||||
self._active_plugins = activePlugins
|
plugin_by_id = {info.get_module_name(): info for info in self.plugins}
|
||||||
|
plugin_by_id_lower = {name.lower(): info for name, info in plugin_by_id.items()}
|
||||||
|
preferred_by_canonical = {
|
||||||
|
info.get_canonical_name().lower(): info.get_module_name()
|
||||||
|
for info in self.plugins if info.preferred_alias
|
||||||
|
}
|
||||||
|
canonical_with_builtin = {
|
||||||
|
info.get_canonical_name().lower()
|
||||||
|
for info in self.plugins if info.builtin
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized_active = []
|
||||||
|
seen_canonicals = set()
|
||||||
|
unknown_active = []
|
||||||
|
|
||||||
|
for name in activePlugins:
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
name_lower = name.lower()
|
||||||
|
plugin_info = plugin_by_id.get(name) or plugin_by_id_lower.get(name_lower)
|
||||||
|
if plugin_info:
|
||||||
|
canonical_lower = plugin_info.get_canonical_name().lower()
|
||||||
|
if canonical_lower in canonical_with_builtin and not plugin_info.builtin:
|
||||||
|
logger.warning(
|
||||||
|
f"Skipping plugin {plugin_info.get_module_name()} because builtin plugin "
|
||||||
|
f"uses name {plugin_info.get_canonical_name()}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if canonical_lower in seen_canonicals:
|
||||||
|
logger.warning(
|
||||||
|
f"Skipping plugin {plugin_info.get_module_name()} because another plugin "
|
||||||
|
f"with name {plugin_info.get_canonical_name()} is already active"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
normalized_active.append(plugin_info.get_module_name())
|
||||||
|
seen_canonicals.add(canonical_lower)
|
||||||
|
continue
|
||||||
|
|
||||||
|
preferred_id = preferred_by_canonical.get(name_lower)
|
||||||
|
if preferred_id:
|
||||||
|
preferred_info = plugin_by_id.get(preferred_id)
|
||||||
|
canonical_lower = name_lower
|
||||||
|
if preferred_info and canonical_lower in canonical_with_builtin and not preferred_info.builtin:
|
||||||
|
logger.warning(
|
||||||
|
f"Skipping plugin {preferred_id} because builtin plugin uses name {preferred_info.get_canonical_name()}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if canonical_lower in seen_canonicals:
|
||||||
|
logger.warning(
|
||||||
|
f"Skipping plugin {preferred_id} because another plugin with name {name} is already active"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
normalized_active.append(preferred_id)
|
||||||
|
seen_canonicals.add(canonical_lower)
|
||||||
|
continue
|
||||||
|
|
||||||
|
unknown_active.append(name)
|
||||||
|
|
||||||
|
self._active_plugins = normalized_active + unknown_active
|
||||||
|
|
||||||
# Log active vs available plugins
|
# Log active vs available plugins
|
||||||
available_plugins = [p.get_module_name() for p in self.plugins]
|
available_plugins = [p.get_module_name() for p in self.plugins]
|
||||||
|
available_aliases = [p.get_canonical_name() for p in self.plugins if p.preferred_alias]
|
||||||
|
available_plugins_lower = {p.lower() for p in available_plugins}
|
||||||
|
available_aliases_lower = {p.lower() for p in available_aliases}
|
||||||
logger.info(f"Available plugins: {available_plugins}")
|
logger.info(f"Available plugins: {available_plugins}")
|
||||||
logger.info(f"Active plugins: {self._active_plugins}")
|
logger.info(f"Active plugins: {self._active_plugins}")
|
||||||
|
|
||||||
@@ -462,7 +703,13 @@ class PluginSystemManager:
|
|||||||
logger.warning("DisplayVersion is NOT in active plugins list!")
|
logger.warning("DisplayVersion is NOT in active plugins list!")
|
||||||
|
|
||||||
# Find missing plugins
|
# Find missing plugins
|
||||||
missing_plugins = [p for p in self._active_plugins if p not in available_plugins]
|
missing_plugins = [
|
||||||
|
p for p in self._active_plugins
|
||||||
|
if p not in available_plugins
|
||||||
|
and p not in available_aliases
|
||||||
|
and p.lower() not in available_plugins_lower
|
||||||
|
and p.lower() not in available_aliases_lower
|
||||||
|
]
|
||||||
if missing_plugins:
|
if missing_plugins:
|
||||||
logger.warning(f"Active plugins not found: {missing_plugins}")
|
logger.warning(f"Active plugins not found: {missing_plugins}")
|
||||||
|
|
||||||
@@ -488,6 +735,7 @@ class PluginSystemManager:
|
|||||||
def isPluginActive(self, pluginInfo):
|
def isPluginActive(self, pluginInfo):
|
||||||
"""Check if a plugin is active."""
|
"""Check if a plugin is active."""
|
||||||
module_name = pluginInfo.get_module_name()
|
module_name = pluginInfo.get_module_name()
|
||||||
|
canonical_name = pluginInfo.get_canonical_name()
|
||||||
|
|
||||||
# Builtin plugins are always active
|
# Builtin plugins are always active
|
||||||
if pluginInfo.builtin:
|
if pluginInfo.builtin:
|
||||||
@@ -507,9 +755,16 @@ class PluginSystemManager:
|
|||||||
logger.debug(f"Plugin {module_name} found in active plugins list")
|
logger.debug(f"Plugin {module_name} found in active plugins list")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if pluginInfo.preferred_alias and canonical_name in active_plugins:
|
||||||
|
logger.debug(f"Plugin {module_name} matched canonical name {canonical_name}")
|
||||||
|
return True
|
||||||
|
|
||||||
# Try case-insensitive match
|
# Try case-insensitive match
|
||||||
module_name_lower = module_name.lower()
|
module_name_lower = module_name.lower()
|
||||||
|
canonical_lower = canonical_name.lower()
|
||||||
is_active = any(plugin.lower() == module_name_lower for plugin in active_plugins)
|
is_active = any(plugin.lower() == module_name_lower for plugin in active_plugins)
|
||||||
|
if not is_active and pluginInfo.preferred_alias:
|
||||||
|
is_active = any(plugin.lower() == canonical_lower for plugin in active_plugins)
|
||||||
|
|
||||||
if is_active:
|
if is_active:
|
||||||
logger.debug(f"Plugin {module_name} found in active plugins list (case-insensitive match)")
|
logger.debug(f"Plugin {module_name} found in active plugins list (case-insensitive match)")
|
||||||
@@ -573,7 +828,8 @@ class PluginSystemManager:
|
|||||||
|
|
||||||
# Fall back to [PluginName].py if plugin.py doesn't exist
|
# Fall back to [PluginName].py if plugin.py doesn't exist
|
||||||
if not os.path.exists(plugin_file):
|
if not os.path.exists(plugin_file):
|
||||||
alternative_plugin_file = os.path.join(plugin_dir, f"{module_name}.py")
|
canonical_name = pluginInfo.get_canonical_name()
|
||||||
|
alternative_plugin_file = os.path.join(plugin_dir, f"{canonical_name}.py")
|
||||||
if os.path.exists(alternative_plugin_file):
|
if os.path.exists(alternative_plugin_file):
|
||||||
plugin_file = alternative_plugin_file
|
plugin_file = alternative_plugin_file
|
||||||
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
|
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ class PluginManager(Plugin):
|
|||||||
try:
|
try:
|
||||||
debug.printMessage(debug.LEVEL_INFO, "PluginManager: Plugin activation starting", True)
|
debug.printMessage(debug.LEVEL_INFO, "PluginManager: Plugin activation starting", True)
|
||||||
|
|
||||||
# Register keybinding for opening plugin manager (Cthulhu+Shift+P)
|
|
||||||
self._register_keybinding()
|
|
||||||
|
|
||||||
self._activated = True
|
self._activated = True
|
||||||
debug.printMessage(debug.LEVEL_INFO, "PluginManager: Plugin activated successfully", True)
|
debug.printMessage(debug.LEVEL_INFO, "PluginManager: Plugin activated successfully", True)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ userCustomizableSettings = [
|
|||||||
"presentTimeFormat",
|
"presentTimeFormat",
|
||||||
"activeProfile",
|
"activeProfile",
|
||||||
"activePlugins",
|
"activePlugins",
|
||||||
|
"pluginSources",
|
||||||
"startingProfile",
|
"startingProfile",
|
||||||
"spellcheckSpellError",
|
"spellcheckSpellError",
|
||||||
"spellcheckSpellSuggestion",
|
"spellcheckSpellSuggestion",
|
||||||
@@ -490,6 +491,7 @@ presentLiveRegionFromInactiveTab = False
|
|||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
activePlugins = ['AIAssistant', 'DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'IndentationAudio']
|
activePlugins = ['AIAssistant', 'DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'IndentationAudio']
|
||||||
|
pluginSources = []
|
||||||
|
|
||||||
# AI Assistant settings (disabled by default for opt-in behavior)
|
# AI Assistant settings (disabled by default for opt-in behavior)
|
||||||
aiAssistantEnabled = True
|
aiAssistantEnabled = True
|
||||||
|
|||||||
Reference in New Issue
Block a user