Compare commits

..

11 Commits

Author SHA1 Message Date
2462a081bf Add a documentation string for the module cli. 2025-01-28 15:30:09 -05:00
1f489a1ae9 Tweaks. 2025-01-28 15:23:04 -05:00
8ce9a8ffeb Move the command line into the main package directory.
The recommendation is that you put you cli script into the package
itself and add in an entry to that module as an entrypoint.
2025-01-28 15:22:07 -05:00
d8395c12e2 Add some things into the file. 2025-01-28 15:21:30 -05:00
3e1c8a2829 Drop the setup.py file if we can. 2025-01-28 15:20:53 -05:00
75199dfe0e Version bump. 2025-01-28 07:49:54 -05:00
160c40e931 Modified lock file. 2025-01-28 04:07:37 -05:00
47c626b420 Add more dynamically computed fields.
That said, let's use the old file for some of the computed fields for
the time being.
2025-01-28 04:06:00 -05:00
f509d89d90 Drop the warning about the dependencies being overwritten.
The pyproject.toml file is the place where things are being migrated
too so it was a bit reduntant.
2025-01-28 04:04:59 -05:00
39a918f74b My testing work with uv.
This seems to be building.
2025-01-24 12:20:41 -05:00
Storm Dragon
71d92e9702 Merged changes from master. 2025-01-08 20:28:16 -05:00
56 changed files with 5123 additions and 3091 deletions

1
.gitignore vendored
View File

@ -6,4 +6,3 @@ dist/
build/
*.kate-swp
.directory
CLAUDE.md

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.13

28
CREDITS
View File

@ -1,30 +1,18 @@
# Fenrir Screen Reader Credits
# Fenrir screen reader
## Current Maintainer
## Developers
* **Storm Dragon** - Project leader and maintainer
## Current Contributors
* **Jeremiah** - Developer
* Storm Dragon: Project leader
* Jeremiah: Coder.
## Previous Developers
* **Chrys** - Original creator and main developer
* Chrys: coder.
## Special Thanks
## Special thanks to:
* **F123 Consulting** - Suggestions, funding, and extensive testing
* **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
* F123 Consulting for suggestions, some funding, and endless testing.
* Stormux for continuation of the project.

517
README.md
View File

@ -1,97 +1,69 @@
# Fenrir
A modern, modular, flexible and fast console screen reader.
A modern, modular, flexible and fast console screenreader.
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.
**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
- Linux (ptyDriver, vcsaDriver, evdevDriver) - Primary platform with full support
- macOS (ptyDriver) - Limited support
- BSD (ptyDriver) - Limited support
- Windows (ptyDriver) - Limited support
- Linux (ptyDriver, vcsaDriver, evdevDriver)
- macOS (ptyDriver)
- BSD (ptyDriver)
- Windows (ptyDriver)
## Core Requirements
- Python 3 >= 3.9 (recommended 3.13+)
- Screen, input, speech, sound driver dependencies (see "Features, Drivers, Extras" section)
- For full functionality on Linux: evdev, speech-dispatcher, sox
- python3 >= 3.3
- screen, input, speech, sound drivers dependencies see "Features, Drivers, Extras".
## Features, Drivers, Extras, Dependencies
### Input Drivers:
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-pyudev
- loaded uinput kernel module
- ReadWrite permission:
- /dev/input
- /dev/uinput
2. **ptyDriver** - Terminal emulation input driver (cross-platform)
- 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)
1. "evdevDriver" input driver for linux evdev
- python-evdev >=0.6.3 (This is commonly referred to as python3-evdev by your distribution)
- python-pyudev
- loaded uinput kernel module
- ReadWrite permission
- /dev/input
- /dev/uinput
2. "ptyDriver" terminal emulation input driver
- python-pyte
### Screen Drivers:
1. **vcsaDriver** - Linux VCSA devices driver (recommended for Linux TTY)
- python-dbus
- Read permission to the following files and services:
- /sys/devices/virtual/tty/tty0/active
- /dev/tty[1-64]
- /dev/vcsa[1-64]
- read logind DBUS
2. **ptyDriver** - Terminal emulation driver (cross-platform)
- python-pyte
1. "vcsaDriver" screen driver for linux VCSA devices
- python-dbus
- Read permission to the following files and services:
- /sys/devices/virtual/tty/tty0/active
- /dev/tty[1-64]
- /dev/vcsa[1-64]
- read logind DBUS
2. "ptyDriver" terminal emulation driver
- python-pyte
### Speech Drivers:
1. **speechdDriver** - Speech-dispatcher driver (recommended)
- Speech-dispatcher
- python-speechd
2. **genericDriver** - Generic subprocess speech driver
- espeak or espeak-ng (or any TTS command)
3. **debugDriver** - Debug speech driver for testing
- No dependencies
1. "genericDriver" (default) speech driver for sound as subprocess:
- espeak or espeak-ng
2. "speechdDriver" speech driver for Speech-dispatcher:
- Speech-dispatcher
- python-speechd
3. "emacspeakDriver" speech driver for emacspeak
- emacspeak
### Sound Drivers:
1. **genericDriver** (default) - Generic subprocess sound driver
- Sox with opus support (recommended)
2. **gstreamerDriver** - GStreamer sound driver
- gstreamer >=1.0
- GLib
3. **debugDriver** - Debug sound driver for testing
- No dependencies
1. "genericDriver" (default) sound driver for sound as subprocess:
- Sox
2. "gstreamerDriver" sound driver for gstreamer
- gstreamer >=1.0
- GLib
## Extras:
@ -119,353 +91,16 @@ 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:
Requires root privileges
cd src/
cd src/fenrir/
sudo ./fenrir
Settings are located in:
- **After installation**: `/etc/fenrir/settings/settings.conf`
- **Development**: `config/settings/settings.conf`
Settings "settings.conf" is located in the "config" directory or after installation in /etc/fenrir/settings.
Take care to use drivers from the config matching your installed drivers.
By default it uses:
- 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
@ -492,64 +127,12 @@ just run the configuration script twice (once as user, once as root):
The script is also located in the tools directory in git
## Command Line Options
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)
## localization
copy fenrir.mo translations file from fenrir/locale/your_language/LC_MESSAGES/fenrir.mo to /usr/share/locale/your_language/LC_MESSAGES/fenrir.mo
## Documentation and Support
- **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](https://git.stormux.org/storm/fenrir/wiki)
- **IRC**: irc.stormux.org #stormux
- **Issues**: Report bugs and feature requests on the project repository
- 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)
- IRC: irc.stormux.org #stormux

View File

@ -73,8 +73,7 @@ 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
KEY_KPPLUS=progress_bar_monitor
KEY_FENRIR,KEY_KPPLUS=silence_until_prompt
KEY_KPPLUS=last_incoming
KEY_FENRIR,KEY_F2=toggle_braille
KEY_FENRIR,KEY_F3=toggle_sound
KEY_FENRIR,KEY_F4=toggle_speech
@ -127,4 +126,3 @@ 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
KEY_FENRIR,KEY_SHIFT,KEY_V=announce_fenrir_version
KEY_F4=cycle_keyboard_layout

View File

@ -75,10 +75,9 @@ KEY_FENRIR,KEY_F2=toggle_braille
KEY_FENRIR,KEY_F3=toggle_sound
KEY_FENRIR,KEY_F4=toggle_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_RIGHTBRACE=toggle_auto_spell_check
KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_ENTER=toggle_output
KEY_FENRIR,KEY_SHIFT,KEY_ENTER=toggle_output
KEY_FENRIR,KEY_SHIFT,KEY_E=toggle_emoticons
KEY_FENRIR,KEY_ENTER=toggle_auto_read
KEY_FENRIR,KEY_CTRL,KEY_T=toggle_auto_time
@ -127,4 +126,3 @@ 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
KEY_FENRIR,KEY_SHIFT,KEY_V=announce_fenrir_version
KEY_F4=cycle_keyboard_layout

View File

@ -72,8 +72,7 @@ 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
KEY_KPPLUS=progress_bar_monitor
KEY_FENRIR,KEY_KPPLUS=silence_until_prompt
KEY_KPPLUS=last_incoming
#=toggle_braille
KEY_FENRIR,KEY_F3=toggle_sound
KEY_FENRIR,KEY_F4=toggle_speech
@ -127,4 +126,3 @@ KEY_FENRIR,KEY_CTRL,KEY_C=save_settings
KEY_FENRIR,KEY_F8=export_clipboard_to_x
KEY_FENRIR,KEY_ALT,KEY_UP=inc_alsa_volume
KEY_FENRIR,KEY_ALT,KEY_DOWN=dec_alsa_volume
KEY_F4=cycle_keyboard_layout

View File

@ -126,4 +126,3 @@ KEY_FENRIR,KEY_CTRL,KEY_C=save_settings
KEY_FENRIR,KEY_F8=export_clipboard_to_x
KEY_FENRIR,KEY_ALT,KEY_UP=inc_alsa_volume
KEY_FENRIR,KEY_ALT,KEY_DOWN=dec_alsa_volume
KEY_F4=cycle_keyboard_layout

View File

