From 040520098033909f339e001343ef9dd784f81bc2 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 1 Jul 2025 13:56:18 -0400 Subject: [PATCH 1/2] 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 --- src/cthulhu/cthulhuVersion.py | 4 +- .../plugins/IndentationAudio/Makefile.am | 10 + .../plugins/IndentationAudio/__init__.py | 14 + .../plugins/IndentationAudio/plugin.info | 9 + .../plugins/IndentationAudio/plugin.py | 334 ++++++++++++++++++ src/cthulhu/plugins/Makefile.am | 2 +- 6 files changed, 370 insertions(+), 3 deletions(-) create mode 100644 src/cthulhu/plugins/IndentationAudio/Makefile.am create mode 100644 src/cthulhu/plugins/IndentationAudio/__init__.py create mode 100644 src/cthulhu/plugins/IndentationAudio/plugin.info create mode 100644 src/cthulhu/plugins/IndentationAudio/plugin.py diff --git a/src/cthulhu/cthulhuVersion.py b/src/cthulhu/cthulhuVersion.py index 5b7ac94..b8e759a 100644 --- a/src/cthulhu/cthulhuVersion.py +++ b/src/cthulhu/cthulhuVersion.py @@ -23,5 +23,5 @@ # Fork of Orca Screen Reader (GNOME) # Original source: https://gitlab.gnome.org/GNOME/orca -version = "2025.06.05" -codeName = "plugins" +version = "2025.07.01" +codeName = "testing" diff --git a/src/cthulhu/plugins/IndentationAudio/Makefile.am b/src/cthulhu/plugins/IndentationAudio/Makefile.am new file mode 100644 index 0000000..ebd1225 --- /dev/null +++ b/src/cthulhu/plugins/IndentationAudio/Makefile.am @@ -0,0 +1,10 @@ +indentationaudio_PYTHON = \ + __init__.py \ + plugin.py + +indentationaudiodir = $(pkgdatadir)/cthulhu/plugins/IndentationAudio + +indentationaudio_DATA = \ + plugin.info + +EXTRA_DIST = $(indentationaudio_DATA) \ No newline at end of file diff --git a/src/cthulhu/plugins/IndentationAudio/__init__.py b/src/cthulhu/plugins/IndentationAudio/__init__.py new file mode 100644 index 0000000..7e5fc5a --- /dev/null +++ b/src/cthulhu/plugins/IndentationAudio/__init__.py @@ -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'] \ No newline at end of file diff --git a/src/cthulhu/plugins/IndentationAudio/plugin.info b/src/cthulhu/plugins/IndentationAudio/plugin.info new file mode 100644 index 0000000..5ab2067 --- /dev/null +++ b/src/cthulhu/plugins/IndentationAudio/plugin.info @@ -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 \ No newline at end of file diff --git a/src/cthulhu/plugins/IndentationAudio/plugin.py b/src/cthulhu/plugins/IndentationAudio/plugin.py new file mode 100644 index 0000000..7b65002 --- /dev/null +++ b/src/cthulhu/plugins/IndentationAudio/plugin.py @@ -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}") \ No newline at end of file diff --git a/src/cthulhu/plugins/Makefile.am b/src/cthulhu/plugins/Makefile.am index a79ed41..cbbb653 100644 --- a/src/cthulhu/plugins/Makefile.am +++ b/src/cthulhu/plugins/Makefile.am @@ -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 From 0c26025a811b93a4ef4b44828bf6f48ff0799108 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 1 Jul 2025 15:55:00 -0400 Subject: [PATCH 2/2] Updates to build files. --- HACKING | 2 +- Makefile.am | 4 ++++ autogen.sh | 12 ++++++++---- configure.ac | 13 +++++++++++-- distro-packages/Arch-Linux/PKGBUILD | 10 +++++----- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/HACKING b/HACKING index 3d54786..2f3e8c5 100644 --- a/HACKING +++ b/HACKING @@ -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 diff --git a/Makefile.am b/Makefile.am index b9a4a76..e4999e0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 diff --git a/autogen.sh b/autogen.sh index b636f0d..21295f9 100755 --- a/autogen.sh +++ b/autogen.sh @@ -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" diff --git a/configure.ac b/configure.ac index 39a5497..88db710 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/distro-packages/Arch-Linux/PKGBUILD b/distro-packages/Arch-Linux/PKGBUILD index 25cd308..02038c9 100644 --- a/distro-packages/Arch-Linux/PKGBUILD +++ b/distro-packages/Arch-Linux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Storm Dragon 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 }