39 Commits

Author SHA1 Message Date
Storm Dragon 0c26025a81 Updates to build files. 2025-07-01 15:55:00 -04:00
Storm Dragon 0405200980 New plugin, updates to a few other things. Yes, I know that's not descriptive, but apparently I forgot to add a few things last time. lol 2025-07-01 13:56:18 -04:00
Storm Dragon 408fb85730 Updated clipboard plugin to work with the now fixed plugin system. 2025-06-05 14:05:23 -04:00
Storm Dragon 0f25245d3d OMG it actually works! Just some finishing touches. 2025-06-05 13:55:30 -04:00
Storm Dragon 13f110ab34 Updated bindings code. 2025-06-05 13:50:36 -04:00
Storm Dragon 62f46c0eb7 Maybe getting closer. 2025-06-05 13:45:12 -04:00
Storm Dragon e2364a154a Another try to get keybindings working. 2025-06-05 13:40:44 -04:00
Storm Dragon 2090767794 Fixed error in event manager. 2025-06-05 13:34:23 -04:00
Storm Dragon ea50d8b024 Fix error in script manager. 2025-06-05 13:31:11 -04:00
Storm Dragon a1d90a7245 more work on keybindings. 2025-06-05 13:27:25 -04:00
Storm Dragon 2eb6d3c7dd updated keybindings.py 2025-06-05 13:20:27 -04:00
Storm Dragon 0edbefac47 more debugging. 2025-06-05 13:11:47 -04:00
Storm Dragon 90aecf8055 Changes to the plugin manager. 2025-06-05 04:13:24 -04:00
Storm Dragon 48d99e8813 Improved debugging to help track down this bug. 2025-06-05 04:02:28 -04:00
Storm Dragon 314aa18a1b Another another attempt to fix the plugin keybindings. 2025-06-05 03:42:02 -04:00
Storm Dragon a21f1aa13b another attempt to fix the keybinding problem for plugins. 2025-06-05 03:32:01 -04:00
Storm Dragon 5181944de0 Move timestamps to the end of the log message instead of the beginning. Makes debugging much less of a PITA. 2025-04-20 15:27:59 -04:00
Storm Dragon 0a8bb684ec Fixed an error in a call to a cthulhu module. 2025-04-20 03:20:12 -04:00
Storm Dragon d94ba1accb Add optional parameter to _on_settings_changed() 2025-04-20 03:13:43 -04:00
Storm Dragon 399f449484 Fixed method call. 2025-04-20 03:07:37 -04:00
Storm Dragon ecd122786f Fixed an error with logging, for real this time. 2025-04-20 02:58:12 -04:00
Storm Dragon 01273618a7 Fixed an error with logging. 2025-04-20 02:50:32 -04:00
Storm Dragon d8df2ed757 Another try at getting keybindings working. 2025-04-20 02:43:59 -04:00
Storm Dragon c376b2489a Getting closer to working bindings. 2025-04-20 02:30:04 -04:00
Storm Dragon 39dca0574a Improve key detection in registerGestureByString 2025-04-19 15:39:43 -04:00
Storm Dragon 8b1f501fe7 fixed an error. 2025-04-19 14:54:32 -04:00
Storm Dragon 96335baf5d Fixed indentation issues. 2025-04-19 14:48:38 -04:00
Storm Dragon 51984a6540 Hopefully got this keybinding thing once and for all... Fingers crossed. 2025-04-19 14:41:17 -04:00
Storm Dragon 3296e5d571 Fix broken method. 2025-04-19 14:15:30 -04:00
Storm Dragon 1e6f4b8913 fixed import error. 2025-04-19 14:06:48 -04:00
Storm Dragon 331b1c3ad5 More debugging efforts. 2025-04-19 14:01:20 -04:00
Storm Dragon 04b8592ed3 Indentation error was worse than I thought. 2025-04-18 14:53:52 -04:00
Storm Dragon c64591a162 Fixed indentation error. 2025-04-18 14:45:33 -04:00
Storm Dragon 80212d616f Added some logging to try and figure out what's going on. 2025-04-18 14:42:02 -04:00
Storm Dragon 9790a8d494 More attempts to fix keyboard. 2025-04-18 14:28:16 -04:00
Storm Dragon ec90906052 Maybe finally solved the plugin keybinding issue... 2025-04-18 13:00:31 -04:00
Storm Dragon f01374d15e One more try before sleep. 2025-04-14 05:04:59 -04:00
Storm Dragon 0347b7feea Another attempt at fixing plugin keyboard shortcuts. 2025-04-14 04:54:48 -04:00
Storm Dragon 0580dda131 A few documentation updates. 2025-04-11 13:17:26 -04:00
25 changed files with 891 additions and 137 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
Welcome to Cthulhu
We are excited to have you here and welcome your contributions to the Cthulhu screen reader project! This project is a fork of Cthulhu, with a focus on creating an open and collaborative community where contributions are encouraged.
We are excited to have you here and welcome your contributions to the Cthulhu screen reader project! This project is a fork of Orca, with a focus on creating an open and collaborative community where contributions are encouraged.
How to Contribute
+4
View File
@@ -1,6 +1,10 @@
ACLOCAL_AMFLAGS = -I m4 ${ACLOCAL_FLAGS}
if BUILD_HELP
SUBDIRS = docs icons po src help
else
SUBDIRS = docs icons po src
endif
DISTCHECK_CONFIGURE_FLAGS = \
--disable-scrollkeeper
-1
View File
@@ -1 +0,0 @@
See http://wiki.gnome.org/Projects/Cthulhu
+6 -2
View File
@@ -1,5 +1,10 @@
# 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
@@ -20,7 +25,7 @@ Cthulhu has the following dependencies:
* Python 3 - Python platform
* pygobject-3.0 - Python bindings for the GObject library
* libpeas - GObject based Plugin engine
* pluggy - Plugin and hook calling mechanisms for python
* 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)
@@ -30,7 +35,6 @@ 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.
+8 -4
View File
@@ -32,10 +32,14 @@ autoreconf --verbose --force --install -Wno-portability || {
exit 1
}
which yelp-build > /dev/null || {
echo "Try installing the 'yelp-tools' package."
exit 1
}
# Only check for yelp-build if help documentation will be built
# Skip check if SKIP_YELP environment variable is set
if [ "${SKIP_YELP}" != "1" ]; then
which yelp-build > /dev/null || {
echo "Try installing the 'yelp-tools' package, or set SKIP_YELP=1 to skip documentation."
exit 1
}
fi
cd "$olddir"
+11 -2
View File
@@ -24,8 +24,16 @@ GETTEXT_PACKAGE=AC_PACKAGE_TARNAME
AC_SUBST(GETTEXT_PACKAGE)
AC_DEFINE_UNQUOTED(GETTEXT_PACKAGE,"$GETTEXT_PACKAGE", [gettext package])
# User Documentation
YELP_HELP_INIT
# User Documentation (optional)
AC_ARG_ENABLE([help],
AS_HELP_STRING([--disable-help], [Disable building help documentation]))
AS_IF([test "x$enable_help" != "xno"], [
YELP_HELP_INIT
BUILD_HELP=yes
], [
BUILD_HELP=no
])
AM_CONDITIONAL(BUILD_HELP, test "x$BUILD_HELP" = "xyes")
PKG_CHECK_MODULES([PYGOBJECT], [pygobject-3.0 >= pygobject_required_version])
PKG_CHECK_MODULES([ATSPI2], [atspi-2 >= atspi_required_version])
@@ -129,6 +137,7 @@ src/cthulhu/plugins/ByeCthulhu/Makefile
src/cthulhu/plugins/HelloCthulhu/Makefile
src/cthulhu/plugins/Clipboard/Makefile
src/cthulhu/plugins/DisplayVersion/Makefile
src/cthulhu/plugins/IndentationAudio/Makefile
src/cthulhu/plugins/hello_world/Makefile
src/cthulhu/plugins/self_voice/Makefile
src/cthulhu/plugins/SimplePluginSystem/Makefile
+5 -5
View File
@@ -1,7 +1,7 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=cthulhu
pkgver=0.4
pkgver=2025.07.01
pkgrel=1
pkgdesc="Screen reader for individuals who are blind or visually impaired forked from Orca"
url="https://git.stormux.org/storm/cthulhu"
@@ -25,6 +25,7 @@ depends=(
python-atspi
python-cairo
python-gobject
python-pluggy
python-setproctitle
socat
speech-dispatcher
@@ -33,15 +34,14 @@ depends=(
)
makedepends=(
git
itstool
yelp-tools
)
source=("git+https://git.stormux.org/storm/cthulhu.git")
b2sums=('SKIP')
prepare() {
cd cthulhu
NOCONFIGURE=1 ./autogen.sh
git checkout testing
NOCONFIGURE=1 SKIP_YELP=1 ./autogen.sh
}
pkgver() {
@@ -51,7 +51,7 @@ pkgver() {
build() {
cd cthulhu
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --disable-help
make
}
+89 -49
View File
@@ -37,91 +37,118 @@ import faulthandler
class APIHelper:
"""Helper class for plugin API interactions, including keybindings."""
def __init__(self, app):
"""Initialize the APIHelper.
Arguments:
- app: the Cthulhu application
"""
self.app = app
self._gestureBindings = {}
def registerGestureByString(self, function, name, gestureString, inputEventType='default', normalizer='cthulhu', learnModeEnabled=True, contextName=None):
"""Register a gesture by string."""
import logging
logger = logging.getLogger(__name__)
def registerGestureByString(self, function, name, gestureString,
inputEventType='default', normalizer='cthulhu',
learnModeEnabled=True, contextName=None):
"""Register a gesture by string.
logger.info(f"=== APIHelper.registerGestureByString called ===")
logger.info(f"gestureString: {gestureString}")
logger.info(f"name: {name}")
logger.info(f"contextName: {contextName}")
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:"):
logger.warning(f"Gesture string doesn't start with 'kb:': {gestureString}")
return None
# Extract the key portion from the gesture string
key = gestureString.split(":", 1)[1]
logger.info(f"Extracted key: {key}")
# Handle Cthulhu modifier specially
if "cthulhu+" in key.lower():
from . import keybindings
key_parts = key.lower().split("+")
# Determine appropriate modifier mask
logger.info(f"Key parts: {key_parts}")
# Start with the base Cthulhu modifier
modifiers = keybindings.CTHULHU_MODIFIER_MASK
# Extract the final key (without modifiers)
final_key = key_parts[-1]
# Check for additional modifiers
logger.info(f"Final key: {final_key}")
# Check for additional modifiers and combine them properly
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
logger.info(f"Using CTHULHU_SHIFT_MODIFIER_MASK: {modifiers}")
else:
logger.info(f"Using CTHULHU_MODIFIER_MASK: {modifiers}")
# Create a keybinding handler
class GestureHandler:
def __init__(self, function, description):
self.function = function
self.description = description
def __call__(self, script, inputEvent):
return self.function(script, inputEvent)
try:
logger.info(f"=== DisplayVersion keybinding handler called! ===")
return function(script, inputEvent)
except Exception as e:
import logging
logging.getLogger(__name__).error(f"Error in keybinding handler: {e}")
return True
handler = GestureHandler(function, name)
# Register the binding with the active script
logger.info(f"Created handler: {handler}")
# 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
from . import cthulhu_state
from . import keybindings
binding = keybindings.KeyBinding(
final_key,
keybindings.defaultModifierMask,
modifiers,
handler)
logger.info(f"Created binding: keysym={binding.keysymstring}, modifiers={binding.modifiers}, mask={binding.modifier_mask}")
# Store binding for later reference
if contextName not in self._gestureBindings:
self._gestureBindings[contextName] = []
self._gestureBindings[contextName].append(binding)
logger.info(f"Stored binding in context '{contextName}'")
# Only add to active script if one exists
if cthulhu_state.activeScript:
logger.info(f"Adding binding to active script: {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)
# Register key grab at the system level
grab_ids = self.app.addKeyGrab(binding)
logger.info(f"Key grab IDs: {grab_ids}")
return binding
# For later removal
if grab_ids:
binding._grab_ids = grab_ids
else:
logger.warning("No active script available - binding stored for later registration")
debug.printMessage(debug.LEVEL_INFO, f"Created binding: {binding.keysymstring} with modifiers {binding.modifiers}", True)
logger.info("=== APIHelper.registerGestureByString completed ===")
return binding
else:
logger.warning(f"Key doesn't contain 'cthulhu+': {key}")
return None
def unregisterShortcut(self, binding, contextName=None):
"""Unregister a previously registered shortcut.
Arguments:
- binding: the binding to unregister
- contextName: the context for this gesture
@@ -131,11 +158,18 @@ class APIHelper:
if cthulhu_state.activeScript:
bindings = cthulhu_state.activeScript.getKeyBindings()
bindings.remove(binding)
# Remove from our tracking
# 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
if contextName in self._gestureBindings:
if binding in self._gestureBindings[contextName]:
self._gestureBindings[contextName].remove(binding)
import gi
import importlib
import os
@@ -629,6 +663,12 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False):
_scriptManager.activate()
_eventManager.activate()
# Refresh keybindings to include plugin bindings (after script manager is active)
cthulhuApp.getPluginSystemManager().refresh_active_script_keybindings()
cthulhuApp.getSignalManager().emitSignal('load-setting-begin')
# cthulhuApp.getPluginSystemManager().register_plugin_keybindings_with_active_script()
cthulhuApp.getSignalManager().emitSignal('load-setting-completed')
+1 -1
View File
@@ -23,5 +23,5 @@
# Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca
version = "2025.04.04"
version = "2025.07.01"
codeName = "testing"
+1 -1
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"{datetime.now().strftime('%H:%M:%S.%f')} - {text}"
text = f"{text} - {datetime.now().strftime('%H:%M:%S.%f')}"
if stack:
text += f" {_stackAsString()}"
+7
View File
@@ -112,6 +112,13 @@ class EventManager:
cthulhu_state.device.key_watcher = cthulhu_state.device.add_key_watcher(
self._processNewKeyboardEvent)
self.newKeyHandlingActive = True
# Notify plugin system that device is now available for keybinding registration
from . import cthulhu
if hasattr(cthulhu, 'cthulhuApp') and cthulhu.cthulhuApp:
plugin_manager = cthulhu.cthulhuApp.getPluginSystemManager()
if plugin_manager:
pass # plugin_manager.register_plugin_keybindings_with_active_script()
def activateLegacyKeyHandling(self):
if not self.legacyKeyHandlingActive:
+60 -2
View File
@@ -251,6 +251,23 @@ class KeyBinding:
if not self.keycode:
self.keycode = getKeycode(self.keysymstring)
# Debug logging for DisplayVersion plugin specifically
if self.keysymstring == 'v' and self.modifiers == 257:
with open('/tmp/displayversion_matches.log', 'a') as f:
f.write(f"=== DisplayVersion matches() debug ===\n")
f.write(f"Self keycode: {self.keycode}\n")
f.write(f"Self keysymstring: {self.keysymstring}\n")
f.write(f"Self modifiers: {self.modifiers}\n")
f.write(f"Self modifier_mask: {self.modifier_mask}\n")
f.write(f"Input keycode: {keycode}\n")
f.write(f"Input modifiers: {modifiers}\n")
f.write(f"Keycode match: {self.keycode == keycode}\n")
if self.keycode == keycode:
result = modifiers & self.modifier_mask
f.write(f"Modifier calculation: {modifiers} & {self.modifier_mask} = {result}\n")
f.write(f"Modifier match: {result == self.modifiers}\n")
f.write(f"Overall match: {self.keycode == keycode and (modifiers & self.modifier_mask) == self.modifiers}\n")
if self.keycode == keycode:
result = modifiers & self.modifier_mask
return result == self.modifiers
@@ -470,13 +487,41 @@ class KeyBindings:
given keycode and modifiers, or None if no match exists.
"""
import logging
logger = logging.getLogger(__name__)
# Check if this might be the DisplayVersion key combination
event_str = keyboardEvent.event_string if hasattr(keyboardEvent, 'event_string') else 'unknown'
if event_str.lower() == 'v':
logger.info(f"=== KeyBindings.getInputHandler: Looking for handler ===")
logger.info(f"Event string: {event_str}")
logger.info(f"Hardware code: {keyboardEvent.hw_code}")
logger.info(f"Modifiers: {keyboardEvent.modifiers}")
logger.info(f"Total keybindings to check: {len(self.keyBindings)}")
with open('/tmp/keybinding_lookup.log', 'a') as f:
f.write(f"=== Looking for 'v' key handler ===\n")
f.write(f"Event string: {event_str}\n")
f.write(f"Hardware code: {keyboardEvent.hw_code}\n")
f.write(f"Modifiers: {keyboardEvent.modifiers}\n")
f.write(f"Total keybindings: {len(self.keyBindings)}\n")
# Log all keybindings for comparison
for i, kb in enumerate(self.keyBindings):
if 'v' in kb.keysymstring.lower() or 'version' in kb.handler.description.lower():
logger.info(f"Binding {i}: keysym={kb.keysymstring}, modifiers={kb.modifiers}, mask={kb.modifier_mask}, desc={kb.handler.description}")
with open('/tmp/keybinding_lookup.log', 'a') as f:
f.write(f"Found V-related binding {i}: keysym={kb.keysymstring}, modifiers={kb.modifiers}, mask={kb.modifier_mask}, desc={kb.handler.description}\n")
matches = []
candidates = []
clickCount = keyboardEvent.getClickCount()
for keyBinding in self.keyBindings:
if keyBinding.matches(keyboardEvent.hw_code, keyboardEvent.modifiers):
if keyBinding.modifier_mask == keyboardEvent.modifiers and \
keyBinding.click_count == clickCount:
if event_str.lower() == 'v':
logger.info(f"MATCH found! keysym={keyBinding.keysymstring}, desc={keyBinding.handler.description}")
if (keyboardEvent.modifiers & keyBinding.modifier_mask) == keyBinding.modifiers and \
keyBinding.click_count == clickCount:
matches.append(keyBinding)
# If there's no keysymstring, it's unbound and cannot be
# a match.
@@ -484,8 +529,17 @@ class KeyBindings:
if keyBinding.keysymstring:
candidates.append(keyBinding)
if event_str.lower() == 'v':
logger.info(f"Exact matches: {len(matches)}")
logger.info(f"Candidates: {len(candidates)}")
with open('/tmp/keybinding_lookup.log', 'a') as f:
f.write(f"Exact matches: {len(matches)}\n")
f.write(f"Candidates: {len(candidates)}\n")
self._checkMatchingBindings(keyboardEvent, matches)
if matches:
if event_str.lower() == 'v':
logger.info(f"Returning exact match handler: {matches[0].handler.description}")
return matches[0].handler
if keyboardEvent.isKeyPadKeyWithNumlockOn():
@@ -499,8 +553,12 @@ class KeyBindings:
self._checkMatchingBindings(keyboardEvent, candidates)
for candidate in candidates:
if candidate.click_count <= clickCount:
if event_str.lower() == 'v':
logger.info(f"Returning candidate handler: {candidate.handler.description}")
return candidate.handler
if event_str.lower() == 'v':
logger.info("No handler found!")
return None
def load(self, keymap, handlers):
+17 -2
View File
@@ -21,7 +21,7 @@ except ImportError:
# Fallback if pluggy is not available
def cthulhu_hookimpl(func=None, **kwargs):
"""Fallback decorator when pluggy is not available.
This is a no-op decorator that returns the original function.
It allows the code to continue working without pluggy, though
plugins will be disabled.
@@ -47,6 +47,8 @@ class Plugin:
self.name = ''
self.version = ''
self.description = ''
self._bindings = None
self._gestureBindings = {}
def set_app(self, app):
"""Set the application reference."""
@@ -75,12 +77,16 @@ 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:
return api_helper.registerGestureByString(
binding = api_helper.registerGestureByString(
function,
name,
gestureString,
@@ -89,4 +95,13 @@ 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
+213 -22
View File
@@ -112,11 +112,190 @@ class PluginSystemManager:
# Create plugin directories
self._setup_plugin_dirs()
# Log available plugins directory paths
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 refresh_active_script_keybindings(self):
"""Force active script to refresh its keybindings to include plugin bindings."""
from . import cthulhu_state
if cthulhu_state.activeScript:
active_script = cthulhu_state.activeScript
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f"=== refresh_active_script_keybindings() CALLED ===\n")
f.write(f"Active script: {active_script.name}\n")
# Force the script to recreate its keybindings to include plugin bindings
old_keybindings = active_script.keyBindings
active_script.keyBindings = active_script.getKeyBindings()
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f"Keybindings refreshed: old={len(old_keybindings.keyBindings) if old_keybindings else 0}, new={len(active_script.keyBindings.keyBindings)}\n")
def register_plugin_keybindings_with_active_script(self):
"""Register all plugin keybindings with the active script."""
logger.info("=== register_plugin_keybindings_with_active_script() CALLED ===")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write("=== register_plugin_keybindings_with_active_script() CALLED ===\n")
if not PLUGGY_AVAILABLE:
logger.warning("PLUGGY not available")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write("ERROR: PLUGGY not available\n")
return
from . import cthulhu_state
if not cthulhu_state.activeScript:
logger.warning("No active script available to register plugin keybindings")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write("ERROR: No active script available\n")
return
active_script = cthulhu_state.activeScript
logger.info(f"Registering plugin keybindings with active script: {active_script}")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f"Active script: {active_script}\n")
# First, register keybindings from APIHelper's stored bindings
# This is where plugin keybindings actually get stored
from . import cthulhu
api_helper = cthulhu.cthulhuApp.getAPIHelper()
if api_helper and hasattr(api_helper, '_gestureBindings'):
logger.info("=== FOUND APIHelper with _gestureBindings ===")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write("=== Registering stored gesture bindings from APIHelper ===\n")
f.write(f"Total contexts: {len(api_helper._gestureBindings)}\n")
for context_name, bindings_list in api_helper._gestureBindings.items():
logger.info(f"Processing context '{context_name}' with {len(bindings_list)} bindings")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f"Context '{context_name}': {len(bindings_list)} bindings\n")
for binding in bindings_list:
logger.info(f"Adding stored binding: {binding.keysymstring} with modifiers {binding.modifiers}")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f" Binding: {binding.keysymstring} modifiers={binding.modifiers} desc={binding.handler.description}\n")
# Check if binding already exists to avoid duplicates
if not active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"):
active_script.getKeyBindings().add(binding)
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f" ADDED to active script!\n")
# Force recalculation of keycode if it wasn't set when device was None
if not binding.keycode and binding.keysymstring:
from . import keybindings
binding.keycode = keybindings.getKeycode(binding.keysymstring)
# Register key grab at system level - this was missing!
grab_ids = cthulhu.addKeyGrab(binding)
if grab_ids:
binding._grab_ids = grab_ids
else:
logger.warning(f"Failed to create key grab for {binding.keysymstring} - device may not be available")
else:
logger.info(f"Binding already exists: {binding.keysymstring}")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write(f" Already exists, skipped\n")
else:
logger.warning("=== NO APIHelper or no _gestureBindings found ===")
with open('/tmp/plugin_registration.log', 'a') as f:
f.write("ERROR: No APIHelper or no _gestureBindings found!\n")
if api_helper:
f.write(f"APIHelper exists but _gestureBindings: {hasattr(api_helper, '_gestureBindings')}\n")
else:
f.write("No APIHelper found!\n")
# Also check the old method for any plugins that use get_bindings()
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}")
# Check if binding already exists to avoid duplicates
if not active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"):
active_script.getKeyBindings().add(binding)
# Force recalculation of keycode if it wasn't set when device was None
if not binding.keycode and binding.keysymstring:
from . import keybindings
binding.keycode = keybindings.getKeycode(binding.keysymstring)
# Register key grab at system level - this was missing!
grab_ids = cthulhu.addKeyGrab(binding)
if grab_ids:
binding._grab_ids = grab_ids
else:
logger.warning(f"Failed to create key grab for {binding.keysymstring} - device may not be available")
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)
@@ -218,26 +397,34 @@ class PluginSystemManager:
def setActivePlugins(self, activePlugins):
"""Set active plugins and sync their state."""
logger.info(f"=== PluginSystemManager.setActivePlugins called ===")
logger.info(f"Setting active plugins: {activePlugins}")
# Make sure we have scanned for plugins first
if not self._plugins:
logger.info("No plugins found, rescanning...")
self.rescanPlugins()
self._active_plugins = activePlugins
# Log active vs available plugins
available_plugins = [p.get_module_name() for p in self.plugins]
logger.info(f"Available plugins: {available_plugins}")
logger.info(f"Active plugins: {self._active_plugins}")
# Check specifically for DisplayVersion
if 'DisplayVersion' in self._active_plugins:
logger.info("DisplayVersion is in active plugins list!")
else:
logger.warning("DisplayVersion is NOT in active plugins list!")
# Find missing plugins
missing_plugins = [p for p in self._active_plugins if p not in available_plugins]
if missing_plugins:
logger.warning(f"Active plugins not found: {missing_plugins}")
self.syncAllPluginsActive()
logger.info("=== PluginSystemManager.setActivePlugins completed ===")
def setPluginActive(self, pluginInfo, active):
"""Set the active state of a plugin."""
@@ -258,7 +445,7 @@ class PluginSystemManager:
def isPluginActive(self, pluginInfo):
"""Check if a plugin is active."""
module_name = pluginInfo.get_module_name()
# Builtin plugins are always active
if pluginInfo.builtin:
logger.debug(f"Plugin {module_name} is builtin, active by default")
@@ -271,34 +458,34 @@ class PluginSystemManager:
# Check case-insensitive match in active plugins list
active_plugins = self.getActivePlugins()
# Try exact match first
if module_name in active_plugins:
logger.debug(f"Plugin {module_name} found in active plugins list")
return True
# Try case-insensitive match
module_name_lower = module_name.lower()
is_active = any(plugin.lower() == module_name_lower for plugin in active_plugins)
if is_active:
logger.debug(f"Plugin {module_name} found in active plugins list (case-insensitive match)")
else:
logger.debug(f"Plugin {module_name} not found in active plugins list")
return is_active
def syncAllPluginsActive(self):
"""Sync the active state of all plugins."""
logger.info("Syncing active state of all plugins")
# Log plugin status before syncing
if PLUGIN_DEBUG:
for pluginInfo in self.plugins:
is_active = self.isPluginActive(pluginInfo)
is_loaded = pluginInfo.loaded
logger.debug(f"Plugin {pluginInfo.get_module_name()}: active={is_active}, loaded={is_loaded}")
# First unload inactive plugins
for pluginInfo in self.plugins:
if not self.isPluginActive(pluginInfo) and pluginInfo.loaded:
@@ -311,7 +498,7 @@ class PluginSystemManager:
logger.info(f"Loading active plugin: {pluginInfo.get_module_name()}")
result = self.loadPlugin(pluginInfo)
logger.info(f"Plugin {pluginInfo.get_module_name()} load result: {result}")
# Log final plugin status
active_plugins = [p.get_module_name() for p in self.plugins if p.loaded]
logger.info(f"Active plugins after sync: {active_plugins}")
@@ -326,7 +513,7 @@ class PluginSystemManager:
return False
module_name = pluginInfo.get_module_name()
logger.info(f"Attempting to load plugin: {module_name}")
logger.info(f"=== PluginSystemManager.loadPlugin starting for: {module_name} ===")
try:
# Already loaded?
@@ -337,27 +524,27 @@ class PluginSystemManager:
# Try to find the plugin file
module_name = pluginInfo.get_module_name()
plugin_dir = pluginInfo.get_module_dir()
# Check for plugin.py first (standard format)
plugin_file = os.path.join(plugin_dir, "plugin.py")
# Fall back to [PluginName].py if plugin.py doesn't exist
if not os.path.exists(plugin_file):
alternative_plugin_file = os.path.join(plugin_dir, f"{module_name}.py")
if os.path.exists(alternative_plugin_file):
plugin_file = alternative_plugin_file
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
if not os.path.exists(plugin_file):
logger.error(f"Plugin file not found: {plugin_file}")
return False
logger.info(f"Loading plugin from: {plugin_file}")
spec = importlib.util.spec_from_file_location(module_name, plugin_file)
if spec is None:
logger.error(f"Failed to create spec for plugin: {module_name}")
return False
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
pluginInfo.module = module
@@ -385,7 +572,7 @@ class PluginSystemManager:
# Ensure plugins have a reference to the app
plugin_instance.app = self.getApp()
logger.info(f"Set app reference for plugin: {module_name}")
if hasattr(plugin_instance, 'set_app'):
plugin_instance.set_app(self.getApp())
logger.info(f"Called set_app() for plugin: {module_name}")
@@ -398,10 +585,10 @@ class PluginSystemManager:
if self.plugin_manager is None:
logger.error(f"Plugin manager is None when loading {module_name}")
return False
logger.info(f"Registering plugin with pluggy: {module_name}")
self.plugin_manager.register(plugin_instance)
try:
logger.info(f"Activating plugin: {module_name}")
self.plugin_manager.hook.activate(plugin=plugin_instance)
@@ -413,6 +600,10 @@ 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:
@@ -23,3 +23,8 @@
# Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca
"""Clipboard plugin package."""
from .plugin import Clipboard
__all__ = ['Clipboard']
+1 -36
View File
@@ -72,42 +72,7 @@ class Clipboard(Plugin):
return
logger.info("Deactivating Clipboard plugin")
try:
# Unregister keyboard shortcut
if self.app:
api_helper = self.app.getAPIHelper()
if api_helper and hasattr(api_helper, 'unregisterShortcut'):
api_helper.unregisterShortcut('kb:cthulhu+shift+c')
logger.debug("Unregistered clipboard shortcut")
except Exception as e:
logger.error(f"Error deactivating Clipboard plugin: {e}")
"""Activate the plugin."""
# Skip if this activation call isn't for us
if plugin is not None and plugin is not self:
return
logger.info("Activating Clipboard plugin")
try:
# Register keyboard shortcut
self.registerGestureByString(self.speakClipboard, _('clipboard'), 'kb:cthulhu+shift+c')
logger.debug("Registered shortcut for clipboard")
except Exception as e:
logger.error(f"Error activating Clipboard plugin: {e}")
@cthulhu_hookimpl
def deactivate(self, plugin=None):
"""Deactivate the plugin."""
# Skip if this deactivation call isn't for us
if plugin is not None and plugin is not self:
return
logger.info("Deactivating Clipboard plugin")
try:
# Unregister keyboard shortcut
self.unregisterGestureByString('kb:cthulhu+shift+c')
logger.debug("Unregistered clipboard shortcut")
except Exception as e:
logger.error(f"Error deactivating Clipboard plugin: {e}")
# Note: Currently no unregister method needed as keybindings are managed by APIHelper
def speakClipboard(self, script=None, inputEvent=None):
"""Present the contents of the clipboard."""
+50 -3
View File
@@ -32,16 +32,62 @@ class DisplayVersion(Plugin):
return
try:
logger.info("Activating DisplayVersion plugin")
logger.info("=== DisplayVersion plugin activation starting ===")
logger.info(f"Plugin name: {self.name}")
logger.info(f"App reference: {self.app}")
# Also write to a debug file
with open('/tmp/displayversion_debug.log', 'a') as f:
f.write("=== DisplayVersion plugin activation starting ===\n")
f.write(f"Plugin name: {self.name}\n")
f.write(f"App reference: {self.app}\n")
# Check if we have access to the API helper
if not self.app:
logger.error("DisplayVersion: No app reference available!")
return
api_helper = self.app.getAPIHelper()
if not api_helper:
logger.error("DisplayVersion: No API helper available!")
return
logger.info(f"API helper: {api_helper}")
# Register keyboard shortcut
gesture_string = 'kb:cthulhu+shift+v'
logger.info(f"DisplayVersion: Attempting to register gesture: {gesture_string}")
self._kb_binding = self.registerGestureByString(
self.speakText,
f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}',
'kb:cthulhu+shift+v'
gesture_string
)
logger.info(f"DisplayVersion: Registered keybinding result: {self._kb_binding}")
if self._kb_binding:
logger.info(f"Binding keysymstring: {self._kb_binding.keysymstring}")
logger.info(f"Binding modifiers: {self._kb_binding.modifiers}")
logger.info(f"Binding modifier_mask: {self._kb_binding.modifier_mask}")
logger.info(f"Binding handler: {self._kb_binding.handler}")
with open('/tmp/displayversion_debug.log', 'a') as f:
f.write(f"SUCCESS: Keybinding created!\n")
f.write(f" keysymstring: {self._kb_binding.keysymstring}\n")
f.write(f" modifiers: {self._kb_binding.modifiers}\n")
f.write(f" modifier_mask: {self._kb_binding.modifier_mask}\n")
else:
logger.error("DisplayVersion: Failed to create keybinding!")
with open('/tmp/displayversion_debug.log', 'a') as f:
f.write("ERROR: Failed to create keybinding!\n")
logger.info("=== DisplayVersion plugin activation completed ===")
with open('/tmp/displayversion_debug.log', 'a') as f:
f.write("=== DisplayVersion plugin activation completed ===\n\n")
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):
@@ -55,6 +101,7 @@ 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:
@@ -65,4 +112,4 @@ class DisplayVersion(Plugin):
return True
except Exception as e:
logger.error(f"Error in DisplayVersion speakText: {e}")
return False
return False
@@ -0,0 +1,10 @@
indentationaudio_PYTHON = \
__init__.py \
plugin.py
indentationaudiodir = $(pkgdatadir)/cthulhu/plugins/IndentationAudio
indentationaudio_DATA = \
plugin.info
EXTRA_DIST = $(indentationaudio_DATA)
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
#
# Copyright (c) 2025 Stormux
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
"""IndentationAudio plugin package."""
from .plugin import IndentationAudio
__all__ = ['IndentationAudio']
@@ -0,0 +1,9 @@
[Core]
Name = IndentationAudio
Module = IndentationAudio
[Documentation]
Description = Provides audio feedback for indentation level changes when navigating code or text
Author = Stormux
Version = 1.0.0
Website = https://git.stormux.org/storm/cthulhu
@@ -0,0 +1,334 @@
#!/usr/bin/env python3
#
# Copyright (c) 2025 Stormux
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
"""IndentationAudio plugin for Cthulhu - Provides audio feedback for indentation level changes."""
import logging
import re
from gi.repository import GLib
from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import cmdnames
from cthulhu import debug
from cthulhu import input_event
from cthulhu import keybindings
from cthulhu import messages
from cthulhu import script_utilities
from cthulhu import settings_manager
from cthulhu import sound
from cthulhu.sound_generator import Tone
logger = logging.getLogger(__name__)
_settingsManager = settings_manager.getManager()
class IndentationAudio(Plugin):
"""Plugin that provides audio cues for indentation level changes."""
def __init__(self, *args, **kwargs):
"""Initialize the IndentationAudio plugin."""
super().__init__(*args, **kwargs)
self._enabled = True # Start enabled by default
self._last_indentation_level = {} # Track per-object indentation
self._event_listener_id = None
self._kb_binding = None
self._player = None
# Audio settings
self._base_frequency = 200 # Base frequency in Hz
self._frequency_step = 80 # Hz per indentation level
self._tone_duration = 0.15 # Seconds
self._max_frequency = 1200 # Cap frequency to avoid harsh sounds
logger.info("IndentationAudio plugin initialized")
@cthulhu_hookimpl
def activate(self, plugin=None):
"""Activate the IndentationAudio plugin."""
if plugin is not None and plugin is not self:
return
try:
logger.info("=== IndentationAudio plugin activation starting ===")
# Initialize sound player
self._player = sound.getPlayer()
# Register keybinding for toggle (Cthulhu+I)
self._register_keybinding()
# Connect to text caret movement events
self._connect_to_events()
logger.info("IndentationAudio plugin activated successfully")
return True
except Exception as e:
logger.error(f"Failed to activate IndentationAudio plugin: {e}")
return False
@cthulhu_hookimpl
def deactivate(self, plugin=None):
"""Deactivate the IndentationAudio plugin."""
if plugin is not None and plugin is not self:
return
try:
logger.info("=== IndentationAudio plugin deactivation starting ===")
# Disconnect from events
self._disconnect_from_events()
# Clear tracking data
self._last_indentation_level.clear()
logger.info("IndentationAudio plugin deactivated successfully")
return True
except Exception as e:
logger.error(f"Failed to deactivate IndentationAudio plugin: {e}")
return False
def _register_keybinding(self):
"""Register the Cthulhu+I keybinding for toggling the plugin."""
try:
if not self.app:
logger.error("IndentationAudio: No app reference available for keybinding")
return
api_helper = self.app.getAPIHelper()
if not api_helper:
logger.error("IndentationAudio: No API helper available")
return
# Register Cthulhu+I keybinding
gesture_string = "Cthulhu+i"
description = "Toggle indentation audio feedback"
handler = self._toggle_indentation_audio
self._kb_binding = api_helper.registerGestureByString(
gesture_string, handler, description
)
if self._kb_binding:
logger.info(f"IndentationAudio: Registered keybinding {gesture_string}")
else:
logger.error(f"IndentationAudio: Failed to register keybinding {gesture_string}")
except Exception as e:
logger.error(f"IndentationAudio: Error registering keybinding: {e}")
def _connect_to_events(self):
"""Connect to text navigation events."""
try:
# Hook into the dynamic API to make ourselves available to scripts
if self.app:
api_manager = self.app.getDynamicApiManager()
api_manager.registerAPI('IndentationAudioPlugin', self)
logger.info("IndentationAudio: Registered with dynamic API manager")
# We'll also monkey-patch the default script's sayLine method
self._monkey_patch_script_methods()
except Exception as e:
logger.error(f"IndentationAudio: Error connecting to events: {e}")
def _disconnect_from_events(self):
"""Disconnect from text navigation events."""
try:
# Unregister from dynamic API
if self.app:
api_manager = self.app.getDynamicApiManager()
api_manager.unregisterAPI('IndentationAudioPlugin')
# Restore original script methods
self._restore_script_methods()
except Exception as e:
logger.error(f"IndentationAudio: Error disconnecting from events: {e}")
def _monkey_patch_script_methods(self):
"""Monkey-patch the default script's line navigation methods."""
try:
# Get the current active script
if self.app:
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
if state and hasattr(state, 'activeScript') and state.activeScript:
script = state.activeScript
# Store original method
if hasattr(script, 'sayLine'):
self._original_sayLine = script.sayLine
# Create wrapped version
def wrapped_sayLine(obj):
# Call original method first
result = self._original_sayLine(obj)
# Add our indentation audio check
try:
line, caretOffset, startOffset = script.getTextLineAtCaret(obj)
self.check_indentation_change(obj, line)
except Exception as e:
logger.error(f"IndentationAudio: Error in wrapped_sayLine: {e}")
return result
# Replace the method
script.sayLine = wrapped_sayLine
logger.info("IndentationAudio: Successfully monkey-patched sayLine method")
except Exception as e:
logger.error(f"IndentationAudio: Error monkey-patching script methods: {e}")
def _restore_script_methods(self):
"""Restore original script methods."""
try:
if self.app and hasattr(self, '_original_sayLine'):
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
if state and hasattr(state, 'activeScript') and state.activeScript:
script = state.activeScript
if hasattr(script, 'sayLine'):
script.sayLine = self._original_sayLine
logger.info("IndentationAudio: Restored original sayLine method")
except Exception as e:
logger.error(f"IndentationAudio: Error restoring script methods: {e}")
def _toggle_indentation_audio(self, script, inputEvent=None):
"""Toggle the indentation audio feedback on/off."""
try:
self._enabled = not self._enabled
state = "enabled" if self._enabled else "disabled"
# Announce the state change
message = f"Indentation audio {state}"
if hasattr(script, 'speakMessage'):
script.speakMessage(message)
logger.info(f"IndentationAudio: Toggled to {state}")
return True
except Exception as e:
logger.error(f"IndentationAudio: Error toggling state: {e}")
return False
def _calculate_indentation_level(self, line_text):
"""Calculate the indentation level of a line."""
if not line_text:
return 0
# Remove non-breaking spaces and convert to regular spaces
line = line_text.replace("\u00a0", " ")
# Find the first non-whitespace character
match = re.search(r"[^ \t]", line)
if not match:
return 0 # Empty or whitespace-only line
indent_text = line[:match.start()]
# Calculate indentation level (4 spaces = 1 level, 1 tab = 1 level)
level = 0
for char in indent_text:
if char == '\t':
level += 1
elif char == ' ':
level += 0.25 # 4 spaces = 1 level
return int(level)
def _generate_indentation_tone(self, new_level, old_level):
"""Generate an audio tone for indentation level change."""
if not self._enabled or not self._player:
return
# Calculate frequency based on new indentation level
frequency = min(
self._base_frequency + (new_level * self._frequency_step),
self._max_frequency
)
# Determine stereo panning based on change direction
# Left channel for indent increase, right for decrease
volume_multiplier = 0.7
try:
# Create tone
tone = Tone(
duration=self._tone_duration,
frequency=frequency,
volumeMultiplier=volume_multiplier,
wave=Tone.SINE_WAVE
)
# Play the tone
self._player.play(tone, interrupt=False)
debug_msg = f"IndentationAudio: Played tone - Level: {new_level}, Freq: {frequency}Hz"
logger.debug(debug_msg)
except Exception as e:
logger.error(f"IndentationAudio: Error generating tone: {e}")
def check_indentation_change(self, obj, line_text):
"""Check if indentation has changed and play audio cue if needed.
This method is intended to be called by scripts during line navigation.
"""
if not self._enabled or not line_text:
return
try:
# Get object identifier for tracking
obj_id = str(obj) if obj else "unknown"
# Calculate current indentation level
current_level = self._calculate_indentation_level(line_text)
# Get previous level for this object
previous_level = self._last_indentation_level.get(obj_id, current_level)
# Update tracking
self._last_indentation_level[obj_id] = current_level
# Play audio cue if indentation changed
if current_level != previous_level:
self._generate_indentation_tone(current_level, previous_level)
debug_msg = f"IndentationAudio: Indentation changed from {previous_level} to {current_level}"
logger.debug(debug_msg)
except Exception as e:
logger.error(f"IndentationAudio: Error checking indentation change: {e}")
def is_enabled(self):
"""Return whether the plugin is currently enabled."""
return self._enabled
def set_enabled(self, enabled):
"""Set the enabled state of the plugin."""
self._enabled = enabled
def on_script_change(self, new_script):
"""Handle when the active script changes."""
try:
# Restore previous script if it was patched
self._restore_script_methods()
# Re-apply patches to new script
self._monkey_patch_script_methods()
# Clear tracking data for new context
self._last_indentation_level.clear()
logger.info("IndentationAudio: Handled script change")
except Exception as e:
logger.error(f"IndentationAudio: Error handling script change: {e}")
+1 -1
View File
@@ -1,4 +1,4 @@
SUBDIRS = Clipboard DisplayVersion hello_world self_voice ByeCthulhu HelloCthulhu SimplePluginSystem
SUBDIRS = Clipboard DisplayVersion IndentationAudio hello_world self_voice ByeCthulhu HelloCthulhu SimplePluginSystem
cthulhu_pythondir=$(pkgpythondir)/plugins
-5
View File
@@ -1,9 +1,6 @@
#!/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
@@ -20,8 +17,6 @@
# 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."""
+7
View File
@@ -318,6 +318,13 @@ class ScriptManager:
return
newScript.activate()
# Register plugin keybindings with the new active script
from . import cthulhu
plugin_manager = cthulhu.cthulhuApp.getPluginSystemManager()
if plugin_manager:
pass # 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)
+37
View File
@@ -363,6 +363,8 @@ class Script(script.Script):
return keyBindings
def getExtensionBindings(self):
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"=== getExtensionBindings() called ===\n")
keyBindings = keybindings.KeyBindings()
bindings = self.notificationPresenter.get_bindings()
@@ -407,6 +409,41 @@ class Script(script.Script):
for keyBinding in bindings.keyBindings:
keyBindings.add(keyBinding)
# Add plugin keybindings from APIHelper storage
try:
import cthulhu.cthulhu as cthulhu
if hasattr(cthulhu, 'cthulhuApp') and cthulhu.cthulhuApp:
api_helper = cthulhu.cthulhuApp.getAPIHelper()
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"=== Checking for plugin bindings ===\n")
f.write(f"api_helper exists: {api_helper is not None}\n")
if api_helper:
f.write(f"api_helper has _gestureBindings: {hasattr(api_helper, '_gestureBindings')}\n")
if hasattr(api_helper, '_gestureBindings'):
f.write(f"_gestureBindings content: {api_helper._gestureBindings}\n")
f.write(f"Available contexts: {list(api_helper._gestureBindings.keys())}\n")
if api_helper and hasattr(api_helper, '_gestureBindings'):
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"=== Adding plugin bindings in getExtensionBindings() ===\n")
for context_name, context_bindings in api_helper._gestureBindings.items():
for binding in context_bindings:
keyBindings.add(binding)
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"Added plugin binding: {binding.keysymstring} modifiers={binding.modifiers} desc={binding.handler.description}\n")
else:
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"=== No plugin bindings available ===\n")
else:
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"=== cthulhuApp not available ===\n")
except Exception as e:
import cthulhu.debug as debug
debug.printMessage(debug.LEVEL_WARNING, f"Failed to add plugin bindings: {e}", True)
with open('/tmp/extension_bindings_debug.log', 'a') as f:
f.write(f"Exception in plugin binding addition: {e}\n")
return keyBindings
def getKeyBindings(self):