@ -85,5 +85,3 @@ alt+f12 - quit fenrir
^[[1;3F=temp_disable_speech
# control+end - toggle auto read
^[[1;5F=toggle_auto_read
# F12 - cycle keyboard layout
^[[24~=cycle_keyboard_layout

View File

@ -0,0 +1,218 @@
# 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

View File

@ -5,7 +5,7 @@
[levelDict]
none:===:
some:===:-$~+*-/\@#
most:===:.,:-_$~+*-/\@!#%^&*()[]}{<>;
most:===:.,:-$~+*-/\@!#%^&*()[]}{<>;
all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~
[punctDict]

View File

@ -5,7 +5,7 @@
[levelDict]
none:===:
some:===:-$~+*-/\@
most:===:.,:-$~+*-_/\@!#%^&*()[]}{<>;
most:===:.,:-$~+*-/\@!#%^&*()[]}{<>;
all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~
[punctDict]

View File

@ -4,7 +4,7 @@
# the entrys are seperated with :===: in words colon tripple equal colon ( to not collide with substitutions)
[levelDict]
none:===:
some:===:-$~+*-/\@_
some:===:-$~+*-/\@
most:===:.,:-$~+*-/\@!#%^&*()[]}{<>;
all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~

View File

@ -15,7 +15,7 @@ theme=default
# Sound volume controls how loud the sounds for your selected soundpack are.
# 0 is quietest, 1.0 is loudest.
volume=0.7
volume=1.0
# shell commands for generic sound driver
# the folowing variable are substituted
@ -28,9 +28,6 @@ genericPlayFileCommand=play -q -v fenrirVolume fenrirSoundFile
#the following command is used to generate a frequency beep
genericFrequencyCommand=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
# Enable progress bar monitoring with ascending tones by default
progressMonitoring=True
[speech]
# Turn speech on or off:
enabled=True
@ -95,8 +92,8 @@ fenrirMaxRate=450
driver=vcsaDriver
encoding=auto
screenUpdateDelay=0.05
ignoreScreen=
autodetectIgnoreScreen=True
suspendingScreen=
autodetectSuspendingScreen=True
[keyboard]
driver=evdevDriver
@ -134,7 +131,7 @@ punctuationProfile=default
punctuationLevel=some
respectPunctuationPause=True
newLinePause=True
numberOfClipboards=50
numberOfClipboards=10
# used path for "export_clipboard_to_file"
# $user is replaced by username
#clipboardExportPath=/home/$user/fenrirClipboard
@ -166,7 +163,7 @@ autoPresentIndent=False
# 1 = sound only
# 2 = speak only
autoPresentIndentMode=1
# play a sound when attributes change
# play a sound when attributes are changeing
hasAttributes=True
# shell for PTY emulatiun (empty = default shell)
shell=
@ -193,7 +190,7 @@ enableSettingsRemote=True
enableCommandRemote=True
[barrier]
enabled=False
enabled=True
leftBarriers=│└┌─
rightBarriers=│┘┐─
@ -214,24 +211,8 @@ list=
vmenuPath=
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]
# automatic time announcement
# automatic time anouncement
enabled=False
# present time
presentTime=True

View File

@ -1,389 +1,4 @@
# Fenrir Development Guide
1. Basic
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

File diff suppressed because it is too large Load Diff

View File

@ -1202,47 +1202,6 @@ link:#Settings[Settings]
=== 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
You can specify options that overwrite the setting.conf. This is done
@ -1265,154 +1224,9 @@ or change the debug level to verbose
fenrir -o "general#debugLevel=3"
....
Example using force all screens option:
....
fenrir -F
....
You can find the available sections and variables here #Settings See
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
the syntax of the link:#Settings[settings.conf] is quite simple and

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,12 @@
#!/usr/bin/env bash
#!/bin/bash
#Basic install script for Fenrir.
read -rp "This will install Fenrir. Press ctrl+C to cancel, or enter to continue."
read -p "This will install Fenrir. Press ctrl+C to cancel, or enter to continue." continue
# Fenrir main application
install -m755 -d /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
# tools
install -m755 -d /usr/share/fenrirscreenreader/tools
@ -32,9 +33,8 @@ cp -af config/sound/template /usr/share/sounds/fenrirscreenreader/template
# config
if [ -f "/etc/fenrirscreenreader/settings/settings.conf" ]; then
echo "Do you want to overwrite your current global settings? (y/n)"
read -r yn
yn="${yn:0:1}"
if [[ "${yn^}" == "Y" ]]; then
read yn
if [ $yn = "Y" -o $yn = "y" ]; then
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."
install -m644 -D "config/settings/settings.conf" /etc/fenrirscreenreader/settings/settings.conf

42
pyproject.toml Normal file
View File

@ -0,0 +1,42 @@
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "fenrir-screenreader"
version="2025.01.28"
authors = [
{name = "Hunter Jozwiak", email = "hunter.t.joz@gmail.com"},
{name="Storm Dragon", email="storm_dragon@stormux.org"},
{name="Jeremiah Ticket", email="seashellpromises@gmail.com"},
{name="Chrys", email="chrys@linux-a11y.org"},
]
maintainers = [
{name = "Hunter Jozwiak", email = "hunter.t.joz@gmail.com"},
{name = "Storm dragon", email = "storm_dragon@stormux.org"}]
keywords=['screenreader', 'a11y', 'accessibility', 'terminal', 'TTY', 'console']
classifiers=[
"Programming Language :: Python",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Development Status :: 5 - Production/Stable",
"Topic :: Multimedia :: Sound/Audio :: Speech",
"Environment :: Console",
]
description = "A TTY screenreader for Linux."
readme = "README.md"
requires-python = ">=3.6"
dependencies = [
"daemonize>=2.5.0",
"dbus-python>=1.2.18",
"evdev>=1.7.1",
"pexpect>=4.9.0",
"pyte>=0.8.1",
"pyudev>=0.23.2",
]
[project.scripts]
fenrir = "fenrirscreenreader:cli.main"
[dependency-groups]
dev = [
"ruff>=0.0.17",
]

10
pyproject.toml~ Normal file
View File

@ -0,0 +1,10 @@
[project]
name = "fenrir-screenreader"
author="Storm Dragon, Jeremiah, Chrys and others"
version = "2024.12.20"
description = "A TTY screenreader for Linux."
readme = "README.md"
requires-python = ">=3.6"
dependencies = [
"evdev>=1.7.1",
]

View File

@ -1,9 +0,0 @@
daemonize
evdev
pexpect
pyenchant
pyperclip
pyte
pyudev
pyxdg
setproctitle

132
setup.py
View File

@ -1,132 +0,0 @@
#!/usr/bin/env python3
import os, glob, sys
import os.path
from shutil import copyfile
from setuptools import find_namespace_packages
from setuptools import setup
# handle flags for package manager like aurman and pacaur.
# Allow both environment variable and command line flag
forceSettingsFlag = (
"--force-settings" in sys.argv or
os.environ.get('FENRIR_FORCE_SETTINGS') == '1'
)
if "--force-settings" in sys.argv:
sys.argv.remove("--force-settings")
dataFiles = []
# Handle locale files
localeFiles = glob.glob('locale/*/LC_MESSAGES/*.mo')
for localeFile in localeFiles:
lang = localeFile.split(os.sep)[1]
destDir = f'/usr/share/locale/{lang}/LC_MESSAGES'
dataFiles.append((destDir, [localeFile]))
# Handle other configuration files
directories = glob.glob('config/*')
for directory in directories:
files = glob.glob(directory+'/*')
destDir = ''
if 'config/punctuation' in directory :
destDir = '/etc/fenrirscreenreader/punctuation'
elif 'config/keyboard' in directory:
destDir = '/etc/fenrirscreenreader/keyboard'
elif 'config/settings' in directory:
destDir = '/etc/fenrirscreenreader/settings'
if not forceSettingsFlag:
try:
files = [f for f in files if not f.endswith('settings.conf')]
except:
pass
elif 'config/scripts' in directory:
destDir = '/usr/share/fenrirscreenreader/scripts'
if destDir != '':
dataFiles.append((destDir, files))
files = glob.glob('config/sound/default/*')
destDir = '/usr/share/sounds/fenrirscreenreader/default'
dataFiles.append((destDir, files))
files = glob.glob('config/sound//template/*')
destDir = '/usr/share/sounds/fenrirscreenreader/template'
dataFiles.append((destDir, files))
files = glob.glob('tools/*')
dataFiles.append(('/usr/share/fenrirscreenreader/tools', files))
dataFiles.append(('/usr/share/man/man1', ['docs/fenrir.1']))
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(
# Application name:
name="fenrir-screenreader",
# description
description="A TTY Screen Reader for Linux.",
long_description=read('README.md'),
long_description_content_type="text/markdown",
keywords=['screenreader', 'a11y', 'accessibility', 'terminal', 'TTY', 'console'],
license="License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
url="https://git.stormux.org/storm/fenrir/",
classifiers=[
"Programming Language :: Python",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Development Status :: 5 - Production/Stable",
"Topic :: Multimedia :: Sound/Audio :: Speech",
"Environment :: Console",
],
# Application author details:
author="Storm Dragon, Jeremiah, Chrys and others",
author_email="storm_dragon@stormux.org",
# Packages
package_dir={'': 'src'},
packages=find_namespace_packages(
where='src',
include=['fenrirscreenreader*']
),
scripts=['src/fenrir'],
# Include additional files into the package
include_package_data=True,
zip_safe=False,
data_files=dataFiles,
# Dependent packages (distributions)
python_requires='>=3.6',
install_requires=[
"evdev>=1.1.2",
"daemonize>=2.5.0",
"dbus-python>=1.2.8",
"pyperclip",
"pyudev>=0.21.0",
"setuptools",
"setproctitle",
"pexpect",
"pyte>=0.7.0",
],
)
if not forceSettingsFlag:
print('')
# create settings file from example if not exist
if not os.path.isfile('/etc/fenrirscreenreader/settings/settings.conf'):
try:
copyfile('config/fenrirscreenreader/settings/settings.conf', '/etc/fenrirscreenreader/settings/settings.conf')
print('create settings file in /etc/fenrirscreenreader/settings/settings.conf')
except OSError as e:
print(f"Could not copy settings file to destination: {e}")
else:
print('settings.conf file found. It is not overwritten automatical')
print('')
print('To have Fenrir start at boot:')
print('sudo systemctl enable fenrir')
print('Pulseaudio users may want to run:')
print('/usr/share/fenrirscreenreader/tools/configure_pulse.sh')
print('once as their user account and once as root to configure Pulseaudio.')
print('Please install the following packages manually:')
print('- Speech-dispatcher: for the default speech driver')
print('- Espeak: as basic TTS engine')
print('- sox: is a player for the generic sound driver')

18
src/fenrir → src/fenrirscreenreader/cli.py Executable file → Normal file
View File

@ -1,9 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
"""Module for managing the command line interface of Fenrir."""
import os
import sys
import inspect
@ -15,8 +14,8 @@ fenrirPath = os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile(in
if not fenrirPath in sys.path:
sys.path.append(fenrirPath)
from fenrirscreenreader.core import fenrirManager
from fenrirscreenreader import fenrirVersion
from .core import fenrirManager
from . import fenrirVersion
def create_argument_parser():
"""Create and return the argument parser for Fenrir"""
@ -67,17 +66,6 @@ def create_argument_parser():
action='store_true',
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
def validate_arguments(cliArgs):

View File

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

View File

@ -5,17 +5,15 @@
# By Chrys, Storm Dragon, and contributers.
from fenrirscreenreader.core import debug
import os
import importlib
import subprocess, os
from subprocess import Popen, PIPE
import _thread
import pyperclip
class command():
def __init__(self):
pass
def initialize(self, environment, scriptPath=''):
def initialize(self, environment):
self.env = environment
self.scriptPath = scriptPath
def shutdown(self):
pass
def getDescription(self):
@ -24,48 +22,56 @@ class command():
_thread.start_new_thread(self._threadRun , ())
def _threadRun(self):
try:
# Check if clipboard is empty
if self.env['runtime']['memoryManager'].isIndexListEmpty('clipboardHistory'):
self.env['runtime']['outputManager'].presentText(_('clipboard empty'), interrupt=True)
return
# Get current clipboard content
clipboard = self.env['runtime']['memoryManager'].getIndexListElement('clipboardHistory')
# Remember original display environment variable if it exists
originalDisplay = os.environ.get('DISPLAY', '')
success = False
# Try different display options
for i in range(10):
display = f":{i}"
try:
# Set display environment variable
os.environ['DISPLAY'] = display
# Attempt to set clipboard content
importlib.reload(pyperclip) # Weird workaround for some distros
pyperclip.copy(clipboard)
# If we get here without exception, we found a working display
success = True
user = self.env['general']['currUser']
# First try to find xclip in common locations
xclip_paths = [
'/usr/bin/xclip',
'/bin/xclip',
'/usr/local/bin/xclip'
]
xclip_path = None
for path in xclip_paths:
if os.path.isfile(path) and os.access(path, os.X_OK):
xclip_path = path
break
except Exception:
# Failed for this display, try next one
continue
# Restore original display setting
if originalDisplay:
os.environ['DISPLAY'] = originalDisplay
else:
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)
if not xclip_path:
self.env['runtime']['outputManager'].presentText(
'xclip not found in common locations',
interrupt=True
)
return
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:
self.env['runtime']['outputManager'].presentText('exported to the X session.', interrupt=True)
except Exception as e:
self.env['runtime']['outputManager'].presentText(str(e), soundIcon='', interrupt=False)
def setCallback(self, callback):
pass

View File

@ -5,11 +5,9 @@
# By Chrys, Storm Dragon, and contributers.
from fenrirscreenreader.core import debug
import importlib
import subprocess, os
from subprocess import Popen, PIPE
import _thread
import pyperclip
import os
class command():
def __init__(self):
pass
@ -24,41 +22,33 @@ class command():
_thread.start_new_thread(self._threadRun , ())
def _threadRun(self):
try:
# Remember original display environment variable if it exists
originalDisplay = os.environ.get('DISPLAY', '')
clipboardContent = None
# Try different display options
for i in range(10):
display = f":{i}"
try:
# Set display environment variable
os.environ['DISPLAY'] = display
# Attempt to get clipboard content
importlib.reload(pyperclip) # Weird workaround for some distros
clipboardContent = pyperclip.paste()
# If we get here without exception, we found a working display
if clipboardContent:
break
except Exception:
# Failed for this display, try next one
continue
# Restore original display setting
if originalDisplay:
os.environ['DISPLAY'] = originalDisplay
# Find xclip path
xclip_paths = ['/usr/bin/xclip', '/bin/xclip', '/usr/local/bin/xclip']
xclip_path = None
for path in xclip_paths:
if os.path.isfile(path) and os.access(path, os.X_OK):
xclip_path = path
break
if not xclip_path:
self.env['runtime']['outputManager'].presentText('xclip not found in common locations', interrupt=True)
return
xClipboard = ''
for display in range(10):
p = Popen('su ' + self.env['general']['currUser'] + ' -p -c "' + xclip_path + ' -d :' + str(display) + ' -o"', stdout=PIPE, stderr=PIPE, shell=True)
stdout, stderr = p.communicate()
self.env['runtime']['outputManager'].interruptOutput()
stderr = stderr.decode('utf-8')
xClipboard = stdout.decode('utf-8')
if (stderr == ''):
break
if stderr != '':
self.env['runtime']['outputManager'].presentText(stderr , soundIcon='', interrupt=False)
else:
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']['memoryManager'].addValueToFirstIndex('clipboardHistory', xClipboard)
self.env['runtime']['outputManager'].presentText('Import to Clipboard', soundIcon='CopyToClipboard', interrupt=True)
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)
self.env['runtime']['outputManager'].presentText(xClipboard, soundIcon='', interrupt=False)
except Exception as e:
self.env['runtime']['outputManager'].presentText(str(e), soundIcon='', interrupt=False)
self.env['runtime']['outputManager'].presentText(e , soundIcon='', interrupt=False)
def setCallback(self, callback):
pass

View File

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

View File

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

View File

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

View File

@ -18,19 +18,15 @@ class command():
def run(self):
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'interruptOnKeyPress'):
return
if self.env['runtime']['inputManager'].noKeyPressed():
return
if self.env['runtime']['screenManager'].isScreenChange():
return
if len(self.env['input']['currInput']) <= len(self.env['input']['prevInput']):
return
# if the filter is set
if self.env['runtime']['settingsManager'].getSetting('keyboard', 'interruptOnKeyPressFilter').strip() != '':
filterList = self.env['runtime']['settingsManager'].getSetting('keyboard', 'interruptOnKeyPressFilter').split(',')
for currInput in self.env['input']['currInput']:
if not currInput in filterList:
return
#if self.env['runtime']['settingsManager'].getSetting('keyboard', 'interruptOnKeyPressFilter').strip() != '':
# filterList = self.env['runtime']['settingsManager'].getSetting('keyboard', 'interruptOnKeyPressFilter').split(',')
# for currInput in self.env['input']['currInput']:
# if not currInput in filterList:
# return
self.env['runtime']['outputManager'].interruptOutput()
def setCallback(self, callback):

View File

@ -35,18 +35,12 @@ class command():
if not (self.env['runtime']['byteManager'].getLastByteKey() in [b'^[[A',b'^[[B']):
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']]
currLine = self.env['screen']['newContentText'].split('\n')[self.env['screen']['newCursor']['y']]
is_blank = currLine.strip() == ''
if prevLine == currLine:
if self.env['screen']['newDelta'] != '':
return
announce = currLine
if not is_blank:
if not currLine.isspace():
currPrompt = currLine.find('$')
rootPrompt = currLine.find('#')
if currPrompt <= 0:
@ -61,13 +55,13 @@ class command():
else:
announce = currLine
if is_blank:
if currLine.isspace():
self.env['runtime']['outputManager'].presentText(_("blank"), soundIcon='EmptyLine', interrupt=True, flush=False)
else:
self.env['runtime']['outputManager'].presentText(announce, interrupt=True, flush=False)
self.env['commandsIgnore']['onScreenUpdate']['CHAR_DELETE_ECHO'] = True
self.env['commandsIgnore']['onScreenUpdate']['CHAR_ECHO'] = True
self.env['commandsIgnore']['onScreenUpdate']['INCOMING_IGNORE'] = True
def setCallback(self, callback):
pass

View File

@ -1,158 +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 '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

View File

@ -1,101 +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 '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

View File

@ -105,7 +105,7 @@ class attributeManager():
cursorPos = cursor.copy()
try:
attribute = self.getAttributeByXY( cursorPos['x'], cursorPos['y'])
if update:
self.setLastCursorAttribute(attribute)
if not self.isLastCursorAttributeChange():
@ -155,13 +155,13 @@ class attributeManager():
attributeFormatString = attributeFormatString.replace('fenrirFGColor', _(attribute[0]))
except Exception as e:
attributeFormatString = attributeFormatString.replace('fenrirFGColor', '')
# 1 BG color (name)
try:
attributeFormatString = attributeFormatString.replace('fenrirBGColor', _(attribute[1]))
except Exception as e:
attributeFormatString = attributeFormatString.replace('fenrirBGColor', '')
# 2 bold (True/ False)
try:
if attribute[2]:
@ -169,7 +169,7 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirBold', '')
# 3 italics (True/ False)
try:
if attribute[3]:
@ -177,7 +177,7 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirItalics', '')
# 4 underline (True/ False)
try:
if attribute[4]:
@ -185,7 +185,7 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirUnderline', '')
# 5 strikethrough (True/ False)
try:
if attribute[5]:
@ -193,7 +193,7 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirStrikethrough', '')
# 6 reverse (True/ False)
try:
if attribute[6]:
@ -201,7 +201,7 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirReverse', '')
# 7 blink (True/ False)
try:
if attribute[7]:
@ -209,7 +209,7 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirBlink', '')
# 8 font size (int/ string)
try:
try:
@ -223,14 +223,14 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirFontSize', _('default'))
# 9 font family (string)
try:
attributeFormatString = attributeFormatString.replace('fenrirFont', attribute[9])
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirFont', _('default'))
return attributeFormatString
def trackHighlights(self):
result = ''
@ -287,4 +287,4 @@ class attributeManager():
useful = True
return useful

View File

@ -18,7 +18,7 @@ class barrierManager():
def updateBarrierChange(self, isBarrier):
self.prefIsBarrier = self.currIsBarrier
self.currIsBarrier = isBarrier
def resetBarrierChange(self):
self.currIsBarrier = False
self.prefIsBarrier = False
@ -38,7 +38,7 @@ class barrierManager():
self.env['runtime']['outputManager'].playSoundIcon(soundIcon='BarrierStart', interrupt=doInterrupt)
else:
self.env['runtime']['outputManager'].playSoundIcon(soundIcon='BarrierEnd', interrupt=doInterrupt)
if not isBarrier:
sayLine = ''
return isBarrier, sayLine

View File

@ -27,7 +27,7 @@ class commandManager():
# scripts for scriptKey
self.env['runtime']['commandManager'].loadScriptCommands()
def shutdown(self):
for commandFolder in self.env['general']['commandFolderList']:
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(str(e),debug.debugLevel.ERROR)
continue
def loadScriptCommands(self, section='commands', scriptPath=''):
if scriptPath =='':
scriptPath = self.env['runtime']['settingsManager'].getSetting('general', 'scriptPath')
@ -159,24 +159,18 @@ class commandManager():
self.env['runtime']['debug'].writeDebugOut("Loading script:" + fileName ,debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e),debug.debugLevel.ERROR)
continue
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]):
try:
self.env['commands'][section][command].shutdown()
del self.env['commands'][section][command]
except Exception as e:
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("Shutdown command:" + section + "." + command ,debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e),debug.debugLevel.ERROR)
continue
def executeSwitchTrigger(self, trigger, unLoadScript, loadScript):
if self.env['runtime']['screenManager'].isIgnoredScreen():
if self.env['runtime']['screenManager'].isSuspendingScreen():
return
#unload
oldScript = unLoadScript
@ -199,7 +193,7 @@ class commandManager():
def executeDefaultTrigger(self, trigger, force=False):
if not force:
if self.env['runtime']['screenManager'].isIgnoredScreen():
if self.env['runtime']['screenManager'].isSuspendingScreen():
return
for command in sorted(self.env['commands'][trigger]):
if self.commandExists(command, trigger):
@ -214,7 +208,7 @@ class commandManager():
self.env['runtime']['debug'].writeDebugOut("Executing trigger:" + trigger + "." + command + str(e) ,debug.debugLevel.ERROR)
def executeCommand(self, command, section = 'commands'):
if self.env['runtime']['screenManager'].isIgnoredScreen():
if self.env['runtime']['screenManager'].isSuspendingScreen():
return
if self.commandExists(command, section):
try:
@ -228,7 +222,7 @@ class commandManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("Executing command:" + section + "." + command +' ' + str(e),debug.debugLevel.ERROR)
def runCommand(self, command, section = 'commands'):
if self.commandExists(command, section):
try:
@ -237,7 +231,7 @@ class commandManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("runCommand command:" + section + "." + command +' ' + str(e),debug.debugLevel.ERROR)
self.env['commandInfo']['lastCommandExecutionTime'] = time.time()
def getCommandDescription(self, command, section = 'commands'):
if self.commandExists(command, section):
try:
@ -245,7 +239,7 @@ class commandManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut('commandManager.getCommandDescription:' + str(e),debug.debugLevel.ERROR)
self.env['commandInfo']['lastCommandExecutionTime'] = time.time()
def commandExists(self, command, section = 'commands'):
try:
return( command in self.env['commands'][section])

