Indentation plugin finally working. Indentation now indicated by beeps.

This commit is contained in:
Storm Dragon
2025-08-02 04:23:51 -04:00
parent 3679609923
commit cb20579625
4 changed files with 194 additions and 63 deletions

View File

@ -1,8 +1,8 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org> # Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=cthulhu pkgname=cthulhu
pkgver=2025.07.01 pkgver=2025.08.02
pkgrel=2 pkgrel=1
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca" pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
url="https://git.stormux.org/storm/cthulhu" url="https://git.stormux.org/storm/cthulhu"
arch=(any) arch=(any)

View File

@ -1,10 +1,6 @@
indentationaudio_PYTHON = \ cthulhu_python_PYTHON = \
__init__.py \ __init__.py \
plugin.info \
plugin.py plugin.py
indentationaudiodir = $(pkgdatadir)/cthulhu/plugins/IndentationAudio cthulhu_pythondir=$(pkgpythondir)/plugins/IndentationAudio
indentationaudio_DATA = \
plugin.info
EXTRA_DIST = $(indentationaudio_DATA)

View File

@ -1,9 +1,8 @@
[Core] name = IndentationAudio
Name = IndentationAudio version = 1.0.0
Module = IndentationAudio description = Provides audio feedback for indentation level changes when navigating code or text
authors = Stormux <storm_dragon@stormux.org>
[Documentation] website = https://git.stormux.org/storm/cthulhu
Description = Provides audio feedback for indentation level changes when navigating code or text copyright = Copyright 2025
Author = Stormux builtin = false
Version = 1.0.0 hidden = false
Website = https://git.stormux.org/storm/cthulhu

View File

