Compare commits

..

7 Commits

Author SHA1 Message Date
Storm Dragon
d36b664319 Merge branch 'testing'
Plugins are in a much better state now, mostly working. The exception is, plugins that create a keyboard shortcut don't actually bind the shortcut. That one is turning out to be a lot harder to fix than I originally thought.
2025-04-05 16:32:17 -04:00
Storm Dragon
3f7d60763d Merge branch 'testing'
Plugins are currently broken as Cthulhu moves over to pluggy. Libpeas and pygobject no longer play nicely together after latest updates. I really did not want to make a new release yet, because it is not ready, but a screen reader that at least reads instead of crashing at launch is better than nothing.
2025-04-03 20:17:14 -04:00
Storm Dragon
6bbf3d0e67 Merge branch 'testing' latest changes merged. 2024-12-22 19:04:57 -05:00
Storm Dragon
cbe3424e29 Fix the version.py file in preparation for merging. 2024-12-22 19:04:39 -05:00
Storm Dragon
327ad99e49 Preparing for stable tag release. 2024-12-18 19:49:25 -05:00
Storm Dragon
c46cf1c939 Merge branch 'testing' fixed preferences GUI. 2024-12-18 19:45:59 -05:00
Storm Dragon
a97bb30ed3 New version system merged. 2024-12-18 11:42:52 -05:00
11 changed files with 88 additions and 220 deletions

1
QUICKSTART Normal file
View File

@ -0,0 +1 @@
See http://wiki.gnome.org/Projects/Cthulhu

View File