View File

@ -11,14 +11,6 @@ class cursorManager():
pass
def initialize(self, 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):
pass
def clearMarks(self):
@ -55,7 +47,7 @@ class cursorManager():
return
self.env['screen']['oldCursorReview'] = None
self.env['screen']['newCursorReview'] = None
def isCursorHorizontalMove(self):
return self.env['screen']['newCursor']['x'] != self.env['screen']['oldCursor']['x']
@ -64,7 +56,7 @@ class cursorManager():
def isReviewMode(self):
return self.env['screen']['newCursorReview'] != None
def enterReviewModeCurrTextCursor(self, overwrite=False):
if self.isReviewMode() and not overwrite:
return
@ -81,7 +73,7 @@ class cursorManager():
self.env['screen']['oldCursorReview'] = self.env['screen']['newCursorReview']
self.env['screen']['newCursorReview']['x'] = x
self.env['screen']['newCursorReview']['y'] = y
def isApplicationWindowSet(self):
try:
currApp = self.env['runtime']['applicationManager'].getCurrentApplication()
@ -116,7 +108,7 @@ class cursorManager():
currApp = self.env['runtime']['applicationManager'].getCurrentApplication()
self.env['commandBuffer']['windowArea'][currApp] = {}
if x1 * y1 <= \
x2 * y2:
self.env['commandBuffer']['windowArea'][currApp]['1'] = {'x':x1, 'y':y1}