@ -14,18 +14,18 @@ import re
from gi.repository import GLib from gi.repository import GLib
from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import cmdnames
from cthulhu import debug from cthulhu import debug
from cthulhu import input_event
from cthulhu import keybindings # Import Cthulhu's sound system
from cthulhu import messages try:
from cthulhu import script_utilities from cthulhu import sound
from cthulhu import settings_manager from cthulhu.sound_generator import Tone
from cthulhu import sound SOUND_AVAILABLE = True
from cthulhu.sound_generator import Tone except ImportError as e:
SOUND_AVAILABLE = False
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Sound import failed: {e}", True)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_settingsManager = settings_manager.getManager()
class IndentationAudio(Plugin): class IndentationAudio(Plugin):
@ -38,8 +38,6 @@ class IndentationAudio(Plugin):
self._last_indentation_level = {} # Track per-object indentation self._last_indentation_level = {} # Track per-object indentation
self._event_listener_id = None self._event_listener_id = None
self._kb_binding = None self._kb_binding = None
self._player = None
# Audio settings # Audio settings
self._base_frequency = 200 # Base frequency in Hz self._base_frequency = 200 # Base frequency in Hz
self._frequency_step = 80 # Hz per indentation level self._frequency_step = 80 # Hz per indentation level
@ -58,7 +56,12 @@ class IndentationAudio(Plugin):
logger.info("=== IndentationAudio plugin activation starting ===") logger.info("=== IndentationAudio plugin activation starting ===")
# Initialize sound player # 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) # Register keybinding for toggle (Cthulhu+I)
self._register_keybinding() self._register_keybinding()
@ -101,23 +104,21 @@ class IndentationAudio(Plugin):
if not self.app: if not self.app:
logger.error("IndentationAudio: No app reference available for keybinding") logger.error("IndentationAudio: No app reference available for keybinding")
return return
api_helper = self.app.getAPIHelper()
if not api_helper:
logger.error("IndentationAudio: No API helper available")
return
# Register Cthulhu+I keybinding # Register Cthulhu+I keybinding using the plugin's registerGestureByString method
gesture_string = "Cthulhu+i" gesture_string = "kb:cthulhu+i"
description = "Toggle indentation audio feedback" description = "Toggle indentation audio feedback"
handler = self._toggle_indentation_audio
self._kb_binding = api_helper.registerGestureByString( self._kb_binding = self.registerGestureByString(
gesture_string, handler, description self._toggle_indentation_audio,
description,
gesture_string
) )
if self._kb_binding: if self._kb_binding:
logger.info(f"IndentationAudio: Registered keybinding {gesture_string}") 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: else:
logger.error(f"IndentationAudio: Failed to register keybinding {gesture_string}") logger.error(f"IndentationAudio: Failed to register keybinding {gesture_string}")
@ -131,27 +132,107 @@ class IndentationAudio(Plugin):
if self.app: if self.app:
api_manager = self.app.getDynamicApiManager() api_manager = self.app.getDynamicApiManager()
api_manager.registerAPI('IndentationAudioPlugin', self) 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 # Try to connect to ATSPI events directly
self._monkey_patch_script_methods() self._connect_to_atspi_events()
except Exception as e: except Exception as e:
logger.error(f"IndentationAudio: Error connecting to events: {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): def _disconnect_from_events(self):
"""Disconnect from text navigation events.""" """Disconnect from text navigation events."""
try: 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 # Unregister from dynamic API
if self.app: if self.app:
api_manager = self.app.getDynamicApiManager() api_manager = self.app.getDynamicApiManager()
api_manager.unregisterAPI('IndentationAudioPlugin') api_manager.unregisterAPI('IndentationAudioPlugin')
# Restore original script methods
self._restore_script_methods()
except Exception as e: except Exception as e:
logger.error(f"IndentationAudio: Error disconnecting from events: {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): def _monkey_patch_script_methods(self):
"""Monkey-patch the default script's line navigation methods.""" """Monkey-patch the default script's line navigation methods."""
@ -212,11 +293,29 @@ class IndentationAudio(Plugin):
if hasattr(script, 'speakMessage'): if hasattr(script, 'speakMessage'):
script.speakMessage(message) 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}") logger.info(f"IndentationAudio: Toggled to {state}")
return True return True
except Exception as e: except Exception as e:
logger.error(f"IndentationAudio: Error toggling state: {e}") logger.error(f"IndentationAudio: Error toggling state: {e}")
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Error toggling: {e}", True)
return False return False
def _calculate_indentation_level(self, line_text): def _calculate_indentation_level(self, line_text):
@ -246,42 +345,76 @@ class IndentationAudio(Plugin):
def _generate_indentation_tone(self, new_level, old_level): def _generate_indentation_tone(self, new_level, old_level):
"""Generate an audio tone for indentation level change.""" """Generate an audio tone for indentation level change."""
if not self._enabled or not self._player: if not self._enabled:
return return
# Calculate frequency based on new indentation level # Calculate frequency based on new indentation level
frequency = min( base_frequency = min(
self._base_frequency + (new_level * self._frequency_step), self._base_frequency + (new_level * self._frequency_step),
self._max_frequency self._max_frequency
) )
# Determine stereo panning based on change direction # Add directional audio cues
# Left channel for indent increase, right for decrease if new_level > old_level:
volume_multiplier = 0.7 # 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: try:
# Create tone debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: About to generate tone for level {new_level} (freq: {frequency}Hz)", True)
tone = Tone(
duration=self._tone_duration,
frequency=frequency,
volumeMultiplier=volume_multiplier,
wave=Tone.SINE_WAVE
)
# Play the tone if not SOUND_AVAILABLE or not self._player:
self._player.play(tone, interrupt=False) 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" # Use Cthulhu's proper sound system
logger.debug(debug_msg) 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: except Exception as e:
logger.error(f"IndentationAudio: Error generating tone: {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): def check_indentation_change(self, obj, line_text):
"""Check if indentation has changed and play audio cue if needed. """Check if indentation has changed and play audio cue if needed.
This method is intended to be called by scripts during line navigation. 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: if not self._enabled or not line_text:
return return
@ -298,15 +431,18 @@ class IndentationAudio(Plugin):
# Update tracking # Update tracking
self._last_indentation_level[obj_id] = current_level 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 # Play audio cue if indentation changed
if current_level != previous_level: if current_level != previous_level:
self._generate_indentation_tone(current_level, previous_level) self._generate_indentation_tone(current_level, previous_level)
debug_msg = f"IndentationAudio: Indentation changed from {previous_level} to {current_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: except Exception as e:
logger.error(f"IndentationAudio: Error checking indentation change: {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): def is_enabled(self):
"""Return whether the plugin is currently enabled.""" """Return whether the plugin is currently enabled."""