diff --git a/.gitignore b/.gitignore index a0f2d31..9d2cbec 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,15 @@ bld patch.* [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/*.omf /help/*/*.page diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6b8e302 --- /dev/null +++ b/CLAUDE.md @@ -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 + +# 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 "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) +``` \ No newline at end of file diff --git a/distro-packages/Arch-Linux/PKGBUILD b/distro-packages/Arch-Linux/PKGBUILD index 02038c9..d550dd4 100644 --- a/distro-packages/Arch-Linux/PKGBUILD +++ b/distro-packages/Arch-Linux/PKGBUILD @@ -27,6 +27,7 @@ depends=( python-gobject python-pluggy python-setproctitle + python-dasbus socat speech-dispatcher xorg-xkbcomp diff --git a/m4/build-to-host.m4 b/m4/build-to-host.m4 index f928e9a..01bff8f 100644 --- a/m4/build-to-host.m4 +++ b/m4/build-to-host.m4 @@ -1,8 +1,10 @@ -# build-to-host.m4 serial 3 -dnl Copyright (C) 2023-2024 Free Software Foundation, Inc. +# build-to-host.m4 +# serial 5 +dnl Copyright (C) 2023-2025 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, 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. @@ -77,3 +79,196 @@ changequote([,])dnl *) gl_tr_cr='\r' ;; 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}" +]) diff --git a/m4/host-cpu-c-abi.m4 b/m4/host-cpu-c-abi.m4 index e860a19..6ca7721 100644 --- a/m4/host-cpu-c-abi.m4 +++ b/m4/host-cpu-c-abi.m4 @@ -1,8 +1,10 @@ -# host-cpu-c-abi.m4 serial 17 -dnl Copyright (C) 2002-2024 Free Software Foundation, Inc. +# host-cpu-c-abi.m4 +# serial 20 +dnl Copyright (C) 2002-2025 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, 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. @@ -35,7 +37,7 @@ dnl * The same canonical name is used for different endiannesses. You can dnl determine the endianness through preprocessor symbols: dnl - 'arm': test __ARMEL__. 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 (Pentium), AMD K7, Pentium II, Pentium IV, etc., because dnl - Instructions that do not exist on all of these CPUs (cmpxchg, @@ -140,7 +142,7 @@ changequote([,])dnl else gl_cv_host_cpu_c_abi=arm fi - rm -f conftest* + rm -fr conftest* ]) ;; @@ -382,6 +384,9 @@ EOF #ifndef __ia64__ #undef __ia64__ #endif +#ifndef __loongarch32__ +#undef __loongarch32__ +#endif #ifndef __loongarch64__ #undef __loongarch64__ #endif @@ -501,9 +506,9 @@ changequote([,])dnl if test -n "$gl_cv_host_cpu_c_abi"; then dnl gl_HOST_CPU_C_ABI has already been run. Use its result. 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 ;; - 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=unknown ;; diff --git a/src/cthulhu/Makefile.am b/src/cthulhu/Makefile.am index 298201e..13baf23 100644 --- a/src/cthulhu/Makefile.am +++ b/src/cthulhu/Makefile.am @@ -31,6 +31,7 @@ cthulhu_python_PYTHON = \ common_keyboardmap.py \ cthulhuVersion.py \ date_and_time_presenter.py \ + dbus_service.py \ debug.py \ desktop_keyboardmap.py \ dynamic_api_manager.py \ diff --git a/src/cthulhu/action_presenter.py b/src/cthulhu/action_presenter.py index 70d4d16..5c69107 100644 --- a/src/cthulhu/action_presenter.py +++ b/src/cthulhu/action_presenter.py @@ -178,6 +178,9 @@ class ActionMenu(Gtk.Menu): self.popup_at_rect(window, rect, Gdk.Gravity.NORTH_WEST, Gdk.Gravity.NORTH_WEST, event) -_presenter = ActionPresenter() +_presenter = None def getPresenter(): + global _presenter + if _presenter is None: + _presenter = ActionPresenter() return _presenter diff --git a/src/cthulhu/common_keyboardmap.py b/src/cthulhu/common_keyboardmap.py index 3b6268f..d768a86 100644 --- a/src/cthulhu/common_keyboardmap.py +++ b/src/cthulhu/common_keyboardmap.py @@ -57,7 +57,7 @@ keymap = ( ("BackSpace", defaultModifierMask, CTHULHU_MODIFIER_MASK, "bypassNextCommandHandler"), - ("q", defaultModifierMask, CTHULHU_CTRL_ALT_MODIFIER_MASK, CTHULHU_SHIFT_MODIFIER_MASK, + ("q", defaultModifierMask, CTHULHU_CTRL_ALT_MODIFIER_MASK | CTHULHU_SHIFT_MODIFIER_MASK, "toggleSleepModeHandler"), ("q", defaultModifierMask, CTHULHU_MODIFIER_MASK, diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index b177a31..76bba92 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -35,6 +35,8 @@ __license__ = "LGPL" import faulthandler +from . import dbus_service + class APIHelper: """Helper class for plugin API interactions, including keybindings.""" @@ -208,7 +210,8 @@ from . import sound from . import mouse_review from .ax_object import AXObject 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 plugin_system_manager # This will now be your pluggy-based implementation from . import guilabels @@ -412,6 +415,8 @@ def _processBrailleEvent(event): # Braille key presses always interrupt speech. # + # Lazy import to avoid circular dependency + from .input_event import BrailleEvent event = BrailleEvent(event) if event.event['command'] not in braille.dontInteruptSpeechKeys: speech.stop() @@ -801,6 +806,10 @@ def init(): signal.signal(signal.SIGALRM, settings.timeoutCallback) signal.alarm(settings.timeoutTime) + # Activate settings manager before loading user settings + debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Activating settings manager', True) + _settingsManager.activate() + loadUserSettings() if settings.timeoutCallback and (settings.timeoutTime > 0): @@ -817,6 +826,16 @@ def init(): 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(): """Starts Cthulhu.""" @@ -848,6 +867,10 @@ def start(): debug.printMessage(debug.LEVEL_INFO, msg, 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() def die(exitCode=1): @@ -878,6 +901,10 @@ def shutdown(script=None, inputEvent=None): 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 if not _initialized: diff --git a/src/cthulhu/date_and_time_presenter.py b/src/cthulhu/date_and_time_presenter.py index b56ecea..b8df145 100644 --- a/src/cthulhu/date_and_time_presenter.py +++ b/src/cthulhu/date_and_time_presenter.py @@ -113,6 +113,9 @@ class DateAndTimePresenter: return True -_presenter = DateAndTimePresenter() +_presenter = None def getPresenter(): + global _presenter + if _presenter is None: + _presenter = DateAndTimePresenter() return _presenter diff --git a/src/cthulhu/flat_review_presenter.py b/src/cthulhu/flat_review_presenter.py index 36a40a4..aff7c45 100644 --- a/src/cthulhu/flat_review_presenter.py +++ b/src/cthulhu/flat_review_presenter.py @@ -1183,8 +1183,11 @@ class FlatReviewContextGUI: self._gui.present_with_time(time_stamp) -_presenter = FlatReviewPresenter() +_presenter = None def getPresenter(): """Returns the Flat Review Presenter""" - + + global _presenter + if _presenter is None: + _presenter = FlatReviewPresenter() return _presenter diff --git a/src/cthulhu/input_event.py b/src/cthulhu/input_event.py index 3ff3041..b5b37a5 100644 --- a/src/cthulhu/input_event.py +++ b/src/cthulhu/input_event.py @@ -34,6 +34,7 @@ __license__ = "LGPL" import gi gi.require_version("Atspi", "2.0") +gi.require_version("Gdk", "3.0") from gi.repository import Atspi import math @@ -55,6 +56,7 @@ from .ax_utilities import AXUtilities KEYBOARD_EVENT = "keyboard" BRAILLE_EVENT = "braille" MOUSE_BUTTON_EVENT = "mouse:button" +REMOTE_CONTROLLER_EVENT = "remote controller" class InputEvent: @@ -1059,6 +1061,13 @@ class MouseButtonEvent(InputEvent): 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: def __init__(self, function, description, learnModeEnabled=True): diff --git a/src/cthulhu/keybindings.py b/src/cthulhu/keybindings.py index d85b453..c3e682e 100644 --- a/src/cthulhu/keybindings.py +++ b/src/cthulhu/keybindings.py @@ -32,10 +32,10 @@ __date__ = "$Date$" __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." __license__ = "LGPL" -from gi.repository import Gdk - import gi +gi.require_version('Gdk', '3.0') gi.require_version('Atspi', '2.0') +from gi.repository import Gdk from gi.repository import Atspi import functools diff --git a/src/cthulhu/learn_mode_presenter.py b/src/cthulhu/learn_mode_presenter.py index 4a68388..7a96970 100644 --- a/src/cthulhu/learn_mode_presenter.py +++ b/src/cthulhu/learn_mode_presenter.py @@ -351,8 +351,11 @@ class CommandListGUI: self._gui.present_with_time(time_stamp) -_presenter = LearnModePresenter() +_presenter = None def getPresenter(): """Returns the Learn Mode Presenter""" - + + global _presenter + if _presenter is None: + _presenter = LearnModePresenter() return _presenter diff --git a/src/cthulhu/mouse_review.py b/src/cthulhu/mouse_review.py index 3f4a041..de9ed6b 100644 --- a/src/cthulhu/mouse_review.py +++ b/src/cthulhu/mouse_review.py @@ -675,6 +675,9 @@ class MouseReviewer: debug.printMessage(debug.LEVEL_INFO, msg, False) -_reviewer = MouseReviewer() +_reviewer = None def getReviewer(): + global _reviewer + if _reviewer is None: + _reviewer = MouseReviewer() return _reviewer diff --git a/src/cthulhu/notification_presenter.py b/src/cthulhu/notification_presenter.py index ebdf7b7..71e0a8b 100644 --- a/src/cthulhu/notification_presenter.py +++ b/src/cthulhu/notification_presenter.py @@ -341,8 +341,11 @@ class NotificationListGUI: time_stamp = Gtk.get_current_event_time() self._gui.present_with_time(time_stamp) -_presenter = NotificationPresenter() +_presenter = None def getPresenter(): """Returns the Notification Presenter""" - + + global _presenter + if _presenter is None: + _presenter = NotificationPresenter() return _presenter diff --git a/src/cthulhu/object_navigator.py b/src/cthulhu/object_navigator.py index 04279e6..bc01f6b 100644 --- a/src/cthulhu/object_navigator.py +++ b/src/cthulhu/object_navigator.py @@ -321,8 +321,11 @@ class ObjectNavigator: return True -_navigator = ObjectNavigator() +_navigator = None def getNavigator(): """Returns the Object Navigator""" - + + global _navigator + if _navigator is None: + _navigator = ObjectNavigator() return _navigator diff --git a/src/cthulhu/scripts/default.py b/src/cthulhu/scripts/default.py index b754d18..d17398f 100644 --- a/src/cthulhu/scripts/default.py +++ b/src/cthulhu/scripts/default.py @@ -777,8 +777,13 @@ class Script(script.Script): def toggleSleepMode(self, input_event=None): """Toggles between sleep mode and regular mode.""" script_manager = _scriptManager - sleepScript = script_manager.createScript("sleepmode", self.app) - script_manager.setActiveScript(sleepScript, "Sleep mode toggled") + debug.printMessage(debug.LEVEL_INFO, f"SLEEP: Attempting to create sleepmode script for app: {self.app}", True) + 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 def bypassNextCommand(self, inputEvent=None): diff --git a/src/cthulhu/scripts/sleepmode/Makefile.am b/src/cthulhu/scripts/sleepmode/Makefile.am index 39c7ea1..64b7127 100644 --- a/src/cthulhu/scripts/sleepmode/Makefile.am +++ b/src/cthulhu/scripts/sleepmode/Makefile.am @@ -3,4 +3,4 @@ cthulhu_python_PYTHON = \ script.py \ script_utilities.py -cthulhu_pythondir=$(pkgpythondir)/scripts/terminal +cthulhu_pythondir=$(pkgpythondir)/scripts/sleepmode diff --git a/src/cthulhu/scripts/sleepmode/__init__.py b/src/cthulhu/scripts/sleepmode/__init__.py index e69de29..241725e 100644 --- a/src/cthulhu/scripts/sleepmode/__init__.py +++ b/src/cthulhu/scripts/sleepmode/__init__.py @@ -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'] \ No newline at end of file diff --git a/src/cthulhu/scripts/sleepmode/script.py b/src/cthulhu/scripts/sleepmode/script.py index 942bd65..c118105 100644 --- a/src/cthulhu/scripts/sleepmode/script.py +++ b/src/cthulhu/scripts/sleepmode/script.py @@ -36,9 +36,12 @@ import cthulhu.scripts.default as default import cthulhu.input_event as input_event import cthulhu.keybindings as keybindings import cthulhu.messages as messages +import cthulhu.script_manager as script_manager from cthulhu.ax_object import AXObject from cthulhu.ax_utilities import AXUtilities +_scriptManager = script_manager.getManager() + class Script(default.Script): """The sleep-mode script.""" @@ -57,7 +60,9 @@ class Script(default.Script): # Present sleep mode status 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): """Called when this script is deactivated.""" @@ -214,3 +219,7 @@ class Script(default.Script): def onWindowDeactivated(self, event): msg = "SLEEP MODE: Ignoring event." debug.printMessage(debug.LEVEL_INFO, msg, True) + +def getScript(app): + """Returns the script for the given application.""" + return Script(app) diff --git a/src/cthulhu/speech_and_verbosity_manager.py b/src/cthulhu/speech_and_verbosity_manager.py index 17f45e7..6a5cc99 100644 --- a/src/cthulhu/speech_and_verbosity_manager.py +++ b/src/cthulhu/speech_and_verbosity_manager.py @@ -552,8 +552,11 @@ class SpeechAndVerbosityManager: script.presentMessage(msg) return True -_manager = SpeechAndVerbosityManager() +_manager = None def getManager(): """Returns the Speech and Verbosity Manager""" - + + global _manager + if _manager is None: + _manager = SpeechAndVerbosityManager() return _manager diff --git a/src/cthulhu/where_am_i_presenter.py b/src/cthulhu/where_am_i_presenter.py index c7fb9da..bbcac10 100644 --- a/src/cthulhu/where_am_i_presenter.py +++ b/src/cthulhu/where_am_i_presenter.py @@ -486,8 +486,11 @@ class WhereAmIPresenter: script.presentationInterrupt() return self._do_where_am_i(script, event, False) -_presenter = WhereAmIPresenter() +_presenter = None def getPresenter(): """Returns the Where Am I Presenter""" - + + global _presenter + if _presenter is None: + _presenter = WhereAmIPresenter() return _presenter