View File

@ -36,14 +36,14 @@ class debugManager():
except Exception as e:
print(e)
def writeDebugOut(self, text, level = debug.debugLevel.DEACTIVE, onAnyLevel=False):
mode = self.env['runtime']['settingsManager'].getSetting('general','debugMode')
if mode == '':
mode = 'FILE'
mode = mode.upper().split(',')
fileMode = 'FILE' in mode
printMode = 'PRINT' in mode
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)) :
if self._fileOpened:
@ -52,15 +52,12 @@ class debugManager():
else:
if not self._fileOpened and fileMode:
self.openDebugFile()
timestamp = str(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f'))
if onAnyLevel:
levelInfo = 'INFO ANY'
msg = 'ANY '+ str(level) + ' ' + str(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f'))
else:
levelInfo = str(level)
# Changed order: text comes first, then level and timestamp
msg = text + ' - ' + levelInfo + ' ' + timestamp
msg = str(level) +' ' + str(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')
)
msg += ': ' + text
if printMode:
print(msg)
if fileMode:

View File

@ -21,7 +21,7 @@ class eventManager():
self.env = environment
def shutdown(self):
self.cleanEventQueue()
def proceedEventLoop(self):
event = self._eventQueue.get()
st = time.time()

View File

@ -23,11 +23,11 @@ class fenrirManager():
raise RuntimeError('Cannot Initialize. Maybe the configfile is not available or not parseable')
except RuntimeError:
raise
self.environment['runtime']['outputManager'].presentText(_("Start Fenrir"), soundIcon='ScreenReaderOn', interrupt=True)
signal.signal(signal.SIGINT, self.captureSignal)
signal.signal(signal.SIGTERM, self.captureSignal)
self.isInitialized = True
self.modifierInput = False
self.singleKeyCommand = False
@ -42,10 +42,10 @@ class fenrirManager():
def handleInput(self, event):
self.environment['runtime']['debug'].writeDebugOut('DEBUG INPUT fenrirMan:' + str(event), debug.debugLevel.INFO)
if not event['Data']:
event['Data'] = self.environment['runtime']['inputManager'].getInputEvent()
if event['Data']:
event['Data']['EventName'] = self.environment['runtime']['inputManager'].convertEventName(event['Data']['EventName'])
self.environment['runtime']['inputManager'].handleInputEvent(event['Data'])
@ -54,8 +54,8 @@ class fenrirManager():
if self.environment['runtime']['inputManager'].noKeyPressed():
self.environment['runtime']['inputManager'].clearLastDeepInput()
if self.environment['runtime']['screenManager'].isIgnoredScreen():
if self.environment['runtime']['screenManager'].isSuspendingScreen():
self.environment['runtime']['inputManager'].writeEventBuffer()
else:
if self.environment['runtime']['helpManager'].isTutorialMode():
@ -74,7 +74,7 @@ class fenrirManager():
self.environment['runtime']['inputManager'].clearEventBuffer()
else:
self.environment['runtime']['inputManager'].writeEventBuffer()
if self.environment['runtime']['inputManager'].noKeyPressed():
self.modifierInput = False
self.singleKeyCommand = False
@ -83,7 +83,7 @@ class fenrirManager():
if self.environment['input']['keyForeward'] > 0:
self.environment['input']['keyForeward'] -= 1
self.environment['runtime']['commandManager'].executeDefaultTrigger('onKeyInput')
def handleByteInput(self, event):
@ -124,14 +124,14 @@ class fenrirManager():
def handleScreenUpdate(self, event):
self.environment['runtime']['screenManager'].handleScreenUpdate(event['Data'])
if time.time() - self.environment['runtime']['inputManager'].getLastInputTime() >= 0.3:
self.environment['runtime']['inputManager'].clearLastDeepInput()
if (self.environment['runtime']['cursorManager'].isCursorVerticalMove() or
self.environment['runtime']['cursorManager'].isCursorHorizontalMove()):
self.environment['runtime']['commandManager'].executeDefaultTrigger('onCursorChange')
self.environment['runtime']['commandManager'].executeDefaultTrigger('onScreenUpdate')
self.environment['runtime']['inputManager'].clearLastDeepInput()
@ -150,17 +150,17 @@ class fenrirManager():
def detectShortcutCommand(self):
if self.environment['input']['keyForeward'] > 0:
return
if len(self.environment['input']['prevInput']) > len(self.environment['input']['currInput']):
return
if self.environment['runtime']['inputManager'].isKeyPress():
self.modifierInput = self.environment['runtime']['inputManager'].currKeyIsModifier()
else:
if not self.environment['runtime']['inputManager'].noKeyPressed():
if self.singleKeyCommand:
self.singleKeyCommand = len(self.environment['input']['currInput']) == 1
if not(self.singleKeyCommand and self.environment['runtime']['inputManager'].noKeyPressed()):
currentShortcut = self.environment['runtime']['inputManager'].getCurrShortcut()
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']['eventManager'].cleanEventQueue()
time.sleep(0.6)
for currentManager in self.environment['general']['managerList']:
if self.environment['runtime'][currentManager]:
self.environment['runtime'][currentManager].shutdown()

View File

@ -42,25 +42,6 @@ class inputDriver():
if not self._initialized:
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):
if not self._initialized:
return False

View File

@ -49,7 +49,6 @@ class inputManager():
return event
def setExecuteDeviceGrab(self, newExecuteDeviceGrab = True):
self.executeDeviceGrab = newExecuteDeviceGrab
def handleDeviceGrab(self, force = False):
if force:
self.setExecuteDeviceGrab()
@ -62,38 +61,17 @@ class inputManager():
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
self.executeDeviceGrab = False
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():
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)
self.env['runtime']['debug'].writeDebugOut(f"retry ungrabAllDevices {retryCount}/{maxRetries}", debug.debugLevel.WARNING)
self.env['runtime']['debug'].writeDebugOut("retry ungrabAllDevices " ,debug.debugLevel.WARNING)
self.env['runtime']['debug'].writeDebugOut("All devices ungrabbed" ,debug.debugLevel.INFO)
else:
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)
self.env['runtime']['debug'].writeDebugOut(f"retry grabAllDevices {retryCount}/{maxRetries}", debug.debugLevel.WARNING)
self.env['runtime']['debug'].writeDebugOut("retry grabAllDevices" ,debug.debugLevel.WARNING)
self.env['runtime']['debug'].writeDebugOut("All devices grabbed" ,debug.debugLevel.INFO)
self.executeDeviceGrab = False
def sendKeys(self, keyMacro):
for e in keyMacro:
key = ''
@ -274,39 +252,17 @@ class inputManager():
def getCurrShortcut(self, inputSequence = None):
shortcut = []
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:
# 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)
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'])
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 = []
self.env['input']['shortcutRepeat'] = 1
shortcut.append(self.env['input']['shortcutRepeat'])
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)
def currKeyIsModifier(self):
@ -389,35 +345,3 @@ class inputManager():
self.lastDetectedDevices =devices
def getLastDetectedDevices(self):
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
)

View File

