diff --git a/distro-packages/Arch-Linux/PKGBUILD b/distro-packages/Arch-Linux/PKGBUILD index 3e57c52..2b44725 100644 --- a/distro-packages/Arch-Linux/PKGBUILD +++ b/distro-packages/Arch-Linux/PKGBUILD @@ -1,8 +1,8 @@ # Maintainer: Storm Dragon pkgname=cthulhu -pkgver=2025.07.01 -pkgrel=2 +pkgver=2025.08.02 +pkgrel=1 pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca" url="https://git.stormux.org/storm/cthulhu" arch=(any) diff --git a/src/cthulhu/plugins/IndentationAudio/Makefile.am b/src/cthulhu/plugins/IndentationAudio/Makefile.am index ebd1225..a1a2f5e 100644 --- a/src/cthulhu/plugins/IndentationAudio/Makefile.am +++ b/src/cthulhu/plugins/IndentationAudio/Makefile.am @@ -1,10 +1,6 @@ -indentationaudio_PYTHON = \ +cthulhu_python_PYTHON = \ __init__.py \ + plugin.info \ plugin.py -indentationaudiodir = $(pkgdatadir)/cthulhu/plugins/IndentationAudio - -indentationaudio_DATA = \ - plugin.info - -EXTRA_DIST = $(indentationaudio_DATA) \ No newline at end of file +cthulhu_pythondir=$(pkgpythondir)/plugins/IndentationAudio \ No newline at end of file diff --git a/src/cthulhu/plugins/IndentationAudio/plugin.info b/src/cthulhu/plugins/IndentationAudio/plugin.info index 5ab2067..b3e61ec 100644 --- a/src/cthulhu/plugins/IndentationAudio/plugin.info +++ b/src/cthulhu/plugins/IndentationAudio/plugin.info @@ -1,9 +1,8 @@ -[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 +name = IndentationAudio +version = 1.0.0 +description = Provides audio feedback for indentation level changes when navigating code or text +authors = Stormux +website = https://git.stormux.org/storm/cthulhu +copyright = Copyright 2025 +builtin = false +hidden = false \ No newline at end of file diff --git a/src/cthulhu/plugins/IndentationAudio/plugin.py b/src/cthulhu/plugins/IndentationAudio/plugin.py index 7b65002..5e38a62 100644 --- a/src/cthulhu/plugins/IndentationAudio/plugin.py +++ b/src/cthulhu/plugins/IndentationAudio/plugin.py @@ -14,18 +14,18 @@ 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 + +# Import Cthulhu's sound system +try: + from cthulhu import sound + from cthulhu.sound_generator import Tone + SOUND_AVAILABLE = True +except ImportError as e: + SOUND_AVAILABLE = False + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Sound import failed: {e}", True) logger = logging.getLogger(__name__) -_settingsManager = settings_manager.getManager() class IndentationAudio(Plugin): @@ -38,8 +38,6 @@ class IndentationAudio(Plugin): 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 @@ -58,7 +56,12 @@ class IndentationAudio(Plugin): logger.info("=== IndentationAudio plugin activation starting ===") # Initialize sound player - self._player = sound.getPlayer() + if SOUND_AVAILABLE: + self._player = sound.getPlayer() + debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Sound player initialized", True) + else: + self._player = None + debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Sound not available", True) # Register keybinding for toggle (Cthulhu+I) self._register_keybinding() @@ -101,23 +104,21 @@ class IndentationAudio(Plugin): 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" + # Register Cthulhu+I keybinding using the plugin's registerGestureByString method + gesture_string = "kb:cthulhu+i" description = "Toggle indentation audio feedback" - handler = self._toggle_indentation_audio - self._kb_binding = api_helper.registerGestureByString( - gesture_string, handler, description + self._kb_binding = self.registerGestureByString( + self._toggle_indentation_audio, + description, + gesture_string ) if self._kb_binding: logger.info(f"IndentationAudio: Registered keybinding {gesture_string}") + logger.info(f"Binding keysymstring: {self._kb_binding.keysymstring}") + logger.info(f"Binding modifiers: {self._kb_binding.modifiers}") else: logger.error(f"IndentationAudio: Failed to register keybinding {gesture_string}") @@ -131,27 +132,107 @@ class IndentationAudio(Plugin): if self.app: api_manager = self.app.getDynamicApiManager() api_manager.registerAPI('IndentationAudioPlugin', self) - logger.info("IndentationAudio: Registered with dynamic API manager") + debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Registered with dynamic API manager", True) - # We'll also monkey-patch the default script's sayLine method - self._monkey_patch_script_methods() + # Try to connect to ATSPI events directly + self._connect_to_atspi_events() except Exception as e: logger.error(f"IndentationAudio: Error connecting to events: {e}") + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Error connecting to events: {e}", True) + + def _connect_to_atspi_events(self): + """Connect to ATSPI caret movement events.""" + try: + # Import ATSPI event system + from gi.repository import Atspi + + # Register for text caret movement events + self._event_listener = Atspi.EventListener.new(self._on_caret_moved) + Atspi.EventListener.register(self._event_listener, "object:text-caret-moved") + + debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Registered for text-caret-moved events", True) + + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: ATSPI connection error: {e}", True) + + def _on_caret_moved(self, event): + """Handle caret movement events.""" + try: + if not self._enabled: + return + + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Caret moved in {event.source}", True) + + # Use Cthulhu's script system to get line text + try: + from cthulhu import cthulhu_state + + # Get current active script + if cthulhu_state.activeScript: + script = cthulhu_state.activeScript + obj = event.source + + # Use script's getTextLineAtCaret method + if hasattr(script, 'getTextLineAtCaret'): + line, caret_offset, start_offset = script.getTextLineAtCaret(obj) + if line is not None: + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Line navigation - line: '{line}'", True) + self.check_indentation_change(obj, line) + else: + debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: getTextLineAtCaret returned None", True) + else: + debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: script has no getTextLineAtCaret method", True) + else: + debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: No active script available", True) + + except Exception as script_e: + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Error using script method: {script_e}", True) + + # Fallback to direct AT-SPI access + try: + obj = event.source + if obj and hasattr(obj, 'queryText'): + text_iface = obj.queryText() + if text_iface: + # Get all text and find current line + full_text = text_iface.getText(0, -1) + caret_pos = text_iface.caretOffset + + if full_text: + lines = full_text.split('\n') + char_count = 0 + for line in lines: + if char_count <= caret_pos <= char_count + len(line): + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Fallback line: '{line}'", True) + self.check_indentation_change(obj, line) + break + char_count += len(line) + 1 # +1 for newline + + except Exception as fallback_e: + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Fallback also failed: {fallback_e}", True) + + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Error in caret moved handler: {e}", True) def _disconnect_from_events(self): """Disconnect from text navigation events.""" try: + # Unregister ATSPI event listener + if hasattr(self, '_event_listener') and self._event_listener: + from gi.repository import Atspi + Atspi.EventListener.deregister(self._event_listener, "object:text-caret-moved") + self._event_listener = None + debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Unregistered ATSPI events", True) + # 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}") + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Error disconnecting: {e}", True) def _monkey_patch_script_methods(self): """Monkey-patch the default script's line navigation methods.""" @@ -212,11 +293,29 @@ class IndentationAudio(Plugin): if hasattr(script, 'speakMessage'): script.speakMessage(message) + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Toggled to {state}", True) + + # Test the indentation detection on current line when enabled + if self._enabled and script: + try: + # Try to get current focus object and line text + from cthulhu import cthulhu_state + obj = cthulhu_state.locusOfFocus + if obj and hasattr(script, 'getTextLineAtCaret'): + line, caretOffset, startOffset = script.getTextLineAtCaret(obj) + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Testing with current line: '{line}'", True) + self.check_indentation_change(obj, line) + else: + debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Could not get focus object or getTextLineAtCaret method", True) + except Exception as test_e: + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Test failed: {test_e}", True) + logger.info(f"IndentationAudio: Toggled to {state}") return True except Exception as e: logger.error(f"IndentationAudio: Error toggling state: {e}") + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Error toggling: {e}", True) return False def _calculate_indentation_level(self, line_text): @@ -246,42 +345,76 @@ class IndentationAudio(Plugin): 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: + if not self._enabled: return # Calculate frequency based on new indentation level - frequency = min( + base_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 + # Add directional audio cues + if new_level > old_level: + # Indentation increased - higher pitch + frequency = base_frequency + 50 + elif new_level < old_level: + # Indentation decreased - lower pitch + frequency = max(base_frequency - 50, 100) + else: + # Same level (shouldn't happen but just in case) + frequency = base_frequency try: - # Create tone - tone = Tone( - duration=self._tone_duration, - frequency=frequency, - volumeMultiplier=volume_multiplier, - wave=Tone.SINE_WAVE - ) + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: About to generate tone for level {new_level} (freq: {frequency}Hz)", True) - # Play the tone - self._player.play(tone, interrupt=False) + if not SOUND_AVAILABLE or not self._player: + debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Sound player not available, using fallback", True) + # Fallback to ASCII bell + if new_level > 0: + beeps = min(new_level, 5) + for i in range(beeps): + print("\a", end="", flush=True) + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Sent {beeps} ASCII bell beeps", True) + return - debug_msg = f"IndentationAudio: Played tone - Level: {new_level}, Freq: {frequency}Hz" - logger.debug(debug_msg) + # Use Cthulhu's proper sound system + try: + # Create a tone based on indentation level + duration = self._tone_duration + volume_multiplier = 0.7 + + tone = Tone( + duration=duration, + frequency=frequency, + volumeMultiplier=volume_multiplier, + wave=Tone.SINE_WAVE + ) + + # Play the tone + self._player.play(tone, interrupt=False) + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Played Cthulhu tone - Level: {new_level}, Freq: {frequency}Hz", True) + + except Exception as sound_e: + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Cthulhu sound failed: {sound_e}", True) + # Fallback to ASCII bell + if new_level > 0: + beeps = min(new_level, 5) + for i in range(beeps): + print("\a", end="", flush=True) + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Used fallback ASCII bell ({beeps} beeps)", True) except Exception as e: logger.error(f"IndentationAudio: Error generating tone: {e}") + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Exception in _generate_indentation_tone: {e}", True) 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. """ + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: check_indentation_change called: enabled={self._enabled}, line='{line_text}'", True) + if not self._enabled or not line_text: return @@ -298,15 +431,18 @@ class IndentationAudio(Plugin): # Update tracking self._last_indentation_level[obj_id] = current_level + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Levels - previous: {previous_level}, current: {current_level}", True) + # 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) + debug.printMessage(debug.LEVEL_INFO, debug_msg, True) except Exception as e: logger.error(f"IndentationAudio: Error checking indentation change: {e}") + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Exception in check_indentation_change: {e}", True) def is_enabled(self): """Return whether the plugin is currently enabled."""