91 Commits

Author SHA1 Message Date
Storm Dragon 2e10c1c43b Version bump for new release. 2026-01-28 16:41:44 -05:00
Storm Dragon 1e67876883 RC1 for next release. 2026-01-11 23:19:12 -05:00
Storm Dragon 14cf6b6088 Clarify other OS than Linux support. 2026-01-11 23:15:28 -05:00
Storm Dragon 900a027643 Merge branch 'testing' 2026-01-10 23:12:13 -05:00
Storm Dragon 0e50175463 Removed sound for echo switching. 2026-01-10 23:11:53 -05:00
Storm Dragon 7283f04778 Merge branch 'testing' 2026-01-10 23:03:50 -05:00
Storm Dragon 8d3495f74f Character echo settings toggle key added. Keyboard files updated. 2026-01-10 23:03:22 -05:00
Storm Dragon a6cd47dafc latest code. 2026-01-10 21:55:25 -05:00
Storm Dragon 0bb2e52deb Fixed fluttery caps speech shifts. 2026-01-10 20:49:22 -05:00
Storm Dragon b8eb815a86 Merged latest from testing. 2026-01-08 16:16:20 -05:00
Storm Dragon beca468338 Finally! Fixed bug that was causing interruption when prompt comes back. 2026-01-08 12:37:55 -05:00
Storm Dragon a26fe26c8c Log level now set to 0 by default so there's no longer a ton of log files created that aren't normally needed. 2026-01-05 08:32:07 -05:00
Storm Dragon 508fd11610 Redesigned the flood protection for incoming text, should hopefully be much better. 2026-01-04 00:33:06 -05:00
Storm Dragon afe0e71a1d A tiny bug fix in prompt checker. 2026-01-04 00:05:52 -05:00
Storm Dragon 9e8d0b3869 Latest changes. 2025-12-30 04:10:52 -05:00
Storm Dragon d7f86ca0de Setting added to choose caps notification type, beep, pitch, both or none. 2025-12-30 04:09:25 -05:00
Storm Dragon 49a79d2722 Removed promoted text option. It wasn't all that useful, and the new speech restore performs the same thing more affectively. 2025-12-28 19:07:54 -05:00
Storm Dragon 4ab024d115 Mostly progress bar fixes. 2025-12-22 12:51:15 -05:00
Storm Dragon c4ae27a01b More progress bar tweaks. 2025-12-20 06:59:19 -05:00
Storm Dragon 668d39b444 Merged for wider testing. 2025-12-19 12:56:33 -05:00
Storm Dragon 8b25afbf5a More progress bar updates. 2025-12-19 12:55:17 -05:00
Storm Dragon efeb040f75 Spelling error and case fixes. Everything seems to work so far. 2025-12-19 03:46:18 -05:00
Storm Dragon 7a17b36d50 More update work on readme and settings. 2025-12-19 03:07:46 -05:00
Storm Dragon 047a31b4bf Latest changes. 2025-12-19 00:11:04 -05:00
Storm Dragon 096aef9f08 Progress bar detection updates. 2025-12-14 19:05:18 -05:00
Storm Dragon 76472b83b5 Fixed a comment in settings file. 2025-12-10 20:20:56 -05:00
Storm Dragon a52bf624ec Merged changes from testing. WARNING: breaking changes, you will need to update or regenerate your settings file. Use the example provided in config/settings/settings.conf or on arch use the .pacnew as a guide. 2025-12-10 20:12:07 -05:00
Storm Dragon f4e28a246f Second pass on updating settings, missed a few the first go round. 2025-12-10 20:08:01 -05:00
Storm Dragon 560ceb26c9 Major: Convert all settings from camelCase to snake_case for PEP8 compliance (WARNING! BREAKING CHANGES)
Breaking change for v3.0 - users must update their settings.conf file.

  - Converted 45 settings across all sections
  - Updated 524 Python files, tests, and documentation
  - All tests passing, zero regressions
  - Moving forward the philosophy is clean code over backward compatibility
  - Next stable release will be considered a major release