@ -22,7 +22,7 @@ class outputManager():
def shutdown(self):
self.env['runtime']['settingsManager'].shutdownDriver('soundDriver')
self.env['runtime']['settingsManager'].shutdownDriver('speechDriver')
def presentText(self, text, interrupt=True, soundIcon='', ignorePunctuation=False, announceCapital=False, flush=True):
if text == '':
return
@ -58,13 +58,13 @@ class outputManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("setting speech language in outputManager.speakText", debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
try:
self.env['runtime']['speechDriver'].setVoice(self.env['runtime']['settingsManager'].getSetting('speech', 'voice'))
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(str(e), debug.debugLevel.ERROR)
try:
if announceCapital:
self.env['runtime']['speechDriver'].setPitch(self.env['runtime']['settingsManager'].getSettingAsFloat('speech', 'capitalPitch'))
@ -73,13 +73,13 @@ class outputManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("setting speech pitch in outputManager.speakText", debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
try:
self.env['runtime']['speechDriver'].setRate(self.env['runtime']['settingsManager'].getSettingAsFloat('speech', 'rate'))
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("setting speech rate in outputManager.speakText", debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
try:
self.env['runtime']['speechDriver'].setModule(self.env['runtime']['settingsManager'].getSetting('speech', 'module'))
except Exception as e:
@ -91,7 +91,7 @@ class outputManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("setting speech volume in outputManager.speakText ", debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
try:
if self.env['runtime']['settingsManager'].getSettingAsBool('general', 'newLinePause'):
cleanText = text.replace('\n', ' , ')

View File

@ -20,7 +20,7 @@ class processManager():
self.addSimpleEventThread(fenrirEventType.HeartBeat, self.heartBeatTimer, multiprocess=True)
def shutdown(self):
self.terminateAllProcesses()
def terminateAllProcesses(self):
for proc in self._Processes:
#try:

View File

@ -57,35 +57,6 @@ class remoteManager():
def shutdown(self):
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):
if not self.env['runtime']['settingsManager'].getSettingAsBool('remote', 'enableSettingsRemote'):
return
@ -106,61 +77,6 @@ class remoteManager():
elif upperSettingsText == self.resetSettingConst:
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):
if not self.env['runtime']['settingsManager'].getSettingAsBool('remote', 'enableCommandRemote'):
return
@ -256,7 +172,7 @@ class remoteManager():
self.env['runtime']['outputManager'].presentText(_('clipboard exported to file'), interrupt=True)
except Exception as e:
self.env['runtime']['debug'].writeDebugOut('export_clipboard_to_file:run: Filepath:'+ clipboardFile +' trace:' + str(e),debug.debugLevel.ERROR)
def saveSettings(self, settingConfigPath = None):
if not settingConfigPath:
settingConfigPath = self.env['runtime']['settingsManager'].getSettingsFile()
@ -269,25 +185,6 @@ class remoteManager():
self.env['runtime']['settingsManager'].parseSettingArgs(settingsArgs)
self.env['runtime']['screenManager'].updateScreenIgnored()
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):
if not eventData:
return

View File

@ -59,7 +59,7 @@ class screenManager():
if self.isCurrScreenIgnoredChanged():
self.env['runtime']['inputManager'].setExecuteDeviceGrab()
self.env['runtime']['inputManager'].handleDeviceGrab()
if not self.isIgnoredScreen(self.env['screen']['newTTY']):
if not self.isSuspendingScreen(self.env['screen']['newTTY']):
self.update(eventData, 'onScreenChange')
self.env['screen']['lastScreenUpdate'] = time.time()
else:
@ -81,7 +81,7 @@ class screenManager():
return self.prevScreenIgnored
def updateScreenIgnored(self):
self.prevScreenIgnored = self.currScreenIgnored
self.currScreenIgnored = self.isIgnoredScreen(self.env['screen']['newTTY'])
self.currScreenIgnored = self.isSuspendingScreen(self.env['screen']['newTTY'])
def update(self, eventData, trigger='onUpdate'):
# set new "old" values
self.env['screen']['oldContentBytes'] = self.env['screen']['newContentBytes']
@ -174,19 +174,16 @@ class screenManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut('screenManager:update:highlight: ' + str(e),debug.debugLevel.ERROR)
def isIgnoredScreen(self, screen = None):
def isSuspendingScreen(self, screen = None):
if screen == None:
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 = []
fixIgnoreScreens = self.env['runtime']['settingsManager'].getSetting('screen', 'ignoreScreen')
fixIgnoreScreens = self.env['runtime']['settingsManager'].getSetting('screen', 'suspendingScreen')
if fixIgnoreScreens != '':
ignoreScreens.extend(fixIgnoreScreens.split(','))
if self.env['runtime']['settingsManager'].getSettingAsBool('screen', 'autodetectIgnoreScreen'):
if self.env['runtime']['settingsManager'].getSettingAsBool('screen', 'autodetectSuspendingScreen'):
ignoreScreens.extend(self.env['screen']['autoIgnoreScreens'])
self.env['runtime']['debug'].writeDebugOut('screenManager:isIgnoredScreen ignore:' + str(ignoreScreens) + ' current:'+ str(screen ), debug.debugLevel.INFO)
self.env['runtime']['debug'].writeDebugOut('screenManager:isSuspendingScreen ignore:' + str(ignoreScreens) + ' current:'+ str(screen ), debug.debugLevel.INFO)
return (screen in ignoreScreens)
def isScreenChange(self):

View File

@ -40,8 +40,8 @@ settingsData = {
'driver': 'vcsaDriver',
'encoding': 'auto',
'screenUpdateDelay': 0.1,
'ignoreScreen': '',
'autodetectIgnoreScreen': False,
'suspendingScreen': '',
'autodetectSuspendingScreen': False,
},
'general':{
'debugLevel': debug.debugLevel.DEACTIVE,

View File

@ -317,17 +317,6 @@ class settingsManager():
environment['runtime']['debug'] = debugManager.debugManager(self.env['runtime']['settingsManager'].getSetting('general','debugFile'))
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 os.path.exists(soundRoot + self.getSetting('sound','theme')):

View File

@ -29,7 +29,7 @@ class speechDriver():
return
if not queueable:
self.cancel()
def cancel(self):
if not self._isInitialized:
return

View File

@ -34,7 +34,7 @@ class tableManager():
return ''
def setRowColumnSep(self, columnSep = ''):
self.rowColumnSep = columnSep
def setHeadLine(self, headLine = ''):
self.setHeadColumnSep()
self.setRowColumnSep()

View File

@ -38,7 +38,7 @@ class textManager():
if name[0] == name[1]:
newText += ' ' + str(numberOfChars) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' '
else:
newText += ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' ' + str(int(numberOfChars / 2)) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[1], True) + ' '
newText += ' ' + str(int(numberOfChars / 2)) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name, True) + ' '
lastPos = span[1]
if lastPos != 0:
newText += ' '
@ -46,7 +46,7 @@ class textManager():
lastPos = 0
for match in self.regExSingle.finditer(newText):
span = match.span()
result += newText[lastPos:span[0]]
result += text[lastPos:span[0]]
numberOfChars = len(newText[span[0]:span[1]])
name = newText[span[0]:span[1]][:2]
if not self.env['runtime']['punctuationManager'].isPuctuation(name[0]):
@ -55,7 +55,7 @@ class textManager():
if name[0] == name[1]:
result += ' ' + str(numberOfChars) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' '
else:
result += ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' ' + str(int(numberOfChars / 2)) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[1], True) + ' '
result += ' ' + str(int(numberOfChars / 2)) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name, True) + ' '
lastPos = span[1]
if lastPos != 0:
result += ' '

View File

@ -3,6 +3,5 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributers.
version = "2025.06.07"
codeName = "master"
version = "2025.01.28"
codeName = "testing"

View File