@ -1,10 +1,5 @@
# Cthulhu
## Note
If you somehow stumbled across this while looking for a desktop screen reader for Linux, you most likely want [Orca](https://orca.gnome.org/) instead. Cthulhu is currently a supplemental screen reader that fills a nitch for some advanced users. E.g. some older QT based programs may work with Cthulhu, and if you use certain window managers like i3, Mozilla applications like Firefox and Thunderbird may work better.
## Introduction
Cthulhu is a free, open source, flexible, and extensible screen reader
@ -25,7 +20,7 @@ Cthulhu has the following dependencies:
* Python 3 - Python platform
* pygobject-3.0 - Python bindings for the GObject library
* pluggy - Plugin and hook calling mechanisms for python
* libpeas - GObject based Plugin engine
* gtk+-3.0 - GTK+ toolkit
* json-py - a JSON (<https://json.org/>) reader and writer in Python
* python-speechd - Python bindings for Speech Dispatcher (optional)
@ -35,6 +30,7 @@ Cthulhu has the following dependencies:
* py-setproctitle - Python library to set the process title (optional)
* gstreamer-1.0 - GStreamer - Streaming media framework (optional)
* socat - Used for self-voicing functionality.
* libpeas - For the plugin system.
You are strongly encouraged to also have the latest stable versions
of AT-SPI2 and ATK.

View File

@ -47,8 +47,22 @@ class APIHelper:
self.app = app
self._gestureBindings = {}
def registerGestureByString(self, function, name, gestureString, inputEventType='default', normalizer='cthulhu', learnModeEnabled=True, contextName=None):
"""Register a gesture by string."""
def registerGestureByString(self, function, name, gestureString,
inputEventType='default', normalizer='cthulhu',
learnModeEnabled=True, contextName=None):
"""Register a gesture by string.
Arguments:
- function: the function to call when the gesture is performed
- name: a human-readable name for this gesture
- gestureString: string representation of the gesture (e.g., 'kb:cthulhu+z')
- inputEventType: the type of input event
- normalizer: the normalizer to use
- learnModeEnabled: whether this should be available in learn mode
- contextName: the context for this gesture (e.g., plugin name)
Returns the binding ID or None if registration failed
"""
if not gestureString.startswith("kb:"):
return None
@ -60,16 +74,19 @@ class APIHelper:
from . import keybindings
key_parts = key.lower().split("+")
# Start with the base Cthulhu modifier
# Determine appropriate modifier mask
modifiers = keybindings.CTHULHU_MODIFIER_MASK
# Extract the final key (without modifiers)
final_key = key_parts[-1]
# Check for additional modifiers and combine them properly
# Check for additional modifiers
if "shift" in key_parts:
# Use the pre-defined combined mask rather than trying to OR them
modifiers = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
elif "ctrl" in key_parts or "control" in key_parts:
modifiers = keybindings.CTHULHU_CTRL_MODIFIER_MASK
elif "alt" in key_parts:
modifiers = keybindings.CTHULHU_ALT_MODIFIER_MASK
# Create a keybinding handler
class GestureHandler:
@ -78,43 +95,26 @@ class APIHelper:
self.description = description
def __call__(self, script, inputEvent):
try:
return function(script, inputEvent)
except Exception as e:
import logging
logging.getLogger(__name__).error(f"Error in keybinding handler: {e}")
return True
return self.function(script, inputEvent)
handler = GestureHandler(function, name)
# Create the binding object regardless of whether there's an active script
# This allows plugins to define bindings that will work when a script becomes active
# Register the binding with the active script
from . import cthulhu_state
from . import keybindings
if cthulhu_state.activeScript:
bindings = cthulhu_state.activeScript.getKeyBindings()
binding = keybindings.KeyBinding(
final_key,
keybindings.defaultModifierMask,
modifiers,
handler)
bindings.add(binding)
# Store binding for later reference
if contextName not in self._gestureBindings:
self._gestureBindings[contextName] = []
self._gestureBindings[contextName].append(binding)
# Only add to active script if one exists
if cthulhu_state.activeScript:
bindings = cthulhu_state.activeScript.getKeyBindings()
bindings.add(binding)
# Register key grab at the system level
grab_ids = self.app.addKeyGrab(binding)
# For later removal
if grab_ids:
binding._grab_ids = grab_ids
debug.printMessage(debug.LEVEL_INFO, f"Created binding: {binding.keysymstring} with modifiers {binding.modifiers}", True)
return binding
return None
@ -132,17 +132,10 @@ class APIHelper:
bindings = cthulhu_state.activeScript.getKeyBindings()
bindings.remove(binding)
# Remove key grab at system level
if hasattr(binding, '_grab_ids'):
for grab_id in binding._grab_ids:
self.app.removeKeyGrab(grab_id)
# Remove from tracking
# Remove from our tracking
if contextName in self._gestureBindings:
if binding in self._gestureBindings[contextName]:
self._gestureBindings[contextName].remove(binding)
import gi
import importlib
import os
@ -637,9 +630,6 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False):
_scriptManager.activate()
_eventManager.activate()
cthulhuApp.getSignalManager().emitSignal('load-setting-begin')
cthulhuApp.getPluginSystemManager().register_plugin_keybindings_with_active_script()
cthulhuApp.getSignalManager().emitSignal('load-setting-completed')
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: User Settings Loaded', True)

View File

@ -23,5 +23,5 @@
# Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca
version = "2025.04.20"
codeName = "plugins"
version = "2025.04.04"
codeName = "testing"

View File

@ -298,7 +298,7 @@ def println(level, text="", timestamp=False, stack=False):
text = text.replace("\ufffc", "[OBJ]")
if timestamp:
text = text.replace("\n", f"\n{' ' * 18}")
text = f"{text} - {datetime.now().strftime('%H:%M:%S.%f')}"
text = f"{datetime.now().strftime('%H:%M:%S.%f')} - {text}"
if stack:
text += f" {_stackAsString()}"

View File

@ -470,10 +470,6 @@ class KeyBindings:
given keycode and modifiers, or None if no match exists.
"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"Looking for handler for key: {keyboardEvent.hw_code} with modifiers {keyboardEvent.modifiers}")
matches = []
candidates = []
clickCount = keyboardEvent.getClickCount()

View File

@ -47,8 +47,6 @@ class Plugin:
self.name = ''
self.version = ''
self.description = ''
self._bindings = None
self._gestureBindings = {}
def set_app(self, app):
"""Set the application reference."""
@ -77,16 +75,12 @@ class Plugin:
return
logger.info(f"Deactivating plugin: {self.name}")
def get_bindings(self):
"""Get keybindings for this plugin. Override in subclasses."""
return self._bindings
def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True):
"""Register a gesture by string."""
if self.app:
api_helper = self.app.getAPIHelper()
if api_helper:
binding = api_helper.registerGestureByString(
return api_helper.registerGestureByString(
function,
name,
gestureString,
@ -95,13 +89,4 @@ class Plugin:
learnModeEnabled,
contextName=self.module_name
)
# Also store the binding locally so get_bindings() can use it
if binding:
if not self._bindings:
from . import keybindings
self._bindings = keybindings.KeyBindings()
self._bindings.add(binding)
return binding
return None

View File

@ -117,96 +117,6 @@ class PluginSystemManager:
logger.info(f"System plugins directory: {PluginType.SYSTEM.get_root_dir()}")
logger.info(f"User plugins directory: {PluginType.USER.get_root_dir()}")
def register_plugin_global_keybindings(self, plugin):
"""Register a plugin's keybindings with all scripts."""
if not hasattr(plugin, 'get_bindings'):
return
try:
bindings = plugin.get_bindings()
if not bindings or not bindings.keyBindings:
return
logger.info(f"Registering global keybindings for plugin: {plugin.name}")
# First register with the active script
from . import cthulhu_state
if cthulhu_state.activeScript:
active_script = cthulhu_state.activeScript
for binding in bindings.keyBindings:
active_script.getKeyBindings().add(binding)
grab_ids = self.app.addKeyGrab(binding)
if grab_ids:
binding._grab_ids = grab_ids
# Store these bindings for future script changes
plugin_name = plugin.name or plugin.module_name
if not hasattr(self, '_plugin_global_bindings'):
self._plugin_global_bindings = {}
self._plugin_global_bindings[plugin_name] = bindings
# Connect to script changes to ensure bindings work with all scripts
if not hasattr(self, '_connected_to_script_changes'):
signal_manager = self.app.getSignalManager()
if signal_manager:
signal_manager.connectSignal('load-setting-completed', self._on_settings_changed, None)
self._connected_to_script_changes = True
except Exception as e:
logger.error(f"Error registering global keybindings for plugin {plugin.name}: {e}")
import traceback
logger.error(traceback.format_exc())
def register_plugin_keybindings_with_active_script(self):
"""Register all plugin keybindings with the active script."""
if not PLUGGY_AVAILABLE:
return
from . import cthulhu_state
if not cthulhu_state.activeScript:
logger.warning("No active script available to register plugin keybindings")
return
active_script = cthulhu_state.activeScript
logger.info(f"Registering plugin keybindings with active script: {active_script}")
for pluginInfo in self._plugins.values():
if not pluginInfo.loaded or not pluginInfo.instance:
continue
plugin = pluginInfo.instance
if not hasattr(plugin, 'get_bindings') or not plugin.get_bindings():
continue
logger.info(f"Registering keybindings for plugin: {plugin.name}")
bindings = plugin.get_bindings()
for binding in bindings.keyBindings:
logger.info(f"Adding binding: {binding.keysymstring} with modifiers {binding.modifiers}")
active_script.getKeyBindings().add(binding)
def _on_settings_changed(self, app=None):
"""Re-register all plugin keybindings when settings change."""
if not hasattr(self, '_plugin_global_bindings'):
return
from . import cthulhu_state
if not cthulhu_state.activeScript:
return
active_script = cthulhu_state.activeScript
for plugin_name, bindings in self._plugin_global_bindings.items():
logger.info(f"Re-registering keybindings for plugin: {plugin_name}")
for binding in bindings.keyBindings:
# Check if binding already exists
if active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"):
continue
active_script.getKeyBindings().add(binding)
from . import cthulhu
grab_ids = cthulhu.addKeyGrab(binding)
if grab_ids:
binding._grab_ids = grab_ids
def _setup_plugin_dirs(self):
"""Ensure plugin directories exist."""
os.makedirs(PluginType.SYSTEM.get_root_dir(), exist_ok=True)
@ -503,10 +413,6 @@ class PluginSystemManager:
pluginInfo.loaded = True
logger.info(f"Successfully loaded plugin: {module_name}")
# Register any global keybindings from the plugin
self.register_plugin_global_keybindings(pluginInfo.instance)
return True
except Exception as e:

View File

@ -40,11 +40,8 @@ class DisplayVersion(Plugin):
f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}',
'kb:cthulhu+shift+v'
)
logger.info(f"Registered keybinding: {self._kb_binding}")
except Exception as e:
logger.error(f"Error activating DisplayVersion plugin: {e}")
import traceback
logger.error(traceback.format_exc())
@cthulhu_hookimpl
def deactivate(self, plugin=None):
@ -58,7 +55,6 @@ class DisplayVersion(Plugin):
def speakText(self, script=None, inputEvent=None):
"""Speak the Cthulhu version when shortcut is pressed."""
try:
logger.info("DisplayVersion plugin: speakText called")
if self.app:
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
if state.activeScript:

View File

@ -1,6 +1,9 @@
#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
# Copyright (c) 2010-2012 The Orca Team
# Copyright (c) 2012 Igalia, S.L.
# Copyright (c) 2005-2010 Sun Microsystems Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@ -17,6 +20,8 @@
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA.
#
# Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca
"""Self Voice plugin for Cthulhu screen reader."""

View File

@ -318,13 +318,6 @@ class ScriptManager:
return
newScript.activate()
# Register plugin keybindings with the new active script
from . import cthulhu
plugin_manager = cthulhu.cthulhuApp.getPluginSystemManager()
if plugin_manager:
plugin_manager.register_plugin_keybindings_with_active_script()
tokens = ["SCRIPT MANAGER: Setting active script to", newScript, "reason:", reason]
debug.printTokens(debug.LEVEL_INFO, tokens, True)