10 Commits

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

View File

@ -1,7 +1,7 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=cthulhu
pkgver=0.4
pkgver=2025.06.06
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

View File

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

View File

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

View File

@ -1,14 +0,0 @@
#!/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']

View File

@ -1,9 +0,0 @@
[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

View File

@ -1,334 +0,0 @@
#!/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}")

View File

@ -1,4 +1,4 @@
SUBDIRS = Clipboard DisplayVersion IndentationAudio hello_world self_voice ByeCthulhu HelloCthulhu SimplePluginSystem
SUBDIRS = Clipboard DisplayVersion hello_world self_voice ByeCthulhu HelloCthulhu SimplePluginSystem
cthulhu_pythondir=$(pkgpythondir)/plugins