@ -59,7 +59,7 @@ class driver(inputDriver):
self.env['runtime']['processManager'].addCustomEventThread(self.inputWatchdog)
self._initialized = True
def plugInputDeviceWatchdogUdev(self, active, eventQueue):
def plugInputDeviceWatchdogUdev(self,active , eventQueue):
context = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(context)
monitor.filter_by(subsystem='input')
@ -72,33 +72,31 @@ class driver(inputDriver):
self.env['runtime']['debug'].writeDebugOut('plugInputDeviceWatchdogUdev:' + str(device), debug.debugLevel.INFO)
try:
try:
# FIX: Check if attributes exist before accessing them
if hasattr(device, 'name') and device.name and device.name.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
if device.name.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
ignorePlug = True
if hasattr(device, 'phys') and device.phys and device.phys.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
if device.phys.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
ignorePlug = True
if hasattr(device, 'name') and device.name and 'BRLTTY' in device.name.upper():
if 'BRLTTY' in device.name.upper():
ignorePlug = True
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:
virtual = '/sys/devices/virtual/input/' in device.sys_path
if device.device_node:
validDevices.append({'device': device.device_node, 'virtual': virtual})
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:
pollTimeout = 1
device = monitor.poll(pollTimeout)
except Exception:
except:
device = None
ignorePlug = False
if validDevices:
eventQueue.put({"Type": fenrirEventType.PlugInputDevice, "Data": validDevices})
eventQueue.put({"Type":fenrirEventType.PlugInputDevice,"Data":validDevices})
return time.time()
def inputWatchdog(self, active, eventQueue):
def inputWatchdog(self,active , eventQueue):
try:
while active.value:
r, w, x = select(self.iDevices, [], [], 0.8)
@ -113,7 +111,7 @@ class driver(inputDriver):
self.removeDevice(fd)
while(event):
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 not foundKeyInSequence:
foundKeyInSequence = True
@ -125,11 +123,11 @@ class driver(inputDriver):
if not isinstance(currMapEvent['EventName'], str):
event = self.iDevices[fd].read_one()
continue
if currMapEvent['EventState'] in [0, 1, 2]:
eventQueue.put({"Type": fenrirEventType.KeyboardInput, "Data": currMapEvent.copy()})
if currMapEvent['EventState'] in [0,1,2]:
eventQueue.put({"Type":fenrirEventType.KeyboardInput,"Data":currMapEvent.copy()})
eventFired = True
else:
if event.type in [2, 3]:
if event.type in [2,3]:
foreward = True
event = self.iDevices[fd].read_one()
@ -138,7 +136,7 @@ class driver(inputDriver):
self.writeEventBuffer()
self.clearEventBuffer()
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):
if not self._initialized:
@ -148,7 +146,7 @@ class driver(inputDriver):
if uDevice:
if self.gDevices[iDevice.fd]:
self.writeUInput(uDevice, event)
except Exception:
except Exception as e:
pass
def writeUInput(self, uDevice, event):
@ -158,7 +156,7 @@ class driver(inputDriver):
time.sleep(0.0000002)
uDevice.syn()
def updateInputDevices(self, newDevices=None, init=False):
def updateInputDevices(self, newDevices = None, init = False):
if init:
self.removeAllDevices()
@ -193,7 +191,7 @@ class driver(inputDriver):
try:
with open(deviceFile) as f:
pass
except Exception:
except Exception as e:
continue
# 3 pos absolute
# 2 pos relative
@ -203,23 +201,22 @@ class driver(inputDriver):
except:
continue
try:
# FIX: Check if attributes exist before accessing them
if hasattr(currDevice, 'name') and currDevice.name and currDevice.name.upper() in ['', 'SPEAKUP', 'FENRIR-UINPUT']:
if currDevice.name.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
continue
if hasattr(currDevice, 'phys') and currDevice.phys and currDevice.phys.upper() in ['', 'SPEAKUP', 'FENRIR-UINPUT']:
if currDevice.phys.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
continue
if hasattr(currDevice, 'name') and currDevice.name and 'BRLTTY' in currDevice.name.upper():
if 'BRLTTY' in currDevice.name.upper():
continue
except:
pass
cap = currDevice.capabilities()
if mode in ['ALL', 'NOMICE']:
if mode in ['ALL','NOMICE']:
if eventType.EV_KEY in cap:
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
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
if mode == 'ALL':
self.addDevice(currDevice)
@ -227,20 +224,16 @@ class driver(inputDriver):
elif mode == 'NOMICE':
if not ((eventType.EV_REL in cap) or (eventType.EV_ABS in cap)):
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:
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:
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(','):
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:
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.env['runtime']['debug'].writeDebugOut("Device Skipped (Exception): " + deviceFile +' ' + currDevice.name +' '+ str(e),debug.debugLevel.INFO)
self.iDeviceNo = len(evdev.list_devices())
self.updateMPiDevicesFD()
@ -254,7 +247,6 @@ class driver(inputDriver):
self.iDevicesFD.remove(fd)
except:
pass
def mapEvent(self, event):
if not self._initialized:
return None
@ -274,12 +266,12 @@ class driver(inputDriver):
mEvent['EventSec'] = event.sec
mEvent['EventUsec'] = event.usec
mEvent['EventState'] = event.value
mEvent['EventType'] = event.type
mEvent['EventType'] = event.type
return mEvent
except Exception:
except Exception as e:
return None
def getLedState(self, led=0):
def getLedState(self, led = 0):
if not self.hasIDevices():
return False
# 0 = Numlock
@ -289,8 +281,7 @@ class driver(inputDriver):
if led in dev.leds():
return True
return False
def toggleLedState(self, led=0):
def toggleLedState(self, led = 0):
if not self.hasIDevices():
return False
ledState = self.getLedState(led)
@ -299,10 +290,9 @@ class driver(inputDriver):
# 17 LEDs
if 17 in self.iDevices[i].capabilities():
if ledState == 1:
self.iDevices[i].set_led(led, 0)
self.iDevices[i].set_led(led , 0)
else:
self.iDevices[i].set_led(led, 1)
self.iDevices[i].set_led(led , 1)
def grabAllDevices(self):
if not self._initialized:
return True
@ -311,7 +301,6 @@ class driver(inputDriver):
if not self.gDevices[fd]:
ok = ok and self.grabDevice(fd)
return ok
def ungrabAllDevices(self):
if not self._initialized:
return True
@ -320,7 +309,6 @@ class driver(inputDriver):
if self.gDevices[fd]:
ok = ok and self.ungrabDevice(fd)
return ok
def createUInputDev(self, fd):
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
self.uDevices[fd] = None
@ -336,21 +324,20 @@ class driver(inputDriver):
self.uDevices[fd] = UInput.from_device(self.iDevices[fd], name='fenrir-uinput', phys='fenrir-uinput')
except Exception as e:
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]
cap = dev.capabilities()
del cap[0]
self.uDevices[fd] = UInput(
cap,
name='fenrir-uinput',
phys='fenrir-uinput'
name = 'fenrir-uinput',
phys = 'fenrir-uinput'
)
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
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:
self.iDevices[newDevice.fd] = newDevice
self.createUInputDev(newDevice.fd)
@ -373,13 +360,10 @@ class driver(inputDriver):
def grabDevice(self, fd):
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
return True
# FIX: Handle exception variable scope correctly
grab_error = None
try:
self.iDevices[fd].grab()
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
if self.uDevices[fd]:
modifierKeys = [e.KEY_LEFTCTRL, e.KEY_RIGHTCTRL, e.KEY_LEFTALT, e.KEY_RIGHTALT, e.KEY_LEFTSHIFT, e.KEY_RIGHTSHIFT]
@ -387,44 +371,33 @@ class driver(inputDriver):
try:
self.uDevices[fd].write(e.EV_KEY, key, 0) # 0 = key up
self.uDevices[fd].syn()
except Exception as mod_error:
self.env['runtime']['debug'].writeDebugOut('Failed to reset modifier key: ' + str(mod_error), debug.debugLevel.WARNING)
except Exception as e:
self.env['runtime']['debug'].writeDebugOut('Failed to reset modifier key: ' + str(e), debug.debugLevel.WARNING)
except IOError:
if not self.gDevices[fd]:
return False
except Exception as ex:
grab_error = ex
if grab_error:
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: grabing not possible: ' + str(grab_error), debug.debugLevel.ERROR)
except Exception as e:
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: grabing not possible: ' + str(e),debug.debugLevel.ERROR)
return False
return True
def ungrabDevice(self, fd):
def ungrabDevice(self,fd):
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
return True
# FIX: Handle exception variable scope correctly
ungrab_error = None
try:
self.iDevices[fd].ungrab()
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:
if self.gDevices[fd]:
return False
except Exception as ex:
ungrab_error = ex
if ungrab_error:
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: ungrabing not possible: ' + str(ungrab_error), debug.debugLevel.ERROR)
# self.gDevices[fd] = False
# #self.removeDevice(fd)
except Exception as e:
return False
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()
try:
self.ungrabDevice(fd)
@ -479,4 +452,4 @@ class driver(inputDriver):
self.iDevices.clear()
self.uDevices.clear()
self.gDevices.clear()
self.iDeviceNo = 0
self.iDeviceNo = 0

View File

@ -33,7 +33,7 @@ class driver(remoteDriver):
os.unlink(socketFile)
self.fenrirSock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.fenrirSock.bind(socketFile)
os.chmod(socketFile, 0o666)
os.chmod(socketFile, 0o222)
self.fenrirSock.listen(1)
while active.value:
# Check if the client is still connected and if data is available:

261
uv.lock generated Normal file
View File

