Merged everything into master, at a decent point to save progress.

This commit is contained in:
Storm Dragon
2025-12-12 20:21:53 -05:00
31 changed files with 3638 additions and 22555 deletions
+13
View File
@@ -50,8 +50,21 @@ src/cthulhu/cthulhu_platform.py
# Python bytecode
*.pyc
*.pyo
__pycache__/
# Editor backup files
*~
*.bak
*.swp
*.tmp
*.orig
*.rej
# AT-SPI test/debug files
debug*.log
debug*.out
# Local build directory and artifacts
local-build/
debug-*.out
-14
View File
@@ -1,14 +0,0 @@
Cthulhu Authors
Marc Mulcahy
Willie Walker
Mike Pedersen
Rich Burridge
Joanmarie Diggs
Eitan Isaacson
Scott Haeger
Cthulhu authors
Storm Dragon
+22 -19
View File
@@ -791,29 +791,32 @@ busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service org.
- 🔄 **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.
### **✅ COMPLETED - Enhanced D-Bus Remote Controller with Speech and Key Echo Controls**
The D-Bus Remote Controller from Orca v49.alpha has been successfully re-ported and enhanced with comprehensive speech and typing echo controls.
**Root Cause of Issues**: D-Bus service startup timing conflicts with ATSPI registry initialization.
**Latest Enhancement (2025)**:
- **SpeechManager Module**: Complete D-Bus control over speech settings (muting, verbosity, punctuation, capitalization, number pronunciation)
- **TypingEchoManager Module**: Granular key echo controls (character/word/sentence echo, per-key-type settings)
- **No systemd dependency**: Direct session bus registration without service files
- **Real-time effect**: All settings take effect immediately
**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 Created/Modified for Enhanced D-Bus Integration**:
- `src/cthulhu/speech_and_verbosity_manager.py` - Enhanced with D-Bus getters/setters for all speech settings
- `src/cthulhu/typing_echo_presenter.py` (NEW FILE) - Complete typing echo system with D-Bus controls
- `src/cthulhu/cthulhu.py` - D-Bus service registration for speech and typing echo managers
- `src/cthulhu/meson.build` - Added typing_echo_presenter.py to build
- `README-REMOTE-CONTROLLER.md` - Updated with comprehensive speech and key echo examples
**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
**Available D-Bus Modules**:
- **SpeechManager**: Speech muting, verbosity, punctuation, capitalization, number styles, indentation speech
- **TypingEchoManager**: Master key echo, character/word/sentence echo, per-key-type controls (alphabetic, numeric, punctuation, space, modifier, function, action, navigation, diacritical keys)
- **DefaultScript**: Core Cthulhu commands
**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()`
**D-Bus Interface Design**:
- Service: `org.stormux.Cthulhu.Service`
- Module paths: `/org/stormux/Cthulhu/Service/ModuleName`
- Generic interface: `org.stormux.Cthulhu.Module`
- Methods: `ExecuteRuntimeGetter`, `ExecuteRuntimeSetter`, `ExecuteCommand`
### Bug Fixes Applied
- Fixed circular imports in presenter modules (learn_mode_presenter, notification_presenter, etc.)
-4
View File
@@ -1,4 +0,0 @@
2009-06-09 Willie Walker <william.walker@sun.com>
As of June 9, 2009, the ChangeLog is auto-generated when releasing.
If you are seeing this, use 'git log' for a detailed list of changes.
-20252
View File
File diff suppressed because it is too large Load Diff
+165 -179
View File
@@ -1,8 +1,15 @@
# Cthulhu Remote Controller (D-Bus Interface)
> **✅ STABLE**: This D-Bus interface has been successfully ported from Orca v49.alpha and integrated
> into Cthulhu. The API is functional and ready for use, providing external control and automation
> capabilities for the Cthulhu screen reader.
> **⚠️⚠️ WORK IN PROGRESS**: This D-Bus interface is brand new and not yet feature complete.
Low-risk feature additions will continue to be made. The API may be
modified beyond bug fixes in future versions based on feedback from consumers of this support.
Such changes will be documented here.
> **💡 Desktop-Agnostic Design**: Cthulhu's D-Bus Remote Controller is built on standard D-Bus
session bus infrastructure and works across all desktop environments (GNOME, KDE Plasma, XFCE,
i3, Sway, etc.). The D-Bus service uses no desktop-specific dependencies and follows universal
D-Bus conventions, making it suitable for integration with any application or automation tool
on any Linux desktop environment or window manager.
[TOC]
@@ -12,16 +19,42 @@ Cthulhu exposes a D-Bus service at:
- **Service Name**: `org.stormux.Cthulhu.Service`
- **Main Object Path**: `/org/stormux/Cthulhu/Service`
- **Module Object Paths**: `/org/stormux/Cthulhu/Service/ModuleName`
- **Module Object Paths**: `/org/stormux.Cthulhu/Service/ModuleName`
(e.g., `/org/stormux/Cthulhu/Service/SpeechAndVerbosityManager`)
See [REMOTE-CONTROLLER-COMMANDS.md](REMOTE-CONTROLLER-COMMANDS.md) for a complete
list of available commands.
## Dependencies
The D-Bus interface requires:
- **dasbus** - Python D-Bus library used by Cthulhu for the remote controller implementation.
([Installation instructions](https://dasbus.readthedocs.io/en/latest/index.html))
- **python-dasbus** package (available on most distributions)
## Alternative Tools for D-Bus Interaction
While this documentation primarily uses `gdbus` for examples, you can use any D-Bus tool or library:
### Using `busctl` (systemd D-Bus tool)
```bash
busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service GetVersion
```
### Using Python with `dasbus`
```python
from dasbus.connection import SessionMessageBus
bus = SessionMessageBus()
proxy = bus.get_proxy("org.stormux.Cthulhu.Service", "/org/stormux/Cthulhu/Service")
version = proxy.GetVersion()
```
### Using `qdbus` (Qt D-Bus tool - available on KDE)
```bash
qdbus org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service.GetVersion
```
## Service-Level Commands
@@ -29,13 +62,6 @@ Commands available directly on the main service (`/org/stormux/Cthulhu/Service`)
### Get Cthulhu's Version
```bash
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service GetVersion
```
**Alternative using gdbus:**
```bash
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service \
@@ -44,17 +70,8 @@ gdbus call --session --dest org.stormux.Cthulhu.Service \
**Returns:** String containing the version (and revision if available)
**Example output:** `s "Cthulhu screen reader version 2025.06.05-plugins (rev 408fb85)"`
### Present a Custom Message in Speech and/or Braille
```bash
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service PresentMessage s "Your message here"
```
**Alternative using gdbus:**
```bash
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service \
@@ -67,15 +84,28 @@ gdbus call --session --dest org.stormux.Cthulhu.Service \
**Returns:** Boolean indicating success
### List Available Service Commands
### Show Cthulhu's Preferences GUI
```bash
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service ListCommands
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service \
--method org.stormux.Cthulhu.Service.ShowPreferences
```
**Alternative using gdbus:**
**Returns:** Boolean indicating success
### Quit Cthulhu
```bash
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service \
--method org.stormux.Cthulhu.Service.Quit
```
**Returns:** Boolean indicating if the quit request was accepted
### List Available Service Commands
```bash
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service \
@@ -86,13 +116,6 @@ gdbus call --session --dest org.stormux.Cthulhu.Service \
### List Registered Modules
```bash
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service ListModules
```
**Alternative using gdbus:**
```bash
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service \
@@ -119,13 +142,6 @@ You can discover and execute these for each module.
#### List Commands for a Module
```bash
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service/ModuleName \
org.stormux.Cthulhu.Module ListCommands
```
**Alternative using gdbus:**
```bash
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service/ModuleName \
@@ -136,15 +152,29 @@ Replace `ModuleName` with an actual module name from `ListModules`.
**Returns:** List of (command_name, description) tuples.
#### List Runtime Getters for a Module
#### List Parameterized Commands for a Module
```bash
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service/ModuleName \
org.stormux.Cthulhu.Module ListRuntimeGetters
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service/ModuleName \
--method org.stormux.Cthulhu.Module.ListParameterizedCommands
```
**Alternative using gdbus:**
Replace `ModuleName` with an actual module name from `ListModules`.
**Returns:** List of (command_name, description, parameters) tuples, where `parameters` is a
list of (parameter_name, parameter_type) tuples.
**Example output:**
```bash
([('GetVoicesForLanguage',
'Returns a list of available voices for the specified language.',
[('language', 'str'), ('variant', 'str'), ('notify_user', 'bool')])],)
```
#### List Runtime Getters for a Module
```bash
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service/ModuleName \
@@ -157,13 +187,6 @@ Replace `ModuleName` with an actual module name from `ListModules`.
#### List Runtime Setters for a Module
```bash
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service/ModuleName \
org.stormux.Cthulhu.Module ListRuntimeSetters
```
**Alternative using gdbus:**
```bash
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service/ModuleName \
@@ -178,13 +201,6 @@ Replace `ModuleName` with an actual module name from `ListModules`.
#### Execute a Runtime Getter
```bash
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service/ModuleName \
org.stormux.Cthulhu.Module ExecuteRuntimeGetter s 'PropertyName'
```
**Alternative using gdbus:**
```bash
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service/ModuleName \
@@ -200,22 +216,16 @@ gdbus call --session --dest org.stormux.Cthulhu.Service \
##### Example: Get the current speech rate
```bash
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service/SpeechAndVerbosityManager \
org.stormux.Cthulhu.Module ExecuteRuntimeGetter s 'Rate'
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service/SpeechAndVerbosityManager \
--method org.stormux.Cthulhu.Module.ExecuteRuntimeGetter 'Rate'
```
This will return the rate as a GLib Variant.
#### Execute a Runtime Setter
```bash
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service/ModuleName \
org.stormux.Cthulhu.Module ExecuteRuntimeSetter s 'PropertyName' v <value>
```
**Alternative using gdbus:**
```bash
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service/ModuleName \
@@ -232,26 +242,13 @@ gdbus call --session --dest org.stormux.Cthulhu.Service \
##### Example: Set the current speech rate
```bash
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service/SpeechAndVerbosityManager \
org.stormux.Cthulhu.Module ExecuteRuntimeSetter s 'Rate' v '<90>'
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service/SpeechAndVerbosityManager \
--method org.stormux.Cthulhu.Module.ExecuteRuntimeSetter 'Rate' '<90>'
```
#### Execute a Module Command
```bash
# With user notification
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service/ModuleName \
org.stormux.Cthulhu.Module ExecuteCommand s 'CommandName' b true
# Without user notification (silent)
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service/ModuleName \
org.stormux.Cthulhu.Module ExecuteCommand s 'CommandName' b false
```
**Alternative using gdbus:**
```bash
# With user notification
gdbus call --session --dest org.stormux.Cthulhu.Service \
@@ -271,116 +268,105 @@ gdbus call --session --dest org.stormux.Cthulhu.Service \
**Returns:** Boolean indicating success
### Please Note
#### Execute a Parameterized Command
**Setting `notify_user=true` is not a guarantee that feedback will be presented.** Some commands
inherently don't make sense to announce. For example:
```bash
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service/ModuleName \
--method org.stormux.Cthulhu.Module.ExecuteParameterizedCommand 'CommandName' \
'{"param1": <"value1">, "param2": <"value2">}' false
```
**Parameters:**
- `CommandName` (string): The name of the parameterized command to execute
- `parameters` (dict): Dictionary of parameter names and values as GLib variants
- `notify_user` (boolean): Whether to notify the user of the action
**Returns:** The result returned by the command as a GLib variant (type depends on the command)
##### Example: Get voices for a specific language
```bash
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service/SpeechAndVerbosityManager \
--method org.stormux.Cthulhu.Module.ExecuteParameterizedCommand 'GetVoicesForLanguage' \
'{"language": <"en-us">, "variant": <"">}' false
```
This will return a list of available voices for US English.
### User Notification Applicability
**Setting `notify_user=true` is not a guarantee that feedback will be presented.**
Some commands inherently don't make sense to announce. For example:
```bash
# This command should simply stop speech, not announce that it is stopping speech.
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service/SpeechAndVerbosityManager \
org.stormux.Cthulhu.Module ExecuteCommand s 'InterruptSpeech' b true
gdbus call --session --dest org.stormux.Cthulhu.Service \
--object-path /org/stormux/Cthulhu/Service/SpeechAndVerbosityManager \
--method org.stormux.Cthulhu.Module.ExecuteCommand 'InterruptSpeech' true
```
In those cases Cthulhu will ignore the value of `notify_user`.
**Setting `notify_user=false` is a guarantee that Cthulhu will remain silent.** If Cthulhu provides any
feedback when `notify_user=false`, it should be considered a bug.
**Setting `notify_user=false` is not a guarantee that Cthulhu will remain silent**, though for the
most part Cthulhu will try to respect this value. The exceptions are:
## Integration with Cthulhu's Plugin System
1. If executing the command has resulted in UI being shown, such as a dialog or menu, the
newly-shown UI will be presented in speech and/or braille based on the user's settings.
Failure to announce that the user has been removed from one window and placed in another
could be extremely confusing.
2. If the *sole* purpose of the command is to announce something without making any other
changes, e.g. `PresentTime`, executing it with `notify_user=false` makes no sense. Adding
checks and early returns to handle this possibility does not seem worth doing. If you
don't want Cthulhu to present the time, don't ask Cthulhu to present the time. 😃
The D-Bus Remote Controller integrates seamlessly with Cthulhu's pluggy-based plugin system. Plugins can:
### Navigator Module "Enabled" State Applicability
- Register their own D-Bus commands using the `@cthulhu_hookimpl` decorator
- Expose plugin-specific functionality via the remote controller
- Access the D-Bus service through the dynamic API manager
**In the Remote Controller, Navigator commands are expected to work even when not "enabled."**
See the main `CLAUDE.md` file for more details on plugin development with D-Bus integration.
Some of Cthulhu's Navigator modules, namely Table Navigator, Caret Navigator, and Structural Navigator,
have an "enabled" state. The reason for this is very much tied to the keyboard-centric nature of
Cthulhu's commands. For instance, if Cthulhu always grabbed "H" (for heading navigation) and the arrow
keys (for caret navigation), normal interaction with applications would be completely broken. For
this reason, Navigator modules whose commands will prevent normal, native interaction with
applications are typically not enabled by default and can be easily disabled.
## Troubleshooting
In contrast, performing Navigator commands via D-Bus does not prevent native interaction with
applications. For instance, one could use the Remote Controller to move to the next heading without
causing H to stop functioning in editable fields. For this reason, and to avoid a performance hit,
the decision was made to not check if (keyboard-centric) navigation commands were enabled. As a
result, it should be possible to use Remote Controller navigation even in "focus mode" or other
cases where Cthulhu is not controlling the caret. This is by design.
### Service Not Available
Given the keyboard-centric nature of Cthulhu's commands, there may be instances in which one uses the
Remote Controller for navigation and Cthulhu fails to correctly update its location in response. If
Cthulhu correctly updates its location when the same navigation command is executed via keyboard,
please report the Remote Controller failure as a new bug in Cthulhu's issue tracker.
If you get "The name is not activatable" or similar errors:
### The "Stickiness" (or Lack Thereof) of On-The-Fly Settings Changes
1. **Check if Cthulhu is running:**
```bash
ps aux | grep cthulhu
```
Cthulhu has a number of keyboard commands to temporarily change settings such as speech rate, pitch,
volume; capitalization style; punctuation level; etc., etc. The question is: how long should
on-the-fly modifications to settings persist?
2. **Check if the D-Bus service is registered:**
```bash
busctl --user list | grep -i cthulhu
```
Early on in Cthulhu's development, the conclusion was that on-the-fly settings changes should be
seen as quite temporary, presumed to be used to address a specific one-time need. For instance,
if reading some difficult-to-understand text, one might want to reduce the speed just for that text.
If one were doing a final proofread of some content, one might want to briefly set the punctuation
level to all. If one needs slow speed and/or verbose punctuation all the time, those should be set
in Cthulhu's Preferences dialogs -- either globally or on a per-app basis. Cthulhu also has a profile
feature through which the user can save settings and quickly load/unload them by switching profiles*.
3. **Verify dasbus is installed:**
```bash
python3 -c "import dasbus; print('dasbus available')"
```
Whether or not that historical decision was the right decision goes beyond the scope of the
Remote Controller. The primary purpose of the Remote Controller is to provide D-Bus access to
commands and runtime settings as if they were performed by the user via keyboard command. Thus if
a setting changed via Remote Controller persists (or fails to persist) in the same way as when
changed via keyboard command, it is not a Remote Controller bug. (It may be a general Cthulhu
bug or feature request, and you are encouraged to file it as such.) On the other hand, if the
behavior of the Remote Controller differs from that of the corresponding or related keyboard
command, please report that Remote Controller failure as a new bug in Cthulhu's issue tracker.
4. **Check Cthulhu debug output:**
```bash
DISPLAY=:0 ~/.local/bin/cthulhu --debug 2>&1 | grep -i dbus
```
### Common Issues
- **Timing Issues**: The D-Bus service starts after ATSPI initialization. Wait a few seconds after Cthulhu startup before attempting D-Bus calls.
- **Permissions**: Ensure you're using `--user` with busctl/gdbus for session bus access.
- **Display**: Make sure `DISPLAY=:0` is set when running Cthulhu in terminal sessions.
## Examples
### Quick Test Script
```bash
#!/bin/bash
# Test Cthulhu D-Bus Remote Controller
echo "Testing Cthulhu D-Bus Remote Controller..."
# Get version
echo "Version:"
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service GetVersion
# Present a message
echo "Presenting message..."
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service PresentMessage s "Hello from D-Bus!"
# List available modules
echo "Available modules:"
busctl --user call org.stormux.Cthulhu.Service \
/org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service ListModules
echo "D-Bus test complete!"
```
## Integration Status
- ✅ **Core D-Bus service**: Fully integrated with Cthulhu
- ✅ **Service lifecycle**: Automatic start/shutdown with Cthulhu
- ✅ **Message presentation**: `PresentMessage()` method working
- ✅ **Version info**: `GetVersion()` method working
- ✅ **Deferred startup**: D-Bus service starts after ATSPI initialization to prevent crashes
- ✅ **Error handling**: Proper exception handling and logging
- 🔄 **Module registration**: Ready for individual managers to register D-Bus commands
- 🔄 **Plugin integration**: Plugins can expose D-Bus commands using decorators
## Future Development
- Add more speech configuration commands, getters, and setters
- Expose Cthulhu's plugin system commands via D-Bus
- Integrate with Cthulhu's advanced features (indentation audio, self-voicing, etc.)
- Progressively expose all of Cthulhu's commands and settings via the remote controller interface
## Related Files
- `src/cthulhu/dbus_service.py` - Main D-Bus service implementation
- `src/cthulhu/cthulhu.py` - Integration and startup logic
- `CLAUDE.md` - Main development guide with plugin integration details
\* *Note: Remote Controller support for profile management is still pending.*
+91
View File
@@ -0,0 +1,91 @@
# Cthulhu Remote Controller - Available Commands
This document lists the currently available D-Bus commands in Cthulhu's Remote Controller interface.
> **Note**: This is a work-in-progress. As more modules are exposed via D-Bus, this document will be expanded. Eventually this will be auto-generated using `tools/generate_dbus_documentation.py`.
## Service-Level Commands
Available on the main service object `/org/stormux/Cthulhu/Service`:
### Service Commands
| Command | Description | Parameters | Returns |
|---------|-------------|------------|---------|
| `GetVersion` | Returns Cthulhu's version string | None | String (version + revision) |
| `PresentMessage` | Present a message via speech/braille | `message` (string) | Boolean (success) |
| `ShowPreferences` | Opens Cthulhu's preferences GUI | None | Boolean (success) |
| `Quit` | Exits Cthulhu | None | Boolean (accepted) |
| `ListCommands` | Lists available service commands | None | List of (name, description) tuples |
| `ListModules` | Lists registered D-Bus modules | None | List of module names |
### Example Usage
```bash
# Get Cthulhu version
busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service GetVersion
# Present a custom message
busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service PresentMessage s "Hello from D-Bus"
# List available commands
busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service ListCommands
# List registered modules
busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service ListModules
# Open preferences
busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service ShowPreferences
# Quit Cthulhu
busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service \
org.stormux.Cthulhu.Service Quit
```
## Module-Level Commands
Currently, no additional modules are exposed via D-Bus beyond the base service commands.
**Planned modules** (to be implemented):
- `SpeechAndVerbosityManager` - Speech settings control (muting, verbosity, punctuation, etc.)
- `TypingEchoManager` - Typing echo settings (character/word/sentence echo)
- `DefaultScript` - Core Cthulhu commands
- Additional navigation and presenter modules
See [README-REMOTE-CONTROLLER.md](README-REMOTE-CONTROLLER.md) for comprehensive D-Bus API documentation and usage examples.
## Verifying D-Bus Service Status
```bash
# Check if Cthulhu's D-Bus service is running
busctl --user list | grep Cthulhu
# Introspect the service to see all available methods
busctl --user introspect org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service
# Get detailed service information
busctl --user status org.stormux.Cthulhu.Service
```
## Contributing
To add new D-Bus-accessible functionality:
1. Add `@dbus_service.command`, `@dbus_service.getter`, or `@dbus_service.setter` decorators to methods
2. Register the module with the D-Bus service in `src/cthulhu/cthulhu.py`
3. Rebuild and test with busctl
4. Update this documentation (or regenerate using `tools/generate_dbus_documentation.py` when available)
## Desktop Environment Compatibility
Cthulhu's D-Bus Remote Controller is desktop-agnostic and works across all Linux desktop environments:
- GNOME, KDE Plasma, XFCE, LXDE
- Tiling window managers (i3, Sway, bspwm, etc.)
- Any environment with D-Bus session bus support
The service uses only standard D-Bus infrastructure with no desktop-specific dependencies.
-104
View File
@@ -1,104 +0,0 @@
dnl a macro to check for ability to create python extensions
dnl AM_CHECK_PYTHON_HEADERS([ACTION-IF-POSSIBLE], [ACTION-IF-NOT-POSSIBLE])
dnl function also defines PYTHON_INCLUDES
AC_DEFUN([AM_CHECK_PYTHON_HEADERS],
[AC_REQUIRE([AM_PATH_PYTHON])
AC_MSG_CHECKING(for headers required to compile python extensions)
dnl deduce PYTHON_INCLUDES
py_prefix=`$PYTHON -c "import sys; print sys.prefix"`
py_exec_prefix=`$PYTHON -c "import sys; print sys.exec_prefix"`
PYTHON_INCLUDES="-I${py_prefix}/include/python${PYTHON_VERSION}"
if test "$py_prefix" != "$py_exec_prefix"; then
PYTHON_INCLUDES="$PYTHON_INCLUDES -I${py_exec_prefix}/include/python${PYTHON_VERSION}"
fi
AC_SUBST(PYTHON_INCLUDES)
dnl check if the headers exist:
save_CPPFLAGS="$CPPFLAGS"
CPPFLAGS="$CPPFLAGS $PYTHON_INCLUDES"
AC_TRY_CPP([#include <Python.h>],dnl
[AC_MSG_RESULT(found)
$1],dnl
[AC_MSG_RESULT(not found)
$2])
CPPFLAGS="$save_CPPFLAGS"
])
dnl AM_CHECK_PYMOD(MODNAME [,SYMBOL [,ACTION-IF-FOUND [,ACTION-IF-NOT-FOUND]]])
dnl Check if a module containing a given symbol is visible to python.
AC_DEFUN([AM_CHECK_PYMOD],
[AC_REQUIRE([AM_PATH_PYTHON])
py_mod_var=`echo $1['_']$2 | sed 'y%./+-%__p_%'`
AC_MSG_CHECKING(for ifelse([$2],[],,[$2 in ])python module $1)
AC_CACHE_VAL(py_cv_mod_$py_mod_var, [
ifelse([$2],[], [prog="
import sys
try:
from gi.repository import GObject
import $1
except ImportError:
sys.exit(1)
except:
sys.exit(0)
sys.exit(0)"], [prog="
import $1
import $1.$2"])
if $PYTHON -c "$prog" 1>&AC_FD_CC 2>&AC_FD_CC
then
eval "py_cv_mod_$py_mod_var=yes"
else
eval "py_cv_mod_$py_mod_var=no"
fi
])
py_val=`eval "echo \`echo '$py_cv_mod_'$py_mod_var\`"`
if test "x$py_val" != xno; then
AC_MSG_RESULT(yes)
ifelse([$3], [],, [$3
])dnl
else
AC_MSG_RESULT(no)
ifelse([$4], [],, [$4
])dnl
fi
])
dnl PYDOC_CHECK()
AC_DEFUN([PYDOC_CHECK],
[
dnl enable/disable documentation building
AC_ARG_ENABLE(pydoc,
AC_HELP_STRING([--enable-pydoc],
[use pydoc to build documentation [default=no]]),,
enable_pydoc=no)
have_pydoc=no
if test x$enable_pydoc = xyes; then
AC_CHECK_FILE("$prefix/bin/pydoc", PYDOC="$prefix/bin/pydoc")
fi
if test -z "$PYDOC"; then
enable_pydoc=no
fi
AM_CONDITIONAL(ENABLE_PYDOC, test x$enable_pydoc = xyes)
])
dnl
dnl JH_ADD_CFLAG(FLAG)
dnl checks whether the C compiler supports the given flag, and if so, adds
dnl it to $CFLAGS. If the flag is already present in the list, then the
dnl check is not performed.
AC_DEFUN([JH_ADD_CFLAG],
[
case " $CFLAGS " in
*@<:@\ \ @:>@$1@<:@\ \ @:>@*)
;;
*)
save_CFLAGS="$CFLAGS"
CFLAGS="$CFLAGS $1"
AC_MSG_CHECKING([whether [$]CC understands $1])
AC_TRY_COMPILE([], [], [jh_has_option=yes], [jh_has_option=no])
AC_MSG_RESULT($jh_has_option)
if test $jh_has_option = no; then
CFLAGS="$save_CFLAGS"
fi
;;
esac])
+11 -4
View File
@@ -1,16 +1,15 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=cthulhu
pkgver=2025.08.19
pkgver=2025.12.12
pkgrel=1
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
url="https://git.stormux.org/storm/cthulhu"
arch=(any)
license=(LGPL)
depends=(
# Core AT-SPI accessibility
# Core AT-SPI accessibility
at-spi2-core
python-atspi
gobject-introspection-runtime
python-gobject
python-cairo
@@ -31,7 +30,6 @@ depends=(
python-dasbus
# AI Assistant dependencies (for screenshots, HTTP requests, and actions)
python-pillow
python-requests
python-pyautogui
@@ -58,6 +56,15 @@ optdepends=(
'openai-codex: ChatGPT AI provider support'
'gemini-cli: Gemini AI provider support'
'ollama: Local AI model support'
# OCR plugin dependencies (optional)
'python-pillow: Image processing for OCR and AI Assistant'
'python-pytesseract: Python wrapper for Tesseract OCR engine'
'python-pdf2image: PDF to image conversion for OCR'
'python-scipy: Scientific computing for OCR color analysis'
'python-webcolors: Color name lookup for OCR text decoration'
'tesseract: OCR engine for text recognition'
'tesseract-data-eng: English language data for Tesseract'
)
makedepends=(
git
-652
View File
@@ -1,652 +0,0 @@
# Makefile.in generated by automake 1.18.1 from Makefile.am.
# @configure_input@
# Copyright (C) 1994-2025 Free Software Foundation, Inc.
# This Makefile.in is free software; the Free Software Foundation
# gives unlimited permission to copy and/or distribute it,
# with or without modifications, as long as this notice is preserved.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE.
@SET_MAKE@
VPATH = @srcdir@
am__is_gnu_make = { \
if test -z '$(MAKELEVEL)'; then \
false; \
elif test -n '$(MAKE_HOST)'; then \
true; \
elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
true; \
else \
false; \
fi; \
}
am__make_running_with_option = \
case $${target_option-} in \
?) ;; \
*) echo "am__make_running_with_option: internal error: invalid" \
"target option '$${target_option-}' specified" >&2; \
exit 1;; \
esac; \
has_opt=no; \
sane_makeflags=$$MAKEFLAGS; \
if $(am__is_gnu_make); then \
sane_makeflags=$$MFLAGS; \
else \
case $$MAKEFLAGS in \
*\\[\ \ ]*) \
bs=\\; \
sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
| sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
esac; \
fi; \
skip_next=no; \
strip_trailopt () \
{ \
flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
}; \
for flg in $$sane_makeflags; do \
test $$skip_next = yes && { skip_next=no; continue; }; \
case $$flg in \
*=*|--*) continue;; \
-*I) strip_trailopt 'I'; skip_next=yes;; \
-*I?*) strip_trailopt 'I';; \
-*O) strip_trailopt 'O'; skip_next=yes;; \
-*O?*) strip_trailopt 'O';; \
-*l) strip_trailopt 'l'; skip_next=yes;; \
-*l?*) strip_trailopt 'l';; \
-[dEDm]) skip_next=yes;; \
-[JT]) skip_next=yes;; \
esac; \
case $$flg in \
*$$target_option*) has_opt=yes; break;; \
esac; \
done; \
test $$has_opt = yes
am__make_dryrun = (target_option=n; $(am__make_running_with_option))
am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
am__rm_f = rm -f $(am__rm_f_notfound)
am__rm_rf = rm -rf $(am__rm_f_notfound)
pkgdatadir = $(datadir)/@PACKAGE@
pkgincludedir = $(includedir)/@PACKAGE@
pkglibdir = $(libdir)/@PACKAGE@
pkglibexecdir = $(libexecdir)/@PACKAGE@
am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
install_sh_DATA = $(install_sh) -c -m 644
install_sh_PROGRAM = $(install_sh) -c
install_sh_SCRIPT = $(install_sh) -c
INSTALL_HEADER = $(INSTALL_DATA)
transform = $(program_transform_name)
NORMAL_INSTALL = :
PRE_INSTALL = :
POST_INSTALL = :
NORMAL_UNINSTALL = :
PRE_UNINSTALL = :
POST_UNINSTALL = :
build_triplet = @build@
host_triplet = @host@
subdir = docs
ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
am__aclocal_m4_deps = $(top_srcdir)/m4/build-to-host.m4 \
$(top_srcdir)/m4/gettext.m4 $(top_srcdir)/m4/host-cpu-c-abi.m4 \
$(top_srcdir)/m4/iconv.m4 $(top_srcdir)/m4/intlmacosx.m4 \
$(top_srcdir)/m4/lib-ld.m4 $(top_srcdir)/m4/lib-link.m4 \
$(top_srcdir)/m4/lib-prefix.m4 $(top_srcdir)/m4/nls.m4 \
$(top_srcdir)/m4/po.m4 $(top_srcdir)/m4/progtest.m4 \
$(top_srcdir)/acinclude.m4 $(top_srcdir)/configure.ac
am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
$(ACLOCAL_M4)
DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON)
mkinstalldirs = $(install_sh) -d
CONFIG_CLEAN_FILES =
CONFIG_CLEAN_VPATH_FILES =
AM_V_P = $(am__v_P_@AM_V@)
am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
am__v_P_0 = false
am__v_P_1 = :
AM_V_GEN = $(am__v_GEN_@AM_V@)
am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
am__v_GEN_0 = @echo " GEN " $@;
am__v_GEN_1 =
AM_V_at = $(am__v_at_@AM_V@)
am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
am__v_at_0 = @
am__v_at_1 =
SOURCES =
DIST_SOURCES =
RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \
ctags-recursive dvi-recursive html-recursive info-recursive \
install-data-recursive install-dvi-recursive \
install-exec-recursive install-html-recursive \
install-info-recursive install-pdf-recursive \
install-ps-recursive install-recursive installcheck-recursive \
installdirs-recursive pdf-recursive ps-recursive \
tags-recursive uninstall-recursive
am__can_run_installinfo = \
case $$AM_UPDATE_INFO_DIR in \
n|no|NO) false;; \
*) (install-info --version) >/dev/null 2>&1;; \
esac
RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \
distclean-recursive maintainer-clean-recursive
am__recursive_targets = \
$(RECURSIVE_TARGETS) \
$(RECURSIVE_CLEAN_TARGETS) \
$(am__extra_recursive_targets)
AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \
distdir distdir-am
am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
# Read a list of newline-separated strings from the standard input,
# and print each of them once, without duplicates. Input order is
# *not* preserved.
am__uniquify_input = $(AWK) '\
BEGIN { nonempty = 0; } \
{ items[$$0] = 1; nonempty = 1; } \
END { if (nonempty) { for (i in items) print i; }; } \
'
# Make sure the list of sources is unique. This is necessary because,
# e.g., the same source file might be shared among _SOURCES variables
# for different programs/libraries.
am__define_uniq_tagged_files = \
list='$(am__tagged_files)'; \
unique=`for i in $$list; do \
if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
done | $(am__uniquify_input)`
DIST_SUBDIRS = $(SUBDIRS)
am__DIST_COMMON = $(srcdir)/Makefile.in
DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
am__relativize = \
dir0=`pwd`; \
sed_first='s,^\([^/]*\)/.*$$,\1,'; \
sed_rest='s,^[^/]*/*,,'; \
sed_last='s,^.*/\([^/]*\)$$,\1,'; \
sed_butlast='s,/*[^/]*$$,,'; \
while test -n "$$dir1"; do \
first=`echo "$$dir1" | sed -e "$$sed_first"`; \
if test "$$first" != "."; then \
if test "$$first" = ".."; then \
dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \
dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \
else \
first2=`echo "$$dir2" | sed -e "$$sed_first"`; \
if test "$$first2" = "$$first"; then \
dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \
else \
dir2="../$$dir2"; \
fi; \
dir0="$$dir0"/"$$first"; \
fi; \
fi; \
dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \
done; \
reldir="$$dir2"
ACLOCAL = @ACLOCAL@
AMTAR = @AMTAR@
AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
ATKBRIDGE_CFLAGS = @ATKBRIDGE_CFLAGS@
ATKBRIDGE_LIBS = @ATKBRIDGE_LIBS@
ATSPI2_CFLAGS = @ATSPI2_CFLAGS@
ATSPI2_LIBS = @ATSPI2_LIBS@
AUTOCONF = @AUTOCONF@
AUTOHEADER = @AUTOHEADER@
AUTOMAKE = @AUTOMAKE@
AWK = @AWK@
CC = @CC@
CCDEPMODE = @CCDEPMODE@
CFLAGS = @CFLAGS@
CPP = @CPP@
CPPFLAGS = @CPPFLAGS@
CSCOPE = @CSCOPE@
CTAGS = @CTAGS@
CYGPATH_W = @CYGPATH_W@
DEFS = @DEFS@
DEPDIR = @DEPDIR@
DESIRED_LINGUAS = @DESIRED_LINGUAS@
ECHO_C = @ECHO_C@
ECHO_N = @ECHO_N@
ECHO_T = @ECHO_T@
ETAGS = @ETAGS@
EXEEXT = @EXEEXT@
GETTEXT_MACRO_VERSION = @GETTEXT_MACRO_VERSION@
GETTEXT_PACKAGE = @GETTEXT_PACKAGE@
GMSGFMT = @GMSGFMT@
GMSGFMT_015 = @GMSGFMT_015@
GSTREAMER_CFLAGS = @GSTREAMER_CFLAGS@
GSTREAMER_LIBS = @GSTREAMER_LIBS@
INSTALL = @INSTALL@
INSTALL_DATA = @INSTALL_DATA@
INSTALL_PROGRAM = @INSTALL_PROGRAM@
INSTALL_SCRIPT = @INSTALL_SCRIPT@
INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
INTLLIBS = @INTLLIBS@
INTL_MACOSX_LIBS = @INTL_MACOSX_LIBS@
LDFLAGS = @LDFLAGS@
LIBICONV = @LIBICONV@
LIBINTL = @LIBINTL@
LIBOBJS = @LIBOBJS@
LIBPEAS_CFLAGS = @LIBPEAS_CFLAGS@
LIBPEAS_LIBS = @LIBPEAS_LIBS@
LIBS = @LIBS@
LOUIS_TABLE_DIR = @LOUIS_TABLE_DIR@
LTLIBICONV = @LTLIBICONV@
LTLIBINTL = @LTLIBINTL@
LTLIBOBJS = @LTLIBOBJS@
MAINT = @MAINT@
MAKEINFO = @MAKEINFO@
MKDIR_P = @MKDIR_P@
MSGFMT = @MSGFMT@
MSGMERGE = @MSGMERGE@
MSGMERGE_FOR_MSGFMT_OPTION = @MSGMERGE_FOR_MSGFMT_OPTION@
OBJEXT = @OBJEXT@
PACKAGE = @PACKAGE@
PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
PACKAGE_NAME = @PACKAGE_NAME@
PACKAGE_STRING = @PACKAGE_STRING@
PACKAGE_TARNAME = @PACKAGE_TARNAME@
PACKAGE_URL = @PACKAGE_URL@
PACKAGE_VERSION = @PACKAGE_VERSION@
PATH_SEPARATOR = @PATH_SEPARATOR@
PKG_CONFIG = @PKG_CONFIG@
PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
PLATFORM_PATH = @PLATFORM_PATH@
POSUB = @POSUB@
PYGOBJECT_CFLAGS = @PYGOBJECT_CFLAGS@
PYGOBJECT_LIBS = @PYGOBJECT_LIBS@
PYTHON = @PYTHON@
PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@
PYTHON_PLATFORM = @PYTHON_PLATFORM@
PYTHON_PREFIX = @PYTHON_PREFIX@
PYTHON_VERSION = @PYTHON_VERSION@
REVISION = @REVISION@
SED = @SED@
SET_MAKE = @SET_MAKE@
SHELL = @SHELL@
STRIP = @STRIP@
USE_NLS = @USE_NLS@
VERSION = @VERSION@
XGETTEXT = @XGETTEXT@
XGETTEXT_015 = @XGETTEXT_015@
XGETTEXT_EXTRA_OPTIONS = @XGETTEXT_EXTRA_OPTIONS@
abs_builddir = @abs_builddir@
abs_srcdir = @abs_srcdir@
abs_top_builddir = @abs_top_builddir@
abs_top_srcdir = @abs_top_srcdir@
ac_ct_CC = @ac_ct_CC@
am__include = @am__include@
am__leading_dot = @am__leading_dot@
am__quote = @am__quote@
am__rm_f_notfound = @am__rm_f_notfound@
am__tar = @am__tar@
am__untar = @am__untar@
am__xargs_n = @am__xargs_n@
bindir = @bindir@
build = @build@
build_alias = @build_alias@
build_cpu = @build_cpu@
build_os = @build_os@
build_vendor = @build_vendor@
builddir = @builddir@
datadir = @datadir@
datarootdir = @datarootdir@
docdir = @docdir@
dvidir = @dvidir@
exec_prefix = @exec_prefix@
host = @host@
host_alias = @host_alias@
host_cpu = @host_cpu@
host_os = @host_os@
host_vendor = @host_vendor@
htmldir = @htmldir@
includedir = @includedir@
infodir = @infodir@
install_sh = @install_sh@
libdir = @libdir@
libexecdir = @libexecdir@
localedir = @localedir@
localedir_c = @localedir_c@
localedir_c_make = @localedir_c_make@
localstatedir = @localstatedir@
mandir = @mandir@
mkdir_p = @mkdir_p@
oldincludedir = @oldincludedir@
pdfdir = @pdfdir@
pkgpyexecdir = @pkgpyexecdir@
pkgpythondir = @pkgpythondir@
prefix = @prefix@
program_transform_name = @program_transform_name@
psdir = @psdir@
pyexecdir = @pyexecdir@
pythondir = @pythondir@
runstatedir = @runstatedir@
sbindir = @sbindir@
sharedstatedir = @sharedstatedir@
srcdir = @srcdir@
sysconfdir = @sysconfdir@
target_alias = @target_alias@
top_build_prefix = @top_build_prefix@
top_builddir = @top_builddir@
top_srcdir = @top_srcdir@
SUBDIRS = man
all: all-recursive
.SUFFIXES:
$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
@for dep in $?; do \
case '$(am__configure_deps)' in \
*$$dep*) \
( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
&& { if test -f $@; then exit 0; else break; fi; }; \
exit 1;; \
esac; \
done; \
echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu docs/Makefile'; \
$(am__cd) $(top_srcdir) && \
$(AUTOMAKE) --gnu docs/Makefile
Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
@case '$?' in \
*config.status*) \
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
*) \
echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
esac;
$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
$(am__aclocal_m4_deps):
# This directory's subdirectories are mostly independent; you can cd
# into them and run 'make' without going through this Makefile.
# To change the values of 'make' variables: instead of editing Makefiles,
# (1) if the variable is set in 'config.status', edit 'config.status'
# (which will cause the Makefiles to be regenerated when you run 'make');
# (2) otherwise, pass the desired values on the 'make' command line.
$(am__recursive_targets):
@fail=; \
if $(am__make_keepgoing); then \
failcom='fail=yes'; \
else \
failcom='exit 1'; \
fi; \
dot_seen=no; \
target=`echo $@ | sed s/-recursive//`; \
case "$@" in \
distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \
*) list='$(SUBDIRS)' ;; \
esac; \
for subdir in $$list; do \
echo "Making $$target in $$subdir"; \
if test "$$subdir" = "."; then \
dot_seen=yes; \
local_target="$$target-am"; \
else \
local_target="$$target"; \
fi; \
($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \
|| eval $$failcom; \
done; \
if test "$$dot_seen" = "no"; then \
$(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \
fi; test -z "$$fail"
ID: $(am__tagged_files)
$(am__define_uniq_tagged_files); mkid -fID $$unique
tags: tags-recursive
TAGS: tags
tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
set x; \
here=`pwd`; \
if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \
include_option=--etags-include; \
empty_fix=.; \
else \
include_option=--include; \
empty_fix=; \
fi; \
list='$(SUBDIRS)'; for subdir in $$list; do \
if test "$$subdir" = .; then :; else \
test ! -f $$subdir/TAGS || \
set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \
fi; \
done; \
$(am__define_uniq_tagged_files); \
shift; \
if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
test -n "$$unique" || unique=$$empty_fix; \
if test $$# -gt 0; then \
$(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
"$$@" $$unique; \
else \
$(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
$$unique; \
fi; \
fi
ctags: ctags-recursive
CTAGS: ctags
ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
$(am__define_uniq_tagged_files); \
test -z "$(CTAGS_ARGS)$$unique" \
|| $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
$$unique
GTAGS:
here=`$(am__cd) $(top_builddir) && pwd` \
&& $(am__cd) $(top_srcdir) \
&& gtags -i $(GTAGS_ARGS) "$$here"
cscopelist: cscopelist-recursive
cscopelist-am: $(am__tagged_files)
list='$(am__tagged_files)'; \
case "$(srcdir)" in \
[\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
*) sdir=$(subdir)/$(srcdir) ;; \
esac; \
for i in $$list; do \
if test -f "$$i"; then \
echo "$(subdir)/$$i"; \
else \
echo "$$sdir/$$i"; \
fi; \
done >> $(top_builddir)/cscope.files
distclean-tags:
-rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
distdir: $(BUILT_SOURCES)
$(MAKE) $(AM_MAKEFLAGS) distdir-am
distdir-am: $(DISTFILES)
@srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
list='$(DISTFILES)'; \
dist_files=`for file in $$list; do echo $$file; done | \
sed -e "s|^$$srcdirstrip/||;t" \
-e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
case $$dist_files in \
*/*) $(MKDIR_P) `echo "$$dist_files" | \
sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
sort -u` ;; \
esac; \
for file in $$dist_files; do \
if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
if test -d $$d/$$file; then \
dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
if test -d "$(distdir)/$$file"; then \
find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
fi; \
if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
fi; \
cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
else \
test -f "$(distdir)/$$file" \
|| cp -p $$d/$$file "$(distdir)/$$file" \
|| exit 1; \
fi; \
done
@list='$(DIST_SUBDIRS)'; for subdir in $$list; do \
if test "$$subdir" = .; then :; else \
$(am__make_dryrun) \
|| test -d "$(distdir)/$$subdir" \
|| $(MKDIR_P) "$(distdir)/$$subdir" \
|| exit 1; \
dir1=$$subdir; dir2="$(distdir)/$$subdir"; \
$(am__relativize); \
new_distdir=$$reldir; \
dir1=$$subdir; dir2="$(top_distdir)"; \
$(am__relativize); \
new_top_distdir=$$reldir; \
echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \
echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \
($(am__cd) $$subdir && \
$(MAKE) $(AM_MAKEFLAGS) \
top_distdir="$$new_top_distdir" \
distdir="$$new_distdir" \
am__remove_distdir=: \
am__skip_length_check=: \
am__skip_mode_fix=: \
distdir) \
|| exit 1; \
fi; \
done
check-am: all-am
check: check-recursive
all-am: Makefile
installdirs: installdirs-recursive
installdirs-am:
install: install-recursive
install-exec: install-exec-recursive
install-data: install-data-recursive
uninstall: uninstall-recursive
install-am: all-am
@$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
installcheck: installcheck-recursive
install-strip:
if test -z '$(STRIP)'; then \
$(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
install; \
else \
$(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
"INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
fi
mostlyclean-generic:
clean-generic:
distclean-generic:
-$(am__rm_f) $(CONFIG_CLEAN_FILES)
-test . = "$(srcdir)" || $(am__rm_f) $(CONFIG_CLEAN_VPATH_FILES)
maintainer-clean-generic:
@echo "This command is intended for maintainers to use"
@echo "it deletes files that may require special tools to rebuild."
clean: clean-recursive
clean-am: clean-generic mostlyclean-am
distclean: distclean-recursive
-rm -f Makefile
distclean-am: clean-am distclean-generic distclean-tags
dvi: dvi-recursive
dvi-am:
html: html-recursive
html-am:
info: info-recursive
info-am:
install-data-am:
install-dvi: install-dvi-recursive
install-dvi-am:
install-exec-am:
install-html: install-html-recursive
install-html-am:
install-info: install-info-recursive
install-info-am:
install-man:
install-pdf: install-pdf-recursive
install-pdf-am:
install-ps: install-ps-recursive
install-ps-am:
installcheck-am:
maintainer-clean: maintainer-clean-recursive
-rm -f Makefile
maintainer-clean-am: distclean-am maintainer-clean-generic
mostlyclean: mostlyclean-recursive
mostlyclean-am: mostlyclean-generic
pdf: pdf-recursive
pdf-am:
ps: ps-recursive
ps-am:
uninstall-am:
.MAKE: $(am__recursive_targets) install-am install-strip
.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am check \
check-am clean clean-generic cscopelist-am ctags ctags-am \
distclean distclean-generic distclean-tags distdir dvi dvi-am \
html html-am info info-am install install-am install-data \
install-data-am install-dvi install-dvi-am install-exec \
install-exec-am install-html install-html-am install-info \
install-info-am install-man install-pdf install-pdf-am \
install-ps install-ps-am install-strip installcheck \
installcheck-am installdirs installdirs-am maintainer-clean \
maintainer-clean-generic mostlyclean mostlyclean-generic pdf \
pdf-am ps ps-am tags tags-am uninstall uninstall-am
.PRECIOUS: Makefile
# Tell versions [3.59,3.63) of GNU make to not export all variables.
# Otherwise a system limit (for SysV at least) may be exceeded.
.NOEXPORT:
# Tell GNU make to disable its built-in pattern rules.
%:: %,v
%:: RCS/%,v
%:: RCS/%
%:: s.%
%:: SCCS/s.%
-554
View File
@@ -1,554 +0,0 @@
# Makefile.in generated by automake 1.18.1 from Makefile.am.
# @configure_input@
# Copyright (C) 1994-2025 Free Software Foundation, Inc.
# This Makefile.in is free software; the Free Software Foundation
# gives unlimited permission to copy and/or distribute it,
# with or without modifications, as long as this notice is preserved.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE.
@SET_MAKE@
VPATH = @srcdir@
am__is_gnu_make = { \
if test -z '$(MAKELEVEL)'; then \
false; \
elif test -n '$(MAKE_HOST)'; then \
true; \
elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
true; \
else \
false; \
fi; \
}
am__make_running_with_option = \
case $${target_option-} in \
?) ;; \
*) echo "am__make_running_with_option: internal error: invalid" \
"target option '$${target_option-}' specified" >&2; \
exit 1;; \
esac; \
has_opt=no; \
sane_makeflags=$$MAKEFLAGS; \
if $(am__is_gnu_make); then \
sane_makeflags=$$MFLAGS; \
else \
case $$MAKEFLAGS in \
*\\[\ \ ]*) \
bs=\\; \
sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
| sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
esac; \
fi; \
skip_next=no; \
strip_trailopt () \
{ \
flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
}; \
for flg in $$sane_makeflags; do \
test $$skip_next = yes && { skip_next=no; continue; }; \
case $$flg in \
*=*|--*) continue;; \
-*I) strip_trailopt 'I'; skip_next=yes;; \
-*I?*) strip_trailopt 'I';; \
-*O) strip_trailopt 'O'; skip_next=yes;; \
-*O?*) strip_trailopt 'O';; \
-*l) strip_trailopt 'l'; skip_next=yes;; \
-*l?*) strip_trailopt 'l';; \
-[dEDm]) skip_next=yes;; \
-[JT]) skip_next=yes;; \
esac; \
case $$flg in \
*$$target_option*) has_opt=yes; break;; \
esac; \
done; \
test $$has_opt = yes
am__make_dryrun = (target_option=n; $(am__make_running_with_option))
am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
am__rm_f = rm -f $(am__rm_f_notfound)
am__rm_rf = rm -rf $(am__rm_f_notfound)
pkgdatadir = $(datadir)/@PACKAGE@
pkgincludedir = $(includedir)/@PACKAGE@
pkglibdir = $(libdir)/@PACKAGE@
pkglibexecdir = $(libexecdir)/@PACKAGE@
am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
install_sh_DATA = $(install_sh) -c -m 644
install_sh_PROGRAM = $(install_sh) -c
install_sh_SCRIPT = $(install_sh) -c
INSTALL_HEADER = $(INSTALL_DATA)
transform = $(program_transform_name)
NORMAL_INSTALL = :
PRE_INSTALL = :
POST_INSTALL = :
NORMAL_UNINSTALL = :
PRE_UNINSTALL = :
POST_UNINSTALL = :
build_triplet = @build@
host_triplet = @host@
subdir = docs/man
ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
am__aclocal_m4_deps = $(top_srcdir)/m4/build-to-host.m4 \
$(top_srcdir)/m4/gettext.m4 $(top_srcdir)/m4/host-cpu-c-abi.m4 \
$(top_srcdir)/m4/iconv.m4 $(top_srcdir)/m4/intlmacosx.m4 \
$(top_srcdir)/m4/lib-ld.m4 $(top_srcdir)/m4/lib-link.m4 \
$(top_srcdir)/m4/lib-prefix.m4 $(top_srcdir)/m4/nls.m4 \
$(top_srcdir)/m4/po.m4 $(top_srcdir)/m4/progtest.m4 \
$(top_srcdir)/acinclude.m4 $(top_srcdir)/configure.ac
am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
$(ACLOCAL_M4)
DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON)
mkinstalldirs = $(install_sh) -d
CONFIG_CLEAN_FILES =
CONFIG_CLEAN_VPATH_FILES =
AM_V_P = $(am__v_P_@AM_V@)
am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
am__v_P_0 = false
am__v_P_1 = :
AM_V_GEN = $(am__v_GEN_@AM_V@)
am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
am__v_GEN_0 = @echo " GEN " $@;
am__v_GEN_1 =
AM_V_at = $(am__v_at_@AM_V@)
am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
am__v_at_0 = @
am__v_at_1 =
SOURCES =
DIST_SOURCES =
am__can_run_installinfo = \
case $$AM_UPDATE_INFO_DIR in \
n|no|NO) false;; \
*) (install-info --version) >/dev/null 2>&1;; \
esac
am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
am__vpath_adj = case $$p in \
$(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
*) f=$$p;; \
esac;
am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
am__install_max = 40
am__nobase_strip_setup = \
srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
am__nobase_strip = \
for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
am__nobase_list = $(am__nobase_strip_setup); \
for p in $$list; do echo "$$p $$p"; done | \
sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
$(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
if (++n[$$2] == $(am__install_max)) \
{ print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
END { for (dir in files) print dir, files[dir] }'
am__base_list = \
sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
am__uninstall_files_from_dir = { \
{ test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
|| { echo " ( cd '$$dir' && rm -f" $$files ")"; \
$(am__cd) "$$dir" && echo $$files | $(am__xargs_n) 40 $(am__rm_f); }; \
}
man1dir = $(mandir)/man1
am__installdirs = "$(DESTDIR)$(man1dir)"
NROFF = nroff
MANS = $(man1_MANS)
am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
am__DIST_COMMON = $(srcdir)/Makefile.in
DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
ACLOCAL = @ACLOCAL@
AMTAR = @AMTAR@
AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
ATKBRIDGE_CFLAGS = @ATKBRIDGE_CFLAGS@
ATKBRIDGE_LIBS = @ATKBRIDGE_LIBS@
ATSPI2_CFLAGS = @ATSPI2_CFLAGS@
ATSPI2_LIBS = @ATSPI2_LIBS@
AUTOCONF = @AUTOCONF@
AUTOHEADER = @AUTOHEADER@
AUTOMAKE = @AUTOMAKE@
AWK = @AWK@
CC = @CC@
CCDEPMODE = @CCDEPMODE@
CFLAGS = @CFLAGS@
CPP = @CPP@
CPPFLAGS = @CPPFLAGS@
CSCOPE = @CSCOPE@
CTAGS = @CTAGS@
CYGPATH_W = @CYGPATH_W@
DEFS = @DEFS@
DEPDIR = @DEPDIR@
DESIRED_LINGUAS = @DESIRED_LINGUAS@
ECHO_C = @ECHO_C@
ECHO_N = @ECHO_N@
ECHO_T = @ECHO_T@
ETAGS = @ETAGS@
EXEEXT = @EXEEXT@
GETTEXT_MACRO_VERSION = @GETTEXT_MACRO_VERSION@
GETTEXT_PACKAGE = @GETTEXT_PACKAGE@
GMSGFMT = @GMSGFMT@
GMSGFMT_015 = @GMSGFMT_015@
GSTREAMER_CFLAGS = @GSTREAMER_CFLAGS@
GSTREAMER_LIBS = @GSTREAMER_LIBS@
INSTALL = @INSTALL@
INSTALL_DATA = @INSTALL_DATA@
INSTALL_PROGRAM = @INSTALL_PROGRAM@
INSTALL_SCRIPT = @INSTALL_SCRIPT@
INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
INTLLIBS = @INTLLIBS@
INTL_MACOSX_LIBS = @INTL_MACOSX_LIBS@
LDFLAGS = @LDFLAGS@
LIBICONV = @LIBICONV@
LIBINTL = @LIBINTL@
LIBOBJS = @LIBOBJS@
LIBPEAS_CFLAGS = @LIBPEAS_CFLAGS@
LIBPEAS_LIBS = @LIBPEAS_LIBS@
LIBS = @LIBS@
LOUIS_TABLE_DIR = @LOUIS_TABLE_DIR@
LTLIBICONV = @LTLIBICONV@
LTLIBINTL = @LTLIBINTL@
LTLIBOBJS = @LTLIBOBJS@
MAINT = @MAINT@
MAKEINFO = @MAKEINFO@
MKDIR_P = @MKDIR_P@
MSGFMT = @MSGFMT@
MSGMERGE = @MSGMERGE@
MSGMERGE_FOR_MSGFMT_OPTION = @MSGMERGE_FOR_MSGFMT_OPTION@
OBJEXT = @OBJEXT@
PACKAGE = @PACKAGE@
PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
PACKAGE_NAME = @PACKAGE_NAME@
PACKAGE_STRING = @PACKAGE_STRING@
PACKAGE_TARNAME = @PACKAGE_TARNAME@
PACKAGE_URL = @PACKAGE_URL@
PACKAGE_VERSION = @PACKAGE_VERSION@
PATH_SEPARATOR = @PATH_SEPARATOR@
PKG_CONFIG = @PKG_CONFIG@
PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
PLATFORM_PATH = @PLATFORM_PATH@
POSUB = @POSUB@
PYGOBJECT_CFLAGS = @PYGOBJECT_CFLAGS@
PYGOBJECT_LIBS = @PYGOBJECT_LIBS@
PYTHON = @PYTHON@
PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@
PYTHON_PLATFORM = @PYTHON_PLATFORM@
PYTHON_PREFIX = @PYTHON_PREFIX@
PYTHON_VERSION = @PYTHON_VERSION@
REVISION = @REVISION@
SED = @SED@
SET_MAKE = @SET_MAKE@
SHELL = @SHELL@
STRIP = @STRIP@
USE_NLS = @USE_NLS@
VERSION = @VERSION@
XGETTEXT = @XGETTEXT@
XGETTEXT_015 = @XGETTEXT_015@
XGETTEXT_EXTRA_OPTIONS = @XGETTEXT_EXTRA_OPTIONS@
abs_builddir = @abs_builddir@
abs_srcdir = @abs_srcdir@
abs_top_builddir = @abs_top_builddir@
abs_top_srcdir = @abs_top_srcdir@
ac_ct_CC = @ac_ct_CC@
am__include = @am__include@
am__leading_dot = @am__leading_dot@
am__quote = @am__quote@
am__rm_f_notfound = @am__rm_f_notfound@
am__tar = @am__tar@
am__untar = @am__untar@
am__xargs_n = @am__xargs_n@
bindir = @bindir@
build = @build@
build_alias = @build_alias@
build_cpu = @build_cpu@
build_os = @build_os@
build_vendor = @build_vendor@
builddir = @builddir@
datadir = @datadir@
datarootdir = @datarootdir@
docdir = @docdir@
dvidir = @dvidir@
exec_prefix = @exec_prefix@
host = @host@
host_alias = @host_alias@
host_cpu = @host_cpu@
host_os = @host_os@
host_vendor = @host_vendor@
htmldir = @htmldir@
includedir = @includedir@
infodir = @infodir@
install_sh = @install_sh@
libdir = @libdir@
libexecdir = @libexecdir@
localedir = @localedir@
localedir_c = @localedir_c@
localedir_c_make = @localedir_c_make@
localstatedir = @localstatedir@
mandir = @mandir@
mkdir_p = @mkdir_p@
oldincludedir = @oldincludedir@
pdfdir = @pdfdir@
pkgpyexecdir = @pkgpyexecdir@
pkgpythondir = @pkgpythondir@
prefix = @prefix@
program_transform_name = @program_transform_name@
psdir = @psdir@
pyexecdir = @pyexecdir@
pythondir = @pythondir@
runstatedir = @runstatedir@
sbindir = @sbindir@
sharedstatedir = @sharedstatedir@
srcdir = @srcdir@
sysconfdir = @sysconfdir@
target_alias = @target_alias@
top_build_prefix = @top_build_prefix@
top_builddir = @top_builddir@
top_srcdir = @top_srcdir@
man1_MANS = cthulhu.1
EXTRA_DIST = \
$(man1_MANS)
all: all-am
.SUFFIXES:
$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
@for dep in $?; do \
case '$(am__configure_deps)' in \
*$$dep*) \
( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
&& { if test -f $@; then exit 0; else break; fi; }; \
exit 1;; \
esac; \
done; \
echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu docs/man/Makefile'; \
$(am__cd) $(top_srcdir) && \
$(AUTOMAKE) --gnu docs/man/Makefile
Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
@case '$?' in \
*config.status*) \
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
*) \
echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
esac;
$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
$(am__aclocal_m4_deps):
install-man1: $(man1_MANS)
@$(NORMAL_INSTALL)
@list1='$(man1_MANS)'; \
list2=''; \
test -n "$(man1dir)" \
&& test -n "`echo $$list1$$list2`" \
|| exit 0; \
echo " $(MKDIR_P) '$(DESTDIR)$(man1dir)'"; \
$(MKDIR_P) "$(DESTDIR)$(man1dir)" || exit 1; \
{ for i in $$list1; do echo "$$i"; done; \
if test -n "$$list2"; then \
for i in $$list2; do echo "$$i"; done \
| sed -n '/\.1[a-z]*$$/p'; \
fi; \
} | while read p; do \
if test -f $$p; then d=; else d="$(srcdir)/"; fi; \
echo "$$d$$p"; echo "$$p"; \
done | \
sed -e 'n;s,.*/,,;p;h;s,.*\.,,;s,^[^1][0-9a-z]*$$,1,;x' \
-e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,' | \
sed 'N;N;s,\n, ,g' | { \
list=; while read file base inst; do \
if test "$$base" = "$$inst"; then list="$$list $$file"; else \
echo " $(INSTALL_DATA) '$$file' '$(DESTDIR)$(man1dir)/$$inst'"; \
$(INSTALL_DATA) "$$file" "$(DESTDIR)$(man1dir)/$$inst" || exit $$?; \
fi; \
done; \
for i in $$list; do echo "$$i"; done | $(am__base_list) | \
while read files; do \
test -z "$$files" || { \
echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(man1dir)'"; \
$(INSTALL_DATA) $$files "$(DESTDIR)$(man1dir)" || exit $$?; }; \
done; }
uninstall-man1:
@$(NORMAL_UNINSTALL)
@list='$(man1_MANS)'; test -n "$(man1dir)" || exit 0; \
files=`{ for i in $$list; do echo "$$i"; done; \
} | sed -e 's,.*/,,;h;s,.*\.,,;s,^[^1][0-9a-z]*$$,1,;x' \
-e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,'`; \
dir='$(DESTDIR)$(man1dir)'; $(am__uninstall_files_from_dir)
tags TAGS:
ctags CTAGS:
cscope cscopelist:
distdir: $(BUILT_SOURCES)
$(MAKE) $(AM_MAKEFLAGS) distdir-am
distdir-am: $(DISTFILES)
@srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
list='$(DISTFILES)'; \
dist_files=`for file in $$list; do echo $$file; done | \
sed -e "s|^$$srcdirstrip/||;t" \
-e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
case $$dist_files in \
*/*) $(MKDIR_P) `echo "$$dist_files" | \
sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
sort -u` ;; \
esac; \
for file in $$dist_files; do \
if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
if test -d $$d/$$file; then \
dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
if test -d "$(distdir)/$$file"; then \
find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
fi; \
if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
fi; \
cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
else \
test -f "$(distdir)/$$file" \
|| cp -p $$d/$$file "$(distdir)/$$file" \
|| exit 1; \
fi; \
done
check-am: all-am
check: check-am
all-am: Makefile $(MANS)
installdirs:
for dir in "$(DESTDIR)$(man1dir)"; do \
test -z "$$dir" || $(MKDIR_P) "$$dir"; \
done
install: install-am
install-exec: install-exec-am
install-data: install-data-am
uninstall: uninstall-am
install-am: all-am
@$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
installcheck: installcheck-am
install-strip:
if test -z '$(STRIP)'; then \
$(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
install; \
else \
$(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
"INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
fi
mostlyclean-generic:
clean-generic:
distclean-generic:
-$(am__rm_f) $(CONFIG_CLEAN_FILES)
-test . = "$(srcdir)" || $(am__rm_f) $(CONFIG_CLEAN_VPATH_FILES)
maintainer-clean-generic:
@echo "This command is intended for maintainers to use"
@echo "it deletes files that may require special tools to rebuild."
clean: clean-am
clean-am: clean-generic mostlyclean-am
distclean: distclean-am
-rm -f Makefile
distclean-am: clean-am distclean-generic
dvi: dvi-am
dvi-am:
html: html-am
html-am:
info: info-am
info-am:
install-data-am: install-man
install-dvi: install-dvi-am
install-dvi-am:
install-exec-am:
install-html: install-html-am
install-html-am:
install-info: install-info-am
install-info-am:
install-man: install-man1
install-pdf: install-pdf-am
install-pdf-am:
install-ps: install-ps-am
install-ps-am:
installcheck-am:
maintainer-clean: maintainer-clean-am
-rm -f Makefile
maintainer-clean-am: distclean-am maintainer-clean-generic
mostlyclean: mostlyclean-am
mostlyclean-am: mostlyclean-generic
pdf: pdf-am
pdf-am:
ps: ps-am
ps-am:
uninstall-am: uninstall-man
uninstall-man: uninstall-man1
.MAKE: install-am install-strip
.PHONY: all all-am check check-am clean clean-generic cscopelist-am \
ctags-am distclean distclean-generic distdir dvi dvi-am html \
html-am info info-am install install-am install-data \
install-data-am install-dvi install-dvi-am install-exec \
install-exec-am install-html install-html-am install-info \
install-info-am install-man install-man1 install-pdf \
install-pdf-am install-ps install-ps-am install-strip \
installcheck installcheck-am installdirs maintainer-clean \
maintainer-clean-generic mostlyclean mostlyclean-generic pdf \
pdf-am ps ps-am tags-am uninstall uninstall-am uninstall-man \
uninstall-man1
.PRECIOUS: Makefile
# Tell versions [3.59,3.63) of GNU make to not export all variables.
# Otherwise a system limit (for SysV at least) may be exceeded.
.NOEXPORT:
# Tell GNU make to disable its built-in pattern rules.
%:: %,v
%:: RCS/%,v
%:: RCS/%
%:: s.%
%:: SCCS/s.%
+5 -2
View File
@@ -1,5 +1,5 @@
project('cthulhu',
version: '2025.08.11',
version: '2025.12.09',
meson_version: '>= 1.0.0',
)
@@ -13,7 +13,7 @@ if not python3.language_version().version_compare(f'>= @python_minimum_version@'
endif
# Hard dependencies (checked via pkg-config)
dependency('atspi-2', version: '>= 2.48.0')
dependency('atspi-2', version: '>= 2.52.0')
dependency('atk-bridge-2.0', version: '>= 2.26.0')
dependency('pygobject-3.0', version: '>= 3.18')
@@ -54,6 +54,9 @@ optional_modules = {
'dasbus': 'D-Bus remote controller',
'psutil': 'system information commands',
'gi.repository.Wnck': 'mouse review',
'pdf2image': 'PDF processing for OCR',
'scipy': 'Scientific computing for OCR analysis',
'webcolors': 'Color name resolution for OCR',
}
summary = {}
-652
View File
@@ -1,652 +0,0 @@
# Makefile.in generated by automake 1.18.1 from Makefile.am.
# @configure_input@
# Copyright (C) 1994-2025 Free Software Foundation, Inc.
# This Makefile.in is free software; the Free Software Foundation
# gives unlimited permission to copy and/or distribute it,
# with or without modifications, as long as this notice is preserved.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE.
@SET_MAKE@
VPATH = @srcdir@
am__is_gnu_make = { \
if test -z '$(MAKELEVEL)'; then \
false; \
elif test -n '$(MAKE_HOST)'; then \
true; \
elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
true; \
else \
false; \
fi; \
}
am__make_running_with_option = \
case $${target_option-} in \
?) ;; \
*) echo "am__make_running_with_option: internal error: invalid" \
"target option '$${target_option-}' specified" >&2; \
exit 1;; \
esac; \
has_opt=no; \
sane_makeflags=$$MAKEFLAGS; \
if $(am__is_gnu_make); then \
sane_makeflags=$$MFLAGS; \
else \
case $$MAKEFLAGS in \
*\\[\ \ ]*) \
bs=\\; \
sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
| sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
esac; \
fi; \
skip_next=no; \
strip_trailopt () \
{ \
flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
}; \
for flg in $$sane_makeflags; do \
test $$skip_next = yes && { skip_next=no; continue; }; \
case $$flg in \
*=*|--*) continue;; \
-*I) strip_trailopt 'I'; skip_next=yes;; \
-*I?*) strip_trailopt 'I';; \
-*O) strip_trailopt 'O'; skip_next=yes;; \
-*O?*) strip_trailopt 'O';; \
-*l) strip_trailopt 'l'; skip_next=yes;; \
-*l?*) strip_trailopt 'l';; \
-[dEDm]) skip_next=yes;; \
-[JT]) skip_next=yes;; \
esac; \
case $$flg in \
*$$target_option*) has_opt=yes; break;; \
esac; \
done; \
test $$has_opt = yes
am__make_dryrun = (target_option=n; $(am__make_running_with_option))
am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
am__rm_f = rm -f $(am__rm_f_notfound)
am__rm_rf = rm -rf $(am__rm_f_notfound)
pkgdatadir = $(datadir)/@PACKAGE@
pkgincludedir = $(includedir)/@PACKAGE@
pkglibdir = $(libdir)/@PACKAGE@
pkglibexecdir = $(libexecdir)/@PACKAGE@
am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
install_sh_DATA = $(install_sh) -c -m 644
install_sh_PROGRAM = $(install_sh) -c
install_sh_SCRIPT = $(install_sh) -c
INSTALL_HEADER = $(INSTALL_DATA)
transform = $(program_transform_name)
NORMAL_INSTALL = :
PRE_INSTALL = :
POST_INSTALL = :
NORMAL_UNINSTALL = :
PRE_UNINSTALL = :
POST_UNINSTALL = :
build_triplet = @build@
host_triplet = @host@
subdir = src
ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
am__aclocal_m4_deps = $(top_srcdir)/m4/build-to-host.m4 \
$(top_srcdir)/m4/gettext.m4 $(top_srcdir)/m4/host-cpu-c-abi.m4 \
$(top_srcdir)/m4/iconv.m4 $(top_srcdir)/m4/intlmacosx.m4 \
$(top_srcdir)/m4/lib-ld.m4 $(top_srcdir)/m4/lib-link.m4 \
$(top_srcdir)/m4/lib-prefix.m4 $(top_srcdir)/m4/nls.m4 \
$(top_srcdir)/m4/po.m4 $(top_srcdir)/m4/progtest.m4 \
$(top_srcdir)/acinclude.m4 $(top_srcdir)/configure.ac
am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
$(ACLOCAL_M4)
DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON)
mkinstalldirs = $(install_sh) -d
CONFIG_CLEAN_FILES =
CONFIG_CLEAN_VPATH_FILES =
AM_V_P = $(am__v_P_@AM_V@)
am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
am__v_P_0 = false
am__v_P_1 = :
AM_V_GEN = $(am__v_GEN_@AM_V@)
am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
am__v_GEN_0 = @echo " GEN " $@;
am__v_GEN_1 =
AM_V_at = $(am__v_at_@AM_V@)
am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
am__v_at_0 = @
am__v_at_1 =
SOURCES =
DIST_SOURCES =
RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \
ctags-recursive dvi-recursive html-recursive info-recursive \
install-data-recursive install-dvi-recursive \
install-exec-recursive install-html-recursive \
install-info-recursive install-pdf-recursive \
install-ps-recursive install-recursive installcheck-recursive \
installdirs-recursive pdf-recursive ps-recursive \
tags-recursive uninstall-recursive
am__can_run_installinfo = \
case $$AM_UPDATE_INFO_DIR in \
n|no|NO) false;; \
*) (install-info --version) >/dev/null 2>&1;; \
esac
RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \
distclean-recursive maintainer-clean-recursive
am__recursive_targets = \
$(RECURSIVE_TARGETS) \
$(RECURSIVE_CLEAN_TARGETS) \
$(am__extra_recursive_targets)
AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \
distdir distdir-am
am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
# Read a list of newline-separated strings from the standard input,
# and print each of them once, without duplicates. Input order is
# *not* preserved.
am__uniquify_input = $(AWK) '\
BEGIN { nonempty = 0; } \
{ items[$$0] = 1; nonempty = 1; } \
END { if (nonempty) { for (i in items) print i; }; } \
'
# Make sure the list of sources is unique. This is necessary because,
# e.g., the same source file might be shared among _SOURCES variables
# for different programs/libraries.
am__define_uniq_tagged_files = \
list='$(am__tagged_files)'; \
unique=`for i in $$list; do \
if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
done | $(am__uniquify_input)`
DIST_SUBDIRS = $(SUBDIRS)
am__DIST_COMMON = $(srcdir)/Makefile.in
DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
am__relativize = \
dir0=`pwd`; \
sed_first='s,^\([^/]*\)/.*$$,\1,'; \
sed_rest='s,^[^/]*/*,,'; \
sed_last='s,^.*/\([^/]*\)$$,\1,'; \
sed_butlast='s,/*[^/]*$$,,'; \
while test -n "$$dir1"; do \
first=`echo "$$dir1" | sed -e "$$sed_first"`; \
if test "$$first" != "."; then \
if test "$$first" = ".."; then \
dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \
dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \
else \
first2=`echo "$$dir2" | sed -e "$$sed_first"`; \
if test "$$first2" = "$$first"; then \
dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \
else \
dir2="../$$dir2"; \
fi; \
dir0="$$dir0"/"$$first"; \
fi; \
fi; \
dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \
done; \
reldir="$$dir2"
ACLOCAL = @ACLOCAL@
AMTAR = @AMTAR@
AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
ATKBRIDGE_CFLAGS = @ATKBRIDGE_CFLAGS@
ATKBRIDGE_LIBS = @ATKBRIDGE_LIBS@
ATSPI2_CFLAGS = @ATSPI2_CFLAGS@
ATSPI2_LIBS = @ATSPI2_LIBS@
AUTOCONF = @AUTOCONF@
AUTOHEADER = @AUTOHEADER@
AUTOMAKE = @AUTOMAKE@
AWK = @AWK@
CC = @CC@
CCDEPMODE = @CCDEPMODE@
CFLAGS = @CFLAGS@
CPP = @CPP@
CPPFLAGS = @CPPFLAGS@
CSCOPE = @CSCOPE@
CTAGS = @CTAGS@
CYGPATH_W = @CYGPATH_W@
DEFS = @DEFS@
DEPDIR = @DEPDIR@
DESIRED_LINGUAS = @DESIRED_LINGUAS@
ECHO_C = @ECHO_C@
ECHO_N = @ECHO_N@
ECHO_T = @ECHO_T@
ETAGS = @ETAGS@
EXEEXT = @EXEEXT@
GETTEXT_MACRO_VERSION = @GETTEXT_MACRO_VERSION@
GETTEXT_PACKAGE = @GETTEXT_PACKAGE@
GMSGFMT = @GMSGFMT@
GMSGFMT_015 = @GMSGFMT_015@
GSTREAMER_CFLAGS = @GSTREAMER_CFLAGS@
GSTREAMER_LIBS = @GSTREAMER_LIBS@
INSTALL = @INSTALL@
INSTALL_DATA = @INSTALL_DATA@
INSTALL_PROGRAM = @INSTALL_PROGRAM@
INSTALL_SCRIPT = @INSTALL_SCRIPT@
INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
INTLLIBS = @INTLLIBS@
INTL_MACOSX_LIBS = @INTL_MACOSX_LIBS@
LDFLAGS = @LDFLAGS@
LIBICONV = @LIBICONV@
LIBINTL = @LIBINTL@
LIBOBJS = @LIBOBJS@
LIBPEAS_CFLAGS = @LIBPEAS_CFLAGS@
LIBPEAS_LIBS = @LIBPEAS_LIBS@
LIBS = @LIBS@
LOUIS_TABLE_DIR = @LOUIS_TABLE_DIR@
LTLIBICONV = @LTLIBICONV@
LTLIBINTL = @LTLIBINTL@
LTLIBOBJS = @LTLIBOBJS@
MAINT = @MAINT@
MAKEINFO = @MAKEINFO@
MKDIR_P = @MKDIR_P@
MSGFMT = @MSGFMT@
MSGMERGE = @MSGMERGE@
MSGMERGE_FOR_MSGFMT_OPTION = @MSGMERGE_FOR_MSGFMT_OPTION@
OBJEXT = @OBJEXT@
PACKAGE = @PACKAGE@
PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
PACKAGE_NAME = @PACKAGE_NAME@
PACKAGE_STRING = @PACKAGE_STRING@
PACKAGE_TARNAME = @PACKAGE_TARNAME@
PACKAGE_URL = @PACKAGE_URL@
PACKAGE_VERSION = @PACKAGE_VERSION@
PATH_SEPARATOR = @PATH_SEPARATOR@
PKG_CONFIG = @PKG_CONFIG@
PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
PLATFORM_PATH = @PLATFORM_PATH@
POSUB = @POSUB@
PYGOBJECT_CFLAGS = @PYGOBJECT_CFLAGS@
PYGOBJECT_LIBS = @PYGOBJECT_LIBS@
PYTHON = @PYTHON@
PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@
PYTHON_PLATFORM = @PYTHON_PLATFORM@
PYTHON_PREFIX = @PYTHON_PREFIX@
PYTHON_VERSION = @PYTHON_VERSION@
REVISION = @REVISION@
SED = @SED@
SET_MAKE = @SET_MAKE@
SHELL = @SHELL@
STRIP = @STRIP@
USE_NLS = @USE_NLS@
VERSION = @VERSION@
XGETTEXT = @XGETTEXT@
XGETTEXT_015 = @XGETTEXT_015@
XGETTEXT_EXTRA_OPTIONS = @XGETTEXT_EXTRA_OPTIONS@
abs_builddir = @abs_builddir@
abs_srcdir = @abs_srcdir@
abs_top_builddir = @abs_top_builddir@
abs_top_srcdir = @abs_top_srcdir@
ac_ct_CC = @ac_ct_CC@
am__include = @am__include@
am__leading_dot = @am__leading_dot@
am__quote = @am__quote@
am__rm_f_notfound = @am__rm_f_notfound@
am__tar = @am__tar@
am__untar = @am__untar@
am__xargs_n = @am__xargs_n@
bindir = @bindir@
build = @build@
build_alias = @build_alias@
build_cpu = @build_cpu@
build_os = @build_os@
build_vendor = @build_vendor@
builddir = @builddir@
datadir = @datadir@
datarootdir = @datarootdir@
docdir = @docdir@
dvidir = @dvidir@
exec_prefix = @exec_prefix@
host = @host@
host_alias = @host_alias@
host_cpu = @host_cpu@
host_os = @host_os@
host_vendor = @host_vendor@
htmldir = @htmldir@
includedir = @includedir@
infodir = @infodir@
install_sh = @install_sh@
libdir = @libdir@
libexecdir = @libexecdir@
localedir = @localedir@
localedir_c = @localedir_c@
localedir_c_make = @localedir_c_make@
localstatedir = @localstatedir@
mandir = @mandir@
mkdir_p = @mkdir_p@
oldincludedir = @oldincludedir@
pdfdir = @pdfdir@
pkgpyexecdir = @pkgpyexecdir@
pkgpythondir = @pkgpythondir@
prefix = @prefix@
program_transform_name = @program_transform_name@
psdir = @psdir@
pyexecdir = @pyexecdir@
pythondir = @pythondir@
runstatedir = @runstatedir@
sbindir = @sbindir@
sharedstatedir = @sharedstatedir@
srcdir = @srcdir@
sysconfdir = @sysconfdir@
target_alias = @target_alias@
top_build_prefix = @top_build_prefix@
top_builddir = @top_builddir@
top_srcdir = @top_srcdir@
SUBDIRS = cthulhu
all: all-recursive
.SUFFIXES:
$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
@for dep in $?; do \
case '$(am__configure_deps)' in \
*$$dep*) \
( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
&& { if test -f $@; then exit 0; else break; fi; }; \
exit 1;; \
esac; \
done; \
echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu src/Makefile'; \
$(am__cd) $(top_srcdir) && \
$(AUTOMAKE) --gnu src/Makefile
Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
@case '$?' in \
*config.status*) \
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
*) \
echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
esac;
$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
$(am__aclocal_m4_deps):
# This directory's subdirectories are mostly independent; you can cd
# into them and run 'make' without going through this Makefile.
# To change the values of 'make' variables: instead of editing Makefiles,
# (1) if the variable is set in 'config.status', edit 'config.status'
# (which will cause the Makefiles to be regenerated when you run 'make');
# (2) otherwise, pass the desired values on the 'make' command line.
$(am__recursive_targets):
@fail=; \
if $(am__make_keepgoing); then \
failcom='fail=yes'; \
else \
failcom='exit 1'; \
fi; \
dot_seen=no; \
target=`echo $@ | sed s/-recursive//`; \
case "$@" in \
distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \
*) list='$(SUBDIRS)' ;; \
esac; \
for subdir in $$list; do \
echo "Making $$target in $$subdir"; \
if test "$$subdir" = "."; then \
dot_seen=yes; \
local_target="$$target-am"; \
else \
local_target="$$target"; \
fi; \
($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \
|| eval $$failcom; \
done; \
if test "$$dot_seen" = "no"; then \
$(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \
fi; test -z "$$fail"
ID: $(am__tagged_files)
$(am__define_uniq_tagged_files); mkid -fID $$unique
tags: tags-recursive
TAGS: tags
tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
set x; \
here=`pwd`; \
if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \
include_option=--etags-include; \
empty_fix=.; \
else \
include_option=--include; \
empty_fix=; \
fi; \
list='$(SUBDIRS)'; for subdir in $$list; do \
if test "$$subdir" = .; then :; else \
test ! -f $$subdir/TAGS || \
set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \
fi; \
done; \
$(am__define_uniq_tagged_files); \
shift; \
if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
test -n "$$unique" || unique=$$empty_fix; \
if test $$# -gt 0; then \
$(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
"$$@" $$unique; \
else \
$(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
$$unique; \
fi; \
fi
ctags: ctags-recursive
CTAGS: ctags
ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
$(am__define_uniq_tagged_files); \
test -z "$(CTAGS_ARGS)$$unique" \
|| $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
$$unique
GTAGS:
here=`$(am__cd) $(top_builddir) && pwd` \
&& $(am__cd) $(top_srcdir) \
&& gtags -i $(GTAGS_ARGS) "$$here"
cscopelist: cscopelist-recursive
cscopelist-am: $(am__tagged_files)
list='$(am__tagged_files)'; \
case "$(srcdir)" in \
[\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
*) sdir=$(subdir)/$(srcdir) ;; \
esac; \
for i in $$list; do \
if test -f "$$i"; then \
echo "$(subdir)/$$i"; \
else \
echo "$$sdir/$$i"; \
fi; \
done >> $(top_builddir)/cscope.files
distclean-tags:
-rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
distdir: $(BUILT_SOURCES)
$(MAKE) $(AM_MAKEFLAGS) distdir-am
distdir-am: $(DISTFILES)
@srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
list='$(DISTFILES)'; \
dist_files=`for file in $$list; do echo $$file; done | \
sed -e "s|^$$srcdirstrip/||;t" \
-e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
case $$dist_files in \
*/*) $(MKDIR_P) `echo "$$dist_files" | \
sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
sort -u` ;; \
esac; \
for file in $$dist_files; do \
if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
if test -d $$d/$$file; then \
dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
if test -d "$(distdir)/$$file"; then \
find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
fi; \
if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
fi; \
cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
else \
test -f "$(distdir)/$$file" \
|| cp -p $$d/$$file "$(distdir)/$$file" \
|| exit 1; \
fi; \
done
@list='$(DIST_SUBDIRS)'; for subdir in $$list; do \
if test "$$subdir" = .; then :; else \
$(am__make_dryrun) \
|| test -d "$(distdir)/$$subdir" \
|| $(MKDIR_P) "$(distdir)/$$subdir" \
|| exit 1; \
dir1=$$subdir; dir2="$(distdir)/$$subdir"; \
$(am__relativize); \
new_distdir=$$reldir; \
dir1=$$subdir; dir2="$(top_distdir)"; \
$(am__relativize); \
new_top_distdir=$$reldir; \
echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \
echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \
($(am__cd) $$subdir && \
$(MAKE) $(AM_MAKEFLAGS) \
top_distdir="$$new_top_distdir" \
distdir="$$new_distdir" \
am__remove_distdir=: \
am__skip_length_check=: \
am__skip_mode_fix=: \
distdir) \
|| exit 1; \
fi; \
done
check-am: all-am
check: check-recursive
all-am: Makefile
installdirs: installdirs-recursive
installdirs-am:
install: install-recursive
install-exec: install-exec-recursive
install-data: install-data-recursive
uninstall: uninstall-recursive
install-am: all-am
@$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
installcheck: installcheck-recursive
install-strip:
if test -z '$(STRIP)'; then \
$(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
install; \
else \
$(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
"INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
fi
mostlyclean-generic:
clean-generic:
distclean-generic:
-$(am__rm_f) $(CONFIG_CLEAN_FILES)
-test . = "$(srcdir)" || $(am__rm_f) $(CONFIG_CLEAN_VPATH_FILES)
maintainer-clean-generic:
@echo "This command is intended for maintainers to use"
@echo "it deletes files that may require special tools to rebuild."
clean: clean-recursive
clean-am: clean-generic mostlyclean-am
distclean: distclean-recursive
-rm -f Makefile
distclean-am: clean-am distclean-generic distclean-tags
dvi: dvi-recursive
dvi-am:
html: html-recursive
html-am:
info: info-recursive
info-am:
install-data-am:
install-dvi: install-dvi-recursive
install-dvi-am:
install-exec-am:
install-html: install-html-recursive
install-html-am:
install-info: install-info-recursive
install-info-am:
install-man:
install-pdf: install-pdf-recursive
install-pdf-am:
install-ps: install-ps-recursive
install-ps-am:
installcheck-am:
maintainer-clean: maintainer-clean-recursive
-rm -f Makefile
maintainer-clean-am: distclean-am maintainer-clean-generic
mostlyclean: mostlyclean-recursive
mostlyclean-am: mostlyclean-generic
pdf: pdf-recursive
pdf-am:
ps: ps-recursive
ps-am:
uninstall-am:
.MAKE: $(am__recursive_targets) install-am install-strip
.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am check \
check-am clean clean-generic cscopelist-am ctags ctags-am \
distclean distclean-generic distclean-tags distdir dvi dvi-am \
html html-am info info-am install install-am install-data \
install-data-am install-dvi install-dvi-am install-exec \
install-exec-am install-html install-html-am install-info \
install-info-am install-man install-pdf install-pdf-am \
install-ps install-ps-am install-strip installcheck \
installcheck-am installdirs installdirs-am maintainer-clean \
maintainer-clean-generic mostlyclean mostlyclean-generic pdf \
pdf-am ps ps-am tags tags-am uninstall uninstall-am
.PRECIOUS: Makefile
# Tell versions [3.59,3.63) of GNU make to not export all variables.
# Otherwise a system limit (for SysV at least) may be exceeded.
.NOEXPORT:
# Tell GNU make to disable its built-in pattern rules.
%:: %,v
%:: RCS/%,v
%:: RCS/%
%:: s.%
%:: SCCS/s.%
+200
View File
@@ -7,6 +7,20 @@
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkAdjustment" id="ocrScaleAdjustment">
<property name="lower">1</property>
<property name="upper">10</property>
<property name="value">3</property>
<property name="step_increment">1</property>
<property name="page_increment">1</property>
</object>
<object class="GtkAdjustment" id="ocrBlackWhiteValueAdjustment">
<property name="lower">0</property>
<property name="upper">255</property>
<property name="value">200</property>
<property name="step_increment">10</property>
<property name="page_increment">50</property>
</object>
<object class="GtkListStore" id="liststore1">
<columns>
<!-- column-name gchararray1 -->
@@ -3636,6 +3650,181 @@
<property name="position">8</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="ocrGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">12</property>
<property name="margin_right">12</property>
<property name="margin_top">12</property>
<property name="margin_bottom">12</property>
<property name="row_spacing">6</property>
<property name="column_spacing">12</property>
<child>
<object class="GtkLabel" id="ocrLanguageLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Language Code:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">ocrLanguageEntry</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="ocrLanguageEntry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text" translatable="yes">eng</property>
<signal name="changed" handler="ocrLanguageChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ocrScaleLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Scale Factor:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">ocrScaleSpinButton</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="ocrScaleSpinButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">ocrScaleAdjustment</property>
<property name="value">3</property>
<signal name="value-changed" handler="ocrScaleChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="ocrGrayscaleCheckButton">
<property name="label" translatable="yes">_Grayscale Image</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="ocrGrayscaleToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="ocrInvertCheckButton">
<property name="label" translatable="yes">_Invert Image</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="ocrInvertToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="ocrBlackWhiteCheckButton">
<property name="label" translatable="yes">_Black and White Image</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="ocrBlackWhiteToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ocrBlackWhiteValueLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Black/White _Threshold:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">ocrBlackWhiteValueSpinButton</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="ocrBlackWhiteValueSpinButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">ocrBlackWhiteValueAdjustment</property>
<property name="value">200</property>
<signal name="value-changed" handler="ocrBlackWhiteValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="ocrColorCalculationCheckButton">
<property name="label" translatable="yes">_Analyze Colors</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="ocrColorCalculationToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">6</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="ocrCopyToClipboardCheckButton">
<property name="label" translatable="yes">Copy Results to _Clipboard</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="ocrCopyToClipboardToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">7</property>
<property name="width">2</property>
</packing>
</child>
</object>
<packing>
<property name="position">9</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="aiTabLabel">
<property name="visible">True</property>
@@ -3647,6 +3836,17 @@
<property name="tab_fill">False</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="ocrTabLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">OCR</property>
</object>
<packing>
<property name="position">9</property>
<property name="tab_fill">False</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
+14
View File
@@ -858,7 +858,21 @@ 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:
# Start the D-Bus service
dbus_service.get_remote_controller().start()
# Register speech and verbosity manager
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Registering SpeechManager D-Bus module', True)
from . import speech_and_verbosity_manager
speech_manager = speech_and_verbosity_manager.getManager()
dbus_service.get_remote_controller().register_decorated_module("SpeechManager", speech_manager)
# Register typing echo presenter
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Registering TypingEchoManager D-Bus module', True)
from . import typing_echo_presenter
typing_echo_manager = typing_echo_presenter.getManager()
dbus_service.get_remote_controller().register_decorated_module("TypingEchoManager", typing_echo_manager)
except Exception as e:
msg = f"CTHULHU: Failed to start D-Bus service: {e}"
debug.printMessage(debug.LEVEL_SEVERE, msg, True)
+1 -1
View File
@@ -23,5 +23,5 @@
# Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca
version = "2025.08.22"
version = "2025.12.12"
codeName = "master"
+78
View File
@@ -1821,6 +1821,10 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
# AI Assistant settings
#
self._initAIState()
# OCR Plugin settings
#
self._initOCRState()
def __initProfileCombo(self):
"""Adding available profiles and setting active as the active one"""
@@ -1945,6 +1949,47 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses local Ollama")
else:
self.aiApiKeyEntry.set_placeholder_text("Path to API key file")
def _initOCRState(self):
"""Initialize OCR Plugin tab widgets with current settings."""
prefs = self.prefsDict
# Store widget references
self.ocrLanguageEntry = self.get_widget("ocrLanguageEntry")
self.ocrScaleSpinButton = self.get_widget("ocrScaleSpinButton")
self.ocrGrayscaleCheckButton = self.get_widget("ocrGrayscaleCheckButton")
self.ocrInvertCheckButton = self.get_widget("ocrInvertCheckButton")
self.ocrBlackWhiteCheckButton = self.get_widget("ocrBlackWhiteCheckButton")
self.ocrBlackWhiteValueSpinButton = self.get_widget("ocrBlackWhiteValueSpinButton")
self.ocrColorCalculationCheckButton = self.get_widget("ocrColorCalculationCheckButton")
self.ocrCopyToClipboardCheckButton = self.get_widget("ocrCopyToClipboardCheckButton")
# Set language code
languageCode = prefs.get("ocrLanguageCode", settings.ocrLanguageCode)
self.ocrLanguageEntry.set_text(languageCode)
# Set scale factor
scaleFactor = prefs.get("ocrScaleFactor", settings.ocrScaleFactor)
self.ocrScaleSpinButton.set_value(scaleFactor)
# Set checkboxes
grayscale = prefs.get("ocrGrayscaleImg", settings.ocrGrayscaleImg)
self.ocrGrayscaleCheckButton.set_active(grayscale)
invert = prefs.get("ocrInvertImg", settings.ocrInvertImg)
self.ocrInvertCheckButton.set_active(invert)
blackWhite = prefs.get("ocrBlackWhiteImg", settings.ocrBlackWhiteImg)
self.ocrBlackWhiteCheckButton.set_active(blackWhite)
blackWhiteValue = prefs.get("ocrBlackWhiteImgValue", settings.ocrBlackWhiteImgValue)
self.ocrBlackWhiteValueSpinButton.set_value(blackWhiteValue)
colorCalculation = prefs.get("ocrColorCalculation", settings.ocrColorCalculation)
self.ocrColorCalculationCheckButton.set_active(colorCalculation)
copyToClipboard = prefs.get("ocrCopyToClipboard", settings.ocrCopyToClipboard)
self.ocrCopyToClipboardCheckButton.set_active(copyToClipboard)
def _updateCthulhuModifier(self):
combobox = self.get_widget("cthulhuModifierComboBox")
@@ -3835,4 +3880,37 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
if 0 <= activeIndex < len(qualities):
self.prefsDict["aiScreenshotQuality"] = qualities[activeIndex]
# OCR Plugin Settings Handlers
def ocrLanguageChanged(self, widget):
"""OCR language code entry changed handler"""
self.prefsDict["ocrLanguageCode"] = widget.get_text()
def ocrScaleChanged(self, widget):
"""OCR scale factor spin button changed handler"""
self.prefsDict["ocrScaleFactor"] = int(widget.get_value())
def ocrGrayscaleToggled(self, widget):
"""OCR grayscale image checkbox toggled handler"""
self.prefsDict["ocrGrayscaleImg"] = widget.get_active()
def ocrInvertToggled(self, widget):
"""OCR invert image checkbox toggled handler"""
self.prefsDict["ocrInvertImg"] = widget.get_active()
def ocrBlackWhiteToggled(self, widget):
"""OCR black and white image checkbox toggled handler"""
self.prefsDict["ocrBlackWhiteImg"] = widget.get_active()
def ocrBlackWhiteValueChanged(self, widget):
"""OCR black/white threshold spin button changed handler"""
self.prefsDict["ocrBlackWhiteImgValue"] = int(widget.get_value())
def ocrColorCalculationToggled(self, widget):
"""OCR color calculation checkbox toggled handler"""
self.prefsDict["ocrColorCalculation"] = widget.get_active()
def ocrCopyToClipboardToggled(self, widget):
"""OCR copy to clipboard checkbox toggled handler"""
self.prefsDict["ocrCopyToClipboard"] = widget.get_active()
+1
View File
@@ -85,6 +85,7 @@ cthulhu_python_sources = files([
'translation_context.py',
'translation_manager.py',
'tutorialgenerator.py',
'typing_echo_presenter.py',
'where_am_i_presenter.py',
])
+23 -65
View File
@@ -46,9 +46,7 @@ class AIAssistant(Plugin):
def __init__(self, *args, **kwargs):
"""Initialize the AI Assistant plugin."""
super().__init__(*args, **kwargs)
# Use print to ensure we see this message
print("DEBUG: AI ASSISTANT __init__ called")
logger.info("AI ASSISTANT: Plugin __init__ starting")
logger.info("AI ASSISTANT: Plugin initialized successfully")
@@ -79,51 +77,40 @@ class AIAssistant(Plugin):
# Prevent multiple activations
if self._enabled:
logger.info("AI ASSISTANT: Already activated, skipping")
print("DEBUG: AI ASSISTANT already activated, skipping")
return
try:
logger.info("AI ASSISTANT: === Plugin activation starting ===")
print("DEBUG: AI ASSISTANT activation starting")
# Check if AI Assistant is enabled in settings
enabled = self._settings_manager.getSetting('aiAssistantEnabled')
logger.info(f"AI ASSISTANT: Enabled setting: {enabled}")
print(f"DEBUG: AI ASSISTANT enabled setting: {enabled}")
if not enabled:
logger.info("AI Assistant is disabled in settings, skipping activation")
print("DEBUG: AI Assistant disabled, skipping activation")
return
# Load AI settings
self._load_ai_settings()
print(f"DEBUG: AI settings loaded - provider: {self._provider_type}")
# Check if we have valid configuration
config_valid = self._validate_configuration()
logger.info(f"AI Assistant configuration valid: {config_valid}")
print(f"DEBUG: AI Assistant configuration valid: {config_valid}")
# Initialize AI provider (may fail but we still want menu access)
if config_valid:
provider_init = self._initialize_ai_provider()
print(f"DEBUG: AI provider initialization: {provider_init}")
else:
logger.warning("AI Assistant configuration invalid, menu will show error messages")
print("DEBUG: AI Assistant configuration invalid, menu will show error messages")
provider_init = False
# Always register keybindings so menu is accessible even with config issues
self._register_keybindings()
print("DEBUG: AI keybindings registered")
self._enabled = True
logger.info("AI Assistant plugin activated successfully")
print("DEBUG: AI Assistant plugin activated successfully")
except Exception as e:
logger.error(f"Error activating AI Assistant plugin: {e}")
print(f"DEBUG: Error activating AI Assistant plugin: {e}")
import traceback
logger.error(traceback.format_exc())
@@ -144,44 +131,35 @@ class AIAssistant(Plugin):
"""Refresh plugin settings and reinitialize provider. Called when settings change."""
try:
logger.info("AI Assistant: Refreshing settings")
print("DEBUG: AI Assistant refreshing settings")
# Reload settings
self._load_ai_settings()
# Validate new configuration
config_valid = self._validate_configuration()
print(f"DEBUG: New configuration valid: {config_valid}")
# Reinitialize provider if configuration is valid
if config_valid:
old_provider = self._ai_provider
provider_init = self._initialize_ai_provider()
print(f"DEBUG: Provider reinitialization: {provider_init}")
if provider_init:
logger.info(f"AI Assistant provider changed to: {self._provider_type}")
print(f"DEBUG: Provider successfully changed to: {self._provider_type}")
else:
logger.warning("Failed to initialize new provider")
print("DEBUG: Failed to initialize new provider")
self._ai_provider = None
else:
logger.warning("New configuration invalid, clearing provider")
print("DEBUG: New configuration invalid, clearing provider")
self._ai_provider = None
except Exception as e:
logger.error(f"Error refreshing AI Assistant settings: {e}")
print(f"DEBUG: Error refreshing settings: {e}")
def _load_ai_settings(self):
"""Load AI Assistant settings from Cthulhu configuration."""
try:
# Get provider
provider = self._settings_manager.getSetting('aiProvider')
print(f"DEBUG: Raw provider setting: '{provider}'")
self._provider_type = provider or settings.AI_PROVIDER_CLAUDE_CODE
print(f"DEBUG: Final provider type: '{self._provider_type}'")
# Load API key from file
api_key_file = self._settings_manager.getSetting('aiApiKeyFile')
@@ -308,10 +286,9 @@ class AIAssistant(Plugin):
"Show AI Assistant menu",
'kb:cthulhu+shift+control+a'
)
logger.info("AI Assistant menu keybinding registered")
print(f"DEBUG: AI Assistant menu keybinding registered: {self._kb_binding_menu}")
except Exception as e:
logger.error(f"Error registering AI menu keybinding: {e}")
@@ -325,22 +302,18 @@ class AIAssistant(Plugin):
"""Show the AI Assistant menu."""
try:
logger.info("AI ASSISTANT: _show_ai_menu called!")
print("DEBUG: AI ASSISTANT _show_ai_menu called!")
# IMPORTANT: Capture screen data BEFORE showing menu
# This ensures we get the actual screen content, not the menu itself
self._pre_menu_screen_data = self._collect_ai_data()
logger.info("Pre-captured screen data for menu actions")
print("DEBUG: Pre-captured screen data for menu actions")
# Now show the menu
self._menu_gui = AIAssistantMenu(self._handle_menu_selection)
self._menu_gui.show_gui()
print("DEBUG: AI menu GUI shown")
return True
except Exception as e:
logger.error(f"Error showing AI menu: {e}")
print(f"DEBUG: Error showing AI menu: {e}")
import traceback
traceback.print_exc()
return False
@@ -407,22 +380,17 @@ class AIAssistant(Plugin):
"""Handle browsing for an image file to analyze."""
try:
logger.info("AI image file browsing requested")
print("DEBUG: _handle_browse_image_file called")
if not self._enabled:
print("DEBUG: AI Assistant not enabled")
self._present_message("AI Assistant is not enabled")
return True
if not self._ai_provider:
print("DEBUG: AI provider not available")
self._present_message("AI provider not available. Check configuration.")
return True
# Show file chooser dialog
print("DEBUG: About to show file chooser")
image_file = self._show_image_file_chooser()
print(f"DEBUG: File chooser returned: {image_file}")
if image_file:
provider_name = self._provider_type.replace('_', ' ').title()
@@ -536,10 +504,8 @@ class AIAssistant(Plugin):
"""Handle main AI Assistant activation - now shows action dialog."""
try:
logger.info("AI Assistant activation requested")
print("DEBUG: AI Assistant activation keybinding triggered!")
if not self._enabled:
print("DEBUG: AI Assistant not enabled, presenting message")
self._present_message("AI Assistant is not enabled")
return True
@@ -2077,13 +2043,9 @@ class AIAssistantMenu(Gtk.Dialog):
# Connect keyboard events for Enter key handling
self.connect("key-press-event", self._on_key_press)
print("DEBUG: AIAssistantMenu dialog created with radio buttons")
def _on_response(self, dialog, response_id):
"""Handler for dialog response."""
print(f"DEBUG: Dialog response: {response_id}")
if response_id == Gtk.ResponseType.OK:
# Determine which radio button is selected
if self.radio_ask.get_active():
@@ -2098,9 +2060,8 @@ class AIAssistantMenu(Gtk.Dialog):
action_id = "browse_image_file"
else:
action_id = None
if action_id:
print(f"DEBUG: Selected action: {action_id}")
self.on_option_selected(action_id)
self.destroy()
@@ -2116,15 +2077,12 @@ class AIAssistantMenu(Gtk.Dialog):
def show_gui(self):
"""Shows the AI Assistant dialog."""
try:
print("DEBUG: Starting dialog show_gui")
self.show_all()
print("DEBUG: Dialog show_all() called - should be visible and accessible now")
# Present the dialog to ensure it gets focus
self.present()
print("DEBUG: Dialog presented")
except Exception as e:
print(f"DEBUG: Error in show_gui: {e}")
logger.error(f"Error in show_gui: {e}")
import traceback
traceback.print_exc()
+135 -24
View File
@@ -1,15 +1,32 @@
# OCR Plugin for Cthulhu Screen Reader
A powerful OCR (Optical Character Recognition) plugin that enables Cthulhu users to extract text from visual content including windows, desktop areas, and clipboard images. Originally based on the ocrdesktop project by Chrys, this plugin integrates seamlessly with Cthulhu's accessibility framework.
A powerful OCR (Optical Character Recognition) plugin that enables Cthulhu users to extract text from visual content and interact with it through precise coordinate mapping. Originally based on the ocrdesktop project by Chrys, this plugin has been enhanced with interactive features and comprehensive settings integration.
## Features
### 🔍 **OCR Operations**
- **Window OCR**: Extract text from the currently active window
- **Desktop OCR**: Extract text from the entire desktop screen
- **Clipboard OCR**: Extract text from images copied to the clipboard
- **Voice Announcements**: Clear audio feedback about OCR operations
- **Multi-threading**: Non-blocking OCR processing with progress tracking
- **Text Cleanup**: Automatic post-processing to improve OCR text quality
- **Interactive OCR**: OCR with coordinate mapping for clicking and navigation
### 🎯 **Interactive Coordinate Mapping**
- **Precise Clicking**: Click any text found in OCR results using exact coordinates
- **Dual View Modes**: Toggle between text view and interactive coordinate table
- **Safety Confirmation**: Preview click coordinates before executing
- **Real-time Navigation**: Browse OCR results and click immediately
### ⚙️ **Comprehensive Settings**
- **Language Configuration**: Support for all Tesseract language packs
- **Image Processing**: Grayscale, invert, black/white, and scaling options
- **Clipboard Integration**: Automatic copying of OCR results to clipboard
- **Quality Tuning**: Adjustable parameters for optimal OCR accuracy
### 🖥️ **Accessibility Integration**
- **Voice Announcements**: Clear audio feedback for all operations
- **Keyboard Navigation**: Full keyboard control of interactive features
- **Settings GUI**: Integrated settings tab in Cthulhu preferences
- **Non-blocking Processing**: Multi-threaded operation with progress tracking
## Keybindings
@@ -18,6 +35,15 @@ A powerful OCR (Optical Character Recognition) plugin that enables Cthulhu users
| `Cthulhu+Control+W` | OCR Active Window | Performs OCR on the currently focused window |
| `Cthulhu+Control+D` | OCR Desktop | Performs OCR on the entire desktop screen |
| `Cthulhu+Control+Shift+C` | OCR Clipboard | Performs OCR on image data from clipboard |
| `Cthulhu+Control+F` | **Interactive OCR** | **Opens OCR results window with coordinate mapping** |
### Interactive OCR Window Controls
| Key | Action | Description |
|-----|--------|-------------|
| `Alt+V` | Toggle View | Switch between text view and coordinate table |
| `Enter` | Click Selected | Click the text at the selected coordinates |
| `Escape` | Close Window | Close the OCR results window |
| `Arrow Keys` | Navigate | Move through OCR results in table view |
## Dependencies
@@ -67,34 +93,68 @@ To add support for other languages, install additional Tesseract language packs:
- Wait for processing to complete
- OCR results will be announced via speech
3. **Best Practices**:
3. **Interactive OCR Workflow**:
- Press `Cthulhu+Control+F` to open OCR results window
- Wait for "Performing OCR on window for interactive results"
- Use `Alt+V` to toggle between text and coordinate table views
- Navigate with arrow keys in table view to find desired text
- Press `Enter` to click on the selected text location
- Confirm the click action in the safety dialog
4. **Best Practices**:
- Ensure good contrast between text and background for better results
- Use window OCR for focused content (faster processing)
- Use desktop OCR for content spanning multiple windows
- Use clipboard OCR for images from web browsers or image viewers
- Enable "Copy Results to Clipboard" for easy text retrieval
- Adjust scale factor for small or blurry text (try 5-7)
## Configuration
### OCR Settings
The plugin uses the following default settings (configurable in plugin.py):
### OCR Settings GUI
Access comprehensive OCR settings through Cthulhu Preferences:
```python
self._languageCode = 'eng' # Tesseract language code
self._scaleFactor = 3 # Image scaling for better OCR
self._grayscaleImg = False # Convert to grayscale
self._invertImg = False # Invert image colors
self._blackWhiteImg = False # Convert to black/white
self._blackWhiteImgValue = 200 # B/W threshold value
```
1. **Open Cthulhu Preferences**: `~/.local/bin/cthulhu -s`
2. **Navigate to OCR Tab**: Use keyboard navigation to find the OCR settings tab
3. **Configure Settings**: Adjust all OCR parameters through the accessible interface
### Changing OCR Language
To change the default OCR language, modify `self._languageCode` in the plugin's `__init__` method:
### Available Settings
```python
# Examples:
self._languageCode = 'fra' # French
self._languageCode = 'deu' # German
self._languageCode = 'spa' # Spanish
#### **Language Configuration**
- **Language Code**: Tesseract language pack to use (default: 'eng')
- Examples: 'fra' (French), 'deu' (German), 'spa' (Spanish)
- Use '+' for multiple languages: 'eng+fra' for English and French
#### **Image Processing**
- **Scale Factor**: Image scaling multiplier (1-10, default: 3)
- Higher values improve OCR accuracy for small text
- Lower values process faster but may miss details
- **Grayscale Image**: Convert to grayscale for better text recognition
- **Invert Image**: Invert colors (useful for white text on dark backgrounds)
- **Black and White Image**: Convert to pure black/white with threshold
- **Black/White Threshold**: Threshold value for black/white conversion (0-255, default: 200)
#### **Advanced Features**
- **Analyze Colors**: Extract color information from OCR regions (requires scipy/webcolors)
- **Copy Results to Clipboard**: Automatically copy all OCR results to system clipboard
### Configuration File
Settings are automatically stored in Cthulhu's configuration system:
- **Global Settings**: `~/.local/share/cthulhu/user-settings.conf`
- **Profile Settings**: `~/.local/share/cthulhu/app-settings/[profile]/`
### Example Configuration Values
```json
{
"ocrLanguageCode": "eng",
"ocrScaleFactor": 3,
"ocrGrayscaleImg": false,
"ocrInvertImg": false,
"ocrBlackWhiteImg": false,
"ocrBlackWhiteImgValue": 200,
"ocrColorCalculation": false,
"ocrCopyToClipboard": true
}
```
## Troubleshooting
@@ -127,6 +187,28 @@ self._languageCode = 'spa' # Spanish
- Test other Cthulhu speech functions
- Verify audio system is working
#### Interactive OCR window doesn't open
- **Cause**: GTK dependencies missing or display issues
- **Solutions**:
- Ensure GTK3 development packages are installed
- Check display/Wayland/X11 compatibility
- Verify Cthulhu GUI components are working
#### Click coordinates are inaccurate
- **Cause**: Window movement, scaling, or coordinate calculation errors
- **Solutions**:
- Ensure window hasn't moved since OCR capture
- Try recapturing with `Cthulhu+Control+F`
- Check display scaling settings
- Verify no window decoration changes occurred
#### Clipboard copy not working
- **Cause**: Clipboard setting disabled or GTK clipboard issues
- **Solutions**:
- Enable "Copy Results to Clipboard" in OCR settings
- Test clipboard functionality with other applications
- Check GTK clipboard permissions
### Debug Information
OCR plugin debug messages are logged to Cthulhu's debug output. To enable debug logging:
@@ -176,8 +258,18 @@ src/cthulhu/plugins/OCR/
- `_ocrActiveWindow()`: Captures and OCRs active window
- `_ocrDesktop()`: Captures and OCRs entire desktop
- `_ocrClipboard()`: OCRs image from clipboard
- `_performOCR()`: Core OCR processing logic
- `_presentOCRResult()`: Announces results via speech
- `_showOCRResultsWindow()`: **NEW** - Interactive OCR with coordinate mapping
- `_performOCR()`: Core OCR processing with coordinate extraction
- `_presentOCRResult()`: Announces results via speech and clipboard
- `_createOCRResultsWindow()`: **NEW** - Creates interactive GTK results window
- `_clickSelectedText()`: **NEW** - Executes click at OCR coordinates
### Interactive Features Architecture
- **Coordinate Mapping**: Uses `pytesseract.image_to_data()` to extract word positions
- **Screen Transformation**: Converts OCR coordinates to actual screen coordinates
- **GTK Interface**: Accessible results window with text and table views
- **Click Safety**: Confirmation dialogs before executing click actions
- **Settings Integration**: Full integration with Cthulhu's preferences system
### Extending the Plugin
To add new OCR modes or features:
@@ -186,11 +278,30 @@ To add new OCR modes or features:
2. Create handler method following pattern `_ocrNewMode()`
3. Implement image capture logic for new mode
4. Use existing `_performOCR()` and `_presentOCRResult()` methods
5. For interactive features, extend `_createOCRResultsWindow()` functionality
## Version History
### Version 2.0 (Enhanced Interactive Features)
- **Interactive OCR Window**: `Cthulhu+Control+F` for coordinate mapping
- **Precise Clicking**: Click any text found in OCR results
- **Settings Integration**: Full GUI settings tab in Cthulhu preferences
- **Clipboard Integration**: Automatic copying with toggle setting
- **Dual View Modes**: Text view and coordinate table with Alt+V toggle
- **Safety Features**: Click confirmation dialogs
- **Enhanced Processing**: Coordinate extraction with quality metrics
### Version 1.0 (Original Implementation)
- Basic OCR for window, desktop, and clipboard
- Text extraction and speech output
- Multi-threading support
- Text cleanup and formatting
## Credits
- **Original ocrdesktop**: Created by Chrys (chrys87@users.noreply.github.com)
- **Cthulhu Integration**: Adapted by Storm Dragon for Cthulhu plugin system
- **Interactive Features**: Enhanced coordinate mapping and GUI integration
- **Cthulhu Screen Reader**: https://git.stormux.org/storm/cthulhu
- **Tesseract OCR**: https://github.com/tesseract-ocr/tesseract
+411 -9
View File
@@ -20,8 +20,13 @@ import tempfile
import threading
from mimetypes import MimeTypes
import gi
gi.require_version('Atspi', '2.0')
from gi.repository import Atspi
from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import debug
from cthulhu import settings_manager
# Note: Removed complex beep system - simple announcements work perfectly!
@@ -88,8 +93,12 @@ class OCRDesktop(Plugin):
self._kb_binding_window = None
self._kb_binding_desktop = None
self._kb_binding_clipboard = None
self._kb_binding_results_window = None
# OCR settings
# Settings manager
self._settings_manager = settings_manager.getManager()
# OCR settings (will be loaded from settings)
self._languageCode = 'eng'
self._scaleFactor = 3
self._grayscaleImg = False
@@ -98,15 +107,24 @@ class OCRDesktop(Plugin):
self._blackWhiteImgValue = 200
self._colorCalculation = False
self._colorCalculationMax = 3
self._copyToClipboard = False
# Internal state
self._img = []
self._modifiedImg = []
self._OCRText = ''
self._OCRWords = {}
self._OCRWordList = []
self._offsetXpos = 0
self._offsetYpos = 0
self._activated = False
# OCR Results Window
self._results_window = None
self._results_tree = None
self._results_textview = None
self._current_view_mode = 0 # 0 = text, 1 = tree
# Progress feedback
self._is_processing = False
@@ -118,6 +136,9 @@ class OCRDesktop(Plugin):
# Set locale for tesseract
locale.setlocale(locale.LC_ALL, 'C')
# Load OCR settings from configuration
self._loadOCRSettings()
# Check dependencies
self._checkDependencies()
@@ -138,6 +159,23 @@ class OCRDesktop(Plugin):
return False
return True
def _loadOCRSettings(self):
"""Load OCR settings from Cthulhu configuration."""
try:
self._languageCode = self._settings_manager.getSetting('ocrLanguageCode') or 'eng'
self._scaleFactor = self._settings_manager.getSetting('ocrScaleFactor') or 3
self._grayscaleImg = self._settings_manager.getSetting('ocrGrayscaleImg') or False
self._invertImg = self._settings_manager.getSetting('ocrInvertImg') or False
self._blackWhiteImg = self._settings_manager.getSetting('ocrBlackWhiteImg') or False
self._blackWhiteImgValue = self._settings_manager.getSetting('ocrBlackWhiteImgValue') or 200
self._colorCalculation = self._settings_manager.getSetting('ocrColorCalculation') or False
self._colorCalculationMax = self._settings_manager.getSetting('ocrColorCalculationMax') or 3
self._copyToClipboard = self._settings_manager.getSetting('ocrCopyToClipboard') or False
debug.printMessage(debug.LEVEL_INFO, f"OCR settings loaded: lang={self._languageCode}, scale={self._scaleFactor}, clipboard={self._copyToClipboard}", True)
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"OCR settings load error: {e}, using defaults", True)
@cthulhu_hookimpl
def activate(self, plugin=None):
"""Activate the plugin."""
@@ -179,6 +217,19 @@ class OCRDesktop(Plugin):
self._activated = False
debug.printMessage(debug.LEVEL_INFO, "OCRDesktop: Plugin deactivated", True)
def refresh_settings(self):
"""Refresh plugin settings when configuration changes."""
try:
debug.printMessage(debug.LEVEL_INFO, "OCRDesktop: Refreshing settings", True)
# Reload OCR settings from configuration
self._loadOCRSettings()
debug.printMessage(debug.LEVEL_INFO, "OCRDesktop: Settings refreshed successfully", True)
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"OCRDesktop: Error refreshing settings: {e}", True)
def _registerKeybindings(self):
"""Register plugin keybindings."""
try:
@@ -203,6 +254,13 @@ class OCRDesktop(Plugin):
'kb:cthulhu+control+shift+c'
)
# OCR results window
self._kb_binding_results_window = self.registerGestureByString(
self._showOCRResultsWindow,
"Show OCR results window for current window",
'kb:cthulhu+control+f'
)
debug.printMessage(debug.LEVEL_INFO, "OCRDesktop: Keybindings registered", True)
except Exception as e:
@@ -409,18 +467,27 @@ class OCRDesktop(Plugin):
return modifiedImg
def _performOCR(self):
"""Perform OCR on captured images."""
"""Perform OCR on captured images with coordinate data extraction."""
if not PYTESSERACT_AVAILABLE:
debug.printMessage(debug.LEVEL_INFO, "OCRDesktop: Tesseract not available", True)
return
debug.printMessage(debug.LEVEL_INFO, "OCRDesktop: Starting OCR", True)
self._OCRText = ''
self._OCRWords = {}
self._OCRWordList = []
for img in self._img:
modifiedImg = self._transformImg(img)
try:
# Simple text extraction
# Extract coordinate data using image_to_data
from pytesseract import Output
OCRWords = pytesseract.image_to_data(modifiedImg, output_type=Output.DICT,
lang=self._languageCode, config='--psm 4')
self._appendToOCRWords(OCRWords)
self._processOCRWords(OCRWords, modifiedImg)
# Also extract simple text for speech output
text = pytesseract.image_to_string(modifiedImg, lang=self._languageCode, config='--psm 4')
self._OCRText += text + '\n'
except Exception as e:
@@ -433,31 +500,36 @@ class OCRDesktop(Plugin):
def _cleanOCRText(self):
"""Clean up OCR text output."""
# Remove multiple spaces
regexSpace = re.compile('[^\S\r\n]{2,}')
regexSpace = re.compile(r'[^\S\r\n]{2,}')
self._OCRText = regexSpace.sub(' ', self._OCRText)
# Remove empty lines
regexSpace = re.compile('\n\s*\n')
regexSpace = re.compile(r'\n\s*\n')
self._OCRText = regexSpace.sub('\n', self._OCRText)
# Remove trailing spaces
regexSpace = re.compile('\s*\n')
regexSpace = re.compile(r'\s*\n')
self._OCRText = regexSpace.sub('\n', self._OCRText)
# Remove leading spaces
regexSpace = re.compile('^\s')
regexSpace = re.compile(r'^\s')
self._OCRText = regexSpace.sub('', self._OCRText)
# Remove trailing newlines
self._OCRText = self._OCRText.strip()
def _presentOCRResult(self):
"""Present OCR result to user via speech."""
"""Present OCR result to user via speech and optionally copy to clipboard."""
try:
if not self._OCRText.strip():
message = "No text found in OCR scan"
else:
message = f"OCR result: {self._OCRText}"
# Copy to clipboard if enabled
if self._copyToClipboard:
self._copyTextToClipboard(self._OCRText)
message += " (copied to clipboard)"
if self.app:
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
@@ -467,4 +539,334 @@ class OCRDesktop(Plugin):
debug.printMessage(debug.LEVEL_INFO, f"OCRDesktop: Presented result: {len(self._OCRText)} characters", True)
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"OCRDesktop: Error presenting result: {e}", True)
debug.printMessage(debug.LEVEL_INFO, f"OCRDesktop: Error presenting result: {e}", True)
def _appendToOCRWords(self, OCRWords):
"""Append OCR words to the main OCR data structure."""
for k, v in OCRWords.items():
try:
x = self._OCRWords[k]
if isinstance(v, list):
self._OCRWords[k].extend(v)
except KeyError:
self._OCRWords[k] = v
def _processOCRWords(self, OCRWords, img):
"""Process OCR words to extract coordinate data."""
boxCounter = len(OCRWords['level'])
if boxCounter == 0:
return False
lastPage = -1
lastBlock = -1
lastPar = -1
lastLine = -1
for i in range(boxCounter):
if (len(OCRWords['text'][i]) == 0) or OCRWords['text'][i].isspace():
continue
# Add word to coordinate list
self._OCRWordList.append([
OCRWords['text'][i], # Text
round(OCRWords['height'][i] / 3 * 0.78, 0), # Calculated fontsize
self._getColorString(OCRWords, i, img), # Color info
'text', # Object type
int(OCRWords['width'][i] / 2 + OCRWords['left'][i]), # X coordinate (center)
int(OCRWords['height'][i] / 2 + OCRWords['top'][i]), # Y coordinate (center)
int(float(OCRWords['conf'][i])) # Confidence
])
lastPage = OCRWords['page_num'][i]
lastBlock = OCRWords['block_num'][i]
lastPar = OCRWords['par_num'][i]
lastLine = OCRWords['line_num'][i]
return True
def _getColorString(self, box, index, img):
"""Get color information for OCR text (simplified version)."""
if not self._colorCalculation:
return 'unknown'
if not SCIPY_AVAILABLE or not WEBCOLORS_AVAILABLE:
return 'unknown'
# Simplified color calculation - just return "unknown" for now
# Full implementation would require the color analysis from ocrdesktop
return 'unknown'
def _showOCRResultsWindow(self, script=None, inputEvent=None):
"""Show OCR results window for current window with coordinate mapping."""
try:
debug.printMessage(debug.LEVEL_INFO, "OCRDesktop: OCR results window requested", True)
if self._is_processing:
debug.printMessage(debug.LEVEL_INFO, "OCRDesktop: Already processing, ignoring request", True)
return True
self._is_processing = True
self._announceOCRStart("window for interactive results")
try:
if self._screenShotWindow():
self._performOCR()
self._createOCRResultsWindow()
finally:
self._is_processing = False
return True
except Exception as e:
self._is_processing = False
debug.printMessage(debug.LEVEL_INFO, f"OCRDesktop: Error in OCR results window: {e}", True)
return False
def _createOCRResultsWindow(self):
"""Create and show the OCR results window with coordinate mapping."""
if not GTK_AVAILABLE:
debug.printMessage(debug.LEVEL_INFO, "OCRDesktop: GTK not available for results window", True)
return
try:
# Create main window
self._results_window = Gtk.Window(title="OCR Results - Cthulhu")
self._results_window.set_default_size(800, 600)
self._results_window.set_modal(True)
# Create main container
vbox = Gtk.VBox()
# Create menu bar
menubar = self._createResultsMenuBar()
vbox.pack_start(menubar, False, False, 0)
# Create text view for OCR text
scrolled_text = Gtk.ScrolledWindow()
self._results_textview = Gtk.TextView()
self._results_textview.set_editable(False)
buffer = self._results_textview.get_buffer()
buffer.set_text(self._OCRText)
scrolled_text.add(self._results_textview)
# Create tree view for coordinate data
scrolled_tree = Gtk.ScrolledWindow()
self._results_tree = self._createResultsTreeView()
scrolled_tree.add(self._results_tree)
# Add both views to container
vbox.pack_start(scrolled_text, True, True, 0)
vbox.pack_start(scrolled_tree, True, True, 0)
# Set initial view (text only)
scrolled_tree.hide()
self._results_window.add(vbox)
self._results_window.connect("destroy", self._onResultsWindowDestroy)
self._results_window.connect("key-press-event", self._onResultsKeyPress)
# Show window
self._results_window.show_all()
scrolled_tree.hide() # Hide tree initially
debug.printMessage(debug.LEVEL_INFO, "OCRDesktop: Results window created", True)
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"OCRDesktop: Error creating results window: {e}", True)
def _createResultsMenuBar(self):
"""Create menu bar for results window."""
menubar = Gtk.MenuBar()
# View menu
view_menu = Gtk.Menu()
view_item = Gtk.MenuItem(label="View")
view_item.set_submenu(view_menu)
# Toggle view option
toggle_item = Gtk.MenuItem(label="Toggle View (Alt+V)")
toggle_item.connect("activate", self._toggleResultsView)
view_menu.append(toggle_item)
menubar.append(view_item)
# Actions menu
actions_menu = Gtk.Menu()
actions_item = Gtk.MenuItem(label="Actions")
actions_item.set_submenu(actions_menu)
# Click action
click_item = Gtk.MenuItem(label="Click Selected (Enter)")
click_item.connect("activate", self._clickSelectedText)
actions_menu.append(click_item)
menubar.append(actions_item)
return menubar
def _createResultsTreeView(self):
"""Create tree view for OCR results with coordinates."""
# Create list store
store = Gtk.ListStore(str, str, int, str, str, int, int, int)
# Create tree view
tree = Gtk.TreeView(model=store)
tree.set_search_column(0)
# Add columns
columns = [
("Text", 0),
("Font Size", 2),
("Color", 3),
("Type", 4),
("X Position", 5),
("Y Position", 6),
("Confidence", 7)
]
for title, col_id in columns:
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(title, renderer, text=col_id)
column.set_sort_column_id(col_id)
tree.append_column(column)
# Populate with OCR data
for row in self._OCRWordList:
# Transform coordinates back to screen coordinates
x_coord = int(row[4] / self._scaleFactor + self._offsetXpos)
y_coord = int(row[5] / self._scaleFactor + self._offsetYpos)
store.append([
row[0], # Text
str(row[1]), # Font size (as string for display)
int(row[1]), # Font size (as int for sorting)
row[2], # Color
row[3], # Type
x_coord, # X coordinate (screen)
y_coord, # Y coordinate (screen)
row[6] # Confidence
])
tree.connect("row-activated", self._onTreeRowActivated)
return tree
def _toggleResultsView(self, widget):
"""Toggle between text and tree view."""
if not self._results_window:
return
# Get the container
vbox = self._results_window.get_child()
scrolled_text = vbox.get_children()[1] # Second child (after menubar)
scrolled_tree = vbox.get_children()[2] # Third child
if self._current_view_mode == 0: # Currently showing text
scrolled_text.hide()
scrolled_tree.show()
self._current_view_mode = 1
self._results_tree.grab_focus()
else: # Currently showing tree
scrolled_tree.hide()
scrolled_text.show()
self._current_view_mode = 0
self._results_textview.grab_focus()
def _onTreeRowActivated(self, tree, path, column):
"""Handle double-click or Enter on tree row."""
self._clickSelectedText(None)
def _clickSelectedText(self, widget):
"""Click at the coordinates of the selected text."""
if not self._results_tree:
return
selection = self._results_tree.get_selection()
if not selection:
return
model, tree_iter = selection.get_selected()
if not tree_iter:
return
# Get coordinates
x_coord = model.get_value(tree_iter, 5) # X position
y_coord = model.get_value(tree_iter, 6) # Y position
text = model.get_value(tree_iter, 0) # Text for confirmation
# Confirm click action
dialog = Gtk.MessageDialog(
self._results_window,
Gtk.DialogFlags.MODAL,
Gtk.MessageType.QUESTION,
Gtk.ButtonsType.YES_NO,
f"Click at coordinates ({x_coord}, {y_coord}) for text '{text}'?"
)
response = dialog.run()
dialog.destroy()
if response == Gtk.ResponseType.YES:
try:
# Hide window before clicking
self._results_window.hide()
# Perform click using AT-SPI2
time.sleep(0.5) # Brief delay
Atspi.generate_mouse_event(x_coord, y_coord, "b1c")
debug.printMessage(debug.LEVEL_INFO, f"OCRDesktop: Clicked at ({x_coord}, {y_coord})", True)
# Destroy window after successful click
self._results_window.destroy()
self._results_window = None
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"OCRDesktop: Error clicking: {e}", True)
# Show window again on error
self._results_window.show()
def _onResultsKeyPress(self, widget, event):
"""Handle key presses in results window."""
keyval = event.keyval
state = event.state
# Alt+V to toggle view
if (keyval == Gdk.KEY_v or keyval == Gdk.KEY_V) and (state & Gdk.ModifierType.MOD1_MASK):
self._toggleResultsView(None)
return True
# Enter to click selected
if keyval == Gdk.KEY_Return or keyval == Gdk.KEY_KP_Enter:
if self._current_view_mode == 1: # Tree view
self._clickSelectedText(None)
return True
# Escape to close
if keyval == Gdk.KEY_Escape:
self._results_window.destroy()
return True
return False
def _onResultsWindowDestroy(self, widget):
"""Handle results window destruction."""
self._results_window = None
self._results_tree = None
self._results_textview = None
self._current_view_mode = 0
debug.printMessage(debug.LEVEL_INFO, "OCRDesktop: Results window destroyed", True)
def _copyTextToClipboard(self, text):
"""Copy text to system clipboard."""
if not GTK_AVAILABLE:
debug.printMessage(debug.LEVEL_INFO, "OCRDesktop: GTK not available for clipboard", True)
return False
try:
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.set_text(text, -1)
clipboard.store()
debug.printMessage(debug.LEVEL_INFO, f"OCRDesktop: Copied {len(text)} characters to clipboard", True)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"OCRDesktop: Error copying to clipboard: {e}", True)
return False
@@ -0,0 +1 @@
from .plugin import SpeechHistory
@@ -0,0 +1,14 @@
speechhistory_python_sources = files([
'__init__.py',
'plugin.py'
])
python3.install_sources(
speechhistory_python_sources,
subdir: 'cthulhu/plugins/SpeechHistory'
)
install_data(
'plugin.info',
install_dir: python3.get_install_dir() / 'cthulhu' / 'plugins' / 'SpeechHistory'
)
@@ -0,0 +1,8 @@
name = Speech History
version = 1.0.0
description = Keeps a history of all speech output with navigation and clipboard support
authors = Cthulhu Plugin System
website = https://git.stormux.org/storm/cthulhu
copyright = Copyright 2024 Stormux
builtin = true
hidden = false
@@ -0,0 +1,235 @@
#!/usr/bin/env python3
import logging
from collections import deque
from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import settings_manager
from cthulhu import debug
logger = logging.getLogger(__name__)
class SpeechHistory(Plugin):
"""Speech History plugin - SAFE manual-only version (no automatic capture)."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
debug.printMessage(debug.LEVEL_INFO, "SpeechHistory SAFE plugin initialized", True)
# History storage - start with some sample items
self._max_history_size = 50
self._history = deque([
"Welcome to safe speech history",
"This version doesn't auto-capture to prevent crashes",
"Use add_to_history() method to manually add items",
"Navigate with Cthulhu+Control+Shift+H (previous)",
"Navigate with Cthulhu+Control+H (next)",
"Copy with Cthulhu+Control+Y"
], maxlen=self._max_history_size)
self._current_history_index = -1
# Keybinding storage
self._kb_nav_prev = None
self._kb_nav_next = None
self._kb_copy_last = None
# Settings integration
self._settings_manager = settings_manager.getManager()
@cthulhu_hookimpl
def activate(self, plugin=None):
"""Activate the plugin."""
if plugin is not None and plugin is not self:
return
try:
debug.printMessage(debug.LEVEL_INFO, "=== SpeechHistory SAFE activation starting ===", True)
# Load settings
self._load_settings()
# Register keybindings only - NO speech capture
self._register_keybindings()
debug.printMessage(debug.LEVEL_INFO, "SpeechHistory SAFE plugin activated successfully", True)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"Error activating SpeechHistory SAFE: {e}", True)
return False
@cthulhu_hookimpl
def deactivate(self, plugin=None):
"""Deactivate the plugin."""
if plugin is not None and plugin is not self:
return
debug.printMessage(debug.LEVEL_INFO, "Deactivating SpeechHistory SAFE plugin", True)
# Clear keybindings
self._kb_nav_prev = None
self._kb_nav_next = None
self._kb_copy_last = None
return True
def _load_settings(self):
"""Load plugin settings."""
try:
self._max_history_size = self._settings_manager.getSetting('speechHistorySize') or 50
# Update deque maxlen if needed
if self._history.maxlen != self._max_history_size:
old_history = list(self._history)
self._history = deque(old_history[-self._max_history_size:], maxlen=self._max_history_size)
debug.printMessage(debug.LEVEL_INFO, f"Speech history size: {self._max_history_size}", True)
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"Error loading settings: {e}", True)
self._max_history_size = 50
def _register_keybindings(self):
"""Register plugin keybindings."""
try:
# Cthulhu+Control+Shift+H (History previous)
self._kb_nav_prev = self.registerGestureByString(
self._navigate_history_prev,
"Speech history previous",
'kb:cthulhu+control+shift+h'
)
# Cthulhu+Control+H (History next)
self._kb_nav_next = self.registerGestureByString(
self._navigate_history_next,
"Speech history next",
'kb:cthulhu+control+h'
)
# Cthulhu+Control+Y (Copy history)
self._kb_copy_last = self.registerGestureByString(
self._copy_last_spoken,
"Copy speech history item to clipboard",
'kb:cthulhu+control+y'
)
debug.printMessage(debug.LEVEL_INFO, f"Registered keybindings: {bool(self._kb_nav_prev)}, {bool(self._kb_nav_next)}, {bool(self._kb_copy_last)}", True)
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"Error registering keybindings: {e}", True)
def _navigate_history_prev(self, script=None, inputEvent=None):
"""Navigate to previous item in speech history."""
try:
if not self._history:
self._present_message("Speech history is empty")
return True
# Move backward in history (to older items)
if self._current_history_index == -1:
self._current_history_index = len(self._history) - 1
elif self._current_history_index > 0:
self._current_history_index -= 1
else:
self._current_history_index = len(self._history) - 1
# Present the history item
history_item = self._history[self._current_history_index]
position = self._current_history_index + 1
self._present_message(f"History {position} of {len(self._history)}: {history_item}")
return True
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"Error navigating to previous: {e}", True)
return False
def _navigate_history_next(self, script=None, inputEvent=None):
"""Navigate to next item in speech history."""
try:
if not self._history:
self._present_message("Speech history is empty")
return True
# Move forward in history (to newer items)
if self._current_history_index == -1:
self._current_history_index = 0
elif self._current_history_index < len(self._history) - 1:
self._current_history_index += 1
else:
self._current_history_index = 0
# Present the history item
history_item = self._history[self._current_history_index]
position = self._current_history_index + 1
self._present_message(f"History {position} of {len(self._history)}: {history_item}")
return True
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"Error navigating to next: {e}", True)
return False
def _copy_last_spoken(self, script=None, inputEvent=None):
"""Copy the last spoken text to clipboard."""
try:
if not self._history:
self._present_message("No speech history to copy")
return True
# Copy the most recent speech
last_spoken = self._history[-1]
try:
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.set_text(last_spoken, -1)
clipboard.store()
# Show confirmation
preview = last_spoken[:50] + ('...' if len(last_spoken) > 50 else '')
self._present_message(f"Copied to clipboard: {preview}")
except Exception as clipboard_error:
debug.printMessage(debug.LEVEL_INFO, f"Clipboard error: {clipboard_error}", True)
self._present_message("Error copying to clipboard")
return True
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"Error copying: {e}", True)
return False
def _present_message(self, message):
"""Present a message to the user via speech."""
try:
if self.app:
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
if state and state.activeScript:
state.activeScript.presentMessage(message, resetStyles=False)
else:
debug.printMessage(debug.LEVEL_INFO, f"Message: {message}", True)
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"Error presenting message: {e}", True)
def add_to_history(self, text):
"""Public method to safely add items to history."""
try:
if not text or not text.strip():
return
clean_text = text.strip()
if len(clean_text) < 2:
return
# Simple duplicate prevention
if self._history and self._history[-1] == clean_text:
return
# Add to history
self._history.append(clean_text)
self._current_history_index = -1
debug.printMessage(debug.LEVEL_INFO, f"Manually added to history: {clean_text[:50]}{'...' if len(clean_text) > 50 else ''}", True)
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"Error adding to history: {e}", True)
+21 -1
View File
@@ -157,7 +157,16 @@ userCustomizableSettings = [
"aiConfirmationRequired",
"aiActionTimeout",
"aiScreenshotQuality",
"aiMaxContextLength"
"aiMaxContextLength",
"ocrLanguageCode",
"ocrScaleFactor",
"ocrGrayscaleImg",
"ocrInvertImg",
"ocrBlackWhiteImg",
"ocrBlackWhiteImgValue",
"ocrColorCalculation",
"ocrColorCalculationMax",
"ocrCopyToClipboard"
]
GENERAL_KEYBOARD_LAYOUT_DESKTOP = 1
@@ -443,3 +452,14 @@ aiConfirmationRequired = True
aiActionTimeout = 30
aiScreenshotQuality = AI_SCREENSHOT_QUALITY_MEDIUM
aiMaxContextLength = 4000
# OCR Plugin settings
ocrLanguageCode = 'eng'
ocrScaleFactor = 3
ocrGrayscaleImg = False
ocrInvertImg = False
ocrBlackWhiteImg = False
ocrBlackWhiteImgValue = 200
ocrColorCalculation = False
ocrColorCalculationMax = 3
ocrCopyToClipboard = False
File diff suppressed because it is too large Load Diff
+515
View File
@@ -0,0 +1,515 @@
#!/usr/bin/env python3
#
# Copyright (c) 2025 Stormux
# Copyright (c) 2025 Igalia, S.L.
#
# 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.
"""Enhanced speech settings management for D-Bus remote controller."""
__id__ = "$Id$"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2025 Stormux"
__license__ = "LGPL"
from . import cthulhu_state
from . import debug
from . import dbus_service
from . import messages
from . import settings
from . import settings_manager
class SpeechDBusManager:
"""Enhanced speech settings for D-Bus remote control."""
def __init__(self):
"""Initialize the speech D-Bus manager."""
self._settings_manager = settings_manager.getManager()
@dbus_service.getter
def get_verbosity_level(self) -> str:
"""Returns the current speech verbosity level."""
level = self._settings_manager.getSetting("speechVerbosityLevel")
if level == settings.VERBOSITY_LEVEL_BRIEF:
return "brief"
else:
return "verbose"
@dbus_service.setter
def set_verbosity_level(self, value: str) -> bool:
"""Sets the speech verbosity level."""
if value.lower() == "brief":
setting_value = settings.VERBOSITY_LEVEL_BRIEF
elif value.lower() == "verbose":
setting_value = settings.VERBOSITY_LEVEL_VERBOSE
else:
msg = f"SPEECH DBUS MANAGER: Invalid verbosity level: {value}"
debug.printMessage(debug.LEVEL_WARNING, msg, True)
return False
msg = f"SPEECH DBUS MANAGER: Setting verbosity level to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("speechVerbosityLevel", setting_value)
return True
@dbus_service.getter
def get_capitalization_style(self) -> str:
"""Returns the current capitalization style."""
style = self._settings_manager.getSetting("capitalizationStyle")
if style == settings.CAPITALIZATION_STYLE_NONE:
return "none"
elif style == settings.CAPITALIZATION_STYLE_SPELL:
return "spell"
elif style == settings.CAPITALIZATION_STYLE_ICON:
return "icon"
else:
return "none"
@dbus_service.setter
def set_capitalization_style(self, value: str) -> bool:
"""Sets the capitalization style."""
value_lower = value.lower()
if value_lower == "none":
setting_value = settings.CAPITALIZATION_STYLE_NONE
elif value_lower == "spell":
setting_value = settings.CAPITALIZATION_STYLE_SPELL
elif value_lower == "icon":
setting_value = settings.CAPITALIZATION_STYLE_ICON
else:
msg = f"SPEECH DBUS MANAGER: Invalid capitalization style: {value}"
debug.printMessage(debug.LEVEL_WARNING, msg, True)
return False
msg = f"SPEECH DBUS MANAGER: Setting capitalization style to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("capitalizationStyle", setting_value)
return True
@dbus_service.getter
def get_punctuation_level(self) -> str:
"""Returns the current punctuation level."""
level = self._settings_manager.getSetting("verbalizePunctuationStyle")
if level == settings.PUNCTUATION_STYLE_NONE:
return "none"
elif level == settings.PUNCTUATION_STYLE_SOME:
return "some"
elif level == settings.PUNCTUATION_STYLE_MOST:
return "most"
elif level == settings.PUNCTUATION_STYLE_ALL:
return "all"
else:
return "some"
@dbus_service.setter
def set_punctuation_level(self, value: str) -> bool:
"""Sets the punctuation level."""
value_lower = value.lower()
if value_lower == "none":
setting_value = settings.PUNCTUATION_STYLE_NONE
elif value_lower == "some":
setting_value = settings.PUNCTUATION_STYLE_SOME
elif value_lower == "most":
setting_value = settings.PUNCTUATION_STYLE_MOST
elif value_lower == "all":
setting_value = settings.PUNCTUATION_STYLE_ALL
else:
msg = f"SPEECH DBUS MANAGER: Invalid punctuation level: {value}"
debug.printMessage(debug.LEVEL_WARNING, msg, True)
return False
msg = f"SPEECH DBUS MANAGER: Setting punctuation level to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("verbalizePunctuationStyle", setting_value)
return True
@dbus_service.getter
def get_speak_numbers_as_digits(self) -> bool:
"""Returns whether numbers are spoken as digits."""
return self._settings_manager.getSetting("speakNumbersAsDigits")
@dbus_service.setter
def set_speak_numbers_as_digits(self, value: bool) -> bool:
"""Sets whether numbers are spoken as digits."""
msg = f"SPEECH DBUS MANAGER: Setting speak numbers as digits to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("speakNumbersAsDigits", value)
return True
@dbus_service.getter
def get_speech_is_muted(self) -> bool:
"""Returns whether speech output is temporarily muted."""
return self._settings_manager.getSetting("silenceSpeech")
@dbus_service.setter
def set_speech_is_muted(self, value: bool) -> bool:
"""Sets whether speech output is temporarily muted."""
msg = f"SPEECH DBUS MANAGER: Setting speech muted to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("silenceSpeech", value)
return True
@dbus_service.getter
def get_only_speak_displayed_text(self) -> bool:
"""Returns whether only displayed text should be spoken."""
return self._settings_manager.getSetting("onlySpeakDisplayedText")
@dbus_service.setter
def set_only_speak_displayed_text(self, value: bool) -> bool:
"""Sets whether only displayed text should be spoken."""
msg = f"SPEECH DBUS MANAGER: Setting only speak displayed text to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("onlySpeakDisplayedText", value)
return True
@dbus_service.getter
def get_speak_indentation_and_justification(self) -> bool:
"""Returns whether speaking of indentation and justification is enabled."""
return self._settings_manager.getSetting("enableSpeechIndentation")
@dbus_service.setter
def set_speak_indentation_and_justification(self, value: bool) -> bool:
"""Sets whether speaking of indentation and justification is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting speak indentation and justification to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableSpeechIndentation", value)
return True
@dbus_service.command
def toggle_speech(self, script=None, event=None):
"""Toggles speech on and off."""
tokens = ["SPEECH DBUS MANAGER: toggle_speech. Script:", script, "Event:", event]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if script is not None:
script.presentationInterrupt()
if self.get_speech_is_muted():
self.set_speech_is_muted(False)
if script is not None:
script.presentMessage(messages.SPEECH_ENABLED)
elif not self._settings_manager.getSetting("enableSpeech"):
self._settings_manager.setSetting("enableSpeech", True)
if script is not None:
script.presentMessage(messages.SPEECH_ENABLED)
else:
if script is not None:
script.presentMessage(messages.SPEECH_DISABLED)
self.set_speech_is_muted(True)
@dbus_service.command
def toggle_verbosity(self, script=None, event=None):
"""Toggles speech verbosity level between verbose and brief."""
tokens = ["SPEECH DBUS MANAGER: toggle_verbosity. Script:", script, "Event:", event]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
current_level = self._settings_manager.getSetting("speechVerbosityLevel")
if current_level == settings.VERBOSITY_LEVEL_BRIEF:
if script is not None:
script.presentMessage(messages.SPEECH_VERBOSITY_VERBOSE)
self._settings_manager.setSetting("speechVerbosityLevel", settings.VERBOSITY_LEVEL_VERBOSE)
else:
if script is not None:
script.presentMessage(messages.SPEECH_VERBOSITY_BRIEF)
self._settings_manager.setSetting("speechVerbosityLevel", settings.VERBOSITY_LEVEL_BRIEF)
@dbus_service.command
def change_number_style(self, script=None, event=None):
"""Changes spoken number style between digits and words."""
tokens = ["SPEECH DBUS MANAGER: change_number_style. Script:", script, "Event:", event]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
speak_digits = self.get_speak_numbers_as_digits()
if speak_digits:
brief = messages.NUMBER_STYLE_WORDS_BRIEF
full = messages.NUMBER_STYLE_WORDS_FULL
else:
brief = messages.NUMBER_STYLE_DIGITS_BRIEF
full = messages.NUMBER_STYLE_DIGITS_FULL
self.set_speak_numbers_as_digits(not speak_digits)
if script is not None:
script.presentMessage(full, brief)
@dbus_service.command
def say_all(self, script=None, event=None):
"""Speaks the entire document or text, starting from the current position."""
tokens = ["SPEECH DBUS MANAGER: say_all. Script:", script, "Event:", event]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
# Use the current active script if not provided
if script is None:
script = cthulhu_state.activeScript
if script is None:
msg = "SPEECH DBUS MANAGER: No active script available for Say All"
debug.printMessage(debug.LEVEL_WARNING, msg, True)
return False
# Call the script's Say All method
try:
script.sayAll(event, notify_user=False)
return True
except Exception as e:
msg = f"SPEECH DBUS MANAGER: Error during Say All: {e}"
debug.printMessage(debug.LEVEL_SEVERE, msg, True)
return False
# Key Echo Controls
@dbus_service.getter
def get_key_echo_enabled(self) -> bool:
"""Returns whether echo of key presses is enabled."""
return self._settings_manager.getSetting("enableKeyEcho")
@dbus_service.setter
def set_key_echo_enabled(self, value: bool) -> bool:
"""Sets whether echo of key presses is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting enable key echo to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableKeyEcho", value)
return True
@dbus_service.getter
def get_character_echo_enabled(self) -> bool:
"""Returns whether echo of inserted characters is enabled."""
return self._settings_manager.getSetting("enableEchoByCharacter")
@dbus_service.setter
def set_character_echo_enabled(self, value: bool) -> bool:
"""Sets whether echo of inserted characters is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting enable character echo to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableEchoByCharacter", value)
return True
@dbus_service.getter
def get_word_echo_enabled(self) -> bool:
"""Returns whether word echo is enabled."""
return self._settings_manager.getSetting("enableEchoByWord")
@dbus_service.setter
def set_word_echo_enabled(self, value: bool) -> bool:
"""Sets whether word echo is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting enable word echo to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableEchoByWord", value)
return True
@dbus_service.getter
def get_sentence_echo_enabled(self) -> bool:
"""Returns whether sentence echo is enabled."""
return self._settings_manager.getSetting("enableEchoBySentence")
@dbus_service.setter
def set_sentence_echo_enabled(self, value: bool) -> bool:
"""Sets whether sentence echo is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting enable sentence echo to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableEchoBySentence", value)
return True
@dbus_service.getter
def get_alphabetic_keys_enabled(self) -> bool:
"""Returns whether alphabetic keys will be echoed when key echo is enabled."""
return self._settings_manager.getSetting("enableAlphabeticKeys")
@dbus_service.setter
def set_alphabetic_keys_enabled(self, value: bool) -> bool:
"""Sets whether alphabetic keys will be echoed when key echo is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting enable alphabetic keys to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableAlphabeticKeys", value)
return True
@dbus_service.getter
def get_numeric_keys_enabled(self) -> bool:
"""Returns whether numeric keys will be echoed when key echo is enabled."""
return self._settings_manager.getSetting("enableNumericKeys")
@dbus_service.setter
def set_numeric_keys_enabled(self, value: bool) -> bool:
"""Sets whether numeric keys will be echoed when key echo is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting enable numeric keys to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableNumericKeys", value)
return True
@dbus_service.getter
def get_punctuation_keys_enabled(self) -> bool:
"""Returns whether punctuation keys will be echoed when key echo is enabled."""
return self._settings_manager.getSetting("enablePunctuationKeys")
@dbus_service.setter
def set_punctuation_keys_enabled(self, value: bool) -> bool:
"""Sets whether punctuation keys will be echoed when key echo is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting enable punctuation keys to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enablePunctuationKeys", value)
return True
@dbus_service.getter
def get_space_enabled(self) -> bool:
"""Returns whether space key will be echoed when key echo is enabled."""
return self._settings_manager.getSetting("enableSpace")
@dbus_service.setter
def set_space_enabled(self, value: bool) -> bool:
"""Sets whether space key will be echoed when key echo is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting enable space to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableSpace", value)
return True
@dbus_service.getter
def get_modifier_keys_enabled(self) -> bool:
"""Returns whether modifier keys will be echoed when key echo is enabled."""
return self._settings_manager.getSetting("enableModifierKeys")
@dbus_service.setter
def set_modifier_keys_enabled(self, value: bool) -> bool:
"""Sets whether modifier keys will be echoed when key echo is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting enable modifier keys to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableModifierKeys", value)
return True
@dbus_service.getter
def get_function_keys_enabled(self) -> bool:
"""Returns whether function keys will be echoed when key echo is enabled."""
return self._settings_manager.getSetting("enableFunctionKeys")
@dbus_service.setter
def set_function_keys_enabled(self, value: bool) -> bool:
"""Sets whether function keys will be echoed when key echo is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting enable function keys to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableFunctionKeys", value)
return True
@dbus_service.getter
def get_action_keys_enabled(self) -> bool:
"""Returns whether action keys will be echoed when key echo is enabled."""
return self._settings_manager.getSetting("enableActionKeys")
@dbus_service.setter
def set_action_keys_enabled(self, value: bool) -> bool:
"""Sets whether action keys will be echoed when key echo is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting enable action keys to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableActionKeys", value)
return True
@dbus_service.getter
def get_navigation_keys_enabled(self) -> bool:
"""Returns whether navigation keys will be echoed when key echo is enabled."""
return self._settings_manager.getSetting("enableNavigationKeys")
@dbus_service.setter
def set_navigation_keys_enabled(self, value: bool) -> bool:
"""Sets whether navigation keys will be echoed when key echo is enabled."""
msg = f"SPEECH DBUS MANAGER: Setting enable navigation keys to {value}."
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._settings_manager.setSetting("enableNavigationKeys", value)
return True
@dbus_service.command
def cycle_key_echo(self, script=None, event=None):
"""Cycle through the key echo levels."""
tokens = ["SPEECH DBUS MANAGER: cycle_key_echo. Script:", script, "Event:", event]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
# Get current settings
key = self._settings_manager.getSetting("enableKeyEcho")
word = self._settings_manager.getSetting("enableEchoByWord")
sentence = self._settings_manager.getSetting("enableEchoBySentence")
# Cycle through the combinations: none -> key -> word -> sentence -> all -> none
if not key and not word and not sentence:
# None -> Key only
new_key, new_word, new_sentence = True, False, False
brief = messages.KEY_ECHO_KEY_BRIEF
full = messages.KEY_ECHO_KEY_FULL
elif key and not word and not sentence:
# Key -> Word
new_key, new_word, new_sentence = False, True, False
brief = messages.KEY_ECHO_WORD_BRIEF
full = messages.KEY_ECHO_WORD_FULL
elif not key and word and not sentence:
# Word -> Sentence
new_key, new_word, new_sentence = False, False, True
brief = messages.KEY_ECHO_SENTENCE_BRIEF
full = messages.KEY_ECHO_SENTENCE_FULL
elif not key and not word and sentence:
# Sentence -> All
new_key, new_word, new_sentence = True, True, True
brief = messages.KEY_ECHO_KEY_AND_WORD_BRIEF
full = messages.KEY_ECHO_KEY_AND_WORD_FULL
else:
# All -> None
new_key, new_word, new_sentence = False, False, False
brief = messages.KEY_ECHO_NONE_BRIEF
full = messages.KEY_ECHO_NONE_FULL
# Apply new settings
self._settings_manager.setSetting("enableKeyEcho", new_key)
self._settings_manager.setSetting("enableEchoByWord", new_word)
self._settings_manager.setSetting("enableEchoBySentence", new_sentence)
if script is not None:
script.presentMessage(full, brief)
+1
View File
@@ -37,6 +37,7 @@ gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
from . import cmdnames
from . import dbus_service
from . import debug
from . import guilabels
from . import input_event
+332
View File
@@ -0,0 +1,332 @@
#!/usr/bin/env python3
# Cthulhu
#
# Copyright 2005-2008 Sun Microsystems Inc.
# Copyright 2011-2025 Igalia, S.L.
# Copyright 2025 Stormux <storm_dragon@stormux.org>
#
# 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.
"""Provides typing echo support with D-Bus controls."""
__id__ = "$Id$"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
"Copyright (c) 2011-2025 Igalia, S.L."
__license__ = "LGPL"
import string
from typing import TYPE_CHECKING
from . import braille
from . import cmdnames
from . import dbus_service
from . import debug
from . import input_event
from . import keybindings
from . import messages
from . import settings
from . import settings_manager
from . import speech
if TYPE_CHECKING:
from . import default
_settings_manager = settings_manager.getManager()
class TypingEchoPresenter:
"""Provides typing echo functionality with D-Bus remote control support."""
def __init__(self):
"""Initialize the typing echo presenter."""
debug.printMessage(debug.LEVEL_INFO, "TYPING ECHO PRESENTER: Initializing", True)
# D-Bus getters and setters for key echo settings
@dbus_service.getter
def get_key_echo_enabled(self) -> bool:
"""Returns whether echo of key presses is enabled."""
return _settings_manager.getSetting('enableKeyEcho')
@dbus_service.setter
def set_key_echo_enabled(self, value: bool) -> bool:
"""Sets whether echo of key presses is enabled."""
try:
_settings_manager.setSetting('enableKeyEcho', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting key echo: {e}", True)
return False
@dbus_service.getter
def get_character_echo_enabled(self) -> bool:
"""Returns whether echo of inserted characters is enabled."""
return _settings_manager.getSetting('enableEchoByCharacter')
@dbus_service.setter
def set_character_echo_enabled(self, value: bool) -> bool:
"""Sets whether echo of inserted characters is enabled."""
try:
_settings_manager.setSetting('enableEchoByCharacter', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting character echo: {e}", True)
return False
@dbus_service.getter
def get_word_echo_enabled(self) -> bool:
"""Returns whether word echo is enabled."""
return _settings_manager.getSetting('enableEchoByWord')
@dbus_service.setter
def set_word_echo_enabled(self, value: bool) -> bool:
"""Sets whether word echo is enabled."""
try:
_settings_manager.setSetting('enableEchoByWord', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting word echo: {e}", True)
return False
@dbus_service.getter
def get_sentence_echo_enabled(self) -> bool:
"""Returns whether sentence echo is enabled."""
return _settings_manager.getSetting('enableEchoBySentence')
@dbus_service.setter
def set_sentence_echo_enabled(self, value: bool) -> bool:
"""Sets whether sentence echo is enabled."""
try:
_settings_manager.setSetting('enableEchoBySentence', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting sentence echo: {e}", True)
return False
@dbus_service.getter
def get_alphabetic_keys_enabled(self) -> bool:
"""Returns whether alphabetic keys will be echoed when key echo is enabled."""
return _settings_manager.getSetting('enableAlphabeticKeys')
@dbus_service.setter
def set_alphabetic_keys_enabled(self, value: bool) -> bool:
"""Sets whether alphabetic keys will be echoed when key echo is enabled."""
try:
_settings_manager.setSetting('enableAlphabeticKeys', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting alphabetic keys: {e}", True)
return False
@dbus_service.getter
def get_numeric_keys_enabled(self) -> bool:
"""Returns whether numeric keys will be echoed when key echo is enabled."""
return _settings_manager.getSetting('enableNumericKeys')
@dbus_service.setter
def set_numeric_keys_enabled(self, value: bool) -> bool:
"""Sets whether numeric keys will be echoed when key echo is enabled."""
try:
_settings_manager.setSetting('enableNumericKeys', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting numeric keys: {e}", True)
return False
@dbus_service.getter
def get_punctuation_keys_enabled(self) -> bool:
"""Returns whether punctuation keys will be echoed when key echo is enabled."""
return _settings_manager.getSetting('enablePunctuationKeys')
@dbus_service.setter
def set_punctuation_keys_enabled(self, value: bool) -> bool:
"""Sets whether punctuation keys will be echoed when key echo is enabled."""
try:
_settings_manager.setSetting('enablePunctuationKeys', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting punctuation keys: {e}", True)
return False
@dbus_service.getter
def get_space_enabled(self) -> bool:
"""Returns whether space key will be echoed when key echo is enabled."""
return _settings_manager.getSetting('enableSpace')
@dbus_service.setter
def set_space_enabled(self, value: bool) -> bool:
"""Sets whether space key will be echoed when key echo is enabled."""
try:
_settings_manager.setSetting('enableSpace', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting space key: {e}", True)
return False
@dbus_service.getter
def get_modifier_keys_enabled(self) -> bool:
"""Returns whether modifier keys will be echoed when key echo is enabled."""
return _settings_manager.getSetting('enableModifierKeys')
@dbus_service.setter
def set_modifier_keys_enabled(self, value: bool) -> bool:
"""Sets whether modifier keys will be echoed when key echo is enabled."""
try:
_settings_manager.setSetting('enableModifierKeys', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting modifier keys: {e}", True)
return False
@dbus_service.getter
def get_function_keys_enabled(self) -> bool:
"""Returns whether function keys will be echoed when key echo is enabled."""
return _settings_manager.getSetting('enableFunctionKeys')
@dbus_service.setter
def set_function_keys_enabled(self, value: bool) -> bool:
"""Sets whether function keys will be echoed when key echo is enabled."""
try:
_settings_manager.setSetting('enableFunctionKeys', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting function keys: {e}", True)
return False
@dbus_service.getter
def get_action_keys_enabled(self) -> bool:
"""Returns whether action keys will be echoed when key echo is enabled."""
return _settings_manager.getSetting('enableActionKeys')
@dbus_service.setter
def set_action_keys_enabled(self, value: bool) -> bool:
"""Sets whether action keys will be echoed when key echo is enabled."""
try:
_settings_manager.setSetting('enableActionKeys', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting action keys: {e}", True)
return False
@dbus_service.getter
def get_navigation_keys_enabled(self) -> bool:
"""Returns whether navigation keys will be echoed when key echo is enabled."""
return _settings_manager.getSetting('enableNavigationKeys')
@dbus_service.setter
def set_navigation_keys_enabled(self, value: bool) -> bool:
"""Sets whether navigation keys will be echoed when key echo is enabled."""
try:
_settings_manager.setSetting('enableNavigationKeys', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting navigation keys: {e}", True)
return False
@dbus_service.getter
def get_diacritical_keys_enabled(self) -> bool:
"""Returns whether diacritical keys will be echoed when key echo is enabled."""
return _settings_manager.getSetting('enableDiacriticalKeys')
@dbus_service.setter
def set_diacritical_keys_enabled(self, value: bool) -> bool:
"""Sets whether diacritical keys will be echoed when key echo is enabled."""
try:
_settings_manager.setSetting('enableDiacriticalKeys', value)
return True
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting diacritical keys: {e}", True)
return False
@dbus_service.command
def cycle_key_echo(self, script: 'default.Script', event=None):
"""Cycles through key echo modes."""
if not _settings_manager.getSetting('enableKeyEcho'):
_settings_manager.setSetting('enableKeyEcho', True)
script.presentMessage(messages.KEY_ECHO_ENABLED)
else:
_settings_manager.setSetting('enableKeyEcho', False)
script.presentMessage(messages.KEY_ECHO_DISABLED)
return True
def should_echo_keyboard_event(self, event: input_event.KeyboardEvent) -> bool:
"""Returns whether the given keyboard event should be echoed."""
if not _settings_manager.getSetting('enableKeyEcho'):
return False
if event.event_string in ["shift", "control", "alt", "meta"]:
return _settings_manager.getSetting('enableModifierKeys')
if event.event_string.startswith("f") and event.event_string[1:].isdigit():
return _settings_manager.getSetting('enableFunctionKeys')
if event.event_string in ["return", "enter", "tab", "escape", "backspace", "delete"]:
return _settings_manager.getSetting('enableActionKeys')
if event.event_string in ["up", "down", "left", "right", "home", "end", "page_up", "page_down"]:
return _settings_manager.getSetting('enableNavigationKeys')
if event.event_string == "space":
return _settings_manager.getSetting('enableSpace')
if len(event.event_string) == 1:
char = event.event_string
if char.isalpha():
return _settings_manager.getSetting('enableAlphabeticKeys')
elif char.isdigit():
return _settings_manager.getSetting('enableNumericKeys')
elif char in string.punctuation:
return _settings_manager.getSetting('enablePunctuationKeys')
return False
def is_character_echoable(self, event: input_event.KeyboardEvent) -> bool:
"""Returns True if the script will echo this event as part of character echo."""
if not _settings_manager.getSetting('enableEchoByCharacter'):
return False
# Character echo is for printable characters being inserted
if len(event.event_string) == 1 and event.event_string.isprintable():
return True
return False
def echo_keyboard_event(self, script: 'default.Script', event: input_event.KeyboardEvent) -> None:
"""Presents the KeyboardEvent event."""
if self.should_echo_keyboard_event(event):
if event.event_string == "space":
script.presentMessage(messages.SPACE)
elif event.event_string == "tab":
script.presentMessage(messages.TAB)
elif event.event_string == "return" or event.event_string == "enter":
script.presentMessage(messages.ENTER)
elif event.event_string == "backspace":
script.presentMessage(messages.BACKSPACE)
elif event.event_string == "delete":
script.presentMessage(messages.DELETE)
else:
# For simple characters and other keys, just speak the event string
script.presentMessage(event.event_string)
# Global instance
_manager = None
def getManager():
"""Get the typing echo presenter manager."""
global _manager
if not _manager:
_manager = TypingEchoPresenter()
return _manager
+477
View File
@@ -0,0 +1,477 @@
#!/usr/bin/python
# generate_dbus_documentation.py
#
# Generate markdown documentation for Cthulhu's D-Bus remote controller.
# Must be run while Cthulhu is active with the D-Bus service enabled.
#
# Copyright 2025 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# 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.
"""Generate markdown documentation for Cthulhu's D-Bus remote controller."""
import os
import sys
from dasbus.connection import SessionMessageBus
from dasbus.error import DBusError
SERVICE_NAME = "org.stormux.Cthulhu.Service"
SERVICE_PATH = "/org/stormux/Cthulhu/Service"
def get_system_commands(proxy):
"""Get system-level commands from the main service interface."""
try:
commands = proxy.ListCommands()
return sorted(commands, key=lambda x: x[0])
except DBusError as e:
print(f"Error getting system commands: {e}", file=sys.stderr)
return []
def get_modules(proxy):
"""Get list of registered modules."""
try:
modules = proxy.ListModules()
return sorted(modules)
except DBusError as e:
print(f"Error getting modules: {e}", file=sys.stderr)
return []
def get_module_info(bus, module_name):
"""Get all commands, parameterized commands, getters, and setters for a module."""
object_path = f"{SERVICE_PATH}/{module_name}"
try:
module_proxy = bus.get_proxy(SERVICE_NAME, object_path)
commands = module_proxy.ListCommands()
parameterized_commands = module_proxy.ListParameterizedCommands()
getters = module_proxy.ListRuntimeGetters()
setters = module_proxy.ListRuntimeSetters()
return {
"commands": sorted(commands, key=lambda x: x[0]),
"parameterized_commands": sorted(parameterized_commands, key=lambda x: x[0]),
"getters": sorted(getters, key=lambda x: x[0]),
"setters": sorted(setters, key=lambda x: x[0])
}
except DBusError as e:
print(f"Error getting info for module {module_name}: {e}", file=sys.stderr)
return {
"commands": [],
"parameterized_commands": [],
"getters": [],
"setters": []
}
def format_system_commands(commands):
"""Format system-level commands as markdown."""
lines = []
lines.append("## Service-Level Commands")
lines.append("")
lines.append(
"These commands are available directly on the main service object "
"at `/org/stormux/Cthulhu/Service`."
)
lines.append("")
for name, description in commands:
lines.append(f"- **`{name}`:** {description}")
lines.append("")
return "\n".join(lines)
def _group_structural_navigator_commands(commands):
"""Group structural navigator commands by object type."""
groups = {}
other = []
def normalize_obj_type(obj_type): # pylint: disable=too-many-return-statements
"""Convert to a canonical form for grouping."""
# Group all heading commands together. Ditto for link commands.
if obj_type.startswith("Heading"):
return "Heading"
if "Link" in obj_type:
return "Link"
# Common plural patterns
if obj_type.endswith("ies"): # Entries -> Entry
return obj_type[:-3] + "y"
if obj_type.endswith("xes"): # Checkboxes -> Checkbox
return obj_type[:-2]
if obj_type.endswith("shes") or obj_type.endswith("ches"): # Matches -> Match
return obj_type[:-2]
if obj_type.endswith("s") and not obj_type.endswith("ss"):
return obj_type[:-1]
return obj_type
for name, description in commands:
# Extract the object type from command names like NextHeading, ListButtons, etc.
if name.startswith("Next") or name.startswith("Previous"):
prefix = "Next" if name.startswith("Next") else "Previous"
obj_type = name[len(prefix):]
normalized = normalize_obj_type(obj_type)
if normalized not in groups:
if normalized == "Heading":
display = "Headings"
elif not obj_type.endswith("s"):
display = obj_type + "s"
else:
display = obj_type
groups[normalized] = {"commands": [], "display_name": display}
groups[normalized]["commands"].append((name, description))
elif name.startswith("List"):
obj_type = name[4:]
normalized = normalize_obj_type(obj_type)
if normalized not in groups:
display = "Headings" if normalized == "Heading" else obj_type
groups[normalized] = {"commands": [], "display_name": display}
else:
current_display = groups[normalized]["display_name"]
if not current_display.endswith("s") or normalized == "Heading":
new_display = "Headings" if normalized == "Heading" else obj_type
groups[normalized]["display_name"] = new_display
groups[normalized]["commands"].append((name, description))
else:
# Other commands like ContainerStart, CycleMode
other.append((name, description))
return groups, other
# pylint: disable-next=too-many-branches,too-many-statements,too-many-locals
def format_module_commands(module_name, info):
"""Format module-level commands as markdown."""
lines = []
lines.append(f"### {module_name}")
lines.append("")
lines.append(f"**Object Path:** `/org/stormux/Cthulhu/Service/{module_name}`")
lines.append("")
# Commands - special handling for certain modules
if info["commands"]:
lines.append("#### Commands")
lines.append("")
lines.append("**Method:** `org.stormux.Cthulhu.Module.ExecuteCommand`")
lines.append("")
lines.append(
"**Parameters:** `CommandName` (string), "
"[`NotifyUser`](README-REMOTE-CONTROLLER.md#user-notification-applicability) (boolean)"
)
lines.append("")
if module_name == "SpeechAndVerbosityManager":
# Group related increase/decrease commands
def sort_speech_commands(cmd_tuple):
name, _ = cmd_tuple
# Define groups and their order
groups = {
"Rate": 0, "Pitch": 1, "Volume": 2,
}
# Check if it's an Increase/Decrease command
for group_name, group_order in groups.items():
if group_name in name:
if name.startswith("Increase"):
return (group_order, 0, name)
if name.startswith("Decrease"):
return (group_order, 1, name)
# Other commands go at the end, sorted alphabetically
return (100, 0, name)
sorted_commands = sorted(info["commands"], key=sort_speech_commands)
for name, description in sorted_commands:
lines.append(f"- **`{name}`:** {description}")
lines.append("")
elif module_name == "FlatReviewPresenter":
# Group Go commands at the top
def sort_flat_review_commands(cmd_tuple):
name, _ = cmd_tuple
if name.startswith("Go"):
return (0, name)
return (1, name)
sorted_commands = sorted(info["commands"], key=sort_flat_review_commands)
for name, description in sorted_commands:
lines.append(f"- **`{name}`:** {description}")
lines.append("")
elif module_name == "CaretNavigator":
# Group related navigation commands
def sort_caret_commands(cmd_tuple):
name, _ = cmd_tuple
# Define groups for Character, Word, Line, File
if "Character" in name:
group = 0
elif "Word" in name:
group = 1
elif "Line" in name:
group = 2
elif "File" in name:
group = 3
else:
group = 100
# Within each group: Next, Previous, Start, End, Toggle
if name.startswith("Next"):
order = 0
elif name.startswith("Previous"):
order = 1
elif name.startswith("Start"):
order = 0
elif name.startswith("End"):
order = 1
elif name.startswith("Toggle"):
order = 2
else:
order = 99
return (group, order, name)
sorted_commands = sorted(info["commands"], key=sort_caret_commands)
for name, description in sorted_commands:
lines.append(f"- **`{name}`:** {description}")
lines.append("")
elif module_name == "StructuralNavigator":
groups, other = _group_structural_navigator_commands(info["commands"])
# Show grouped commands by object type first
for obj_type in sorted(groups.keys()):
cmds = groups[obj_type]
display_name = cmds["display_name"]
lines.append(f"##### {display_name}")
lines.append("")
# Sort commands in a specific order: Next, Previous, List, grouped by variant
def sort_key(cmd_tuple):
name, _ = cmd_tuple
# Extract base command and any suffix (like Level1, UnvisitedLink, etc.)
if name.startswith("Next"):
prefix_order = 0
suffix = name[4:] # Remove "Next"
elif name.startswith("Previous"):
prefix_order = 1
suffix = name[8:] # Remove "Previous"
elif name.startswith("List"):
prefix_order = 2
suffix = name[4:] # Remove "List"
else:
prefix_order = 3
suffix = name
# Extract level number or variant for proper ordering
level_or_variant = 0
if "Level" in suffix:
# For headings: extract level number
try:
level_or_variant = int(suffix.split("Level")[1])
except (IndexError, ValueError):
pass
elif "Unvisited" in suffix:
# For links: Unvisited comes after base
level_or_variant = 1
elif "Visited" in suffix:
# For links: Visited comes after Unvisited
level_or_variant = 2
return (level_or_variant, prefix_order, name)
sorted_commands = sorted(cmds["commands"], key=sort_key)
for name, desc in sorted_commands:
lines.append(f"- **`{name}`:** {desc}")
lines.append("")
# Show uncategorized commands at the end
if other:
lines.append("##### Other")
lines.append("")
for name, description in other:
lines.append(f"- **`{name}`:** {description}")
lines.append("")
else:
for name, description in info["commands"]:
lines.append(f"- **`{name}`:** {description}")
lines.append("")
# Parameterized Commands
if info["parameterized_commands"]:
lines.append("#### Parameterized Commands")
lines.append("")
lines.append("**Method:** `org.stormux.Cthulhu.Module.ExecuteParameterizedCommand`")
lines.append("")
for name, description, parameters in info["parameterized_commands"]:
param_list = ", ".join([f"`{pname}` ({ptype})" for pname, ptype in parameters])
if param_list:
lines.append(f"- **`{name}`:** {description} Parameters: {param_list}")
else:
lines.append(f"- **`{name}`:** {description}")
lines.append("")
# Runtime Settings (combine getters and setters)
if info["getters"] or info["setters"]:
lines.append("#### Settings")
lines.append("")
lines.append("**Methods:** `org.stormux.Cthulhu.Module.ExecuteRuntimeGetter` / `org.stormux.Cthulhu.Module.ExecuteRuntimeSetter`")
lines.append("")
lines.append(
"**Parameters:** `PropertyName` (string), "
"`Value` (variant, setter only)"
)
lines.append("")
# Build a merged dictionary of properties
# Prefer setter descriptions as they may contain range/default info
properties = {}
for name, description in info["getters"]:
properties[name] = {"description": description, "getter": True, "setter": False}
for name, description in info["setters"]:
if name in properties:
properties[name]["setter"] = True
properties[name]["description"] = description
else:
properties[name] = {"description": description, "getter": False, "setter": True}
# Output sorted properties with annotations
for name in sorted(properties.keys()):
prop = properties[name]
description = prop["description"]
annotation = ""
if prop["getter"] and not prop["setter"]:
annotation = " (getter only)"
elif prop["setter"] and not prop["getter"]:
annotation = " (setter only)"
else:
# Both getter and setter - change "Returns" or "Sets" to "Gets/Sets"
if description.startswith("Returns "):
description = description.replace("Returns ", "Gets/Sets ", 1)
elif description.startswith("Sets "):
description = description.replace("Sets ", "Gets/Sets ", 1)
lines.append(f"- **`{name}`:** {description}{annotation}")
lines.append("")
return "\n".join(lines)
def generate_documentation():
"""Generate the complete documentation."""
try:
bus = SessionMessageBus()
except DBusError as e:
print(f"Error connecting to D-Bus: {e}", file=sys.stderr)
return None
try:
proxy = bus.get_proxy(SERVICE_NAME, SERVICE_PATH)
except DBusError as e:
print(f"Error connecting to Cthulhu service: {e}", file=sys.stderr)
print("Make sure Cthulhu is running with the D-Bus service enabled.", file=sys.stderr)
return None
system_commands = get_system_commands(proxy)
modules = get_modules(proxy)
module_infos = {}
for module_name in modules:
module_infos[module_name] = get_module_info(bus, module_name)
total_commands = len(system_commands)
total_commands += sum(len(info["commands"]) for info in module_infos.values())
total_commands += sum(len(info["parameterized_commands"]) for info in module_infos.values())
total_getters = sum(len(info["getters"]) for info in module_infos.values())
total_setters = sum(len(info["setters"]) for info in module_infos.values())
lines = []
lines.append("# Cthulhu D-Bus Service Commands Reference")
lines.append("")
lines.append(
f"This document lists all commands ({total_commands}), "
f"runtime getters ({total_getters}), and runtime setters ({total_setters}) available"
)
lines.append("via Cthulhu's D-Bus Remote Controller interface.")
lines.append("")
lines.append("The service can be accessed at:")
lines.append("")
lines.append("- **Service Name:** `org.stormux.Cthulhu.Service`")
lines.append("- **Main Object Path:** `/org/stormux/Cthulhu/Service`")
lines.append("- **Module Object Paths:** `/org/stormux/Cthulhu/Service/ModuleName`")
lines.append("")
lines.append(
"Additional information about using the remote controller can be found in "
"[README-REMOTE-CONTROLLER.md](README-REMOTE-CONTROLLER.md)."
)
lines.append("")
lines.append("---")
lines.append("")
# System commands
lines.append(format_system_commands(system_commands))
lines.append("---")
lines.append("")
# Module commands
lines.append("## Modules")
lines.append("")
lines.append(
"Each module exposes commands, getters, and setters on its object "
"at `/org/stormux/Cthulhu/Service/ModuleName`."
)
lines.append("")
for module_name in modules:
lines.append(format_module_commands(module_name, module_infos[module_name]))
lines.append("---")
lines.append("")
return "\n".join(lines)
def main():
"""Main entry point."""
# Write to parent directory since script is in tools/
script_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(script_dir)
output_file = os.path.join(parent_dir, "REMOTE-CONTROLLER-COMMANDS.md")
print("Generating D-Bus documentation...", file=sys.stderr)
documentation = generate_documentation()
if documentation is None:
print("Failed to generate documentation.", file=sys.stderr)
return 1
try:
with open(output_file, "w", encoding="utf-8") as f:
f.write(documentation)
print(f"Documentation written to {output_file}", file=sys.stderr)
return 0
except IOError as e:
print(f"Error writing to {output_file}: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())