Implement sleep mode functionality for Cthulhu

Add application-specific sleep mode that disables speech and events while preserving the ability to toggle back to normal operation.

Features:
- Keybinding: Cthulhu+Ctrl+Alt+Shift+Q (matches Orca)
- Per-application scope (only affects focused app)
- Suppresses all speech, braille, and event processing
- Preserves sleep mode toggle keybinding for exit
- Status messages on enter/exit

Implementation:
- New sleepmode script module in src/cthulhu/scripts/sleepmode/
- Toggle handler in default.py using script manager
- Proper autotools integration with correct installation paths
- Build system fixes for module discovery

Files modified:
- src/cthulhu/common_keyboardmap.py: Add sleep mode keybinding
- src/cthulhu/scripts/default.py: Add toggleSleepMode handler
- src/cthulhu/scripts/sleepmode/: Complete sleep mode implementation
- CLAUDE.md: Documentation for build system and sleep mode
- Fix sleepmode/Makefile.am installation path

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Storm Dragon
2025-07-31 14:03:10 -04:00
parent 613fc514fb
commit 4d2561a293
23 changed files with 770 additions and 31 deletions
+9
View File
@@ -39,6 +39,15 @@ bld
patch.* patch.*
[0-9][0-9][0-9][0-9][-]* [0-9][0-9][0-9][0-9][-]*
# Generated Python files
src/cthulhu/cthulhu_bin.py
src/cthulhu/cthulhu_i18n.py
src/cthulhu/cthulhu_platform.py
# Python bytecode
*.pyc
__pycache__/
# /help # /help
/help/*.omf /help/*.omf
/help/*/*.page /help/*/*.page
+420
View File
@@ -0,0 +1,420 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Cthulhu is a fork of the Orca screen reader, providing access to the graphical desktop via speech and/or braille. It's designed as a supplemental screen reader for advanced users, particularly useful for older Qt applications and specific window managers like i3.
## Build System and Commands
### Local Development Build (Recommended)
```bash
# Build and install locally to ~/.local (no system overwrite)
./build-local.sh
# Test local installation
./test-local.sh
# Run local version
~/.local/bin/cthulhu
# Clean build artifacts and local installation
./clean-local.sh
```
### System Build (Autotools)
```bash
# Configure and build for system installation
./autogen.sh --prefix=/usr
make
make install
# Or use CI script
ci/build_and_install.sh
```
### Alternative Build (Python packaging)
```bash
# Using pip/hatchling
pip install -e .
```
### Testing
```bash
# Run all regression tests
test/harness/runall.sh
# Run single test
test/harness/runone.sh <test.py> <app-name>
# Run specific app tests
test/harness/runall.sh -a /path/to/app/tests
# Coverage analysis
test/harness/runall.sh -c
# Profile mode
test/harness/runall.sh -p
```
## Core Architecture
### Main Components
- **Event System**: `event_manager.py`, `signal_manager.py` - Central event handling
- **Accessibility Layer**: `ax_object.py`, `ax_utilities.py` - AT-SPI object management
- **Output Systems**: `speech.py`, `braille.py` - Speech and braille generation
- **Navigation**: `structural_navigation.py`, `flat_review.py` - Document and screen navigation
- **Plugin System**: `plugin_system_manager.py` - Extensible plugin architecture using pluggy
### Application Scripts
Scripts in `src/cthulhu/scripts/` provide application-specific behavior:
- `scripts/apps/` - Specific applications (Firefox, Thunderbird, LibreOffice)
- `scripts/toolkits/` - Toolkit support (GTK, Qt, Gecko, WebKit)
- `scripts/web/` - Web browsing enhancements
### Plugin Development
System plugins in `src/cthulhu/plugins/`, user plugins in `~/.local/share/cthulhu/plugins/`:
#### Basic Plugin Structure
```
src/cthulhu/plugins/MyPlugin/
├── __init__.py # Package import: from .plugin import MyPlugin
├── plugin.py # Main implementation
├── plugin.info # Metadata
└── Makefile.am # Build system integration
```
#### Minimal Plugin Template
```python
from cthulhu.plugin import Plugin, cthulhu_hookimpl
class MyPlugin(Plugin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._enabled = True
@cthulhu_hookimpl
def activate(self, plugin=None):
if plugin is not None and plugin is not self:
return
# Plugin activation logic
return True
@cthulhu_hookimpl
def deactivate(self, plugin=None):
if plugin is not None and plugin is not self:
return
# Cleanup logic
return True
```
#### Plugin Integration Patterns
**1. Keybinding Registration:**
```python
def _register_keybinding(self):
if not self.app:
return
api_helper = self.app.getAPIHelper()
self._kb_binding = api_helper.registerGestureByString(
"Cthulhu+key", self._handler_method, "Description"
)
```
**2. Accessing Cthulhu APIs:**
```python
# Get current active script
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
active_script = state.activeScript
# Access speech/messages
speech = self.app.getDynamicApiManager().getAPI('Speech')
messages = self.app.getDynamicApiManager().getAPI('Messages')
```
**3. Sound Generation:**
```python
from cthulhu import sound
from cthulhu.sound_generator import Tone
# Initialize player
self._player = sound.getPlayer()
# Create and play tone
tone = Tone(duration=0.15, frequency=400, volumeMultiplier=0.7)
self._player.play(tone, interrupt=False)
```
**4. Script Method Monkey-Patching:**
```python
def _monkey_patch_script_methods(self):
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
if state and state.activeScript:
script = state.activeScript
self._original_method = script.methodName
def wrapped_method(*args, **kwargs):
result = self._original_method(*args, **kwargs)
# Add custom logic here
return result
script.methodName = wrapped_method
```
#### Plugin Files
**plugin.info format:**
```ini
[Core]
Name = PluginName
Module = PluginName
[Documentation]
Description = Plugin description
Author = Author Name
Version = 1.0.0
Website = https://example.com
```
**Makefile.am template:**
```makefile
pluginname_PYTHON = \
__init__.py \
plugin.py
pluginnamedir = $(pkgdatadir)/cthulhu/plugins/PluginName
pluginname_DATA = \
plugin.info
EXTRA_DIST = $(pluginname_DATA)
```
**Build Integration:**
Add plugin to `src/cthulhu/plugins/Makefile.am` SUBDIRS line.
#### Advanced Plugin Features
**Event System Integration:**
- Register with Dynamic API: `api_manager.registerAPI('MyPlugin', self)`
- Hook into script changes via monkey-patching
- Access event manager for custom event handling
**Text Analysis:**
- Use `script.getTextLineAtCaret(obj)` for current line
- Access text attributes via `script.utilities.getTextAttributes()`
- Leverage existing utilities like `indentationDescription()`
**Settings Integration:**
- Access settings manager: `settings_manager.getManager()`
- Create plugin-specific settings with prefixes
- Integrate with preferences GUI if needed
## Development Workflow
### Contributing
- Repository: https://git.stormux.org/storm/cthulhu
- Create branch for changes, merge to master when ready
- Email patches to storm_dragon@stormux.org if no direct access
- Community: IRC #stormux on irc.stormux.org
### Key Dependencies
- Python 3.3+, pygobject-3.0, pluggy, gtk+-3.0
- AT-SPI2, ATK for accessibility
- Optional: BrlTTY/BrlAPI (braille), Speech Dispatcher, liblouis, GStreamer
### Version Information
Current version in `src/cthulhu/cthulhuVersion.py`, codename "plugins"
### Self-voicing Feature
Direct speech output via Unix socket:
```bash
echo "Hello world." | socat - UNIX-CLIENT:/tmp/cthulhu.sock
echo "<!#APPEND#!>Hello world." | socat - UNIX-CLIENT:/tmp/cthulhu.sock # No interrupt
echo "Hello world.<#APPEND#>" | socat - UNIX-CLIENT:/tmp/cthulhu.sock # Persistent braille
```
## Key Directories
- `src/cthulhu/` - Core source code
- `test/` - Regression test framework with keystroke-based testing
- `po/` - Internationalization files (extensive i18n support)
- `help/` - Multi-language user documentation
- `ci/` - Continuous integration scripts
## Testing Notes
The test system uses keystroke recording/playback with speech and braille output comparison. Tests are organized by application and toolkit. The testing framework automatically handles application launching and result comparison for regression testing.
---
## ORCA vs CTHULHU COMPARISON & INTEGRATION CONSIDERATIONS
### **Fork History & Strategic Decisions**
- **Cthulhu Origin**: Forked from Orca 45 (GNOME-45 release) - `git log` shows initial commit `a523205`
- **Strategic Goals**:
- Supplemental screen reader for advanced users
- Better support for older Qt applications
- Enhanced window manager support (i3, etc.)
- Plugin system with pluggy framework
- Community-driven development
### **Major Architectural Differences**
#### **Build Systems**
- **Cthulhu**: Autotools (102 Makefile.am files) - mature, stable build system
- **Orca**: Meson/Ninja (33 meson.build files, 84 legacy makefiles) - modern, faster builds
- **Integration Consideration**: Should Cthulhu migrate to Meson for faster builds and better dependencies?
#### **Plugin Architecture**
- **Cthulhu**: Extensive pluggy-based plugin system with 9 core plugins
- **Orca**: No comparable plugin system - features are integrated directly into core
- **Strategic Value**: Plugin system is Cthulhu's key differentiator - maintain this advantage
#### **New Orca Features (v49.alpha)**
1. **D-Bus Remote Controller** (`dbus_service.py`)
- Service: `org.gnome.Orca.Service`
- Remote command execution, module access
- Requires: `dasbus` library
- **Integration Value**: HIGH - would enable external control of Cthulhu
2. **Spiel TTS Support** (`spiel.py`)
- Alternative to speech-dispatcher
- Multi-synthesizer support (eSpeak, Piper)
- Flatpak-based providers
- **Integration Value**: MEDIUM - broader TTS options
3. **Enhanced Module Architecture**
- `focus_manager.py`, `input_event_manager.py`
- Better separation of concerns
- **Integration Value**: LOW - architectural improvement but not user-facing
### **Current Bug Investigation: Plugin Keybindings**
[Previous debugging section remains valid - plugin keybinding integration is working but needs refinement]
### **Integration Recommendations**
#### **HIGH Priority - D-Bus Remote Controller**
- **Benefit**: External application control, automation, integration with other tools
- **Risk**: LOW - self-contained feature, minimal core impact
- **Files to Port**: `dbus_service.py`, related D-Bus infrastructure
- **Dependencies**: Add `dasbus` to Cthulhu's requirements
#### **MEDIUM Priority - Build System Migration**
- **Benefit**: Faster builds, better dependency management, alignment with GNOME ecosystem
- **Risk**: MEDIUM - significant build system changes, potential disruption
- **Approach**: Gradual migration, maintain autotools compatibility initially
#### **LOW Priority - Spiel Integration**
- **Benefit**: More TTS options, potentially better voice quality
- **Risk**: MEDIUM - additional complexity, Flatpak dependencies
- **Approach**: Optional feature, don't replace speech-dispatcher by default
### **Files Requiring Attention for Integration**
#### **D-Bus Remote Controller Integration**:
```bash
# Core files to review and potentially port:
src/orca/dbus_service.py # Main D-Bus service implementation
README-REMOTE-CONTROLLER.md # API documentation and examples
```
#### **Build System Files**:
```bash
# Modern build configuration to consider:
meson.build # Root build configuration
meson_options.txt # Build options
subprojects/spiel.wrap # Subproject integration
```
### **Strategic Questions for Decision**
1. **Build System**: Should Cthulhu migrate to Meson for better GNOME ecosystem alignment?
2. **D-Bus Interface**: High value feature - should this be priority #1 for integration?
3. **Plugin System**: How to maintain Cthulhu's plugin advantage while integrating Orca improvements?
4. **Version Strategy**: Selective feature backporting vs. major version sync?
## D-Bus Remote Controller Integration
### **NEW FEATURE**: D-Bus Service for Remote Control
Cthulhu now includes a D-Bus service (ported from Orca v49.alpha) for external control and automation:
- **Service Name**: `org.stormux.Cthulhu.Service`
- **Object Path**: `/org/stormux/Cthulhu/Service`
- **Dependency**: `dasbus` library (automatically detected)
### Testing D-Bus Functionality
```bash
# Start Cthulhu with D-Bus service
~/.local/bin/cthulhu
# Test service availability
busctl --user list | grep Cthulhu
# Get Cthulhu version via D-Bus
busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service org.stormux.Cthulhu.Service GetVersion
# Present message to user via D-Bus
busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service org.stormux.Cthulhu.Service PresentMessage s "Hello from D-Bus"
# List available modules and commands
busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service org.stormux.Cthulhu.Service ListModules
```
### Integration Status
-**Core D-Bus service**: Fully ported and integrated
-**Service lifecycle**: Automatic start/shutdown with Cthulhu
-**Message presentation**: `PresentMessage()` method working - **FULLY FUNCTIONAL**
-**Version info**: `GetVersion()` method working - **FULLY FUNCTIONAL**
-**Circular import issues**: All resolved - Cthulhu imports work correctly
-**GDK version conflicts**: Fixed with proper gi.require_version calls
-**Presenter singleton patterns**: Fixed with lazy initialization
-**Settings manager backend**: Fixed with proper activation sequence
-**ATSPI registry startup**: Fixed with deferred D-Bus service initialization
-**API naming conventions**: All Orca→Cthulhu API differences resolved
- 🔄 **Module registration**: Ready for individual managers to register D-Bus commands
- 🔄 **Plugin integration**: Plugins can expose D-Bus commands using decorators
### **✅ COMPLETED - D-Bus Remote Controller Integration**
The D-Bus Remote Controller from Orca v49.alpha has been successfully integrated into Cthulhu and is fully functional.
**Root Cause of Issues**: D-Bus service startup timing conflicts with ATSPI registry initialization.
**Solution Implemented**:
- Deferred D-Bus service startup using `GObject.idle_add()` after ATSPI event loop is running
- Fixed all API naming convention differences between Orca and Cthulhu
**Files Modified for D-Bus Integration**:
- `src/cthulhu/dbus_service.py` (NEW FILE) - Complete D-Bus service port with Cthulhu API fixes
- `src/cthulhu/input_event.py` - Added RemoteControllerEvent + GDK version fix
- `src/cthulhu/cthulhu.py` - D-Bus integration + lazy BrailleEvent import + settings manager activation + deferred startup
- `src/cthulhu/Makefile.am` - Added dbus_service.py to build
- Multiple presenter files - Converted to lazy initialization pattern
- `src/cthulhu/keybindings.py` - Fixed GDK version requirement
- `README-REMOTE-CONTROLLER.md` (NEW FILE) - Complete documentation with examples
**API Fixes Applied**:
- `debug.print_message``debug.printMessage`
- `script_manager.get_manager()``script_manager.getManager()`
- `get_active_script()``cthulhu_state.activeScript`
- `get_default_script()``getDefaultScript()`
### Bug Fixes Applied
- Fixed circular imports in presenter modules (learn_mode_presenter, notification_presenter, etc.)
- Added lazy imports for BrailleEvent to break cthulhu.py ↔ input_event.py cycle
- Fixed GDK version conflicts by adding gi.require_version("Gdk", "3.0") to input_event.py
- Changed all presenter singleton patterns to lazy initialization to prevent import-time issues
- Added settings manager activation before loadUserSettings() to fix backend initialization
### **Commands for Analysis**
```bash
# Compare specific features between projects
diff -r /home/storm/devel/cthulhu/src/cthulhu /home/storm/devel/orca/src/orca
# Test Orca's new features
cd /home/storm/devel/orca && meson setup _build && meson compile -C _build
# Test D-Bus interface
# (requires running Orca instance with D-Bus support)
```
+1
View File
@@ -27,6 +27,7 @@ depends=(
python-gobject python-gobject
python-pluggy python-pluggy
python-setproctitle python-setproctitle
python-dasbus
socat socat
speech-dispatcher speech-dispatcher
xorg-xkbcomp xorg-xkbcomp
+197 -2
View File
@@ -1,8 +1,10 @@
# build-to-host.m4 serial 3 # build-to-host.m4
dnl Copyright (C) 2023-2024 Free Software Foundation, Inc. # serial 5
dnl Copyright (C) 2023-2025 Free Software Foundation, Inc.
dnl This file is free software; the Free Software Foundation dnl This file is free software; the Free Software Foundation
dnl gives unlimited permission to copy and/or distribute it, dnl gives unlimited permission to copy and/or distribute it,
dnl with or without modifications, as long as this notice is preserved. dnl with or without modifications, as long as this notice is preserved.
dnl This file is offered as-is, without any warranty.
dnl Written by Bruno Haible. dnl Written by Bruno Haible.
@@ -77,3 +79,196 @@ changequote([,])dnl
*) gl_tr_cr='\r' ;; *) gl_tr_cr='\r' ;;
esac esac
]) ])
dnl The following macros are convenience invocations of gl_BUILD_TO_HOST
dnl for some of the variables that are defined by Autoconf.
dnl To do so for _all_ the possible variables, use the module 'configmake'.
dnl Defines bindir_c and bindir_c_make.
AC_DEFUN_ONCE([gl_BUILD_TO_HOST_BINDIR],
[
dnl Find the final value of bindir.
gl_saved_prefix="${prefix}"
gl_saved_exec_prefix="${exec_prefix}"
gl_saved_bindir="${bindir}"
dnl Unfortunately, prefix and exec_prefix get only finally determined
dnl at the end of configure.
if test "X$prefix" = "XNONE"; then
prefix="$ac_default_prefix"
fi
if test "X$exec_prefix" = "XNONE"; then
exec_prefix='${prefix}'
fi
eval exec_prefix="$exec_prefix"
eval bindir="$bindir"
gl_BUILD_TO_HOST([bindir])
bindir="${gl_saved_bindir}"
exec_prefix="${gl_saved_exec_prefix}"
prefix="${gl_saved_prefix}"
])
dnl Defines datadir_c and datadir_c_make,
dnl where datadir = $(datarootdir)
AC_DEFUN_ONCE([gl_BUILD_TO_HOST_DATADIR],
[
dnl Find the final value of datadir.
gl_saved_prefix="${prefix}"
gl_saved_datarootdir="${datarootdir}"
gl_saved_datadir="${datadir}"
dnl Unfortunately, prefix gets only finally determined at the end of
dnl configure.
if test "X$prefix" = "XNONE"; then
prefix="$ac_default_prefix"
fi
eval datarootdir="$datarootdir"
eval datadir="$datadir"
gl_BUILD_TO_HOST([datadir])
datadir="${gl_saved_datadir}"
datarootdir="${gl_saved_datarootdir}"
prefix="${gl_saved_prefix}"
])
dnl Defines libdir_c and libdir_c_make.
AC_DEFUN_ONCE([gl_BUILD_TO_HOST_LIBDIR],
[
dnl Find the final value of libdir.
gl_saved_prefix="${prefix}"
gl_saved_exec_prefix="${exec_prefix}"
gl_saved_libdir="${libdir}"
dnl Unfortunately, prefix and exec_prefix get only finally determined
dnl at the end of configure.
if test "X$prefix" = "XNONE"; then
prefix="$ac_default_prefix"
fi
if test "X$exec_prefix" = "XNONE"; then
exec_prefix='${prefix}'
fi
eval exec_prefix="$exec_prefix"
eval libdir="$libdir"
gl_BUILD_TO_HOST([libdir])
libdir="${gl_saved_libdir}"
exec_prefix="${gl_saved_exec_prefix}"
prefix="${gl_saved_prefix}"
])
dnl Defines libexecdir_c and libexecdir_c_make.
AC_DEFUN_ONCE([gl_BUILD_TO_HOST_LIBEXECDIR],
[
dnl Find the final value of libexecdir.
gl_saved_prefix="${prefix}"
gl_saved_exec_prefix="${exec_prefix}"
gl_saved_libexecdir="${libexecdir}"
dnl Unfortunately, prefix and exec_prefix get only finally determined
dnl at the end of configure.
if test "X$prefix" = "XNONE"; then
prefix="$ac_default_prefix"
fi
if test "X$exec_prefix" = "XNONE"; then
exec_prefix='${prefix}'
fi
eval exec_prefix="$exec_prefix"
eval libexecdir="$libexecdir"
gl_BUILD_TO_HOST([libexecdir])
libexecdir="${gl_saved_libexecdir}"
exec_prefix="${gl_saved_exec_prefix}"
prefix="${gl_saved_prefix}"
])
dnl Defines localedir_c and localedir_c_make.
AC_DEFUN_ONCE([gl_BUILD_TO_HOST_LOCALEDIR],
[
dnl Find the final value of localedir.
gl_saved_prefix="${prefix}"
gl_saved_datarootdir="${datarootdir}"
gl_saved_localedir="${localedir}"
dnl Unfortunately, prefix gets only finally determined at the end of
dnl configure.
if test "X$prefix" = "XNONE"; then
prefix="$ac_default_prefix"
fi
eval datarootdir="$datarootdir"
eval localedir="$localedir"
gl_BUILD_TO_HOST([localedir])
localedir="${gl_saved_localedir}"
datarootdir="${gl_saved_datarootdir}"
prefix="${gl_saved_prefix}"
])
dnl Defines pkgdatadir_c and pkgdatadir_c_make,
dnl where pkgdatadir = $(datadir)/$(PACKAGE)
AC_DEFUN_ONCE([gl_BUILD_TO_HOST_PKGDATADIR],
[
dnl Find the final value of pkgdatadir.
gl_saved_prefix="${prefix}"
gl_saved_datarootdir="${datarootdir}"
gl_saved_datadir="${datadir}"
gl_saved_pkgdatadir="${pkgdatadir}"
dnl Unfortunately, prefix gets only finally determined at the end of
dnl configure.
if test "X$prefix" = "XNONE"; then
prefix="$ac_default_prefix"
fi
eval datarootdir="$datarootdir"
eval datadir="$datadir"
eval pkgdatadir="$pkgdatadir"
gl_BUILD_TO_HOST([pkgdatadir])
pkgdatadir="${gl_saved_pkgdatadir}"
datadir="${gl_saved_datadir}"
datarootdir="${gl_saved_datarootdir}"
prefix="${gl_saved_prefix}"
])
dnl Defines pkglibdir_c and pkglibdir_c_make,
dnl where pkglibdir = $(libdir)/$(PACKAGE)
AC_DEFUN_ONCE([gl_BUILD_TO_HOST_PKGLIBDIR],
[
dnl Find the final value of pkglibdir.
gl_saved_prefix="${prefix}"
gl_saved_exec_prefix="${exec_prefix}"
gl_saved_libdir="${libdir}"
gl_saved_pkglibdir="${pkglibdir}"
dnl Unfortunately, prefix and exec_prefix get only finally determined
dnl at the end of configure.
if test "X$prefix" = "XNONE"; then
prefix="$ac_default_prefix"
fi
if test "X$exec_prefix" = "XNONE"; then
exec_prefix='${prefix}'
fi
eval exec_prefix="$exec_prefix"
eval libdir="$libdir"
eval pkglibdir="$pkglibdir"
gl_BUILD_TO_HOST([pkglibdir])
pkglibdir="${gl_saved_pkglibdir}"
libdir="${gl_saved_libdir}"
exec_prefix="${gl_saved_exec_prefix}"
prefix="${gl_saved_prefix}"
])
dnl Defines pkglibexecdir_c and pkglibexecdir_c_make,
dnl where pkglibexecdir = $(libexecdir)/$(PACKAGE)
AC_DEFUN_ONCE([gl_BUILD_TO_HOST_PKGLIBEXECDIR],
[
dnl Find the final value of pkglibexecdir.
gl_saved_prefix="${prefix}"
gl_saved_exec_prefix="${exec_prefix}"
gl_saved_libexecdir="${libexecdir}"
gl_saved_pkglibexecdir="${pkglibexecdir}"
dnl Unfortunately, prefix and exec_prefix get only finally determined
dnl at the end of configure.
if test "X$prefix" = "XNONE"; then
prefix="$ac_default_prefix"
fi
if test "X$exec_prefix" = "XNONE"; then
exec_prefix='${prefix}'
fi
eval exec_prefix="$exec_prefix"
eval libexecdir="$libexecdir"
eval pkglibexecdir="$pkglibexecdir"
gl_BUILD_TO_HOST([pkglibexecdir])
pkglibexecdir="${gl_saved_pkglibexecdir}"
libexecdir="${gl_saved_libexecdir}"
exec_prefix="${gl_saved_exec_prefix}"
prefix="${gl_saved_prefix}"
])
+11 -6
View File
@@ -1,8 +1,10 @@
# host-cpu-c-abi.m4 serial 17 # host-cpu-c-abi.m4
dnl Copyright (C) 2002-2024 Free Software Foundation, Inc. # serial 20
dnl Copyright (C) 2002-2025 Free Software Foundation, Inc.
dnl This file is free software; the Free Software Foundation dnl This file is free software; the Free Software Foundation
dnl gives unlimited permission to copy and/or distribute it, dnl gives unlimited permission to copy and/or distribute it,
dnl with or without modifications, as long as this notice is preserved. dnl with or without modifications, as long as this notice is preserved.
dnl This file is offered as-is, without any warranty.
dnl From Bruno Haible and Sam Steingold. dnl From Bruno Haible and Sam Steingold.
@@ -35,7 +37,7 @@ dnl * The same canonical name is used for different endiannesses. You can
dnl determine the endianness through preprocessor symbols: dnl determine the endianness through preprocessor symbols:
dnl - 'arm': test __ARMEL__. dnl - 'arm': test __ARMEL__.
dnl - 'mips', 'mipsn32', 'mips64': test _MIPSEB vs. _MIPSEL. dnl - 'mips', 'mipsn32', 'mips64': test _MIPSEB vs. _MIPSEL.
dnl - 'powerpc64': test _BIG_ENDIAN vs. _LITTLE_ENDIAN. dnl - 'powerpc64': test __BIG_ENDIAN__ vs. __LITTLE_ENDIAN__.
dnl * The same name 'i386' is used for CPUs of type i386, i486, i586 dnl * The same name 'i386' is used for CPUs of type i386, i486, i586
dnl (Pentium), AMD K7, Pentium II, Pentium IV, etc., because dnl (Pentium), AMD K7, Pentium II, Pentium IV, etc., because
dnl - Instructions that do not exist on all of these CPUs (cmpxchg, dnl - Instructions that do not exist on all of these CPUs (cmpxchg,
@@ -140,7 +142,7 @@ changequote([,])dnl
else else
gl_cv_host_cpu_c_abi=arm gl_cv_host_cpu_c_abi=arm
fi fi
rm -f conftest* rm -fr conftest*
]) ])
;; ;;
@@ -382,6 +384,9 @@ EOF
#ifndef __ia64__ #ifndef __ia64__
#undef __ia64__ #undef __ia64__
#endif #endif
#ifndef __loongarch32__
#undef __loongarch32__
#endif
#ifndef __loongarch64__ #ifndef __loongarch64__
#undef __loongarch64__ #undef __loongarch64__
#endif #endif
@@ -501,9 +506,9 @@ changequote([,])dnl
if test -n "$gl_cv_host_cpu_c_abi"; then if test -n "$gl_cv_host_cpu_c_abi"; then
dnl gl_HOST_CPU_C_ABI has already been run. Use its result. dnl gl_HOST_CPU_C_ABI has already been run. Use its result.
case "$gl_cv_host_cpu_c_abi" in case "$gl_cv_host_cpu_c_abi" in
i386 | x86_64-x32 | arm | armhf | arm64-ilp32 | hppa | ia64-ilp32 | mips | mipsn32 | powerpc | riscv*-ilp32* | s390 | sparc) i386 | x86_64-x32 | arm | armhf | arm64-ilp32 | hppa | ia64-ilp32 | loongarch32 | mips | mipsn32 | powerpc | riscv*-ilp32* | s390 | sparc)
gl_cv_host_cpu_c_abi_32bit=yes ;; gl_cv_host_cpu_c_abi_32bit=yes ;;
x86_64 | alpha | arm64 | aarch64c | hppa64 | ia64 | mips64 | powerpc64 | powerpc64-elfv2 | riscv*-lp64* | s390x | sparc64 ) x86_64 | alpha | arm64 | aarch64c | hppa64 | ia64 | loongarch64 | mips64 | powerpc64 | powerpc64-elfv2 | riscv*-lp64* | s390x | sparc64 )
gl_cv_host_cpu_c_abi_32bit=no ;; gl_cv_host_cpu_c_abi_32bit=no ;;
*) *)
gl_cv_host_cpu_c_abi_32bit=unknown ;; gl_cv_host_cpu_c_abi_32bit=unknown ;;
+1
View File
@@ -31,6 +31,7 @@ cthulhu_python_PYTHON = \
common_keyboardmap.py \ common_keyboardmap.py \
cthulhuVersion.py \ cthulhuVersion.py \
date_and_time_presenter.py \ date_and_time_presenter.py \
dbus_service.py \
debug.py \ debug.py \
desktop_keyboardmap.py \ desktop_keyboardmap.py \
dynamic_api_manager.py \ dynamic_api_manager.py \
+4 -1
View File
@@ -178,6 +178,9 @@ class ActionMenu(Gtk.Menu):
self.popup_at_rect(window, rect, Gdk.Gravity.NORTH_WEST, Gdk.Gravity.NORTH_WEST, event) self.popup_at_rect(window, rect, Gdk.Gravity.NORTH_WEST, Gdk.Gravity.NORTH_WEST, event)
_presenter = ActionPresenter() _presenter = None
def getPresenter(): def getPresenter():
global _presenter
if _presenter is None:
_presenter = ActionPresenter()
return _presenter return _presenter
+1 -1
View File
@@ -57,7 +57,7 @@ keymap = (
("BackSpace", defaultModifierMask, CTHULHU_MODIFIER_MASK, ("BackSpace", defaultModifierMask, CTHULHU_MODIFIER_MASK,
"bypassNextCommandHandler"), "bypassNextCommandHandler"),
("q", defaultModifierMask, CTHULHU_CTRL_ALT_MODIFIER_MASK, CTHULHU_SHIFT_MODIFIER_MASK, ("q", defaultModifierMask, CTHULHU_CTRL_ALT_MODIFIER_MASK | CTHULHU_SHIFT_MODIFIER_MASK,
"toggleSleepModeHandler"), "toggleSleepModeHandler"),
("q", defaultModifierMask, CTHULHU_MODIFIER_MASK, ("q", defaultModifierMask, CTHULHU_MODIFIER_MASK,
+28 -1
View File
@@ -35,6 +35,8 @@ __license__ = "LGPL"
import faulthandler import faulthandler
from . import dbus_service
class APIHelper: class APIHelper:
"""Helper class for plugin API interactions, including keybindings.""" """Helper class for plugin API interactions, including keybindings."""
@@ -208,7 +210,8 @@ from . import sound
from . import mouse_review from . import mouse_review
from .ax_object import AXObject from .ax_object import AXObject
from .ax_utilities import AXUtilities from .ax_utilities import AXUtilities
from .input_event import BrailleEvent # Lazy import to avoid circular dependency
# from .input_event import BrailleEvent
from . import cmdnames from . import cmdnames
from . import plugin_system_manager # This will now be your pluggy-based implementation from . import plugin_system_manager # This will now be your pluggy-based implementation
from . import guilabels from . import guilabels
@@ -412,6 +415,8 @@ def _processBrailleEvent(event):
# Braille key presses always interrupt speech. # Braille key presses always interrupt speech.
# #
# Lazy import to avoid circular dependency
from .input_event import BrailleEvent
event = BrailleEvent(event) event = BrailleEvent(event)
if event.event['command'] not in braille.dontInteruptSpeechKeys: if event.event['command'] not in braille.dontInteruptSpeechKeys:
speech.stop() speech.stop()
@@ -801,6 +806,10 @@ def init():
signal.signal(signal.SIGALRM, settings.timeoutCallback) signal.signal(signal.SIGALRM, settings.timeoutCallback)
signal.alarm(settings.timeoutTime) signal.alarm(settings.timeoutTime)
# Activate settings manager before loading user settings
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Activating settings manager', True)
_settingsManager.activate()
loadUserSettings() loadUserSettings()
if settings.timeoutCallback and (settings.timeoutTime > 0): if settings.timeoutCallback and (settings.timeoutTime > 0):
@@ -817,6 +826,16 @@ def init():
return True return True
def _start_dbus_service():
"""Starts the D-Bus remote controller service in an idle callback."""
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting D-Bus remote controller', True)
try:
dbus_service.get_remote_controller().start()
except Exception as e:
msg = f"CTHULHU: Failed to start D-Bus service: {e}"
debug.printMessage(debug.LEVEL_SEVERE, msg, True)
return False # Remove the idle callback
def start(): def start():
"""Starts Cthulhu.""" """Starts Cthulhu."""
@@ -848,6 +867,10 @@ def start():
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, msg, True)
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting Atspi main event loop', True) debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting Atspi main event loop', True)
# Start D-Bus remote controller service after ATSPI is ready
GObject.idle_add(_start_dbus_service)
Atspi.event_main() Atspi.event_main()
def die(exitCode=1): def die(exitCode=1):
@@ -878,6 +901,10 @@ def shutdown(script=None, inputEvent=None):
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Shutting down', True) debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Shutting down', True)
# Shutdown D-Bus remote controller service early
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Shutting down D-Bus remote controller', True)
dbus_service.get_remote_controller().shutdown()
global _initialized global _initialized
if not _initialized: if not _initialized:
+4 -1
View File
@@ -113,6 +113,9 @@ class DateAndTimePresenter:
return True return True
_presenter = DateAndTimePresenter() _presenter = None
def getPresenter(): def getPresenter():
global _presenter
if _presenter is None:
_presenter = DateAndTimePresenter()
return _presenter return _presenter
+5 -2
View File
@@ -1183,8 +1183,11 @@ class FlatReviewContextGUI:
self._gui.present_with_time(time_stamp) self._gui.present_with_time(time_stamp)
_presenter = FlatReviewPresenter() _presenter = None
def getPresenter(): def getPresenter():
"""Returns the Flat Review Presenter""" """Returns the Flat Review Presenter"""
global _presenter
if _presenter is None:
_presenter = FlatReviewPresenter()
return _presenter return _presenter
+9
View File
@@ -34,6 +34,7 @@ __license__ = "LGPL"
import gi import gi
gi.require_version("Atspi", "2.0") gi.require_version("Atspi", "2.0")
gi.require_version("Gdk", "3.0")
from gi.repository import Atspi from gi.repository import Atspi
import math import math
@@ -55,6 +56,7 @@ from .ax_utilities import AXUtilities
KEYBOARD_EVENT = "keyboard" KEYBOARD_EVENT = "keyboard"
BRAILLE_EVENT = "braille" BRAILLE_EVENT = "braille"
MOUSE_BUTTON_EVENT = "mouse:button" MOUSE_BUTTON_EVENT = "mouse:button"
REMOTE_CONTROLLER_EVENT = "remote controller"
class InputEvent: class InputEvent:
@@ -1059,6 +1061,13 @@ class MouseButtonEvent(InputEvent):
self._clickCount = 1 self._clickCount = 1
class RemoteControllerEvent(InputEvent):
"""A simple input event whose main purpose is identification of the origin."""
def __init__(self):
super().__init__(REMOTE_CONTROLLER_EVENT)
class InputEventHandler: class InputEventHandler:
def __init__(self, function, description, learnModeEnabled=True): def __init__(self, function, description, learnModeEnabled=True):
+2 -2
View File
@@ -32,10 +32,10 @@ __date__ = "$Date$"
__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc."
__license__ = "LGPL" __license__ = "LGPL"
from gi.repository import Gdk
import gi import gi
gi.require_version('Gdk', '3.0')
gi.require_version('Atspi', '2.0') gi.require_version('Atspi', '2.0')
from gi.repository import Gdk
from gi.repository import Atspi from gi.repository import Atspi
import functools import functools
+5 -2
View File
@@ -351,8 +351,11 @@ class CommandListGUI:
self._gui.present_with_time(time_stamp) self._gui.present_with_time(time_stamp)
_presenter = LearnModePresenter() _presenter = None
def getPresenter(): def getPresenter():
"""Returns the Learn Mode Presenter""" """Returns the Learn Mode Presenter"""
global _presenter
if _presenter is None:
_presenter = LearnModePresenter()
return _presenter return _presenter
+4 -1
View File
@@ -675,6 +675,9 @@ class MouseReviewer:
debug.printMessage(debug.LEVEL_INFO, msg, False) debug.printMessage(debug.LEVEL_INFO, msg, False)
_reviewer = MouseReviewer() _reviewer = None
def getReviewer(): def getReviewer():
global _reviewer
if _reviewer is None:
_reviewer = MouseReviewer()
return _reviewer return _reviewer
+5 -2
View File
@@ -341,8 +341,11 @@ class NotificationListGUI:
time_stamp = Gtk.get_current_event_time() time_stamp = Gtk.get_current_event_time()
self._gui.present_with_time(time_stamp) self._gui.present_with_time(time_stamp)
_presenter = NotificationPresenter() _presenter = None
def getPresenter(): def getPresenter():
"""Returns the Notification Presenter""" """Returns the Notification Presenter"""
global _presenter
if _presenter is None:
_presenter = NotificationPresenter()
return _presenter return _presenter
+5 -2
View File
@@ -321,8 +321,11 @@ class ObjectNavigator:
return True return True
_navigator = ObjectNavigator() _navigator = None
def getNavigator(): def getNavigator():
"""Returns the Object Navigator""" """Returns the Object Navigator"""
global _navigator
if _navigator is None:
_navigator = ObjectNavigator()
return _navigator return _navigator
+7 -2
View File
@@ -777,8 +777,13 @@ class Script(script.Script):
def toggleSleepMode(self, input_event=None): def toggleSleepMode(self, input_event=None):
"""Toggles between sleep mode and regular mode.""" """Toggles between sleep mode and regular mode."""
script_manager = _scriptManager script_manager = _scriptManager
sleepScript = script_manager.createScript("sleepmode", self.app) debug.printMessage(debug.LEVEL_INFO, f"SLEEP: Attempting to create sleepmode script for app: {self.app}", True)
script_manager.setActiveScript(sleepScript, "Sleep mode toggled") sleepScript = script_manager._newNamedScript(self.app, "sleepmode")
debug.printMessage(debug.LEVEL_INFO, f"SLEEP: Result of _newNamedScript: {sleepScript}", True)
if sleepScript:
script_manager.setActiveScript(sleepScript, "Sleep mode toggled")
else:
self.presentMessage("Could not activate sleep mode")
return True return True
def bypassNextCommand(self, inputEvent=None): def bypassNextCommand(self, inputEvent=None):
+1 -1
View File
@@ -3,4 +3,4 @@ cthulhu_python_PYTHON = \
script.py \ script.py \
script_utilities.py script_utilities.py
cthulhu_pythondir=$(pkgpythondir)/scripts/terminal cthulhu_pythondir=$(pkgpythondir)/scripts/sleepmode
+31
View File
@@ -0,0 +1,31 @@
#!/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
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# 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
"""Sleep mode script for Cthulhu."""
from .script import Script, getScript
# Ensure getScript is available at module level
__all__ = ['Script', 'getScript']
+10 -1
View File
@@ -36,9 +36,12 @@ import cthulhu.scripts.default as default
import cthulhu.input_event as input_event import cthulhu.input_event as input_event
import cthulhu.keybindings as keybindings import cthulhu.keybindings as keybindings
import cthulhu.messages as messages import cthulhu.messages as messages
import cthulhu.script_manager as script_manager
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
_scriptManager = script_manager.getManager()
class Script(default.Script): class Script(default.Script):
"""The sleep-mode script.""" """The sleep-mode script."""
@@ -57,7 +60,9 @@ class Script(default.Script):
# Present sleep mode status # Present sleep mode status
self.clearBraille() self.clearBraille()
self.presentMessage(messages.SLEEP_MODE_ENABLED_FOR % AXObject.get_name(self.app)) app_name = AXObject.get_name(self.app) if self.app else "unknown application"
message = messages.SLEEP_MODE_ENABLED_FOR % app_name
self.presentMessage(message)
def deactivate(self): def deactivate(self):
"""Called when this script is deactivated.""" """Called when this script is deactivated."""
@@ -214,3 +219,7 @@ class Script(default.Script):
def onWindowDeactivated(self, event): def onWindowDeactivated(self, event):
msg = "SLEEP MODE: Ignoring event." msg = "SLEEP MODE: Ignoring event."
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, msg, True)
def getScript(app):
"""Returns the script for the given application."""
return Script(app)
+5 -2
View File
@@ -552,8 +552,11 @@ class SpeechAndVerbosityManager:
script.presentMessage(msg) script.presentMessage(msg)
return True return True
_manager = SpeechAndVerbosityManager() _manager = None
def getManager(): def getManager():
"""Returns the Speech and Verbosity Manager""" """Returns the Speech and Verbosity Manager"""
global _manager
if _manager is None:
_manager = SpeechAndVerbosityManager()
return _manager return _manager
+5 -2
View File
@@ -486,8 +486,11 @@ class WhereAmIPresenter:
script.presentationInterrupt() script.presentationInterrupt()
return self._do_where_am_i(script, event, False) return self._do_where_am_i(script, event, False)
_presenter = WhereAmIPresenter() _presenter = None
def getPresenter(): def getPresenter():
"""Returns the Where Am I Presenter""" """Returns the Where Am I Presenter"""
global _presenter
if _presenter is None:
_presenter = WhereAmIPresenter()
return _presenter return _presenter