@ -0,0 +1,261 @@
version = 1
requires-python = ">=3.6"
resolution-markers = [
"python_full_version >= '3.8'",
"python_full_version == '3.7.*'",
"python_full_version < '3.7'",
]
[[package]]
name = "daemonize"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/20/96f7dbc23812cfe4cf479c87af3e4305d0d115fd1fffec32ddeee7b9c82b/daemonize-2.5.0.tar.gz", hash = "sha256:dd026e4ff8d22cb016ed2130bc738b7d4b1da597ef93c074d2adb9e4dea08bc3", size = 8759 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/ad/1b20db02287afd40d3130a218ac5ce2f7d2ab581cfda29bada5e1c4bee17/daemonize-2.5.0-py2.py3-none-any.whl", hash = "sha256:9b6b91311a9d934ff3f5f766666635ca280d3de8e7137e4cd7d3f052543b989f", size = 5231 },
]
[[package]]
name = "dbus-python"
version = "1.2.18"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.7'",
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/5c/ccfc167485806c1936f7d3ba97db6c448d0089c5746ba105b6eb22dba60e/dbus-python-1.2.18.tar.gz", hash = "sha256:92bdd1e68b45596c833307a5ff4b217ee6929a1502f5341bae28fd120acf7260", size = 578204 }
[[package]]
name = "dbus-python"
version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.8'",
"python_full_version == '3.7.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/c1/d3/6be85a9c772d6ebba0cc3ab37390dd6620006dcced758667e0217fb13307/dbus-python-1.3.2.tar.gz", hash = "sha256:ad67819308618b5069537be237f8e68ca1c7fcc95ee4a121fe6845b1418248f8", size = 605495 }
[[package]]
name = "evdev"
version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/12/bb/f622a8a5e64d46ca83020a761877c0ead19140903c9aaf1431f3c531fdf6/evdev-1.7.1.tar.gz", hash = "sha256:0c72c370bda29d857e188d931019c32651a9c1ea977c08c8d939b1ced1637fde", size = 30705 }
[[package]]
name = "fenrir-screenreader"
version = "2025.1.28"
source = { editable = "." }
dependencies = [
{ name = "daemonize" },
{ name = "dbus-python", version = "1.2.18", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.7'" },
{ name = "dbus-python", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.7'" },
{ name = "evdev" },
{ name = "pexpect" },
{ name = "pyte", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" },
{ name = "pyte", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" },
{ name = "pyudev", version = "0.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.7'" },
{ name = "pyudev", version = "0.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.7'" },
]
[package.dev-dependencies]
dev = [
{ name = "ruff", version = "0.0.17", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.7'" },
{ name = "ruff", version = "0.9.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.7'" },
]
[package.metadata]
requires-dist = [
{ name = "daemonize", specifier = ">=2.5.0" },
{ name = "dbus-python", specifier = ">=1.2.18" },
{ name = "evdev", specifier = ">=1.7.1" },
{ name = "pexpect", specifier = ">=4.9.0" },
{ name = "pyte", specifier = ">=0.8.1" },
{ name = "pyudev", specifier = ">=0.23.2" },
]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.0.17" }]
[[package]]
name = "pexpect"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ptyprocess" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 },
]
[[package]]
name = "ptyprocess"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 },
]
[[package]]
name = "pyte"
version = "0.8.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.7.*'",
"python_full_version < '3.7'",
]
dependencies = [
{ name = "wcwidth", marker = "python_full_version < '3.8'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/60/442cdc1cba83710770672ef61e186be8746f419a12b2c84ba36e9a96276d/pyte-0.8.1.tar.gz", hash = "sha256:b9bfd1b781759e7572a6e553c010cc93eef58a19d8d1590446d84c19b1b097b0", size = 51657 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/c8/c7313e4e1849a86ff8bdbb9731fd6a32cb555feb27f33529a1cdc2c0427a/pyte-0.8.1-py3-none-any.whl", hash = "sha256:d760ea9a7d455d179d9d7a4288fac3d231190b5226715f1fe8c62547bed9b9aa", size = 30767 },
]
[[package]]
name = "pyte"
version = "0.8.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.8'",
]
dependencies = [
{ name = "wcwidth", marker = "python_full_version >= '3.8'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/ab/b599762933eba04de7dc5b31ae083112a6c9a9db15b01d3109ad797559d9/pyte-0.8.2.tar.gz", hash = "sha256:5af970e843fa96a97149d64e170c984721f20e52227a2f57f0a54207f08f083f", size = 92301 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/d0/bb522283b90853afbf506cd5b71c650cf708829914efd0003d615cf426cd/pyte-0.8.2-py3-none-any.whl", hash = "sha256:85db42a35798a5aafa96ac4d8da78b090b2c933248819157fc0e6f78876a0135", size = 31627 },
]
[[package]]
name = "pyudev"
version = "0.23.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.7'",
]
dependencies = [
{ name = "six", marker = "python_full_version < '3.7'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/fa/ae6c1a1a75f19560bbd875a579b2ca9b32deeae6a4c4a1997f4ec69a013e/pyudev-0.23.2.tar.gz", hash = "sha256:32ae3585b320a51bc283e0a04000fd8a25599edb44541e2f5034f6afee5d15cc", size = 87199 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/95/4c67255c65da9c939903cb95c57bd1ad7c920a7b711066aaa56cd7d149ab/pyudev-0.23.2-py3-none-any.whl", hash = "sha256:50d94bef0669f9aabd323a2259be67e8d49b9ebab9eae27b2cf8262767f9a2ae", size = 63903 },
]
[[package]]
name = "pyudev"
version = "0.24.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.8'",
"python_full_version == '3.7.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/5c/6cc034da13830e3da123ccf9a30910bc868fa16670362f004e4b788d0df1/pyudev-0.24.3.tar.gz", hash = "sha256:2e945427a21674893bb97632401db62139d91cea1ee96137cc7b07ad22198fc7", size = 55970 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/3b/c37870f68ceb067707ca7b04db364a1478fcd40c6194007fb6e492ff9a92/pyudev-0.24.3-py3-none-any.whl", hash = "sha256:e8246f0a014fe370119ba2bc781bfbe62c0298d0d6b39c94e83102a8a3f56960", size = 62677 },
]
[[package]]
name = "ruff"
version = "0.0.17"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.7'",
]
sdist = { url = "https://files.pythonhosted.org/packages/aa/ed/7adc91572c08f346976335f6b1b22774ea555d11043a9ff013f962affab5/ruff-0.0.17.tar.gz", hash = "sha256:5815383171ccbab333d6b6d54253e91003ee6be4627738d56855cbefc393df41", size = 54259 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/3b/4a6b289ab3ca80109402c15dd0fc83ef1c77572453b346a74ebc55666db5/ruff-0.0.17-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:2fa56d385b31462e26a605477c626023b16fb5a399b619ba0966d0d2b8d88eca", size = 1665731 },
{ url = "https://files.pythonhosted.org/packages/3d/23/5e519dd38ae42a75f8e6a952a3c5ea842804e2cb8c60a1d72807131d8aba/ruff-0.0.17-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:4f5694f9876cde21b95ad9c1691d0513617d2e88c0749f400b866505217fd5a0", size = 3202527 },
{ url = "https://files.pythonhosted.org/packages/9c/05/69872574ea3044cfbac4becef1c25c0b1227499c91c2232b83bad9bb104c/ruff-0.0.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d1349f4e5a4d53294fce92f42ecf881a73c180d71f14121461cac7d251abafd4", size = 1543942 },
{ url = "https://files.pythonhosted.org/packages/24/16/e5c7cb9b77d1f64d94d507d0c3d7ef491a10dc75825f458cb1bb05ae41cf/ruff-0.0.17-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f4e60d690898be3c3bf24387e67ac89496a97eb8814b8c0f0670ea43b6e83ee2", size = 1788392 },
{ url = "https://files.pythonhosted.org/packages/76/a4/dad616f277880963968d8a5e5719556f4ecadc7333c0da8bfb5060c5750c/ruff-0.0.17-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:522cacc6e550a7d59ead3b0ca65623582d51bfe32f6c780770ccf5d1bc3246cb", size = 1758753 },
{ url = "https://files.pythonhosted.org/packages/bc/44/08d68e219e2e7659991b467122f41326fde1e2e959107746ed7559f5e498/ruff-0.0.17-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e608511d0349a6211a0a123744cc0960f88539dbf62a0b8a77e3ee483237a6da", size = 1677835 },
{ url = "https://files.pythonhosted.org/packages/d6/7d/4d897311a299007b8542bfcd83dff9b09db6a7130f48ad3252e8ba3740b9/ruff-0.0.17-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:78f14cf1056ded6bda77162d7483e11a2f2a29763538422adaa5412654ff1a94", size = 1700213 },
{ url = "https://files.pythonhosted.org/packages/90/af/2c5dfa97f6994cf462e2b1934662f730608eec7ccfd6b992bab1af002ce9/ruff-0.0.17-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7cd7180893a3ed789c82838c8082fda074ba1cec46f383e255e696533f634be", size = 1894523 },
{ url = "https://files.pythonhosted.org/packages/40/e8/74569b7b05ece82a9e298eed7e01b2ae18205cee88de0283d0647370d120/ruff-0.0.17-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a90917b0e9d2f851294e445f0b898fa94051c4d9edcc1ab6d40bc1129fb9bb1e", size = 2119346 },
{ url = "https://files.pythonhosted.org/packages/7c/46/2cb84ccb0944c37208a2cea880ba8c6cb2a1752759771d385d6217de46b2/ruff-0.0.17-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9bb485eb3e0ba0c19ffd14b533659448c1c5e2958171e818fc1bc42c76f3d99a", size = 1679168 },
{ url = "https://files.pythonhosted.org/packages/47/65/cfd4e305851fdb94b7c8147d9d839ed83f9a93a941cc89d71e633a094642/ruff-0.0.17-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f3a399e4d211cdbec9229a89b1b7e77345eae881e9c3682fef7e90044de6a864", size = 1715985 },
{ url = "https://files.pythonhosted.org/packages/23/7a/96a4e5c51bab9538d9ee27a0eba95bd790d120bcc76a195417abbfb2cc81/ruff-0.0.17-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:80fe80b12ea042b9f0d8e80608db400e0c8e419d74a4dcf8b3b4fea9ec03362f", size = 1782188 },
{ url = "https://files.pythonhosted.org/packages/fb/3c/92267e9b9336bb1ba9ba7f5e3b9028e57ee4d77f32f728992c23da69ab53/ruff-0.0.17-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3b067ed2bd3fd0d4be591ea9afc796c07706291d78efe5a8eda4172c4d43525c", size = 1812203 },
{ url = "https://files.pythonhosted.org/packages/ca/d9/52cef313261a61931c4f3be228a774c5dad89f45042518d0f483864902ca/ruff-0.0.17-cp310-none-win32.whl", hash = "sha256:e3aba30e3aad77f260095ea1dbcf2834ab64d75133ff8d260625bb22887e2799", size = 1626413 },
{ url = "https://files.pythonhosted.org/packages/fe/d7/741e229667d0038d004783286ea6fa4554ff7a3b52bfe9b26b0380462e56/ruff-0.0.17-cp310-none-win_amd64.whl", hash = "sha256:a068bced7aff34173319931972fde3d7e68e3894915edac4e0f8c9b7bec7a226", size = 1654976 },
{ url = "https://files.pythonhosted.org/packages/20/50/470c8688e96fecac2096205cb45438676f6277b9c713a0aa1c4e633af503/ruff-0.0.17-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:b708d650c2ba25458d9e735c51981b687bf6747a4b28403eb7f6bae1aa93cfcf", size = 1665730 },
{ url = "https://files.pythonhosted.org/packages/13/13/f68c059cafb95122214c2defac7005c8a5ce278c7f1768d6849c167df07a/ruff-0.0.17-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:91019d271c223a4c562dbc2fbf2a2a96157524999a1173a4c858816d0c1bb9e7", size = 1543941 },
{ url = "https://files.pythonhosted.org/packages/70/e9/353db015927b6336bffbb7fe16af3bf76e860ab0fbddd66bf0a6995a2bfa/ruff-0.0.17-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:edbaf1c7f6b6483a206e549026f03ef7e04e480b5204437e21370de508dcf736", size = 1788393 },
{ url = "https://files.pythonhosted.org/packages/32/e5/2c41a62d58763948fa332619226cb11ded2ec161c678d70b7991b839ecf5/ruff-0.0.17-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:52110ed886d674497531d44ead5fa2fe99be930adfc9cec4b1f39409043efb13", size = 1758755 },
{ url = "https://files.pythonhosted.org/packages/3f/a9/f0cc3416b6f2e3755c8b37271112c609dfdf862b2825d86f4fc926eee037/ruff-0.0.17-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:febfbe4fa02e680b93b3fc2dcb0ffe5d601435c9719a51e35fe51fff1d0cc2c6", size = 1677834 },
{ url = "https://files.pythonhosted.org/packages/ed/54/ad7286c9e136260eaf8d6236202384dfe71a884a7b31288e129a43c33cb7/ruff-0.0.17-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6e72d62bd47f086d14ea5796ea18d1b98089a839dad693afa471a1fcdb6ae0d", size = 1700213 },
{ url = "https://files.pythonhosted.org/packages/bd/f7/12a3cfa5b88485ecdc724d0e915a8ebe4dfebfa94422855a445850564d01/ruff-0.0.17-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27d1bd7c71a90522e383853e411fcb402ccdcaf8778c6e0d54359153772f7870", size = 1894524 },
{ url = "https://files.pythonhosted.org/packages/e2/1a/1ab955830e84dcad9f606954d5b1094e186de144a2ad37247aa37145cbcd/ruff-0.0.17-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bed7beec36834e6fc40a03af92bc0da67599c70fbe24d8d820a1a2110b25eaf", size = 2119348 },
{ url = "https://files.pythonhosted.org/packages/3c/00/a67b2904d92034abd2f35a4d930eae08abf64106f64a90a67770395db8a7/ruff-0.0.17-cp37-none-win32.whl", hash = "sha256:c664d897e21b9aab2b20c764434653aa394e32c32d38e751fd4f381ace3a4e58", size = 1626410 },
{ url = "https://files.pythonhosted.org/packages/9c/a9/dacfff99065b0588ff9cf07411d7bbc8a167d1542d92a7e46f5825e262fa/ruff-0.0.17-cp37-none-win_amd64.whl", hash = "sha256:8c5900fd09baa2c7a4aecbdc754d3a43f2842906ee571812fa3eb28b8e7973a5", size = 1654975 },
{ url = "https://files.pythonhosted.org/packages/f3/a9/e5a048d209e246b3702a23540b1e09faa79b313b1b23f27993e33df3b01a/ruff-0.0.17-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db04c29f114c68f447aeec23f9be6118ea11a18a2444416cb4afb0fd918e50db", size = 1665728 },
{ url = "https://files.pythonhosted.org/packages/32/3d/13114ef5793e43fd5c8ee17047fbdc86e9eacb81f708c0e01ca9d5db773f/ruff-0.0.17-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:8527c8aacdd0e911c6f7d6f1b109a17e68300ac0f255ccce73e748bb8835c722", size = 3202527 },
{ url = "https://files.pythonhosted.org/packages/09/3f/657daee09a7412bda8af1963d6dd1b4adfdbc0e2d37c7261984fc4953e19/ruff-0.0.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:889aa57c771140ec6b17e15af8308e88e43a07b7369b97cb0426e1393e3d10b5", size = 1543942 },
{ url = "https://files.pythonhosted.org/packages/c1/6b/15e1c6744216236a3a08a8e40e318c329f86febdee7a1d62e3c449d75009/ruff-0.0.17-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7be31e77c1f98d9d02a7f6f2d4c05e8236cd9c82d0c3356b083162a011fc4d23", size = 1788391 },
{ url = "https://files.pythonhosted.org/packages/ff/fa/0cf5c91b2d415416ef0d534a8f19bdc07d11a52fd54aface524d006be1ce/ruff-0.0.17-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fe0c047d6fc74b55fcd1ac4f18110500ba871de2014039e4838ed7444d3459ff", size = 1758753 },
{ url = "https://files.pythonhosted.org/packages/8f/8b/ac8d7a1cce151c74fa9458f0297ca6fc287f9fe1a57fe710f99de970bfdb/ruff-0.0.17-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4933e275f0c3af3a78ecf1f9a4e12cc0426cd76398c7e904786f99c1ea8a0dd", size = 1677834 },
{ url = "https://files.pythonhosted.org/packages/9f/38/e51bff666939e38155ef12930e6c1d4970f5a28c7f1ff3867e97b00a4cd0/ruff-0.0.17-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a9f2fb8b3bac67d0fa9e50411ab424a863e4bc29a3336a046cc38f06d3ec17f", size = 1700211 },
{ url = "https://files.pythonhosted.org/packages/e2/96/949c17bed22816136d8e0456a242059da202e316ca4510715560c026d0d8/ruff-0.0.17-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d45439cbe73332a064306c39528d6bbf4856abb6d377ad8244b6e74a737daaa", size = 1894522 },
{ url = "https://files.pythonhosted.org/packages/89/61/cec1cd7093eea58e083060fc66c3fcf758737785a21b752fc568f57eabab/ruff-0.0.17-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7433f20d39a3819e322a3497dce037c6110f9588ec51ba136a938109dd31e71", size = 2119345 },
{ url = "https://files.pythonhosted.org/packages/a9/32/27bb21e91e0e7d6e76be6d5fce844c3ac52278ae49d8964eb356a024cf26/ruff-0.0.17-cp38-none-win32.whl", hash = "sha256:5f8f4f4310018807402c77d81ae020666b742d2173a73b147ce0d1e0a08f022f", size = 1626409 },
{ url = "https://files.pythonhosted.org/packages/d0/3f/cd9d27f2dbbd4975c7880d8284f893ea99cf73c646f453b9cef1b3924db5/ruff-0.0.17-cp38-none-win_amd64.whl", hash = "sha256:f0d0e8058d903b8fe899e04e1a957127ca97452553cf70ba9b4d1b277f034ad1", size = 1654979 },
{ url = "https://files.pythonhosted.org/packages/d3/4e/c854d4587c180936b33eac57344b11f52564878d2939fd6d9d842fa6e5ae/ruff-0.0.17-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:e6f24c3746d199bdb0d47149ed5353a41f0192630911396822fda0f8a6feaa0b", size = 1665735 },
{ url = "https://files.pythonhosted.org/packages/49/8b/8d2de1c9f7e2056bd4edaea393d6ad3494e75e99136cf127402afb4c496c/ruff-0.0.17-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:71cb773b19240f1be64c5f71aa2ad52b9f44fde1605c2c2f4089a5d61cb552d2", size = 3202528 },
{ url = "https://files.pythonhosted.org/packages/aa/9f/7235c23ed12dfd44823d3a127d5654cc2fbfbe3daa1aca00c43ad2ccd519/ruff-0.0.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bab716debcab46d9a1d7c8d00e2acf2b48ff28ec519b2b4c0eba873236782c21", size = 1543942 },
{ url = "https://files.pythonhosted.org/packages/2e/69/1d4a2819146458d478e8c8a194452da263ab60202083d8eba02307fde216/ruff-0.0.17-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ef57186452c0cfe71f09dac434bda0f1a804808f92221142adb9de28c3f422e6", size = 1788392 },
{ url = "https://files.pythonhosted.org/packages/c2/35/c3e2a3c3690e732860342b16e606795e977d87a90176ed1dae13c001ee86/ruff-0.0.17-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b00be13abaf107c30b8bcaf2ac89dc2b3abd164728c229339910211c05e8c43", size = 1758753 },
{ url = "https://files.pythonhosted.org/packages/ad/3d/7d71456c7e1c543ed223b400a2e962a344b5467a27e196c4dd6f2d1d30c3/ruff-0.0.17-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9c1ba3383995091ce6f3618e89f1bb0ed5caa730f64bb79a1c60184682dc5c3", size = 1677834 },
{ url = "https://files.pythonhosted.org/packages/f3/73/e87e31367fe7af0a027672e49703b61ea6566912d09e34a8e3e43bce3455/ruff-0.0.17-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:865114aa655dc54e5699f18b258a33a15a36da915de4936d7a458425e7f6351d", size = 1700211 },
{ url = "https://files.pythonhosted.org/packages/2b/27/7449e2a8bca1957c0e2d57316ca8fdcdf8d83277b23d50a33bdded703aa6/ruff-0.0.17-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a390b4657cc1eebd9bb0e581da768aa557b1157f5eeed6fc8b5b920991061b04", size = 1894520 },
{ url = "https://files.pythonhosted.org/packages/80/39/db6441f33216e25e5dba811e9d908cff898df7c9930006c735ba6578dd65/ruff-0.0.17-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d3fec0e9e8f285324127b97c55b525fe61e8e16e93e1a03d34aba80e3aff9f21", size = 2119346 },
{ url = "https://files.pythonhosted.org/packages/73/cb/d7ae9d2276f23f89642df0af808c85acc632aceca5d7039ae3afe4585afb/ruff-0.0.17-cp39-none-win32.whl", hash = "sha256:3f063c889d65d71fb189d6246ccd537c23c9d0f6e483c961ac0b5e8477d6e3ca", size = 1626409 },
{ url = "https://files.pythonhosted.org/packages/d8/a7/3ccc344a2b228a15b52217ed2a2982214ad77684745c3e09ace2b1f8e9bf/ruff-0.0.17-cp39-none-win_amd64.whl", hash = "sha256:4ba403b8a5f38753ed3ba7ca16fb7c67eaee96a4e4a9e9709f3ad8cd3909012c", size = 1654973 },
]
[[package]]
name = "ruff"
version = "0.9.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.8'",
"python_full_version == '3.7.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/1e/7f/60fda2eec81f23f8aa7cbbfdf6ec2ca11eb11c273827933fb2541c2ce9d8/ruff-0.9.3.tar.gz", hash = "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a", size = 3586740 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/77/4fb790596d5d52c87fd55b7160c557c400e90f6116a56d82d76e95d9374a/ruff-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624", size = 11656815 },
{ url = "https://files.pythonhosted.org/packages/a2/a8/3338ecb97573eafe74505f28431df3842c1933c5f8eae615427c1de32858/ruff-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c", size = 11594821 },
{ url = "https://files.pythonhosted.org/packages/8e/89/320223c3421962762531a6b2dd58579b858ca9916fb2674874df5e97d628/ruff-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4", size = 11040475 },
{ url = "https://files.pythonhosted.org/packages/b2/bd/1d775eac5e51409535804a3a888a9623e87a8f4b53e2491580858a083692/ruff-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439", size = 11856207 },
{ url = "https://files.pythonhosted.org/packages/7f/c6/3e14e09be29587393d188454064a4aa85174910d16644051a80444e4fd88/ruff-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5", size = 11420460 },
{ url = "https://files.pythonhosted.org/packages/ef/42/b7ca38ffd568ae9b128a2fa76353e9a9a3c80ef19746408d4ce99217ecc1/ruff-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4", size = 12605472 },
{ url = "https://files.pythonhosted.org/packages/a6/a1/3167023f23e3530fde899497ccfe239e4523854cb874458ac082992d206c/ruff-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1", size = 13243123 },
{ url = "https://files.pythonhosted.org/packages/d0/b4/3c600758e320f5bf7de16858502e849f4216cb0151f819fa0d1154874802/ruff-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5", size = 12744650 },
{ url = "https://files.pythonhosted.org/packages/be/38/266fbcbb3d0088862c9bafa8b1b99486691d2945a90b9a7316336a0d9a1b/ruff-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4", size = 14458585 },
{ url = "https://files.pythonhosted.org/packages/63/a6/47fd0e96990ee9b7a4abda62de26d291bd3f7647218d05b7d6d38af47c30/ruff-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6", size = 12419624 },
{ url = "https://files.pythonhosted.org/packages/84/5d/de0b7652e09f7dda49e1a3825a164a65f4998175b6486603c7601279baad/ruff-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730", size = 11843238 },
{ url = "https://files.pythonhosted.org/packages/9e/be/3f341ceb1c62b565ec1fb6fd2139cc40b60ae6eff4b6fb8f94b1bb37c7a9/ruff-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2", size = 11484012 },
{ url = "https://files.pythonhosted.org/packages/a3/c8/ff8acbd33addc7e797e702cf00bfde352ab469723720c5607b964491d5cf/ruff-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519", size = 12038494 },
{ url = "https://files.pythonhosted.org/packages/73/b1/8d9a2c0efbbabe848b55f877bc10c5001a37ab10aca13c711431673414e5/ruff-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b", size = 12473639 },
{ url = "https://files.pythonhosted.org/packages/cb/44/a673647105b1ba6da9824a928634fe23186ab19f9d526d7bdf278cd27bc3/ruff-0.9.3-py3-none-win32.whl", hash = "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c", size = 9834353 },
{ url = "https://files.pythonhosted.org/packages/c3/01/65cadb59bf8d4fbe33d1a750103e6883d9ef302f60c28b73b773092fbde5/ruff-0.9.3-py3-none-win_amd64.whl", hash = "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4", size = 10821444 },
{ url = "https://files.pythonhosted.org/packages/69/cb/b3fe58a136a27d981911cba2f18e4b29f15010623b79f0f2510fd0d31fd3/ruff-0.9.3-py3-none-win_arm64.whl", hash = "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b", size = 10038168 },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
]
[[package]]
name = "wcwidth"
version = "0.2.13"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
]