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
- **Hot-reloadable plugins**: Add functionality without restarting
- **Community plugins**: User and system plugin directories
- **Plugin preferences**: Plugins can provide preferences pages that appear only when the plugin is active
### Remote Control
- **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)
- `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
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')
# 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)
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_status = None
self._plugin_update_in_progress = False
self._plugin_tabs = {}
self._plugin_tabs_cached = False
self._dynamic_plugin_tabs = {}
self.screenHeight = None
self.screenWidth = None
self.speechFamiliesChoice = None
@@ -396,6 +399,9 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
label = Gtk.Label(label=AXObject.get_name(self.script.app))
self.get_widget("notebook").append_page(appPage, label)
self._cache_plugin_tabs()
self._update_plugin_tabs()
self._refresh_dynamic_plugin_tabs()
self._initGUIState()
self._initSoundThemeState()
@@ -799,6 +805,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
path = self._plugin_model.get_path(new_iter)
self._plugin_treeview.expand_to_path(path)
selection.select_path(path)
self._update_plugin_tabs()
def _get_active_plugins_from_ui(self):
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._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):
"""Return the ACSS value for the given voice type.
@@ -864,11 +1012,31 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
pronunciationDict = self.getModelDict(self.pronunciationModel)
keyBindingsDict = self.getKeyBindingsModelDict(self.keyBindingsModel)
self.prefsDict.update(self.script.getPreferencesFromGUI())
self.prefsDict.update(self._get_plugin_preferences_from_gui())
_settingsManager.saveSettings(self.script,
self.prefsDict,
pronunciationDict,
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):
"""Look for the value of the given key in the voice dictionary
for the given voice type.
@@ -2283,9 +2451,13 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
if self.script.app:
self.get_widget('profilesFrame').set_sensitive(False)
active_plugins = self._get_active_plugins_for_tabs()
self._update_plugin_tabs(active_plugins)
# AI Assistant settings
#
self._initAIState()
if self._plugin_active(active_plugins, ["AIAssistant"]):
self._initAIState()
# Indentation settings
#
@@ -2293,7 +2465,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
# OCR Plugin settings
#
self._initOCRState()
if self._plugin_active(active_plugins, ["OCR"]):
self._initOCRState()
def __initProfileCombo(self):
"""Adding available profiles and setting active as the active one"""
@@ -4149,6 +4322,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._apply_plugin_changes()
self.writeUserPreferences()
cthulhu.loadUserSettings(self.script)
self._refresh_dynamic_plugin_tabs()
braille.checkBrailleSetting()
self._initSpeechState()
self._populateKeyBindings()
+8
View File
@@ -87,6 +87,14 @@ class Plugin:
"""Get keybindings for this plugin. Override in subclasses."""
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):
"""Register a gesture by string."""
if self.app:
+2 -2
View File
@@ -3,5 +3,5 @@ Name = AI Assistant
Module = AIAssistant
Description = AI-powered accessibility assistant for analyzing screens and taking actions
Authors = Stormux <storm_dragon@stormux.org>
Version = 1.0.0
Category = Accessibility
Version = 2.0.0
Category = Accessibility
+211
View File
@@ -69,6 +69,8 @@ class AIAssistant(Plugin):
# Pre-captured screen data (to avoid capturing dialog itself)
self._current_screen_data = None
self._prefs_grid = None
self._prefs_widgets = {}
@cthulhu_hookimpl
def activate(self, plugin=None):
@@ -128,6 +130,215 @@ class AIAssistant(Plugin):
self._unregister_keybindings()
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):
"""Refresh plugin settings and reinitialize provider. Called when settings change."""
@@ -1,8 +1,8 @@
name = IndentationAudio
version = 1.0.0
version = 2.0.0
description = Provides audio feedback for indentation level changes when navigating code or text
authors = Stormux <storm_dragon@stormux.org>
website = https://git.stormux.org/storm/cthulhu
copyright = Copyright 2025
builtin = false
hidden = false
hidden = false
+163 -1
View File
@@ -12,7 +12,7 @@
import logging
import math
import re
from gi.repository import GLib
from gi.repository import GLib, Gtk
from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import debug
@@ -56,6 +56,8 @@ class IndentationAudio(Plugin):
self._saved_speech_indentation = None
self._activated = False
self._prefs_grid = None
self._prefs_widgets = {}
debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin initialized", True)
@cthulhu_hookimpl
@@ -86,6 +88,7 @@ class IndentationAudio(Plugin):
# Connect to text caret movement events
self._connect_to_events()
self._enabled = True
self._activated = True
debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin activated successfully", True)
return True
@@ -109,6 +112,7 @@ class IndentationAudio(Plugin):
# Clear tracking data
self._last_indentation_data.clear()
self._enabled = False
self._activated = False
debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin deactivated successfully", True)
return True
@@ -116,6 +120,164 @@ class IndentationAudio(Plugin):
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: ERROR deactivating plugin: {e}", True)
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):
"""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:
- 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**:
- 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:
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
### 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)
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:
logger.error(f"PluginManager: Error toggling plugin {plugin_name}: {error}")
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
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
# Plugins
activePlugins = ['AIAssistant', 'DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'IndentationAudio', 'WindowTitleReader']
activePlugins = ['DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'WindowTitleReader']
pluginSources = []
# AI Assistant settings (disabled by default for opt-in behavior)
aiAssistantEnabled = True
aiProvider = AI_PROVIDER_CLAUDE_CODE
aiAssistantEnabled = False
aiProvider = AI_PROVIDER_OLLAMA
aiApiKeyFile = ""
aiOllamaModel = "llama3.2-vision"
aiOllamaEndpoint = "http://localhost:11434"