First round of bug fixes and plugin capability extensions. Creating a preferences tab should no longer require editing Cthulhu itself.

This commit is contained in:
Storm Dragon
2026-01-13 07:49:51 -05:00
parent c510f5a45c
commit 06cd376cd4
12 changed files with 607 additions and 15 deletions
+8
View File
@@ -29,6 +29,7 @@ toolkit, OpenOffice/LibreOffice, Gecko, WebKitGtk, and KDE Qt toolkit.
- **Extensible architecture**: Plugin system using pluggy framework - **Extensible architecture**: Plugin system using pluggy framework
- **Hot-reloadable plugins**: Add functionality without restarting - **Hot-reloadable plugins**: Add functionality without restarting
- **Community plugins**: User and system plugin directories - **Community plugins**: User and system plugin directories
- **Plugin preferences**: Plugins can provide preferences pages that appear only when the plugin is active
### Remote Control ### Remote Control
- **D-Bus interface**: External control via D-Bus service - **D-Bus interface**: External control via D-Bus service
@@ -83,6 +84,13 @@ The `PluginSystemManager` module provides **session-only** plugin control (no pr
- `SetPluginActive` (parameterized) - `SetPluginActive` (parameterized)
- `RescanPlugins` - `RescanPlugins`
### Plugin Preferences Pages
Plugins can add their own Preferences tab without modifying Cthulhu core code.
Implement `getPreferencesGUI()` to return a Gtk widget (or `(widget, label)`),
and `getPreferencesFromGUI()` to return a settings dict. The tab appears only
when the plugin is active.
### More Documentation ### More Documentation
See `README-REMOTE-CONTROLLER.md` and `REMOTE-CONTROLLER-COMMANDS.md` for the full D-Bus API See `README-REMOTE-CONTROLLER.md` and `REMOTE-CONTROLLER-COMMANDS.md` for the full D-Bus API
+12 -1
View File
@@ -470,7 +470,18 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False):
cthulhuApp.getSignalManager().emitSignal('load-setting-begin') cthulhuApp.getSignalManager().emitSignal('load-setting-begin')
# NOW load plugins after script system is ready # NOW load plugins after script system is ready
activePlugins = list(_settingsManager.getSetting('activePlugins')) activePluginsSetting = _settingsManager.getSetting('activePlugins') or []
debug.printMessage(
debug.LEVEL_INFO,
f"CTHULHU: Active plugins defaults: {settings.activePlugins}",
True,
)
debug.printMessage(
debug.LEVEL_INFO,
f"CTHULHU: Active plugins from settings manager: {activePluginsSetting}",
True,
)
activePlugins = list(activePluginsSetting)
debug.printMessage(debug.LEVEL_INFO, f'CTHULHU: Loading active plugins: {activePlugins}', True) debug.printMessage(debug.LEVEL_INFO, f'CTHULHU: Loading active plugins: {activePlugins}', True)
cthulhuApp.getPluginSystemManager().setActivePlugins(activePlugins) cthulhuApp.getPluginSystemManager().setActivePlugins(activePlugins)
+176 -2
View File
@@ -155,6 +155,9 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._plugin_update_progress = None self._plugin_update_progress = None
self._plugin_update_status = None self._plugin_update_status = None
self._plugin_update_in_progress = False self._plugin_update_in_progress = False
self._plugin_tabs = {}
self._plugin_tabs_cached = False
self._dynamic_plugin_tabs = {}
self.screenHeight = None self.screenHeight = None
self.screenWidth = None self.screenWidth = None
self.speechFamiliesChoice = None self.speechFamiliesChoice = None
@@ -396,6 +399,9 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
label = Gtk.Label(label=AXObject.get_name(self.script.app)) label = Gtk.Label(label=AXObject.get_name(self.script.app))
self.get_widget("notebook").append_page(appPage, label) self.get_widget("notebook").append_page(appPage, label)
self._cache_plugin_tabs()
self._update_plugin_tabs()
self._refresh_dynamic_plugin_tabs()
self._initGUIState() self._initGUIState()
self._initSoundThemeState() self._initSoundThemeState()
@@ -799,6 +805,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
path = self._plugin_model.get_path(new_iter) path = self._plugin_model.get_path(new_iter)
self._plugin_treeview.expand_to_path(path) self._plugin_treeview.expand_to_path(path)
selection.select_path(path) selection.select_path(path)
self._update_plugin_tabs()
def _get_active_plugins_from_ui(self): def _get_active_plugins_from_ui(self):
existing_plugins = list(self.prefsDict.get("activePlugins", settings.activePlugins) or []) existing_plugins = list(self.prefsDict.get("activePlugins", settings.activePlugins) or [])
@@ -835,6 +842,147 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._plugin_sources_original = list(plugin_sources) self._plugin_sources_original = list(plugin_sources)
self._populate_plugin_list() self._populate_plugin_list()
self._update_plugin_tabs(active_plugins)
def _get_active_plugin_infos(self):
manager = cthulhu.cthulhuApp.getPluginSystemManager()
if not manager:
return []
return [info for info in manager.plugins if info.loaded and info.instance]
def _get_plugin_preferences_page(self, plugin_info):
plugin_instance = plugin_info.instance
if not plugin_instance:
return None
if hasattr(plugin_instance, "getPreferencesGUI"):
page = plugin_instance.getPreferencesGUI()
elif hasattr(plugin_instance, "get_preferences_gui"):
page = plugin_instance.get_preferences_gui()
else:
return None
if not page:
return None
label = plugin_info.get_name() or plugin_info.get_module_name()
if isinstance(page, (list, tuple)) and len(page) == 2:
page, label = page
if isinstance(label, str):
label_widget = Gtk.Label(label=label)
else:
label_widget = label
return page, label_widget
def _refresh_dynamic_plugin_tabs(self):
notebook = self.get_widget("notebook")
if not notebook:
return
for tab in list(self._dynamic_plugin_tabs.values()):
page = tab.get("page")
if page:
page_num = notebook.page_num(page)
if page_num != -1:
notebook.remove_page(page_num)
self._dynamic_plugin_tabs = {}
plugin_infos = self._get_active_plugin_infos()
for plugin_info in sorted(plugin_infos, key=lambda item: (item.get_name() or item.get_module_name()).lower()):
result = self._get_plugin_preferences_page(plugin_info)
if not result:
continue
page, label = result
if not page or not label:
continue
notebook.append_page(page, label)
page.show_all()
label.show()
self._dynamic_plugin_tabs[plugin_info.get_module_name()] = {
"page": page,
"label": label,
}
for plugin_name in self._dynamic_plugin_tabs:
tab = self._plugin_tabs.get(plugin_name)
if not tab:
continue
page = tab.get("page")
if not page:
continue
page_num = notebook.page_num(page)
if page_num != -1:
notebook.remove_page(page_num)
def _cache_plugin_tabs(self):
if self._plugin_tabs_cached:
return
notebook = self.get_widget("notebook")
if not notebook:
return
tab_specs = [
("AIAssistant", "aiPage", "aiTabLabel"),
("OCR", "ocrGrid", "ocrTabLabel"),
]
for plugin_name, page_id, label_id in tab_specs:
page = self.get_widget(page_id)
label = self.get_widget(label_id)
if not page or not label:
continue
position = notebook.page_num(page)
self._plugin_tabs[plugin_name] = {
"page": page,
"label": label,
"position": position,
}
self._plugin_tabs_cached = True
def _get_active_plugins_for_tabs(self):
if self._plugin_iters:
return self._get_active_plugins_from_ui()
return list(self.prefsDict.get("activePlugins", settings.activePlugins) or [])
@staticmethod
def _plugin_active(active_plugins, names):
active_lower = {name.lower() for name in active_plugins}
return any(name.lower() in active_lower for name in names)
def _update_plugin_tabs(self, active_plugins=None):
if not self._plugin_tabs:
return
notebook = self.get_widget("notebook")
if not notebook:
return
if active_plugins is None:
active_plugins = self._get_active_plugins_for_tabs()
for plugin_name, tab in self._plugin_tabs.items():
if plugin_name in self._dynamic_plugin_tabs:
continue
page = tab["page"]
label = tab["label"]
position = tab["position"]
page_num = notebook.page_num(page)
is_active = self._plugin_active(active_plugins, [plugin_name])
if is_active and page_num == -1:
insert_pos = min(max(position, 0), notebook.get_n_pages())
notebook.insert_page(page, label, insert_pos)
page.show_all()
label.show()
elif not is_active and page_num != -1:
notebook.remove_page(page_num)
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.
@@ -864,11 +1012,31 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
pronunciationDict = self.getModelDict(self.pronunciationModel) pronunciationDict = self.getModelDict(self.pronunciationModel)
keyBindingsDict = self.getKeyBindingsModelDict(self.keyBindingsModel) keyBindingsDict = self.getKeyBindingsModelDict(self.keyBindingsModel)
self.prefsDict.update(self.script.getPreferencesFromGUI()) self.prefsDict.update(self.script.getPreferencesFromGUI())
self.prefsDict.update(self._get_plugin_preferences_from_gui())
_settingsManager.saveSettings(self.script, _settingsManager.saveSettings(self.script,
self.prefsDict, self.prefsDict,
pronunciationDict, pronunciationDict,
keyBindingsDict) keyBindingsDict)
def _get_plugin_preferences_from_gui(self):
preferences = {}
for plugin_info in self._get_active_plugin_infos():
plugin_instance = plugin_info.instance
if not plugin_instance:
continue
if hasattr(plugin_instance, "getPreferencesFromGUI"):
plugin_prefs = plugin_instance.getPreferencesFromGUI()
elif hasattr(plugin_instance, "get_preferences_from_gui"):
plugin_prefs = plugin_instance.get_preferences_from_gui()
else:
continue
if isinstance(plugin_prefs, dict):
preferences.update(plugin_prefs)
return preferences
def _getKeyValueForVoiceType(self, voiceType, key, useDefault=True): def _getKeyValueForVoiceType(self, voiceType, key, useDefault=True):
"""Look for the value of the given key in the voice dictionary """Look for the value of the given key in the voice dictionary
for the given voice type. for the given voice type.
@@ -2283,9 +2451,13 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
if self.script.app: if self.script.app:
self.get_widget('profilesFrame').set_sensitive(False) self.get_widget('profilesFrame').set_sensitive(False)
active_plugins = self._get_active_plugins_for_tabs()
self._update_plugin_tabs(active_plugins)
# AI Assistant settings # AI Assistant settings
# #
self._initAIState() if self._plugin_active(active_plugins, ["AIAssistant"]):
self._initAIState()
# Indentation settings # Indentation settings
# #
@@ -2293,7 +2465,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
# OCR Plugin settings # OCR Plugin settings
# #
self._initOCRState() if self._plugin_active(active_plugins, ["OCR"]):
self._initOCRState()
def __initProfileCombo(self): def __initProfileCombo(self):
"""Adding available profiles and setting active as the active one""" """Adding available profiles and setting active as the active one"""
@@ -4149,6 +4322,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._apply_plugin_changes() self._apply_plugin_changes()
self.writeUserPreferences() self.writeUserPreferences()
cthulhu.loadUserSettings(self.script) cthulhu.loadUserSettings(self.script)
self._refresh_dynamic_plugin_tabs()
braille.checkBrailleSetting() braille.checkBrailleSetting()
self._initSpeechState() self._initSpeechState()
self._populateKeyBindings() self._populateKeyBindings()
+8
View File
@@ -87,6 +87,14 @@ class Plugin:
"""Get keybindings for this plugin. Override in subclasses.""" """Get keybindings for this plugin. Override in subclasses."""
return self._bindings return self._bindings
def getPreferencesGUI(self):
"""Return a Gtk widget for plugin preferences, or None if not provided."""
return None
def getPreferencesFromGUI(self):
"""Return a dict of plugin preferences from the GUI."""
return {}
def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True): def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True):
"""Register a gesture by string.""" """Register a gesture by string."""
if self.app: if self.app:
+2 -2
View File
@@ -3,5 +3,5 @@ Name = AI Assistant
Module = AIAssistant Module = AIAssistant
Description = AI-powered accessibility assistant for analyzing screens and taking actions Description = AI-powered accessibility assistant for analyzing screens and taking actions
Authors = Stormux <storm_dragon@stormux.org> Authors = Stormux <storm_dragon@stormux.org>
Version = 1.0.0 Version = 2.0.0
Category = Accessibility Category = Accessibility
+211
View File
@@ -69,6 +69,8 @@ class AIAssistant(Plugin):
# Pre-captured screen data (to avoid capturing dialog itself) # Pre-captured screen data (to avoid capturing dialog itself)
self._current_screen_data = None self._current_screen_data = None
self._prefs_grid = None
self._prefs_widgets = {}
@cthulhu_hookimpl @cthulhu_hookimpl
def activate(self, plugin=None): def activate(self, plugin=None):
@@ -128,6 +130,215 @@ class AIAssistant(Plugin):
self._unregister_keybindings() self._unregister_keybindings()
self._enabled = False self._enabled = False
def getPreferencesGUI(self):
if not self._prefs_grid:
self._prefs_grid = self._build_preferences_gui()
self._load_preferences_into_widgets()
return self._prefs_grid, "AI Assistant"
def getPreferencesFromGUI(self):
if not self._prefs_widgets:
return {}
provider_values = self._prefs_widgets.get("provider_values", [])
quality_values = self._prefs_widgets.get("quality_values", [])
provider_index = self._prefs_widgets["provider_combo"].get_active()
quality_index = self._prefs_widgets["quality_combo"].get_active()
if provider_index < 0 or provider_index >= len(provider_values):
provider = settings.aiProvider
else:
provider = provider_values[provider_index]
if quality_index < 0 or quality_index >= len(quality_values):
screenshot_quality = settings.aiScreenshotQuality
else:
screenshot_quality = quality_values[quality_index]
return {
"aiAssistantEnabled": self._prefs_widgets["enable_check"].get_active(),
"aiProvider": provider,
"aiApiKeyFile": self._prefs_widgets["api_key_entry"].get_text().strip(),
"aiOllamaModel": self._prefs_widgets["ollama_model_entry"].get_text().strip(),
"aiOllamaEndpoint": self._prefs_widgets["ollama_endpoint_entry"].get_text().strip(),
"aiConfirmationRequired": self._prefs_widgets["confirmation_check"].get_active(),
"aiScreenshotQuality": screenshot_quality,
}
def _build_preferences_gui(self):
grid = Gtk.Grid(
row_spacing=6,
column_spacing=12,
margin_left=12,
margin_right=12,
margin_top=12,
margin_bottom=12,
)
row = 0
enable_check = Gtk.CheckButton(label="Enable AI Assistant")
enable_check.connect("toggled", self._on_ai_enabled_toggled)
grid.attach(enable_check, 0, row, 2, 1)
row += 1
provider_label = Gtk.Label(label="_Provider:")
provider_label.set_use_underline(True)
provider_label.set_halign(Gtk.Align.START)
provider_combo = Gtk.ComboBoxText()
provider_combo.set_hexpand(True)
provider_values = [
settings.AI_PROVIDER_CLAUDE_CODE,
settings.AI_PROVIDER_CODEX,
settings.AI_PROVIDER_GEMINI,
settings.AI_PROVIDER_OLLAMA,
]
provider_labels = ["Claude Code", "Codex", "Gemini", "Ollama"]
for label in provider_labels:
provider_combo.append_text(label)
provider_combo.connect("changed", self._on_provider_changed)
provider_label.set_mnemonic_widget(provider_combo)
grid.attach(provider_label, 0, row, 1, 1)
grid.attach(provider_combo, 1, row, 1, 1)
row += 1
api_key_label = Gtk.Label(label="API _Key File:")
api_key_label.set_use_underline(True)
api_key_label.set_halign(Gtk.Align.START)
api_key_entry = Gtk.Entry()
api_key_entry.set_hexpand(True)
api_key_label.set_mnemonic_widget(api_key_entry)
grid.attach(api_key_label, 0, row, 1, 1)
grid.attach(api_key_entry, 1, row, 1, 1)
row += 1
ollama_model_label = Gtk.Label(label="_Ollama Model:")
ollama_model_label.set_use_underline(True)
ollama_model_label.set_halign(Gtk.Align.START)
ollama_model_entry = Gtk.Entry()
ollama_model_entry.set_hexpand(True)
ollama_model_label.set_mnemonic_widget(ollama_model_entry)
grid.attach(ollama_model_label, 0, row, 1, 1)
grid.attach(ollama_model_entry, 1, row, 1, 1)
row += 1
ollama_endpoint_label = Gtk.Label(label="Ollama _Endpoint:")
ollama_endpoint_label.set_use_underline(True)
ollama_endpoint_label.set_halign(Gtk.Align.START)
ollama_endpoint_entry = Gtk.Entry()
ollama_endpoint_entry.set_hexpand(True)
ollama_endpoint_label.set_mnemonic_widget(ollama_endpoint_entry)
grid.attach(ollama_endpoint_label, 0, row, 1, 1)
grid.attach(ollama_endpoint_entry, 1, row, 1, 1)
row += 1
confirmation_check = Gtk.CheckButton(label="Require confirmation before actions")
grid.attach(confirmation_check, 0, row, 2, 1)
row += 1
quality_label = Gtk.Label(label="Screenshot _Quality:")
quality_label.set_use_underline(True)
quality_label.set_halign(Gtk.Align.START)
quality_combo = Gtk.ComboBoxText()
quality_combo.set_hexpand(True)
quality_values = [
settings.AI_SCREENSHOT_QUALITY_LOW,
settings.AI_SCREENSHOT_QUALITY_MEDIUM,
settings.AI_SCREENSHOT_QUALITY_HIGH,
]
quality_labels = ["Low", "Medium", "High"]
for label in quality_labels:
quality_combo.append_text(label)
quality_label.set_mnemonic_widget(quality_combo)
grid.attach(quality_label, 0, row, 1, 1)
grid.attach(quality_combo, 1, row, 1, 1)
self._prefs_widgets = {
"enable_check": enable_check,
"provider_combo": provider_combo,
"provider_values": provider_values,
"api_key_entry": api_key_entry,
"ollama_model_entry": ollama_model_entry,
"ollama_endpoint_entry": ollama_endpoint_entry,
"confirmation_check": confirmation_check,
"quality_combo": quality_combo,
"quality_values": quality_values,
}
return grid
def _load_preferences_into_widgets(self):
if not self._prefs_widgets:
return
enabled = self._settings_manager.getSetting("aiAssistantEnabled")
if enabled is None:
enabled = settings.aiAssistantEnabled
self._prefs_widgets["enable_check"].set_active(enabled)
provider = self._settings_manager.getSetting("aiProvider") or settings.aiProvider
provider_values = self._prefs_widgets.get("provider_values", [])
try:
provider_index = provider_values.index(provider)
except ValueError:
provider_index = 0
self._prefs_widgets["provider_combo"].set_active(provider_index)
api_key_file = self._settings_manager.getSetting("aiApiKeyFile") or settings.aiApiKeyFile
self._prefs_widgets["api_key_entry"].set_text(api_key_file or "")
ollama_model = self._settings_manager.getSetting("aiOllamaModel") or settings.aiOllamaModel
self._prefs_widgets["ollama_model_entry"].set_text(ollama_model or "")
ollama_endpoint = self._settings_manager.getSetting("aiOllamaEndpoint") or settings.aiOllamaEndpoint
self._prefs_widgets["ollama_endpoint_entry"].set_text(ollama_endpoint or "")
confirmation_required = self._settings_manager.getSetting("aiConfirmationRequired")
if confirmation_required is None:
confirmation_required = settings.aiConfirmationRequired
self._prefs_widgets["confirmation_check"].set_active(confirmation_required)
screenshot_quality = self._settings_manager.getSetting("aiScreenshotQuality") or settings.aiScreenshotQuality
quality_values = self._prefs_widgets.get("quality_values", [])
try:
quality_index = quality_values.index(screenshot_quality)
except ValueError:
quality_index = 0
self._prefs_widgets["quality_combo"].set_active(quality_index)
self._update_preferences_sensitivity()
def _on_ai_enabled_toggled(self, widget):
self._update_preferences_sensitivity()
def _on_provider_changed(self, widget):
self._update_preferences_sensitivity()
def _update_preferences_sensitivity(self):
if not self._prefs_widgets:
return
enabled = self._prefs_widgets["enable_check"].get_active()
provider_values = self._prefs_widgets.get("provider_values", [])
provider_index = self._prefs_widgets["provider_combo"].get_active()
provider = provider_values[provider_index] if 0 <= provider_index < len(provider_values) else settings.aiProvider
is_gemini = provider == settings.AI_PROVIDER_GEMINI
is_ollama = provider == settings.AI_PROVIDER_OLLAMA
self._prefs_widgets["provider_combo"].set_sensitive(enabled)
self._prefs_widgets["api_key_entry"].set_sensitive(enabled and is_gemini)
self._prefs_widgets["ollama_model_entry"].set_sensitive(enabled and is_ollama)
self._prefs_widgets["ollama_endpoint_entry"].set_sensitive(enabled and is_ollama)
self._prefs_widgets["confirmation_check"].set_sensitive(enabled)
self._prefs_widgets["quality_combo"].set_sensitive(enabled)
def refresh_settings(self): def refresh_settings(self):
"""Refresh plugin settings and reinitialize provider. Called when settings change.""" """Refresh plugin settings and reinitialize provider. Called when settings change."""
@@ -1,8 +1,8 @@
name = IndentationAudio name = IndentationAudio
version = 1.0.0 version = 2.0.0
description = Provides audio feedback for indentation level changes when navigating code or text description = Provides audio feedback for indentation level changes when navigating code or text
authors = Stormux <storm_dragon@stormux.org> authors = Stormux <storm_dragon@stormux.org>
website = https://git.stormux.org/storm/cthulhu website = https://git.stormux.org/storm/cthulhu
copyright = Copyright 2025 copyright = Copyright 2025
builtin = false builtin = false
hidden = false hidden = false
+163 -1
View File
@@ -12,7 +12,7 @@
import logging import logging
import math import math
import re import re
from gi.repository import GLib from gi.repository import GLib, Gtk
from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import debug from cthulhu import debug
@@ -56,6 +56,8 @@ class IndentationAudio(Plugin):
self._saved_speech_indentation = None self._saved_speech_indentation = None
self._activated = False self._activated = False
self._prefs_grid = None
self._prefs_widgets = {}
debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin initialized", True) debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin initialized", True)
@cthulhu_hookimpl @cthulhu_hookimpl
@@ -86,6 +88,7 @@ class IndentationAudio(Plugin):
# Connect to text caret movement events # Connect to text caret movement events
self._connect_to_events() self._connect_to_events()
self._enabled = True
self._activated = True self._activated = True
debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin activated successfully", True) debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin activated successfully", True)
return True return True
@@ -109,6 +112,7 @@ class IndentationAudio(Plugin):
# Clear tracking data # Clear tracking data
self._last_indentation_data.clear() self._last_indentation_data.clear()
self._enabled = False
self._activated = False self._activated = False
debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin deactivated successfully", True) debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin deactivated successfully", True)
return True return True
@@ -116,6 +120,164 @@ class IndentationAudio(Plugin):
except Exception as e: except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: ERROR deactivating plugin: {e}", True) debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: ERROR deactivating plugin: {e}", True)
return False return False
def getPreferencesGUI(self):
if not self._prefs_grid:
self._prefs_grid = self._build_preferences_gui()
self._load_preferences_into_widgets()
return self._prefs_grid, "Indentation Audio"
def getPreferencesFromGUI(self):
if not self._prefs_widgets:
return {}
audio_unit = settings.INDENTATION_UNIT_COLUMNS
if self._prefs_widgets["audio_unit_levels"].get_active():
audio_unit = settings.INDENTATION_UNIT_LEVELS
return {
"indentationAudioUnit": audio_unit,
"indentationAudioBaseFrequency": self._prefs_widgets["base_frequency_spin"].get_value_as_int(),
"indentationAudioStepFrequency": self._prefs_widgets["step_frequency_spin"].get_value_as_int(),
"indentationAudioMaxFrequency": self._prefs_widgets["max_frequency_spin"].get_value_as_int(),
"indentationAudioDuration": float(self._prefs_widgets["duration_spin"].get_value()),
"indentationAudioVolume": float(self._prefs_widgets["volume_spin"].get_value()),
}
def _build_preferences_gui(self):
grid = Gtk.Grid(
row_spacing=6,
column_spacing=12,
margin_left=12,
margin_right=12,
margin_top=12,
margin_bottom=12,
)
row = 0
description = Gtk.Label(label="Configure audio feedback for indentation changes.")
description.set_line_wrap(True)
description.set_halign(Gtk.Align.START)
grid.attach(description, 0, row, 2, 1)
row += 1
audio_unit_label = Gtk.Label(label="Audio _Unit:")
audio_unit_label.set_use_underline(True)
audio_unit_label.set_halign(Gtk.Align.START)
audio_unit_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
audio_unit_levels = Gtk.RadioButton(label="Levels")
audio_unit_columns = Gtk.RadioButton.new_with_label_from_widget(audio_unit_levels, "Columns")
audio_unit_box.pack_start(audio_unit_levels, False, False, 0)
audio_unit_box.pack_start(audio_unit_columns, False, False, 0)
audio_unit_label.set_mnemonic_widget(audio_unit_levels)
grid.attach(audio_unit_label, 0, row, 1, 1)
grid.attach(audio_unit_box, 1, row, 1, 1)
row += 1
base_adjust = Gtk.Adjustment(200, 50, 2000, 10, 50, 0)
step_adjust = Gtk.Adjustment(80, 1, 500, 5, 20, 0)
max_adjust = Gtk.Adjustment(1200, 200, 5000, 50, 100, 0)
duration_adjust = Gtk.Adjustment(0.15, 0.01, 2, 0.01, 0.1, 0)
volume_adjust = Gtk.Adjustment(0.7, 0, 1, 0.05, 0.1, 0)
base_frequency_spin, row = self._add_spin_row(
grid,
row,
"Base _Frequency (Hz):",
base_adjust,
0,
)
step_frequency_spin, row = self._add_spin_row(
grid,
row,
"_Step per Unit (Hz):",
step_adjust,
0,
)
max_frequency_spin, row = self._add_spin_row(
grid,
row,
"_Maximum Frequency (Hz):",
max_adjust,
0,
)
duration_spin, row = self._add_spin_row(
grid,
row,
"Tone _Duration (sec):",
duration_adjust,
2,
)
volume_spin, row = self._add_spin_row(
grid,
row,
"_Volume Multiplier:",
volume_adjust,
2,
)
self._prefs_widgets = {
"audio_unit_levels": audio_unit_levels,
"audio_unit_columns": audio_unit_columns,
"base_frequency_spin": base_frequency_spin,
"step_frequency_spin": step_frequency_spin,
"max_frequency_spin": max_frequency_spin,
"duration_spin": duration_spin,
"volume_spin": volume_spin,
}
return grid
def _add_spin_row(self, grid, row, label_text, adjustment, digits):
label = Gtk.Label(label=label_text)
label.set_use_underline(True)
label.set_halign(Gtk.Align.START)
spin = Gtk.SpinButton(adjustment=adjustment, climb_rate=1, numeric=True)
if digits:
spin.set_digits(digits)
label.set_mnemonic_widget(spin)
grid.attach(label, 0, row, 1, 1)
grid.attach(spin, 1, row, 1, 1)
return spin, row + 1
def _load_preferences_into_widgets(self):
if not self._prefs_widgets:
return
audio_unit = _settingsManager.getSetting("indentationAudioUnit") or settings.indentationAudioUnit
if audio_unit == settings.INDENTATION_UNIT_LEVELS:
self._prefs_widgets["audio_unit_levels"].set_active(True)
else:
self._prefs_widgets["audio_unit_columns"].set_active(True)
base_frequency = _settingsManager.getSetting("indentationAudioBaseFrequency")
if base_frequency is None:
base_frequency = settings.indentationAudioBaseFrequency
self._prefs_widgets["base_frequency_spin"].set_value(base_frequency)
step_frequency = _settingsManager.getSetting("indentationAudioStepFrequency")
if step_frequency is None:
step_frequency = settings.indentationAudioStepFrequency
self._prefs_widgets["step_frequency_spin"].set_value(step_frequency)
max_frequency = _settingsManager.getSetting("indentationAudioMaxFrequency")
if max_frequency is None:
max_frequency = settings.indentationAudioMaxFrequency
self._prefs_widgets["max_frequency_spin"].set_value(max_frequency)
duration_value = _settingsManager.getSetting("indentationAudioDuration")
if duration_value is None:
duration_value = settings.indentationAudioDuration
self._prefs_widgets["duration_spin"].set_value(duration_value)
volume_value = _settingsManager.getSetting("indentationAudioVolume")
if volume_value is None:
volume_value = settings.indentationAudioVolume
self._prefs_widgets["volume_spin"].set_value(volume_value)
def _register_keybinding(self): def _register_keybinding(self):
"""Register the Cthulhu+I keybinding for toggling the plugin.""" """Register the Cthulhu+I keybinding for toggling the plugin."""
+3 -3
View File
@@ -84,7 +84,7 @@ To add support for other languages, install additional Tesseract language packs:
1. **Enable the Plugin**: The OCR plugin is enabled by default in Cthulhu. If disabled, you can enable it through: 1. **Enable the Plugin**: The OCR plugin is enabled by default in Cthulhu. If disabled, you can enable it through:
- Cthulhu Preferences → Plugins → Check "OCR" - Cthulhu Preferences → Plugins → Check "OCR"
- Or ensure `'OCR'` is in the `activePlugins` list in settings.py - Or add `OCR` to your `activePlugins` preference
2. **Basic OCR Workflow**: 2. **Basic OCR Workflow**:
- Navigate to content you want to OCR - Navigate to content you want to OCR
@@ -115,7 +115,7 @@ To add support for other languages, install additional Tesseract language packs:
Access comprehensive OCR settings through Cthulhu Preferences: Access comprehensive OCR settings through Cthulhu Preferences:
1. **Open Cthulhu Preferences**: `~/.local/bin/cthulhu -s` 1. **Open Cthulhu Preferences**: `~/.local/bin/cthulhu -s`
2. **Navigate to OCR Tab**: Use keyboard navigation to find the OCR settings tab 2. **Navigate to OCR Tab**: Use keyboard navigation to find the OCR settings tab (only shown when the plugin is active)
3. **Configure Settings**: Adjust all OCR parameters through the accessible interface 3. **Configure Settings**: Adjust all OCR parameters through the accessible interface
### Available Settings ### Available Settings
@@ -318,4 +318,4 @@ For issues, questions, or contributions:
--- ---
*Part of the Cthulhu Screen Reader project - Making the desktop accessible for everyone.* *Part of the Cthulhu Screen Reader project - Making the desktop accessible for everyone.*
@@ -494,6 +494,15 @@ class PluginManager(Plugin):
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Updated active plugins: {active_plugins}", True) debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Updated active plugins: {active_plugins}", True)
try:
from cthulhu import plugin_system_manager
manager = plugin_system_manager.getManager()
if manager:
manager.setActivePlugins(active_plugins)
debug.printMessage(debug.LEVEL_INFO, "PluginManager: Applied active plugin changes", True)
except Exception as apply_error:
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Failed to apply plugin changes: {apply_error}", True)
except Exception as error: except Exception as error:
logger.error(f"PluginManager: Error toggling plugin {plugin_name}: {error}") logger.error(f"PluginManager: Error toggling plugin {plugin_name}: {error}")
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error toggling {plugin_name}: {error}", True) debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error toggling {plugin_name}: {error}", True)
+10 -1
View File
@@ -30,4 +30,13 @@ pip install pluggy
## Usage ## Usage
The plugin will be automatically loaded when Cthulhu starts if it's listed in the activePlugins setting. Enable the plugin in Cthulhu Preferences → Plugins to load it at startup. You can also add it to your
`activePlugins` preference.
## Preferences Pages (Optional)
Plugins can add their own Preferences tab by implementing:
- `getPreferencesGUI()` → returns a Gtk widget (or `(widget, label)`).
- `getPreferencesFromGUI()` → returns a dict of settings to save.
Tabs are only shown when the plugin is active.
+3 -3
View File
@@ -494,12 +494,12 @@ presentChatRoomLast = False
presentLiveRegionFromInactiveTab = False presentLiveRegionFromInactiveTab = False
# Plugins # Plugins
activePlugins = ['AIAssistant', 'DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'IndentationAudio', 'WindowTitleReader'] activePlugins = ['DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'WindowTitleReader']
pluginSources = [] pluginSources = []
# AI Assistant settings (disabled by default for opt-in behavior) # AI Assistant settings (disabled by default for opt-in behavior)
aiAssistantEnabled = True aiAssistantEnabled = False
aiProvider = AI_PROVIDER_CLAUDE_CODE aiProvider = AI_PROVIDER_OLLAMA
aiApiKeyFile = "" aiApiKeyFile = ""
aiOllamaModel = "llama3.2-vision" aiOllamaModel = "llama3.2-vision"
aiOllamaEndpoint = "http://localhost:11434" aiOllamaEndpoint = "http://localhost:11434"