2025-12-10 19:39:45 -05:00
Storm Dragon 61868c94e5 Experimental fix to hopefully let users who rebind their capslock key keep those settings while using Fenrir. 2025-12-10 08:22:17 -05:00
Storm Dragon f462ca7990 Merge branch 'testing' minor settings file update. 2025-12-03 16:25:47 -05:00
Storm Dragon f0bbcb8a38 Updated settings file to document that capslock as fenrir key and echo mode 2 are incompatible. 2025-12-03 16:25:22 -05:00
Storm Dragon aed627ec2a Discovered through much pain that echo mode 2 and capslock as fenrir key are incompatible. Documented in settings file. 2025-12-03 16:18:02 -05:00
Storm Dragon e62b887e9c Some socket improvements for remote manager I thought should make it into this release. 2025-12-03 12:20:14 -05:00
Storm Dragon bf0d134187 Tests added, see the documentation in the tests directory for details. Improved the socket code. 2025-12-03 02:51:49 -05:00
Storm Dragon c66a9ba9c2 Problems with voice selection fixed.: 2025-12-02 18:38:06 -05:00
Storm Dragon 2092a3e257 Fixed voice selection. 2025-12-02 18:36:46 -05:00
Storm Dragon d46d8de3ee Updated sound driver to gstreamer by default. 2025-12-02 16:25:25 -05:00
Storm Dragon 75a8447759 One more feature addition before hopefully releasing the new version. 2025-12-02 16:13:15 -05:00
Storm Dragon 1650eec768 Add ability to switch speech-dispatcher module and voice to the speeach keys. 2025-12-02 16:11:47 -05:00
Storm Dragon 5bb786ef4c Bug fix in vmenu for keyboard layouts. 2025-11-27 22:44:51 -05:00
Storm Dragon 7f7faa17d3 keyboard layout fixed in vmenu. 2025-11-27 22:42:36 -05:00
Storm Dragon 2766f70c5d Some cleanup and minor fixes in docs. 2025-11-24 09:07:45 -05:00
Storm Dragon 8d781643bc Remove group and user comments from the Arch service file, not being used, so just extra stuff. 2025-11-24 08:49:31 -05:00
Storm Dragon c184cf023a Code cleanups, fixes to systemd files, url corrections. 2025-11-24 08:44:49 -05:00
Storm Dragon 841c221c7b Preparing for new tagged release. 2025-11-23 18:51:02 -05:00
Storm Dragon 87553bdc38 more fixes for the pickle error. 2025-11-23 18:37:21 -05:00
Storm Dragon 77a3aae5a4 Attempt to fix cannot pickle 'TextIOWrapper' instances error for some distros. 2025-11-23 18:29:38 -05:00
Storm Dragon aabc202d83 Latest version of configure_pipewire.sh tested and appears to work. 2025-10-17 22:19:58 -04:00
Storm Dragon 2f3a114790 Pipewire configuration tool updated. In a bout of pure insanity I tested this on my production system and it worked without a hitch, so should be good to go. 2025-10-17 22:19:30 -04:00
Storm Dragon c797974560 Pre tag release. If no problems reported this will become the new stable release. 2025-10-17 21:27:36 -04:00
Storm Dragon af4740d5ad Various minor fixes in preparation for new release. 2025-10-17 21:26:13 -04:00
Storm Dragon 5ef5faaebe Progressbar tweaks. 2025-10-17 21:03:12 -04:00
Storm Dragon 7041d2567a Progress bar updates. 2025-09-26 18:07:35 -04:00
Storm Dragon 2c38bcf5f4 Another attempt at fixing external numpad detection. I *think* some of them send numlock state with every event. If that's the case, this should fix it. 2025-09-13 14:11:27 -04:00
Storm Dragon 96cdda99c4 Separate menu entries for some settings, improves usability. Fixed device detection for devices that do not contain what they do in their name, e.g. numpads that are not listed as numpads. This hopefully fixes a bug with some external numpads. 2025-09-12 12:09:00 -04:00
Storm Dragon 0658d37ae8 I don't wanna say this too loud, but I think tab completion is much more reliable now. No more not reading when you press tab and something appears. 2025-08-31 20:45:32 -04:00
Storm Dragon a6bb3e1301 A bit of code cleanup. Nothing should be changed at all functionally. 2025-08-31 20:30:06 -04:00
Storm Dragon 5ff653bd00 Progress bar and commit validator updates. 2025-08-31 14:54:07 -04:00
Storm Dragon 356f4b01c1 Got the version file wrong again. Need to be more careful. 2025-08-31 14:44:46 -04:00
Storm Dragon c7ad4d9200 merged to master. 2025-08-31 14:39:26 -04:00
Storm Dragon 90ffc2fc08 removing keyboard layouts that are no longer used 2025-08-23 18:29:42 -04:00
Storm Dragon b635f7538b Latest changes merged. Minor fixes to progress bar detection. Fixed my old habbit of camel case variable names. :) 2025-08-23 12:20:05 -04:00
Storm Dragon e255651c28 Merged testing. 2025-08-22 00:29:02 -04:00
Storm Dragon 98b9c56af7 Read all code added. It's definiately a work in progress and does not function currently. 2025-08-04 14:41:14 -04:00
Storm Dragon 8bada48a09 Fixed errors in README. Moved the audio configuration script stuff nearer the top. 2025-08-04 14:36:43 -04:00
Storm Dragon e9a0101fe7 Merged README.md 2025-08-04 14:26:55 -04:00
Storm Dragon 914535d12b A few bug fixes, better checking in place to make sure syntax and other errors do not make it to commits. 2025-07-24 18:34:12 -04:00
Storm Dragon 2dd732dc9d Emojis added, improvements to pyt mode. 2025-07-19 16:50:53 -04:00
Storm Dragon e177c7f486 Final merge for version 2025.07.16. If no problems will tag later today. 2025-07-16 12:33:21 -04:00
Storm Dragon b9abf02b12 A few minor tweaks in preparation for release. 2025-07-13 15:31:57 -04:00
Storm Dragon fe5e2c065e Merge branch 'testing' moving closer to tagged release. 2025-07-09 18:32:15 -04:00
Storm Dragon ef3ebee10c Preparing for new tagged version. Please watch for bugs. 2025-07-09 09:33:19 -04:00
Storm Dragon 271c4fc18f Table mode fixes and improvements to application detection. 2025-07-08 14:41:43 -04:00
Storm Dragon ea56b90b48 Oops, getting used to this pep8 thing myself. Fixed codeName to code_name. 2025-07-06 18:51:43 -04:00
Storm Dragon 1268d989b7 Merge after mostly converting to pep8 compliance. 2025-07-06 18:34:28 -04:00
Storm Dragon 23c3ad20a1 More code optmizations. Removed fenrir+pk_plus. The functionality of bringing back speech has been added to the temperary speech interruption key kp_enter. 2025-06-30 22:25:01 -04:00
Storm Dragon 8af1cca879 Latest changes and bug fixes. 2025-06-27 21:18:27 -04:00
Storm Dragon a394ea0222 Code cleanup and bug fixes. 2025-06-20 02:19:57 -04:00
Storm Dragon efb308ac72 latest testing code merged. Nothing major reported from testing branch, so if we get no reports, this will become the next stable release. I'm waiting a bit to tag because major new features introduced. 2025-06-17 00:53:28 -04:00
Storm Dragon f6be6c54fb Bug fixes mostlry, tested and seems to be working better. 2025-06-13 23:21:46 -04:00
Storm Dragon f18c31df6c Merge branch 'testing' bug fix for remoteDriver 2025-06-07 18:24:44 -04:00
Storm Dragon 3dca3e5b23 Merged for new release. 2025-06-07 12:23:53 -04:00
Storm Dragon 1b9a9a90b1 Fixed version conflict. 2025-06-06 20:35:07 -04:00
Storm Dragon 4c8c8d896d Fixed version conflict. 2025-06-05 16:05:11 -04:00
Storm Dragon 4672592dba Latest merge from testing. 2025-04-28 15:41:14 -04:00
Storm Dragon 7a12992b88 latest release. 2025-04-17 00:36:26 -04:00
Storm Dragon 7a87fb51bb Fixed version for master branch. 2025-04-14 20:04:14 -04:00
Storm Dragon 2cc2fda28c Actually fix the version file this time. 2025-03-02 17:59:20 -05:00
Storm Dragon c99d0f6ee1 Fixed version.py. 2025-03-02 17:44:32 -05:00
Storm Dragon 5b642cd9e2 Fixed error in settings file. 2025-02-26 17:41:01 -05:00
225 changed files with 4938 additions and 1734 deletions
+41 -38
View File
@@ -1,7 +1,6 @@
# Fenrir # Fenrir
A modern, modular, flexible and fast console screen reader. A modern, modular, flexible and fast console screen reader for Linux.
It should run on any operating system. If you want to help, or write drivers to make it work on other systems, just let me know.
This software is licensed under the LGPL v3. This software is licensed under the LGPL v3.
**Current maintainer:** Storm Dragon **Current maintainer:** Storm Dragon
@@ -24,12 +23,16 @@ This software is licensed under the LGPL v3.
- **Tutorial Mode**: Built-in help system for learning keyboard shortcuts - **Tutorial Mode**: Built-in help system for learning keyboard shortcuts
## OS Requirements ## Platform Support
- Linux (ptyDriver, vcsaDriver, evdevDriver) - Primary platform with full support Fenrir is a Linux screen reader. Linux is the only officially supported platform.
- macOS (ptyDriver) - Limited support
- BSD (ptyDriver) - Limited support **Other platforms (macOS, BSD, Windows):** Pull requests adding support for other operating systems may be accepted provided they do not break Linux functionality. However, no special care will be taken to preserve functionality on secondary platforms. If changes to Fenrir break support on a non-Linux OS, it is the responsibility of third-party contributors to submit fixes.
- Windows (ptyDriver) - Limited support
- Linux (ptyDriver, vcsaDriver, evdevDriver) - Full support
- macOS (ptyDriver) - Community-maintained, no guarantees
- BSD (ptyDriver) - Community-maintained, no guarantees
- Windows (ptyDriver) - Community-maintained, no guarantees
## Core Requirements ## Core Requirements
@@ -222,7 +225,7 @@ Fenrir supports two main keyboard layouts:
Configure in `/etc/fenrir/settings/settings.conf`: Configure in `/etc/fenrir/settings/settings.conf`:
```ini ```ini
[keyboard] [keyboard]
keyboardLayout=desktop # or 'laptop' keyboard_layout=desktop # or 'laptop'
``` ```
### First Time Setup ### First Time Setup
@@ -255,9 +258,9 @@ Enable remote control in `/etc/fenrir/settings/settings.conf`:
enable=True enable=True
driver=unixDriver # or tcpDriver driver=unixDriver # or tcpDriver
port=22447 # for TCP driver port=22447 # for TCP driver
socketFile= # custom socket path (optional) socket_file= # custom socket path (optional)
enableSettingsRemote=True # allow settings changes enable_settings_remote=True # allow settings changes
enableCommandRemote=True # allow command execution enable_command_remote=True # allow command execution
``` ```
### Remote Drivers ### Remote Drivers
@@ -299,8 +302,8 @@ echo "setting set speech#pitch=0.6" | socat - UNIX-CLIENT:/tmp/fenrirscreenreade
echo "setting set speech#volume=0.9" | 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) # Change punctuation level (none/some/most/all)
echo "setting set general#punctuationLevel=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting set general#punctuation_level=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set general#punctuationLevel=none" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting set general#punctuation_level=none" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
# Voice and TTS engine control # Voice and TTS engine control
echo "setting set speech#voice=en-us+f3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting set speech#voice=en-us+f3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
@@ -311,14 +314,14 @@ echo "setting set sound#enabled=False" | socat - UNIX-CLIENT:/tmp/fenrirscreenre
echo "setting set sound#volume=0.5" | 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 # Keyboard and input settings
echo "setting set keyboard#charEchoMode=1" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting set keyboard#char_echo_mode=1" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set keyboard#wordEcho=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting set keyboard#word_echo=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
# Screen control (ignore specific TTYs) # Screen control (ignore specific TTYs)
echo "setting set screen#ignoreScreen=1,2,3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting set screen#ignore_screen=1,2,3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
# Multiple settings at once # 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 echo "setting set speech#rate=0.8;sound#volume=0.7;general#punctuation_level=most" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
# Reset all settings to defaults # Reset all settings to defaults
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
@@ -421,7 +424,7 @@ setting <action> [parameters]
- `speech#voice=voice_name` - Voice selection (e.g., "en-us+f3") - `speech#voice=voice_name` - Voice selection (e.g., "en-us+f3")
- `speech#module=module_name` - TTS module (e.g., "espeak-ng") - `speech#module=module_name` - TTS module (e.g., "espeak-ng")
- `speech#driver=driver_name` - Speech driver (speechdDriver/genericDriver) - `speech#driver=driver_name` - Speech driver (speechdDriver/genericDriver)
- `speech#autoReadIncoming=True/False` - Auto-read new text - `speech#auto_read_incoming=True/False` - Auto-read new text
*Sound Settings:* *Sound Settings:*
- `sound#enabled=True/False` - Enable/disable sound - `sound#enabled=True/False` - Enable/disable sound
@@ -430,32 +433,32 @@ setting <action> [parameters]
- `sound#theme=theme_name` - Sound theme - `sound#theme=theme_name` - Sound theme
*General Settings:* *General Settings:*
- `general#punctuationLevel=none/some/most/all` - Punctuation verbosity - `general#punctuation_level=none/some/most/all` - Punctuation verbosity
- `general#debugLevel=0-3` - Debug level - `general#debug_level=0-3` - Debug level
- `general#emoticons=True/False` - Enable emoticon replacement - `general#emoticons=True/False` - Enable emoticon replacement
- `general#autoSpellCheck=True/False` - Automatic spell checking - `general#auto_spell_check=True/False` - Automatic spell checking
*Focus Settings:* *Focus Settings:*
- `focus#cursor=True/False` - Follow text cursor - `focus#cursor=True/False` - Follow text cursor
- `focus#highlight=True/False` - Follow text highlighting - `focus#highlight=True/False` - Follow text highlighting
*Keyboard Settings:* *Keyboard Settings:*
- `keyboard#charEchoMode=0-2` - Character echo (0=none, 1=always, 2=capslock only) - `keyboard#char_echo_mode=0-2` - Character echo (0=none, 1=always, 2=capslock only)
- `keyboard#wordEcho=True/False` - Echo complete words - `keyboard#word_echo=True/False` - Echo complete words
- `keyboard#charDeleteEcho=True/False` - Echo deleted characters - `keyboard#char_delete_echo=True/False` - Echo deleted characters
- `keyboard#interruptOnKeyPress=True/False` - Interrupt speech on key press - `keyboard#interrupt_on_key_press=True/False` - Interrupt speech on key press
*Screen Settings:* *Screen Settings:*
- `screen#ignoreScreen=1,2,3` - TTY screens to ignore - `screen#ignore_screen=1,2,3` - TTY screens to ignore
- `screen#autodetectIgnoreScreen=True/False` - Auto-detect screens to ignore - `screen#autodetect_ignore_screen=True/False` - Auto-detect screens to ignore
- `screen#screenUpdateDelay=float` - Screen update delay - `screen#screen_update_delay=float` - Screen update delay
*Time Settings:* *Time Settings:*
- `time#enabled=True/False` - Enable time announcements - `time#enabled=True/False` - Enable time announcements
- `time#presentTime=True/False` - Announce time - `time#present_time=True/False` - Announce time
- `time#presentDate=True/False` - Announce date changes - `time#present_date=True/False` - Announce date changes
- `time#delaySec=seconds` - Announcement interval - `time#delay_sec=seconds` - Announcement interval
- `time#onMinutes=00,30` - Specific minutes to announce - `time#on_minutes=00,30` - Specific minutes to announce
## Table Navigation ## Table Navigation
@@ -623,7 +626,7 @@ rsync -av source/ destination/ # File synchronization
### Customization ### Customization
Progress monitoring can be configured through settings: Progress monitoring can be configured through settings:
- **Default enabled**: Set `progressMonitoring=True` in sound section - **Default enabled**: Set `progress_monitoring=True` in sound section
- **Sound integration**: Works with all sound drivers (sox, gstreamer) - **Sound integration**: Works with all sound drivers (sox, gstreamer)
- **Remote control**: Enable/disable through remote commands - **Remote control**: Enable/disable through remote commands
@@ -677,8 +680,8 @@ send_fenrir_command("setting set speech#rate=0.9")
- TCP driver binds only to localhost (127.0.0.1) - TCP driver binds only to localhost (127.0.0.1)
- Socket file permissions are set to write-only (0o222) - Socket file permissions are set to write-only (0o222)
- Commands are processed with Fenrir's privileges - Commands are processed with Fenrir's privileges
- Settings changes can be disabled via `enableSettingsRemote=False` - Settings changes can be disabled via `enable_settings_remote=False`
- Command execution can be disabled via `enableCommandRemote=False` - Command execution can be disabled via `enable_command_remote=False`
### Troubleshooting ### Troubleshooting
@@ -688,7 +691,7 @@ send_fenrir_command("setting set speech#rate=0.9")
- Ensure remote driver is enabled in settings - Ensure remote driver is enabled in settings
**Commands not working:** **Commands not working:**
- Verify `enableCommandRemote=True` in settings - Verify `enable_command_remote=True` in settings
- Check Fenrir debug logs: `/var/log/fenrir.log` - Check Fenrir debug logs: `/var/log/fenrir.log`
- Test with simple command: `echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock` - Test with simple command: `echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock`
@@ -710,7 +713,7 @@ fenrir [OPTIONS]
- `-p, --print` - Print debug messages to screen - `-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-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) - `-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 - `-F, --force-all-screens` - Force Fenrir to respond on all screens, ignoring ignore_screen setting
- `-i, -I, --ignore-screen SCREEN` - Ignore specific screen(s). Can be used multiple times. Combines with existing ignore settings. - `-i, -I, --ignore-screen SCREEN` - Ignore specific screen(s). Can be used multiple times. Combines with existing ignore settings.
### Examples: ### Examples:
@@ -724,7 +727,7 @@ sudo fenrir -e
# Override settings via command line # Override settings via command line
sudo fenrir -o "speech#rate=0.8;sound#volume=0.5" sudo fenrir -o "speech#rate=0.8;sound#volume=0.5"
# Force Fenrir to work on all screens (ignore ignoreScreen setting) # Force Fenrir to work on all screens (ignore ignore_screen setting)
sudo fenrir -F sudo fenrir -F
# Ignore specific screens # Ignore specific screens
-3
View File
@@ -1,3 +0,0 @@
V2.0
Cleanup folders and config files.
+1 -3
View File
@@ -4,12 +4,10 @@ Wants=systemd-udev-settle.service
After=systemd-udev-settle.service getty.target After=systemd-udev-settle.service getty.target
[Service] [Service]
Type=forking Type=forking
PIDFile=/var/run/fenrir.pid PIDFile=/run/fenrir.pid
ExecStart=/usr/bin/fenrir ExecStart=/usr/bin/fenrir
ExecReload=/usr/bin/kill -HUP $MAINPID ExecReload=/usr/bin/kill -HUP $MAINPID
Restart=always Restart=always
#Group=fenrirscreenreader
#User=fenrirscreenreader
[Install] [Install]
WantedBy=getty.target WantedBy=getty.target
+1 -1
View File
@@ -4,7 +4,7 @@ Wants=systemd-udev-settle.service
After=systemd-udev-settle.service sound.target After=systemd-udev-settle.service sound.target
[Service] [Service]
Type=forking Type=forking
PIDFile=/var/run/fenrir.pid PIDFile=/run/fenrir.pid
ExecStart=/usr/local/bin/fenrir ExecStart=/usr/local/bin/fenrir
ExecReload=/usr/bin/kill -HUP $MAINPID ExecReload=/usr/bin/kill -HUP $MAINPID
Restart=always Restart=always
+2 -2
View File
@@ -1,5 +1,5 @@
Please report Bugs and feature requests to: Please report bugs and feature requests to:
https://github.com/chrys87/fenrir/issues https://git.stormux.org/storm/fenrir/issues
For bugs, please provide a debug file that shows the issue. For bugs, please provide a debug file that shows the issue.
How to create a debug file: How to create a debug file:
+4 -2
View File
@@ -83,6 +83,7 @@ KEY_FENRIR,KEY_CTRL,KEY_P=toggle_punctuation_level
KEY_FENRIR,KEY_RIGHTBRACE=toggle_auto_spell_check KEY_FENRIR,KEY_RIGHTBRACE=toggle_auto_spell_check
KEY_FENRIR,KEY_BACKSLASH=toggle_output KEY_FENRIR,KEY_BACKSLASH=toggle_output
KEY_FENRIR,KEY_CTRL,KEY_E=toggle_emoticons KEY_FENRIR,KEY_CTRL,KEY_E=toggle_emoticons
KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_E=cycle_key_echo
key_FENRIR,KEY_KPENTER=toggle_auto_read key_FENRIR,KEY_KPENTER=toggle_auto_read
KEY_FENRIR,KEY_CTRL,KEY_T=toggle_auto_time KEY_FENRIR,KEY_CTRL,KEY_T=toggle_auto_time
KEY_FENRIR,KEY_KPASTERISK=toggle_highlight_tracking KEY_FENRIR,KEY_KPASTERISK=toggle_highlight_tracking
@@ -126,7 +127,8 @@ KEY_FENRIR,KEY_CTRL,KEY_S=save_settings
# linux specific # linux specific
KEY_FENRIR,KEY_F7=import_clipboard_from_x KEY_FENRIR,KEY_F7=import_clipboard_from_x
KEY_FENRIR,KEY_F8=export_clipboard_to_x KEY_FENRIR,KEY_F8=export_clipboard_to_x
KEY_FENRIR,KEY_CTRL,KEY_UP=inc_alsa_volume # Read-all functionality
KEY_FENRIR,KEY_CTRL,KEY_DOWN=dec_alsa_volume KEY_FENRIR,KEY_CTRL,KEY_DOWN=read_all_by_line
KEY_FENRIR,KEY_CTRL,KEY_PAGEDOWN=read_all_by_page
KEY_FENRIR,KEY_SHIFT,KEY_V=announce_fenrir_version KEY_FENRIR,KEY_SHIFT,KEY_V=announce_fenrir_version
KEY_FENRIR,KEY_LEFTCTRL,KEY_F4=cycle_keyboard_layout KEY_FENRIR,KEY_LEFTCTRL,KEY_F4=cycle_keyboard_layout
+4 -2
View File
@@ -81,6 +81,7 @@ KEY_FENRIR,KEY_SHIFT,KEY_CTRL,KEY_P=toggle_punctuation_level
KEY_FENRIR,KEY_RIGHTBRACE=toggle_auto_spell_check KEY_FENRIR,KEY_RIGHTBRACE=toggle_auto_spell_check
KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_ENTER=toggle_output KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_ENTER=toggle_output
KEY_FENRIR,KEY_SHIFT,KEY_E=toggle_emoticons KEY_FENRIR,KEY_SHIFT,KEY_E=toggle_emoticons
KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_E=cycle_key_echo
KEY_FENRIR,KEY_CTRL,KEY_T=toggle_auto_time KEY_FENRIR,KEY_CTRL,KEY_T=toggle_auto_time
KEY_FENRIR,KEY_Y=toggle_highlight_tracking KEY_FENRIR,KEY_Y=toggle_highlight_tracking
#=toggle_barrier #=toggle_barrier
@@ -126,7 +127,8 @@ KEY_FENRIR,KEY_CTRL,KEY_S=save_settings
# linux specific # linux specific
KEY_FENRIR,KEY_F7=import_clipboard_from_x KEY_FENRIR,KEY_F7=import_clipboard_from_x
KEY_FENRIR,KEY_F8=export_clipboard_to_x KEY_FENRIR,KEY_F8=export_clipboard_to_x
KEY_FENRIR,KEY_CTRL,KEY_UP=inc_alsa_volume # Read-all functionality
KEY_FENRIR,KEY_CTRL,KEY_DOWN=dec_alsa_volume KEY_FENRIR,KEY_CTRL,KEY_DOWN=read_all_by_line
KEY_FENRIR,KEY_CTRL,KEY_PAGEDOWN=read_all_by_page
KEY_FENRIR,KEY_SHIFT,KEY_V=announce_fenrir_version KEY_FENRIR,KEY_SHIFT,KEY_V=announce_fenrir_version
KEY_FENRIR,KEY_LEFTCTRL,KEY_F4=cycle_keyboard_layout KEY_FENRIR,KEY_LEFTCTRL,KEY_F4=cycle_keyboard_layout
+175 -109
View File
@@ -3,14 +3,13 @@
enabled=True enabled=True
# Select the driver used to play sounds, choices are genericDriver and gstreamerDriver. # Select the driver used to play sounds, choices are genericDriver and gstreamerDriver.
# Sox is the default. # Generic driver uses fewer dependencies but spawns a process for each sound played including progress bar beeps
#driver=gstreamerDriver # Gstreamer is the default.
driver=genericDriver driver=gstreamerDriver
#driver=genericDriver
# Sound themes. These are the pack of sounds used for sound alerts. # Sound themes. These are the pack of sounds used for sound alerts.
# Sound packs may be located at /usr/share/sounds # Sound packs may be located at /usr/share/sounds
# For system wide availability, or ~/.local/share/fenrirscreenreader/sounds
# For the current user.
theme=default theme=default
# Sound volume controls how loud the sounds for your selected soundpack are. # Sound volume controls how loud the sounds for your selected soundpack are.
@@ -24,12 +23,12 @@ volume=0.7
# fenrirFrequence = the frequency to play # fenrirFrequence = the frequency to play
# fenrirDuration = the duration of the frequency # fenrirDuration = the duration of the frequency
# the following command is used to play a soundfile # the following command is used to play a soundfile
genericPlayFileCommand=play -q -v fenrirVolume fenrirSoundFile generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile
#the following command is used to generate a frequency beep #the following command is used to generate a frequency beep
genericFrequencyCommand=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
# Enable progress bar monitoring with ascending tones by default # Enable progress bar monitoring with ascending tones by default
progressMonitoring=True progress_monitoring=True
[speech] [speech]
# Turn speech on or off: # Turn speech on or off:
@@ -45,7 +44,13 @@ rate=0.5
# Pitch controls the pitch of the voice, select from 0, lowest, to 1.0, highest. # Pitch controls the pitch of the voice, select from 0, lowest, to 1.0, highest.
pitch=0.5 pitch=0.5
# Pitch for capital letters # Pitch for capital letters
capitalPitch=0.9 capital_pitch=0.9
# How to indicate capital letters:
# pitch = change speech pitch (uses capital_pitch value)
# beep = play Caps.wav sound icon
# both = play beep AND change pitch
# none = no special indication
capital_indicator=pitch
# Volume controls the loudness of the voice, select from 0, quietest, to 1.0, loudest. # Volume controls the loudness of the voice, select from 0, quietest, to 1.0, loudest.
volume=1.0 volume=1.0
@@ -65,10 +70,26 @@ volume=1.0
#language=en #language=en
# Read new text as it happens? # Read new text as it happens?
autoReadIncoming=True auto_read_incoming=True
# Speak individual numbers instead of whole string. # Speak individual numbers instead of whole string.
readNumbersAsDigits = False read_numbers_as_digits = False
# Flood control: batch rapid updates instead of speaking each one
# Number of updates within rapid_update_window to trigger batching
rapid_update_threshold=5
# Time window (seconds) for detecting rapid updates
rapid_update_window=0.3
# How often to speak batched content (seconds)
batch_flush_interval=0.5
# Maximum lines to keep when batching (keeps newest, drops oldest)
max_batch_lines=100
# Only enable flood control if this many new lines appear in the window
flood_line_threshold=500
# genericSpeechCommand is the command that is executed for talking # genericSpeechCommand is the command that is executed for talking
# the following variables are replaced with values # the following variables are replaced with values
@@ -81,105 +102,135 @@ readNumbersAsDigits = False
# fenrirVolume = is replaced with the current volume # fenrirVolume = is replaced with the current volume
# fenrirPitch = is replaced with the current pitch # fenrirPitch = is replaced with the current pitch
# fenrirRate = is replaced with the current speed (speech rate) # fenrirRate = is replaced with the current speed (speech rate)
genericSpeechCommand=espeak-ng -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice -- "fenrirText" generic_speech_command=espeak-ng -a fenrir_volume -s fenrir_rate -p fenrir_pitch -v fenrir_voice -- "fenrir_text"
# min and max values of the TTS system that is used in genericSpeechCommand # min and max values of the TTS system that is used in generic_speech_command
fenrirMinVolume=0 fenrir_min_volume=0
fenrirMaxVolume=200 fenrir_max_volume=200
fenrirMinPitch=0 fenrir_min_pitch=0
fenrirMaxPitch=99 fenrir_max_pitch=99
fenrirMinRate=80 fenrir_min_rate=80
fenrirMaxRate=450 fenrir_max_rate=450
[screen] [screen]
# Screen driver: vcsaDriver (Linux TTY/virtual console), ptyDriver (terminal emulation)
driver=vcsaDriver driver=vcsaDriver
# Text encoding: auto (auto-detect), utf-8, latin1, etc.
encoding=auto encoding=auto
screenUpdateDelay=0.05 # Delay in seconds between screen updates (lower = more responsive, higher = less CPU)
ignoreScreen=7 # Recommended: 0.05 for most systems
autodetectIgnoreScreen=True screen_update_delay=0.05
# TTY number to completely ignore (e.g., 7 for X11/graphical login screen)
ignore_screen=7
# Automatically detect and ignore graphical TTYs (X11, Wayland sessions)
autodetect_ignore_screen=True
[keyboard] [keyboard]
driver=evdevDriver driver=evdevDriver
# filter input devices NOMICE, ALL or a DEVICE NAME # filter input devices NOMICE, ALL or a DEVICE NAME
device=ALL device=ALL
# gives Fenrir exclusive access to the keyboard and lets it control keystrokes. # gives Fenrir exclusive access to the keyboard and lets it control keystrokes.
grabDevices=True grab_devices=True
ignoreShortcuts=False # Ignore shortcut bindings and pass all keys through without processing Fenrir commands.
# When True, Fenrir will only monitor screen content without intercepting keyboard input.
# the current shortcut layout located in /etc/fenrirscreenreader/keyboard # the current shortcut layout located in /etc/fenrirscreenreader/keyboard
keyboardLayout=desktop ignore_shortcuts=False
keyboard_layout=desktop
# echo chars while typing. # echo chars while typing.
# 0 = None # 0 = None
# 1 = always # 1 = always
# 2 = only while capslock # 2 = only while capslock (not compatible with capslock as fenrir key)
charEchoMode=1 char_echo_mode=1
# echo deleted chars # echo deleted chars
charDeleteEcho=True char_delete_echo=True
# echo word after pressing space # echo word after pressing space
wordEcho=False word_echo=False
# interrupt speech on any keypress # interrupt speech on any keypress
interruptOnKeyPress=True interrupt_on_key_press=True
# you can filter the keys on that the speech should interrupt (empty = all keys, otherwhise the given keys) # you can filter the keys on that the speech should interrupt (empty = all keys, otherwhise the given keys)
interruptOnKeyPressFilter= interrupt_on_key_press_filter=
# timeout for double tap in sec # timeout for double tap in sec
doubleTapTimeout=0.2 double_tap_timeout=0.2
[general] [general]
# Debug levels: 0=DEACTIVE, 1=ERROR, 2=WARNING, 3=INFO (most verbose) # Debug levels: 0=NONE, 1=ERROR, 2=WARNING, 3=INFO (most verbose)
# For production use, WARNING (2) provides good balance of useful info without spam # For production use, WARNING (2) provides good balance of useful info without spam
debugLevel=2 # The default is 0, no logging.
debug_level=0
# debugMode sets where the debug output should send to: # debugMode sets where the debug output should send to:
# debugMode=File writes to debugFile (Default:/tmp/fenrir-PID.log) # debugMode=File writes to debug_file (Default:/tmp/fenrir-PID.log)
# debugMode=Print just prints on the screen # debugMode=Print just prints on the screen
debugMode=File debug_mode=File
debugFile= debug_file=
punctuationProfile=default # Punctuation settings control how punctuation is spoken during text review.
punctuationLevel=some # Profile selects a punctuation definition file from config/punctuation/ (e.g., default.conf)
respectPunctuationPause=True punctuation_profile=default
punctuation_level=some
respect_punctuation_pause=True
# Replace undefined punctuation with spaces instead of removing them # Replace undefined punctuation with spaces instead of removing them
# This improves readability of text with punctuation like [X]mute, IP addresses, etc. # This improves readability of text with punctuation like [X]mute, IP addresses, etc.
replaceUndefinedPunctuationWithSpace=True replace_undefined_punctuation_with_space=True
newLinePause=True # Pause speech briefly at newline characters for better readability
numberOfClipboards=50 new_line_pause=True
number_of_clipboards=50
# used path for "export_clipboard_to_file" # used path for "export_clipboard_to_file"
# $user is replaced by username # $user is replaced by username
#clipboardExportPath=/home/$user/fenrirClipboard #clipboardExportPath=/home/$user/fenrirClipboard
clipboardExportPath=/tmp/fenrirClipboard clipboard_export_path=/tmp/fenrirClipboard
# Convert text emoticons like :) to descriptive text (e.g., "smiling face")
emoticons=True emoticons=True
# define the current Fenrir key # Define the Fenrir modifier key(s) - used to trigger Fenrir commands
fenrirKeys=KEY_KP0,KEY_META,KEY_INSERT # Examples: KEY_KP0 (numpad 0), KEY_META (Super/Windows), KEY_INSERT
scriptKeys=KEY_COMPOSE # Multiple keys: KEY_KP0,KEY_META,KEY_INSERT
timeFormat=%%I:%%M%%P fenrir_keys=KEY_KP0,KEY_META,KEY_INSERT
dateFormat=%%A, %%B %%d, %%Y # Script key - used to execute custom scripts in script_path
autoSpellCheck=True script_keys=KEY_COMPOSE
spellCheckLanguage=en_US # Time format using Python strftime codes (https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior)
# path for your scripts "scriptKeys" functionality # Default: %%I:%%M%%P (12-hour format, e.g., "06:38pm")
scriptPath=/usr/share/fenrirscreenreader/scripts # 24-hour format: %%H:%%M (e.g., "18:38")
# overload commands, and create new one without changing Fenrir default # Common codes: %%I=12hr hour, %%H=24hr hour, %%M=minute, %%S=second, %%P=am/pm lowercase, %%p=AM/PM uppercase
commandPath= time_format=%%I:%%M%%P
#fenrirBGColor = the backgroundcolor # Date format using Python strftime codes
#fenrirFGColor = the foregroundcolor # Default: %%A, %%B %%d, %%Y (e.g., "Tuesday, December 10, 2024")
#fenrirUnderline = speak the underline attribute # ISO format: %%Y-%%m-%%d (e.g., "2024-12-10")
#fenrirBold = speak the bold attribute # Common codes: %%A=weekday name, %%B=month name, %%d=day, %%Y=year with century, %%m=month number
#fenrirBlink = speak the blink attribute date_format=%%A, %%B %%d, %%Y
#fenrirFont = the font # Automatically spell check words when reviewing character-by-character
#fenrirFontSize = the fontsize auto_spell_check=True
attributeFormatString=Background fenrirBGColor,Foreground fenrirFGColor,fenrirUnderline,fenrirBold,fenrirBlink, Font fenrirFont,Fontsize fenrirFontSize # Language for spell checking (format: language_COUNTRY, e.g., en_US, en_GB, es_ES)
# present indentation spell_check_language=en_US
autoPresentIndent=False # path for your scripts "script_keys" functionality
# speak is only invoked on changeing ident level, sound always script_path=/usr/share/fenrirscreenreader/scripts
# 0 = sound and speak # Override default commands or add custom commands without modifying Fenrir installation
# 1 = sound only # Leave empty to use default commands only
# 2 = speak only command_path=
autoPresentIndentMode=1 # Format string for announcing text attributes (colors, formatting)
# play a sound when attributes change # Available variables:
hasAttributes=True # fenrirBGColor = the background color
# shell for PTY emulatiun (empty = default shell) # fenrirFGColor = the foreground color
# fenrirUnderline = speak the underline attribute
# fenrirBold = speak the bold attribute
# fenrirBlink = speak the blink attribute
# fenrirFont = the font name
# fenrirFontSize = the font size
attribute_format_string=Background fenrirBGColor,Foreground fenrirFGColor,fenrirUnderline,fenrirBold,fenrirBlink, Font fenrirFont,Fontsize fenrirFontSize
# Automatically announce indentation level changes (useful for Python, YAML, etc.)
auto_present_indent=False
# How to present indentation changes:
# 0 = sound and speak (both audio feedback and voice announcement)
# 1 = sound only (just play a tone)
# 2 = speak only (just voice announcement)
auto_present_indent_mode=1
# Play a sound when text attributes (color, bold, etc.) change
has_attributes=True
# Shell to use for PTY emulation mode (empty = use system default shell)
# Examples: /bin/bash, /bin/zsh, /usr/bin/fish
shell= shell=
[focus] [focus]
#follow the text cursor # Follow and announce text cursor position changes
cursor=True cursor=True
#follow highlighted text changes # Follow and announce highlighted/selected text changes (useful in menus)
highlight=False highlight=False
[remote] [remote]
@@ -191,33 +242,43 @@ driver=unixDriver
# tcp port # tcp port
port=22447 port=22447
# socket filepath # socket filepath
socketFile= socket_file=
# allow settings to overwrite # allow settings to overwrite
enableSettingsRemote=True enable_settings_remote=True
# allow commands to be executed # allow commands to be executed
enableCommandRemote=True enable_command_remote=True
[barrier] [barrier]
# Enable barrier detection - automatically detects table/box borders for improved navigation
enabled=False enabled=False
leftBarriers=│└┌─ # Characters that represent left-side barriers/borders (for table/box detection)
rightBarriers=│┘┐ left_barriers=│└┌
# Characters that represent right-side barriers/borders (for table/box detection)
right_barriers=│┘┐─
[review] [review]
lineBreak=True # Announce line breaks during review mode
endOfScreen=True line_break=True
# leave the review when pressing a key # Announce when reaching the end of screen during review
leaveReviewOnCursorChange=True end_of_screen=True
# leave the review when changing the screen # Exit review mode when cursor position changes (e.g., when typing)
leaveReviewOnScreenChange=True leave_review_on_cursor_change=True
# Exit review mode when switching to a different TTY/screen
[promote] leave_review_on_screen_change=True
enabled=True
inactiveTimeoutSec=120
list=
[menu] [menu]
vmenuPath= # Custom path for VMenu (virtual menu) profiles
quickMenu=speech#rate;speech#pitch;speech#volume # Leave empty to use default location (/etc/fenrirscreenreader/vmenu-profiles/)
vmenu_path=
# Quick menu: Semicolon-separated list of settings for rapid adjustment via VMenu
# Access with Fenrir+F10, navigate to "Quick Settings"
# Format: section#setting;section#setting;...
# Supported settings:
# - speech#rate, speech#pitch, speech#volume (0.0-1.0)
# - speech#module, speech#voice (speechdDriver only, auto-added)
# Note: speech#module and speech#voice are automatically added when
# speechdDriver is active. Do not add them manually.
quick_menu=speech#rate;speech#pitch;speech#volume
[prompt] [prompt]
# Custom prompt patterns for silence until prompt feature # Custom prompt patterns for silence until prompt feature
@@ -235,27 +296,32 @@ quickMenu=speech#rate;speech#pitch;speech#volume
# For "[user@hostname ~] $" use: \[.*@.*\s.*\]\s*[$#>]\s* # For "[user@hostname ~] $" use: \[.*@.*\s.*\]\s*[$#>]\s*
# For custom prompts ending with specific strings, use patterns like: .*your_prompt_ending$ # For custom prompts ending with specific strings, use patterns like: .*your_prompt_ending$
# For custom package manager prompts: .*your_package_manager.*\[[YyNn]/[YyNn]\].* # For custom package manager prompts: .*your_package_manager.*\[[YyNn]/[YyNn]\].*
customPatterns= custom_patterns=
# Specific prompt strings to match exactly (useful for very specific custom prompts) # Specific prompt strings to match exactly (useful for very specific custom prompts)
# Format: exactMatches=prompt1,prompt2,prompt3 # Format: exact_matches=prompt1,prompt2,prompt3
# Examples: # Examples:
# exactMatches=[storm@fenrir ~] $,[root@fenrir ~] #,Continue installation? [Y/n] # exact_matches=[storm@fenrir ~] $,[root@fenrir ~] #,Continue installation? [Y/n]
exactMatches= exact_matches=
[time] [time]
# automatic time announcement # Enable automatic time announcements
enabled=False enabled=False
# present time # Announce the current time
presentTime=True present_time=True
# present date (on change) # Announce the date (only when it changes, e.g., at midnight)
presentDate=True present_date=True
# present time after a given period of seconds # Announce time every N seconds (0 = disabled)
delaySec=0 # If delay_sec > 0, on_minutes is ignored
# present time after to given minutes example every 15 minutes: 00,15,30,45 delay_sec=0
# if delaySec is >0 onMinutes is ignored # Announce time at specific minutes of each hour (comma-separated)
onMinutes=00,30 # Examples:
# announce via soundicon (not interrupting) # 00 = Only on the hour (1:00, 2:00, 3:00, etc.)
# 00,30 = On the hour and half-hour (1:00, 1:30, 2:00, 2:30, etc.)
# 00,15,30,45 = Every 15 minutes
# Note: This is ignored if delay_sec > 0
on_minutes=00,30
# Play a sound icon before time announcement (non-interrupting)
announce=True announce=True
# interrupt current speech for time announcement # Interrupt current speech to announce time immediately
interrupt=False interrupt=False
Binary file not shown.
-2
View File
@@ -46,8 +46,6 @@ ErrorSpeech='ErrorSpeech.wav'
ErrorScreen='ErrorScreen.wav' ErrorScreen='ErrorScreen.wav'
# If you cursor over an text that has attributs (like color) # If you cursor over an text that has attributs (like color)
HasAttributes='has_attribute.wav' HasAttributes='has_attribute.wav'
# fenrir can promote strings if they appear on the screen.
PromotedText='PromotedText.wav'
# missspelled indicator # missspelled indicator
mispell='mispell.wav' mispell='mispell.wav'
# the for capital letter # the for capital letter
-2
View File
@@ -50,8 +50,6 @@ ErrorBraille=''
ErrorScreen='' ErrorScreen=''
# If you cursor over an text that has attributs (like color) # If you cursor over an text that has attributs (like color)
HasAttributes='' HasAttributes=''
# fenrir can promote strings if they appear on the screen.
PromotedText=''
# misspelled indicator # misspelled indicator
mispell='' mispell=''
# the for capital letter: # the for capital letter:
+39 -89
View File
@@ -7,14 +7,14 @@ configurable and easy to customize and extend.
=== Credit and intended audience === Credit and intended audience
This document is just a customization for Slint of the genuine This document is just a customization for Slint of the genuine
https://github.com/chrys87/fenrir/blob/master/docu/user.txt[Fenrir User https://git.stormux.org/storm/fenrir/src/branch/master/docs/user.txt[Fenrir User
Manual] motly written by Chrys, main developer of Fenrir. Manual] motly written by Chrys, main developer of Fenrir.
It has been adapted to its intended audience: end users of Fenrir on It has been adapted to its intended audience: end users of Fenrir on
Slint where it is already installed, thus concentrates on its setting Slint where it is already installed, thus concentrates on its setting
and usage. You will find more information about its features, and usage. You will find more information about its features,
installation and how customize and troubleshoot it and contribute to its installation and how customize and troubleshoot it and contribute to its
development on https://github.com/chrys87/fenrir[the Fenrir Git development on https://git.stormux.org/storm/fenrir[the Fenrir Git
repository]. repository].
=== Getting started with Fenrir === Getting started with Fenrir
@@ -1095,23 +1095,6 @@ announce=True
interrupt=False interrupt=False
.... ....
==== Promoted List
Promoted Lists are a nice feature if you are away from your computer or
performing more longer tasks. you can define a list of words which you
want to hear a sound icon for after a period of inactivity. Example if
the word "Chrys" appears after 120 Seconds of inactivity:
....
[promote]
enabled=True
inactiveTimeoutSec=120
list=Chrys
....
See section link:#Promote[Promote] in `+settings.conf+` for more
information.
=== Dictionary === Dictionary
You can make use of different kinds of built-in dictionary's. A You can make use of different kinds of built-in dictionary's. A
@@ -1286,8 +1269,8 @@ Enable remote control in settings.conf:
[remote] [remote]
enable=True enable=True
driver=unixDriver driver=unixDriver
enableSettingsRemote=True enable_settings_remote=True
enableCommandRemote=True enable_command_remote=True
.... ....
==== Using socat with Unix Sockets ==== Using socat with Unix Sockets
@@ -1317,13 +1300,13 @@ echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenr
echo "setting set speech#rate=0.8" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting set speech#rate=0.8" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
# Change punctuation level (none/some/most/all) # Change punctuation level (none/some/most/all)
echo "setting set general#punctuationLevel=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting set general#punctuation_level=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
# Voice and TTS control # Voice and TTS control
echo "setting set speech#voice=en-us+f3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting set speech#voice=en-us+f3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
# Multiple settings at once # 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 echo "setting set speech#rate=0.8;sound#volume=0.7;general#punctuation_level=most" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
# Reset all settings to defaults # Reset all settings to defaults
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
@@ -1394,8 +1377,8 @@ echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-dea
* `+speech#module=module_name+` - TTS module (e.g., "espeak-ng") * `+speech#module=module_name+` - TTS module (e.g., "espeak-ng")
*General Settings:* *General Settings:*
* `+general#punctuationLevel=none/some/most/all+` - Punctuation verbosity * `+general#punctuation_level=none/some/most/all+` - Punctuation verbosity
* `+general#autoSpellCheck=True/False+` - Automatic spell checking * `+general#auto_spell_check=True/False+` - Automatic spell checking
* `+general#emoticons=True/False+` - Enable emoticon replacement * `+general#emoticons=True/False+` - Enable emoticon replacement
*Sound Settings:* *Sound Settings:*
@@ -1407,11 +1390,11 @@ echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-dea
* `+focus#highlight=True/False+` - Follow text highlighting * `+focus#highlight=True/False+` - Follow text highlighting
*Keyboard Settings:* *Keyboard Settings:*
* `+keyboard#charEchoMode=0-2+` - Character echo (0=none, 1=always, 2=capslock only) * `+keyboard#char_echo_mode=0-2+` - Character echo (0=none, 1=always, 2=capslock only)
* `+keyboard#wordEcho=True/False+` - Echo complete words * `+keyboard#word_echo=True/False+` - Echo complete words
*Screen Settings:* *Screen Settings:*
* `+screen#ignoreScreen=1,2,3+` - TTY screens to ignore * `+screen#ignore_screen=1,2,3+` - TTY screens to ignore
==== settings.conf syntax ==== settings.conf syntax
@@ -1511,14 +1494,14 @@ frequencies.
sound file. sound file.
.... ....
genericPlayFileCommand=<your command for playing a file> generic_play_file_command=<your command for playing a file>
.... ....
`+genericFrequencyCommand+` defines the command that is used playing `+genericFrequencyCommand+` defines the command that is used playing
frequencies. frequencies.
.... ....
genericFrequencyCommand=<your command for playing a frequence> generic_frequency_command=<your command for playing a frequence>
.... ....
The following variables are substituted in `+genericPlayFileCommand+` The following variables are substituted in `+genericPlayFileCommand+`
@@ -1532,13 +1515,13 @@ and `+genericFrequencyCommand+`:
Example genericPlayFileCommand (default) Example genericPlayFileCommand (default)
.... ....
genericPlayFileCommand=play -q -v fenrirVolume fenrirSoundFile generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile
.... ....
Example genericFrequencyCommand (default) Example genericFrequencyCommand (default)
.... ....
genericFrequencyCommand=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
.... ....
==== Speech ==== Speech
@@ -1579,7 +1562,7 @@ Values: Range Minimum:`+0.0+` is lowest, Maximum:`+1.0+` is highest.
A Pitch for capital letters can be set. A Pitch for capital letters can be set.
.... ....
capitalPitch=0.9 capital_pitch=0.9
.... ....
Values: Range Minimum:`+0.0+` is lowest, Maximum:`+1.0+` is highest. Values: Range Minimum:`+0.0+` is lowest, Maximum:`+1.0+` is highest.
@@ -1681,7 +1664,7 @@ the pico module:
language=de-DE language=de-DE
.... ....
Read new text as it occurs autoReadIncoming=True Values: on=`+True+`, Read new text as it occurs auto_read_incoming=True Values: on=`+True+`,
off=`+False+` off=`+False+`
==== Screen ==== Screen
@@ -1711,7 +1694,7 @@ Values:`+cp850+` is used for Western languages like USA or Europe.
The driver updates Fenrir with changes on the screen. The driver updates Fenrir with changes on the screen.
.... ....
screenUpdateDelay=0.05 screen_update_delay=0.05
.... ....
Values: in Seconds Values: in Seconds
@@ -1776,7 +1759,7 @@ Gives Fenrir exclusive access to the keyboard and lets it control
keystrokes. This is needed to intercept Fenrir related shortcuts. keystrokes. This is needed to intercept Fenrir related shortcuts.
.... ....
grabDevices=True grab_devices=True
.... ....
Values: on=`+True+`, off=`+False+` Values: on=`+True+`, off=`+False+`
@@ -1785,7 +1768,7 @@ The following makes sense if you are using a second screenreader and
want to have some hooked events. Fenrir ignores all shortcuts then. want to have some hooked events. Fenrir ignores all shortcuts then.
.... ....
ignoreShortcuts=False ignore_shortcuts=False
.... ....
Values: on=`+True+`, off=`+False+` Values: on=`+True+`, off=`+False+`
@@ -1793,7 +1776,7 @@ Values: on=`+True+`, off=`+False+`
The current keyboard layout used for shortcuts. The current keyboard layout used for shortcuts.
.... ....
keyboardLayout=desktop keyboard_layout=desktop
.... ....
Values: An absolute Path to a Keyboard definition file or a Filename Values: An absolute Path to a Keyboard definition file or a Filename
@@ -1810,7 +1793,7 @@ Values: on=`+True+`, off=`+False+`
Announce deleted characters Announce deleted characters
.... ....
charDeleteEcho=True char_delete_echo=True
.... ....
Values: on=`+True+`, off=`+False+` Values: on=`+True+`, off=`+False+`
@@ -1818,7 +1801,7 @@ Values: on=`+True+`, off=`+False+`
Announce word after pressing space Announce word after pressing space
.... ....
wordEcho=False word_echo=False
.... ....
Values: on=`+True+`, off=`+False+` Values: on=`+True+`, off=`+False+`
@@ -1826,7 +1809,7 @@ Values: on=`+True+`, off=`+False+`
Interrupt speech on any keypress Interrupt speech on any keypress
.... ....
interruptOnKeyPress=False interrupt_on_key_press=False
.... ....
Values: on=`+True+`, off=`+False+` Values: on=`+True+`, off=`+False+`
@@ -1834,7 +1817,7 @@ Values: on=`+True+`, off=`+False+`
You can filter the keys that speech should interrupt You can filter the keys that speech should interrupt
.... ....
interruptOnKeyPressFilter= interrupt_on_key_press_filter=
.... ....
Values: (List) empty = all keys, otherwise interrupt with specified keys Values: (List) empty = all keys, otherwise interrupt with specified keys
@@ -1842,7 +1825,7 @@ Values: (List) empty = all keys, otherwise interrupt with specified keys
The timeout that is used for double tap shortcuts The timeout that is used for double tap shortcuts
.... ....
doubleTapTimeout=0.2 double_tap_timeout=0.2
.... ....
Values: Seconds Values: Seconds
@@ -1862,7 +1845,7 @@ Values: off=0, error=1, warning=2, info=3
the current punctuation and dict file in use: the current punctuation and dict file in use:
.... ....
punctuationProfile=default punctuation_profile=default
.... ....
Values: Text, see available profiles in `+/etc/fenrir/punctuation+` or Values: Text, see available profiles in `+/etc/fenrir/punctuation+` or
@@ -1871,7 +1854,7 @@ in `+sourceTree/config/punctuation+`
The current punctuation level in use: The current punctuation level in use:
.... ....
punctuationLevel=some punctuation_level=some
.... ....
Values: Text, See available levels in the used punctuation file. Values: Text, See available levels in the used punctuation file.
@@ -1879,7 +1862,7 @@ Values: Text, See available levels in the used punctuation file.
Respect pause for punctuations: Respect pause for punctuations:
.... ....
respectPunctuationPause=True respect_punctuation_pause=True
.... ....
Values: on=`+True+`, off=`+False+` Values: on=`+True+`, off=`+False+`
@@ -1887,7 +1870,7 @@ Values: on=`+True+`, off=`+False+`
Add a pause on Line break: Add a pause on Line break:
.... ....
newLinePause=True new_line_pause=True
.... ....
Values: on=`+True+`, off=`+False+` Values: on=`+True+`, off=`+False+`
@@ -1905,7 +1888,7 @@ Values: Text, Systemfilepath
The number of available clipboards: The number of available clipboards:
.... ....
numberOfClipboards=10 number_of_clipboards=10
.... ....
Values: Integer, 1 - 999 Values: Integer, 1 - 999
@@ -1921,7 +1904,7 @@ Values: on=`+True+`, off=`+False+`
Define the current Fenrir keys: Define the current Fenrir keys:
.... ....
fenrirKeys=KEY_KP0,KEY_META,KEY_INSERT fenrir_keys=KEY_KP0,KEY_META,KEY_INSERT
.... ....
Values, Text list, separated by comma. Values, Text list, separated by comma.
@@ -1955,7 +1938,7 @@ https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior[d
Enable or Disable spellcheck whilst typing: Enable or Disable spellcheck whilst typing:
.... ....
autoSpellCheck=True auto_spell_check=True
.... ....
Values: on=`+True+`, off=`+False+` Values: on=`+True+`, off=`+False+`
@@ -1963,7 +1946,7 @@ Values: on=`+True+`, off=`+False+`
The use of the dictionary with spellcheck: The use of the dictionary with spellcheck:
.... ....
spellCheckLanguage=en_US spell_check_language=en_US
.... ....
Values: Text, see aspell dictionary's. Values: Text, see aspell dictionary's.
@@ -2044,44 +2027,11 @@ Values: on=`+True+`, off=`+False+`
Leave the review mode when changing the screen (From TTY3 to TTY4): Leave the review mode when changing the screen (From TTY3 to TTY4):
.... ....
leaveReviewOnScreenChange=True leave_review_on_screen_change=True
.... ....
Values: on=`+True+`, off=`+False+` Values: on=`+True+`, off=`+False+`
==== Promote
"Promoted Lists" are configured in the section `+[promote]+`. Turn
Promoted Lists" on or off:
....
enabled=True
....
Values: on=`+True+`, off=`+False+`
The minimum time interval of inactivity to activate promoting. By
default it promotes after 120 Seconds inactivity:
....
inactiveTimeoutSec=120
....
Values: in Seconds
Define a list of promoted words comma seperated:
....
list=
....
Values: text (comma seperated) Example to promote the word "nickname" or
a bash prompt:
....
list=nickname,$:,#:
....
==== Time ==== Time
The automated time announcement is configured in the section `+[time]+`. The automated time announcement is configured in the section `+[time]+`.
@@ -2193,9 +2143,9 @@ settings.conf). Commands are python files with a special scheme. You can
assign them to a shortcut using the filename without an extension or assign them to a shortcut using the filename without an extension or
place them in a hook trigger like OnInput or OnScreenChange. For further place them in a hook trigger like OnInput or OnScreenChange. For further
information see developer guide. Good Examples: information see developer guide. Good Examples:
https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/commands/date.py["date.py"] https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/commands/date.py["date.py"]
(announce the Date), (announce the Date),
https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/commands/shut_up.py["shut_up.py"] https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/commands/shut_up.py["shut_up.py"]
(interrupt output) the basic scheme for a command is as follows: (interrupt output) the basic scheme for a command is as follows:
.... ....
@@ -2218,7 +2168,7 @@ class command():
pass pass
.... ....
* https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/command_template.py[Template * https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/command_template.py[Template
lives here] lives here]
* The class needs to have the name "command". * The class needs to have the name "command".
* "initialize" is running once whilst loading the command. * "initialize" is running once whilst loading the command.
@@ -2276,7 +2226,7 @@ root.
=== Bugreports and feature requests === Bugreports and feature requests
Please report Bugs and feature requests to: Please report Bugs and feature requests to:
https://github.com/chrys87/fenrir/issues https://git.stormux.org/storm/fenrir/issues
for bugs please provide a link:#Howto create a debug file[debug] file for bugs please provide a link:#Howto create a debug file[debug] file
that shows the issue. that shows the issue.
+11 -11
View File
@@ -102,17 +102,17 @@ volume=0.7
[keyboard] [keyboard]
driver=evdevDriver driver=evdevDriver
keyboardLayout=desktop keyboard_layout=desktop
[screen] [screen]
driver=vcsaDriver driver=vcsaDriver
ignoreScreen= ignore_screen=
[remote] [remote]
enable=True enable=True
driver=unixDriver driver=unixDriver
enableSettingsRemote=True enable_settings_remote=True
enableCommandRemote=True enable_command_remote=True
``` ```
## Remote Control ## Remote Control
@@ -126,8 +126,8 @@ Enable remote control in settings:
enable=True enable=True
driver=unixDriver # or tcpDriver driver=unixDriver # or tcpDriver
port=22447 # for TCP driver port=22447 # for TCP driver
enableSettingsRemote=True # allow settings changes enable_settings_remote=True # allow settings changes
enableCommandRemote=True # allow command execution enable_command_remote=True # allow command execution
``` ```
### Basic Usage with socat ### Basic Usage with socat
@@ -155,14 +155,14 @@ echo "setting set speech#pitch=0.6" | socat - UNIX-CLIENT:/tmp/fenrirscreenreade
echo "setting set speech#volume=0.9" | 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) # Change punctuation level (none/some/most/all)
echo "setting set general#punctuationLevel=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting set general#punctuation_level=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
# Voice and TTS control # Voice and TTS control
echo "setting set speech#voice=en-us+f3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock 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 echo "setting set speech#module=espeak-ng" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
# Multiple settings at once # 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 echo "setting set speech#rate=0.8;sound#volume=0.7;general#punctuation_level=most" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
# Reset all settings # Reset all settings
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
@@ -208,11 +208,11 @@ echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-dea
- `command resetvmenu` - Reset virtual menu - `command resetvmenu` - Reset virtual menu
**Key Settings You Can Change:** **Key Settings You Can Change:**
- Punctuation level: `setting set general#punctuationLevel=all` - Punctuation level: `setting set general#punctuation_level=all`
- Speech parameters: `setting set speech#rate=0.8;speech#pitch=0.6` - Speech parameters: `setting set speech#rate=0.8;speech#pitch=0.6`
- Voice selection: `setting set speech#voice=en-us+f3` - Voice selection: `setting set speech#voice=en-us+f3`
- Character echo: `setting set keyboard#charEchoMode=1` - Character echo: `setting set keyboard#char_echo_mode=1`
- Screen ignore: `setting set screen#ignoreScreen=1,2,3` - Screen ignore: `setting set screen#ignore_screen=1,2,3`
### Scripting Integration ### Scripting Integration
+36 -62
View File
@@ -160,7 +160,7 @@ For Arch there are PKGBUILDs in the AUR:
- Download the latest stable version from the [[https://linux-a11y.org/index.php?page=fenrir-screenreader|Fenrir-Project]] site. - Download the latest stable version from the [[https://linux-a11y.org/index.php?page=fenrir-screenreader|Fenrir-Project]] site.
- Unpack the archive - Unpack the archive
- Check the needed Dependencys by running [[https://github.com/chrys87/fenrir/blob/master/check-dependencies.py|check-dependencys.py]] script - Check the needed Dependencys by running [[https://git.stormux.org/storm/fenrir/src/branch/master/check-dependencies.py|check-dependencys.py]] script
- install the missing dependencies an standard installation requires the following: - install the missing dependencies an standard installation requires the following:
* python3 >= 3.3 (and all the following is needed for python3 ) * python3 >= 3.3 (and all the following is needed for python3 )
* python3-speechd (screen) * python3-speechd (screen)
@@ -171,7 +171,7 @@ For Arch there are PKGBUILDs in the AUR:
* python3-pyenchant (spellchecker) * python3-pyenchant (spellchecker)
* your language for aspell (aspell-<lang>) (spellchecker) * your language for aspell (aspell-<lang>) (spellchecker)
* sox (sound) * sox (sound)
* For an individual installation see [[#Support and Requirements|Support and Requirements]] or consult the [[https://github.com/chrys87/fenrir/blob/master/README.md|Readme]]) * For an individual installation see [[#Support and Requirements|Support and Requirements]] or consult the [[https://git.stormux.org/storm/fenrir/src/branch/master/README.md|Readme]])
- run "install.sh" as root - run "install.sh" as root
this installs Fenrir as the following this installs Fenrir as the following
@@ -185,7 +185,7 @@ to remove Fenrir just run uninstall.sh as root
if you want to get the latest code you can use git to get a development snapshot: if you want to get the latest code you can use git to get a development snapshot:
git clone https://github.com/chrys87/fenrir.git git clone https://git.stormux.org/storm/fenrir.git
===== Auto Start ===== ===== Auto Start =====
@@ -729,15 +729,6 @@ Example on fix minutes in an hour. example every quarter "delaySec=0" and "onMin
onMinutes=00,15,30,45 onMinutes=00,15,30,45
announce=True announce=True
interrupt=False interrupt=False
==== Promoted List ====
Promoted Lists are a nice feature if you are away from your computer or performing more longer tasks.
you can define a list of words which you want to hear a sound icon for after a period of inactivity.
Example if the word "Chrys" appears after 120 Seconds of inactivity:
[promote]
enabled=True
inactiveTimeoutSec=120
list=Chrys
See section [[#Promote|Promote]] in ''settings.conf'' for more information.
==== Punctuation ==== ==== Punctuation ====
Fenrir handles punctuation levels and names for you with several provided dictionaries. Fenrir handles punctuation levels and names for you with several provided dictionaries.
@@ -867,9 +858,9 @@ Values: ''0.0'' is quietest, ''1.0'' is loudest.
The generic sound driver uses shell commands for play sound and frequencies. The generic sound driver uses shell commands for play sound and frequencies.
''genericPlayFileCommand'' defines the command that is used to play a sound file. ''genericPlayFileCommand'' defines the command that is used to play a sound file.
genericPlayFileCommand=<your command for playing a file> generic_play_file_command=<your command for playing a file>
''genericFrequencyCommand'' defines the command that is used playing frequencies. ''genericFrequencyCommand'' defines the command that is used playing frequencies.
genericFrequencyCommand=<your command for playing a frequence> generic_frequency_command=<your command for playing a frequence>
The following variables are substituted in ''genericPlayFileCommand'' and ''genericFrequencyCommand'': The following variables are substituted in ''genericPlayFileCommand'' and ''genericFrequencyCommand'':
* ''fenrirVolume'' = the current volume setting * ''fenrirVolume'' = the current volume setting
@@ -878,9 +869,9 @@ The following variables are substituted in ''genericPlayFileCommand'' and ''gene
* ''fenrirDuration'' = the duration of the frequency * ''fenrirDuration'' = the duration of the frequency
Example genericPlayFileCommand (default) Example genericPlayFileCommand (default)
genericPlayFileCommand=play -q -v fenrirVolume fenrirSoundFile generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile
Example genericFrequencyCommand (default) Example genericFrequencyCommand (default)
genericFrequencyCommand=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
==== Speech ==== ==== Speech ====
Speech is configured in section ''[speech]''. Speech is configured in section ''[speech]''.
Turn speech on or off: Turn speech on or off:
@@ -910,7 +901,7 @@ Pitch controls the pitch of the voice.
Values: Range Minimum:''0.0'' is lowest, Maximum:''1.0'' is highest. Values: Range Minimum:''0.0'' is lowest, Maximum:''1.0'' is highest.
A Pitch for capital letters can be set. A Pitch for capital letters can be set.
capitalPitch=0.9 capital_pitch=0.9
Values: Range Minimum:''0.0'' is lowest, Maximum:''1.0'' is highest. Values: Range Minimum:''0.0'' is lowest, Maximum:''1.0'' is highest.
The Volume controls the loudness of the voice. The Volume controls the loudness of the voice.
@@ -931,7 +922,7 @@ Select the language you want Fenrir to use.
Values: Text, see your TTS synths documentation what is available. Values: Text, see your TTS synths documentation what is available.
Read new text as it occurs Read new text as it occurs
autoReadIncoming=True auto_read_incoming=True
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
=== Generic Driver === === Generic Driver ===
@@ -948,16 +939,16 @@ The following variables are substituted in ''genericSpeechCommand'':
* ''fenrirRate'' = is replaced with the current speed (speech rate) * ''fenrirRate'' = is replaced with the current speed (speech rate)
Example genericSpeechCommand (default): Example genericSpeechCommand (default):
genericSpeechCommand=espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice "fenrirText" generic_speech_command=espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice "fenrirText"
These are the minimum and maximum values of the TTS system used in genericSpeechCommand. They are needed to calculate the abstract range in volume, rate and pitch 0.0 - 1.0. These are the minimum and maximum values of the TTS system used in genericSpeechCommand. They are needed to calculate the abstract range in volume, rate and pitch 0.0 - 1.0.
FenrirMinVolume=0 FenrirMinVolume=0
fenrirMaxVolume=200 fenrir_max_volume=200
fenrirMinPitch=0 fenrir_min_pitch=0
fenrirMaxPitch=99 fenrir_max_pitch=99
fenrirMinRate=80 fenrir_min_rate=80
fenrirMaxRate=450 fenrir_max_rate=450
The current volume, pitch and rate is calculated like this The current volume, pitch and rate is calculated like this
value = min + [volume,pitch,rate] * (min - max ) value = min + [volume,pitch,rate] * (min - max )
@@ -1033,7 +1024,7 @@ The encoding of the screen
Values:''cp850'' is used for Western languages like USA or Europe. Values:''cp850'' is used for Western languages like USA or Europe.
The driver updates Fenrir with changes on the screen. The driver updates Fenrir with changes on the screen.
screenUpdateDelay=0.05 screen_update_delay=0.05
Values: in Seconds Values: in Seconds
If you want Fenrir to not be active on any screen for various reasons. Maybe an X server or Wayland is running on that screen. You can make Fenrir ignore it or multiple screens seperated by '','' with: If you want Fenrir to not be active on any screen for various reasons. Maybe an X server or Wayland is running on that screen. You can make Fenrir ignore it or multiple screens seperated by '','' with:
@@ -1064,15 +1055,15 @@ Values:
* ''<Device Name>'' just use the device with the given name. * ''<Device Name>'' just use the device with the given name.
Gives Fenrir exclusive access to the keyboard and lets it control keystrokes. This is needed to intercept Fenrir related shortcuts. Gives Fenrir exclusive access to the keyboard and lets it control keystrokes. This is needed to intercept Fenrir related shortcuts.
grabDevices=True grab_devices=True
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
The following makes sense if you are using a second screenreader and want to have some hooked events. Fenrir ignores all shortcuts then. The following makes sense if you are using a second screenreader and want to have some hooked events. Fenrir ignores all shortcuts then.
ignoreShortcuts=False ignore_shortcuts=False
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
The current keyboard layout used for shortcuts. The current keyboard layout used for shortcuts.
keyboardLayout=desktop keyboard_layout=desktop
Values: An absolute Path to a Keyboard definition file or a Filename without extension located in ''/etc/fenrir/keyboard'' Values: An absolute Path to a Keyboard definition file or a Filename without extension located in ''/etc/fenrir/keyboard''
Announce characters while typing. Announce characters while typing.
@@ -1080,23 +1071,23 @@ Announce characters while typing.
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
Announce deleted characters Announce deleted characters
charDeleteEcho=True char_delete_echo=True
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
Announce word after pressing space Announce word after pressing space
wordEcho=False word_echo=False
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
Interrupt speech on any keypress Interrupt speech on any keypress
interruptOnKeyPress=False interrupt_on_key_press=False
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
You can filter the keys that speech should interrupt You can filter the keys that speech should interrupt
interruptOnKeyPressFilter= interrupt_on_key_press_filter=
Values: (List) empty = all keys, otherwise interrupt with specified keys Values: (List) empty = all keys, otherwise interrupt with specified keys
The timeout that is used for double tap shortcuts The timeout that is used for double tap shortcuts
doubleTapTimeout=0.2 double_tap_timeout=0.2
Values: Seconds Values: Seconds
==== General ==== ==== General ====
Overall settings can be configured from the section ''[general]''. Overall settings can be configured from the section ''[general]''.
@@ -1106,19 +1097,19 @@ Set the current debug level:
Values: off=0, error=1, warning=2, info=3 Values: off=0, error=1, warning=2, info=3
the current punctuation and dict file in use: the current punctuation and dict file in use:
punctuationProfile=default punctuation_profile=default
Values: Text, see available profiles in ''/etc/fenrir/punctuation'' or in ''sourceTree/config/punctuation'' Values: Text, see available profiles in ''/etc/fenrir/punctuation'' or in ''sourceTree/config/punctuation''
The current punctuation level in use: The current punctuation level in use:
punctuationLevel=some punctuation_level=some
Values: Text, See available levels in the used punctuation file. Values: Text, See available levels in the used punctuation file.
Respect pause for punctuations: Respect pause for punctuations:
respectPunctuationPause=True respect_punctuation_pause=True
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
Add a pause on Line break: Add a pause on Line break:
newLinePause=True new_line_pause=True
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
Specify the path where the clipboard should be exported to. Specify the path where the clipboard should be exported to.
@@ -1128,7 +1119,7 @@ The variable ''$user'' is replaced by the current logged username.
Values: Text, Systemfilepath Values: Text, Systemfilepath
The number of available clipboards: The number of available clipboards:
numberOfClipboards=10 number_of_clipboards=10
Values: Integer, 1 - 999 Values: Integer, 1 - 999
Replace emoticons like :) or ;) with text insertions: Replace emoticons like :) or ;) with text insertions:
@@ -1136,7 +1127,7 @@ Replace emoticons like :) or ;) with text insertions:
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
Define the current Fenrir keys: Define the current Fenrir keys:
fenrirKeys=KEY_KP0,KEY_META,KEY_INSERT fenrir_keys=KEY_KP0,KEY_META,KEY_INSERT
Values, Text list, separated by comma. Values, Text list, separated by comma.
Define the current script keys: Define the current script keys:
@@ -1152,11 +1143,11 @@ The date format to be used for (date command) output:
Values: see python specification for [[https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior|datetime.strftime]] Values: see python specification for [[https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior|datetime.strftime]]
Enable or Disable spellcheck whilst typing: Enable or Disable spellcheck whilst typing:
autoSpellCheck=True auto_spell_check=True
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
The use of the dictionary with spellcheck: The use of the dictionary with spellcheck:
spellCheckLanguage=en_US spell_check_language=en_US
Values: Text, see aspell dictionary's. Values: Text, see aspell dictionary's.
Folder Path for your scripts "scriptKey" functionality: Folder Path for your scripts "scriptKey" functionality:
@@ -1197,25 +1188,8 @@ Leave the review mode when pressing a key:
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
Leave the review mode when changing the screen (From TTY3 to TTY4): Leave the review mode when changing the screen (From TTY3 to TTY4):
leaveReviewOnScreenChange=True leave_review_on_screen_change=True
Values: on=''True'', off=''False'' Values: on=''True'', off=''False''
==== Promote ====
"Promoted Lists" are configured in the section ''[promote]''.
Turn Promoted Lists" on or off:
enabled=True
Values: on=''True'', off=''False''
The minimum time interval of inactivity to activate promoting.
By default it promotes after 120 Seconds inactivity:
inactiveTimeoutSec=120
Values: in Seconds
Define a list of promoted words comma seperated:
list=
Values: text (comma seperated)
Example to promote the word "nickname" or a bash prompt:
list=nickname,$:,#:
==== Time ==== ==== Time ====
The automated time announcement is configured in the section ''[time]''. The automated time announcement is configured in the section ''[time]''.
Time announcement is disabled by default. Time announcement is disabled by default.
@@ -1270,7 +1244,7 @@ File: ''/usr/share/fenrirscreenreader/scripts/helloWorld__-__key_h.sh'':
===== Commands ===== ===== Commands =====
You can place your own commands in "/usr/share/fenrirscreenreader/commands" (path is configurable in settings.conf). You can place your own commands in "/usr/share/fenrirscreenreader/commands" (path is configurable in settings.conf).
Commands are python files with a special scheme. You can assign them to a shortcut using the filename without an extension or place them in a hook trigger like OnInput or OnScreenChange. For further information see developer guide. Commands are python files with a special scheme. You can assign them to a shortcut using the filename without an extension or place them in a hook trigger like OnInput or OnScreenChange. For further information see developer guide.
Good Examples: [[https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/commands/date.py|"date.py"]] (announce the Date), [[https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/commands/shut_up.py|"shut_up.py"]] (interrupt output) Good Examples: [[https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/commands/date.py|"date.py"]] (announce the Date), [[https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/commands/shut_up.py|"shut_up.py"]] (interrupt output)
the basic scheme for a command is as follows: the basic scheme for a command is as follows:
from core import debug from core import debug
@@ -1289,7 +1263,7 @@ the basic scheme for a command is as follows:
def setCallback(self, callback): def setCallback(self, callback):
pass pass
* [[https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/command_template.py|Template lives here]] * [[https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/command_template.py|Template lives here]]
* The class needs to have the name "command". * The class needs to have the name "command".
* "initialize" is running once whilst loading the command. * "initialize" is running once whilst loading the command.
* "shutdown" is running on unload like the command (quit fenrir) * "shutdown" is running on unload like the command (quit fenrir)
@@ -1319,7 +1293,7 @@ the basic scheme for a command is as follows:
- You can test if speech-dispatcher works by invoking it as root\\ ''sudo spd-say "hello world"'' - You can test if speech-dispatcher works by invoking it as root\\ ''sudo spd-say "hello world"''
===== Bugreports and feature requests ===== ===== Bugreports and feature requests =====
Please report Bugs and feature requests to: Please report Bugs and feature requests to:
[[https://github.com/chrys87/fenrir/issues|https://github.com/chrys87/fenrir/issues]] [[https://git.stormux.org/storm/fenrir/issues|https://git.stormux.org/storm/fenrir/issues]]
for bugs please provide a [[#Howto create a debug file|debug]] file that shows the issue. for bugs please provide a [[#Howto create a debug file|debug]] file that shows the issue.
==== How-to create a debug file ==== ==== How-to create a debug file ====
+1 -1
View File
@@ -56,7 +56,7 @@ To test Fenrir:
sudo fenrir sudo fenrir
To have Fenrir start on system boot using systemd: To have Fenrir start on system boot using systemd:
download service file: https://raw.githubusercontent.com/chrys87/fenrir/master/autostart/systemd/Arch/fenrir.service download service file: https://git.stormux.org/storm/fenrir/raw/branch/master/autostart/systemd/Arch/fenrir.service
move the service file to: /etc/systemd/system/fenrir.service move the service file to: /etc/systemd/system/fenrir.service
sudo systemctl enable fenrir sudo systemctl enable fenrir
+69
View File
@@ -0,0 +1,69 @@
[pytest]
# Pytest configuration for Fenrir screen reader
# Test discovery patterns
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
# Test paths
testpaths = tests
# Minimum Python version
minversion = 3.7
# Output options
addopts =
# Verbose output with test names
-v
# Show extra test summary info
-ra
# Enable strict markers (only registered markers allowed)
--strict-markers
# Show local variables in tracebacks
--showlocals
# Warnings configuration
-W ignore::DeprecationWarning
# Optional plugins (uncomment if installed):
# --timeout=30 # Requires pytest-timeout
# --cov-report=term-missing # Requires pytest-cov
# -x # Stop on first failure
# Register custom markers
markers =
unit: Unit tests (fast, no mocking)
integration: Integration tests (require mocking)
driver: Driver tests (require root access)
slow: Tests that take more than 1 second
remote: Tests for remote control functionality
settings: Tests for settings and configuration
commands: Tests for command system
vmenu: Tests for VMenu system
# Coverage configuration
[coverage:run]
source = src/fenrirscreenreader
omit =
*/tests/*
*/vmenu-profiles/*
*/__pycache__/*
*/site-packages/*
[coverage:report]
# Fail if coverage falls below this percentage
# fail_under = 70
exclude_lines =
# Standard pragma
pragma: no cover
# Don't complain about missing debug code
def __repr__
# Don't complain if tests don't hit defensive assertion code
raise AssertionError
raise NotImplementedError
# Don't complain about abstract methods
@abstractmethod
# Don't complain about initialization
if __name__ == .__main__.:
[coverage:html]
directory = htmlcov
@@ -22,7 +22,7 @@ class command:
self.env["runtime"]["MemoryManager"].add_index_list( self.env["runtime"]["MemoryManager"].add_index_list(
"clipboardHistory", "clipboardHistory",
self.env["runtime"]["SettingsManager"].get_setting_as_int( self.env["runtime"]["SettingsManager"].get_setting_as_int(
"general", "numberOfClipboards" "general", "number_of_clipboards"
), ),
) )
@@ -36,11 +36,11 @@ class command:
def update_spell_language(self): def update_spell_language(self):
self.spellChecker = enchant.Dict( self.spellChecker = enchant.Dict(
self.env["runtime"]["SettingsManager"].get_setting( self.env["runtime"]["SettingsManager"].get_setting(
"general", "spellCheckLanguage" "general", "spell_check_language"
) )
) )
self.language = self.env["runtime"]["SettingsManager"].get_setting( self.language = self.env["runtime"]["SettingsManager"].get_setting(
"general", "spellCheckLanguage" "general", "spell_check_language"
) )
def run(self): def run(self):
@@ -48,7 +48,7 @@ class command:
return return
if ( if (
self.env["runtime"]["SettingsManager"].get_setting( self.env["runtime"]["SettingsManager"].get_setting(
"general", "spellCheckLanguage" "general", "spell_check_language"
) )
!= self.language != self.language
): ):
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
from fenrirscreenreader.core.i18n import _
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _("Cycle through key echo modes: character, word, off")
def run(self):
settings_manager = self.env["runtime"]["SettingsManager"]
output_manager = self.env["runtime"]["OutputManager"]
# Get current settings
char_echo_mode = settings_manager.get_setting("keyboard", "char_echo_mode")
word_echo = settings_manager.get_setting_as_bool("keyboard", "word_echo")
# Determine current state and cycle to next
# States: character (char=1, word=False) -> word (char=0, word=True) -> off (char=0, word=False)
if char_echo_mode == "1" and not word_echo:
# Currently character echo, switch to word echo
settings_manager.set_setting("keyboard", "char_echo_mode", "0")
settings_manager.set_setting("keyboard", "word_echo", "True")
output_manager.present_text(
_("Echo by word"), interrupt=True
)
elif word_echo:
# Currently word echo, switch to off
settings_manager.set_setting("keyboard", "char_echo_mode", "0")
settings_manager.set_setting("keyboard", "word_echo", "False")
output_manager.present_text(
_("Echo off"), interrupt=True
)
else:
# Currently off (or caps mode), switch to character echo
settings_manager.set_setting("keyboard", "char_echo_mode", "1")
settings_manager.set_setting("keyboard", "word_echo", "False")
output_manager.present_text(
_("Echo by character"), interrupt=True
)
def set_callback(self, callback):
pass
@@ -59,7 +59,7 @@ class command:
def run(self): def run(self):
current_layout = self.env["runtime"]["SettingsManager"].get_setting( current_layout = self.env["runtime"]["SettingsManager"].get_setting(
"keyboard", "keyboardLayout" "keyboard", "keyboard_layout"
) )
# Extract layout name from full path if needed # Extract layout name from full path if needed
@@ -83,7 +83,7 @@ class command:
# Update setting and reload shortcuts # Update setting and reload shortcuts
self.env["runtime"]["SettingsManager"].set_setting( self.env["runtime"]["SettingsManager"].set_setting(
"keyboard", "keyboardLayout", next_layout "keyboard", "keyboard_layout", next_layout
) )
# Reload shortcuts with new layout # Reload shortcuts with new layout
@@ -1,21 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import importlib.util
import os
from fenrirscreenreader.core.i18n import _
_base_path = os.path.join(os.path.dirname(__file__), "adjustment_base.py")
_spec = importlib.util.spec_from_file_location("adjustment_base", _base_path)
_module = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_module)
adjustment_command = _module.adjustment_command
class command(adjustment_command):
def __init__(self):
super().__init__("alsa", "volume", "dec")
@@ -26,7 +26,7 @@ class command:
def run(self): def run(self):
clipboard_file_path = self.env["runtime"][ clipboard_file_path = self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting("general", "clipboardExportPath") ].get_setting("general", "clipboard_export_path")
clipboard_file_path = clipboard_file_path.replace( clipboard_file_path = clipboard_file_path.replace(
"$user", self.env["general"]["curr_user"] "$user", self.env["general"]["curr_user"]
) )
@@ -22,7 +22,7 @@ class command:
return _("sends the following keypress to the terminal or application") return _("sends the following keypress to the terminal or application")
def run(self): def run(self):
self.env["input"]["keyForeward"] = 3 self.env["input"]["key_forward"] = 3
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("Forward next keypress"), interrupt=True _("Forward next keypress"), interrupt=True
) )
@@ -26,7 +26,7 @@ class command:
def run(self): def run(self):
clipboard_file_path = self.env["runtime"][ clipboard_file_path = self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting("general", "clipboardExportPath") ].get_setting("general", "clipboard_export_path")
clipboard_file_path = clipboard_file_path.replace( clipboard_file_path = clipboard_file_path.replace(
"$user", self.env["general"]["curr_user"] "$user", self.env["general"]["curr_user"]
) )
@@ -1,21 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import importlib.util
import os
from fenrirscreenreader.core.i18n import _
_base_path = os.path.join(os.path.dirname(__file__), "adjustment_base.py")
_spec = importlib.util.spec_from_file_location("adjustment_base", _base_path)
_module = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_module)
adjustment_command = _module.adjustment_command
class command(adjustment_command):
def __init__(self):
super().__init__("alsa", "volume", "inc")
@@ -18,7 +18,7 @@ class command:
self.env["runtime"]["MemoryManager"].add_index_list( self.env["runtime"]["MemoryManager"].add_index_list(
"clipboardHistory", "clipboardHistory",
self.env["runtime"]["SettingsManager"].get_setting_as_int( self.env["runtime"]["SettingsManager"].get_setting_as_int(
"general", "numberOfClipboards" "general", "number_of_clipboards"
), ),
) )
@@ -18,17 +18,17 @@ class command:
def initialize(self, environment): def initialize(self, environment):
self.env = environment self.env = environment
# Use commandBuffer like other commands # Use commandBuffer like other commands
if "progressMonitoring" not in self.env["commandBuffer"]: if "progress_monitoring" not in self.env["commandBuffer"]:
# Check if progress monitoring should be enabled by default from # Check if progress monitoring should be enabled by default from
# settings # settings
try: try:
default_enabled = self.env["runtime"][ default_enabled = self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting_as_bool("sound", "progressMonitoring") ].get_setting_as_bool("sound", "progress_monitoring")
except Exception as e: except Exception as e:
# If setting doesn't exist, default to False # If setting doesn't exist, default to False
default_enabled = False default_enabled = False
self.env["commandBuffer"]["progressMonitoring"] = default_enabled self.env["commandBuffer"]["progress_monitoring"] = default_enabled
self.env["commandBuffer"]["lastProgressTime"] = 0 self.env["commandBuffer"]["lastProgressTime"] = 0
self.env["commandBuffer"]["lastProgressValue"] = -1 self.env["commandBuffer"]["lastProgressValue"] = -1
@@ -40,12 +40,12 @@ class command:
def run(self): def run(self):
# Check if commandBuffer exists # Check if commandBuffer exists
if "progressMonitoring" not in self.env["commandBuffer"]: if "progress_monitoring" not in self.env["commandBuffer"]:
self.env["commandBuffer"]["progressMonitoring"] = False self.env["commandBuffer"]["progress_monitoring"] = False
self.env["commandBuffer"]["lastProgressTime"] = 0 self.env["commandBuffer"]["lastProgressTime"] = 0
self.env["commandBuffer"]["lastProgressValue"] = -1 self.env["commandBuffer"]["lastProgressValue"] = -1
if self.env["commandBuffer"]["progressMonitoring"]: if self.env["commandBuffer"]["progress_monitoring"]:
self.stop_progress_monitoring() self.stop_progress_monitoring()
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("Progress monitoring disabled"), interrupt=True _("Progress monitoring disabled"), interrupt=True
@@ -57,17 +57,17 @@ class command:
) )
def start_progress_monitoring(self): def start_progress_monitoring(self):
self.env["commandBuffer"]["progressMonitoring"] = True self.env["commandBuffer"]["progress_monitoring"] = True
self.env["commandBuffer"]["lastProgressTime"] = time.time() self.env["commandBuffer"]["lastProgressTime"] = time.time()
self.env["commandBuffer"]["lastProgressValue"] = -1 self.env["commandBuffer"]["lastProgressValue"] = -1
# Don't control speech - let user decide with silence_until_prompt # Don't control speech - let user decide with silence_until_prompt
def stop_progress_monitoring(self): def stop_progress_monitoring(self):
self.env["commandBuffer"]["progressMonitoring"] = False self.env["commandBuffer"]["progress_monitoring"] = False
# Don't control speech - progress monitor is beep-only # Don't control speech - progress monitor is beep-only
def detect_progress(self, text): def detect_progress(self, text):
if not self.env["commandBuffer"]["progressMonitoring"]: if not self.env["commandBuffer"]["progress_monitoring"]:
return return
# Skip progress detection if current screen looks like a prompt # Skip progress detection if current screen looks like a prompt
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.core import debug
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _("Read all text line by line from current position")
def run(self):
self.env["runtime"]["OutputManager"].present_text(
"Read-all functionality temporarily disabled",
interrupt=True
)
return
# Check if speechd is available for callbacks
try:
speech_driver = self.env['runtime']['SpeechDriver']
if not (hasattr(speech_driver, '_sd') and speech_driver._sd):
self.env["runtime"]["OutputManager"].present_text(
_("Read all requires speech-dispatcher - not available with current speech driver"),
interrupt=True
)
return
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
f"ReadAllByLine run: Error checking speech driver: {e}",
debug.DebugLevel.ERROR
)
return
# Check if ReadAllManager is available
if "ReadAllManager" not in self.env["runtime"]:
self.env["runtime"]["OutputManager"].present_text(
_("Read all manager not in runtime"),
interrupt=True
)
return
elif not self.env["runtime"]["ReadAllManager"]:
self.env["runtime"]["OutputManager"].present_text(
_("Read all manager is None"),
interrupt=True
)
return
# Start continuous line-by-line reading
if self.env["runtime"]["ReadAllManager"].is_active():
# Stop if already active
self.env["runtime"]["ReadAllManager"].stop_read_all()
else:
# Start line-by-line reading
self.env["runtime"]["ReadAllManager"].start_read_all('line')
def set_callback(self, callback):
pass
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.core import debug
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _("Read all text page by page from current position")
def run(self):
# Check if speechd is available for callbacks
try:
speech_driver = self.env['runtime']['SpeechDriver']
if not (hasattr(speech_driver, '_sd') and speech_driver._sd):
self.env["runtime"]["OutputManager"].present_text(
_("Read all requires speech-dispatcher - not available with current speech driver"),
interrupt=True
)
return
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
f"ReadAllByPage run: Error checking speech driver: {e}",
debug.DebugLevel.ERROR
)
return
# Check if ReadAllManager is available
if "ReadAllManager" not in self.env["runtime"]:
self.env["runtime"]["OutputManager"].present_text(
_("Read all manager not in runtime"),
interrupt=True
)
return
elif not self.env["runtime"]["ReadAllManager"]:
self.env["runtime"]["OutputManager"].present_text(
_("Read all manager is None"),
interrupt=True
)
return
# Start continuous page-by-page reading
if self.env["runtime"]["ReadAllManager"].is_active():
# Stop if already active
self.env["runtime"]["ReadAllManager"].stop_read_all()
else:
# Start page-by-page reading
self.env["runtime"]["ReadAllManager"].start_read_all('page')
def set_callback(self, callback):
pass
@@ -36,11 +36,11 @@ class command:
def update_spell_language(self): def update_spell_language(self):
self.spellChecker = enchant.Dict( self.spellChecker = enchant.Dict(
self.env["runtime"]["SettingsManager"].get_setting( self.env["runtime"]["SettingsManager"].get_setting(
"general", "spellCheckLanguage" "general", "spell_check_language"
) )
) )
self.language = self.env["runtime"]["SettingsManager"].get_setting( self.language = self.env["runtime"]["SettingsManager"].get_setting(
"general", "spellCheckLanguage" "general", "spell_check_language"
) )
def run(self): def run(self):
@@ -51,7 +51,7 @@ class command:
return return
if ( if (
self.env["runtime"]["SettingsManager"].get_setting( self.env["runtime"]["SettingsManager"].get_setting(
"general", "spellCheckLanguage" "general", "spell_check_language"
) )
!= self.language != self.language
): ):
@@ -22,8 +22,8 @@ class command:
return _("Interrupts the current presentation") return _("Interrupts the current presentation")
def run(self): def run(self):
if len(self.env["input"]["prevDeepestInput"]) > len( if len(self.env["input"]["prev_deepest_input"]) > len(
self.env["input"]["currInput"] self.env["input"]["curr_input"]
): ):
return return
self.env["runtime"]["OutputManager"].interrupt_output() self.env["runtime"]["OutputManager"].interrupt_output()
@@ -36,11 +36,11 @@ class command:
return return
self.spellChecker = enchant.Dict( self.spellChecker = enchant.Dict(
self.env["runtime"]["SettingsManager"].get_setting( self.env["runtime"]["SettingsManager"].get_setting(
"general", "spellCheckLanguage" "general", "spell_check_language"
) )
) )
self.language = self.env["runtime"]["SettingsManager"].get_setting( self.language = self.env["runtime"]["SettingsManager"].get_setting(
"general", "spellCheckLanguage" "general", "spell_check_language"
) )
def run(self): def run(self):
@@ -51,7 +51,7 @@ class command:
return return
if ( if (
self.env["runtime"]["SettingsManager"].get_setting( self.env["runtime"]["SettingsManager"].get_setting(
"general", "spellCheckLanguage" "general", "spell_check_language"
) )
!= self.language != self.language
): ):
@@ -25,15 +25,15 @@ class command:
def run(self): def run(self):
self.env["runtime"]["SettingsManager"].set_setting( self.env["runtime"]["SettingsManager"].set_setting(
"general", "general",
"autoPresentIndent", "auto_present_indent",
str( str(
not self.env["runtime"]["SettingsManager"].get_setting_as_bool( not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"general", "autoPresentIndent" "general", "auto_present_indent"
) )
), ),
) )
if self.env["runtime"]["SettingsManager"].get_setting_as_bool( if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"general", "autoPresentIndent" "general", "auto_present_indent"
): ):
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("autoindent enabled"), sound_icon="", interrupt=True _("autoindent enabled"), sound_icon="", interrupt=True
@@ -25,15 +25,15 @@ class command:
def run(self): def run(self):
self.env["runtime"]["SettingsManager"].set_setting( self.env["runtime"]["SettingsManager"].set_setting(
"speech", "speech",
"autoReadIncoming", "auto_read_incoming",
str( str(
not self.env["runtime"]["SettingsManager"].get_setting_as_bool( not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"speech", "autoReadIncoming" "speech", "auto_read_incoming"
) )
), ),
) )
if self.env["runtime"]["SettingsManager"].get_setting_as_bool( if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"speech", "autoReadIncoming" "speech", "auto_read_incoming"
): ):
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("autoread enabled"), sound_icon="", interrupt=True _("autoread enabled"), sound_icon="", interrupt=True
@@ -24,15 +24,15 @@ class command:
def run(self): def run(self):
self.env["runtime"]["SettingsManager"].set_setting( self.env["runtime"]["SettingsManager"].set_setting(
"general", "general",
"autoSpellCheck", "auto_spell_check",
str( str(
not self.env["runtime"]["SettingsManager"].get_setting_as_bool( not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"general", "autoSpellCheck" "general", "auto_spell_check"
) )
), ),
) )
if self.env["runtime"]["SettingsManager"].get_setting_as_bool( if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"general", "autoSpellCheck" "general", "auto_spell_check"
): ):
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("auto spellcheck enabled"), sound_icon="", interrupt=True _("auto spellcheck enabled"), sound_icon="", interrupt=True
@@ -25,7 +25,7 @@ class command:
if self.env["runtime"]["PunctuationManager"].cycle_punctuation(): if self.env["runtime"]["PunctuationManager"].cycle_punctuation():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
self.env["runtime"]["SettingsManager"].get_setting( self.env["runtime"]["SettingsManager"].get_setting(
"general", "punctuationLevel" "general", "punctuation_level"
), ),
interrupt=True, interrupt=True,
ignore_punctuation=True, ignore_punctuation=True,
@@ -23,31 +23,31 @@ class command:
def run(self): def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool( if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"keyboard", "interruptOnKeyPress" "keyboard", "interrupt_on_key_press"
): ):
return return
if self.env["runtime"]["InputManager"].no_key_pressed(): if self.env["runtime"]["InputManager"].no_key_pressed():
return return
if self.env["runtime"]["ScreenManager"].is_screen_change(): if self.env["runtime"]["ScreenManager"].is_screen_change():
return return
if len(self.env["input"]["currInput"]) <= len( if len(self.env["input"]["curr_input"]) <= len(
self.env["input"]["prevInput"] self.env["input"]["prev_input"]
): ):
return return
# if the filter is set # if the filter is set
if ( if (
self.env["runtime"]["SettingsManager"] self.env["runtime"]["SettingsManager"]
.get_setting("keyboard", "interruptOnKeyPressFilter") .get_setting("keyboard", "interrupt_on_key_press_filter")
.strip() .strip()
!= "" != ""
): ):
filter_list = ( filter_list = (
self.env["runtime"]["SettingsManager"] self.env["runtime"]["SettingsManager"]
.get_setting("keyboard", "interruptOnKeyPressFilter") .get_setting("keyboard", "interrupt_on_key_press_filter")
.split(",") .split(",")
) )
for currInput in self.env["input"]["currInput"]: for curr_key in self.env["input"]["curr_input"]:
if currInput not in filter_list: if curr_key not in filter_list:
return return
self.env["runtime"]["OutputManager"].interrupt_output() self.env["runtime"]["OutputManager"].interrupt_output()
@@ -24,14 +24,14 @@ class command:
def run(self): def run(self):
# enabled? # enabled?
active = self.env["runtime"]["SettingsManager"].get_setting_as_int( active = self.env["runtime"]["SettingsManager"].get_setting_as_int(
"keyboard", "charEchoMode" "keyboard", "char_echo_mode"
) )
# 0 = off # 0 = off
if active == 0: if active == 0:
return return
# 2 = caps only # 2 = caps only
if active == 2: if active == 2:
if not self.env["input"]["newCapsLock"]: if not self.env["input"]["new_caps_lock"]:
return return
# big changes are no char (but the value is bigger than one maybe the # big changes are no char (but the value is bigger than one maybe the
# differ needs longer than you can type, so a little strange random # differ needs longer than you can type, so a little strange random
@@ -26,7 +26,7 @@ class command:
def run(self): def run(self):
# is it enabled? # is it enabled?
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool( if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"keyboard", "wordEcho" "keyboard", "word_echo"
): ):
return return
# is navigation? # is navigation?
@@ -39,23 +39,23 @@ class command:
return return
self.spellChecker = enchant.Dict( self.spellChecker = enchant.Dict(
self.env["runtime"]["SettingsManager"].get_setting( self.env["runtime"]["SettingsManager"].get_setting(
"general", "spellCheckLanguage" "general", "spell_check_language"
) )
) )
self.language = self.env["runtime"]["SettingsManager"].get_setting( self.language = self.env["runtime"]["SettingsManager"].get_setting(
"general", "spellCheckLanguage" "general", "spell_check_language"
) )
def run(self): def run(self):
if not initialized: if not initialized:
return return
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool( if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"general", "autoSpellCheck" "general", "auto_spell_check"
): ):
return return
if ( if (
self.env["runtime"]["SettingsManager"].get_setting( self.env["runtime"]["SettingsManager"].get_setting(
"general", "spellCheckLanguage" "general", "spell_check_language"
) )
!= self.language != self.language
): ):
@@ -23,7 +23,7 @@ class command:
def run(self): def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool( if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"keyboard", "charDeleteEcho" "keyboard", "char_delete_echo"
): ):
return return
# detect typing or chilling # detect typing or chilling
@@ -46,7 +46,7 @@ class command:
# echo word insteed of char # echo word insteed of char
if self.env["runtime"]["SettingsManager"].get_setting_as_bool( if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"keyboard", "wordEcho" "keyboard", "word_echo"
): ):
if ( if (
abs( abs(
@@ -71,6 +71,13 @@ class command:
self.env["screen"]["new_cursor"]["y"], self.env["screen"]["new_cursor"]["y"],
self.env["screen"]["new_content_text"], self.env["screen"]["new_content_text"],
) )
# Don't interrupt ongoing auto-read announcements
do_interrupt = True
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"speech", "auto_read_incoming"
):
do_interrupt = False
if curr_char.isspace(): if curr_char.isspace():
# Only announce spaces during pure navigation (arrow keys) # Only announce spaces during pure navigation (arrow keys)
# Check if this is really navigation by looking at input history # Check if this is really navigation by looking at input history
@@ -87,14 +94,14 @@ class command:
char_utils.present_char_for_review( char_utils.present_char_for_review(
self.env, self.env,
curr_char, curr_char,
interrupt=True, interrupt=do_interrupt,
announce_capital=True, announce_capital=True,
flush=False, flush=False,
) )
else: else:
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
curr_char, curr_char,
interrupt=True, interrupt=do_interrupt,
ignore_punctuation=True, ignore_punctuation=True,
announce_capital=True, announce_capital=True,
flush=False, flush=False,
@@ -4,7 +4,7 @@
# Fenrir TTY screen reader # Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors. # By Chrys, Storm Dragon, and contributors.
import time
from fenrirscreenreader.core.i18n import _ from fenrirscreenreader.core.i18n import _
@@ -14,55 +14,157 @@ class command:
def initialize(self, environment): def initialize(self, environment):
self.env = environment self.env = environment
# Initialize tab completion state tracking
if "tabCompletion" not in self.env["commandBuffer"]:
self.env["commandBuffer"]["tabCompletion"] = {
"lastTabTime": 0,
"pendingCompletion": None,
"retryCount": 0
}
def shutdown(self): def shutdown(self):
pass pass
def get_description(self): def get_description(self):
return "No Description found" return _("Announces tab completions when detected")
def _is_recent_tab_input(self):
"""Check if TAB was pressed recently (within 200ms window)"""
current_time = time.time()
tab_detected = False
# Check KEY mode
if self.env["runtime"]["InputManager"].get_shortcut_type() in ["KEY"]:
if (self.env["runtime"]["InputManager"].get_last_deepest_input()
in [["KEY_TAB"]]):
tab_detected = True
self.env["commandBuffer"]["tabCompletion"]["lastTabTime"] = current_time
# Check BYTE mode
elif self.env["runtime"]["InputManager"].get_shortcut_type() in ["BYTE"]:
for currByte in self.env["runtime"]["ByteManager"].get_last_byte_key():
if currByte == 9: # Tab character
tab_detected = True
self.env["commandBuffer"]["tabCompletion"]["lastTabTime"] = current_time
# Check if tab was pressed recently (200ms window)
if not tab_detected:
time_since_tab = current_time - self.env["commandBuffer"]["tabCompletion"]["lastTabTime"]
if time_since_tab <= 0.2: # 200ms window
tab_detected = True
return tab_detected
def _is_flexible_completion_match(self, x_move, delta_text):
"""Use flexible matching instead of strict equality"""
if not delta_text:
return False
delta_len = len(delta_text)
# Exact match (preserve original behavior)
if x_move == delta_len:
return True
# Flexible range: allow ±2 characters difference
# Handles spacing adjustments and unicode width variations
if abs(x_move - delta_len) <= 2 and delta_len > 0:
return True
# For longer completions, allow proportional variance
if delta_len > 10 and abs(x_move - delta_len) <= (delta_len * 0.2):
return True
return False
def _detect_completion_patterns(self, delta_text):
"""Detect common tab completion patterns for improved accuracy"""
if not delta_text:
return False
delta_stripped = delta_text.strip()
# File extension completion
if '.' in delta_stripped and delta_stripped.count('.') <= 2:
return True
# Path completion (contains / or \)
if '/' in delta_stripped or '\\' in delta_stripped:
return True
# Command parameter completion (starts with -)
if delta_stripped.startswith('-') and len(delta_stripped) > 1:
return True
# Word boundary completion (alphanumeric content)
if delta_stripped.isalnum() and len(delta_stripped) >= 2:
return True
return False
def run(self): def run(self):
# try to detect the tab completion by cursor change """Enhanced tab completion detection with improved reliability"""
# Basic cursor movement check (preserve original logic)
x_move = ( x_move = (
self.env["screen"]["new_cursor"]["x"] self.env["screen"]["new_cursor"]["x"]
- self.env["screen"]["old_cursor"]["x"] - self.env["screen"]["old_cursor"]["x"]
) )
if x_move <= 0: if x_move <= 0:
return return
if self.env["runtime"]["InputManager"].get_shortcut_type() in ["KEY"]:
if not ( # Enhanced tab input detection with persistence
self.env["runtime"]["InputManager"].get_last_deepest_input() tab_detected = self._is_recent_tab_input()
in [["KEY_TAB"]]
): # Fallback for non-tab movements (preserve original thresholds)
if x_move < 5: if not tab_detected:
return if x_move < 5:
elif self.env["runtime"]["InputManager"].get_shortcut_type() in [ return
"BYTE"
]: # Screen delta availability check
found = False
for currByte in self.env["runtime"][
"ByteManager"
].get_last_byte_key():
if currByte == 9:
found = True
if not found:
if x_move < 5:
return
# is there any change?
if not self.env["runtime"]["ScreenManager"].is_delta(): if not self.env["runtime"]["ScreenManager"].is_delta():
# If tab was detected but no delta yet, store for potential retry
if tab_detected and self.env["commandBuffer"]["tabCompletion"]["retryCount"] < 2:
self.env["commandBuffer"]["tabCompletion"]["pendingCompletion"] = {
"x_move": x_move,
"timestamp": time.time()
}
self.env["commandBuffer"]["tabCompletion"]["retryCount"] += 1
return return
if not x_move == len(self.env["screen"]["new_delta"]):
return delta_text = self.env["screen"]["new_delta"]
# filter unneded space on word begin
curr_delta = self.env["screen"]["new_delta"] # Enhanced correlation checking with flexible matching
if ( if not self._is_flexible_completion_match(x_move, delta_text):
len(curr_delta.strip()) != len(curr_delta) # Additional pattern-based validation for edge cases
and curr_delta.strip() != "" if not (tab_detected and self._detect_completion_patterns(delta_text)):
): return
# Reset retry counter on successful detection
self.env["commandBuffer"]["tabCompletion"]["retryCount"] = 0
self.env["commandBuffer"]["tabCompletion"]["pendingCompletion"] = None
# Mark that we've handled this delta to prevent duplicate announcements
# This prevents the incoming text handler from also announcing the same content
self.env["commandBuffer"]["tabCompletion"]["lastProcessedDelta"] = delta_text
self.env["commandBuffer"]["tabCompletion"]["lastProcessedTime"] = time.time()
# Text filtering and announcement (preserve original behavior)
curr_delta = delta_text
if (len(curr_delta.strip()) != len(curr_delta) and curr_delta.strip() != ""):
curr_delta = curr_delta.strip() curr_delta = curr_delta.strip()
self.env["runtime"]["OutputManager"].present_text(
curr_delta, interrupt=True, announce_capital=True, flush=False # Don't interrupt ongoing auto-read announcements
) do_interrupt = True
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"speech", "auto_read_incoming"
):
do_interrupt = False
# Enhanced announcement with better handling of empty completions
if curr_delta:
self.env["runtime"]["OutputManager"].present_text(
curr_delta, interrupt=do_interrupt, announce_capital=True, flush=False
)
def set_callback(self, callback): def set_callback(self, callback):
pass pass
@@ -66,8 +66,15 @@ class command:
): ):
return return
# Don't interrupt ongoing auto-read announcements
do_interrupt = True
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"speech", "auto_read_incoming"
):
do_interrupt = False
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
curr_word, interrupt=True, flush=False curr_word, interrupt=do_interrupt, flush=False
) )
def set_callback(self, callback): def set_callback(self, callback):
@@ -30,8 +30,8 @@ class command:
if self.env["runtime"]["ScreenManager"].is_screen_change(): if self.env["runtime"]["ScreenManager"].is_screen_change():
self.lastIdent = 0 self.lastIdent = 0
return return
# this leads to problems in vim -> status line change -> no # Don't announce cursor movements when auto-read is handling incoming text
# announcement, so we do check the lengh as hack # This prevents interrupting ongoing auto-read announcements
if self.env["runtime"]["ScreenManager"].is_delta(): if self.env["runtime"]["ScreenManager"].is_delta():
return return
@@ -44,28 +44,34 @@ class command:
self.env["screen"]["new_cursor"]["y"], self.env["screen"]["new_cursor"]["y"],
self.env["screen"]["new_content_text"], self.env["screen"]["new_content_text"],
) )
# Don't interrupt ongoing auto-read announcements with cursor movement
do_interrupt = True
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"speech", "auto_read_incoming"
):
do_interrupt = False
if curr_line.isspace(): if curr_line.isspace():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("blank"), sound_icon="EmptyLine", interrupt=True, flush=False _("blank"), sound_icon="EmptyLine", interrupt=do_interrupt, flush=False
) )
else: else:
# ident # ident
curr_ident = len(curr_line) - len(curr_line.lstrip()) curr_ident = len(curr_line) - len(curr_line.lstrip())
if self.lastIdent == -1: if self.lastIdent == -1:
self.lastIdent = curr_ident self.lastIdent = curr_ident
do_interrupt = True
if self.env["runtime"]["SettingsManager"].get_setting_as_bool( if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"general", "autoPresentIndent" "general", "auto_present_indent"
): ):
if self.env["runtime"]["SettingsManager"].get_setting_as_int( if self.env["runtime"]["SettingsManager"].get_setting_as_int(
"general", "autoPresentIndentMode" "general", "auto_present_indent_mode"
) in [0, 1]: ) in [0, 1]:
if self.lastIdent != curr_ident: if self.lastIdent != curr_ident:
self.env["runtime"]["OutputManager"].play_frequence( self.env["runtime"]["OutputManager"].play_frequence(
curr_ident * 50, 0.1, interrupt=do_interrupt curr_ident * 50, 0.1, interrupt=do_interrupt
) )
if self.env["runtime"]["SettingsManager"].get_setting_as_int( if self.env["runtime"]["SettingsManager"].get_setting_as_int(
"general", "autoPresentIndentMode" "general", "auto_present_indent_mode"
) in [0, 2]: ) in [0, 2]:
if self.lastIdent != curr_ident: if self.lastIdent != curr_ident:
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
@@ -68,10 +68,10 @@ class command:
# Only beep/announce if indentation level has changed # Only beep/announce if indentation level has changed
if self.env["runtime"]["SettingsManager"].get_setting_as_bool( if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"general", "autoPresentIndent" "general", "auto_present_indent"
) and self.lastIdent != curr_ident: ) and self.lastIdent != curr_ident:
if self.env["runtime"]["SettingsManager"].get_setting_as_int( if self.env["runtime"]["SettingsManager"].get_setting_as_int(
"general", "autoPresentIndentMode" "general", "auto_present_indent_mode"
) in [0, 1]: ) in [0, 1]:
self.env["runtime"]["OutputManager"].play_frequence( self.env["runtime"]["OutputManager"].play_frequence(
curr_ident * 50, 0.1, interrupt=False curr_ident * 50, 0.1, interrupt=False
@@ -23,7 +23,7 @@ class command:
def run(self): def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool( if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "leaveReviewOnCursorChange" "review", "leave_review_on_cursor_change"
): ):
return return
if self.env["runtime"]["CursorManager"].is_review_mode(): if self.env["runtime"]["CursorManager"].is_review_mode():
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import time
from fenrirscreenreader.core.i18n import _
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _("Handles delayed retry for tab completion detection")
def run(self):
"""Check for and process pending tab completions with slight delay"""
# Only process if we have tab completion state
if "tabCompletion" not in self.env["commandBuffer"]:
return
tab_state = self.env["commandBuffer"]["tabCompletion"]
pending = tab_state.get("pendingCompletion")
if not pending:
return
current_time = time.time()
# Process pending completion after 50ms delay
if current_time - pending["timestamp"] < 0.05:
return
# Check if screen delta is now available
if not self.env["runtime"]["ScreenManager"].is_delta():
# Give up after 200ms total
if current_time - pending["timestamp"] > 0.2:
tab_state["pendingCompletion"] = None
tab_state["retryCount"] = 0
return
# Process the delayed completion
delta_text = self.env["screen"]["new_delta"]
x_move = pending["x_move"]
# Use the same flexible matching logic as main tab completion
match_found = self._is_flexible_completion_match(x_move, delta_text)
if not match_found:
# Try pattern-based detection as final fallback
match_found = self._detect_completion_patterns(delta_text)
if match_found and delta_text:
# Mark that we've handled this delta to prevent duplicate announcements
tab_state["lastProcessedDelta"] = delta_text
tab_state["lastProcessedTime"] = current_time
# Filter and announce the completion
curr_delta = delta_text
if (len(curr_delta.strip()) != len(curr_delta) and
curr_delta.strip() != ""):
curr_delta = curr_delta.strip()
if curr_delta:
self.env["runtime"]["OutputManager"].present_text(
curr_delta, interrupt=True, announce_capital=True, flush=False
)
# Clear pending completion
tab_state["pendingCompletion"] = None
tab_state["retryCount"] = 0
def _is_flexible_completion_match(self, x_move, delta_text):
"""Use flexible matching (duplicated from main command for heartbeat use)"""
if not delta_text:
return False
delta_len = len(delta_text)
# Exact match
if x_move == delta_len:
return True
# Flexible range: allow ±2 characters difference
if abs(x_move - delta_len) <= 2 and delta_len > 0:
return True
# For longer completions, allow proportional variance
if delta_len > 10 and abs(x_move - delta_len) <= (delta_len * 0.2):
return True
return False
def _detect_completion_patterns(self, delta_text):
"""Detect common tab completion patterns (duplicated from main command)"""
if not delta_text:
return False
delta_stripped = delta_text.strip()
# File extension completion
if '.' in delta_stripped and delta_stripped.count('.') <= 2:
return True
# Path completion
if '/' in delta_stripped or '\\' in delta_stripped:
return True
# Command parameter completion
if delta_stripped.startswith('-') and len(delta_stripped) > 1:
return True
# Word boundary completion
if delta_stripped.isalnum() and len(delta_stripped) >= 2:
return True
return False
def set_callback(self, callback):
pass
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import time
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return ""
def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"speech", "auto_read_incoming"
):
return
if "pendingPromptText" not in self.env["commandBuffer"]:
return
pending_text = self.env["commandBuffer"]["pendingPromptText"]
if not pending_text:
return
pending_time = self.env["commandBuffer"].get("pendingPromptTime", 0)
delay = self.env["runtime"]["SettingsManager"].get_setting_as_float(
"speech", "batch_flush_interval"
)
if time.time() - pending_time < delay:
return
self.env["runtime"]["OutputManager"].present_text(
pending_text, interrupt=False, flush=False
)
self.env["commandBuffer"]["pendingPromptText"] = ""
def set_callback(self, callback):
pass
@@ -22,31 +22,31 @@ class command:
def run(self): def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool( if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"keyboard", "interruptOnKeyPress" "keyboard", "interrupt_on_key_press"
): ):
return return
if self.env["runtime"]["InputManager"].no_key_pressed(): if self.env["runtime"]["InputManager"].no_key_pressed():
return return
if self.env["runtime"]["ScreenManager"].is_screen_change(): if self.env["runtime"]["ScreenManager"].is_screen_change():
return return
if len(self.env["input"]["currInput"]) <= len( if len(self.env["input"]["curr_input"]) <= len(
self.env["input"]["prevInput"] self.env["input"]["prev_input"]
): ):
return return
# if the filter is set # if the filter is set
if ( if (
self.env["runtime"]["SettingsManager"] self.env["runtime"]["SettingsManager"]
.get_setting("keyboard", "interruptOnKeyPressFilter") .get_setting("keyboard", "interrupt_on_key_press_filter")
.strip() .strip()
!= "" != ""
): ):
filter_list = ( filter_list = (
self.env["runtime"]["SettingsManager"] self.env["runtime"]["SettingsManager"]
.get_setting("keyboard", "interruptOnKeyPressFilter") .get_setting("keyboard", "interrupt_on_key_press_filter")
.split(",") .split(",")
) )
for currInput in self.env["input"]["currInput"]: for curr_key in self.env["input"]["curr_input"]:
if currInput not in filter_list: if curr_key not in filter_list:
return return
self.env["runtime"]["OutputManager"].interrupt_output() self.env["runtime"]["OutputManager"].interrupt_output()
@@ -24,7 +24,7 @@ class command:
def run(self): def run(self):
if self.env["runtime"]["InputManager"].no_key_pressed(): if self.env["runtime"]["InputManager"].no_key_pressed():
return return
if len(self.env["input"]["prevInput"]) > 0: if len(self.env["input"]["prev_input"]) > 0:
return return
if not self.env["commandBuffer"]["enableSpeechOnKeypress"]: if not self.env["commandBuffer"]["enableSpeechOnKeypress"]:
return return
@@ -23,11 +23,11 @@ class command:
def run(self): def run(self):
if ( if (
self.env["input"]["oldCapsLock"] self.env["input"]["old_caps_lock"]
== self.env["input"]["newCapsLock"] == self.env["input"]["new_caps_lock"]
): ):
return return
if self.env["input"]["newCapsLock"]: if self.env["input"]["new_caps_lock"]:
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("Capslock on"), interrupt=True _("Capslock on"), interrupt=True
) )
@@ -23,11 +23,11 @@ class command:
def run(self): def run(self):
if ( if (
self.env["input"]["oldScrollLock"] self.env["input"]["old_scroll_lock"]
== self.env["input"]["newScrollLock"] == self.env["input"]["new_scroll_lock"]
): ):
return return
if self.env["input"]["newScrollLock"]: if self.env["input"]["new_scroll_lock"]:
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("Scrolllock on"), interrupt=True _("Scrolllock on"), interrupt=True
) )
@@ -22,14 +22,25 @@ class command:
return "No description found" return "No description found"
def run(self): def run(self):
if self.env["input"]["oldNumLock"] == self.env["input"]["newNumLock"]: if self.env["input"]["old_num_lock"] == self.env["input"]["new_num_lock"]:
return return
# Only announce numlock changes if an actual numlock key was pressed # Only announce numlock changes if an actual numlock key was pressed
# This prevents spurious announcements from external numpad automatic state changes # AND the LED state actually changed (some numpads send spurious NUMLOCK events)
current_input = self.env["input"]["currInput"] current_input = self.env["input"]["curr_input"]
if current_input and "KEY_NUMLOCK" in current_input:
if self.env["input"]["newNumLock"]: # Check if this is a genuine numlock key press by verifying:
# 1. KEY_NUMLOCK is in the current input sequence
# 2. The LED state has actually changed
# 3. This isn't just a side effect from a KP_ key (which some buggy numpads do)
is_genuine_numlock = (
current_input and
"KEY_NUMLOCK" in current_input and
not any(key.startswith("KEY_KP") for key in current_input if isinstance(key, str))
)
if is_genuine_numlock:
if self.env["input"]["new_num_lock"]:
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("Numlock on"), interrupt=True _("Numlock on"), interrupt=True
) )
@@ -23,7 +23,7 @@ class command:
def run(self): def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool( if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "leaveReviewOnScreenChange" "review", "leave_review_on_screen_change"
): ):
return return
self.env["runtime"]["CursorManager"].clear_review_cursor() self.env["runtime"]["CursorManager"].clear_review_cursor()
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.core import debug
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _(
"TUI focus mode handler - suppresses screen update spam "
"for interactive TUI applications"
)
def run(self):
# Check if TUI mode is enabled
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"focus", "tui"
):
return
# TUI mode is active - set suppression flag for incoming handler
# This prevents the 70000-incoming.py command from announcing
# screen updates
self.env["commandBuffer"]["tuiSuppressIncoming"] = True
self.env["runtime"]["DebugManager"].write_debug_out(
"tui_focus_handler: TUI mode active, suppressing incoming text",
debug.DebugLevel.INFO
)
def set_callback(self, callback):
pass
@@ -25,8 +25,8 @@ class command:
# Only run if progress monitoring is enabled # Only run if progress monitoring is enabled
try: try:
if ( if (
"progressMonitoring" in self.env["commandBuffer"] "progress_monitoring" in self.env["commandBuffer"]
and self.env["commandBuffer"]["progressMonitoring"] and self.env["commandBuffer"]["progress_monitoring"]
): ):
# Check if current line is a prompt - if so, reset progress # Check if current line is a prompt - if so, reset progress
# state # state
@@ -40,13 +40,19 @@ class command:
): ):
self.detect_progress(self.env["screen"]["new_delta"]) self.detect_progress(self.env["screen"]["new_delta"])
except Exception as e: except Exception as e:
# Silently ignore errors to avoid disrupting normal operation # Log errors for debugging instead of silently ignoring
pass self.env["runtime"]["DebugManager"].write_debug_out(
"Progress detector error: " + str(e),
debug.DebugLevel.ERROR,
)
def is_real_progress_update(self): def is_real_progress_update(self):
"""Check if this is a real progress update vs screen change/window switch""" """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 the screen/application changed, it's not a progress update
if self.env["runtime"]["ScreenManager"].is_screen_change(): if self.env["runtime"]["ScreenManager"].is_screen_change():
self.env["runtime"]["DebugManager"].write_debug_out(
"Progress filter: screen change detected", debug.DebugLevel.INFO
)
return False return False
# If there was a large cursor movement, it's likely navigation, not # If there was a large cursor movement, it's likely navigation, not
@@ -62,19 +68,40 @@ class command:
) )
# Large movements suggest navigation, not progress output # Large movements suggest navigation, not progress output
if y_move > 2 or x_move > 20: if y_move > 2 or x_move > 20:
self.env["runtime"]["DebugManager"].write_debug_out(
f"Progress filter: large cursor move y={y_move} x={x_move}",
debug.DebugLevel.INFO,
)
return False return False
# Check if delta is too large (screen change) vs small incremental # Check if delta is too large (screen change) vs small incremental
# updates # updates
delta_length = len(self.env["screen"]["new_delta"]) delta_text = self.env["screen"]["new_delta"]
delta_length = len(delta_text)
if ( if (
delta_length > 200 delta_length > 200
): # Allow longer progress lines like Claude Code's status ): # Allow longer progress lines like Claude Code's status
self.env["runtime"]["DebugManager"].write_debug_out(
f"Progress filter: delta too long ({delta_length})",
debug.DebugLevel.INFO,
)
return False
# If delta contains newlines and is substantial, let incoming handler
# deal with it to avoid interfering with multi-line text output
if '\n' in delta_text and delta_length > 50:
self.env["runtime"]["DebugManager"].write_debug_out(
f"Progress filter: multiline delta ({delta_length} chars)",
debug.DebugLevel.INFO,
)
return False return False
# Check if current line looks like a prompt - progress unlikely during # Check if current line looks like a prompt - progress unlikely during
# prompts # prompts
if self.is_current_line_prompt(): if self.is_current_line_prompt():
self.env["runtime"]["DebugManager"].write_debug_out(
"Progress filter: prompt detected", debug.DebugLevel.INFO
)
return False return False
return True return True
@@ -270,7 +297,7 @@ class command:
self.env["commandBuffer"]["lastProgressTime"] = current_time self.env["commandBuffer"]["lastProgressTime"] = current_time
return return
# Pattern 5: Braille progress indicators # Pattern 5: Braille spinner indicators
braille_match = re.search(r'[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⡿⣟⣯⣷⣾⣽⣻⢿]', text) braille_match = re.search(r'[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⡿⣟⣯⣷⣾⣽⣻⢿]', text)
if braille_match: if braille_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0: if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
@@ -278,27 +305,115 @@ class command:
self.env["commandBuffer"]["lastProgressTime"] = current_time self.env["commandBuffer"]["lastProgressTime"] = current_time
return return
# Pattern 6: Claude Code progress indicators # Pattern 6: Claude Code working indicators (various symbols + activity text + "esc/ctrl+c to interrupt")
claude_progress_match = re.search(r'^[·✶✢✻*]\s+[\w\s-]+[…\.]*\s*\(esc to interrupt\)\s*$', text) # Matches any: [symbol] [Task description]… (... to interrupt ...)
# Symbols include: * ✢ ✽ ✶ ✻ · • ◦ ○ ● ◆ and similar decorative characters
# Example: ✽ Reviewing script for issues… (ctrl+c to interrupt · 33s · ↑ 1.6k tokens · thought for 4s)
claude_progress_match = re.search(
r'[*✢✽✶✻·•◦○●◆]\s+\w+.*?…\s*\(.*(?:esc|ctrl\+c) to interrupt.*\)',
text,
re.IGNORECASE,
)
if claude_progress_match: if claude_progress_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
self.env["runtime"]["DebugManager"].write_debug_out(
"Playing Claude Code activity beep",
debug.DebugLevel.INFO,
)
self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time
return
# Pattern 6b: Claude Code tool invocation indicators (● Tool Name(...))
# Example: ● Web Search("query here")
tool_invocation_match = re.search(
r'[●○◉•◦]\s+(?:Web\s*Search|Read|Write|Edit|Bash|Glob|Grep|Task|WebFetch)\s*\(',
text,
re.IGNORECASE,
)
if tool_invocation_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
self.env["runtime"]["DebugManager"].write_debug_out(
"Playing Claude Code tool invocation beep",
debug.DebugLevel.INFO,
)
self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time
return
# Pattern 6c: Bullet/white bullet activity lines (•/◦ ...)
bullet_activity_match = re.search(
(
r'^\s*[•◦]\s+.*(?:…|\.{3,}|\b(?:thinking|working|processing|'
r'analyzing|searching|reading|writing|planning|running|'
r'executing|updating|building|installing|compiling|downloading|'
r'reviewing|generating|responding|applying|fixing|editing|'
r'creating|preparing|checking|opening|loading|fetching|'
r'retrieving|scanning|indexing|summarizing)\b)'
),
text,
re.IGNORECASE,
)
if bullet_activity_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
self.env["runtime"]["DebugManager"].write_debug_out(
"Playing bullet activity beep",
debug.DebugLevel.INFO,
)
self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time
return
# Pattern 7: Moon phase spinner indicators
moon_match = re.search(r'[🌑🌒🌓🌔🌕🌖🌗🌘]', text)
if moon_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0: if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
self.play_activity_beep() self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time self.env["commandBuffer"]["lastProgressTime"] = current_time
return return
# Pattern 7: Moon phase progress indicators # Pattern 8: Thinking/processing with timing (🔄 Thinking... 23s)
moon_match = re.search(r'[🌑🌒🌓🌔🌕🌖🌗🌘]', text) thinking_match = re.search(r'🔄[^\w]*(?:thinking|processing|working|analyzing)[^\d]*(\d+)s?\b', text, re.IGNORECASE)
if moon_match: if thinking_match:
moon_phases = { # Extract timing value for activity beep frequency adjustment
'🌑': 0, '🌒': 12.5, '🌓': 25, '🌔': 37.5, seconds = int(thinking_match.group(1))
'🌕': 50, '🌖': 62.5, '🌗': 75, '🌘': 87.5 # Use slightly longer interval for thinking patterns to avoid spam
} thinking_interval = 1.5 if seconds < 10 else 2.0
moon_char = moon_match.group(0) if (
if moon_char in moon_phases: current_time - self.env["commandBuffer"]["lastProgressTime"]
percentage = moon_phases[moon_char] >= thinking_interval
if percentage != self.env["commandBuffer"]["lastProgressValue"]: ):
self.play_progress_tone(percentage) self.env["runtime"]["DebugManager"].write_debug_out(
self.env["commandBuffer"]["lastProgressValue"] = percentage f"Playing thinking activity beep (timing: {seconds}s)",
debug.DebugLevel.INFO,
)
self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time
return
# Pattern 9: Half-circle/circle progress indicators (◐ ◓ ◒ ◑)
circle_match = re.search(r'[◐◓◒◑]', text)
if circle_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time
return
# Pattern 10: Task status indicators (● Task Output, ○ Task Running, etc.)
# Matches bullet points with task-related status text
task_status_match = re.search(
r'[●○◉]\s+(?:Task\s+)?(?:Output|Running|Pending|Working|Processing)\s+[a-zA-Z0-9]+',
text,
re.IGNORECASE,
)
if task_status_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.5:
self.env["runtime"]["DebugManager"].write_debug_out(
"Playing task status activity beep",
debug.DebugLevel.INFO,
)
self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time
return return
def play_progress_tone(self, percentage): def play_progress_tone(self, percentage):
@@ -158,6 +158,16 @@ class command:
def _restore_speech(self): def _restore_speech(self):
"""Helper method to restore speech when prompt is detected""" """Helper method to restore speech when prompt is detected"""
# If speech is already enabled, just clear flags to avoid unnecessary
# interrupts on prompt return
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"speech", "enabled"
):
self.env["commandBuffer"]["silenceUntilPrompt"] = False
if "enableSpeechOnKeypress" in self.env["commandBuffer"]:
self.env["commandBuffer"]["enableSpeechOnKeypress"] = False
return
# Disable silence mode # Disable silence mode
self.env["commandBuffer"]["silenceUntilPrompt"] = False self.env["commandBuffer"]["silenceUntilPrompt"] = False
# Also disable the keypress-based speech restoration since we're # Also disable the keypress-based speech restoration since we're
@@ -4,13 +4,17 @@
# Fenrir TTY screen reader # Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors. # By Chrys, Storm Dragon, and contributors.
import time
from fenrirscreenreader.core.i18n import _ from fenrirscreenreader.core.i18n import _
class command: class command:
def __init__(self): def __init__(self):
pass self._update_times = []
self._line_count_times = []
self._batched_text = []
self._last_flush_time = 0
self._in_flood_mode = False
def initialize(self, environment): def initialize(self, environment):
self.env = environment self.env = environment
@@ -19,17 +23,112 @@ class command:
pass pass
def get_description(self): def get_description(self):
return "No Description found" return _("Announces incoming text changes")
def _reset_flood_state(self):
self._update_times = []
self._line_count_times = []
self._batched_text = []
self._last_flush_time = 0
self._in_flood_mode = False
def _is_rapid_updates(self):
current_time = time.time()
window = self.env["runtime"]["SettingsManager"].get_setting_as_float(
"speech", "rapid_update_window"
)
threshold = self.env["runtime"]["SettingsManager"].get_setting_as_int(
"speech", "rapid_update_threshold"
)
self._update_times = [
ts for ts in self._update_times if current_time - ts < window
]
self._update_times.append(current_time)
return len(self._update_times) >= threshold
def _is_high_volume(self, delta_text):
current_time = time.time()
window = self.env["runtime"]["SettingsManager"].get_setting_as_float(
"speech", "rapid_update_window"
)
threshold = self.env["runtime"]["SettingsManager"].get_setting_as_int(
"speech", "flood_line_threshold"
)
line_count = max(1, delta_text.count("\n") + 1)
self._line_count_times = [
(ts, count)
for ts, count in self._line_count_times
if current_time - ts < window
]
self._line_count_times.append((current_time, line_count))
total_lines = sum(count for _, count in self._line_count_times)
return total_lines >= threshold
def _add_to_batch(self, text):
new_lines = text.splitlines()
if text.endswith("\n"):
new_lines.append("")
self._batched_text.extend(new_lines)
max_lines = self.env["runtime"]["SettingsManager"].get_setting_as_int(
"speech", "max_batch_lines"
)
if len(self._batched_text) > max_lines:
self._batched_text = self._batched_text[-max_lines:]
def _flush_batch(self):
if not self._batched_text:
return
text = "\n".join(self._batched_text)
self._batched_text = []
self._last_flush_time = time.time()
self.env["runtime"]["OutputManager"].present_text(
text, interrupt=False, flush=False
)
def _was_handled_by_tab_completion(self, delta_text):
"""Check if this delta was already handled by tab completion to avoid duplicates"""
if "tabCompletion" not in self.env["commandBuffer"]:
return False
tab_state = self.env["commandBuffer"]["tabCompletion"]
# Check if this exact delta was processed recently by tab completion
if (tab_state.get("lastProcessedDelta") == delta_text and
tab_state.get("lastProcessedTime")):
# Only suppress if processed within the last 50ms to avoid stale suppression
# Reduced from 100ms to minimize false positives with rapid multi-line updates
time_since_processed = time.time() - tab_state["lastProcessedTime"]
if time_since_processed <= 0.05:
return True
return False
def run(self): def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool( if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"speech", "autoReadIncoming" "speech", "auto_read_incoming"
): ):
return return
# is there something to read? # is there something to read?
if not self.env["runtime"]["ScreenManager"].is_delta(ignoreSpace=True): if not self.env["runtime"]["ScreenManager"].is_delta(ignoreSpace=True):
return return
delta_text = self.env["screen"]["new_delta"]
if self.env["runtime"]["ScreenManager"].is_screen_change():
self._reset_flood_state()
# Skip if tab completion already handled this delta
if self._was_handled_by_tab_completion(delta_text):
return
# this must be a keyecho or something # this must be a keyecho or something
# if len(self.env['screen']['new_delta'].strip(' \n\t')) <= 1: # if len(self.env['screen']['new_delta'].strip(' \n\t')) <= 1:
x_move = abs( x_move = abs(
@@ -41,14 +140,37 @@ class command:
- self.env["screen"]["old_cursor"]["y"] - self.env["screen"]["old_cursor"]["y"]
) )
if (x_move >= 1) and x_move == len(self.env["screen"]["new_delta"]): if (x_move >= 1) and x_move == len(delta_text):
# if len(self.env['screen']['new_delta'].strip(' \n\t0123456789')) # if len(self.env['screen']['new_delta'].strip(' \n\t0123456789'))
# <= 2: # <= 2:
if "\n" not in self.env["screen"]["new_delta"]: if "\n" not in delta_text:
return return
rapid = self._is_rapid_updates()
high_volume = self._is_high_volume(delta_text)
if (rapid and high_volume) or self._in_flood_mode:
if not self._in_flood_mode:
self._last_flush_time = time.time()
self._in_flood_mode = True
self._add_to_batch(delta_text)
interval = self.env["runtime"][
"SettingsManager"
].get_setting_as_float("speech", "batch_flush_interval")
if time.time() - self._last_flush_time >= interval:
self._flush_batch()
if not rapid or not high_volume:
if self._batched_text:
self._flush_batch()
self._in_flood_mode = False
return
# print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta'])) # print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta']))
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
self.env["screen"]["new_delta"], interrupt=False, flush=False delta_text, interrupt=False, flush=False
) )
def set_callback(self, callback): def set_callback(self, callback):
@@ -1,66 +0,0 @@
#!/usr/bin/env python3
import time
from fenrirscreenreader.core.i18n import _
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return "No Description found"
def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"promote", "enabled"
):
return
if (
self.env["runtime"]["SettingsManager"]
.get_setting("promote", "list")
.strip(" \t\n")
== ""
):
return
if int(time.time() - self.env["input"]["lastInputTime"]) < self.env[
"runtime"
]["SettingsManager"].get_setting_as_int(
"promote", "inactiveTimeoutSec"
):
return
if (
len(
self.env["runtime"]["SettingsManager"].get_setting(
"promote", "list"
)
)
== 0
):
return
for promote in (
self.env["runtime"]["SettingsManager"]
.get_setting("promote", "list")
.split(",")
):
if promote in self.env["screen"]["new_delta"]:
self.env["runtime"]["OutputManager"].play_sound_icon(
"PromotedText"
)
self.env["input"]["lastInputTime"] = time.time()
return
def set_callback(self, callback):
pass
@@ -1 +1 @@
# Emoji VMenu category # Emoji VMenu category
@@ -1 +1 @@
# Flags emoji subcategory # Flags emoji subcategory
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added Canada flag to clipboard", "Added Canada flag to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added UK flag to clipboard", "Added UK flag to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added USA flag to clipboard", "Added USA flag to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -1 +1 @@
# Food emoji subcategory # Food emoji subcategory
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added red apple to clipboard", "Added red apple to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added avocado to clipboard", "Added avocado to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added beer to clipboard", "Added beer to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added birthday cake to clipboard", "Added birthday cake to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added coffee to clipboard", "Added coffee to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added donut to clipboard", "Added donut to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added hamburger to clipboard", "Added hamburger to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added pizza to clipboard", "Added pizza to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added strawberry to clipboard", "Added strawberry to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added taco to clipboard", "Added taco to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -1 +1 @@
# Holidays emoji subcategory # Holidays emoji subcategory
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added alien monster to clipboard", "Added alien monster to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added bat to clipboard", "Added bat to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added black cat to clipboard", "Added black cat to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added bunny to clipboard", "Added bunny to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added mage to clipboard", "Added mage to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added Christmas tree to clipboard", "Added Christmas tree to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added coffin to clipboard", "Added coffin to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added Easter egg to clipboard", "Added Easter egg to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added fireworks to clipboard", "Added fireworks to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added ghost to clipboard", "Added ghost to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added gift to clipboard", "Added gift to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added jack o'lantern to clipboard", "Added jack o'lantern to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added mummy to clipboard", "Added mummy to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added Santa to clipboard", "Added Santa to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added shamrock to clipboard", "Added shamrock to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added skull to clipboard", "Added skull to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added snowman to clipboard", "Added snowman to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added spider to clipboard", "Added spider to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added turkey to clipboard", "Added turkey to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added vampire to clipboard", "Added vampire to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added spider web to clipboard", "Added spider web to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added witch to clipboard", "Added witch to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added zombie to clipboard", "Added zombie to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )

Some files were not shown because too many files have changed in this diff Show More