Compare commits
46 Commits
2025.02.26
...
f18c31df6c
| Author | SHA1 | Date | |
|---|---|---|---|
| f18c31df6c | |||
| ca0e3b5987 | |||
| 0009d90a68 | |||
| 2c2efc56f0 | |||
| 3dca3e5b23 | |||
| 27c35939b1 | |||
| 7e87ebf04b | |||
| ec6c135581 | |||
| 998c63cc71 | |||
| 26c6e32c59 | |||
| 97e2da614b | |||
| 0930a86ce7 | |||
| 1b9a9a90b1 | |||
| a742c12cd8 | |||
| 8d50003730 | |||
| 4c8c8d896d | |||
| bd151c7cec | |||
| 6f4784daed | |||
| 8c471adfa4 | |||
| 77065c55b4 | |||
| 7f75c231e1 | |||
| 4672592dba | |||
| 6b84a8e9bc | |||
| 3212962a5b | |||
| 9e9cb883e7 | |||
| 7a12992b88 | |||
| 26a8c8cf86 | |||
| bbfd2790a9 | |||
| 6edb743c23 | |||
| a1fcee9a45 | |||
| 4e6e6e2d17 | |||
| 7a87fb51bb | |||
| 676c2b07a9 | |||
| 2dda73ac87 | |||
| f68a1af223 | |||
| 5ab66f6978 | |||
| 2cc2fda28c | |||
| c99d0f6ee1 | |||
| 1552b962a1 | |||
| 09391bfe84 | |||
| e76ca9889a | |||
| 73206ce393 | |||
| 4966b87ba1 | |||
| 145cab6221 | |||
| e46926f145 | |||
| 8cd50c5070 |
@@ -6,3 +6,4 @@ dist/
|
|||||||
build/
|
build/
|
||||||
*.kate-swp
|
*.kate-swp
|
||||||
.directory
|
.directory
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
# Fenrir screen reader
|
# Fenrir Screen Reader Credits
|
||||||
|
|
||||||
|
|
||||||
## Developers
|
## Current Maintainer
|
||||||
|
|
||||||
* Storm Dragon: Project leader
|
* **Storm Dragon** - Project leader and maintainer
|
||||||
* Jeremiah: Coder.
|
|
||||||
|
|
||||||
|
## Current Contributors
|
||||||
|
|
||||||
|
* **Jeremiah** - Developer
|
||||||
|
|
||||||
|
|
||||||
## Previous Developers
|
## Previous Developers
|
||||||
|
|
||||||
* Chrys: coder.
|
* **Chrys** - Original creator and main developer
|
||||||
|
|
||||||
|
|
||||||
## Special thanks to:
|
## Special Thanks
|
||||||
|
|
||||||
* F123 Consulting for suggestions, some funding, and endless testing.
|
* **F123 Consulting** - Suggestions, funding, and extensive testing
|
||||||
* Stormux for continuation of the project.
|
* **Stormux Community** - Continuation of the project and ongoing support
|
||||||
|
* **All contributors** - Bug reports, feature requests, and community support
|
||||||
|
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
* IRC: irc.stormux.org #stormux
|
||||||
|
* Email list: stormux+subscribe@groups.io
|
||||||
|
* Wiki: https://git.stormux.org/storm/fenrir/wiki
|
||||||
|
|||||||
@@ -1,69 +1,97 @@
|
|||||||
# Fenrir
|
# Fenrir
|
||||||
|
|
||||||
A modern, modular, flexible and fast console screenreader.
|
A modern, modular, flexible and fast console screen reader.
|
||||||
It should run on any operating system. If you want to help, or write drivers to make it work on other systems, just let me know.
|
It should run on any operating system. If you want to help, or write drivers to make it work on other systems, just let me know.
|
||||||
This software is licensed under the LGPL v3.
|
This software is licensed under the LGPL v3.
|
||||||
|
|
||||||
|
**Current maintainer:** Storm Dragon
|
||||||
|
**Previous developer:** Chrys
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Multiple Interface Support**: Works in Linux TTY, and terminal emulators
|
||||||
|
- **Flexible Driver System**: Modular architecture with multiple drivers for speech, sound, input, and screen
|
||||||
|
- **Review Mode**: Navigate and review screen content without moving the edit cursor
|
||||||
|
- **Multiple Clipboard Support**: Manage multiple clipboard entries
|
||||||
|
- **Configurable Key Bindings**: Desktop and laptop keyboard layouts
|
||||||
|
- **Sound Icons**: Audio feedback for various events
|
||||||
|
- **Spell Checking**: Built-in spell checker with word management
|
||||||
|
- **Language Support**: Multiple speech synthesis languages and voices
|
||||||
|
- **Bookmark System**: Quick access to specific screen areas
|
||||||
|
- **Auto-announcement**: Automatic reading of incoming text and time announcements
|
||||||
|
- **Tutorial Mode**: Built-in help system for learning keyboard shortcuts
|
||||||
|
|
||||||
|
|
||||||
## OS Requirements
|
## OS Requirements
|
||||||
|
|
||||||
- Linux (ptyDriver, vcsaDriver, evdevDriver)
|
- Linux (ptyDriver, vcsaDriver, evdevDriver) - Primary platform with full support
|
||||||
- macOS (ptyDriver)
|
- macOS (ptyDriver) - Limited support
|
||||||
- BSD (ptyDriver)
|
- BSD (ptyDriver) - Limited support
|
||||||
- Windows (ptyDriver)
|
- Windows (ptyDriver) - Limited support
|
||||||
|
|
||||||
|
|
||||||
## Core Requirements
|
## Core Requirements
|
||||||
|
|
||||||
- python3 >= 3.3
|
- Python 3 >= 3.9 (recommended 3.13+)
|
||||||
- screen, input, speech, sound drivers dependencies see "Features, Drivers, Extras".
|
- Screen, input, speech, sound driver dependencies (see "Features, Drivers, Extras" section)
|
||||||
|
- For full functionality on Linux: evdev, speech-dispatcher, sox
|
||||||
|
|
||||||
|
|
||||||
## Features, Drivers, Extras, Dependencies
|
## Features, Drivers, Extras, Dependencies
|
||||||
|
|
||||||
### Input Drivers:
|
### Input Drivers:
|
||||||
1. "evdevDriver" input driver for linux evdev
|
1. **evdevDriver** - Linux evdev input driver (recommended for Linux)
|
||||||
- python-evdev >=0.6.3 (This is commonly referred to as python3-evdev by your distribution)
|
- python-evdev >=0.6.3 (This is commonly referred to as python3-evdev by your distribution)
|
||||||
- python-pyudev
|
- python-pyudev
|
||||||
- loaded uinput kernel module
|
- loaded uinput kernel module
|
||||||
- ReadWrite permission
|
- ReadWrite permission:
|
||||||
- /dev/input
|
- /dev/input
|
||||||
- /dev/uinput
|
- /dev/uinput
|
||||||
2. "ptyDriver" terminal emulation input driver
|
2. **ptyDriver** - Terminal emulation input driver (cross-platform)
|
||||||
- python-pyte
|
- python-pyte
|
||||||
|
3. **atspiDriver** - AT-SPI input driver for desktop environments
|
||||||
|
- python-pyatspi2
|
||||||
|
|
||||||
|
### Remote Drivers:
|
||||||
|
1. **unixDriver** - Unix socket remote control (default)
|
||||||
|
- socat (for command-line interaction)
|
||||||
|
2. **tcpDriver** - TCP socket remote control (localhost only)
|
||||||
|
- netcat or telnet (for command-line interaction)
|
||||||
|
|
||||||
|
|
||||||
### Screen Drivers:
|
### Screen Drivers:
|
||||||
|
|
||||||
1. "vcsaDriver" screen driver for linux VCSA devices
|
1. **vcsaDriver** - Linux VCSA devices driver (recommended for Linux TTY)
|
||||||
- python-dbus
|
- python-dbus
|
||||||
- Read permission to the following files and services:
|
- Read permission to the following files and services:
|
||||||
- /sys/devices/virtual/tty/tty0/active
|
- /sys/devices/virtual/tty/tty0/active
|
||||||
- /dev/tty[1-64]
|
- /dev/tty[1-64]
|
||||||
- /dev/vcsa[1-64]
|
- /dev/vcsa[1-64]
|
||||||
- read logind DBUS
|
- read logind DBUS
|
||||||
2. "ptyDriver" terminal emulation driver
|
2. **ptyDriver** - Terminal emulation driver (cross-platform)
|
||||||
- python-pyte
|
- python-pyte
|
||||||
|
|
||||||
|
|
||||||
### Speech Drivers:
|
### Speech Drivers:
|
||||||
|
|
||||||
1. "genericDriver" (default) speech driver for sound as subprocess:
|
1. **speechdDriver** - Speech-dispatcher driver (recommended)
|
||||||
- espeak or espeak-ng
|
- Speech-dispatcher
|
||||||
2. "speechdDriver" speech driver for Speech-dispatcher:
|
- python-speechd
|
||||||
- Speech-dispatcher
|
2. **genericDriver** - Generic subprocess speech driver
|
||||||
- python-speechd
|
- espeak or espeak-ng (or any TTS command)
|
||||||
3. "emacspeakDriver" speech driver for emacspeak
|
3. **debugDriver** - Debug speech driver for testing
|
||||||
- emacspeak
|
- No dependencies
|
||||||
|
|
||||||
|
|
||||||
### Sound Drivers:
|
### Sound Drivers:
|
||||||
|
|
||||||
1. "genericDriver" (default) sound driver for sound as subprocess:
|
1. **genericDriver** (default) - Generic subprocess sound driver
|
||||||
- Sox
|
- Sox with opus support (recommended)
|
||||||
2. "gstreamerDriver" sound driver for gstreamer
|
2. **gstreamerDriver** - GStreamer sound driver
|
||||||
- gstreamer >=1.0
|
- gstreamer >=1.0
|
||||||
- GLib
|
- GLib
|
||||||
|
3. **debugDriver** - Debug sound driver for testing
|
||||||
|
- No dependencies
|
||||||
|
|
||||||
|
|
||||||
## Extras:
|
## Extras:
|
||||||
@@ -91,16 +119,353 @@ If there is a package for your distrobution of choice, please let us know so we
|
|||||||
- You can also just run it from Git without installing:
|
- You can also just run it from Git without installing:
|
||||||
Requires root privileges
|
Requires root privileges
|
||||||
|
|
||||||
cd src/fenrir/
|
cd src/
|
||||||
sudo ./fenrir
|
sudo ./fenrir
|
||||||
|
|
||||||
Settings "settings.conf" is located in the "config" directory or after installation in /etc/fenrir/settings.
|
Settings are located in:
|
||||||
Take care to use drivers from the config matching your installed drivers.
|
- **After installation**: `/etc/fenrir/settings/settings.conf`
|
||||||
By default it uses:
|
- **Development**: `config/settings/settings.conf`
|
||||||
- sound driver: genericDriver (via sox, could configured in settings.conf)
|
|
||||||
- speech driver: genericDriver (via espeak or espeak-ng, could configured in settings.conf)
|
|
||||||
- input driver: evdevDriver
|
|
||||||
|
|
||||||
|
By default Fenrir uses:
|
||||||
|
- **Sound driver**: genericDriver (via sox)
|
||||||
|
- **Speech driver**: speechdDriver (via speech-dispatcher)
|
||||||
|
- **Input driver**: evdevDriver (Linux) or ptyDriver (other platforms)
|
||||||
|
- **Screen driver**: vcsaDriver (Linux TTY) or ptyDriver (terminal emulation)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
1. **Start Fenrir**:
|
||||||
|
```bash
|
||||||
|
sudo systemctl start fenrir # If installed as service
|
||||||
|
# OR
|
||||||
|
sudo fenrir # Run directly
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Basic Navigation**:
|
||||||
|
- **Fenrir Key**: By default `Insert`, `Keypad Insert`, or `Meta/Super` key
|
||||||
|
- **Tutorial Mode**: `Fenrir + H` to learn all commands interactively
|
||||||
|
- **Quit Fenrir**: `Fenrir + Q`
|
||||||
|
|
||||||
|
3. **Essential Commands**:
|
||||||
|
- `Ctrl` - Stop speech (shut up)
|
||||||
|
- `Fenrir + Keypad 5` - Read current screen
|
||||||
|
- `Keypad 8` - Read current line
|
||||||
|
- `Keypad 5` - Read current word
|
||||||
|
- `Keypad 2` - Read current character
|
||||||
|
- `Fenrir + T` - Announce time
|
||||||
|
- `Fenrir + S` - Spell check current word
|
||||||
|
|
||||||
|
### Keyboard Layouts
|
||||||
|
|
||||||
|
Fenrir supports two main keyboard layouts:
|
||||||
|
|
||||||
|
- **Desktop Layout**: Uses numeric keypad for navigation (recommended for desktop users)
|
||||||
|
- **Laptop Layout**: Alternative bindings for keyboards without numeric keypad
|
||||||
|
|
||||||
|
Configure in `/etc/fenrir/settings/settings.conf`:
|
||||||
|
```ini
|
||||||
|
[keyboard]
|
||||||
|
keyboardLayout=desktop # or 'laptop'
|
||||||
|
```
|
||||||
|
|
||||||
|
### First Time Setup
|
||||||
|
|
||||||
|
1. **Enable Fenrir at boot**:
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable fenrir
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure audio** (if needed):
|
||||||
|
- For PulseAudio: Run configure_pulse.sh script (see below)
|
||||||
|
- For PipeWire: Run configure_pipewire.sh script (see below)
|
||||||
|
|
||||||
|
3. **Test speech**:
|
||||||
|
```bash
|
||||||
|
# Test speech-dispatcher directly
|
||||||
|
sudo spd-say "Hello World"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Remote Control
|
||||||
|
|
||||||
|
Fenrir includes a powerful remote control system that allows external applications and scripts to control Fenrir through Unix sockets or TCP connections. This is particularly useful for automation, integration with other applications, or providing alternative control methods.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Enable remote control in `/etc/fenrir/settings/settings.conf`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[remote]
|
||||||
|
enable=True
|
||||||
|
driver=unixDriver # or tcpDriver
|
||||||
|
port=22447 # for TCP driver
|
||||||
|
socketFile= # custom socket path (optional)
|
||||||
|
enableSettingsRemote=True # allow settings changes
|
||||||
|
enableCommandRemote=True # allow command execution
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remote Drivers
|
||||||
|
|
||||||
|
1. **unixDriver** (recommended): Uses Unix domain sockets
|
||||||
|
- Socket location: `/tmp/fenrirscreenreader-deamon.sock` (TTY mode) or `/tmp/fenrirscreenreader-<pid>.sock`
|
||||||
|
- More secure, local-only access
|
||||||
|
- Works with `socat`
|
||||||
|
|
||||||
|
2. **tcpDriver**: Uses TCP sockets on localhost
|
||||||
|
- Default port: 22447
|
||||||
|
- Works with `netcat`, `telnet`, or any TCP client
|
||||||
|
- Local connections only (127.0.0.1)
|
||||||
|
|
||||||
|
### Using socat with Unix Sockets
|
||||||
|
|
||||||
|
The `socat` command provides the easiest way to send commands to Fenrir:
|
||||||
|
|
||||||
|
#### Basic Speech Control
|
||||||
|
```bash
|
||||||
|
# Interrupt current speech
|
||||||
|
echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Speak custom text
|
||||||
|
echo "command say Hello, this is a test message" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Temporarily disable speech (until next keystroke)
|
||||||
|
echo "command tempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Settings Control
|
||||||
|
```bash
|
||||||
|
# Enable highlight tracking mode
|
||||||
|
echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Change speech parameters
|
||||||
|
echo "setting set speech#rate=0.8" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
echo "setting set speech#pitch=0.6" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
echo "setting set speech#volume=0.9" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Change punctuation level (none/some/most/all)
|
||||||
|
echo "setting set general#punctuationLevel=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
echo "setting set general#punctuationLevel=none" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Voice and TTS engine control
|
||||||
|
echo "setting set speech#voice=en-us+f3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
echo "setting set speech#module=espeak-ng" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Disable sound temporarily
|
||||||
|
echo "setting set sound#enabled=False" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
echo "setting set sound#volume=0.5" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Keyboard and input settings
|
||||||
|
echo "setting set keyboard#charEchoMode=1" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
echo "setting set keyboard#wordEcho=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Screen control (ignore specific TTYs)
|
||||||
|
echo "setting set screen#ignoreScreen=1,2,3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Multiple settings at once
|
||||||
|
echo "setting set speech#rate=0.8;sound#volume=0.7;general#punctuationLevel=most" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Reset all settings to defaults
|
||||||
|
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Save current settings
|
||||||
|
echo "setting save" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
echo "setting saveas /tmp/my-fenrir-settings.conf" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Clipboard Operations
|
||||||
|
```bash
|
||||||
|
# Place text into clipboard
|
||||||
|
echo "command clipboard This text will be copied to clipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Export clipboard to file
|
||||||
|
echo "command exportclipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Window Management
|
||||||
|
```bash
|
||||||
|
# Define a window area (x1 y1 x2 y2)
|
||||||
|
echo "command window 0 0 80 24" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Reset window to full screen
|
||||||
|
echo "command resetwindow" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
#### VMenu Control
|
||||||
|
```bash
|
||||||
|
# Set virtual menu context
|
||||||
|
echo "command vmenu nano/file" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Reset virtual menu
|
||||||
|
echo "command resetvmenu" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Application Control
|
||||||
|
```bash
|
||||||
|
# Quit Fenrir
|
||||||
|
echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using TCP Driver
|
||||||
|
|
||||||
|
If using the TCP driver, replace socat commands with netcat:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using netcat
|
||||||
|
echo "command say Hello from TCP" | nc localhost 22447
|
||||||
|
|
||||||
|
# Using telnet
|
||||||
|
echo "command interrupt" | telnet localhost 22447
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remote Command Reference
|
||||||
|
|
||||||
|
#### Command Format
|
||||||
|
```
|
||||||
|
command <action> [parameters]
|
||||||
|
setting <action> [parameters]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Available Commands
|
||||||
|
|
||||||
|
**Speech Commands:**
|
||||||
|
- `command say <text>` - Speak the specified text
|
||||||
|
- `command interrupt` - Stop current speech
|
||||||
|
- `command tempdisablespeech` - Disable speech until next key press
|
||||||
|
|
||||||
|
**Clipboard Commands:**
|
||||||
|
- `command clipboard <text>` - Add text to clipboard
|
||||||
|
- `command exportclipboard` - Export clipboard to file
|
||||||
|
|
||||||
|
**Window Commands:**
|
||||||
|
- `command window <x1> <y1> <x2> <y2>` - Define window area
|
||||||
|
- `command resetwindow` - Reset to full screen
|
||||||
|
|
||||||
|
**VMenu Commands:**
|
||||||
|
- `command vmenu <menu_path>` - Set vmenu context
|
||||||
|
- `command resetvmenu` - Reset vmenu
|
||||||
|
|
||||||
|
**Application Commands:**
|
||||||
|
- `command quitapplication` - Quit Fenrir
|
||||||
|
|
||||||
|
#### Available Settings
|
||||||
|
|
||||||
|
**Settings Commands:**
|
||||||
|
- `setting set <section>#<key>=<value>` - Set configuration value
|
||||||
|
- `setting reset` - Reset all settings to defaults
|
||||||
|
- `setting save [path]` - Save current settings
|
||||||
|
- `setting saveas <path>` - Save settings to specific file
|
||||||
|
|
||||||
|
**Common Settings:**
|
||||||
|
|
||||||
|
*Speech Settings:*
|
||||||
|
- `speech#enabled=True/False` - Enable/disable speech
|
||||||
|
- `speech#rate=0.1-1.0` - Speech rate (speed)
|
||||||
|
- `speech#pitch=0.1-1.0` - Speech pitch (tone)
|
||||||
|
- `speech#volume=0.1-1.0` - Speech volume
|
||||||
|
- `speech#voice=voice_name` - Voice selection (e.g., "en-us+f3")
|
||||||
|
- `speech#module=module_name` - TTS module (e.g., "espeak-ng")
|
||||||
|
- `speech#driver=driver_name` - Speech driver (speechdDriver/genericDriver)
|
||||||
|
- `speech#autoReadIncoming=True/False` - Auto-read new text
|
||||||
|
|
||||||
|
*Sound Settings:*
|
||||||
|
- `sound#enabled=True/False` - Enable/disable sound
|
||||||
|
- `sound#volume=0.1-1.0` - Sound volume
|
||||||
|
- `sound#driver=driver_name` - Sound driver (genericDriver/gstreamerDriver)
|
||||||
|
- `sound#theme=theme_name` - Sound theme
|
||||||
|
|
||||||
|
*General Settings:*
|
||||||
|
- `general#punctuationLevel=none/some/most/all` - Punctuation verbosity
|
||||||
|
- `general#debugLevel=0-3` - Debug level
|
||||||
|
- `general#emoticons=True/False` - Enable emoticon replacement
|
||||||
|
- `general#autoSpellCheck=True/False` - Automatic spell checking
|
||||||
|
|
||||||
|
*Focus Settings:*
|
||||||
|
- `focus#cursor=True/False` - Follow text cursor
|
||||||
|
- `focus#highlight=True/False` - Follow text highlighting
|
||||||
|
|
||||||
|
*Keyboard Settings:*
|
||||||
|
- `keyboard#charEchoMode=0-2` - Character echo (0=none, 1=always, 2=capslock only)
|
||||||
|
- `keyboard#wordEcho=True/False` - Echo complete words
|
||||||
|
- `keyboard#charDeleteEcho=True/False` - Echo deleted characters
|
||||||
|
- `keyboard#interruptOnKeyPress=True/False` - Interrupt speech on key press
|
||||||
|
|
||||||
|
*Screen Settings:*
|
||||||
|
- `screen#ignoreScreen=1,2,3` - TTY screens to ignore
|
||||||
|
- `screen#autodetectIgnoreScreen=True/False` - Auto-detect screens to ignore
|
||||||
|
- `screen#screenUpdateDelay=float` - Screen update delay
|
||||||
|
|
||||||
|
*Time Settings:*
|
||||||
|
- `time#enabled=True/False` - Enable time announcements
|
||||||
|
- `time#presentTime=True/False` - Announce time
|
||||||
|
- `time#presentDate=True/False` - Announce date changes
|
||||||
|
- `time#delaySec=seconds` - Announcement interval
|
||||||
|
- `time#onMinutes=00,30` - Specific minutes to announce
|
||||||
|
|
||||||
|
### Scripting Examples
|
||||||
|
|
||||||
|
#### Bash Script for Speech Notifications
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# notify_fenrir.sh - Send notifications to Fenrir
|
||||||
|
|
||||||
|
SOCKET="/tmp/fenrirscreenreader-deamon.sock"
|
||||||
|
|
||||||
|
fenrir_say() {
|
||||||
|
echo "command say $1" | socat - UNIX-CLIENT:$SOCKET
|
||||||
|
}
|
||||||
|
|
||||||
|
fenrir_interrupt() {
|
||||||
|
echo "command interrupt" | socat - UNIX-CLIENT:$SOCKET
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage examples
|
||||||
|
fenrir_say "Build completed successfully"
|
||||||
|
fenrir_interrupt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python Integration
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import socket
|
||||||
|
import os
|
||||||
|
|
||||||
|
def send_fenrir_command(command):
|
||||||
|
"""Send command to Fenrir via Unix socket"""
|
||||||
|
socket_path = "/tmp/fenrirscreenreader-deamon.sock"
|
||||||
|
if os.path.exists(socket_path):
|
||||||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
try:
|
||||||
|
sock.connect(socket_path)
|
||||||
|
sock.send(command.encode('utf-8'))
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
send_fenrir_command("command say Processing complete")
|
||||||
|
send_fenrir_command("setting set speech#rate=0.9")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
|
||||||
|
- Unix sockets are accessible only to the user running Fenrir
|
||||||
|
- TCP driver binds only to localhost (127.0.0.1)
|
||||||
|
- Socket file permissions are set to write-only (0o222)
|
||||||
|
- Commands are processed with Fenrir's privileges
|
||||||
|
- Settings changes can be disabled via `enableSettingsRemote=False`
|
||||||
|
- Command execution can be disabled via `enableCommandRemote=False`
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
**Socket not found:**
|
||||||
|
- Verify Fenrir is running: `ps aux | grep fenrir`
|
||||||
|
- Check socket location: `/tmp/fenrirscreenreader-*`
|
||||||
|
- Ensure remote driver is enabled in settings
|
||||||
|
|
||||||
|
**Commands not working:**
|
||||||
|
- Verify `enableCommandRemote=True` in settings
|
||||||
|
- Check Fenrir debug logs: `/var/log/fenrir.log`
|
||||||
|
- Test with simple command: `echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock`
|
||||||
|
|
||||||
## Configure pulseaudio
|
## Configure pulseaudio
|
||||||
|
|
||||||
@@ -127,12 +492,64 @@ just run the configuration script twice (once as user, once as root):
|
|||||||
|
|
||||||
The script is also located in the tools directory in git
|
The script is also located in the tools directory in git
|
||||||
|
|
||||||
## localization
|
## Command Line Options
|
||||||
copy fenrir.mo translations file from fenrir/locale/your_language/LC_MESSAGES/fenrir.mo to /usr/share/locale/your_language/LC_MESSAGES/fenrir.mo
|
|
||||||
|
Fenrir supports several command-line options for different use cases:
|
||||||
|
|
||||||
|
```
|
||||||
|
fenrir [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options:
|
||||||
|
- `-h, --help` - Show help message and exit
|
||||||
|
- `-v, --version` - Show version information and exit
|
||||||
|
- `-f, --foreground` - Run in foreground (don't daemonize)
|
||||||
|
- `-s, --setting SETTING-FILE` - Path to custom settings file
|
||||||
|
- `-o, --options SECTION#SETTING=VALUE;..` - Override settings file options
|
||||||
|
- `-d, --debug` - Enable debug mode
|
||||||
|
- `-p, --print` - Print debug messages to screen
|
||||||
|
- `-e, --emulated-pty` - Use PTY emulation with escape sequences for input (enables desktop/X/Wayland usage)
|
||||||
|
- `-E, --emulated-evdev` - Use PTY emulation with evdev for input (single instance)
|
||||||
|
- `-F, --force-all-screens` - Force Fenrir to respond on all screens, ignoring ignoreScreen setting
|
||||||
|
- `-i, -I, --ignore-screen SCREEN` - Ignore specific screen(s). Can be used multiple times. Combines with existing ignore settings.
|
||||||
|
|
||||||
|
### Examples:
|
||||||
|
```bash
|
||||||
|
# Run in foreground with debug output
|
||||||
|
sudo fenrir -f -d
|
||||||
|
|
||||||
|
# Use PTY emulation for desktop use
|
||||||
|
sudo fenrir -e
|
||||||
|
|
||||||
|
# Override settings via command line
|
||||||
|
sudo fenrir -o "speech#rate=0.8;sound#volume=0.5"
|
||||||
|
|
||||||
|
# Force Fenrir to work on all screens (ignore ignoreScreen setting)
|
||||||
|
sudo fenrir -F
|
||||||
|
|
||||||
|
# Ignore specific screens
|
||||||
|
sudo fenrir --ignore-screen 1
|
||||||
|
sudo fenrir -i 1 -i 2 # Ignore screens 1 and 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Localization
|
||||||
|
Translation files are located in the `locale/` directory. To install translations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy translation file to system location
|
||||||
|
sudo cp locale/your_language/LC_MESSAGES/fenrir.mo /usr/share/locale/your_language/LC_MESSAGES/fenrir.mo
|
||||||
|
```
|
||||||
|
|
||||||
|
Available languages:
|
||||||
|
- German (de)
|
||||||
|
- Spanish (es)
|
||||||
|
- Polish (pl)
|
||||||
|
- Portuguese (pt)
|
||||||
|
- Russian (ru)
|
||||||
|
|
||||||
|
|
||||||
## Documentation and Support
|
## Documentation and Support
|
||||||
|
|
||||||
- Email list: [stormux+subscribe@groups.io](mailto:stormux+subscribe@groups.io?subject=subscribe) with the subject subscribe.
|
- **Email list**: [stormux+subscribe@groups.io](mailto:stormux+subscribe@groups.io?subject=subscribe) with the subject subscribe
|
||||||
- [Fenrir Wiki](https://git.stormux.org/storm/fenrir/wiki)
|
- **Fenrir Wiki**: [https://git.stormux.org/storm/fenrir/wiki](https://git.stormux.org/storm/fenrir/wiki)
|
||||||
- IRC: irc.stormux.org #stormux
|
- **IRC**: irc.stormux.org #stormux
|
||||||
|
- **Issues**: Report bugs and feature requests on the project repository
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ KEY_FENRIR,KEY_SHIFT,KEY_0=set_bookmark_10
|
|||||||
KEY_FENRIR,KEY_0=bookmark_10
|
KEY_FENRIR,KEY_0=bookmark_10
|
||||||
KEY_FENRIR,KEY_KPSLASH=set_window_application
|
KEY_FENRIR,KEY_KPSLASH=set_window_application
|
||||||
2,KEY_FENRIR,KEY_KPSLASH=clear_window_application
|
2,KEY_FENRIR,KEY_KPSLASH=clear_window_application
|
||||||
KEY_KPPLUS=last_incoming
|
KEY_KPPLUS=progress_bar_monitor
|
||||||
|
KEY_FENRIR,KEY_KPPLUS=silence_until_prompt
|
||||||
KEY_FENRIR,KEY_F2=toggle_braille
|
KEY_FENRIR,KEY_F2=toggle_braille
|
||||||
KEY_FENRIR,KEY_F3=toggle_sound
|
KEY_FENRIR,KEY_F3=toggle_sound
|
||||||
KEY_FENRIR,KEY_F4=toggle_speech
|
KEY_FENRIR,KEY_F4=toggle_speech
|
||||||
@@ -126,3 +127,4 @@ KEY_FENRIR,KEY_F8=export_clipboard_to_x
|
|||||||
KEY_FENRIR,KEY_CTRL,KEY_UP=inc_alsa_volume
|
KEY_FENRIR,KEY_CTRL,KEY_UP=inc_alsa_volume
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_DOWN=dec_alsa_volume
|
KEY_FENRIR,KEY_CTRL,KEY_DOWN=dec_alsa_volume
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_V=announce_fenrir_version
|
KEY_FENRIR,KEY_SHIFT,KEY_V=announce_fenrir_version
|
||||||
|
KEY_F4=cycle_keyboard_layout
|
||||||
|
|||||||
@@ -75,9 +75,10 @@ KEY_FENRIR,KEY_F2=toggle_braille
|
|||||||
KEY_FENRIR,KEY_F3=toggle_sound
|
KEY_FENRIR,KEY_F3=toggle_sound
|
||||||
KEY_FENRIR,KEY_F4=toggle_speech
|
KEY_FENRIR,KEY_F4=toggle_speech
|
||||||
KEY_FENRIR,KEY_ENTER=temp_disable_speech
|
KEY_FENRIR,KEY_ENTER=temp_disable_speech
|
||||||
|
KEY_FENRIR,KEY_SHIFT,KEY_ENTER=silence_until_prompt
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_CTRL,KEY_P=toggle_punctuation_level
|
KEY_FENRIR,KEY_SHIFT,KEY_CTRL,KEY_P=toggle_punctuation_level
|
||||||
KEY_FENRIR,KEY_RIGHTBRACE=toggle_auto_spell_check
|
KEY_FENRIR,KEY_RIGHTBRACE=toggle_auto_spell_check
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_ENTER=toggle_output
|
KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_ENTER=toggle_output
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_E=toggle_emoticons
|
KEY_FENRIR,KEY_SHIFT,KEY_E=toggle_emoticons
|
||||||
KEY_FENRIR,KEY_ENTER=toggle_auto_read
|
KEY_FENRIR,KEY_ENTER=toggle_auto_read
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_T=toggle_auto_time
|
KEY_FENRIR,KEY_CTRL,KEY_T=toggle_auto_time
|
||||||
@@ -126,3 +127,4 @@ KEY_FENRIR,KEY_F8=export_clipboard_to_x
|
|||||||
KEY_FENRIR,KEY_CTRL,KEY_UP=inc_alsa_volume
|
KEY_FENRIR,KEY_CTRL,KEY_UP=inc_alsa_volume
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_DOWN=dec_alsa_volume
|
KEY_FENRIR,KEY_CTRL,KEY_DOWN=dec_alsa_volume
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_V=announce_fenrir_version
|
KEY_FENRIR,KEY_SHIFT,KEY_V=announce_fenrir_version
|
||||||
|
KEY_F4=cycle_keyboard_layout
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ KEY_FENRIR,KEY_SHIFT,KEY_0=set_bookmark_10
|
|||||||
KEY_FENRIR,KEY_0=bookmark_10
|
KEY_FENRIR,KEY_0=bookmark_10
|
||||||
KEY_FENRIR,KEY_KPSLASH=set_window_application
|
KEY_FENRIR,KEY_KPSLASH=set_window_application
|
||||||
2,KEY_FENRIR,KEY_KPSLASH=clear_window_application
|
2,KEY_FENRIR,KEY_KPSLASH=clear_window_application
|
||||||
KEY_KPPLUS=last_incoming
|
KEY_KPPLUS=progress_bar_monitor
|
||||||
|
KEY_FENRIR,KEY_KPPLUS=silence_until_prompt
|
||||||
#=toggle_braille
|
#=toggle_braille
|
||||||
KEY_FENRIR,KEY_F3=toggle_sound
|
KEY_FENRIR,KEY_F3=toggle_sound
|
||||||
KEY_FENRIR,KEY_F4=toggle_speech
|
KEY_FENRIR,KEY_F4=toggle_speech
|
||||||
@@ -126,3 +127,4 @@ KEY_FENRIR,KEY_CTRL,KEY_C=save_settings
|
|||||||
KEY_FENRIR,KEY_F8=export_clipboard_to_x
|
KEY_FENRIR,KEY_F8=export_clipboard_to_x
|
||||||
KEY_FENRIR,KEY_ALT,KEY_UP=inc_alsa_volume
|
KEY_FENRIR,KEY_ALT,KEY_UP=inc_alsa_volume
|
||||||
KEY_FENRIR,KEY_ALT,KEY_DOWN=dec_alsa_volume
|
KEY_FENRIR,KEY_ALT,KEY_DOWN=dec_alsa_volume
|
||||||
|
KEY_F4=cycle_keyboard_layout
|
||||||
|
|||||||
@@ -126,3 +126,4 @@ KEY_FENRIR,KEY_CTRL,KEY_C=save_settings
|
|||||||
KEY_FENRIR,KEY_F8=export_clipboard_to_x
|
KEY_FENRIR,KEY_F8=export_clipboard_to_x
|
||||||
KEY_FENRIR,KEY_ALT,KEY_UP=inc_alsa_volume
|
KEY_FENRIR,KEY_ALT,KEY_UP=inc_alsa_volume
|
||||||
KEY_FENRIR,KEY_ALT,KEY_DOWN=dec_alsa_volume
|
KEY_FENRIR,KEY_ALT,KEY_DOWN=dec_alsa_volume
|
||||||
|
KEY_F4=cycle_keyboard_layout
|
||||||
|
|||||||
@@ -85,3 +85,5 @@ alt+f12 - quit fenrir
|
|||||||
^[[1;3F=temp_disable_speech
|
^[[1;3F=temp_disable_speech
|
||||||
# control+end - toggle auto read
|
# control+end - toggle auto read
|
||||||
^[[1;5F=toggle_auto_read
|
^[[1;5F=toggle_auto_read
|
||||||
|
# F12 - cycle keyboard layout
|
||||||
|
^[[24~=cycle_keyboard_layout
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
# Fenrir comment: copy of speakup DefaultKeyAssignments converted to fenrir syntax
|
|
||||||
# Fenrir comment: https://android.googlesource.com/kernel/msm/+/android-7.1.0_r0.2/drivers/staging/speakup/DefaultKeyAssignments
|
|
||||||
# Fenrir comment: The insert or shift key named below is the fenrir key
|
|
||||||
|
|
||||||
# This file is intended to give you an overview of the default keys used
|
|
||||||
# by speakup for it's review functions. You may change them to be
|
|
||||||
# anything you want but that will take some familiarity with key
|
|
||||||
# mapping.
|
|
||||||
|
|
||||||
# We have remapped the insert or zero key on the keypad to act as a
|
|
||||||
# shift key. Well, actually as an altgr key. So in the following list
|
|
||||||
# InsKeyPad-period means hold down the insert key like a shift key and
|
|
||||||
# hit the keypad period.
|
|
||||||
|
|
||||||
# KeyPad-8 Say current Line
|
|
||||||
KEY_KP8=review_curr_line
|
|
||||||
|
|
||||||
# InsKeyPad-8 say from top of screen to reading cursor.
|
|
||||||
KEY_FENRIR,KEY_KP8=curr_screen_before_cursor
|
|
||||||
|
|
||||||
# KeyPad-7 Say Previous Line (UP one line)
|
|
||||||
KEY_KP7=review_prev_line
|
|
||||||
|
|
||||||
# KeyPad-9 Say Next Line (down one line)
|
|
||||||
KEY_KP9=review_next_line
|
|
||||||
|
|
||||||
# KeyPad-5 Say Current Word
|
|
||||||
KEY_KP5=review_curr_word
|
|
||||||
|
|
||||||
# InsKeyPad-5 Spell Current Word
|
|
||||||
KEY_FENRIR,KEY_KP5=review_curr_word_phonetic
|
|
||||||
|
|
||||||
# KeyPad-4 Say Previous Word (left one word)
|
|
||||||
KEY_KP4=review_prev_word
|
|
||||||
|
|
||||||
# InsKeyPad-4 say from left edge of line to reading cursor.
|
|
||||||
KEY_FENRIR,KEY_KP4=cursor_read_line_to_cursor
|
|
||||||
|
|
||||||
# KeyPad-6 Say Next Word (right one word)
|
|
||||||
KEY_KP6=review_next_word
|
|
||||||
|
|
||||||
# InsKeyPad-6 Say from reading cursor to right edge of line.
|
|
||||||
KEY_FENRIR,KEY_KP6=cursor_read_to_end_of_line
|
|
||||||
|
|
||||||
# KeyPad-2 Say Current Letter
|
|
||||||
KEY_KP2=review_curr_char
|
|
||||||
|
|
||||||
# InsKeyPad-2 say current letter phonetically
|
|
||||||
KEY_FENRIR,KEY_KP2=review_curr_char_phonetic
|
|
||||||
|
|
||||||
# KeyPad-1 Say Previous Character (left one letter)
|
|
||||||
KEY_KP1=review_prev_char
|
|
||||||
|
|
||||||
# KeyPad-3 Say Next Character (right one letter)
|
|
||||||
KEY_KP3=review_next_char
|
|
||||||
|
|
||||||
# KeyPad-plus Say Entire Screen
|
|
||||||
KEY_KPPLUS=curr_screen
|
|
||||||
|
|
||||||
# InsKeyPad-plus Say from reading cursor line to bottom of screen.
|
|
||||||
KEY_FENRIR,KEY_KPPLUS=curr_screen_after_cursor
|
|
||||||
|
|
||||||
# KeyPad-Minus Park reading cursor (toggle)
|
|
||||||
# TODO
|
|
||||||
|
|
||||||
# InsKeyPad-minus Say character hex and decimal value.
|
|
||||||
# TODO
|
|
||||||
|
|
||||||
# KeyPad-period Say Position (current line, position and console)
|
|
||||||
KEY_KPDOT=cursor_position
|
|
||||||
|
|
||||||
# InsKeyPad-period say colour attributes of current position.
|
|
||||||
KEY_FENRIR,KEY_KPDOT=attribute_cursor
|
|
||||||
|
|
||||||
# InsKeyPad-9 Move reading cursor to top of screen (insert pgup)
|
|
||||||
KEY_FENRIR,KEY_KP9=review_bottom
|
|
||||||
|
|
||||||
# InsKeyPad-3 Move reading cursor to bottom of screen (insert pgdn)
|
|
||||||
KEY_FENRIR,KEY_KP3=review_top
|
|
||||||
|
|
||||||
# InsKeyPad-7 Move reading cursor to left edge of screen (insert home)
|
|
||||||
KEY_FENRIR,KEY_KP7=review_screen_first_char
|
|
||||||
|
|
||||||
# InsKeyPad-1 Move reading cursor to right edge of screen (insert end)
|
|
||||||
KEY_FENRIR,KEY_KP1=review_screen_last_char
|
|
||||||
|
|
||||||
# ControlKeyPad-1 Move reading cursor to last character on current line.
|
|
||||||
KEY_CTRL,KEY_KP1=review_line_end
|
|
||||||
|
|
||||||
# KeyPad-Enter Shut Up (until another key is hit) and sync reading cursor
|
|
||||||
KEY_KPENTER=temp_disable_speech
|
|
||||||
|
|
||||||
# InsKeyPad-Enter Shut Up (until toggled back on).
|
|
||||||
KEY_FENRIR,KEY_KPENTER=toggle_speech
|
|
||||||
|
|
||||||
# InsKeyPad-star n<x|y> go to line (y) or column (x). Where 'n' is any
|
|
||||||
# allowed value for the row or column for your current screen.
|
|
||||||
# TODO
|
|
||||||
|
|
||||||
# KeyPad-/ Mark and Cut screen region.
|
|
||||||
KEY_KPSLASH=copy_marked_to_clipboard
|
|
||||||
|
|
||||||
# InsKeyPad-/ Paste screen region into any console.
|
|
||||||
KEY_FENRIR,KEY_KPSLASH=paste_clipboard
|
|
||||||
|
|
||||||
|
|
||||||
# Hitting any key while speakup is outputting speech will quiet the
|
|
||||||
# synth until it has caught up with what is being printed on the
|
|
||||||
# console.
|
|
||||||
|
|
||||||
# following by other fenrir commands
|
|
||||||
KEY_FENRIR,KEY_H=toggle_tutorial_mode
|
|
||||||
KEY_CTRL=shut_up
|
|
||||||
|
|
||||||
|
|
||||||
KEY_FENRIR,KEY_KP4=review_line_begin
|
|
||||||
#=review_line_end
|
|
||||||
#=review_line_first_char
|
|
||||||
#=review_line_last_char
|
|
||||||
KEY_FENRIR,KEY_ALT,KEY_1=present_first_line
|
|
||||||
KEY_FENRIR,KEY_ALT,KEY_2=present_last_line
|
|
||||||
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_KP4=review_prev_word_phonetic
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_KP6=review_next_word_phonetic
|
|
||||||
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_KP1=review_prev_char_phonetic
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_KP3=review_next_char_phonetic
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_KP8=review_up
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_KP2=review_down
|
|
||||||
#=exit_review
|
|
||||||
KEY_FENRIR,KEY_I=indent_curr_line
|
|
||||||
KEY_KPPLUS=curr_screen
|
|
||||||
#=cursor_column
|
|
||||||
#=cursor_lineno
|
|
||||||
#=braille_flush
|
|
||||||
#=braille_return_to_cursor
|
|
||||||
#=braille_pan_left
|
|
||||||
#=braille_pan_right
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_1=clear_bookmark_1
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_1=set_bookmark_1
|
|
||||||
KEY_FENRIR,KEY_1=bookmark_1
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_2=clear_bookmark_2
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_2=set_bookmark_2
|
|
||||||
KEY_FENRIR,KEY_2=bookmark_2
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_3=clear_bookmark_3
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_3=set_bookmark_3
|
|
||||||
KEY_FENRIR,KEY_3=bookmark_3
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_4=clear_bookmark_4
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_4=set_bookmark_4
|
|
||||||
KEY_FENRIR,KEY_4=bookmark_4
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_5=clear_bookmark_5
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_5=set_bookmark_5
|
|
||||||
KEY_FENRIR,KEY_5=bookmark_5
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_6=clear_bookmark_6
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_6=set_bookmark_6
|
|
||||||
KEY_FENRIR,KEY_6=bookmark_6
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_7=clear_bookmark_7
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_7=set_bookmark_7
|
|
||||||
KEY_FENRIR,KEY_7=bookmark_7
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_8=clear_bookmark_8
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_8=set_bookmark_8
|
|
||||||
KEY_FENRIR,KEY_8=bookmark_8
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_9=clear_bookmark_9
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_9=set_bookmark_9
|
|
||||||
KEY_FENRIR,KEY_9=bookmark_9
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_0=clear_bookmark_10
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_0=set_bookmark_10
|
|
||||||
KEY_FENRIR,KEY_0=bookmark_10
|
|
||||||
KEY_FENRIR,KEY_KPSLASH=set_window_application
|
|
||||||
2,KEY_FENRIR,KEY_KPSLASH=clear_window_application
|
|
||||||
#=last_incoming
|
|
||||||
KEY_FENRIR,KEY_F2=toggle_braille
|
|
||||||
KEY_FENRIR,KEY_F3=toggle_sound
|
|
||||||
|
|
||||||
KEY_FENRIR,KEY_F9=toggle_punctuation_level
|
|
||||||
KEY_FENRIR,KEY_RIGHTBRACE=toggle_auto_spell_check
|
|
||||||
KEY_FENRIR,KEY_BACKSLASH=toggle_output
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_E=toggle_emoticons
|
|
||||||
key_FENRIR,KEY_KPENTER=toggle_auto_read
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_T=toggle_auto_time
|
|
||||||
KEY_FENRIR,KEY_KPASTERISK=toggle_highlight_tracking
|
|
||||||
KEY_FENRIR,KEY_KPMINUS=toggle_barrier
|
|
||||||
KEY_FENRIR,KEY_Q=quit_fenrir
|
|
||||||
KEY_FENRIR,KEY_T=time
|
|
||||||
2,KEY_FENRIR,KEY_T=date
|
|
||||||
KEY_KPSLASH=toggle_auto_indent
|
|
||||||
#=toggle_has_attribute
|
|
||||||
KEY_FENRIR,KEY_S=spell_check
|
|
||||||
2,KEY_FENRIR,KEY_S=add_word_to_spell_check
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_S=remove_word_from_spell_check
|
|
||||||
KEY_FENRIR,KEY_BACKSPACE=forward_keypress
|
|
||||||
KEY_FENRIR,KEY_ALT,KEY_UP=inc_sound_volume
|
|
||||||
KEY_FENRIR,KEY_ALT,KEY_DOWN=dec_sound_volume
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_C=clear_clipboard
|
|
||||||
KEY_FENRIR,KEY_HOME=first_clipboard
|
|
||||||
KEY_FENRIR,KEY_END=last_clipboard
|
|
||||||
KEY_FENRIR,KEY_PAGEUP=prev_clipboard
|
|
||||||
KEY_FENRIR,KEY_PAGEDOWN=next_clipboard
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_C=curr_clipboard
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_C=copy_last_echo_to_clipboard
|
|
||||||
KEY_FENRIR,KEY_F5=import_clipboard_from_file
|
|
||||||
KEY_FENRIR,KEY_F6=export_clipboard_to_file
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_X=remove_marks
|
|
||||||
KEY_FENRIR,KEY_X=set_mark
|
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_X=marked_text
|
|
||||||
KEY_FENRIR,KEY_F10=toggle_vmenu_mode
|
|
||||||
KEY_FENRIR,KEY_SPACE=current_quick_menu_entry
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_SPACE=current_quick_menu_value
|
|
||||||
KEY_FENRIR,KEY_RIGHT=next_quick_menu_entry
|
|
||||||
KEY_FENRIR,KEY_UP=next_quick_menu_value
|
|
||||||
KEY_FENRIR,KEY_LEFT=prev_quick_menu_entry
|
|
||||||
KEY_FENRIR,KEY_DOWN=prev_quick_menu_value
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_S=save_settings
|
|
||||||
# linux specific
|
|
||||||
KEY_FENRIR,KEY_F7=import_clipboard_from_x
|
|
||||||
KEY_FENRIR,KEY_F8=export_clipboard_to_x
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_UP=inc_alsa_volume
|
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_DOWN=dec_alsa_volume
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
[levelDict]
|
[levelDict]
|
||||||
none:===:
|
none:===:
|
||||||
some:===:-$~+*-/\@#
|
some:===:-$~+*-/\@#
|
||||||
most:===:.,:-$~+*-/\@!#%^&*()[]}{<>;
|
most:===:.,:-_$~+*-/\@!#%^&*()[]}{<>;
|
||||||
all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~
|
all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~
|
||||||
|
|
||||||
[punctDict]
|
[punctDict]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
[levelDict]
|
[levelDict]
|
||||||
none:===:
|
none:===:
|
||||||
some:===:-$~+*-/\@
|
some:===:-$~+*-/\@
|
||||||
most:===:.,:-$~+*-/\@!#%^&*()[]}{<>;
|
most:===:.,:-$~+*-_/\@!#%^&*()[]}{<>;
|
||||||
all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~
|
all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~
|
||||||
|
|
||||||
[punctDict]
|
[punctDict]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# the entrys are seperated with :===: in words colon tripple equal colon ( to not collide with substitutions)
|
# the entrys are seperated with :===: in words colon tripple equal colon ( to not collide with substitutions)
|
||||||
[levelDict]
|
[levelDict]
|
||||||
none:===:
|
none:===:
|
||||||
some:===:-$~+*-/\@
|
some:===:-$~+*-/\@_
|
||||||
most:===:.,:-$~+*-/\@!#%^&*()[]}{<>;
|
most:===:.,:-$~+*-/\@!#%^&*()[]}{<>;
|
||||||
all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~
|
all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ theme=default
|
|||||||
|
|
||||||
# Sound volume controls how loud the sounds for your selected soundpack are.
|
# Sound volume controls how loud the sounds for your selected soundpack are.
|
||||||
# 0 is quietest, 1.0 is loudest.
|
# 0 is quietest, 1.0 is loudest.
|
||||||
volume=1.0
|
volume=0.7
|
||||||
|
|
||||||
# shell commands for generic sound driver
|
# shell commands for generic sound driver
|
||||||
# the folowing variable are substituted
|
# the folowing variable are substituted
|
||||||
@@ -28,6 +28,9 @@ genericPlayFileCommand=play -q -v fenrirVolume fenrirSoundFile
|
|||||||
#the following command is used to generate a frequency beep
|
#the following command is used to generate a frequency beep
|
||||||
genericFrequencyCommand=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
|
genericFrequencyCommand=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
|
||||||
|
|
||||||
|
# Enable progress bar monitoring with ascending tones by default
|
||||||
|
progressMonitoring=True
|
||||||
|
|
||||||
[speech]
|
[speech]
|
||||||
# Turn speech on or off:
|
# Turn speech on or off:
|
||||||
enabled=True
|
enabled=True
|
||||||
@@ -92,8 +95,8 @@ fenrirMaxRate=450
|
|||||||
driver=vcsaDriver
|
driver=vcsaDriver
|
||||||
encoding=auto
|
encoding=auto
|
||||||
screenUpdateDelay=0.05
|
screenUpdateDelay=0.05
|
||||||
suspendingScreen=
|
ignoreScreen=
|
||||||
autodetectSuspendingScreen=True
|
autodetectIgnoreScreen=True
|
||||||
|
|
||||||
[keyboard]
|
[keyboard]
|
||||||
driver=evdevDriver
|
driver=evdevDriver
|
||||||
@@ -131,7 +134,7 @@ punctuationProfile=default
|
|||||||
punctuationLevel=some
|
punctuationLevel=some
|
||||||
respectPunctuationPause=True
|
respectPunctuationPause=True
|
||||||
newLinePause=True
|
newLinePause=True
|
||||||
numberOfClipboards=10
|
numberOfClipboards=50
|
||||||
# used path for "export_clipboard_to_file"
|
# used path for "export_clipboard_to_file"
|
||||||
# $user is replaced by username
|
# $user is replaced by username
|
||||||
#clipboardExportPath=/home/$user/fenrirClipboard
|
#clipboardExportPath=/home/$user/fenrirClipboard
|
||||||
@@ -163,7 +166,7 @@ autoPresentIndent=False
|
|||||||
# 1 = sound only
|
# 1 = sound only
|
||||||
# 2 = speak only
|
# 2 = speak only
|
||||||
autoPresentIndentMode=1
|
autoPresentIndentMode=1
|
||||||
# play a sound when attributes are changeing
|
# play a sound when attributes change
|
||||||
hasAttributes=True
|
hasAttributes=True
|
||||||
# shell for PTY emulatiun (empty = default shell)
|
# shell for PTY emulatiun (empty = default shell)
|
||||||
shell=
|
shell=
|
||||||
@@ -190,7 +193,7 @@ enableSettingsRemote=True
|
|||||||
enableCommandRemote=True
|
enableCommandRemote=True
|
||||||
|
|
||||||
[barrier]
|
[barrier]
|
||||||
enabled=True
|
enabled=False
|
||||||
leftBarriers=│└┌─
|
leftBarriers=│└┌─
|
||||||
rightBarriers=│┘┐─
|
rightBarriers=│┘┐─
|
||||||
|
|
||||||
@@ -211,8 +214,24 @@ list=
|
|||||||
vmenuPath=
|
vmenuPath=
|
||||||
quickMenu=speech#rate;speech#pitch;speech#volume
|
quickMenu=speech#rate;speech#pitch;speech#volume
|
||||||
|
|
||||||
|
[prompt]
|
||||||
|
# Custom prompt patterns for silence until prompt feature
|
||||||
|
# You can add your own shell prompt patterns as regular expressions
|
||||||
|
# Each pattern should be on a separate line, format: customPatterns=pattern1,pattern2,pattern3
|
||||||
|
# Examples:
|
||||||
|
# For PS1='[\u@\h \W] \$ ' use: \[.*@.*\s.*\]\s*[$#>]\s*
|
||||||
|
# For "[user@hostname ~] $" use: \[.*@.*\s.*\]\s*[$#>]\s*
|
||||||
|
# For custom prompts ending with specific strings, use patterns like: .*your_prompt_ending$
|
||||||
|
customPatterns=
|
||||||
|
|
||||||
|
# Specific prompt strings to match exactly (useful for very specific custom prompts)
|
||||||
|
# Format: exactMatches=prompt1,prompt2,prompt3
|
||||||
|
# Examples:
|
||||||
|
# exactMatches=[storm@fenrir ~] $,[root@fenrir ~] #
|
||||||
|
exactMatches=
|
||||||
|
|
||||||
[time]
|
[time]
|
||||||
# automatic time anouncement
|
# automatic time announcement
|
||||||
enabled=False
|
enabled=False
|
||||||
# present time
|
# present time
|
||||||
presentTime=True
|
presentTime=True
|
||||||
|
|||||||
+388
-3
@@ -1,4 +1,389 @@
|
|||||||
1. Basic
|
# Fenrir Development Guide
|
||||||
2. Commands
|
|
||||||
3. Useful API
|
|
||||||
|
|
||||||
|
This document provides information for developers who want to contribute to Fenrir or understand its architecture.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
Fenrir follows a modular, driver-based architecture:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/fenrirscreenreader/
|
||||||
|
├── core/ # Core system modules
|
||||||
|
│ ├── fenrirManager.py # Main application manager
|
||||||
|
│ ├── screenManager.py # Screen handling
|
||||||
|
│ ├── inputManager.py # Input handling
|
||||||
|
│ ├── outputManager.py # Speech/sound output
|
||||||
|
│ ├── commandManager.py # Command system
|
||||||
|
│ └── settingsManager.py # Configuration management
|
||||||
|
├── commands/ # Command implementations
|
||||||
|
│ ├── commands/ # User-invoked commands
|
||||||
|
│ ├── onCursorChange/ # Cursor movement hooks
|
||||||
|
│ ├── onScreenUpdate/ # Screen update hooks
|
||||||
|
│ ├── onKeyInput/ # Key input hooks
|
||||||
|
│ └── help/ # Tutorial system
|
||||||
|
├── drivers/ # Driver implementations
|
||||||
|
│ ├── inputDriver/ # Input drivers (evdev, pty, atspi)
|
||||||
|
│ ├── screenDriver/ # Screen drivers (vcsa, pty)
|
||||||
|
│ ├── speechDriver/ # Speech drivers (speechd, generic)
|
||||||
|
│ └── soundDriver/ # Sound drivers (generic, gstreamer)
|
||||||
|
└── utils/ # Utility modules
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Architecture
|
||||||
|
|
||||||
|
### Driver System
|
||||||
|
Fenrir uses a pluggable driver architecture:
|
||||||
|
|
||||||
|
1. **Input Drivers**: Capture keyboard input
|
||||||
|
- evdevDriver: Linux evdev (recommended)
|
||||||
|
- ptyDriver: Terminal emulation
|
||||||
|
- atspiDriver: AT-SPI for desktop
|
||||||
|
|
||||||
|
2. **Screen Drivers**: Read screen content
|
||||||
|
- vcsaDriver: Linux VCSA devices
|
||||||
|
- ptyDriver: Terminal emulation
|
||||||
|
|
||||||
|
3. **Speech Drivers**: Text-to-speech output
|
||||||
|
- speechdDriver: Speech-dispatcher
|
||||||
|
- genericDriver: Command-line TTS
|
||||||
|
|
||||||
|
4. **Sound Drivers**: Audio output
|
||||||
|
- genericDriver: Sox-based
|
||||||
|
- gstreamerDriver: GStreamer
|
||||||
|
|
||||||
|
5. **Remote Drivers**: Remote control interfaces
|
||||||
|
- unixDriver: Unix socket control
|
||||||
|
- tcpDriver: TCP socket control
|
||||||
|
|
||||||
|
### Command System
|
||||||
|
Commands are Python modules that implement specific functionality:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class command():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return _('Command description')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# Command implementation
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Hooks
|
||||||
|
Fenrir supports various event hooks:
|
||||||
|
|
||||||
|
- **onCursorChange**: Triggered when cursor moves
|
||||||
|
- **onScreenUpdate**: Triggered on screen content changes
|
||||||
|
- **onKeyInput**: Triggered on key presses
|
||||||
|
- **onByteInput**: Triggered on byte-level input
|
||||||
|
- **onScreenChanged**: Triggered when switching screens
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- Python 3.6+
|
||||||
|
- python3-evdev
|
||||||
|
- python3-pyudev
|
||||||
|
- speech-dispatcher
|
||||||
|
- sox
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://git.stormux.org/storm/fenrir.git
|
||||||
|
cd fenrir
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
sudo pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
# Run from source
|
||||||
|
cd src/
|
||||||
|
sudo ./fenrir -f -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run in debug mode
|
||||||
|
sudo ./fenrir -f -d -p
|
||||||
|
|
||||||
|
# Debug output goes to:
|
||||||
|
# - Console (with -p flag)
|
||||||
|
# - /var/log/fenrir.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating Commands
|
||||||
|
|
||||||
|
### Basic Command
|
||||||
|
Create a file in `src/fenrirscreenreader/commands/commands/`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fenrirscreenreader.core import debug
|
||||||
|
|
||||||
|
class command():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return _('My custom command')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# Get current text
|
||||||
|
text = self.env['screen']['newContentText']
|
||||||
|
|
||||||
|
# Speak something
|
||||||
|
self.env['runtime']['outputManager'].presentText('Hello World')
|
||||||
|
|
||||||
|
# Play sound
|
||||||
|
self.env['runtime']['outputManager'].playSoundIcon('Accept')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Bindings
|
||||||
|
Add key bindings in keyboard layout files:
|
||||||
|
`config/keyboard/desktop.conf` or `config/keyboard/laptop.conf`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[KEY_CTRL]#[KEY_ALT]#[KEY_H]=my_command
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Hooks
|
||||||
|
Create event handlers in appropriate directories:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# onCursorChange/my_hook.py
|
||||||
|
class command():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return _('My cursor change handler')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if self.env['runtime']['cursorManager'].isCursorHorizontalMove():
|
||||||
|
# Handle horizontal cursor movement
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating Drivers
|
||||||
|
|
||||||
|
### Driver Template
|
||||||
|
```python
|
||||||
|
class driver():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Driver-specific methods...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Input Driver
|
||||||
|
Implement these methods:
|
||||||
|
- `getInputEvent()`: Return input events
|
||||||
|
- `writeEventBuffer()`: Handle output events
|
||||||
|
- `grabDevices()`: Take exclusive control
|
||||||
|
- `releaseDevices()`: Release control
|
||||||
|
|
||||||
|
### Screen Driver
|
||||||
|
Implement these methods:
|
||||||
|
- `getCurrScreen()`: Get current screen content
|
||||||
|
- `getSessionInformation()`: Get session info
|
||||||
|
|
||||||
|
### Speech Driver
|
||||||
|
Implement these methods:
|
||||||
|
- `speak()`: Speak text
|
||||||
|
- `cancel()`: Stop speech
|
||||||
|
- `setCallback()`: Set callback functions
|
||||||
|
|
||||||
|
### Remote Driver
|
||||||
|
Implement these methods:
|
||||||
|
- `initialize()`: Setup socket/connection
|
||||||
|
- `watchDog()`: Listen for incoming commands
|
||||||
|
- `shutdown()`: Clean up connections
|
||||||
|
|
||||||
|
#### Remote Driver Example
|
||||||
|
```python
|
||||||
|
class driver(remoteDriver):
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
# Start watchdog thread
|
||||||
|
self.env['runtime']['processManager'].addCustomEventThread(
|
||||||
|
self.watchDog, multiprocess=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def watchDog(self, active, eventQueue):
|
||||||
|
# Listen for connections and process commands
|
||||||
|
while active.value:
|
||||||
|
# Accept connections
|
||||||
|
# Parse incoming data
|
||||||
|
# Send to event queue
|
||||||
|
eventQueue.put({
|
||||||
|
"Type": fenrirEventType.RemoteIncomming,
|
||||||
|
"Data": command_text
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Settings System
|
||||||
|
Settings are hierarchical:
|
||||||
|
1. Command-line options (`-o`)
|
||||||
|
2. Configuration file
|
||||||
|
3. Hard-coded defaults
|
||||||
|
|
||||||
|
### Adding Settings
|
||||||
|
1. Add default value to `core/settingsData.py`
|
||||||
|
2. Access via `self.env['runtime']['settingsManager'].getSetting(section, key)`
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
### Debug Levels
|
||||||
|
- 0: DEACTIVE
|
||||||
|
- 1: ERROR
|
||||||
|
- 2: WARNING
|
||||||
|
- 3: INFO
|
||||||
|
|
||||||
|
### Debug Output
|
||||||
|
```python
|
||||||
|
self.env['runtime']['debug'].writeDebugOut(
|
||||||
|
'Debug message',
|
||||||
|
debug.debugLevel.INFO
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Commands
|
||||||
|
```bash
|
||||||
|
# Test specific functionality
|
||||||
|
sudo fenrir -f -d -o "general#debugLevel=3"
|
||||||
|
|
||||||
|
# Test with custom config
|
||||||
|
sudo fenrir -f -s /path/to/test.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
- Follow PEP 8
|
||||||
|
- Use descriptive variable names
|
||||||
|
- Add docstrings for complex functions
|
||||||
|
- Handle exceptions gracefully
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Test with different drivers
|
||||||
|
- Test keyboard layouts
|
||||||
|
- Test on different terminals
|
||||||
|
- Verify accessibility features
|
||||||
|
|
||||||
|
### Submitting Changes
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create feature branch
|
||||||
|
3. Make changes with clear commit messages
|
||||||
|
4. Test thoroughly
|
||||||
|
5. Submit pull request
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Environment Structure
|
||||||
|
The `environment` dict contains all runtime data:
|
||||||
|
|
||||||
|
```python
|
||||||
|
environment = {
|
||||||
|
'runtime': {
|
||||||
|
'settingsManager': settingsManager,
|
||||||
|
'commandManager': commandManager,
|
||||||
|
'screenManager': screenManager,
|
||||||
|
'inputManager': inputManager,
|
||||||
|
'outputManager': outputManager,
|
||||||
|
'debug': debugManager,
|
||||||
|
# ... other managers
|
||||||
|
},
|
||||||
|
'screen': {
|
||||||
|
'newContentText': '',
|
||||||
|
'oldContentText': '',
|
||||||
|
'newCursor': {'x': 0, 'y': 0},
|
||||||
|
'oldCursor': {'x': 0, 'y': 0},
|
||||||
|
# ... screen data
|
||||||
|
},
|
||||||
|
'general': {
|
||||||
|
'prevCommand': '',
|
||||||
|
'currCommand': '',
|
||||||
|
# ... general data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Operations
|
||||||
|
|
||||||
|
#### Speaking Text
|
||||||
|
```python
|
||||||
|
self.env['runtime']['outputManager'].presentText('Hello')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Playing Sounds
|
||||||
|
```python
|
||||||
|
self.env['runtime']['outputManager'].playSoundIcon('Accept')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Getting Settings
|
||||||
|
```python
|
||||||
|
rate = self.env['runtime']['settingsManager'].getSetting('speech', 'rate')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Cursor Information
|
||||||
|
```python
|
||||||
|
x = self.env['screen']['newCursor']['x']
|
||||||
|
y = self.env['screen']['newCursor']['y']
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Screen Content
|
||||||
|
```python
|
||||||
|
text = self.env['screen']['newContentText']
|
||||||
|
lines = text.split('\n')
|
||||||
|
current_line = lines[self.env['screen']['newCursor']['y']]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Release Process
|
||||||
|
1. Update version in `fenrirVersion.py`
|
||||||
|
2. Update changelog
|
||||||
|
3. Test on multiple systems
|
||||||
|
4. Tag release
|
||||||
|
5. Update documentation
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- Maintain Python 3.6+ compatibility
|
||||||
|
- Test on multiple Linux distributions
|
||||||
|
- Ensure driver compatibility
|
||||||
|
- Check dependencies
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **Repository**: https://git.stormux.org/storm/fenrir
|
||||||
|
- **Wiki**: https://git.stormux.org/storm/fenrir/wiki
|
||||||
|
- **Issues**: Use repository issue tracker
|
||||||
|
- **Community**: IRC irc.stormux.org #stormux
|
||||||
|
- **Email**: stormux+subscribe@groups.io
|
||||||
+509
-2754
File diff suppressed because it is too large
Load Diff
@@ -1202,6 +1202,47 @@ link:#Settings[Settings]
|
|||||||
|
|
||||||
=== Commandline Arguments
|
=== Commandline Arguments
|
||||||
|
|
||||||
|
Fenrir supports several command-line options:
|
||||||
|
|
||||||
|
....
|
||||||
|
fenrir [OPTIONS]
|
||||||
|
....
|
||||||
|
|
||||||
|
==== Available Options
|
||||||
|
|
||||||
|
`+-h, --help+`::
|
||||||
|
Show help message and exit.
|
||||||
|
|
||||||
|
`+-v, --version+`::
|
||||||
|
Show version information and exit.
|
||||||
|
|
||||||
|
`+-f, --foreground+`::
|
||||||
|
Run Fenrir in the foreground instead of as a daemon.
|
||||||
|
|
||||||
|
`+-s, --setting SETTING-FILE+`::
|
||||||
|
Path to a custom settings file.
|
||||||
|
|
||||||
|
`+-o, --options SECTION#SETTING=VALUE;..+`::
|
||||||
|
Override settings file options (see below for details).
|
||||||
|
|
||||||
|
`+-d, --debug+`::
|
||||||
|
Enable debug mode. Debug information will be logged.
|
||||||
|
|
||||||
|
`+-p, --print+`::
|
||||||
|
Print debug messages to screen in addition to logging them.
|
||||||
|
|
||||||
|
`+-e, --emulated-pty+`::
|
||||||
|
Use PTY emulation with escape sequences for input. This enables usage in desktop/X11/Wayland environments and terminal emulators.
|
||||||
|
|
||||||
|
`+-E, --emulated-evdev+`::
|
||||||
|
Use PTY emulation with evdev for input (single instance mode).
|
||||||
|
|
||||||
|
`+-F, --force-all-screens+`::
|
||||||
|
Force Fenrir to respond on all screens, ignoring the ignoreScreen setting. This temporarily overrides screen filtering for the current session.
|
||||||
|
|
||||||
|
`+-i, -I, --ignore-screen <SCREEN>+`::
|
||||||
|
Ignore specific screen(s). Can be used multiple times to ignore multiple screens. This is equivalent to setting ignoreScreen in the configuration file and will be combined with any existing ignore settings.
|
||||||
|
|
||||||
==== Set settings options
|
==== Set settings options
|
||||||
|
|
||||||
You can specify options that overwrite the setting.conf. This is done
|
You can specify options that overwrite the setting.conf. This is done
|
||||||
@@ -1224,9 +1265,154 @@ or change the debug level to verbose
|
|||||||
fenrir -o "general#debugLevel=3"
|
fenrir -o "general#debugLevel=3"
|
||||||
....
|
....
|
||||||
|
|
||||||
|
Example using force all screens option:
|
||||||
|
|
||||||
|
....
|
||||||
|
fenrir -F
|
||||||
|
....
|
||||||
|
|
||||||
You can find the available sections and variables here #Settings See
|
You can find the available sections and variables here #Settings See
|
||||||
Syntax link:#settings.conf syntax[#settings.conf syntax]
|
Syntax link:#settings.conf syntax[#settings.conf syntax]
|
||||||
|
|
||||||
|
=== Remote Control
|
||||||
|
|
||||||
|
Fenrir includes a powerful remote control system that allows external applications and scripts to control Fenrir through Unix sockets or TCP connections.
|
||||||
|
|
||||||
|
==== Configuration
|
||||||
|
|
||||||
|
Enable remote control in settings.conf:
|
||||||
|
|
||||||
|
....
|
||||||
|
[remote]
|
||||||
|
enable=True
|
||||||
|
driver=unixDriver
|
||||||
|
enableSettingsRemote=True
|
||||||
|
enableCommandRemote=True
|
||||||
|
....
|
||||||
|
|
||||||
|
==== Using socat with Unix Sockets
|
||||||
|
|
||||||
|
The `+socat+` command provides the easiest way to send commands to Fenrir:
|
||||||
|
|
||||||
|
===== Basic Speech Control
|
||||||
|
|
||||||
|
....
|
||||||
|
# Interrupt current speech
|
||||||
|
echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Speak custom text
|
||||||
|
echo "command say Hello, this is a test message" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Temporarily disable speech (until next keystroke)
|
||||||
|
echo "command tempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
....
|
||||||
|
|
||||||
|
===== Settings Control
|
||||||
|
|
||||||
|
....
|
||||||
|
# Enable highlight tracking mode
|
||||||
|
echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Change speech rate
|
||||||
|
echo "setting set speech#rate=0.8" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Change punctuation level (none/some/most/all)
|
||||||
|
echo "setting set general#punctuationLevel=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Voice and TTS control
|
||||||
|
echo "setting set speech#voice=en-us+f3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Multiple settings at once
|
||||||
|
echo "setting set speech#rate=0.8;sound#volume=0.7;general#punctuationLevel=most" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Reset all settings to defaults
|
||||||
|
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
....
|
||||||
|
|
||||||
|
===== Clipboard Operations
|
||||||
|
|
||||||
|
....
|
||||||
|
# Place text into clipboard
|
||||||
|
echo "command clipboard This text will be copied to clipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
|
||||||
|
# Export clipboard to file
|
||||||
|
echo "command exportclipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
....
|
||||||
|
|
||||||
|
===== Application Control
|
||||||
|
|
||||||
|
....
|
||||||
|
# Quit Fenrir
|
||||||
|
echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||||
|
....
|
||||||
|
|
||||||
|
==== Command Reference
|
||||||
|
|
||||||
|
===== Available Commands
|
||||||
|
|
||||||
|
*Speech Commands:*
|
||||||
|
|
||||||
|
* `+command say <text>+` - Speak the specified text
|
||||||
|
* `+command interrupt+` - Stop current speech
|
||||||
|
* `+command tempdisablespeech+` - Disable speech until next key press
|
||||||
|
|
||||||
|
*Clipboard Commands:*
|
||||||
|
|
||||||
|
* `+command clipboard <text>+` - Add text to clipboard
|
||||||
|
* `+command exportclipboard+` - Export clipboard to file
|
||||||
|
|
||||||
|
*Window Commands:*
|
||||||
|
|
||||||
|
* `+command window <x1> <y1> <x2> <y2>+` - Define window area
|
||||||
|
* `+command resetwindow+` - Reset to full screen
|
||||||
|
|
||||||
|
*VMenu Commands:*
|
||||||
|
|
||||||
|
* `+command vmenu <menu_path>+` - Set virtual menu context
|
||||||
|
* `+command resetvmenu+` - Reset virtual menu
|
||||||
|
|
||||||
|
*Application Commands:*
|
||||||
|
|
||||||
|
* `+command quitapplication+` - Quit Fenrir
|
||||||
|
|
||||||
|
===== Available Settings
|
||||||
|
|
||||||
|
*Settings Commands:*
|
||||||
|
|
||||||
|
* `+setting set <section>#<key>=<value>+` - Set configuration value
|
||||||
|
* `+setting reset+` - Reset all settings to defaults
|
||||||
|
* `+setting save [path]+` - Save current settings
|
||||||
|
|
||||||
|
*Key Settings You Can Control:*
|
||||||
|
|
||||||
|
*Speech Settings:*
|
||||||
|
* `+speech#enabled=True/False+` - Enable/disable speech
|
||||||
|
* `+speech#rate=0.1-1.0+` - Speech rate (speed)
|
||||||
|
* `+speech#pitch=0.1-1.0+` - Speech pitch (tone)
|
||||||
|
* `+speech#volume=0.1-1.0+` - Speech volume
|
||||||
|
* `+speech#voice=voice_name+` - Voice selection (e.g., "en-us+f3")
|
||||||
|
* `+speech#module=module_name+` - TTS module (e.g., "espeak-ng")
|
||||||
|
|
||||||
|
*General Settings:*
|
||||||
|
* `+general#punctuationLevel=none/some/most/all+` - Punctuation verbosity
|
||||||
|
* `+general#autoSpellCheck=True/False+` - Automatic spell checking
|
||||||
|
* `+general#emoticons=True/False+` - Enable emoticon replacement
|
||||||
|
|
||||||
|
*Sound Settings:*
|
||||||
|
* `+sound#enabled=True/False+` - Enable/disable sound
|
||||||
|
* `+sound#volume=0.1-1.0+` - Sound volume
|
||||||
|
|
||||||
|
*Focus Settings:*
|
||||||
|
* `+focus#cursor=True/False+` - Follow text cursor
|
||||||
|
* `+focus#highlight=True/False+` - Follow text highlighting
|
||||||
|
|
||||||
|
*Keyboard Settings:*
|
||||||
|
* `+keyboard#charEchoMode=0-2+` - Character echo (0=none, 1=always, 2=capslock only)
|
||||||
|
* `+keyboard#wordEcho=True/False+` - Echo complete words
|
||||||
|
|
||||||
|
*Screen Settings:*
|
||||||
|
* `+screen#ignoreScreen=1,2,3+` - TTY screens to ignore
|
||||||
|
|
||||||
==== settings.conf syntax
|
==== settings.conf syntax
|
||||||
|
|
||||||
the syntax of the link:#Settings[settings.conf] is quite simple and
|
the syntax of the link:#Settings[settings.conf] is quite simple and
|
||||||
|
|||||||
+319
-1573
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -1,12 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
#Basic install script for Fenrir.
|
#Basic install script for Fenrir.
|
||||||
read -p "This will install Fenrir. Press ctrl+C to cancel, or enter to continue." continue
|
read -rp "This will install Fenrir. Press ctrl+C to cancel, or enter to continue."
|
||||||
|
|
||||||
# Fenrir main application
|
# Fenrir main application
|
||||||
install -m755 -d /opt/fenrirscreenreader
|
install -m755 -d /opt/fenrirscreenreader
|
||||||
cp -af src/* /opt/fenrirscreenreader
|
cp -af src/* /opt/fenrirscreenreader
|
||||||
|
|
||||||
ln -fs /opt/fenrirscreenreader/fenrir-daemon /usr/bin/fenrir-daemon
|
|
||||||
ln -fs /opt/fenrirscreenreader/fenrir /usr/bin/fenrir
|
ln -fs /opt/fenrirscreenreader/fenrir /usr/bin/fenrir
|
||||||
# tools
|
# tools
|
||||||
install -m755 -d /usr/share/fenrirscreenreader/tools
|
install -m755 -d /usr/share/fenrirscreenreader/tools
|
||||||
@@ -33,8 +32,9 @@ cp -af config/sound/template /usr/share/sounds/fenrirscreenreader/template
|
|||||||
# config
|
# config
|
||||||
if [ -f "/etc/fenrirscreenreader/settings/settings.conf" ]; then
|
if [ -f "/etc/fenrirscreenreader/settings/settings.conf" ]; then
|
||||||
echo "Do you want to overwrite your current global settings? (y/n)"
|
echo "Do you want to overwrite your current global settings? (y/n)"
|
||||||
read yn
|
read -r yn
|
||||||
if [ $yn = "Y" -o $yn = "y" ]; then
|
yn="${yn:0:1}"
|
||||||
|
if [[ "${yn^}" == "Y" ]]; then
|
||||||
mv /etc/fenrirscreenreader/settings/settings.conf /etc/fenrirscreenreader/settings/settings.conf.bak
|
mv /etc/fenrirscreenreader/settings/settings.conf /etc/fenrirscreenreader/settings/settings.conf.bak
|
||||||
echo "Your old settings.conf has been backed up to settings.conf.bak."
|
echo "Your old settings.conf has been backed up to settings.conf.bak."
|
||||||
install -m644 -D "config/settings/settings.conf" /etc/fenrirscreenreader/settings/settings.conf
|
install -m644 -D "config/settings/settings.conf" /etc/fenrirscreenreader/settings/settings.conf
|
||||||
|
|||||||
+8
-6
@@ -1,7 +1,9 @@
|
|||||||
evdev>=1.1.2
|
daemonize
|
||||||
daemonize>=2.5.0
|
evdev
|
||||||
dbus-python>=1.2.8
|
|
||||||
pyudev>=0.21.0
|
|
||||||
pexpect
|
pexpect
|
||||||
pyttsx3
|
pyenchant
|
||||||
pyte>=0.7.0
|
pyperclip
|
||||||
|
pyte
|
||||||
|
pyudev
|
||||||
|
pyxdg
|
||||||
|
setproctitle
|
||||||
|
|||||||
@@ -99,8 +99,10 @@ setup(
|
|||||||
"evdev>=1.1.2",
|
"evdev>=1.1.2",
|
||||||
"daemonize>=2.5.0",
|
"daemonize>=2.5.0",
|
||||||
"dbus-python>=1.2.8",
|
"dbus-python>=1.2.8",
|
||||||
|
"pyperclip",
|
||||||
"pyudev>=0.21.0",
|
"pyudev>=0.21.0",
|
||||||
"setuptools",
|
"setuptools",
|
||||||
|
"setproctitle",
|
||||||
"pexpect",
|
"pexpect",
|
||||||
"pyte>=0.7.0",
|
"pyte>=0.7.0",
|
||||||
],
|
],
|
||||||
@@ -111,10 +113,10 @@ if not forceSettingsFlag:
|
|||||||
# create settings file from example if not exist
|
# create settings file from example if not exist
|
||||||
if not os.path.isfile('/etc/fenrirscreenreader/settings/settings.conf'):
|
if not os.path.isfile('/etc/fenrirscreenreader/settings/settings.conf'):
|
||||||
try:
|
try:
|
||||||
copyfile('/etc/fenrirscreenreader/settings/settings.conf.example', '/etc/fenrirscreenreader/settings/settings.conf')
|
copyfile('config/fenrirscreenreader/settings/settings.conf', '/etc/fenrirscreenreader/settings/settings.conf')
|
||||||
print('create settings file in /etc/fenrirscreenreader/settings/settings.conf')
|
print('create settings file in /etc/fenrirscreenreader/settings/settings.conf')
|
||||||
except:
|
except OSError as e:
|
||||||
pass
|
print(f"Could not copy settings file to destination: {e}")
|
||||||
else:
|
else:
|
||||||
print('settings.conf file found. It is not overwritten automatical')
|
print('settings.conf file found. It is not overwritten automatical')
|
||||||
|
|
||||||
|
|||||||
+11
@@ -67,6 +67,17 @@ def create_argument_parser():
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help='Use PTY emulation with evdev for input (single instance)'
|
help='Use PTY emulation with evdev for input (single instance)'
|
||||||
)
|
)
|
||||||
|
argumentParser.add_argument(
|
||||||
|
'-F', '--force-all-screens',
|
||||||
|
action='store_true',
|
||||||
|
help='Force Fenrir to respond on all screens, ignoring ignoreScreen setting'
|
||||||
|
)
|
||||||
|
argumentParser.add_argument(
|
||||||
|
'-i', '-I', '--ignore-screen',
|
||||||
|
metavar='SCREEN',
|
||||||
|
action='append',
|
||||||
|
help='Ignore specific screen(s). Can be used multiple times. Same as ignoreScreen setting.'
|
||||||
|
)
|
||||||
return argumentParser
|
return argumentParser
|
||||||
|
|
||||||
def validate_arguments(cliArgs):
|
def validate_arguments(cliArgs):
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Fenrir TTY screen reader
|
||||||
|
# By Chrys, Storm Dragon, and contributers.
|
||||||
|
|
||||||
|
from fenrirscreenreader.core import debug
|
||||||
|
import os
|
||||||
|
|
||||||
|
class command():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
def getDescription(self):
|
||||||
|
return _('cycles between available keyboard layouts')
|
||||||
|
|
||||||
|
def getAvailableLayouts(self):
|
||||||
|
"""Get list of available keyboard layout files"""
|
||||||
|
layouts = []
|
||||||
|
|
||||||
|
# Check standard locations for keyboard layouts
|
||||||
|
settingsRoot = '/etc/fenrirscreenreader/'
|
||||||
|
if not os.path.exists(settingsRoot):
|
||||||
|
# Fallback to source directory
|
||||||
|
import fenrirscreenreader
|
||||||
|
fenrirPath = os.path.dirname(fenrirscreenreader.__file__)
|
||||||
|
settingsRoot = fenrirPath + '/../../config/'
|
||||||
|
|
||||||
|
keyboardPath = settingsRoot + 'keyboard/'
|
||||||
|
|
||||||
|
if os.path.exists(keyboardPath):
|
||||||
|
for file in os.listdir(keyboardPath):
|
||||||
|
if file.endswith('.conf') and not file.startswith('__') and not file.lower().startswith('pty'):
|
||||||
|
layout_name = file.replace('.conf', '')
|
||||||
|
if layout_name not in layouts:
|
||||||
|
layouts.append(layout_name)
|
||||||
|
|
||||||
|
# Ensure we have at least basic layouts
|
||||||
|
if not layouts:
|
||||||
|
layouts = ['desktop', 'laptop']
|
||||||
|
else:
|
||||||
|
layouts.sort()
|
||||||
|
|
||||||
|
return layouts
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
current_layout = self.env['runtime']['settingsManager'].getSetting('keyboard', 'keyboardLayout')
|
||||||
|
|
||||||
|
# Extract layout name from full path if needed
|
||||||
|
if '/' in current_layout:
|
||||||
|
current_layout = os.path.basename(current_layout).replace('.conf', '')
|
||||||
|
|
||||||
|
# Get available layouts
|
||||||
|
available_layouts = self.getAvailableLayouts()
|
||||||
|
|
||||||
|
# Find next layout in cycle
|
||||||
|
try:
|
||||||
|
current_index = available_layouts.index(current_layout)
|
||||||
|
next_index = (current_index + 1) % len(available_layouts)
|
||||||
|
except ValueError:
|
||||||
|
# If current layout not found, start from beginning
|
||||||
|
next_index = 0
|
||||||
|
|
||||||
|
next_layout = available_layouts[next_index]
|
||||||
|
|
||||||
|
# Update setting and reload shortcuts
|
||||||
|
self.env['runtime']['settingsManager'].setSetting('keyboard', 'keyboardLayout', next_layout)
|
||||||
|
|
||||||
|
# Reload shortcuts with new layout
|
||||||
|
try:
|
||||||
|
self.env['runtime']['inputManager'].reloadShortcuts()
|
||||||
|
self.env['runtime']['outputManager'].presentText(
|
||||||
|
_('Switched to {} keyboard layout').format(next_layout),
|
||||||
|
interrupt=True
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut(
|
||||||
|
"Error reloading shortcuts: " + str(e),
|
||||||
|
debug.debugLevel.ERROR
|
||||||
|
)
|
||||||
|
self.env['runtime']['outputManager'].presentText(
|
||||||
|
_('Error switching keyboard layout'),
|
||||||
|
interrupt=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
||||||
@@ -5,15 +5,17 @@
|
|||||||
# By Chrys, Storm Dragon, and contributers.
|
# By Chrys, Storm Dragon, and contributers.
|
||||||
|
|
||||||
from fenrirscreenreader.core import debug
|
from fenrirscreenreader.core import debug
|
||||||
import subprocess, os
|
import os
|
||||||
from subprocess import Popen, PIPE
|
import importlib
|
||||||
import _thread
|
import _thread
|
||||||
|
import pyperclip
|
||||||
|
|
||||||
class command():
|
class command():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
def initialize(self, environment):
|
def initialize(self, environment, scriptPath=''):
|
||||||
self.env = environment
|
self.env = environment
|
||||||
|
self.scriptPath = scriptPath
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
pass
|
pass
|
||||||
def getDescription(self):
|
def getDescription(self):
|
||||||
@@ -22,56 +24,48 @@ class command():
|
|||||||
_thread.start_new_thread(self._threadRun , ())
|
_thread.start_new_thread(self._threadRun , ())
|
||||||
def _threadRun(self):
|
def _threadRun(self):
|
||||||
try:
|
try:
|
||||||
|
# Check if clipboard is empty
|
||||||
if self.env['runtime']['memoryManager'].isIndexListEmpty('clipboardHistory'):
|
if self.env['runtime']['memoryManager'].isIndexListEmpty('clipboardHistory'):
|
||||||
self.env['runtime']['outputManager'].presentText(_('clipboard empty'), interrupt=True)
|
self.env['runtime']['outputManager'].presentText(_('clipboard empty'), interrupt=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Get current clipboard content
|
||||||
clipboard = self.env['runtime']['memoryManager'].getIndexListElement('clipboardHistory')
|
clipboard = self.env['runtime']['memoryManager'].getIndexListElement('clipboardHistory')
|
||||||
user = self.env['general']['currUser']
|
|
||||||
|
# Remember original display environment variable if it exists
|
||||||
# First try to find xclip in common locations
|
originalDisplay = os.environ.get('DISPLAY', '')
|
||||||
xclip_paths = [
|
success = False
|
||||||
'/usr/bin/xclip',
|
|
||||||
'/bin/xclip',
|
# Try different display options
|
||||||
'/usr/local/bin/xclip'
|
for i in range(10):
|
||||||
]
|
display = f":{i}"
|
||||||
|
try:
|
||||||
xclip_path = None
|
# Set display environment variable
|
||||||
for path in xclip_paths:
|
os.environ['DISPLAY'] = display
|
||||||
if os.path.isfile(path) and os.access(path, os.X_OK):
|
# Attempt to set clipboard content
|
||||||
xclip_path = path
|
importlib.reload(pyperclip) # Weird workaround for some distros
|
||||||
|
pyperclip.copy(clipboard)
|
||||||
|
# If we get here without exception, we found a working display
|
||||||
|
success = True
|
||||||
break
|
break
|
||||||
|
except Exception:
|
||||||
if not xclip_path:
|
# Failed for this display, try next one
|
||||||
self.env['runtime']['outputManager'].presentText(
|
continue
|
||||||
'xclip not found in common locations',
|
|
||||||
interrupt=True
|
# Restore original display setting
|
||||||
)
|
if originalDisplay:
|
||||||
return
|
os.environ['DISPLAY'] = originalDisplay
|
||||||
|
|
||||||
for display in range(10):
|
|
||||||
p = Popen(
|
|
||||||
['su', user, '-p', '-c', f"{xclip_path} -d :{display} -selection clipboard"],
|
|
||||||
stdin=PIPE, stdout=PIPE, stderr=PIPE, preexec_fn=os.setpgrp
|
|
||||||
)
|
|
||||||
stdout, stderr = p.communicate(input=clipboard.encode('utf-8'))
|
|
||||||
|
|
||||||
self.env['runtime']['outputManager'].interruptOutput()
|
|
||||||
|
|
||||||
stderr = stderr.decode('utf-8')
|
|
||||||
stdout = stdout.decode('utf-8')
|
|
||||||
|
|
||||||
if stderr == '':
|
|
||||||
break
|
|
||||||
|
|
||||||
if stderr != '':
|
|
||||||
self.env['runtime']['outputManager'].presentText(stderr, soundIcon='', interrupt=False)
|
|
||||||
else:
|
else:
|
||||||
self.env['runtime']['outputManager'].presentText('exported to the X session.', interrupt=True)
|
os.environ.pop('DISPLAY', None)
|
||||||
|
|
||||||
|
# Notify the user of the result
|
||||||
|
if success:
|
||||||
|
self.env['runtime']['outputManager'].presentText(_('exported to the X session.'), interrupt=True)
|
||||||
|
else:
|
||||||
|
self.env['runtime']['outputManager'].presentText(_('failed to export to X clipboard. No available display found.'), interrupt=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['outputManager'].presentText(str(e), soundIcon='', interrupt=False)
|
self.env['runtime']['outputManager'].presentText(str(e), soundIcon='', interrupt=False)
|
||||||
|
|
||||||
|
|
||||||
def setCallback(self, callback):
|
def setCallback(self, callback):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
# By Chrys, Storm Dragon, and contributers.
|
# By Chrys, Storm Dragon, and contributers.
|
||||||
|
|
||||||
from fenrirscreenreader.core import debug
|
from fenrirscreenreader.core import debug
|
||||||
import subprocess, os
|
import importlib
|
||||||
from subprocess import Popen, PIPE
|
|
||||||
import _thread
|
import _thread
|
||||||
|
import pyperclip
|
||||||
|
import os
|
||||||
|
|
||||||
class command():
|
class command():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
@@ -22,33 +24,41 @@ class command():
|
|||||||
_thread.start_new_thread(self._threadRun , ())
|
_thread.start_new_thread(self._threadRun , ())
|
||||||
def _threadRun(self):
|
def _threadRun(self):
|
||||||
try:
|
try:
|
||||||
# Find xclip path
|
# Remember original display environment variable if it exists
|
||||||
xclip_paths = ['/usr/bin/xclip', '/bin/xclip', '/usr/local/bin/xclip']
|
originalDisplay = os.environ.get('DISPLAY', '')
|
||||||
xclip_path = None
|
clipboardContent = None
|
||||||
for path in xclip_paths:
|
|
||||||
if os.path.isfile(path) and os.access(path, os.X_OK):
|
# Try different display options
|
||||||
xclip_path = path
|
for i in range(10):
|
||||||
break
|
display = f":{i}"
|
||||||
if not xclip_path:
|
try:
|
||||||
self.env['runtime']['outputManager'].presentText('xclip not found in common locations', interrupt=True)
|
# Set display environment variable
|
||||||
return
|
os.environ['DISPLAY'] = display
|
||||||
xClipboard = ''
|
# Attempt to get clipboard content
|
||||||
for display in range(10):
|
importlib.reload(pyperclip) # Weird workaround for some distros
|
||||||
p = Popen('su ' + self.env['general']['currUser'] + ' -p -c "' + xclip_path + ' -d :' + str(display) + ' -o"', stdout=PIPE, stderr=PIPE, shell=True)
|
clipboardContent = pyperclip.paste()
|
||||||
stdout, stderr = p.communicate()
|
# If we get here without exception, we found a working display
|
||||||
self.env['runtime']['outputManager'].interruptOutput()
|
if clipboardContent:
|
||||||
stderr = stderr.decode('utf-8')
|
break
|
||||||
xClipboard = stdout.decode('utf-8')
|
except Exception:
|
||||||
if (stderr == ''):
|
# Failed for this display, try next one
|
||||||
break
|
continue
|
||||||
if stderr != '':
|
|
||||||
self.env['runtime']['outputManager'].presentText(stderr , soundIcon='', interrupt=False)
|
# Restore original display setting
|
||||||
|
if originalDisplay:
|
||||||
|
os.environ['DISPLAY'] = originalDisplay
|
||||||
else:
|
else:
|
||||||
self.env['runtime']['memoryManager'].addValueToFirstIndex('clipboardHistory', xClipboard)
|
os.environ.pop('DISPLAY', None)
|
||||||
|
|
||||||
|
# Process the clipboard content if we found any
|
||||||
|
if clipboardContent and isinstance(clipboardContent, str):
|
||||||
|
self.env['runtime']['memoryManager'].addValueToFirstIndex('clipboardHistory', clipboardContent)
|
||||||
self.env['runtime']['outputManager'].presentText('Import to Clipboard', soundIcon='CopyToClipboard', interrupt=True)
|
self.env['runtime']['outputManager'].presentText('Import to Clipboard', soundIcon='CopyToClipboard', interrupt=True)
|
||||||
self.env['runtime']['outputManager'].presentText(xClipboard, soundIcon='', interrupt=False)
|
self.env['runtime']['outputManager'].presentText(clipboardContent, soundIcon='', interrupt=False)
|
||||||
|
else:
|
||||||
|
self.env['runtime']['outputManager'].presentText('No text found in clipboard or no accessible display', interrupt=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['outputManager'].presentText(e , soundIcon='', interrupt=False)
|
self.env['runtime']['outputManager'].presentText(str(e), soundIcon='', interrupt=False)
|
||||||
|
|
||||||
def setCallback(self, callback):
|
def setCallback(self, callback):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Fenrir TTY screen reader
|
|
||||||
# By Chrys, Storm Dragon, and contributers.
|
|
||||||
|
|
||||||
from fenrirscreenreader.core import debug
|
|
||||||
|
|
||||||
class command():
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
def initialize(self, environment):
|
|
||||||
self.env = environment
|
|
||||||
def shutdown(self):
|
|
||||||
pass
|
|
||||||
def getDescription(self):
|
|
||||||
return _('Presents the text which was last received')
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.env['runtime']['outputManager'].presentText(self.env['screen']['newDelta'], interrupt=True)
|
|
||||||
|
|
||||||
def setCallback(self, callback):
|
|
||||||
pass
|
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Fenrir TTY screen reader
|
||||||
|
# By Chrys, Storm Dragon, and contributers.
|
||||||
|
|
||||||
|
from fenrirscreenreader.core import debug
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
class command():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
# Use commandBuffer like other commands
|
||||||
|
if 'progressMonitoring' not in self.env['commandBuffer']:
|
||||||
|
# Check if progress monitoring should be enabled by default from settings
|
||||||
|
try:
|
||||||
|
defaultEnabled = self.env['runtime']['settingsManager'].getSettingAsBool('sound', 'progressMonitoring')
|
||||||
|
except:
|
||||||
|
# If setting doesn't exist, default to False
|
||||||
|
defaultEnabled = False
|
||||||
|
self.env['commandBuffer']['progressMonitoring'] = defaultEnabled
|
||||||
|
self.env['commandBuffer']['lastProgressTime'] = 0
|
||||||
|
self.env['commandBuffer']['lastProgressValue'] = -1
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
self.stopProgressMonitoring()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return _('Toggle progress bar monitoring with ascending tones')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# Check if commandBuffer exists
|
||||||
|
if 'progressMonitoring' not in self.env['commandBuffer']:
|
||||||
|
self.env['commandBuffer']['progressMonitoring'] = False
|
||||||
|
self.env['commandBuffer']['lastProgressTime'] = 0
|
||||||
|
self.env['commandBuffer']['lastProgressValue'] = -1
|
||||||
|
|
||||||
|
if self.env['commandBuffer']['progressMonitoring']:
|
||||||
|
self.stopProgressMonitoring()
|
||||||
|
self.env['runtime']['outputManager'].presentText(_("Progress monitoring disabled"), interrupt=True)
|
||||||
|
else:
|
||||||
|
self.startProgressMonitoring()
|
||||||
|
self.env['runtime']['outputManager'].presentText(_("Progress monitoring enabled"), interrupt=True)
|
||||||
|
|
||||||
|
def startProgressMonitoring(self):
|
||||||
|
self.env['commandBuffer']['progressMonitoring'] = True
|
||||||
|
self.env['commandBuffer']['lastProgressTime'] = time.time()
|
||||||
|
self.env['commandBuffer']['lastProgressValue'] = -1
|
||||||
|
# Don't control speech - let user decide with silence_until_prompt
|
||||||
|
|
||||||
|
def stopProgressMonitoring(self):
|
||||||
|
self.env['commandBuffer']['progressMonitoring'] = False
|
||||||
|
# Don't control speech - progress monitor is beep-only
|
||||||
|
|
||||||
|
def detectProgress(self, text):
|
||||||
|
if not self.env['runtime']['progressMonitoring']:
|
||||||
|
return
|
||||||
|
|
||||||
|
currentTime = time.time()
|
||||||
|
|
||||||
|
# Pattern 1: Percentage (50%, 25.5%, etc.)
|
||||||
|
percentMatch = re.search(r'(\d+(?:\.\d+)?)\s*%', text)
|
||||||
|
if percentMatch:
|
||||||
|
percentage = float(percentMatch.group(1))
|
||||||
|
if percentage != self.env['runtime']['lastProgressValue']:
|
||||||
|
self.playProgressTone(percentage)
|
||||||
|
self.env['runtime']['lastProgressValue'] = percentage
|
||||||
|
self.env['runtime']['lastProgressTime'] = currentTime
|
||||||
|
return
|
||||||
|
|
||||||
|
# Pattern 2: Fraction (15/100, 3 of 10, etc.)
|
||||||
|
fractionMatch = re.search(r'(\d+)\s*(?:of|/)\s*(\d+)', text)
|
||||||
|
if fractionMatch:
|
||||||
|
current = int(fractionMatch.group(1))
|
||||||
|
total = int(fractionMatch.group(2))
|
||||||
|
if total > 0:
|
||||||
|
percentage = (current / total) * 100
|
||||||
|
if percentage != self.env['runtime']['lastProgressValue']:
|
||||||
|
self.playProgressTone(percentage)
|
||||||
|
self.env['runtime']['lastProgressValue'] = percentage
|
||||||
|
self.env['runtime']['lastProgressTime'] = currentTime
|
||||||
|
return
|
||||||
|
|
||||||
|
# Pattern 3: Progress bars ([#### ], [====> ], etc.)
|
||||||
|
barMatch = re.search(r'\[([#=\-\*]+)([^\]]*)\]', text)
|
||||||
|
if barMatch:
|
||||||
|
filled = len(barMatch.group(1))
|
||||||
|
total = filled + len(barMatch.group(2))
|
||||||
|
if total > 0:
|
||||||
|
percentage = (filled / total) * 100
|
||||||
|
if percentage != self.env['runtime']['lastProgressValue']:
|
||||||
|
self.playProgressTone(percentage)
|
||||||
|
self.env['runtime']['lastProgressValue'] = percentage
|
||||||
|
self.env['runtime']['lastProgressTime'] = currentTime
|
||||||
|
return
|
||||||
|
|
||||||
|
# Pattern 4: Generic activity indicators (Loading..., Working..., etc.)
|
||||||
|
activityPattern = re.search(r'(loading|processing|working|installing|downloading|compiling|building).*\.{2,}', text, re.IGNORECASE)
|
||||||
|
if activityPattern:
|
||||||
|
# Play a steady beep every 2 seconds for ongoing activity
|
||||||
|
if currentTime - self.env['runtime']['lastProgressTime'] >= 2.0:
|
||||||
|
self.playActivityBeep()
|
||||||
|
self.env['runtime']['lastProgressTime'] = currentTime
|
||||||
|
|
||||||
|
def playProgressTone(self, percentage):
|
||||||
|
# Map 0-100% to 400-1200Hz frequency range
|
||||||
|
frequency = 400 + (percentage * 8)
|
||||||
|
frequency = max(400, min(1200, frequency)) # Clamp to safe range
|
||||||
|
self.env['runtime']['outputManager'].playFrequence(frequency, 0.15, interrupt=False)
|
||||||
|
|
||||||
|
def playActivityBeep(self):
|
||||||
|
# Single tone for generic activity
|
||||||
|
self.env['runtime']['outputManager'].playFrequence(800, 0.1, interrupt=False)
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Fenrir TTY screen reader
|
||||||
|
# By Chrys, Storm Dragon, and contributers.
|
||||||
|
|
||||||
|
from fenrirscreenreader.core import debug
|
||||||
|
import re
|
||||||
|
|
||||||
|
class command():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
# Use commandBuffer like other commands
|
||||||
|
if 'silenceUntilPrompt' not in self.env['commandBuffer']:
|
||||||
|
self.env['commandBuffer']['silenceUntilPrompt'] = False
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return _('Toggle speech silence until shell prompt returns')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if self.env['commandBuffer']['silenceUntilPrompt']:
|
||||||
|
self.disableSilence()
|
||||||
|
else:
|
||||||
|
self.enableSilence()
|
||||||
|
|
||||||
|
def enableSilence(self):
|
||||||
|
self.env['commandBuffer']['silenceUntilPrompt'] = True
|
||||||
|
self.env['runtime']['outputManager'].presentText(_("Speech silenced until prompt returns"), soundIcon='SpeechOff', interrupt=True)
|
||||||
|
# Disable speech but don't use the normal temp disable that reactivates on keypress
|
||||||
|
self.env['runtime']['settingsManager'].setSetting('speech', 'enabled', 'False')
|
||||||
|
|
||||||
|
def disableSilence(self):
|
||||||
|
self.env['commandBuffer']['silenceUntilPrompt'] = False
|
||||||
|
# Re-enable speech
|
||||||
|
self.env['runtime']['settingsManager'].setSetting('speech', 'enabled', 'True')
|
||||||
|
self.env['runtime']['outputManager'].presentText(_("Speech restored"), soundIcon='SpeechOn', interrupt=True)
|
||||||
|
|
||||||
|
def checkForPrompt(self, text):
|
||||||
|
"""Check if the current line contains a shell prompt pattern"""
|
||||||
|
if not self.env['commandBuffer']['silenceUntilPrompt']:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# First check for exact matches from settings (with backward compatibility)
|
||||||
|
try:
|
||||||
|
exactMatches = self.env['runtime']['settingsManager'].getSetting('prompt', 'exactMatches')
|
||||||
|
if exactMatches:
|
||||||
|
exactList = [match.strip() for match in exactMatches.split(',') if match.strip()]
|
||||||
|
for exactMatch in exactList:
|
||||||
|
if text.strip() == exactMatch:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Found exact prompt match: " + exactMatch, debug.debugLevel.INFO)
|
||||||
|
self.disableSilence()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
# Prompt section doesn't exist in settings, skip custom exact matches
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get custom patterns from settings (with backward compatibility)
|
||||||
|
promptPatterns = []
|
||||||
|
try:
|
||||||
|
customPatterns = self.env['runtime']['settingsManager'].getSetting('prompt', 'customPatterns')
|
||||||
|
# Add custom patterns from settings if they exist
|
||||||
|
if customPatterns:
|
||||||
|
customList = [pattern.strip() for pattern in customPatterns.split(',') if pattern.strip()]
|
||||||
|
promptPatterns.extend(customList)
|
||||||
|
except:
|
||||||
|
# Prompt section doesn't exist in settings, skip custom patterns
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Add default shell prompt patterns
|
||||||
|
promptPatterns.extend([
|
||||||
|
r'^\s*\\\$\s*$', # Just $ (with whitespace)
|
||||||
|
r'^\s*#\s*$', # Just # (with whitespace)
|
||||||
|
r'^\s*>\s*$', # Just > (with whitespace)
|
||||||
|
r'.*@.*[\\\$#>]\s*$', # Contains @ and ends with prompt char (user@host style)
|
||||||
|
r'^\[.*\]\s*[\\\$#>]\s*$', # [anything]$ style prompts
|
||||||
|
r'^[a-zA-Z0-9._-]+[\\\$#>]\s*$', # Simple shell names like bash-5.1$
|
||||||
|
])
|
||||||
|
|
||||||
|
for pattern in promptPatterns:
|
||||||
|
try:
|
||||||
|
if re.search(pattern, text.strip()):
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Found prompt pattern: " + pattern, debug.debugLevel.INFO)
|
||||||
|
self.disableSilence()
|
||||||
|
return True
|
||||||
|
except re.error as e:
|
||||||
|
# Invalid regex pattern, skip it and log the error
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Invalid prompt pattern: " + pattern + " Error: " + str(e), debug.debugLevel.ERROR)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
||||||
@@ -18,15 +18,19 @@ class command():
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'interruptOnKeyPress'):
|
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'interruptOnKeyPress'):
|
||||||
|
return
|
||||||
|
if self.env['runtime']['inputManager'].noKeyPressed():
|
||||||
return
|
return
|
||||||
if self.env['runtime']['screenManager'].isScreenChange():
|
if self.env['runtime']['screenManager'].isScreenChange():
|
||||||
return
|
return
|
||||||
|
if len(self.env['input']['currInput']) <= len(self.env['input']['prevInput']):
|
||||||
|
return
|
||||||
# if the filter is set
|
# if the filter is set
|
||||||
#if self.env['runtime']['settingsManager'].getSetting('keyboard', 'interruptOnKeyPressFilter').strip() != '':
|
if self.env['runtime']['settingsManager'].getSetting('keyboard', 'interruptOnKeyPressFilter').strip() != '':
|
||||||
# filterList = self.env['runtime']['settingsManager'].getSetting('keyboard', 'interruptOnKeyPressFilter').split(',')
|
filterList = self.env['runtime']['settingsManager'].getSetting('keyboard', 'interruptOnKeyPressFilter').split(',')
|
||||||
# for currInput in self.env['input']['currInput']:
|
for currInput in self.env['input']['currInput']:
|
||||||
# if not currInput in filterList:
|
if not currInput in filterList:
|
||||||
# return
|
return
|
||||||
self.env['runtime']['outputManager'].interruptOutput()
|
self.env['runtime']['outputManager'].interruptOutput()
|
||||||
|
|
||||||
def setCallback(self, callback):
|
def setCallback(self, callback):
|
||||||
|
|||||||
@@ -35,12 +35,18 @@ class command():
|
|||||||
if not (self.env['runtime']['byteManager'].getLastByteKey() in [b'^[[A',b'^[[B']):
|
if not (self.env['runtime']['byteManager'].getLastByteKey() in [b'^[[A',b'^[[B']):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Get the current cursor's line from both old and new content
|
||||||
prevLine = self.env['screen']['oldContentText'].split('\n')[self.env['screen']['newCursor']['y']]
|
prevLine = self.env['screen']['oldContentText'].split('\n')[self.env['screen']['newCursor']['y']]
|
||||||
currLine = self.env['screen']['newContentText'].split('\n')[self.env['screen']['newCursor']['y']]
|
currLine = self.env['screen']['newContentText'].split('\n')[self.env['screen']['newCursor']['y']]
|
||||||
|
|
||||||
|
is_blank = currLine.strip() == ''
|
||||||
|
|
||||||
if prevLine == currLine:
|
if prevLine == currLine:
|
||||||
if self.env['screen']['newDelta'] != '':
|
if self.env['screen']['newDelta'] != '':
|
||||||
return
|
return
|
||||||
if not currLine.isspace():
|
|
||||||
|
announce = currLine
|
||||||
|
if not is_blank:
|
||||||
currPrompt = currLine.find('$')
|
currPrompt = currLine.find('$')
|
||||||
rootPrompt = currLine.find('#')
|
rootPrompt = currLine.find('#')
|
||||||
if currPrompt <= 0:
|
if currPrompt <= 0:
|
||||||
@@ -55,13 +61,13 @@ class command():
|
|||||||
else:
|
else:
|
||||||
announce = currLine
|
announce = currLine
|
||||||
|
|
||||||
if currLine.isspace():
|
if is_blank:
|
||||||
self.env['runtime']['outputManager'].presentText(_("blank"), soundIcon='EmptyLine', interrupt=True, flush=False)
|
self.env['runtime']['outputManager'].presentText(_("blank"), soundIcon='EmptyLine', interrupt=True, flush=False)
|
||||||
else:
|
else:
|
||||||
self.env['runtime']['outputManager'].presentText(announce, interrupt=True, flush=False)
|
self.env['runtime']['outputManager'].presentText(announce, interrupt=True, flush=False)
|
||||||
|
|
||||||
self.env['commandsIgnore']['onScreenUpdate']['CHAR_DELETE_ECHO'] = True
|
self.env['commandsIgnore']['onScreenUpdate']['CHAR_DELETE_ECHO'] = True
|
||||||
self.env['commandsIgnore']['onScreenUpdate']['CHAR_ECHO'] = True
|
self.env['commandsIgnore']['onScreenUpdate']['CHAR_ECHO'] = True
|
||||||
self.env['commandsIgnore']['onScreenUpdate']['INCOMING_IGNORE'] = True
|
self.env['commandsIgnore']['onScreenUpdate']['INCOMING_IGNORE'] = True
|
||||||
def setCallback(self, callback):
|
def setCallback(self, callback):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Fenrir TTY screen reader
|
||||||
|
# By Chrys, Storm Dragon, and contributers.
|
||||||
|
|
||||||
|
from fenrirscreenreader.core import debug
|
||||||
|
|
||||||
|
class command():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return 'Detects progress patterns for progress bar monitoring'
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# Only run if progress monitoring is enabled
|
||||||
|
try:
|
||||||
|
if 'progressMonitoring' in self.env['commandBuffer'] and self.env['commandBuffer']['progressMonitoring']:
|
||||||
|
# Only check new incoming text (newDelta), but filter out screen changes
|
||||||
|
if self.env['screen']['newDelta'] and self.isRealProgressUpdate():
|
||||||
|
self.detectProgress(self.env['screen']['newDelta'])
|
||||||
|
except Exception as e:
|
||||||
|
# Silently ignore errors to avoid disrupting normal operation
|
||||||
|
pass
|
||||||
|
|
||||||
|
def isRealProgressUpdate(self):
|
||||||
|
"""Check if this is a real progress update vs screen change/window switch"""
|
||||||
|
# If the screen/application changed, it's not a progress update
|
||||||
|
if self.env['runtime']['screenManager'].isScreenChange():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If there was a large cursor movement, it's likely navigation, not progress
|
||||||
|
if self.env['runtime']['cursorManager'].isCursorVerticalMove():
|
||||||
|
xMove = abs(self.env['screen']['newCursor']['x'] - self.env['screen']['oldCursor']['x'])
|
||||||
|
yMove = abs(self.env['screen']['newCursor']['y'] - self.env['screen']['oldCursor']['y'])
|
||||||
|
# Large movements suggest navigation, not progress output
|
||||||
|
if yMove > 2 or xMove > 20:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if delta is too large (screen change) vs small incremental updates
|
||||||
|
deltaLength = len(self.env['screen']['newDelta'])
|
||||||
|
if deltaLength > 200: # Allow longer progress lines like Claude Code's status
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def detectProgress(self, text):
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
currentTime = time.time()
|
||||||
|
|
||||||
|
# Debug: Print what we're checking
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Progress detector checking: '" + text + "'", debug.debugLevel.INFO)
|
||||||
|
|
||||||
|
# Note: Auto-disable on 100% completion removed to respect user settings
|
||||||
|
|
||||||
|
# Pattern 1: Percentage (50%, 25.5%, etc.)
|
||||||
|
percentMatch = re.search(r'(\d+(?:\.\d+)?)\s*%', text)
|
||||||
|
if percentMatch:
|
||||||
|
percentage = float(percentMatch.group(1))
|
||||||
|
# Only trigger on realistic progress percentages (0-100%)
|
||||||
|
if 0 <= percentage <= 100:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Found percentage: " + str(percentage), debug.debugLevel.INFO)
|
||||||
|
if percentage != self.env['commandBuffer']['lastProgressValue']:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Playing tone for: " + str(percentage), debug.debugLevel.INFO)
|
||||||
|
self.playProgressTone(percentage)
|
||||||
|
self.env['commandBuffer']['lastProgressValue'] = percentage
|
||||||
|
self.env['commandBuffer']['lastProgressTime'] = currentTime
|
||||||
|
return
|
||||||
|
|
||||||
|
# Pattern 1b: Time/token activity (not percentage-based, so use single beep)
|
||||||
|
timeMatch = re.search(r'(\d+)s\s', text)
|
||||||
|
tokenMatch = re.search(r'(\d+)\s+tokens', text)
|
||||||
|
# Pattern 1c: dd command output (bytes copied with transfer rate)
|
||||||
|
ddMatch = re.search(r'\d+\s+bytes.*copied.*\d+\s+s.*[kMGT]?B/s', text)
|
||||||
|
# Pattern 1d: Curl-style transfer data (bytes, speed indicators)
|
||||||
|
curlMatch = re.search(r'(\d+\s+\d+\s+\d+\s+\d+.*?(?:k|M|G)?.*?--:--:--|Speed)', text)
|
||||||
|
|
||||||
|
if timeMatch or tokenMatch or ddMatch or curlMatch:
|
||||||
|
# For non-percentage progress, use a single activity beep every 2 seconds
|
||||||
|
if currentTime - self.env['commandBuffer']['lastProgressTime'] >= 2.0:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Playing activity beep for transfer progress", debug.debugLevel.INFO)
|
||||||
|
self.playActivityBeep()
|
||||||
|
self.env['commandBuffer']['lastProgressTime'] = currentTime
|
||||||
|
return
|
||||||
|
|
||||||
|
# Pattern 2: Fraction (15/100, 3 of 10, etc.)
|
||||||
|
fractionMatch = re.search(r'(\d+)\s*(?:of|/)\s*(\d+)', text)
|
||||||
|
if fractionMatch:
|
||||||
|
current = int(fractionMatch.group(1))
|
||||||
|
total = int(fractionMatch.group(2))
|
||||||
|
if total > 0:
|
||||||
|
percentage = (current / total) * 100
|
||||||
|
if percentage != self.env['commandBuffer']['lastProgressValue']:
|
||||||
|
self.playProgressTone(percentage)
|
||||||
|
self.env['commandBuffer']['lastProgressValue'] = percentage
|
||||||
|
self.env['commandBuffer']['lastProgressTime'] = currentTime
|
||||||
|
return
|
||||||
|
|
||||||
|
# Pattern 3: Progress bars ([#### ], [====> ], etc.)
|
||||||
|
barMatch = re.search(r'\[([#=\-\*]+)([^\]]*)\]', text)
|
||||||
|
if barMatch:
|
||||||
|
filled = len(barMatch.group(1))
|
||||||
|
total = filled + len(barMatch.group(2))
|
||||||
|
if total > 0:
|
||||||
|
percentage = (filled / total) * 100
|
||||||
|
if percentage != self.env['commandBuffer']['lastProgressValue']:
|
||||||
|
self.playProgressTone(percentage)
|
||||||
|
self.env['commandBuffer']['lastProgressValue'] = percentage
|
||||||
|
self.env['commandBuffer']['lastProgressTime'] = currentTime
|
||||||
|
return
|
||||||
|
|
||||||
|
# Pattern 4: Generic activity indicators (Loading..., Working..., etc.)
|
||||||
|
activityPattern = re.search(r'(loading|processing|working|installing|downloading|compiling|building).*\.{2,}', text, re.IGNORECASE)
|
||||||
|
if activityPattern:
|
||||||
|
# Play a steady beep every 2 seconds for ongoing activity
|
||||||
|
if currentTime - self.env['commandBuffer']['lastProgressTime'] >= 2.0:
|
||||||
|
self.playActivityBeep()
|
||||||
|
self.env['commandBuffer']['lastProgressTime'] = currentTime
|
||||||
|
|
||||||
|
def playProgressTone(self, percentage):
|
||||||
|
# Map 0-100% to 400-1200Hz frequency range
|
||||||
|
frequency = 400 + (percentage * 8)
|
||||||
|
frequency = max(400, min(1200, frequency)) # Clamp to safe range
|
||||||
|
|
||||||
|
# Use Sox directly for clean quiet tones like: play -qn synth .1 tri 400 gain -8
|
||||||
|
self.playQuietTone(frequency, 0.1)
|
||||||
|
|
||||||
|
def playActivityBeep(self):
|
||||||
|
# Single tone for generic activity
|
||||||
|
self.playQuietTone(800, 0.08)
|
||||||
|
|
||||||
|
def playQuietTone(self, frequency, duration):
|
||||||
|
"""Play a quiet tone using Sox directly"""
|
||||||
|
import subprocess
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
# Build the Sox command: play -qn synth <duration> tri <frequency> gain -8
|
||||||
|
command = f"play -qn synth {duration} tri {frequency} gain -8"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Only play if sound is enabled
|
||||||
|
if self.env['runtime']['settingsManager'].getSettingAsBool('sound', 'enabled'):
|
||||||
|
subprocess.Popen(shlex.split(command), stdin=None, stdout=None, stderr=None, shell=False)
|
||||||
|
except Exception as e:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Sox tone error: " + str(e), debug.debugLevel.ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Fenrir TTY screen reader
|
||||||
|
# By Chrys, Storm Dragon, and contributers.
|
||||||
|
|
||||||
|
from fenrirscreenreader.core import debug
|
||||||
|
|
||||||
|
class command():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return 'Detects shell prompts for silence until prompt feature'
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# Only run if silence until prompt is active
|
||||||
|
try:
|
||||||
|
if 'silenceUntilPrompt' in self.env['commandBuffer'] and self.env['commandBuffer']['silenceUntilPrompt']:
|
||||||
|
# Check the current line for prompt patterns
|
||||||
|
if self.env['screen']['newContentText']:
|
||||||
|
lines = self.env['screen']['newContentText'].split('\n')
|
||||||
|
if lines and self.env['screen']['newCursor']['y'] < len(lines):
|
||||||
|
currentLine = lines[self.env['screen']['newCursor']['y']]
|
||||||
|
self.checkForPrompt(currentLine)
|
||||||
|
except Exception as e:
|
||||||
|
# Silently ignore errors to avoid disrupting normal operation
|
||||||
|
pass
|
||||||
|
|
||||||
|
def checkForPrompt(self, text):
|
||||||
|
"""Check if the current line contains a shell prompt pattern"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Debug: Print what we're checking
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Prompt detector checking: '" + text + "'", debug.debugLevel.INFO)
|
||||||
|
|
||||||
|
# First check for exact matches from settings (with backward compatibility)
|
||||||
|
try:
|
||||||
|
exactMatches = self.env['runtime']['settingsManager'].getSetting('prompt', 'exactMatches')
|
||||||
|
if exactMatches:
|
||||||
|
exactList = [match.strip() for match in exactMatches.split(',') if match.strip()]
|
||||||
|
for exactMatch in exactList:
|
||||||
|
if text.strip() == exactMatch:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Found exact prompt match: " + exactMatch, debug.debugLevel.INFO)
|
||||||
|
self._restoreSpeech()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
# Prompt section doesn't exist in settings, skip custom exact matches
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get custom patterns from settings (with backward compatibility)
|
||||||
|
promptPatterns = []
|
||||||
|
try:
|
||||||
|
customPatterns = self.env['runtime']['settingsManager'].getSetting('prompt', 'customPatterns')
|
||||||
|
# Add custom patterns from settings if they exist
|
||||||
|
if customPatterns:
|
||||||
|
customList = [pattern.strip() for pattern in customPatterns.split(',') if pattern.strip()]
|
||||||
|
promptPatterns.extend(customList)
|
||||||
|
except:
|
||||||
|
# Prompt section doesn't exist in settings, skip custom patterns
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Add default shell prompt patterns
|
||||||
|
promptPatterns.extend([
|
||||||
|
r'^\s*\\\$\s*$', # Just $ (with whitespace)
|
||||||
|
r'^\s*#\s*$', # Just # (with whitespace)
|
||||||
|
r'^\s*>\s*$', # Just > (with whitespace)
|
||||||
|
r'.*@.*[\\\$#>]\s*$', # Contains @ and ends with prompt char (user@host style)
|
||||||
|
r'^\[.*\]\s*[\\\$#>]\s*$', # [anything]$ style prompts
|
||||||
|
r'^[a-zA-Z0-9._-]+[\\\$#>]\s*$', # Simple shell names like bash-5.1$
|
||||||
|
])
|
||||||
|
|
||||||
|
for pattern in promptPatterns:
|
||||||
|
try:
|
||||||
|
if re.search(pattern, text.strip()):
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Found prompt pattern: " + pattern, debug.debugLevel.INFO)
|
||||||
|
self._restoreSpeech()
|
||||||
|
return True
|
||||||
|
except re.error as e:
|
||||||
|
# Invalid regex pattern, skip it and log the error
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Invalid prompt pattern: " + pattern + " Error: " + str(e), debug.debugLevel.ERROR)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _restoreSpeech(self):
|
||||||
|
"""Helper method to restore speech when prompt is detected"""
|
||||||
|
# Disable silence mode
|
||||||
|
self.env['commandBuffer']['silenceUntilPrompt'] = False
|
||||||
|
# Re-enable speech
|
||||||
|
self.env['runtime']['settingsManager'].setSetting('speech', 'enabled', 'True')
|
||||||
|
self.env['runtime']['outputManager'].presentText(_("Speech restored"), soundIcon='SpeechOn', interrupt=True)
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
||||||
@@ -105,7 +105,7 @@ class attributeManager():
|
|||||||
cursorPos = cursor.copy()
|
cursorPos = cursor.copy()
|
||||||
try:
|
try:
|
||||||
attribute = self.getAttributeByXY( cursorPos['x'], cursorPos['y'])
|
attribute = self.getAttributeByXY( cursorPos['x'], cursorPos['y'])
|
||||||
|
|
||||||
if update:
|
if update:
|
||||||
self.setLastCursorAttribute(attribute)
|
self.setLastCursorAttribute(attribute)
|
||||||
if not self.isLastCursorAttributeChange():
|
if not self.isLastCursorAttributeChange():
|
||||||
@@ -155,13 +155,13 @@ class attributeManager():
|
|||||||
attributeFormatString = attributeFormatString.replace('fenrirFGColor', _(attribute[0]))
|
attributeFormatString = attributeFormatString.replace('fenrirFGColor', _(attribute[0]))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
attributeFormatString = attributeFormatString.replace('fenrirFGColor', '')
|
attributeFormatString = attributeFormatString.replace('fenrirFGColor', '')
|
||||||
|
|
||||||
# 1 BG color (name)
|
# 1 BG color (name)
|
||||||
try:
|
try:
|
||||||
attributeFormatString = attributeFormatString.replace('fenrirBGColor', _(attribute[1]))
|
attributeFormatString = attributeFormatString.replace('fenrirBGColor', _(attribute[1]))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
attributeFormatString = attributeFormatString.replace('fenrirBGColor', '')
|
attributeFormatString = attributeFormatString.replace('fenrirBGColor', '')
|
||||||
|
|
||||||
# 2 bold (True/ False)
|
# 2 bold (True/ False)
|
||||||
try:
|
try:
|
||||||
if attribute[2]:
|
if attribute[2]:
|
||||||
@@ -169,7 +169,7 @@ class attributeManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
attributeFormatString = attributeFormatString.replace('fenrirBold', '')
|
attributeFormatString = attributeFormatString.replace('fenrirBold', '')
|
||||||
|
|
||||||
# 3 italics (True/ False)
|
# 3 italics (True/ False)
|
||||||
try:
|
try:
|
||||||
if attribute[3]:
|
if attribute[3]:
|
||||||
@@ -177,7 +177,7 @@ class attributeManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
attributeFormatString = attributeFormatString.replace('fenrirItalics', '')
|
attributeFormatString = attributeFormatString.replace('fenrirItalics', '')
|
||||||
|
|
||||||
# 4 underline (True/ False)
|
# 4 underline (True/ False)
|
||||||
try:
|
try:
|
||||||
if attribute[4]:
|
if attribute[4]:
|
||||||
@@ -185,7 +185,7 @@ class attributeManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
attributeFormatString = attributeFormatString.replace('fenrirUnderline', '')
|
attributeFormatString = attributeFormatString.replace('fenrirUnderline', '')
|
||||||
|
|
||||||
# 5 strikethrough (True/ False)
|
# 5 strikethrough (True/ False)
|
||||||
try:
|
try:
|
||||||
if attribute[5]:
|
if attribute[5]:
|
||||||
@@ -193,7 +193,7 @@ class attributeManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
attributeFormatString = attributeFormatString.replace('fenrirStrikethrough', '')
|
attributeFormatString = attributeFormatString.replace('fenrirStrikethrough', '')
|
||||||
|
|
||||||
# 6 reverse (True/ False)
|
# 6 reverse (True/ False)
|
||||||
try:
|
try:
|
||||||
if attribute[6]:
|
if attribute[6]:
|
||||||
@@ -201,7 +201,7 @@ class attributeManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
attributeFormatString = attributeFormatString.replace('fenrirReverse', '')
|
attributeFormatString = attributeFormatString.replace('fenrirReverse', '')
|
||||||
|
|
||||||
# 7 blink (True/ False)
|
# 7 blink (True/ False)
|
||||||
try:
|
try:
|
||||||
if attribute[7]:
|
if attribute[7]:
|
||||||
@@ -209,7 +209,7 @@ class attributeManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
attributeFormatString = attributeFormatString.replace('fenrirBlink', '')
|
attributeFormatString = attributeFormatString.replace('fenrirBlink', '')
|
||||||
|
|
||||||
# 8 font size (int/ string)
|
# 8 font size (int/ string)
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
@@ -223,14 +223,14 @@ class attributeManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
attributeFormatString = attributeFormatString.replace('fenrirFontSize', _('default'))
|
attributeFormatString = attributeFormatString.replace('fenrirFontSize', _('default'))
|
||||||
|
|
||||||
# 9 font family (string)
|
# 9 font family (string)
|
||||||
try:
|
try:
|
||||||
attributeFormatString = attributeFormatString.replace('fenrirFont', attribute[9])
|
attributeFormatString = attributeFormatString.replace('fenrirFont', attribute[9])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
attributeFormatString = attributeFormatString.replace('fenrirFont', _('default'))
|
attributeFormatString = attributeFormatString.replace('fenrirFont', _('default'))
|
||||||
|
|
||||||
return attributeFormatString
|
return attributeFormatString
|
||||||
def trackHighlights(self):
|
def trackHighlights(self):
|
||||||
result = ''
|
result = ''
|
||||||
@@ -287,4 +287,4 @@ class attributeManager():
|
|||||||
useful = True
|
useful = True
|
||||||
|
|
||||||
return useful
|
return useful
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class barrierManager():
|
|||||||
def updateBarrierChange(self, isBarrier):
|
def updateBarrierChange(self, isBarrier):
|
||||||
self.prefIsBarrier = self.currIsBarrier
|
self.prefIsBarrier = self.currIsBarrier
|
||||||
self.currIsBarrier = isBarrier
|
self.currIsBarrier = isBarrier
|
||||||
|
|
||||||
def resetBarrierChange(self):
|
def resetBarrierChange(self):
|
||||||
self.currIsBarrier = False
|
self.currIsBarrier = False
|
||||||
self.prefIsBarrier = False
|
self.prefIsBarrier = False
|
||||||
@@ -38,7 +38,7 @@ class barrierManager():
|
|||||||
self.env['runtime']['outputManager'].playSoundIcon(soundIcon='BarrierStart', interrupt=doInterrupt)
|
self.env['runtime']['outputManager'].playSoundIcon(soundIcon='BarrierStart', interrupt=doInterrupt)
|
||||||
else:
|
else:
|
||||||
self.env['runtime']['outputManager'].playSoundIcon(soundIcon='BarrierEnd', interrupt=doInterrupt)
|
self.env['runtime']['outputManager'].playSoundIcon(soundIcon='BarrierEnd', interrupt=doInterrupt)
|
||||||
|
|
||||||
if not isBarrier:
|
if not isBarrier:
|
||||||
sayLine = ''
|
sayLine = ''
|
||||||
return isBarrier, sayLine
|
return isBarrier, sayLine
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class commandManager():
|
|||||||
|
|
||||||
# scripts for scriptKey
|
# scripts for scriptKey
|
||||||
self.env['runtime']['commandManager'].loadScriptCommands()
|
self.env['runtime']['commandManager'].loadScriptCommands()
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
for commandFolder in self.env['general']['commandFolderList']:
|
for commandFolder in self.env['general']['commandFolderList']:
|
||||||
self.env['runtime']['commandManager'].shutdownCommands(commandFolder)
|
self.env['runtime']['commandManager'].shutdownCommands(commandFolder)
|
||||||
@@ -99,7 +99,7 @@ class commandManager():
|
|||||||
self.env['runtime']['debug'].writeDebugOut("loadCommands: Loading command:" + command ,debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("loadCommands: Loading command:" + command ,debug.debugLevel.ERROR)
|
||||||
self.env['runtime']['debug'].writeDebugOut(str(e),debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut(str(e),debug.debugLevel.ERROR)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
def loadScriptCommands(self, section='commands', scriptPath=''):
|
def loadScriptCommands(self, section='commands', scriptPath=''):
|
||||||
if scriptPath =='':
|
if scriptPath =='':
|
||||||
scriptPath = self.env['runtime']['settingsManager'].getSetting('general', 'scriptPath')
|
scriptPath = self.env['runtime']['settingsManager'].getSetting('general', 'scriptPath')
|
||||||
@@ -159,18 +159,24 @@ class commandManager():
|
|||||||
self.env['runtime']['debug'].writeDebugOut("Loading script:" + fileName ,debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("Loading script:" + fileName ,debug.debugLevel.ERROR)
|
||||||
self.env['runtime']['debug'].writeDebugOut(str(e),debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut(str(e),debug.debugLevel.ERROR)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
def shutdownCommands(self, section):
|
def shutdownCommands(self, section):
|
||||||
|
# Check if the section exists in the commands dictionary
|
||||||
|
if section not in self.env['commands']:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("shutdownCommands: section not found:" + section, debug.debugLevel.WARNING)
|
||||||
|
return
|
||||||
|
|
||||||
for command in sorted(self.env['commands'][section]):
|
for command in sorted(self.env['commands'][section]):
|
||||||
try:
|
try:
|
||||||
self.env['commands'][section][command].shutdown()
|
self.env['commands'][section][command].shutdown()
|
||||||
del self.env['commands'][section][command]
|
del self.env['commands'][section][command]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut("Shutdown command:" + section + "." + command ,debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("Shutdown command:" + section + "." + command, debug.debugLevel.ERROR)
|
||||||
self.env['runtime']['debug'].writeDebugOut(str(e),debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
def executeSwitchTrigger(self, trigger, unLoadScript, loadScript):
|
def executeSwitchTrigger(self, trigger, unLoadScript, loadScript):
|
||||||
if self.env['runtime']['screenManager'].isSuspendingScreen():
|
if self.env['runtime']['screenManager'].isIgnoredScreen():
|
||||||
return
|
return
|
||||||
#unload
|
#unload
|
||||||
oldScript = unLoadScript
|
oldScript = unLoadScript
|
||||||
@@ -193,7 +199,7 @@ class commandManager():
|
|||||||
|
|
||||||
def executeDefaultTrigger(self, trigger, force=False):
|
def executeDefaultTrigger(self, trigger, force=False):
|
||||||
if not force:
|
if not force:
|
||||||
if self.env['runtime']['screenManager'].isSuspendingScreen():
|
if self.env['runtime']['screenManager'].isIgnoredScreen():
|
||||||
return
|
return
|
||||||
for command in sorted(self.env['commands'][trigger]):
|
for command in sorted(self.env['commands'][trigger]):
|
||||||
if self.commandExists(command, trigger):
|
if self.commandExists(command, trigger):
|
||||||
@@ -208,7 +214,7 @@ class commandManager():
|
|||||||
self.env['runtime']['debug'].writeDebugOut("Executing trigger:" + trigger + "." + command + str(e) ,debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("Executing trigger:" + trigger + "." + command + str(e) ,debug.debugLevel.ERROR)
|
||||||
|
|
||||||
def executeCommand(self, command, section = 'commands'):
|
def executeCommand(self, command, section = 'commands'):
|
||||||
if self.env['runtime']['screenManager'].isSuspendingScreen():
|
if self.env['runtime']['screenManager'].isIgnoredScreen():
|
||||||
return
|
return
|
||||||
if self.commandExists(command, section):
|
if self.commandExists(command, section):
|
||||||
try:
|
try:
|
||||||
@@ -222,7 +228,7 @@ class commandManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut("Executing command:" + section + "." + command +' ' + str(e),debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("Executing command:" + section + "." + command +' ' + str(e),debug.debugLevel.ERROR)
|
||||||
|
|
||||||
|
|
||||||
def runCommand(self, command, section = 'commands'):
|
def runCommand(self, command, section = 'commands'):
|
||||||
if self.commandExists(command, section):
|
if self.commandExists(command, section):
|
||||||
try:
|
try:
|
||||||
@@ -231,7 +237,7 @@ class commandManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut("runCommand command:" + section + "." + command +' ' + str(e),debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("runCommand command:" + section + "." + command +' ' + str(e),debug.debugLevel.ERROR)
|
||||||
self.env['commandInfo']['lastCommandExecutionTime'] = time.time()
|
self.env['commandInfo']['lastCommandExecutionTime'] = time.time()
|
||||||
|
|
||||||
def getCommandDescription(self, command, section = 'commands'):
|
def getCommandDescription(self, command, section = 'commands'):
|
||||||
if self.commandExists(command, section):
|
if self.commandExists(command, section):
|
||||||
try:
|
try:
|
||||||
@@ -239,7 +245,7 @@ class commandManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut('commandManager.getCommandDescription:' + str(e),debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut('commandManager.getCommandDescription:' + str(e),debug.debugLevel.ERROR)
|
||||||
self.env['commandInfo']['lastCommandExecutionTime'] = time.time()
|
self.env['commandInfo']['lastCommandExecutionTime'] = time.time()
|
||||||
|
|
||||||
def commandExists(self, command, section = 'commands'):
|
def commandExists(self, command, section = 'commands'):
|
||||||
try:
|
try:
|
||||||
return( command in self.env['commands'][section])
|
return( command in self.env['commands'][section])
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ class cursorManager():
|
|||||||
pass
|
pass
|
||||||
def initialize(self, environment):
|
def initialize(self, environment):
|
||||||
self.env = environment
|
self.env = environment
|
||||||
|
def shouldProcessNumpadCommands(self):
|
||||||
|
"""
|
||||||
|
Check if numpad commands should be processed based on numlock state
|
||||||
|
Return True if numlock is OFF (commands should work)
|
||||||
|
Return False if numlock is ON (let keys type numbers)
|
||||||
|
"""
|
||||||
|
# Return False if numlock is ON
|
||||||
|
return not self.env['input']['newNumLock']
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
pass
|
pass
|
||||||
def clearMarks(self):
|
def clearMarks(self):
|
||||||
@@ -47,7 +55,7 @@ class cursorManager():
|
|||||||
return
|
return
|
||||||
self.env['screen']['oldCursorReview'] = None
|
self.env['screen']['oldCursorReview'] = None
|
||||||
self.env['screen']['newCursorReview'] = None
|
self.env['screen']['newCursorReview'] = None
|
||||||
|
|
||||||
def isCursorHorizontalMove(self):
|
def isCursorHorizontalMove(self):
|
||||||
return self.env['screen']['newCursor']['x'] != self.env['screen']['oldCursor']['x']
|
return self.env['screen']['newCursor']['x'] != self.env['screen']['oldCursor']['x']
|
||||||
|
|
||||||
@@ -56,7 +64,7 @@ class cursorManager():
|
|||||||
|
|
||||||
def isReviewMode(self):
|
def isReviewMode(self):
|
||||||
return self.env['screen']['newCursorReview'] != None
|
return self.env['screen']['newCursorReview'] != None
|
||||||
|
|
||||||
def enterReviewModeCurrTextCursor(self, overwrite=False):
|
def enterReviewModeCurrTextCursor(self, overwrite=False):
|
||||||
if self.isReviewMode() and not overwrite:
|
if self.isReviewMode() and not overwrite:
|
||||||
return
|
return
|
||||||
@@ -73,7 +81,7 @@ class cursorManager():
|
|||||||
self.env['screen']['oldCursorReview'] = self.env['screen']['newCursorReview']
|
self.env['screen']['oldCursorReview'] = self.env['screen']['newCursorReview']
|
||||||
self.env['screen']['newCursorReview']['x'] = x
|
self.env['screen']['newCursorReview']['x'] = x
|
||||||
self.env['screen']['newCursorReview']['y'] = y
|
self.env['screen']['newCursorReview']['y'] = y
|
||||||
|
|
||||||
def isApplicationWindowSet(self):
|
def isApplicationWindowSet(self):
|
||||||
try:
|
try:
|
||||||
currApp = self.env['runtime']['applicationManager'].getCurrentApplication()
|
currApp = self.env['runtime']['applicationManager'].getCurrentApplication()
|
||||||
@@ -108,7 +116,7 @@ class cursorManager():
|
|||||||
|
|
||||||
currApp = self.env['runtime']['applicationManager'].getCurrentApplication()
|
currApp = self.env['runtime']['applicationManager'].getCurrentApplication()
|
||||||
self.env['commandBuffer']['windowArea'][currApp] = {}
|
self.env['commandBuffer']['windowArea'][currApp] = {}
|
||||||
|
|
||||||
if x1 * y1 <= \
|
if x1 * y1 <= \
|
||||||
x2 * y2:
|
x2 * y2:
|
||||||
self.env['commandBuffer']['windowArea'][currApp]['1'] = {'x':x1, 'y':y1}
|
self.env['commandBuffer']['windowArea'][currApp]['1'] = {'x':x1, 'y':y1}
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ class debugManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
def writeDebugOut(self, text, level = debug.debugLevel.DEACTIVE, onAnyLevel=False):
|
def writeDebugOut(self, text, level = debug.debugLevel.DEACTIVE, onAnyLevel=False):
|
||||||
|
|
||||||
mode = self.env['runtime']['settingsManager'].getSetting('general','debugMode')
|
mode = self.env['runtime']['settingsManager'].getSetting('general','debugMode')
|
||||||
if mode == '':
|
if mode == '':
|
||||||
mode = 'FILE'
|
mode = 'FILE'
|
||||||
mode = mode.upper().split(',')
|
mode = mode.upper().split(',')
|
||||||
fileMode = 'FILE' in mode
|
fileMode = 'FILE' in mode
|
||||||
printMode = 'PRINT' in mode
|
printMode = 'PRINT' in mode
|
||||||
|
|
||||||
if (self.env['runtime']['settingsManager'].getSettingAsInt('general','debugLevel') < int(level)) and \
|
if (self.env['runtime']['settingsManager'].getSettingAsInt('general','debugLevel') < int(level)) and \
|
||||||
not (onAnyLevel and self.env['runtime']['settingsManager'].getSettingAsInt('general','debugLevel') > int(debug.debugLevel.DEACTIVE)) :
|
not (onAnyLevel and self.env['runtime']['settingsManager'].getSettingAsInt('general','debugLevel') > int(debug.debugLevel.DEACTIVE)) :
|
||||||
if self._fileOpened:
|
if self._fileOpened:
|
||||||
@@ -52,12 +52,15 @@ class debugManager():
|
|||||||
else:
|
else:
|
||||||
if not self._fileOpened and fileMode:
|
if not self._fileOpened and fileMode:
|
||||||
self.openDebugFile()
|
self.openDebugFile()
|
||||||
|
timestamp = str(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f'))
|
||||||
if onAnyLevel:
|
if onAnyLevel:
|
||||||
msg = 'ANY '+ str(level) + ' ' + str(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f'))
|
levelInfo = 'INFO ANY'
|
||||||
else:
|
else:
|
||||||
msg = str(level) +' ' + str(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')
|
levelInfo = str(level)
|
||||||
)
|
|
||||||
msg += ': ' + text
|
# Changed order: text comes first, then level and timestamp
|
||||||
|
msg = text + ' - ' + levelInfo + ' ' + timestamp
|
||||||
|
|
||||||
if printMode:
|
if printMode:
|
||||||
print(msg)
|
print(msg)
|
||||||
if fileMode:
|
if fileMode:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class eventManager():
|
|||||||
self.env = environment
|
self.env = environment
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.cleanEventQueue()
|
self.cleanEventQueue()
|
||||||
|
|
||||||
def proceedEventLoop(self):
|
def proceedEventLoop(self):
|
||||||
event = self._eventQueue.get()
|
event = self._eventQueue.get()
|
||||||
st = time.time()
|
st = time.time()
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ class fenrirManager():
|
|||||||
raise RuntimeError('Cannot Initialize. Maybe the configfile is not available or not parseable')
|
raise RuntimeError('Cannot Initialize. Maybe the configfile is not available or not parseable')
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
self.environment['runtime']['outputManager'].presentText(_("Start Fenrir"), soundIcon='ScreenReaderOn', interrupt=True)
|
self.environment['runtime']['outputManager'].presentText(_("Start Fenrir"), soundIcon='ScreenReaderOn', interrupt=True)
|
||||||
signal.signal(signal.SIGINT, self.captureSignal)
|
signal.signal(signal.SIGINT, self.captureSignal)
|
||||||
signal.signal(signal.SIGTERM, self.captureSignal)
|
signal.signal(signal.SIGTERM, self.captureSignal)
|
||||||
|
|
||||||
self.isInitialized = True
|
self.isInitialized = True
|
||||||
self.modifierInput = False
|
self.modifierInput = False
|
||||||
self.singleKeyCommand = False
|
self.singleKeyCommand = False
|
||||||
@@ -42,10 +42,10 @@ class fenrirManager():
|
|||||||
|
|
||||||
def handleInput(self, event):
|
def handleInput(self, event):
|
||||||
self.environment['runtime']['debug'].writeDebugOut('DEBUG INPUT fenrirMan:' + str(event), debug.debugLevel.INFO)
|
self.environment['runtime']['debug'].writeDebugOut('DEBUG INPUT fenrirMan:' + str(event), debug.debugLevel.INFO)
|
||||||
|
|
||||||
if not event['Data']:
|
if not event['Data']:
|
||||||
event['Data'] = self.environment['runtime']['inputManager'].getInputEvent()
|
event['Data'] = self.environment['runtime']['inputManager'].getInputEvent()
|
||||||
|
|
||||||
if event['Data']:
|
if event['Data']:
|
||||||
event['Data']['EventName'] = self.environment['runtime']['inputManager'].convertEventName(event['Data']['EventName'])
|
event['Data']['EventName'] = self.environment['runtime']['inputManager'].convertEventName(event['Data']['EventName'])
|
||||||
self.environment['runtime']['inputManager'].handleInputEvent(event['Data'])
|
self.environment['runtime']['inputManager'].handleInputEvent(event['Data'])
|
||||||
@@ -54,8 +54,8 @@ class fenrirManager():
|
|||||||
|
|
||||||
if self.environment['runtime']['inputManager'].noKeyPressed():
|
if self.environment['runtime']['inputManager'].noKeyPressed():
|
||||||
self.environment['runtime']['inputManager'].clearLastDeepInput()
|
self.environment['runtime']['inputManager'].clearLastDeepInput()
|
||||||
|
|
||||||
if self.environment['runtime']['screenManager'].isSuspendingScreen():
|
if self.environment['runtime']['screenManager'].isIgnoredScreen():
|
||||||
self.environment['runtime']['inputManager'].writeEventBuffer()
|
self.environment['runtime']['inputManager'].writeEventBuffer()
|
||||||
else:
|
else:
|
||||||
if self.environment['runtime']['helpManager'].isTutorialMode():
|
if self.environment['runtime']['helpManager'].isTutorialMode():
|
||||||
@@ -74,7 +74,7 @@ class fenrirManager():
|
|||||||
self.environment['runtime']['inputManager'].clearEventBuffer()
|
self.environment['runtime']['inputManager'].clearEventBuffer()
|
||||||
else:
|
else:
|
||||||
self.environment['runtime']['inputManager'].writeEventBuffer()
|
self.environment['runtime']['inputManager'].writeEventBuffer()
|
||||||
|
|
||||||
if self.environment['runtime']['inputManager'].noKeyPressed():
|
if self.environment['runtime']['inputManager'].noKeyPressed():
|
||||||
self.modifierInput = False
|
self.modifierInput = False
|
||||||
self.singleKeyCommand = False
|
self.singleKeyCommand = False
|
||||||
@@ -83,7 +83,7 @@ class fenrirManager():
|
|||||||
|
|
||||||
if self.environment['input']['keyForeward'] > 0:
|
if self.environment['input']['keyForeward'] > 0:
|
||||||
self.environment['input']['keyForeward'] -= 1
|
self.environment['input']['keyForeward'] -= 1
|
||||||
|
|
||||||
self.environment['runtime']['commandManager'].executeDefaultTrigger('onKeyInput')
|
self.environment['runtime']['commandManager'].executeDefaultTrigger('onKeyInput')
|
||||||
|
|
||||||
def handleByteInput(self, event):
|
def handleByteInput(self, event):
|
||||||
@@ -124,14 +124,14 @@ class fenrirManager():
|
|||||||
|
|
||||||
def handleScreenUpdate(self, event):
|
def handleScreenUpdate(self, event):
|
||||||
self.environment['runtime']['screenManager'].handleScreenUpdate(event['Data'])
|
self.environment['runtime']['screenManager'].handleScreenUpdate(event['Data'])
|
||||||
|
|
||||||
if time.time() - self.environment['runtime']['inputManager'].getLastInputTime() >= 0.3:
|
if time.time() - self.environment['runtime']['inputManager'].getLastInputTime() >= 0.3:
|
||||||
self.environment['runtime']['inputManager'].clearLastDeepInput()
|
self.environment['runtime']['inputManager'].clearLastDeepInput()
|
||||||
|
|
||||||
if (self.environment['runtime']['cursorManager'].isCursorVerticalMove() or
|
if (self.environment['runtime']['cursorManager'].isCursorVerticalMove() or
|
||||||
self.environment['runtime']['cursorManager'].isCursorHorizontalMove()):
|
self.environment['runtime']['cursorManager'].isCursorHorizontalMove()):
|
||||||
self.environment['runtime']['commandManager'].executeDefaultTrigger('onCursorChange')
|
self.environment['runtime']['commandManager'].executeDefaultTrigger('onCursorChange')
|
||||||
|
|
||||||
self.environment['runtime']['commandManager'].executeDefaultTrigger('onScreenUpdate')
|
self.environment['runtime']['commandManager'].executeDefaultTrigger('onScreenUpdate')
|
||||||
self.environment['runtime']['inputManager'].clearLastDeepInput()
|
self.environment['runtime']['inputManager'].clearLastDeepInput()
|
||||||
|
|
||||||
@@ -150,17 +150,17 @@ class fenrirManager():
|
|||||||
def detectShortcutCommand(self):
|
def detectShortcutCommand(self):
|
||||||
if self.environment['input']['keyForeward'] > 0:
|
if self.environment['input']['keyForeward'] > 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(self.environment['input']['prevInput']) > len(self.environment['input']['currInput']):
|
if len(self.environment['input']['prevInput']) > len(self.environment['input']['currInput']):
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.environment['runtime']['inputManager'].isKeyPress():
|
if self.environment['runtime']['inputManager'].isKeyPress():
|
||||||
self.modifierInput = self.environment['runtime']['inputManager'].currKeyIsModifier()
|
self.modifierInput = self.environment['runtime']['inputManager'].currKeyIsModifier()
|
||||||
else:
|
else:
|
||||||
if not self.environment['runtime']['inputManager'].noKeyPressed():
|
if not self.environment['runtime']['inputManager'].noKeyPressed():
|
||||||
if self.singleKeyCommand:
|
if self.singleKeyCommand:
|
||||||
self.singleKeyCommand = len(self.environment['input']['currInput']) == 1
|
self.singleKeyCommand = len(self.environment['input']['currInput']) == 1
|
||||||
|
|
||||||
if not(self.singleKeyCommand and self.environment['runtime']['inputManager'].noKeyPressed()):
|
if not(self.singleKeyCommand and self.environment['runtime']['inputManager'].noKeyPressed()):
|
||||||
currentShortcut = self.environment['runtime']['inputManager'].getCurrShortcut()
|
currentShortcut = self.environment['runtime']['inputManager'].getCurrShortcut()
|
||||||
self.command = self.environment['runtime']['inputManager'].getCommandForShortcut(currentShortcut)
|
self.command = self.environment['runtime']['inputManager'].getCommandForShortcut(currentShortcut)
|
||||||
@@ -220,7 +220,7 @@ class fenrirManager():
|
|||||||
self.environment['runtime']['outputManager'].presentText(_("Quit Fenrir"), soundIcon='ScreenReaderOff', interrupt=True)
|
self.environment['runtime']['outputManager'].presentText(_("Quit Fenrir"), soundIcon='ScreenReaderOff', interrupt=True)
|
||||||
self.environment['runtime']['eventManager'].cleanEventQueue()
|
self.environment['runtime']['eventManager'].cleanEventQueue()
|
||||||
time.sleep(0.6)
|
time.sleep(0.6)
|
||||||
|
|
||||||
for currentManager in self.environment['general']['managerList']:
|
for currentManager in self.environment['general']['managerList']:
|
||||||
if self.environment['runtime'][currentManager]:
|
if self.environment['runtime'][currentManager]:
|
||||||
self.environment['runtime'][currentManager].shutdown()
|
self.environment['runtime'][currentManager].shutdown()
|
||||||
|
|||||||
@@ -42,6 +42,25 @@ class inputDriver():
|
|||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
return True
|
return True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def forceUngrab(self):
|
||||||
|
"""Emergency method to release grabbed devices in case of failure"""
|
||||||
|
if not self._initialized:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
# Try standard ungrab first
|
||||||
|
return self.ungrabAllDevices()
|
||||||
|
except Exception as e:
|
||||||
|
# Just log the failure and inform the user
|
||||||
|
if hasattr(self, 'env') and 'runtime' in self.env and 'debug' in self.env['runtime']:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut(
|
||||||
|
f"Emergency device release failed: {str(e)}",
|
||||||
|
debug.debugLevel.ERROR
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"Emergency device release failed: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
def hasIDevices(self):
|
def hasIDevices(self):
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class inputManager():
|
|||||||
return event
|
return event
|
||||||
def setExecuteDeviceGrab(self, newExecuteDeviceGrab = True):
|
def setExecuteDeviceGrab(self, newExecuteDeviceGrab = True):
|
||||||
self.executeDeviceGrab = newExecuteDeviceGrab
|
self.executeDeviceGrab = newExecuteDeviceGrab
|
||||||
|
|
||||||
def handleDeviceGrab(self, force = False):
|
def handleDeviceGrab(self, force = False):
|
||||||
if force:
|
if force:
|
||||||
self.setExecuteDeviceGrab()
|
self.setExecuteDeviceGrab()
|
||||||
@@ -61,17 +62,38 @@ class inputManager():
|
|||||||
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
|
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
|
||||||
self.executeDeviceGrab = False
|
self.executeDeviceGrab = False
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Add maximum retries to prevent infinite loops
|
||||||
|
maxRetries = 5
|
||||||
|
retryCount = 0
|
||||||
|
grabTimeout = 3 # Timeout in seconds
|
||||||
|
startTime = time.time()
|
||||||
|
|
||||||
if self.env['runtime']['screenManager'].getCurrScreenIgnored():
|
if self.env['runtime']['screenManager'].getCurrScreenIgnored():
|
||||||
while not self.ungrabAllDevices():
|
while not self.ungrabAllDevices():
|
||||||
|
retryCount += 1
|
||||||
|
if retryCount >= maxRetries or (time.time() - startTime) > grabTimeout:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Failed to ungrab devices after multiple attempts", debug.debugLevel.ERROR)
|
||||||
|
# Force a release of devices if possible through alternative means
|
||||||
|
try:
|
||||||
|
self.env['runtime']['inputDriver'].forceUngrab()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
break
|
||||||
time.sleep(0.25)
|
time.sleep(0.25)
|
||||||
self.env['runtime']['debug'].writeDebugOut("retry ungrabAllDevices " ,debug.debugLevel.WARNING)
|
self.env['runtime']['debug'].writeDebugOut(f"retry ungrabAllDevices {retryCount}/{maxRetries}", debug.debugLevel.WARNING)
|
||||||
self.env['runtime']['debug'].writeDebugOut("All devices ungrabbed" ,debug.debugLevel.INFO)
|
|
||||||
else:
|
else:
|
||||||
while not self.grabAllDevices():
|
while not self.grabAllDevices():
|
||||||
|
retryCount += 1
|
||||||
|
if retryCount >= maxRetries or (time.time() - startTime) > grabTimeout:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Failed to grab devices after multiple attempts", debug.debugLevel.ERROR)
|
||||||
|
# Continue without grabbing input - limited functionality but not locked
|
||||||
|
break
|
||||||
time.sleep(0.25)
|
time.sleep(0.25)
|
||||||
self.env['runtime']['debug'].writeDebugOut("retry grabAllDevices" ,debug.debugLevel.WARNING)
|
self.env['runtime']['debug'].writeDebugOut(f"retry grabAllDevices {retryCount}/{maxRetries}", debug.debugLevel.WARNING)
|
||||||
self.env['runtime']['debug'].writeDebugOut("All devices grabbed" ,debug.debugLevel.INFO)
|
|
||||||
self.executeDeviceGrab = False
|
self.executeDeviceGrab = False
|
||||||
|
|
||||||
def sendKeys(self, keyMacro):
|
def sendKeys(self, keyMacro):
|
||||||
for e in keyMacro:
|
for e in keyMacro:
|
||||||
key = ''
|
key = ''
|
||||||
@@ -252,17 +274,39 @@ class inputManager():
|
|||||||
def getCurrShortcut(self, inputSequence = None):
|
def getCurrShortcut(self, inputSequence = None):
|
||||||
shortcut = []
|
shortcut = []
|
||||||
shortcut.append(self.env['input']['shortcutRepeat'])
|
shortcut.append(self.env['input']['shortcutRepeat'])
|
||||||
|
|
||||||
|
numpadKeys = ['KEY_KP0', 'KEY_KP1', 'KEY_KP2', 'KEY_KP3', 'KEY_KP4',
|
||||||
|
'KEY_KP5', 'KEY_KP6', 'KEY_KP7', 'KEY_KP8', 'KEY_KP9',
|
||||||
|
'KEY_KPDOT', 'KEY_KPPLUS', 'KEY_KPMINUS', 'KEY_KPASTERISK',
|
||||||
|
'KEY_KPSLASH', 'KEY_KPENTER', 'KEY_KPEQUAL']
|
||||||
if inputSequence:
|
if inputSequence:
|
||||||
|
# Check if any key in the sequence is a numpad key and numlock is ON
|
||||||
|
# If numlock is ON and any key in the sequence is a numpad key, return an empty shortcut
|
||||||
|
if not self.env['runtime']['cursorManager'].shouldProcessNumpadCommands():
|
||||||
|
for key in inputSequence:
|
||||||
|
if key in numpadKeys:
|
||||||
|
# Return an empty/invalid shortcut that won't match any command
|
||||||
|
return "[]"
|
||||||
|
|
||||||
shortcut.append(inputSequence)
|
shortcut.append(inputSequence)
|
||||||
else:
|
else:
|
||||||
|
# Same check for current input
|
||||||
|
|
||||||
|
if not self.env['runtime']['cursorManager'].shouldProcessNumpadCommands():
|
||||||
|
for key in self.env['input']['currInput']:
|
||||||
|
if key in numpadKeys:
|
||||||
|
# Return an empty/invalid shortcut that won't match any command
|
||||||
|
return "[]"
|
||||||
|
|
||||||
shortcut.append(self.env['input']['currInput'])
|
shortcut.append(self.env['input']['currInput'])
|
||||||
|
|
||||||
if len(self.env['input']['prevInput']) < len(self.env['input']['currInput']):
|
if len(self.env['input']['prevInput']) < len(self.env['input']['currInput']):
|
||||||
if self.env['input']['shortcutRepeat'] > 1 and not self.shortcutExists(str(shortcut)):
|
if self.env['input']['shortcutRepeat'] > 1 and not self.shortcutExists(str(shortcut)):
|
||||||
shortcut = []
|
shortcut = []
|
||||||
self.env['input']['shortcutRepeat'] = 1
|
self.env['input']['shortcutRepeat'] = 1
|
||||||
shortcut.append(self.env['input']['shortcutRepeat'])
|
shortcut.append(self.env['input']['shortcutRepeat'])
|
||||||
shortcut.append(self.env['input']['currInput'])
|
shortcut.append(self.env['input']['currInput'])
|
||||||
self.env['runtime']['debug'].writeDebugOut("currShortcut " + str(shortcut) ,debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut("currShortcut " + str(shortcut), debug.debugLevel.INFO)
|
||||||
return str(shortcut)
|
return str(shortcut)
|
||||||
|
|
||||||
def currKeyIsModifier(self):
|
def currKeyIsModifier(self):
|
||||||
@@ -345,3 +389,35 @@ class inputManager():
|
|||||||
self.lastDetectedDevices =devices
|
self.lastDetectedDevices =devices
|
||||||
def getLastDetectedDevices(self):
|
def getLastDetectedDevices(self):
|
||||||
return self.lastDetectedDevices
|
return self.lastDetectedDevices
|
||||||
|
def reloadShortcuts(self):
|
||||||
|
"""Reload keyboard shortcuts from current layout setting"""
|
||||||
|
# Clear existing bindings
|
||||||
|
self.env['bindings'].clear()
|
||||||
|
self.env['rawBindings'].clear()
|
||||||
|
|
||||||
|
# Get current layout path
|
||||||
|
layout_setting = self.env['runtime']['settingsManager'].getSetting('keyboard', 'keyboardLayout')
|
||||||
|
|
||||||
|
# Resolve full path if needed
|
||||||
|
if not os.path.exists(layout_setting):
|
||||||
|
settingsRoot = '/etc/fenrirscreenreader/'
|
||||||
|
if not os.path.exists(settingsRoot):
|
||||||
|
import fenrirscreenreader
|
||||||
|
fenrirPath = os.path.dirname(fenrirscreenreader.__file__)
|
||||||
|
settingsRoot = fenrirPath + '/../../config/'
|
||||||
|
|
||||||
|
layout_path = settingsRoot + 'keyboard/' + layout_setting + '.conf'
|
||||||
|
if not os.path.exists(layout_path):
|
||||||
|
# Fallback to default if layout not found
|
||||||
|
layout_path = settingsRoot + 'keyboard/desktop.conf'
|
||||||
|
else:
|
||||||
|
layout_path = layout_setting
|
||||||
|
|
||||||
|
# Reload shortcuts
|
||||||
|
self.loadShortcuts(layout_path)
|
||||||
|
|
||||||
|
self.env['runtime']['debug'].writeDebugOut(
|
||||||
|
"Reloaded shortcuts from: " + layout_path,
|
||||||
|
debug.debugLevel.INFO,
|
||||||
|
onAnyLevel=True
|
||||||
|
)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class outputManager():
|
|||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.env['runtime']['settingsManager'].shutdownDriver('soundDriver')
|
self.env['runtime']['settingsManager'].shutdownDriver('soundDriver')
|
||||||
self.env['runtime']['settingsManager'].shutdownDriver('speechDriver')
|
self.env['runtime']['settingsManager'].shutdownDriver('speechDriver')
|
||||||
|
|
||||||
def presentText(self, text, interrupt=True, soundIcon='', ignorePunctuation=False, announceCapital=False, flush=True):
|
def presentText(self, text, interrupt=True, soundIcon='', ignorePunctuation=False, announceCapital=False, flush=True):
|
||||||
if text == '':
|
if text == '':
|
||||||
return
|
return
|
||||||
@@ -58,13 +58,13 @@ class outputManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut("setting speech language in outputManager.speakText", debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("setting speech language in outputManager.speakText", debug.debugLevel.ERROR)
|
||||||
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.env['runtime']['speechDriver'].setVoice(self.env['runtime']['settingsManager'].getSetting('speech', 'voice'))
|
self.env['runtime']['speechDriver'].setVoice(self.env['runtime']['settingsManager'].getSetting('speech', 'voice'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut("Error while setting speech voice in outputManager.speakText", debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("Error while setting speech voice in outputManager.speakText", debug.debugLevel.ERROR)
|
||||||
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if announceCapital:
|
if announceCapital:
|
||||||
self.env['runtime']['speechDriver'].setPitch(self.env['runtime']['settingsManager'].getSettingAsFloat('speech', 'capitalPitch'))
|
self.env['runtime']['speechDriver'].setPitch(self.env['runtime']['settingsManager'].getSettingAsFloat('speech', 'capitalPitch'))
|
||||||
@@ -73,13 +73,13 @@ class outputManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut("setting speech pitch in outputManager.speakText", debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("setting speech pitch in outputManager.speakText", debug.debugLevel.ERROR)
|
||||||
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.env['runtime']['speechDriver'].setRate(self.env['runtime']['settingsManager'].getSettingAsFloat('speech', 'rate'))
|
self.env['runtime']['speechDriver'].setRate(self.env['runtime']['settingsManager'].getSettingAsFloat('speech', 'rate'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut("setting speech rate in outputManager.speakText", debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("setting speech rate in outputManager.speakText", debug.debugLevel.ERROR)
|
||||||
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.env['runtime']['speechDriver'].setModule(self.env['runtime']['settingsManager'].getSetting('speech', 'module'))
|
self.env['runtime']['speechDriver'].setModule(self.env['runtime']['settingsManager'].getSetting('speech', 'module'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -91,7 +91,7 @@ class outputManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut("setting speech volume in outputManager.speakText ", debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("setting speech volume in outputManager.speakText ", debug.debugLevel.ERROR)
|
||||||
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.env['runtime']['settingsManager'].getSettingAsBool('general', 'newLinePause'):
|
if self.env['runtime']['settingsManager'].getSettingAsBool('general', 'newLinePause'):
|
||||||
cleanText = text.replace('\n', ' , ')
|
cleanText = text.replace('\n', ' , ')
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class processManager():
|
|||||||
self.addSimpleEventThread(fenrirEventType.HeartBeat, self.heartBeatTimer, multiprocess=True)
|
self.addSimpleEventThread(fenrirEventType.HeartBeat, self.heartBeatTimer, multiprocess=True)
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.terminateAllProcesses()
|
self.terminateAllProcesses()
|
||||||
|
|
||||||
def terminateAllProcesses(self):
|
def terminateAllProcesses(self):
|
||||||
for proc in self._Processes:
|
for proc in self._Processes:
|
||||||
#try:
|
#try:
|
||||||
|
|||||||
@@ -57,6 +57,35 @@ class remoteManager():
|
|||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.env['runtime']['settingsManager'].shutdownDriver('remoteDriver')
|
self.env['runtime']['settingsManager'].shutdownDriver('remoteDriver')
|
||||||
|
|
||||||
|
def handleSettingsChangeWithResponse(self, settingsText):
|
||||||
|
if not self.env['runtime']['settingsManager'].getSettingAsBool('remote', 'enableSettingsRemote'):
|
||||||
|
return {"success": False, "message": "Settings remote control is disabled"}
|
||||||
|
|
||||||
|
upperSettingsText = settingsText.upper()
|
||||||
|
try:
|
||||||
|
# set setting
|
||||||
|
if upperSettingsText.startswith(self.setSettingConst):
|
||||||
|
parameterText = settingsText[len(self.setSettingConst):]
|
||||||
|
self.setSettings(parameterText)
|
||||||
|
return {"success": True, "message": f"Setting applied: {parameterText}"}
|
||||||
|
# save as setting
|
||||||
|
elif upperSettingsText.startswith(self.saveAsSettingConst):
|
||||||
|
parameterText = settingsText[len(self.saveAsSettingConst):]
|
||||||
|
self.saveSettings(parameterText)
|
||||||
|
return {"success": True, "message": f"Settings saved to: {parameterText}"}
|
||||||
|
# save setting
|
||||||
|
elif upperSettingsText == self.saveSettingConst:
|
||||||
|
self.saveSettings()
|
||||||
|
return {"success": True, "message": "Settings saved"}
|
||||||
|
# reset setting
|
||||||
|
elif upperSettingsText == self.resetSettingConst:
|
||||||
|
self.resetSettings()
|
||||||
|
return {"success": True, "message": "Settings reset to defaults"}
|
||||||
|
else:
|
||||||
|
return {"success": False, "message": f"Unknown settings command: {settingsText}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "message": f"Settings error: {str(e)}"}
|
||||||
|
|
||||||
def handleSettingsChange(self, settingsText):
|
def handleSettingsChange(self, settingsText):
|
||||||
if not self.env['runtime']['settingsManager'].getSettingAsBool('remote', 'enableSettingsRemote'):
|
if not self.env['runtime']['settingsManager'].getSettingAsBool('remote', 'enableSettingsRemote'):
|
||||||
return
|
return
|
||||||
@@ -77,6 +106,61 @@ class remoteManager():
|
|||||||
elif upperSettingsText == self.resetSettingConst:
|
elif upperSettingsText == self.resetSettingConst:
|
||||||
self.resetSettings()
|
self.resetSettings()
|
||||||
|
|
||||||
|
def handleCommandExecutionWithResponse(self, commandText):
|
||||||
|
if not self.env['runtime']['settingsManager'].getSettingAsBool('remote', 'enableCommandRemote'):
|
||||||
|
return {"success": False, "message": "Command remote control is disabled"}
|
||||||
|
|
||||||
|
upperCommandText = commandText.upper()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# say
|
||||||
|
if upperCommandText.startswith(self.sayConst):
|
||||||
|
parameterText = commandText[len(self.sayConst):]
|
||||||
|
self.say(parameterText)
|
||||||
|
return {"success": True, "message": f"Speaking: {parameterText[:50]}{'...' if len(parameterText) > 50 else ''}"}
|
||||||
|
# interrupt
|
||||||
|
elif upperCommandText == self.interruptConst:
|
||||||
|
self.interruptSpeech()
|
||||||
|
return {"success": True, "message": "Speech interrupted"}
|
||||||
|
# temp disable speech
|
||||||
|
elif upperCommandText == self.tempDisableSpeechConst:
|
||||||
|
self.tempDisableSpeech()
|
||||||
|
return {"success": True, "message": "Speech temporarily disabled"}
|
||||||
|
# set vmenu
|
||||||
|
elif upperCommandText.startswith(self.vmenuConst):
|
||||||
|
parameterText = commandText[len(self.vmenuConst):]
|
||||||
|
self.setVMenu(parameterText)
|
||||||
|
return {"success": True, "message": f"VMenu set to: {parameterText}"}
|
||||||
|
# reset vmenu
|
||||||
|
elif upperCommandText == self.resetVmenuConst:
|
||||||
|
self.resetVMenu()
|
||||||
|
return {"success": True, "message": "VMenu reset"}
|
||||||
|
# quit fenrir
|
||||||
|
elif upperCommandText == self.quitAppConst:
|
||||||
|
self.quitFenrir()
|
||||||
|
return {"success": True, "message": "Fenrir shutting down"}
|
||||||
|
# define window
|
||||||
|
elif upperCommandText.startswith(self.defineWindowConst):
|
||||||
|
parameterText = commandText[len(self.defineWindowConst):]
|
||||||
|
self.defineWindow(parameterText)
|
||||||
|
return {"success": True, "message": f"Window defined: {parameterText}"}
|
||||||
|
# reset window
|
||||||
|
elif upperCommandText == self.resetWindowConst:
|
||||||
|
self.resetWindow()
|
||||||
|
return {"success": True, "message": "Window reset"}
|
||||||
|
# set clipboard
|
||||||
|
elif upperCommandText.startswith(self.setClipboardConst):
|
||||||
|
parameterText = commandText[len(self.setClipboardConst):]
|
||||||
|
self.setClipboard(parameterText)
|
||||||
|
return {"success": True, "message": f"Clipboard set: {parameterText[:50]}{'...' if len(parameterText) > 50 else ''}"}
|
||||||
|
elif upperCommandText.startswith(self.exportClipboardConst):
|
||||||
|
self.exportClipboard()
|
||||||
|
return {"success": True, "message": "Clipboard exported to file"}
|
||||||
|
else:
|
||||||
|
return {"success": False, "message": f"Unknown command: {commandText}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "message": f"Command error: {str(e)}"}
|
||||||
|
|
||||||
def handleCommandExecution(self, commandText):
|
def handleCommandExecution(self, commandText):
|
||||||
if not self.env['runtime']['settingsManager'].getSettingAsBool('remote', 'enableCommandRemote'):
|
if not self.env['runtime']['settingsManager'].getSettingAsBool('remote', 'enableCommandRemote'):
|
||||||
return
|
return
|
||||||
@@ -172,7 +256,7 @@ class remoteManager():
|
|||||||
self.env['runtime']['outputManager'].presentText(_('clipboard exported to file'), interrupt=True)
|
self.env['runtime']['outputManager'].presentText(_('clipboard exported to file'), interrupt=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut('export_clipboard_to_file:run: Filepath:'+ clipboardFile +' trace:' + str(e),debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut('export_clipboard_to_file:run: Filepath:'+ clipboardFile +' trace:' + str(e),debug.debugLevel.ERROR)
|
||||||
|
|
||||||
def saveSettings(self, settingConfigPath = None):
|
def saveSettings(self, settingConfigPath = None):
|
||||||
if not settingConfigPath:
|
if not settingConfigPath:
|
||||||
settingConfigPath = self.env['runtime']['settingsManager'].getSettingsFile()
|
settingConfigPath = self.env['runtime']['settingsManager'].getSettingsFile()
|
||||||
@@ -185,6 +269,25 @@ class remoteManager():
|
|||||||
self.env['runtime']['settingsManager'].parseSettingArgs(settingsArgs)
|
self.env['runtime']['settingsManager'].parseSettingArgs(settingsArgs)
|
||||||
self.env['runtime']['screenManager'].updateScreenIgnored()
|
self.env['runtime']['screenManager'].updateScreenIgnored()
|
||||||
self.env['runtime']['inputManager'].handleDeviceGrab(force = True)
|
self.env['runtime']['inputManager'].handleDeviceGrab(force = True)
|
||||||
|
def handleRemoteIncommingWithResponse(self, eventData):
|
||||||
|
if not eventData:
|
||||||
|
return {"success": False, "message": "No data received"}
|
||||||
|
|
||||||
|
upperEventData = eventData.upper()
|
||||||
|
self.env['runtime']['debug'].writeDebugOut('remoteManager:handleRemoteIncommingWithResponse: event: ' + str(eventData),debug.debugLevel.INFO)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if upperEventData.startswith(self.settingConst):
|
||||||
|
settingsText = eventData[len(self.settingConst):]
|
||||||
|
return self.handleSettingsChangeWithResponse(settingsText)
|
||||||
|
elif upperEventData.startswith(self.commandConst):
|
||||||
|
commandText = eventData[len(self.commandConst):]
|
||||||
|
return self.handleCommandExecutionWithResponse(commandText)
|
||||||
|
else:
|
||||||
|
return {"success": False, "message": "Unknown command format. Use 'COMMAND ...' or 'SETTING ...'"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "message": f"Exception: {str(e)}"}
|
||||||
|
|
||||||
def handleRemoteIncomming(self, eventData):
|
def handleRemoteIncomming(self, eventData):
|
||||||
if not eventData:
|
if not eventData:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class screenManager():
|
|||||||
if self.isCurrScreenIgnoredChanged():
|
if self.isCurrScreenIgnoredChanged():
|
||||||
self.env['runtime']['inputManager'].setExecuteDeviceGrab()
|
self.env['runtime']['inputManager'].setExecuteDeviceGrab()
|
||||||
self.env['runtime']['inputManager'].handleDeviceGrab()
|
self.env['runtime']['inputManager'].handleDeviceGrab()
|
||||||
if not self.isSuspendingScreen(self.env['screen']['newTTY']):
|
if not self.isIgnoredScreen(self.env['screen']['newTTY']):
|
||||||
self.update(eventData, 'onScreenChange')
|
self.update(eventData, 'onScreenChange')
|
||||||
self.env['screen']['lastScreenUpdate'] = time.time()
|
self.env['screen']['lastScreenUpdate'] = time.time()
|
||||||
else:
|
else:
|
||||||
@@ -81,7 +81,7 @@ class screenManager():
|
|||||||
return self.prevScreenIgnored
|
return self.prevScreenIgnored
|
||||||
def updateScreenIgnored(self):
|
def updateScreenIgnored(self):
|
||||||
self.prevScreenIgnored = self.currScreenIgnored
|
self.prevScreenIgnored = self.currScreenIgnored
|
||||||
self.currScreenIgnored = self.isSuspendingScreen(self.env['screen']['newTTY'])
|
self.currScreenIgnored = self.isIgnoredScreen(self.env['screen']['newTTY'])
|
||||||
def update(self, eventData, trigger='onUpdate'):
|
def update(self, eventData, trigger='onUpdate'):
|
||||||
# set new "old" values
|
# set new "old" values
|
||||||
self.env['screen']['oldContentBytes'] = self.env['screen']['newContentBytes']
|
self.env['screen']['oldContentBytes'] = self.env['screen']['newContentBytes']
|
||||||
@@ -174,16 +174,19 @@ class screenManager():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut('screenManager:update:highlight: ' + str(e),debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut('screenManager:update:highlight: ' + str(e),debug.debugLevel.ERROR)
|
||||||
|
|
||||||
def isSuspendingScreen(self, screen = None):
|
def isIgnoredScreen(self, screen = None):
|
||||||
if screen == None:
|
if screen == None:
|
||||||
screen = self.env['screen']['newTTY']
|
screen = self.env['screen']['newTTY']
|
||||||
|
# Check if force all screens flag is set
|
||||||
|
if self.env['runtime'].get('force_all_screens', False):
|
||||||
|
return False
|
||||||
ignoreScreens = []
|
ignoreScreens = []
|
||||||
fixIgnoreScreens = self.env['runtime']['settingsManager'].getSetting('screen', 'suspendingScreen')
|
fixIgnoreScreens = self.env['runtime']['settingsManager'].getSetting('screen', 'ignoreScreen')
|
||||||
if fixIgnoreScreens != '':
|
if fixIgnoreScreens != '':
|
||||||
ignoreScreens.extend(fixIgnoreScreens.split(','))
|
ignoreScreens.extend(fixIgnoreScreens.split(','))
|
||||||
if self.env['runtime']['settingsManager'].getSettingAsBool('screen', 'autodetectSuspendingScreen'):
|
if self.env['runtime']['settingsManager'].getSettingAsBool('screen', 'autodetectIgnoreScreen'):
|
||||||
ignoreScreens.extend(self.env['screen']['autoIgnoreScreens'])
|
ignoreScreens.extend(self.env['screen']['autoIgnoreScreens'])
|
||||||
self.env['runtime']['debug'].writeDebugOut('screenManager:isSuspendingScreen ignore:' + str(ignoreScreens) + ' current:'+ str(screen ), debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut('screenManager:isIgnoredScreen ignore:' + str(ignoreScreens) + ' current:'+ str(screen ), debug.debugLevel.INFO)
|
||||||
return (screen in ignoreScreens)
|
return (screen in ignoreScreens)
|
||||||
|
|
||||||
def isScreenChange(self):
|
def isScreenChange(self):
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ settingsData = {
|
|||||||
'driver': 'vcsaDriver',
|
'driver': 'vcsaDriver',
|
||||||
'encoding': 'auto',
|
'encoding': 'auto',
|
||||||
'screenUpdateDelay': 0.1,
|
'screenUpdateDelay': 0.1,
|
||||||
'suspendingScreen': '',
|
'ignoreScreen': '',
|
||||||
'autodetectSuspendingScreen': False,
|
'autodetectIgnoreScreen': False,
|
||||||
},
|
},
|
||||||
'general':{
|
'general':{
|
||||||
'debugLevel': debug.debugLevel.DEACTIVE,
|
'debugLevel': debug.debugLevel.DEACTIVE,
|
||||||
|
|||||||
@@ -317,6 +317,17 @@ class settingsManager():
|
|||||||
|
|
||||||
environment['runtime']['debug'] = debugManager.debugManager(self.env['runtime']['settingsManager'].getSetting('general','debugFile'))
|
environment['runtime']['debug'] = debugManager.debugManager(self.env['runtime']['settingsManager'].getSetting('general','debugFile'))
|
||||||
environment['runtime']['debug'].initialize(environment)
|
environment['runtime']['debug'].initialize(environment)
|
||||||
|
|
||||||
|
if cliArgs.force_all_screens:
|
||||||
|
environment['runtime']['force_all_screens'] = True
|
||||||
|
|
||||||
|
if cliArgs.ignore_screen:
|
||||||
|
currentIgnoreScreen = self.getSetting('screen', 'ignoreScreen')
|
||||||
|
if currentIgnoreScreen:
|
||||||
|
ignoreScreens = currentIgnoreScreen.split(',') + cliArgs.ignore_screen
|
||||||
|
else:
|
||||||
|
ignoreScreens = cliArgs.ignore_screen
|
||||||
|
self.setSetting('screen', 'ignoreScreen', ','.join(ignoreScreens))
|
||||||
|
|
||||||
if not os.path.exists(self.getSetting('sound','theme') + '/soundicons.conf'):
|
if not os.path.exists(self.getSetting('sound','theme') + '/soundicons.conf'):
|
||||||
if os.path.exists(soundRoot + self.getSetting('sound','theme')):
|
if os.path.exists(soundRoot + self.getSetting('sound','theme')):
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class speechDriver():
|
|||||||
return
|
return
|
||||||
if not queueable:
|
if not queueable:
|
||||||
self.cancel()
|
self.cancel()
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
if not self._isInitialized:
|
if not self._isInitialized:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class tableManager():
|
|||||||
return ''
|
return ''
|
||||||
def setRowColumnSep(self, columnSep = ''):
|
def setRowColumnSep(self, columnSep = ''):
|
||||||
self.rowColumnSep = columnSep
|
self.rowColumnSep = columnSep
|
||||||
|
|
||||||
def setHeadLine(self, headLine = ''):
|
def setHeadLine(self, headLine = ''):
|
||||||
self.setHeadColumnSep()
|
self.setHeadColumnSep()
|
||||||
self.setRowColumnSep()
|
self.setRowColumnSep()
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class textManager():
|
|||||||
if name[0] == name[1]:
|
if name[0] == name[1]:
|
||||||
newText += ' ' + str(numberOfChars) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' '
|
newText += ' ' + str(numberOfChars) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' '
|
||||||
else:
|
else:
|
||||||
newText += ' ' + str(int(numberOfChars / 2)) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name, True) + ' '
|
newText += ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' ' + str(int(numberOfChars / 2)) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[1], True) + ' '
|
||||||
lastPos = span[1]
|
lastPos = span[1]
|
||||||
if lastPos != 0:
|
if lastPos != 0:
|
||||||
newText += ' '
|
newText += ' '
|
||||||
@@ -46,7 +46,7 @@ class textManager():
|
|||||||
lastPos = 0
|
lastPos = 0
|
||||||
for match in self.regExSingle.finditer(newText):
|
for match in self.regExSingle.finditer(newText):
|
||||||
span = match.span()
|
span = match.span()
|
||||||
result += text[lastPos:span[0]]
|
result += newText[lastPos:span[0]]
|
||||||
numberOfChars = len(newText[span[0]:span[1]])
|
numberOfChars = len(newText[span[0]:span[1]])
|
||||||
name = newText[span[0]:span[1]][:2]
|
name = newText[span[0]:span[1]][:2]
|
||||||
if not self.env['runtime']['punctuationManager'].isPuctuation(name[0]):
|
if not self.env['runtime']['punctuationManager'].isPuctuation(name[0]):
|
||||||
@@ -55,7 +55,7 @@ class textManager():
|
|||||||
if name[0] == name[1]:
|
if name[0] == name[1]:
|
||||||
result += ' ' + str(numberOfChars) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' '
|
result += ' ' + str(numberOfChars) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' '
|
||||||
else:
|
else:
|
||||||
result += ' ' + str(int(numberOfChars / 2)) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name, True) + ' '
|
result += ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' ' + str(int(numberOfChars / 2)) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[1], True) + ' '
|
||||||
lastPos = span[1]
|
lastPos = span[1]
|
||||||
if lastPos != 0:
|
if lastPos != 0:
|
||||||
result += ' '
|
result += ' '
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
# Fenrir TTY screen reader
|
# Fenrir TTY screen reader
|
||||||
# By Chrys, Storm Dragon, and contributers.
|
# By Chrys, Storm Dragon, and contributers.
|
||||||
|
|
||||||
version = "2025.02.26"
|
version = "2025.06.07"
|
||||||
codeName = "master"
|
codeName = "master"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class driver(inputDriver):
|
|||||||
self.env['runtime']['processManager'].addCustomEventThread(self.inputWatchdog)
|
self.env['runtime']['processManager'].addCustomEventThread(self.inputWatchdog)
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
def plugInputDeviceWatchdogUdev(self,active , eventQueue):
|
def plugInputDeviceWatchdogUdev(self, active, eventQueue):
|
||||||
context = pyudev.Context()
|
context = pyudev.Context()
|
||||||
monitor = pyudev.Monitor.from_netlink(context)
|
monitor = pyudev.Monitor.from_netlink(context)
|
||||||
monitor.filter_by(subsystem='input')
|
monitor.filter_by(subsystem='input')
|
||||||
@@ -72,31 +72,33 @@ class driver(inputDriver):
|
|||||||
self.env['runtime']['debug'].writeDebugOut('plugInputDeviceWatchdogUdev:' + str(device), debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut('plugInputDeviceWatchdogUdev:' + str(device), debug.debugLevel.INFO)
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
if device.name.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
|
# FIX: Check if attributes exist before accessing them
|
||||||
|
if hasattr(device, 'name') and device.name and device.name.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
|
||||||
ignorePlug = True
|
ignorePlug = True
|
||||||
if device.phys.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
|
if hasattr(device, 'phys') and device.phys and device.phys.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
|
||||||
ignorePlug = True
|
ignorePlug = True
|
||||||
if 'BRLTTY' in device.name.upper():
|
if hasattr(device, 'name') and device.name and 'BRLTTY' in device.name.upper():
|
||||||
ignorePlug = True
|
ignorePlug = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut("plugInputDeviceWatchdogUdev CHECK NAME CRASH: " + str(e),debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("plugInputDeviceWatchdogUdev CHECK NAME CRASH: " + str(e), debug.debugLevel.ERROR)
|
||||||
|
|
||||||
if not ignorePlug:
|
if not ignorePlug:
|
||||||
virtual = '/sys/devices/virtual/input/' in device.sys_path
|
virtual = '/sys/devices/virtual/input/' in device.sys_path
|
||||||
if device.device_node:
|
if device.device_node:
|
||||||
validDevices.append({'device': device.device_node, 'virtual': virtual})
|
validDevices.append({'device': device.device_node, 'virtual': virtual})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut("plugInputDeviceWatchdogUdev APPEND CRASH: " + str(e),debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("plugInputDeviceWatchdogUdev APPEND CRASH: " + str(e), debug.debugLevel.ERROR)
|
||||||
try:
|
try:
|
||||||
pollTimeout = 1
|
pollTimeout = 1
|
||||||
device = monitor.poll(pollTimeout)
|
device = monitor.poll(pollTimeout)
|
||||||
except:
|
except Exception:
|
||||||
device = None
|
device = None
|
||||||
ignorePlug = False
|
ignorePlug = False
|
||||||
if validDevices:
|
if validDevices:
|
||||||
eventQueue.put({"Type":fenrirEventType.PlugInputDevice,"Data":validDevices})
|
eventQueue.put({"Type": fenrirEventType.PlugInputDevice, "Data": validDevices})
|
||||||
return time.time()
|
return time.time()
|
||||||
|
|
||||||
def inputWatchdog(self,active , eventQueue):
|
def inputWatchdog(self, active, eventQueue):
|
||||||
try:
|
try:
|
||||||
while active.value:
|
while active.value:
|
||||||
r, w, x = select(self.iDevices, [], [], 0.8)
|
r, w, x = select(self.iDevices, [], [], 0.8)
|
||||||
@@ -111,7 +113,7 @@ class driver(inputDriver):
|
|||||||
self.removeDevice(fd)
|
self.removeDevice(fd)
|
||||||
while(event):
|
while(event):
|
||||||
self.env['runtime']['debug'].writeDebugOut('inputWatchdog: EVENT:' + str(event), debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut('inputWatchdog: EVENT:' + str(event), debug.debugLevel.INFO)
|
||||||
self.env['input']['eventBuffer'].append( [self.iDevices[fd], self.uDevices[fd], event])
|
self.env['input']['eventBuffer'].append([self.iDevices[fd], self.uDevices[fd], event])
|
||||||
if event.type == evdev.events.EV_KEY:
|
if event.type == evdev.events.EV_KEY:
|
||||||
if not foundKeyInSequence:
|
if not foundKeyInSequence:
|
||||||
foundKeyInSequence = True
|
foundKeyInSequence = True
|
||||||
@@ -123,11 +125,11 @@ class driver(inputDriver):
|
|||||||
if not isinstance(currMapEvent['EventName'], str):
|
if not isinstance(currMapEvent['EventName'], str):
|
||||||
event = self.iDevices[fd].read_one()
|
event = self.iDevices[fd].read_one()
|
||||||
continue
|
continue
|
||||||
if currMapEvent['EventState'] in [0,1,2]:
|
if currMapEvent['EventState'] in [0, 1, 2]:
|
||||||
eventQueue.put({"Type":fenrirEventType.KeyboardInput,"Data":currMapEvent.copy()})
|
eventQueue.put({"Type": fenrirEventType.KeyboardInput, "Data": currMapEvent.copy()})
|
||||||
eventFired = True
|
eventFired = True
|
||||||
else:
|
else:
|
||||||
if event.type in [2,3]:
|
if event.type in [2, 3]:
|
||||||
foreward = True
|
foreward = True
|
||||||
|
|
||||||
event = self.iDevices[fd].read_one()
|
event = self.iDevices[fd].read_one()
|
||||||
@@ -136,7 +138,7 @@ class driver(inputDriver):
|
|||||||
self.writeEventBuffer()
|
self.writeEventBuffer()
|
||||||
self.clearEventBuffer()
|
self.clearEventBuffer()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut("INPUT WATCHDOG CRASH: "+str(e),debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut("INPUT WATCHDOG CRASH: " + str(e), debug.debugLevel.ERROR)
|
||||||
|
|
||||||
def writeEventBuffer(self):
|
def writeEventBuffer(self):
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
@@ -146,7 +148,7 @@ class driver(inputDriver):
|
|||||||
if uDevice:
|
if uDevice:
|
||||||
if self.gDevices[iDevice.fd]:
|
if self.gDevices[iDevice.fd]:
|
||||||
self.writeUInput(uDevice, event)
|
self.writeUInput(uDevice, event)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def writeUInput(self, uDevice, event):
|
def writeUInput(self, uDevice, event):
|
||||||
@@ -156,7 +158,7 @@ class driver(inputDriver):
|
|||||||
time.sleep(0.0000002)
|
time.sleep(0.0000002)
|
||||||
uDevice.syn()
|
uDevice.syn()
|
||||||
|
|
||||||
def updateInputDevices(self, newDevices = None, init = False):
|
def updateInputDevices(self, newDevices=None, init=False):
|
||||||
if init:
|
if init:
|
||||||
self.removeAllDevices()
|
self.removeAllDevices()
|
||||||
|
|
||||||
@@ -191,7 +193,7 @@ class driver(inputDriver):
|
|||||||
try:
|
try:
|
||||||
with open(deviceFile) as f:
|
with open(deviceFile) as f:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
# 3 pos absolute
|
# 3 pos absolute
|
||||||
# 2 pos relative
|
# 2 pos relative
|
||||||
@@ -201,22 +203,23 @@ class driver(inputDriver):
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
if currDevice.name.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
|
# FIX: Check if attributes exist before accessing them
|
||||||
|
if hasattr(currDevice, 'name') and currDevice.name and currDevice.name.upper() in ['', 'SPEAKUP', 'FENRIR-UINPUT']:
|
||||||
continue
|
continue
|
||||||
if currDevice.phys.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
|
if hasattr(currDevice, 'phys') and currDevice.phys and currDevice.phys.upper() in ['', 'SPEAKUP', 'FENRIR-UINPUT']:
|
||||||
continue
|
continue
|
||||||
if 'BRLTTY' in currDevice.name.upper():
|
if hasattr(currDevice, 'name') and currDevice.name and 'BRLTTY' in currDevice.name.upper():
|
||||||
continue
|
continue
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
cap = currDevice.capabilities()
|
cap = currDevice.capabilities()
|
||||||
if mode in ['ALL','NOMICE']:
|
if mode in ['ALL', 'NOMICE']:
|
||||||
if eventType.EV_KEY in cap:
|
if eventType.EV_KEY in cap:
|
||||||
if 116 in cap[eventType.EV_KEY] and len(cap[eventType.EV_KEY]) < 10:
|
if 116 in cap[eventType.EV_KEY] and len(cap[eventType.EV_KEY]) < 10:
|
||||||
self.env['runtime']['debug'].writeDebugOut('Device Skipped (has 116):' + currDevice.name,debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut('Device Skipped (has 116):' + currDevice.name, debug.debugLevel.INFO)
|
||||||
continue
|
continue
|
||||||
if len(cap[eventType.EV_KEY]) < 60:
|
if len(cap[eventType.EV_KEY]) < 60:
|
||||||
self.env['runtime']['debug'].writeDebugOut('Device Skipped (< 60 keys):' + currDevice.name,debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut('Device Skipped (< 60 keys):' + currDevice.name, debug.debugLevel.INFO)
|
||||||
continue
|
continue
|
||||||
if mode == 'ALL':
|
if mode == 'ALL':
|
||||||
self.addDevice(currDevice)
|
self.addDevice(currDevice)
|
||||||
@@ -224,16 +227,20 @@ class driver(inputDriver):
|
|||||||
elif mode == 'NOMICE':
|
elif mode == 'NOMICE':
|
||||||
if not ((eventType.EV_REL in cap) or (eventType.EV_ABS in cap)):
|
if not ((eventType.EV_REL in cap) or (eventType.EV_ABS in cap)):
|
||||||
self.addDevice(currDevice)
|
self.addDevice(currDevice)
|
||||||
self.env['runtime']['debug'].writeDebugOut('Device added (NOMICE):' + self.iDevices[currDevice.fd].name,debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut('Device added (NOMICE):' + self.iDevices[currDevice.fd].name, debug.debugLevel.INFO)
|
||||||
else:
|
else:
|
||||||
self.env['runtime']['debug'].writeDebugOut('Device Skipped (NOMICE):' + currDevice.name,debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut('Device Skipped (NOMICE):' + currDevice.name, debug.debugLevel.INFO)
|
||||||
else:
|
else:
|
||||||
self.env['runtime']['debug'].writeDebugOut('Device Skipped (no EV_KEY):' + currDevice.name,debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut('Device Skipped (no EV_KEY):' + currDevice.name, debug.debugLevel.INFO)
|
||||||
elif currDevice.name.upper() in mode.split(','):
|
elif currDevice.name.upper() in mode.split(','):
|
||||||
self.addDevice(currDevice)
|
self.addDevice(currDevice)
|
||||||
self.env['runtime']['debug'].writeDebugOut('Device added (Name):' + self.iDevices[currDevice.fd].name,debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut('Device added (Name):' + self.iDevices[currDevice.fd].name, debug.debugLevel.INFO)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut("Device Skipped (Exception): " + deviceFile +' ' + currDevice.name +' '+ str(e),debug.debugLevel.INFO)
|
try:
|
||||||
|
device_name = currDevice.name if hasattr(currDevice, 'name') else "unknown"
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Device Skipped (Exception): " + deviceFile + ' ' + device_name + ' ' + str(e), debug.debugLevel.INFO)
|
||||||
|
except:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut("Device Skipped (Exception): " + deviceFile + ' ' + str(e), debug.debugLevel.INFO)
|
||||||
self.iDeviceNo = len(evdev.list_devices())
|
self.iDeviceNo = len(evdev.list_devices())
|
||||||
self.updateMPiDevicesFD()
|
self.updateMPiDevicesFD()
|
||||||
|
|
||||||
@@ -247,6 +254,7 @@ class driver(inputDriver):
|
|||||||
self.iDevicesFD.remove(fd)
|
self.iDevicesFD.remove(fd)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def mapEvent(self, event):
|
def mapEvent(self, event):
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
return None
|
return None
|
||||||
@@ -266,12 +274,12 @@ class driver(inputDriver):
|
|||||||
mEvent['EventSec'] = event.sec
|
mEvent['EventSec'] = event.sec
|
||||||
mEvent['EventUsec'] = event.usec
|
mEvent['EventUsec'] = event.usec
|
||||||
mEvent['EventState'] = event.value
|
mEvent['EventState'] = event.value
|
||||||
mEvent['EventType'] = event.type
|
mEvent['EventType'] = event.type
|
||||||
return mEvent
|
return mEvent
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def getLedState(self, led = 0):
|
def getLedState(self, led=0):
|
||||||
if not self.hasIDevices():
|
if not self.hasIDevices():
|
||||||
return False
|
return False
|
||||||
# 0 = Numlock
|
# 0 = Numlock
|
||||||
@@ -281,7 +289,8 @@ class driver(inputDriver):
|
|||||||
if led in dev.leds():
|
if led in dev.leds():
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
def toggleLedState(self, led = 0):
|
|
||||||
|
def toggleLedState(self, led=0):
|
||||||
if not self.hasIDevices():
|
if not self.hasIDevices():
|
||||||
return False
|
return False
|
||||||
ledState = self.getLedState(led)
|
ledState = self.getLedState(led)
|
||||||
@@ -290,9 +299,10 @@ class driver(inputDriver):
|
|||||||
# 17 LEDs
|
# 17 LEDs
|
||||||
if 17 in self.iDevices[i].capabilities():
|
if 17 in self.iDevices[i].capabilities():
|
||||||
if ledState == 1:
|
if ledState == 1:
|
||||||
self.iDevices[i].set_led(led , 0)
|
self.iDevices[i].set_led(led, 0)
|
||||||
else:
|
else:
|
||||||
self.iDevices[i].set_led(led , 1)
|
self.iDevices[i].set_led(led, 1)
|
||||||
|
|
||||||
def grabAllDevices(self):
|
def grabAllDevices(self):
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
return True
|
return True
|
||||||
@@ -301,6 +311,7 @@ class driver(inputDriver):
|
|||||||
if not self.gDevices[fd]:
|
if not self.gDevices[fd]:
|
||||||
ok = ok and self.grabDevice(fd)
|
ok = ok and self.grabDevice(fd)
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
def ungrabAllDevices(self):
|
def ungrabAllDevices(self):
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
return True
|
return True
|
||||||
@@ -309,6 +320,7 @@ class driver(inputDriver):
|
|||||||
if self.gDevices[fd]:
|
if self.gDevices[fd]:
|
||||||
ok = ok and self.ungrabDevice(fd)
|
ok = ok and self.ungrabDevice(fd)
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
def createUInputDev(self, fd):
|
def createUInputDev(self, fd):
|
||||||
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
|
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
|
||||||
self.uDevices[fd] = None
|
self.uDevices[fd] = None
|
||||||
@@ -324,20 +336,21 @@ class driver(inputDriver):
|
|||||||
self.uDevices[fd] = UInput.from_device(self.iDevices[fd], name='fenrir-uinput', phys='fenrir-uinput')
|
self.uDevices[fd] = UInput.from_device(self.iDevices[fd], name='fenrir-uinput', phys='fenrir-uinput')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
try:
|
try:
|
||||||
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: compat fallback: ' + str(e),debug.debugLevel.WARNING)
|
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: compat fallback: ' + str(e), debug.debugLevel.WARNING)
|
||||||
dev = self.iDevices[fd]
|
dev = self.iDevices[fd]
|
||||||
cap = dev.capabilities()
|
cap = dev.capabilities()
|
||||||
del cap[0]
|
del cap[0]
|
||||||
self.uDevices[fd] = UInput(
|
self.uDevices[fd] = UInput(
|
||||||
cap,
|
cap,
|
||||||
name = 'fenrir-uinput',
|
name='fenrir-uinput',
|
||||||
phys = 'fenrir-uinput'
|
phys='fenrir-uinput'
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: init Uinput not possible: ' + str(e),debug.debugLevel.ERROR)
|
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: init Uinput not possible: ' + str(e), debug.debugLevel.ERROR)
|
||||||
return
|
return
|
||||||
|
|
||||||
def addDevice(self, newDevice):
|
def addDevice(self, newDevice):
|
||||||
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: device added: ' + str(newDevice.fd) + ' ' +str(newDevice),debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: device added: ' + str(newDevice.fd) + ' ' + str(newDevice), debug.debugLevel.INFO)
|
||||||
try:
|
try:
|
||||||
self.iDevices[newDevice.fd] = newDevice
|
self.iDevices[newDevice.fd] = newDevice
|
||||||
self.createUInputDev(newDevice.fd)
|
self.createUInputDev(newDevice.fd)
|
||||||
@@ -360,10 +373,13 @@ class driver(inputDriver):
|
|||||||
def grabDevice(self, fd):
|
def grabDevice(self, fd):
|
||||||
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
|
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# FIX: Handle exception variable scope correctly
|
||||||
|
grab_error = None
|
||||||
try:
|
try:
|
||||||
self.iDevices[fd].grab()
|
self.iDevices[fd].grab()
|
||||||
self.gDevices[fd] = True
|
self.gDevices[fd] = True
|
||||||
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: grab device ('+ str(self.iDevices[fd].name) + ')',debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: grab device (' + str(self.iDevices[fd].name) + ')', debug.debugLevel.INFO)
|
||||||
# Reset modifier keys on successful grab
|
# Reset modifier keys on successful grab
|
||||||
if self.uDevices[fd]:
|
if self.uDevices[fd]:
|
||||||
modifierKeys = [e.KEY_LEFTCTRL, e.KEY_RIGHTCTRL, e.KEY_LEFTALT, e.KEY_RIGHTALT, e.KEY_LEFTSHIFT, e.KEY_RIGHTSHIFT]
|
modifierKeys = [e.KEY_LEFTCTRL, e.KEY_RIGHTCTRL, e.KEY_LEFTALT, e.KEY_RIGHTALT, e.KEY_LEFTSHIFT, e.KEY_RIGHTSHIFT]
|
||||||
@@ -371,33 +387,44 @@ class driver(inputDriver):
|
|||||||
try:
|
try:
|
||||||
self.uDevices[fd].write(e.EV_KEY, key, 0) # 0 = key up
|
self.uDevices[fd].write(e.EV_KEY, key, 0) # 0 = key up
|
||||||
self.uDevices[fd].syn()
|
self.uDevices[fd].syn()
|
||||||
except Exception as e:
|
except Exception as mod_error:
|
||||||
self.env['runtime']['debug'].writeDebugOut('Failed to reset modifier key: ' + str(e), debug.debugLevel.WARNING)
|
self.env['runtime']['debug'].writeDebugOut('Failed to reset modifier key: ' + str(mod_error), debug.debugLevel.WARNING)
|
||||||
except IOError:
|
except IOError:
|
||||||
if not self.gDevices[fd]:
|
if not self.gDevices[fd]:
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: grabing not possible: ' + str(e),debug.debugLevel.ERROR)
|
grab_error = ex
|
||||||
|
|
||||||
|
if grab_error:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: grabing not possible: ' + str(grab_error), debug.debugLevel.ERROR)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def ungrabDevice(self,fd):
|
def ungrabDevice(self, fd):
|
||||||
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
|
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# FIX: Handle exception variable scope correctly
|
||||||
|
ungrab_error = None
|
||||||
try:
|
try:
|
||||||
self.iDevices[fd].ungrab()
|
self.iDevices[fd].ungrab()
|
||||||
self.gDevices[fd] = False
|
self.gDevices[fd] = False
|
||||||
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: ungrab device ('+ str(self.iDevices[fd].name) + ')',debug.debugLevel.INFO)
|
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: ungrab device (' + str(self.iDevices[fd].name) + ')', debug.debugLevel.INFO)
|
||||||
except IOError:
|
except IOError:
|
||||||
if self.gDevices[fd]:
|
if self.gDevices[fd]:
|
||||||
return False
|
return False
|
||||||
# self.gDevices[fd] = False
|
except Exception as ex:
|
||||||
# #self.removeDevice(fd)
|
ungrab_error = ex
|
||||||
except Exception as e:
|
|
||||||
|
if ungrab_error:
|
||||||
|
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: ungrabing not possible: ' + str(ungrab_error), debug.debugLevel.ERROR)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
def removeDevice(self,fd):
|
|
||||||
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: device removed: ' + str(fd) + ' ' +str(self.iDevices[fd]),debug.debugLevel.INFO)
|
def removeDevice(self, fd):
|
||||||
|
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: device removed: ' + str(fd) + ' ' + str(self.iDevices[fd]), debug.debugLevel.INFO)
|
||||||
self.clearEventBuffer()
|
self.clearEventBuffer()
|
||||||
try:
|
try:
|
||||||
self.ungrabDevice(fd)
|
self.ungrabDevice(fd)
|
||||||
@@ -452,4 +479,4 @@ class driver(inputDriver):
|
|||||||
self.iDevices.clear()
|
self.iDevices.clear()
|
||||||
self.uDevices.clear()
|
self.uDevices.clear()
|
||||||
self.gDevices.clear()
|
self.gDevices.clear()
|
||||||
self.iDeviceNo = 0
|
self.iDeviceNo = 0
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class driver(remoteDriver):
|
|||||||
os.unlink(socketFile)
|
os.unlink(socketFile)
|
||||||
self.fenrirSock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
self.fenrirSock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
self.fenrirSock.bind(socketFile)
|
self.fenrirSock.bind(socketFile)
|
||||||
os.chmod(socketFile, 0o222)
|
os.chmod(socketFile, 0o666)
|
||||||
self.fenrirSock.listen(1)
|
self.fenrirSock.listen(1)
|
||||||
while active.value:
|
while active.value:
|
||||||
# Check if the client is still connected and if data is available:
|
# Check if the client is still connected and if data is available:
|
||||||
|
|||||||
Reference in New Issue
Block a user