97 Commits
Author SHA1 Message Date
Storm Dragon 9e6e806947 Latest Codex app exposed a bug where redrawing the screen could cause speech to slip through even when it's turned off. Hopefully fixed now. 2026-08-21 18:36:04 -04:00
Storm Dragon 45019091ab I think I fixed the import and export clipboard manual buttons. They work for me for now at least. 2026-08-17 03:25:57 -04:00
Storm Dragon 68462d00cb Fixed crash bug. Wide characters were sometimes causing an index out of range error. Now they are just treated as spaces. 2026-08-17 02:26:04 -04:00
Storm Dragon 93a2745065 Move auxiliary work off the main event loop
Run voice discovery, voice testing, clipboard I/O, and external scripts in isolated background worker lanes. Deliver results through Fenrir's event queue so manager state remains owned by the main loop.
2026-08-14 23:29:15 -04:00
Storm Dragon 4ed431640e Improve tutorial help navigation 2026-08-13 09:29:43 -04:00
Storm Dragon e7f69b2159 Fenrir should no longer fail to report its current speech-dispatcher module. 2026-08-13 08:32:47 -04:00
Storm Dragon a2e4a8a086 Improve review spelling shortcuts 2026-08-13 00:42:13 -04:00
Storm Dragon cca688328b Support modifier-first Fenrir shortcuts 2026-08-12 23:42:01 -04:00
Storm Dragon ee441c319a Improved progress bar detections. 2026-08-06 13:49:53 -04:00
Storm Dragon 7593c8d0b3 Fix to socket routing for self-voicing. 2026-07-30 04:52:52 -04:00
Storm Dragon 122a6a38b3 Fix Unix remote watchdog socket leak 2026-07-29 14:13:53 -04:00
Storm Dragon 76b8f2f2cf Very scary test here. Trying to see if we can boost speed on very large terminals. 2026-07-26 22:01:11 -04:00
Storm Dragon d81565fbf5 Still attempting to kill off this delay. 2026-07-26 21:49:20 -04:00
Storm Dragon 186512f79a Another try for lag. 2026-07-26 21:42:53 -04:00
Storm Dragon 17b6e6f909 Attempt to decrease dectalk lag. Hoping you don't have to send settings over every time you want it to say something. 2026-07-26 21:34:04 -04:00
Storm Dragon fac3065c6a More tweaks to dectalk driver. Think we're nearly there. Hopefully. 2026-07-26 21:24:28 -04:00
Storm Dragon e93aba109a Fixes with the dectalk driver with rate, etc. 2026-07-26 21:10:39 -04:00
Storm Dragon 11a4244cc3 Fixed some errors with my dectalk protocol implementation. Thanks Codex. :) 2026-07-26 20:59:14 -04:00
Storm Dragon 9ac2e9e844 Found and hopefullyfixed a bug with fenrir watchdog. 2026-07-26 20:44:50 -04:00
Storm Dragon c863245c24 More attempts at getting this usb dectalk working. 2026-07-26 20:18:17 -04:00
Storm Dragon 45bd33f756 Improvements to Dectalk driver. 2026-07-26 19:44:10 -04:00
Storm Dragon 6d9260e79b Fixed 2 speech-dispatcher bugs. First, multiword voice names were not available. Next, when the module changed the old voice name was carried in causing some modules to fail. 2026-07-25 04:36:39 -04:00
Storm Dragon 385356e745 Prevent incoming output from bypassing auto-read toggle 2026-07-19 15:59:51 -04:00
Storm Dragon 28e4f8ab0c Progress bar beeps now work on ffmpeg output. 2026-07-12 15:06:44 -04:00
lilmike 4d94b56ee9 Fix very high cpu usage when a console fenrir under root and an x fenrir under your user are running at the same time. 2026-07-01 23:44:12 -07:00
Storm Dragon eb93a8aa62 Fixed a typo in tests/unit/test_hardware_speech_drivers.py. It was reading 9 bytes while comparing against a 10-byte payload. Thanks Samuel Thibault for finding and pointing this out. 2026-06-27 16:59:05 -04:00
Storm Dragon ad863ad706 Add ability for X clibpard and Fenrir's clipboard to stay in sync. 2026-06-27 16:55:00 -04:00
Storm Dragon 447e2cc896 Missed one commented setting. 2026-06-18 10:51:44 -04:00
Storm Dragon 2b7d205f06 Fixed some half-completed work in generic drivers. Minor -x bug fixes. 2026-06-18 10:35:48 -04:00
Storm Dragon fd5fe5b328 More progressbar updates. Removed Claud specific progress bar detection, hopefully caught now by generic progress bar updates. They do change it all the time, so it may work, but shouldn't be expected to do so. 2026-06-01 03:04:47 -04:00
Storm Dragon 4ed3f4d6ab Fixed version. 2026-05-31 22:37:59 -04:00
Storm Dragon 2cb83632f9 Fixed keyboard handling regression. 2026-05-31 22:36:35 -04:00
Storm Dragon 0c4fe50606 Updated log names. Should be easier to find if you forget to delete old logs. Now just called fenrir.log, we don't tend to need to have multiple logs anyway. 2026-05-30 14:16:23 -04:00
Storm Dragon 15f2435749 Hopefully fix some weirdness on tab completeion where it would read the entire screen instead of suggested tab completions. 2026-05-30 13:56:25 -04:00
Storm Dragon 3897b63068 Iproved logging for startup flags. 2026-05-29 20:22:06 -04:00
Storm Dragon f1a8e6af21 Fixed long standing bug where bottom of screen played for both top and bottom, found a couple other things that were off in the process. 2026-05-29 19:50:38 -04:00
Storm Dragon bd54ec0edb Fixed version. 2026-05-24 17:14:58 -04:00
Storm Dragon b9518f52ec Vmenu fixed I think. Hopefully last thing before new version. 2026-05-24 17:13:38 -04:00
Storm Dragon c143c9a561 Found a vmenu bug in -x. I thought we were close to a new release... 2026-05-24 17:03:41 -04:00
Storm Dragon 7e2f927596 fixed version. 2026-05-24 14:15:02 -04:00
Storm Dragon 788e678ed6 Attempted fix for some progress bars that were being skipped by progress bar detection. 2026-05-24 14:13:29 -04:00
Storm Dragon ea89e90c2f Merge branch 'testing' Hopefully final release candidate for the new version. 2026-05-23 18:59:14 -04:00
Storm Dragon ce43d64e77 Removed auto as a hardware synth device option. It was too flakey. 2026-05-23 18:58:55 -04:00
Storm Dragon 618987546a Adjust timeout for auto detection. I forgot these devices would be slow because most of them are very old with much less speed than would be expected today. 2026-05-23 18:41:42 -04:00
Storm Dragon 604221a29d Attempt to make auto at least somewhat more reliable. Recommend that device be explicitly set if possible. 2026-05-23 18:23:58 -04:00
Storm Dragon 89b85c6f17 Hardware synth code now verified working. New release candidate. 2026-05-23 18:05:55 -04:00
Storm Dragon 6e3d7fee94 Parse the settings correctly lol. 2026-05-23 17:57:02 -04:00
Storm Dragon 089850ac18 More hw synth refinement. 2026-05-23 17:39:16 -04:00
Storm Dragon 5b7c08260a Another iteration based on feedback from hardware synth testing. 2026-05-23 17:23:52 -04:00
Storm Dragon d4b2fec1db speculative fixes for hardware speech. 2026-05-23 17:10:46 -04:00
Storm Dragon 1f7aa99cc0 Release candidate. 2026-05-23 16:13:51 -04:00
Storm Dragon d853e1b24d Fixed the -x keyboard problem for real this time I'm pretty sure. 2026-05-22 20:23:31 -04:00
Storm Dragon e8bc34eaf5 Merge bug fixes, fix version. 2026-05-21 01:08:27 -04:00
Storm Dragon f84167a7fb Of course, soon as I feel things are stable enough to merge to master bugs come crawling out of the woodwork. Fix for being sure Fenrir switches out of its modal mode completely when leaving speech history. 2026-05-21 01:07:01 -04:00
Storm Dragon 29a6c3eb42 A few teaks. Maybe close to actual release. 2026-05-21 00:50:20 -04:00
Storm Dragon ac7348895f Speech history added, bound to fenrir+control+h. 2026-05-20 21:02:56 -04:00
Storm Dragon 4caef89f6b More work on sockets attempt to get socket command to standard daemon.sock to a running instance. Also, fixed a long standing misspelling in daemon, was deamon, so if your scripts that self-voice or whatever with fenrir no longer work, this is why, please update scripts to the new, correct, daemon.sock. 2026-05-20 20:11:21 -04:00
Storm Dragon 8467bd74c3 New hardware synth support added. Untested, so consider this experimental. 2026-05-20 18:02:51 -04:00
Storm Dragon f09437ea60 Speculative fix for a sometimes speech crash bug. 2026-05-19 01:54:09 -04:00
Storm Dragon 19194e73fc Fenrir was not cleaning up after itself properly. Fixed several possible stale file bugs and hopefully this problem is now gone. 2026-05-18 17:59:47 -04:00
Storm Dragon 096919a2da Hopefully fixed bug where keys like super weren't being forwarded with bypass mode. 2026-05-14 21:45:30 -04:00
Storm Dragon a8e4d7bb2a Forward keypress bug fixed. 2026-05-14 20:10:11 -04:00
Storm Dragon 8966275071 Missed a couple of keys in translation for -x. 2026-05-13 23:39:15 -04:00
Storm Dragon c48a9a6731 Fixed a regression in reading dialog based TUI. 2026-05-13 18:31:11 -04:00
Storm Dragon 6876547590 Update documentation. 2026-05-12 22:34:30 -04:00
Storm Dragon 116f254e11 Fixed scripts with simlinks. 2026-05-12 20:01:46 -04:00
Storm Dragon 5dccdd27c4 Another shot at squishing this bug. 2026-05-12 18:30:23 -04:00
Storm Dragon fa63ecadbe The freeze bug reared its ugly head again. 2026-05-12 18:18:37 -04:00
Storm Dragon 96c5184450 Tab completion fixes. 2026-05-12 17:39:56 -04:00
Storm Dragon b599a25945 Add more local sourced directories for scripts and sounds meaning each user can have own scripts and sound themes when using -x. Hopefully fixed the remainder of the random freeze bug. 2026-05-12 17:23:50 -04:00
Storm Dragon 4022fc4006 Work on tightening up keyboard handling. 2026-05-12 16:06:31 -04:00
Storm Dragon ab7818fd4c Attempt to prevent thread lock that is causing freeze. 2026-05-12 12:42:57 -04:00
Storm Dragon 878eef5c5b Tests updated. Attempt to fix remaining problems with forward keypress and numlock reporting. 2026-05-12 02:32:26 -04:00
Storm Dragon 38ef1c2d72 Attempt to fix forward keypress and numpad state. 2026-05-12 02:26:02 -04:00
Storm Dragon 57980225ad Implemented local settings save in ~/.local/share/stormux. Also, I think I got interrupt on any keypress working better when using -x. 2026-05-12 02:06:07 -04:00
Storm Dragon 9e2d5a89b3 Fixed horrible crash bug with opening vim. 2026-05-10 22:31:17 -04:00
Storm Dragon c7d74726da Bump testing version for tab completion verification 2026-05-09 00:30:42 -04:00
Storm Dragon be78db0d58 Hopefully final touches on the new tab completion. 2026-05-09 00:14:36 -04:00
Storm Dragon 0af7d94014 That does it, I'm breaking out codex on this one. Take that tab completion! 2026-05-08 23:38:59 -04:00
Storm Dragon 42ba3fdad2 Another attempt at improving tab completion detection. 2026-05-08 21:50:27 -04:00
Storm Dragon 6fb8298b9f More trouble with pty vs console behavior. 2026-05-08 21:26:58 -04:00
Storm Dragon 114a7b0da7 Hopefully make pty more similar to tested console behavior. 2026-05-08 20:10:08 -04:00
Storm Dragon 90efad362b More fixes to pty reading. 2026-05-08 19:58:42 -04:00
Storm Dragon 3c61b6629a Possibly fixed a bug in the pty code used by -x flag. 2026-05-08 19:45:59 -04:00
Storm Dragon b6689d93bf Use single shared remote command lock 2026-05-08 00:48:11 -04:00
Storm Dragon a60efdbe07 Use shared locks for Fenrir instance coordination 2026-05-08 00:01:58 -04:00
Storm Dragon b38b0a2dab Fix cross-user Fenrir instance registry pruning 2026-05-07 23:51:07 -04:00
Storm Dragon 37e281a1f7 Fix Fenrir Unix socket ownership reporting 2026-05-07 23:43:11 -04:00
Storm Dragon 6d4f55ffe5 A couple of socket fixes so root fenrir and user fenrir do not battle. 2026-05-07 23:37:01 -04:00
Storm Dragon 8638bca1d5 Improve socket handling for -x spawned fenrir instances. 2026-05-07 23:24:54 -04:00
Storm Dragon 0273f9b956 Make x support optional. 2026-05-07 12:50:45 -04:00
Storm Dragon 1670c5711b Few fixes with the -x X driver code. 2026-05-07 02:03:36 -04:00
Storm Dragon fed8f24126 Add X11 terminal input mode 2026-05-07 01:45:00 -04:00
Storm Dragon 40120f825c 2026.04.04 merged 2026-04-04 10:08:46 -04:00
Storm Dragon 17dea6b026 Fix IRC incoming announcements and add focus.tui default 2026-03-25 03:24:29 -04:00
Storm Dragon 57c09e0db9 Guard evdev uinput setup 2026-03-04 04:01:19 -05:00
Storm Dragon 4050c32a16 Add speech-first diff review mode with navigation and tests 2026-02-15 15:55:46 -05:00
194 changed files with 16648 additions and 3503 deletions
+128
View File
@@ -0,0 +1,128 @@
## Development Philosophy
**Clean code over backward compatibility.** Breaking changes are acceptable in major versions.
- PEP8 compliance (snake_case for all variables/functions/settings)
- Remove deprecated code completely - no compatibility hacks
- Trust users to update configs when upgrading
**Exception**: Never break core accessibility (speech output, basic navigation).
## Core Architecture
### Drivers
- **Screen**: `vcsaDriver` (TTY), `ptyDriver` (terminal emulation)
- **Input**: `evdevDriver` (evdev), `x11Driver` (X11 terminal mode)
- **Speech**: `speechdDriver` (speech-dispatcher), `genericDriver` (subprocess)
- **Sound**: `gstreamerDriver` (GStreamer), `genericDriver` (Sox subprocess)
- **Remote**: `unixDriver` (Unix sockets), `tcpDriver`
### Command System
Commands in trigger folders execute automatically:
- `onKeyInput/`, `onCursorChange/`, `onScreenUpdate/`, `onScreenChanged/`, `onHeartBeat/`
- Numeric prefixes control priority (lower = higher priority): `15000-char_echo.py`
- Manual commands in `commands/commands/`
### Core Managers (in `src/fenrirscreenreader/core/`)
`fenrirManager.py`, `commandManager.py`, `eventManager.py`, `screenManager.py`, `inputManager.py`, `outputManager.py`
### Configuration
- Main: `config/settings/settings.conf`
- Keyboards: `config/keyboard/` (desktop.conf, laptop.conf)
- Sounds: `config/sound/`
- Punctuation: `config/punctuation/`
## 🚨 CRITICAL: Files to NEVER Modify
**These are timing-critical - only fix critical bugs, never refactor:**
- `evdevDriver.py` - Input event processing
- `vcsaDriver.py` - Screen monitoring
Symptoms of breakage: echo changes, input lag, missing screen updates, sync issues.
## Development Commands
```bash
# Run from source (requires root)
cd src/ && sudo ./fenrir -f -d # foreground with debug
# Validation before commit
python3 tools/validate_syntax.py --fix
python3 tools/validate_release.py --quick
# Tests
pytest tests/
pytest tests/ --cov=src/fenrirscreenreader --cov-report=html
```
**Note**: Standard TTY Fenrir blocks - only one instance at a time. `fenrir -x`
can run multiple foreground X terminal instances. Ask user to run tests that
need real Fenrir instances or root access.
## Creating Commands
Use `commands/command_template.py` as base. Required methods:
- `initialize(self, environment)`, `shutdown()`, `run()`, `get_description()`
### Import Pattern (Required)
Fenrir's command loader doesn't support relative imports:
```python
import os, importlib.util
_base_path = os.path.join(os.path.dirname(__file__), 'base_file.py')
_spec = importlib.util.spec_from_file_location("base_module", _base_path)
_module = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_module)
BaseClass = _module.BaseClass
```
### Consolidated Base Classes
- **Bookmarks**: `bookmark_base.py` - `BookmarkCommand(id, 'read'|'set'|'clear')`
- **Adjustments**: `adjustment_base.py` - `AdjustmentCommand('speech'|'sound', 'rate'|'pitch'|'volume', 'inc'|'dec')`
- **VMenu Search**: `vmenu_search_base.py` - `VMenuSearchCommand('a'-'z')`
## VMenu System
Application-specific menus in `vmenu-profiles/KEY/{app}/`:
- Directories = submenus, `.py` files = actions
- Activate: `Fenrir+F10`, navigate with arrows, Enter to execute
## Remote Control
Unix socket: `/tmp/fenrirscreenreader-daemon.sock`
TCP: localhost:22447
```bash
echo "command say Hello" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "ls" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set speech#rate=0.8" | nc localhost 22447
```
In `fenrir -x`, each instance has a private socket:
`/tmp/fenrirscreenreader-<pid>.sock`. One instance may also own the standard
socket. Use `ls`/`list` on the standard socket to discover registered instances
and their socket paths. Untargeted remote commands use a short cross-process
claim lock so only one instance handles the same broadcast command; direct
commands to a private instance socket still run on that instance.
Disable in untrusted environments: `[remote] enable=False`
## Key Implementation Details
- Commands via `command_mod.command()` instantiation
- Bookmark structure: `env['commandBuffer']['bookMarks'][ID][app]['1'/'2']`
- VMenu search: `env['runtime']['vmenuManager'].searchEntry(char)`
## Recent Fixes (Reference)
- Enhanced application detection for screen/tmux sessions
- Table navigation consistency (sound feedback)
- Blank line detection in 22 command files
- Character/space review during navigation
## Safe vs Risky Changes
**Safe**: Commands, VMenu profiles, config handling, sound/speech drivers, docs
**Risky (extra testing)**: Core managers, event processing
**Avoid**: evdevDriver, vcsaDriver, timing-critical code
+135 -56
View File
@@ -8,7 +8,7 @@ This software is licensed under the LGPL v3.
## Key Features
- **Multiple Interface Support**: Works in Linux TTY, and terminal emulators
- **Linux Console Support**: Works as a Linux TTY screen reader, with optional X11 terminal mode
- **Flexible Driver System**: Modular architecture with multiple drivers for speech, sound, input, and screen
- **Review Mode**: Navigate and review screen content without moving the edit cursor
- **Table Navigation**: Advanced table mode with column headers, cell-by-cell navigation, and boundary feedback
@@ -29,10 +29,8 @@ Fenrir is a Linux screen reader. Linux is the only officially supported platform
**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.
- Linux (ptyDriver, vcsaDriver, evdevDriver) - Full support
- macOS (ptyDriver) - Community-maintained, no guarantees
- BSD (ptyDriver) - Community-maintained, no guarantees
- Windows (ptyDriver) - Community-maintained, no guarantees
- Linux TTY (`vcsaDriver`, `evdevDriver`) - Full support
- X11 terminal emulators (`-x`, `ptyDriver`, `x11Driver`) - Supported
## Core Requirements
@@ -52,11 +50,8 @@ Fenrir is a Linux screen reader. Linux is the only officially supported platform
- ReadWrite permission:
- /dev/input
- /dev/uinput
2. **ptyDriver** - Terminal emulation input driver (cross-platform)
- python-pyte
3. **atspiDriver** - AT-SPI input driver for desktop environments
- python-pyatspi2
2. **x11Driver** - X11 terminal-scoped input driver for `fenrir -x`
- python-xlib
### Remote Drivers:
1. **unixDriver** - Unix socket remote control (default)
- socat (for command-line interaction)
@@ -73,7 +68,7 @@ Fenrir is a Linux screen reader. Linux is the only officially supported platform
- /dev/tty[1-64]
- /dev/vcsa[1-64]
- read logind DBUS
2. **ptyDriver** - Terminal emulation driver (cross-platform)
2. **ptyDriver** - Terminal emulation screen driver for `fenrir -x`
- python-pyte
@@ -84,7 +79,18 @@ Fenrir is a Linux screen reader. Linux is the only officially supported platform
- python-speechd
2. **genericDriver** - Generic subprocess speech driver
- espeak or espeak-ng (or any TTS command)
3. **debugDriver** - Debug speech driver for testing
3. **dectalkDriver** - Serial DECtalk-compatible hardware speech driver
- RPITalk gadget mode or a DECtalk-compatible serial device
4. **dectalkUsbDriver** - Native Access Solutions DECtalk USB driver
- USB ID `0dd0:2001`
- PyUSB and a libusb backend
5. **litetalkDriver** - Serial LiteTalk-compatible hardware speech driver
- RPITalk gadget mode or a LiteTalk-compatible serial device
6. **doubletalkDriver** - Serial DoubleTalk LT-compatible hardware speech driver
- DoubleTalk LT; does not support the internal DoubleTalk PC card
7. **tripletalkDriver** - Serial TripleTalk-compatible hardware speech driver
- External DB9 serial TripleTalk devices, or USB models that expose a tty serial device
8. **debugDriver** - Debug speech driver for testing
- No dependencies
@@ -128,14 +134,24 @@ Requires root privileges
sudo ./fenrir
Settings are located in:
- **After installation**: `/etc/fenrir/settings/settings.conf`
- **System-wide installation**: `/etc/fenrirscreenreader/settings/settings.conf`
- **User-local settings**: `~/.local/share/stormux/fenrirscreenreader/settings/settings.conf`
- **Development**: `config/settings/settings.conf`
When Fenrir runs as a regular user, including `fenrir -x`, it loads the
user-local settings file if it exists. If it does not exist, Fenrir falls back
to the system settings file, but saving settings writes to the user-local path.
When Fenrir runs as root, settings save to the system-wide path by default.
User sound themes are loaded before system themes from
`~/.local/stormux/fenrir/sounds/`. User scripts are loaded before configured
system scripts from `~/.local/stormux/fenrir/`.
By default Fenrir uses:
- **Sound driver**: genericDriver (via sox)
- **Speech driver**: speechdDriver (via speech-dispatcher)
- **Input driver**: evdevDriver (Linux) or ptyDriver (other platforms)
- **Screen driver**: vcsaDriver (Linux TTY) or ptyDriver (terminal emulation)
- **Input driver**: evdevDriver for Linux TTY, x11Driver for `fenrir -x`
- **Screen driver**: vcsaDriver for Linux TTY, ptyDriver for `fenrir -x`
## Audio Configuration
@@ -203,6 +219,7 @@ sudo /usr/share/fenrirscreenreader/tools/configure_pipewire.sh
2. **Basic Navigation**:
- **Fenrir Key**: By default `Insert`, `Keypad Insert`, or `Meta/Super` key
- **Tutorial Mode**: `Fenrir + H` to learn all commands interactively
- **Speech History**: `Fenrir + Ctrl + H` to review recent speech
- **Quit Fenrir**: `Fenrir + Q`
3. **Essential Commands**:
@@ -222,7 +239,8 @@ Fenrir supports two main keyboard layouts:
- **Desktop Layout**: Uses numeric keypad for navigation (recommended for desktop users)
- **Laptop Layout**: Alternative bindings for keyboards without numeric keypad
Configure in `/etc/fenrir/settings/settings.conf`:
Configure in `/etc/fenrirscreenreader/settings/settings.conf` or your
user-local settings file:
```ini
[keyboard]
keyboard_layout=desktop # or 'laptop'
@@ -251,7 +269,8 @@ Fenrir includes a powerful remote control system that allows external applicatio
### Configuration
Enable remote control in `/etc/fenrir/settings/settings.conf`:
Enable remote control in `/etc/fenrirscreenreader/settings/settings.conf` or
your user-local settings file:
```ini
[remote]
@@ -266,7 +285,8 @@ enable_command_remote=True # allow command execution
### Remote Drivers
1. **unixDriver** (recommended): Uses Unix domain sockets
- Socket location: `/tmp/fenrirscreenreader-deamon.sock` (TTY mode) or `/tmp/fenrirscreenreader-<pid>.sock`
- Socket location: `/tmp/fenrirscreenreader-daemon.sock` for the standard control socket
- `fenrir -x` instances also create private sockets: `/tmp/fenrirscreenreader-<pid>.sock`
- More secure, local-only access
- Works with `socat`
@@ -279,89 +299,105 @@ enable_command_remote=True # allow command execution
The `socat` command provides the easiest way to send commands to Fenrir:
#### Instance Discovery
```bash
# List registered Fenrir instances and their socket paths
echo "ls" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
In X terminal mode (`fenrir -x`), multiple Fenrir instances can run at the same
time. Each instance has a private socket, and one instance may also own the
standard control socket. Use `ls` or `command ls` on the standard socket to find
the private socket for a specific instance. Commands sent to the standard socket
are handled by its owner when possible; otherwise they are forwarded to a
registered private socket, preferring the sender's Fenrir ancestor when one can
be found. Untargeted commands sent through a shared or broadcast path are
claimed by one instance so duplicate instances do not all perform the same
action.
#### Basic Speech Control
```bash
# Interrupt current speech
echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Speak custom text
echo "command say Hello, this is a test message" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command say Hello, this is a test message" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Temporarily disable speech (until next keystroke)
echo "command tempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command tempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
#### Settings Control
```bash
# Enable highlight tracking mode
echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Change speech parameters
echo "setting set speech#rate=0.8" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set speech#pitch=0.6" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set speech#volume=0.9" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set speech#rate=0.8" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set speech#pitch=0.6" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set speech#volume=0.9" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Change punctuation level (none/some/most/all)
echo "setting set general#punctuation_level=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set general#punctuation_level=none" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set general#punctuation_level=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set general#punctuation_level=none" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Voice and TTS engine control
echo "setting set speech#voice=en-us+f3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set speech#module=espeak-ng" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set speech#voice=en-us+f3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set speech#module=espeak-ng" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Disable sound temporarily
echo "setting set sound#enabled=False" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set sound#volume=0.5" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set sound#enabled=False" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set sound#volume=0.5" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Keyboard and input settings
echo "setting set keyboard#char_echo_mode=1" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set keyboard#word_echo=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set keyboard#char_echo_mode=1" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set keyboard#word_echo=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Screen control (ignore specific TTYs)
echo "setting set screen#ignore_screen=1,2,3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set screen#ignore_screen=1,2,3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Multiple settings at once
echo "setting set speech#rate=0.8;sound#volume=0.7;general#punctuation_level=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-daemon.sock
# Reset all settings to defaults
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Save current settings
echo "setting save" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting saveas /tmp/my-fenrir-settings.conf" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting save" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting saveas /tmp/my-fenrir-settings.conf" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
#### Clipboard Operations
```bash
# Place text into clipboard
echo "command clipboard This text will be copied to clipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command clipboard This text will be copied to clipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Export clipboard to file
echo "command exportclipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command exportclipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
#### Window Management
```bash
# Define a window area (x1 y1 x2 y2)
echo "command window 0 0 80 24" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command window 0 0 80 24" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Reset window to full screen
echo "command resetwindow" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command resetwindow" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
#### VMenu Control
```bash
# Set virtual menu context
echo "command vmenu nano/file" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command vmenu nano/file" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Reset virtual menu
echo "command resetvmenu" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command resetvmenu" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
#### Application Control
```bash
# Quit Fenrir
echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
### Using TCP Driver
@@ -406,6 +442,9 @@ setting <action> [parameters]
**Application Commands:**
- `command quitapplication` - Quit Fenrir
**Instance Commands:**
- `ls` / `list` / `command ls` / `command list` - List registered Fenrir instances and their socket paths
#### Available Settings
**Settings Commands:**
@@ -423,7 +462,22 @@ setting <action> [parameters]
- `speech#volume=0.1-1.0` - Speech volume
- `speech#voice=voice_name` - Voice selection (e.g., "en-us+f3")
- `speech#module=module_name` - TTS module (e.g., "espeak-ng")
- `speech#driver=driver_name` - Speech driver (speechdDriver/genericDriver)
- `speech#driver=driver_name` - Speech driver (speechdDriver/genericDriver/dectalkDriver/dectalkUsbDriver/litetalkDriver/doubletalkDriver/tripletalkDriver)
- `speech#hardware_device=/dev/ttyS0` - Hardware synth serial device for dectalkDriver/litetalkDriver
- `speech#hardware_baud_rate=9600` - Hardware synth serial baud rate
- `speech#history_size=50` - Number of spoken items kept in runtime speech history
The native Access Solutions DECtalk USB, USB ID `0dd0:2001`, uses
`dectalkUsbDriver`. It is autodetected and does not use
`speech#hardware_device` or `speech#hardware_baud_rate`. Install PyUSB and a
libusb backend first. From a source checkout, use
`python3 -m pip install '.[dectalk_usb]'`; distributions may package PyUSB as
`python3-usb` or `python-pyusb`. Root-run Fenrir can access the device directly.
A non-root Fenrir process needs a local udev rule granting its account
read/write access to `0dd0:2001`; do not make the device world-writable. Other
USB hardware synths are supported only when Linux exposes them as a serial tty
such as `/dev/ttyACM0` or `/dev/ttyUSB0`. Use an explicit
`speech#hardware_device` path for serial hardware speech.
- `speech#auto_read_incoming=True/False` - Auto-read new text
*Sound Settings:*
@@ -552,7 +606,7 @@ Fenrir provides intelligent progress bar detection and audio feedback for variou
To enable progress monitoring:
1. Add a key binding in your keyboard layout file
2. Or use the remote control system: `echo "command progress_bar_monitor" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock`
2. Or use the remote control system: `echo "command progress_bar_monitor" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock`
### Progress Detection Patterns
@@ -609,11 +663,15 @@ Building...
- **Non-blocking**: Progress tones don't interrupt speech or other functionality
- **Configurable**: Can be enabled/disabled as needed
Fenrir detects stable progress structures rather than application-specific
status formats. Application-specific formats change too frequently to support
reliably.
### Usage Examples
```bash
# Enable progress monitoring
echo "command progress_bar_monitor" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command progress_bar_monitor" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Common scenarios where progress monitoring is useful:
wget https://example.com/large-file.zip # Download progress
@@ -637,7 +695,7 @@ Progress monitoring can be configured through settings:
#!/bin/bash
# notify_fenrir.sh - Send notifications to Fenrir
SOCKET="/tmp/fenrirscreenreader-deamon.sock"
SOCKET="/tmp/fenrirscreenreader-daemon.sock"
fenrir_say() {
echo "command say $1" | socat - UNIX-CLIENT:$SOCKET
@@ -660,7 +718,7 @@ import os
def send_fenrir_command(command):
"""Send command to Fenrir via Unix socket"""
socket_path = "/tmp/fenrirscreenreader-deamon.sock"
socket_path = "/tmp/fenrirscreenreader-daemon.sock"
if os.path.exists(socket_path):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
@@ -692,8 +750,8 @@ send_fenrir_command("setting set speech#rate=0.9")
**Commands not working:**
- Verify `enable_command_remote=True` in settings
- Check Fenrir debug logs: `/var/log/fenrir.log`
- Test with simple command: `echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock`
- Check Fenrir debug logs: `/tmp/fenrir.log`
- Test with simple command: `echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock`
## Command Line Options
@@ -711,8 +769,8 @@ fenrir [OPTIONS]
- `-o, --options SECTION#SETTING=VALUE;..` - Override settings file options
- `-d, --debug` - Enable debug mode
- `-p, --print` - Print debug messages to screen
- `-e, --emulated-pty` - Use PTY emulation with escape sequences for input (enables desktop/X/Wayland usage)
- `-E, --emulated-evdev` - Use PTY emulation with evdev for input (single instance)
- `-x, --x11` - Use PTY emulation with X11 keyboard input scoped to the terminal window
- `--x11-window-id WINDOWID` - X11 window id to use for `--x11` terminal mode
- `-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.
@@ -721,8 +779,11 @@ fenrir [OPTIONS]
# Run in foreground with debug output
sudo fenrir -f -d
# Use PTY emulation for desktop use
sudo fenrir -e
# Use PTY emulation with X11 terminal-scoped keybindings
fenrir -x
# Use X11 mode with an explicit terminal window id
fenrir -x --x11-window-id 0x123456
# Override settings via command line
sudo fenrir -o "speech#rate=0.8;sound#volume=0.5"
@@ -735,6 +796,24 @@ sudo fenrir --ignore-screen 1
sudo fenrir -i 1 -i 2 # Ignore screens 1 and 2
```
### X11 Terminal Mode
`fenrir -x` runs Fenrir inside a GUI terminal without root. It uses the PTY screen driver for terminal contents and the X11 input driver for Fenrir keybindings. Unlike evdev mode, it does not listen to the whole desktop; key grabs are scoped to the target terminal window so desktop screen readers can keep their own global bindings outside that terminal.
By default, Fenrir targets the X11 window from `WINDOWID`, falling back to the active X11 window. If a terminal does not set `WINDOWID`, pass the window explicitly with `--x11-window-id`.
X11 terminal mode uses the normal keyboard layout files, including desktop and laptop bindings. It supports Fenrir keys such as numpad Insert/`KEY_KP0`, CapsLock/`KEY_CAPSLOCK`, Insert/`KEY_INSERT`, and Super/Windows/`KEY_META`. Script keys such as Compose use `KEY_COMPOSE`.
In this mode Fenrir runs as a regular user. Settings are loaded from
`~/.local/share/stormux/fenrirscreenreader/settings/settings.conf` when present,
otherwise from `/etc/fenrirscreenreader/settings/settings.conf`; saving settings
creates or updates the user-local file. User sound themes and scripts are read
first from `~/.local/stormux/fenrir/sounds/` and `~/.local/stormux/fenrir/`.
This mode requires `python-xlib`.
For users who want a dedicated PTY/terminal screen reader instead of Fenrir's Linux console focus, see TDSR: https://github.com/tspivey/tdsr
## Localization
Translation files are located in the `locale/` directory. To install translations:
+4 -4
View File
@@ -88,11 +88,11 @@ ls -la config/punctuation/default.conf
# Test basic functionality (ask user to run)
sudo ./src/fenrir --help
# Test in emulation mode (safer for desktop environments)
sudo ./src/fenrir -e --version
# Test version output
./src/fenrir --version
# Quick functionality test (3-5 seconds)
sudo timeout 5 ./src/fenrir -e -f || echo "Timeout reached (expected)"
# Quick console functionality test (3-5 seconds, ask user to run on a TTY)
sudo timeout 5 ./src/fenrir -f || echo "Timeout reached (expected)"
```
**Expected Result**: No immediate crashes, basic help/version output works
+5 -5
View File
@@ -3,13 +3,13 @@ https://git.stormux.org/storm/fenrir/issues
For bugs, please provide a debug file that shows the issue.
How to create a debug file:
1. first delete old stuff:
sudo rm /var/log/fenrir.log
2. start fenrir in debug mode
1. start fenrir in debug mode
sudo fenrir -d
<do your stuff>
3.
2.
stop fenrir (fenrirKey + q)
the debug file is in /var/log/fenrir.log
the debug file is in /tmp/fenrir.log
if another Fenrir debug instance is already using it, check /tmp/fenrir2.log,
/tmp/fenrir3.log, etc.
please be as precise as possible to make it easy to solve the problem.
+10 -12
View File
@@ -50,7 +50,13 @@ def check_dependency(dep: Dependency) -> bool:
dependencyList = [
# Core dependencies
Dependency('FenrirCore', 'core', 'core',
pythonImports=['daemonize', 'enchant', 'pyperclip', 'setproctitle']),
pythonImports=[
'daemonize',
'enchant',
'pyperclip',
'setproctitle',
'xdg'
]),
# Screen drivers
Dependency('DummyScreen', 'screen', 'dummyDriver'),
@@ -58,16 +64,13 @@ dependencyList = [
pythonImports=['dbus'],
devicePaths=['/dev/vcsa']),
Dependency('PTY', 'screen', 'ptyDriver',
pythonImports=['pyte', 'xdg']),
pythonImports=['pyte']),
# Input drivers
Dependency('DummyInput', 'input', 'dummyDriver'),
Dependency('DebugInput', 'input', 'debugDriver'),
Dependency('Evdev', 'input', 'evdevDriver',
pythonImports=['evdev', 'evdev.InputDevice', 'evdev.UInput', 'pyudev']),
Dependency('PTYInput', 'input', 'ptyDriver',
pythonImports=['pyte']),
# Sound drivers
Dependency('DummySound', 'sound', 'dummyDriver'),
Dependency('DebugSound', 'sound', 'debugDriver'),
@@ -82,11 +85,7 @@ dependencyList = [
Dependency('Speechd', 'speech', 'speechdDriver',
pythonImports=['speechd']),
Dependency('GenericSpeech', 'speech', 'genericDriver',
checkCommands=['espeak-ng']),
# Additional dependencies
Dependency('Pexpect', 'core', 'pexpectDriver',
pythonImports=['pexpect'])
checkCommands=['espeak-ng'])
]
defaultModules = {
@@ -94,8 +93,7 @@ defaultModules = {
'VCSA',
'Evdev',
'GenericSpeech',
'GenericSound',
'Pexpect'
'GenericSound'
}
def check_all_dependencies():
+10 -7
View File
@@ -7,10 +7,6 @@ This directory contains keyboard layout files for Fenrir screen reader.
- **desktop.conf** - Desktop layout using numeric keypad (recommended)
- **laptop.conf** - Laptop layout for keyboards without numeric keypad
- **nvda-desktop.conf** - NVDA-compatible desktop layout
- **nvda-laptop.conf** - NVDA-compatible laptop layout
- **pty.conf** - PTY emulation layout for terminal use
- **pty2.conf** - Alternative PTY emulation layout
## Key Features
@@ -30,13 +26,21 @@ This directory contains keyboard layout files for Fenrir screen reader.
- **Exit review**: `Fenrir + Keypad .`
- **Screen reading**: `Fenrir + Keypad 5` (current screen)
### Speech History
- **Open speech history**: `Fenrir + Ctrl + H`
- **Navigate history**: `Up` and `Down`
- **Copy current item**: `Enter`
- **Exit speech history**: `Escape`
## Configuration
To change keyboard layout, edit `/etc/fenrir/settings/settings.conf`:
To change keyboard layout, edit `/etc/fenrirscreenreader/settings/settings.conf`
or your user-local settings file at
`~/.local/share/stormux/fenrirscreenreader/settings/settings.conf`:
```ini
[keyboard]
keyboardLayout=desktop # or laptop, nvda-desktop, nvda-laptop, pty, pty2
keyboard_layout=desktop # or laptop
```
## Available Key Constants
@@ -582,4 +586,3 @@ BTN_TRIGGER_HAPPY37
BTN_TRIGGER_HAPPY38
BTN_TRIGGER_HAPPY39
BTN_TRIGGER_HAPPY40
+5 -7
View File
@@ -1,4 +1,5 @@
KEY_FENRIR,KEY_H=toggle_tutorial_mode
KEY_FENRIR,KEY_CTRL,KEY_H=speech_history
KEY_CTRL=shut_up
KEY_FENRIR,KEY_KP9=review_bottom
KEY_FENRIR,KEY_KP7=review_top
@@ -14,15 +15,12 @@ KEY_FENRIR,KEY_ALT,KEY_2=present_last_line
KEY_KP5=review_curr_word
KEY_KP4=review_prev_word
KEY_KP6=review_next_word
KEY_FENRIR,KEY_SHIFT,KEY_KP5=review_curr_word_phonetic
KEY_FENRIR,KEY_SHIFT,KEY_KP4=review_prev_word_phonetic
KEY_FENRIR,KEY_SHIFT,KEY_KP6=review_next_word_phonetic
2,KEY_KP5=review_curr_word_spell
3,KEY_KP5=review_curr_word_phonetic
KEY_KP2=review_curr_char
KEY_KP1=review_prev_char
KEY_KP3=review_next_char
KEY_FENRIR,KEY_SHIFT,KEY_KP2=review_curr_char_phonetic
KEY_FENRIR,KEY_SHIFT,KEY_KP1=review_prev_char_phonetic
KEY_FENRIR,KEY_SHIFT,KEY_KP3=review_next_char_phonetic
2,KEY_KP2=review_curr_char_phonetic
KEY_FENRIR,KEY_CTRL,KEY_KP8=review_up
KEY_FENRIR,KEY_CTRL,KEY_KP2=review_down
KEY_FENRIR,KEY_KPDOT=exit_review
@@ -83,6 +81,7 @@ KEY_FENRIR,KEY_CTRL,KEY_P=toggle_punctuation_level
KEY_FENRIR,KEY_RIGHTBRACE=toggle_auto_spell_check
KEY_FENRIR,KEY_BACKSLASH=toggle_output
KEY_FENRIR,KEY_CTRL,KEY_E=toggle_emoticons
KEY_FENRIR,KEY_CTRL,KEY_D=toggle_diff_mode
KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_E=cycle_key_echo
key_FENRIR,KEY_KPENTER=toggle_auto_read
KEY_FENRIR,KEY_CTRL,KEY_T=toggle_auto_time
@@ -131,4 +130,3 @@ KEY_FENRIR,KEY_F8=export_clipboard_to_x
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_LEFTCTRL,KEY_F4=cycle_keyboard_layout
+5 -7
View File
@@ -1,4 +1,5 @@
KEY_FENRIR,KEY_H=toggle_tutorial_mode
KEY_FENRIR,KEY_CTRL,KEY_H=speech_history
KEY_CTRL=shut_up
KEY_FENRIR,KEY_SHIFT,KEY_O=review_bottom
KEY_FENRIR,KEY_SHIFT,KEY_U=review_top
@@ -14,15 +15,12 @@ KEY_FENRIR,KEY_ALT,KEY_2=present_last_line
KEY_FENRIR,KEY_K=review_curr_word
KEY_FENRIR,KEY_J=review_prev_word
KEY_FENRIR,KEY_L=review_next_word
KEY_FENRIR,KEY_ALT,KEY_K=review_curr_word_phonetic
KEY_FENRIR,KEY_ALT,KEY_J=review_prev_word_phonetic
KEY_FENRIR,KEY_ALT,KEY_L=review_next_word_phonetic
2,KEY_FENRIR,KEY_K=review_curr_word_spell
3,KEY_FENRIR,KEY_K=review_curr_word_phonetic
KEY_FENRIR,KEY_COMMA=review_curr_char
KEY_FENRIR,KEY_M=review_prev_char
KEY_FENRIR,KEY_DOT=review_next_char
KEY_FENRIR,KEY_ALT,KEY_COMMA=curr_char_phonetic
KEY_FENRIR,KEY_ALT,KEY_M=prev_char_phonetic
KEY_FENRIR,KEY_ALT,KEY_DOT=next_char_phonetic
2,KEY_FENRIR,KEY_COMMA=review_curr_char_phonetic
KEY_FENRIR,KEY_CTRL,KEY_I=review_up
KEY_FENRIR,KEY_CTRL,KEY_COMMA=review_down
KEY_FENRIR,KEY_SLASH=exit_review
@@ -81,6 +79,7 @@ KEY_FENRIR,KEY_SHIFT,KEY_CTRL,KEY_P=toggle_punctuation_level
KEY_FENRIR,KEY_RIGHTBRACE=toggle_auto_spell_check
KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_ENTER=toggle_output
KEY_FENRIR,KEY_SHIFT,KEY_E=toggle_emoticons
KEY_FENRIR,KEY_CTRL,KEY_D=toggle_diff_mode
KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_E=cycle_key_echo
KEY_FENRIR,KEY_CTRL,KEY_T=toggle_auto_time
KEY_FENRIR,KEY_Y=toggle_highlight_tracking
@@ -131,4 +130,3 @@ KEY_FENRIR,KEY_F8=export_clipboard_to_x
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_LEFTCTRL,KEY_F4=cycle_keyboard_layout
-89
View File
@@ -1,89 +0,0 @@
# This file contains terminal escape sequences as shortcut
# It is used for PTY screen / Input driver (Terminal emulation)
# ^[ is used as escape
# f1 - fenrir help
^[OP=toggle_tutorial_mode
# double tap control+end read attributes
2,^[[1;5F=attribute_cursor
#=toggle_has_attribute
# escape - stop speech
^[=shut_up
# context menu key - stop speech
^[[29~=shut_up
# alt+shift+down - review to bottom
^[[1;4B=review_bottom
# alt+shift_up - review to top
^[[1;4A=review_top
# alt+down - review current line
^[[1;3B=review_curr_line
# alt+left - review previous line
^[[1;3D=review_prev_line
# alt+right - review next line
^[[1;3C=review_next_line
# alt+shift+left - beginning of line
^[[1;4D=review_line_begin
# alt+shift+right - end of line
^[[1;4C=review_line_end
# control+down - review current word
^[[1;5B=review_curr_word
# control+left - review previous word
^[[1;5D=review_prev_word
# control+right - review next word
^[[1;5C=review_next_word
# shift+down - review current character
^[[1;2B=review_curr_char
# shift+left - review previous character
^[[1;2D=review_prev_char
# shift+right - review next character
^[[1;2C=review_next_char
# control+shift+down - current character phonetic
^[[1;6B=curr_char_phonetic
# control+shift+left - previous character phonetic
^[[1;6D=prev_char_phonetic
# control+shift+right - next character phonetic
^[[1;6C=next_char_phonetic
# f2 - toggle sound
^[OQ=toggle_sound
# f3 - toggle highlight tracking
^[OR=toggle_highlight_tracking
alt+f12 - quit fenrir
^[[24;3~=quit_fenrir
# alt+f12 - time
^[[24;3~=time
# 2,alt+f12 - date
2,^[[24;3~=date
# alt+[ - previous clipboard
^[[=prev_clipboard
# alt+] - next clipboard
^[]=next_clipboard
# control+f6 - Read current clipboard
^[[17;5~=curr_clipboard
# f6 - copy to clipboard
^[[17~=copy_marked_to_clipboard
# shift+f6 - clear clipboard
^[[17;2~=clear_clipboard
# f7 - paste clipboard
^[[18~=paste_clipboard
# alt+f8 - export clipboard to X
^[[19;3~=export_clipboard_to_x
# control+f8 - import clipboard from X
^[[19;5~=import_clipboard_from_x
# alt+f9 - export clipboard to file
^[[20;3~=export_clipboard_to_file
# control+f9 - import clipboard from file
^[[20;5~=import_clipboard_from_file
# shift+f5 - remove marks
^[[15;2~=remove_marks
# f5 - set mark
^[[15~=set_mark
# f8 - Last utterance to clipboard
^[[19~=copy_last_echo_to_clipboard
# lat+\ Toggle auto announcement of indentation
^[\=toggle_auto_indent
# alt+end - temperarily disable speech
^[[1;3F=temp_disable_speech
# control+end - toggle auto read
^[[1;5F=toggle_auto_read
# F12 - cycle keyboard layout
^[[24~=cycle_keyboard_layout
-46
View File
@@ -1,46 +0,0 @@
# This file contains terminal escape sequences as shortcut
# It is used for PTY screen / Input driver (Terminal emulation)
# ^[ is used as escape
^[h=toggle_tutorial_mode
^[/=shut_up
^[[D=shut_up
^[O=review_bottom
^[U=review_top
#^[[1;3B=review_curr_line
^[i=review_curr_line
^[u=review_prev_line
^[o=review_next_line
^[J=review_line_begin
^[L=review_line_end
^[j=review_line_first_char
^[L=review_line_last_char
^[k=review_curr_word
^[j=review_prev_word
^[l=review_next_word
^[,=review_curr_char
^[m=review_prev_char
^[.=review_next_char
^[<=curr_char_phonetic
^[M=prev_char_phonetic
^[>=next_char_phonetic
^[OR=toggle_sound
^[OS=toggle_speech
^[8=toggle_highlight_tracking
^[Q=quit_fenrir
^[t=time
^[T=date
^[[5~=prev_clipboard
^[[6~=next_clipboard
^[C=curr_clipboard
^[c=copy_marked_to_clipboard
^[v=paste_clipboard
^[[15~=import_clipboard_from_file
^[X=remove_marks
^[x=set_mark
^[\=toggle_auto_indent
^[B=copy_last_echo_to_clipboard
# alt+f8 - export clipboard to X
# ^[[19;3~=export_clipboard_to_x
# # control+f8 - import clipboard from X
# ^[[19;5~=import_clipboard_from_x
+73 -23
View File
@@ -9,7 +9,8 @@ driver=gstreamerDriver
#driver=genericDriver
# 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 ~/.local/stormux/fenrir/sounds,
# /usr/share/sounds/fenrir, or /usr/share/sounds/fenrirscreenreader.
theme=default
# Sound volume controls how loud the sounds for your selected soundpack are.
@@ -18,14 +19,14 @@ volume=0.7
# shell commands for generic sound driver
# the folowing variable are substituted
# fenrirVolume = the current volume setting
# fenrirSoundFile = the soundfile for an soundicon
# fenrirFrequence = the frequency to play
# fenrirDuration = the duration of the frequency
# fenrir_volume = the current volume setting
# fenrir_sound_file = the soundfile for an soundicon
# fenrir_frequency = the frequency to play
# fenrir_duration = the duration of the frequency
# the following command is used to play a soundfile
generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile
generic_play_file_command=play -q -v fenrir_volume fenrir_sound_file
#the following command is used to generate a frequency beep
generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
generic_frequency_command=play -q -v fenrir_volume -n -c1 synth fenrir_duration sine fenrir_frequency
# Enable progress bar monitoring with ascending tones by default
progress_monitoring=True
@@ -34,9 +35,16 @@ progress_monitoring=True
# Turn speech on or off:
enabled=True
# Select speech driver, options are speechdDriver or genericDriver:
# Select speech driver, options are speechdDriver, genericDriver,
# dectalkDriver, dectalkUsbDriver, litetalkDriver, doubletalkDriver,
# or tripletalkDriver:
driver=speechdDriver
#driver=genericDriver
#driver=dectalkDriver
#driver=dectalkUsbDriver
#driver=litetalkDriver
#driver=doubletalkDriver
#driver=tripletalkDriver
# The rate selects how fast Fenrir will speak. Options range from 0, slowest, to 1.0, fastest.
rate=0.5
@@ -69,12 +77,30 @@ volume=1.0
# Select the language you want Fenrir to use.
#language=en
# Hardware speech synthesizer serial device.
# Used by dectalkDriver, litetalkDriver, doubletalkDriver, and tripletalkDriver.
# dectalkUsbDriver autodetects the Access Solutions device and ignores this.
# USB serial devices are supported if Linux exposes them as /dev/ttyACM*
# or /dev/ttyUSB*. USB-only synths with no tty device need a separate driver.
# Set an explicit device for hardware speech.
# Examples:
# hardware_device=/dev/ttyACM0 # RPITalk USB gadget mode
# hardware_device=/dev/ttyUSB0 # USB serial adapter
# hardware_device=/dev/ttyS0 # built-in serial port
hardware_device=/dev/ttyS0
# Serial baud rate for hardware speech synthesizers.
hardware_baud_rate=9600
# Read new text as it happens?
auto_read_incoming=True
# Speak individual numbers instead of whole string.
read_numbers_as_digits = False
# Number of spoken items kept in runtime speech history.
history_size=50
# Flood control: batch rapid updates instead of speaking each one
# Number of updates within rapid_update_window to trigger batching
rapid_update_threshold=5
@@ -91,17 +117,17 @@ 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
# generic_speech_command is the command that is executed for talking
# the following variables are replaced with values
# fenrirText = is the text that should be spoken
# fenrirModule = may be the speech module like used in speech-dispatcher, not every TTY need this
# fenrirLanguage = the language
# fenrirVoice = is the current voice that should be used. Set the voice variable above.
# fenrir_text = is the text that should be spoken
# fenrir_module = may be the speech module like used in speech-dispatcher, not every TTY need this
# fenrir_language = the language
# fenrir_voice = is the current voice that should be used. Set the voice variable above.
# the current volume, pitch and rate is calculated like this
# value = min + settingValue * (min - max )
# fenrirVolume = is replaced with the current volume
# fenrirPitch = is replaced with the current pitch
# fenrirRate = is replaced with the current speed (speech rate)
# fenrir_volume = is replaced with the current volume
# fenrir_pitch = is replaced with the current pitch
# fenrir_rate = is replaced with the current speed (speech rate)
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 generic_speech_command
@@ -131,6 +157,8 @@ driver=evdevDriver
device=ALL
# gives Fenrir exclusive access to the keyboard and lets it control keystrokes.
grab_devices=True
# Optional X11 target window id for x11Driver. Empty uses WINDOWID or active window.
x11_window_id=
# 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
@@ -158,7 +186,8 @@ double_tap_timeout=0.2
# The default is 0, no logging.
debug_level=0
# debugMode sets where the debug output should send to:
# debugMode=File writes to debug_file (Default:/tmp/fenrir-PID.log)
# debugMode=File writes to debug_file (Default:/tmp/fenrir.log)
# If the default log is already in use, Fenrir uses /tmp/fenrir2.log, etc.
# debugMode=Print just prints on the screen
debug_mode=File
debug_file=
@@ -172,11 +201,6 @@ respect_punctuation_pause=True
replace_undefined_punctuation_with_space=True
# Pause speech briefly at newline characters for better readability
new_line_pause=True
number_of_clipboards=50
# used path for "export_clipboard_to_file"
# $user is replaced by username
#clipboardExportPath=/home/$user/fenrirClipboard
clipboard_export_path=/tmp/fenrirClipboard
# Convert text emoticons like :) to descriptive text (e.g., "smiling face")
emoticons=True
# Define the Fenrir modifier key(s) - used to trigger Fenrir commands
@@ -199,7 +223,8 @@ date_format=%%A, %%B %%d, %%Y
auto_spell_check=True
# Language for spell checking (format: language_COUNTRY, e.g., en_US, en_GB, es_ES)
spell_check_language=en_US
# path for your scripts "script_keys" functionality
# path for your scripts "script_keys" functionality.
# User-local scripts in ~/.local/stormux/fenrir are loaded first.
script_path=/usr/share/fenrirscreenreader/scripts
# Override default commands or add custom commands without modifying Fenrir installation
# Leave empty to use default commands only
@@ -226,12 +251,37 @@ has_attributes=True
# Shell to use for PTY emulation mode (empty = use system default shell)
# Examples: /bin/bash, /bin/zsh, /usr/bin/fish
shell=
# Diff review presentation mode:
# speech = spoken feedback only
# sound = sound cues only (falls back to speech when cue is unavailable)
# both = speech and sound cues
diff_presentation=both
# Diff review verbosity:
# compact = concise role/location feedback
# verbose = include diff line content during navigation
diff_verbosity=compact
[clipboard]
# Number of clipboard history entries Fenrir keeps.
number_of_clipboards=50
# used path for "export_clipboard_to_file"
# $user is replaced by username
#clipboard_export_path=/home/$user/fenrirClipboard
clipboard_export_path=/tmp/fenrirClipboard
# Keep Fenrir's clipboard history and the X clipboard synchronized.
# In fenrir -x, an empty sync_display uses the current DISPLAY.
# In console/TTY mode, set sync_display explicitly, for example :0.
sync_enabled=False
sync_display=
sync_interval=0.5
[focus]
# Follow and announce text cursor position changes
cursor=True
# Follow and announce highlighted/selected text changes (useful in menus)
highlight=False
# Suppress generic incoming announcements for full-screen TUIs that redraw often
tui=False
[remote]
enable=True
Binary file not shown.
-2
View File
@@ -1,8 +1,6 @@
# Screen Reader Turned On or Off
ScreenReaderOn='ScreenReaderOn.wav'
ScreenReaderOff='ScreenReaderOff.wav'
# PTY bypass
PTYBypass='PTYBypass.wav'
# Cancel the current command
Cancel='Cancel.wav'
# Accept command
-2
View File
@@ -1,8 +1,6 @@
# Screen Reader Turned On or Off
ScreenReaderOn=''
ScreenReaderOff=''
# PTY bypass
PTYBypass=''
# Cancel the current command
Cancel=''
# Accept command
+4 -6
View File
@@ -22,7 +22,7 @@ src/fenrirscreenreader/
│ ├── onKeyInput/ # Key input hooks
│ └── help/ # Tutorial system
├── drivers/ # Driver implementations
│ ├── inputDriver/ # Input drivers (evdev, pty, atspi)
│ ├── inputDriver/ # Input drivers (evdev, x11)
│ ├── screenDriver/ # Screen drivers (vcsa, pty)
│ ├── speechDriver/ # Speech drivers (speechd, generic)
│ └── soundDriver/ # Sound drivers (generic, gstreamer)
@@ -36,8 +36,7 @@ Fenrir uses a pluggable driver architecture:
1. **Input Drivers**: Capture keyboard input
- evdevDriver: Linux evdev (recommended)
- ptyDriver: Terminal emulation
- atspiDriver: AT-SPI for desktop
- x11Driver: X11 terminal-scoped input
2. **Screen Drivers**: Read screen content
- vcsaDriver: Linux VCSA devices
@@ -83,7 +82,6 @@ Fenrir supports various event hooks:
- **onCursorChange**: Triggered when cursor moves
- **onScreenUpdate**: Triggered on screen content changes
- **onKeyInput**: Triggered on key presses
- **onByteInput**: Triggered on byte-level input
- **onScreenChanged**: Triggered when switching screens
## Development Setup
@@ -116,7 +114,7 @@ sudo ./fenrir -f -d -p
# Debug output goes to:
# - Console (with -p flag)
# - /var/log/fenrir.log
# - /tmp/fenrir.log
```
## Creating Commands
@@ -386,4 +384,4 @@ current_line = lines[self.env['screen']['newCursor']['y']]
- **Wiki**: https://git.stormux.org/storm/fenrir/wiki
- **Issues**: Use repository issue tracker
- **Community**: IRC irc.stormux.org #stormux
- **Email**: stormux+subscribe@groups.io
- **Email**: stormux+subscribe@groups.io
+48
View File
@@ -0,0 +1,48 @@
# Diff Review Mode
Diff review mode provides read-only navigation for unified and classic diff files with speech-first output.
## Quick Workflow
1. Copy the full absolute path of a diff file to the Fenrir clipboard.
2. Press `Fenrir + Ctrl + D` to toggle diff mode on.
3. Review the diff using the keys below.
4. Press `Esc` to leave diff mode.
If the clipboard does not contain a full absolute file path, Fenrir announces an error and does not enter diff mode.
## Keys (Active Only In Diff Mode)
- `h` - Next hunk
- `Shift + h` - Previous hunk
- `f` - Next file section
- `Shift + f` - Previous file section
- `a` - Next added line
- `Shift + a` - Previous added line
- `d` - Next removed line
- `Shift + d` - Previous removed line
- `Up` - Previous line
- `Down` - Next line
- `Left` - Previous character
- `Right` - Next character
- `Ctrl + Left` - Previous word
- `Ctrl + Right` - Next word
- `s` - Diff summary
- `F1` - Speak key help
- `Esc` - Exit diff mode
## Speech Behavior
- Added and removed content is spoken as `Added:` and `Removed:` lines.
- Marker-only lines are normalized for speech:
- `+++` is spoken as `added`
- `---` is spoken as `removed`
- Classic diff markers are spoken in plain language:
- `17c17` -> `line 17 changed`
- `17d16` -> `line 17 deleted`
- `16a17` -> `line 17 added`
## Notes
- Diff mode is read-only and does not modify the diff file.
- Normal Fenrir key bindings are restored when diff mode exits.
+96 -49
View File
@@ -13,13 +13,16 @@ fenrir \- A modern, modular console screen reader for Linux
.IR SECTION#SETTING=VALUE;.. ]
.RB [ \-d ]
.RB [ \-p ]
.RB [ \-e ]
.RB [ \-E ]
.RB [ \-x ]
.RB [ \-\-x11-window-id
.IR WINDOWID ]
.RB [ \-F ]
.RB [ \-i
.IR SCREEN ]
.SH DESCRIPTION
Fenrir is a modern, modular, flexible and fast console screen reader written in Python 3.
It provides spoken feedback for Linux console applications and supports multiple interface types including TTY, terminal emulators, and desktop environments.
It provides spoken feedback for Linux console applications and supports Linux TTYs plus X11 terminal mode.
Fenrir features a modular driver architecture supporting multiple speech synthesizers, sound systems, input methods, and screen reading techniques. It includes advanced features like review mode, multiple clipboards, spell checking, bookmarks, and configurable key bindings.
@@ -38,7 +41,7 @@ Run Fenrir in the foreground instead of as a daemon. Useful for debugging and te
.TP
.BR \-s ", " \-\-setting " " \fISETTING-FILE\fR
Path to a custom settings file. Default is /etc/fenrir/settings/settings.conf.
Path to a custom settings file. Default is /etc/fenrirscreenreader/settings/settings.conf for root and ~/.local/share/stormux/fenrirscreenreader/settings/settings.conf for regular-user saves.
.TP
.BR \-o ", " \-\-options " " \fISECTION#SETTING=VALUE;..\fR
@@ -47,27 +50,29 @@ Multiple settings can be separated by semicolons.
.TP
.BR \-d ", " \-\-debug
Enable debug mode. Debug information will be logged to /var/log/fenrir.log.
Enable debug mode. Debug information will be logged to /tmp/fenrir.log by
default. If another Fenrir debug instance is already using it, Fenrir uses
/tmp/fenrir2.log, /tmp/fenrir3.log, etc.
.TP
.BR \-p ", " \-\-print
Print debug messages to screen in addition to logging them.
.TP
.BR \-e ", " \-\-emulated-pty
Use PTY emulation with escape sequences for input. This enables usage in desktop/X11/Wayland environments and terminal emulators.
.TP
.BR \-E ", " \-\-emulated-evdev
Use PTY emulation with evdev for input (single instance mode).
.TP
.BR \-F ", " \-\-force-all-screens
Force Fenrir to respond on all screens, ignoring the ignoreScreen setting. This temporarily overrides screen filtering for the current session.
Force Fenrir to respond on all screens, ignoring the ignore_screen setting. This temporarily overrides screen filtering for the current session.
.TP
.BR \-i ", " \-I ", " \-\-ignore-screen " \fISCREEN\fR"
Ignore specific screen(s). Can be used multiple times to ignore multiple screens. This is equivalent to setting ignoreScreen in the configuration file and will be combined with any existing ignore settings.
Ignore specific screen(s). Can be used multiple times to ignore multiple screens. This is equivalent to setting ignore_screen in the configuration file and will be combined with any existing ignore settings.
.TP
.BR \-x ", " \-\-x11
Run in X11 terminal mode using PTY screen monitoring and X11 keyboard input scoped to the terminal window.
.TP
.BR \-\-x11-window-id " " \fIWINDOWID\fR
Use a specific X11 terminal window id for X11 terminal mode.
.SH KEY CONCEPTS
@@ -186,12 +191,24 @@ Announce marked text
.SS Settings File
The main configuration file is located at:
.IP \[bu] 2
.B /etc/fenrir/settings/settings.conf
.B /etc/fenrirscreenreader/settings/settings.conf
(system-wide installation)
.IP \[bu] 2
.B ~/.local/share/stormux/fenrirscreenreader/settings/settings.conf
(user-local settings)
.IP \[bu] 2
.B config/settings/settings.conf
(development/git version)
When Fenrir runs as a regular user, including fenrir -x, it loads the
user-local settings file if it exists. If it does not exist, Fenrir falls back
to the system settings file, but saving settings writes to the user-local path.
When Fenrir runs as root, settings save to the system-wide path by default.
User sound themes are loaded before system themes from
~/.local/stormux/fenrir/sounds/. User scripts are loaded before configured
system scripts from ~/.local/stormux/fenrir/.
.SS Key Sections
.TP
.B [speech]
@@ -223,6 +240,16 @@ speechdDriver - Speech-dispatcher (recommended)
.IP \[bu] 4
genericDriver - Command-line TTS (espeak, etc.)
.IP \[bu] 4
dectalkDriver - DECtalk-compatible serial hardware speech
.IP \[bu] 4
dectalkUsbDriver - Native Access Solutions DECtalk USB hardware speech
.IP \[bu] 4
litetalkDriver - LiteTalk-compatible serial hardware speech
.IP \[bu] 4
doubletalkDriver - DoubleTalk LT-compatible serial hardware speech
.IP \[bu] 4
tripletalkDriver - TripleTalk-compatible serial hardware speech
.IP \[bu] 4
debugDriver - Debug/testing
.TP
@@ -234,14 +261,12 @@ gstreamerDriver - GStreamer-based
.IP \[bu] 4
debugDriver - Debug/testing
.TP
.TP
.B Input Drivers:
.IP \[bu] 4
evdevDriver - Linux evdev (recommended for Linux)
.IP \[bu] 4
ptyDriver - Terminal emulation (cross-platform)
.IP \[bu] 4
atspiDriver - AT-SPI for desktop environments
x11Driver - X11 terminal-scoped input for fenrir -x
.TP
.B Screen Drivers:
@@ -267,17 +292,13 @@ Start Fenrir as a daemon with default settings.
.B fenrir -f -d
Run Fenrir in foreground with debug output.
.TP
.B fenrir -e
Run Fenrir with PTY emulation for desktop/terminal use.
.TP
.B fenrir -o "speech#rate=0.8;sound#volume=0.5"
Override speech rate and sound volume settings.
.TP
.B fenrir -F
Force Fenrir to work on all screens, ignoring ignoreScreen settings.
Force Fenrir to work on all screens, ignoring ignore_screen settings.
.TP
.B fenrir -s /home/user/my-fenrir.conf
@@ -288,61 +309,77 @@ Use a custom configuration file.
Fenrir includes a powerful remote control system for automation and integration with external applications.
.SS Configuration
Enable remote control in /etc/fenrir/settings/settings.conf:
Enable remote control in /etc/fenrirscreenreader/settings/settings.conf or the user-local settings file:
.EX
[remote]
enable=True
driver=unixDriver
enableSettingsRemote=True
enableCommandRemote=True
enable_settings_remote=True
enable_command_remote=True
.EE
.SS Using socat with Unix Sockets
.TP
.B Instance Discovery:
.EX
# List registered Fenrir instances and their socket paths
echo "ls" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
.EE
In X terminal mode (fenrir -x), multiple Fenrir instances can run at the
same time. Each instance has a private socket at
/tmp/fenrirscreenreader-<pid>.sock, and one instance may also own the
standard control socket. Use ls or "command ls" on the standard socket to
find the private socket for a specific instance. Commands sent to the standard
socket are handled by its owner when possible; otherwise they are forwarded to a
registered private socket, preferring the sender's Fenrir ancestor when one can
be found.
.TP
.B Basic Speech Control:
.EX
# Interrupt current speech
echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Speak custom text
echo "command say Hello, this is a test" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command say Hello, this is a test" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Temporarily disable speech
echo "command tempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command tempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
.EE
.TP
.B Settings Control:
.EX
# Enable highlight tracking
echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Change speech rate
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-daemon.sock
# Change punctuation level (none/some/most/all)
echo "setting set general#punctuationLevel=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set general#punctuation_level=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# 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-daemon.sock
# Multiple settings at once
echo "setting set speech#rate=0.8;sound#volume=0.7;general#punctuationLevel=most" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set speech#rate=0.8;sound#volume=0.7;general#punctuation_level=most" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Reset all settings
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
.EE
.TP
.B Clipboard Operations:
.EX
# Add text to clipboard
echo "command clipboard Text to copy" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command clipboard Text to copy" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Export clipboard to file
echo "command exportclipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command exportclipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
.EE
.SS Command Reference
@@ -355,6 +392,8 @@ command say <text> - Speak the specified text
command interrupt - Stop current speech
.IP \[bu] 2
command tempdisablespeech - Disable speech until next key press
.IP \[bu] 2
ls / list / command ls / command list - List registered Fenrir instances
.TP
.B Settings Commands:
@@ -385,7 +424,7 @@ command quitapplication - Quit Fenrir
.TP
.B Speech Control:
.IP \[bu] 2
general#punctuationLevel=none/some/most/all - Punctuation verbosity
general#punctuation_level=none/some/most/all - Punctuation verbosity
.IP \[bu] 2
speech#rate=0.1-1.0 - Speech rate
.IP \[bu] 2
@@ -400,28 +439,32 @@ speech#module=module_name - TTS module
.TP
.B Screen Control:
.IP \[bu] 2
screen#ignoreScreen=1,2,3 - TTY screens to ignore
screen#ignore_screen=1,2,3 - TTY screens to ignore
.IP \[bu] 2
focus#highlight=True/False - Follow text highlighting
.TP
.B Input Control:
.IP \[bu] 2
keyboard#charEchoMode=0-2 - Character echo mode
keyboard#char_echo_mode=0-2 - Character echo mode
.IP \[bu] 2
keyboard#wordEcho=True/False - Word echo
keyboard#word_echo=True/False - Word echo
.SH FILES
.TP
.B /etc/fenrir/settings/settings.conf
.B /etc/fenrirscreenreader/settings/settings.conf
Main configuration file
.TP
.B /etc/fenrir/keyboard/
.B ~/.local/share/stormux/fenrirscreenreader/settings/settings.conf
User-local configuration file
.TP
.B /etc/fenrirscreenreader/keyboard/
Keyboard layout definitions
.TP
.B /etc/fenrir/punctuation/
.B /etc/fenrirscreenreader/punctuation/
Punctuation level definitions
.TP
@@ -429,11 +472,15 @@ Punctuation level definitions
System sound themes
.TP
.B ~/.local/share/fenrirscreenreader/sounds/
.B ~/.local/stormux/fenrir/sounds/
User sound themes
.TP
.B /var/log/fenrir.log
.B ~/.local/stormux/fenrir/
User scripts
.TP
.B /tmp/fenrir.log
Debug log file
.TP
@@ -516,4 +563,4 @@ This software is licensed under the LGPL v3.
Full documentation: https://git.stormux.org/storm/fenrir/wiki
.PP
Support: stormux+subscribe@groups.io
Support: stormux+subscribe@groups.io
+103 -51
View File
@@ -27,9 +27,12 @@ and voices provided by Speech Dispatcher. +
Keyboard shortcuts are bound to commands to read the screen, review it
and perform other actions like modify the settings on the fly, switch
languages, copy and paste text, provide miscellaneous information. +
=== Configuration file === The permanent settings are recorded in the
configuration file /etc/fenrirscreenreader/settings.conf, that is
heavily commented. +
=== Configuration file === The system-wide settings are recorded in the
configuration file /etc/fenrirscreenreader/settings/settings.conf, that is
heavily commented. Regular-user sessions, including `+fenrir -x+`, load
`+~/.local/share/stormux/fenrirscreenreader/settings/settings.conf+` when it
exists, otherwise they fall back to the system settings file. Saving settings
as a regular user creates or updates the user-local file. +
These settings are described in this user guide.
To start Fenrir once type as root:
@@ -1214,17 +1217,11 @@ Enable debug mode. Debug information will be logged.
`+-p, --print+`::
Print debug messages to screen in addition to logging them.
`+-e, --emulated-pty+`::
Use PTY emulation with escape sequences for input. This enables usage in desktop/X11/Wayland environments and terminal emulators.
`+-E, --emulated-evdev+`::
Use PTY emulation with evdev for input (single instance mode).
`+-F, --force-all-screens+`::
Force Fenrir to respond on all screens, ignoring the ignoreScreen setting. This temporarily overrides screen filtering for the current session.
Force Fenrir to respond on all screens, ignoring the ignore_screen setting. This temporarily overrides screen filtering for the current session.
`+-i, -I, --ignore-screen <SCREEN>+`::
Ignore specific screen(s). Can be used multiple times to ignore multiple screens. This is equivalent to setting ignoreScreen in the configuration file and will be combined with any existing ignore settings.
Ignore specific screen(s). Can be used multiple times to ignore multiple screens. This is equivalent to setting ignore_screen in the configuration file and will be combined with any existing ignore settings.
==== Set settings options
@@ -1277,56 +1274,72 @@ enable_command_remote=True
The `+socat+` command provides the easiest way to send commands to Fenrir:
===== Instance Discovery
....
# List registered Fenrir instances and their socket paths
echo "ls" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
....
In X terminal mode (`+fenrir -x+`), multiple Fenrir instances can run at the
same time. Each instance has a private socket at
`+/tmp/fenrirscreenreader-<pid>.sock+`, and one instance may also own the
standard control socket. Use `+ls+` or `+command ls+` on the standard socket to
find the private socket for a specific instance. Commands sent to the standard
socket are handled by its owner when possible; otherwise they are forwarded to a
registered private socket, preferring the sender's Fenrir ancestor when one can
be found.
===== Basic Speech Control
....
# Interrupt current speech
echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Speak custom text
echo "command say Hello, this is a test message" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command say Hello, this is a test message" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Temporarily disable speech (until next keystroke)
echo "command tempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command tempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
....
===== Settings Control
....
# Enable highlight tracking mode
echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Change speech rate
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-daemon.sock
# Change punctuation level (none/some/most/all)
echo "setting set general#punctuation_level=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set general#punctuation_level=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# 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-daemon.sock
# Multiple settings at once
echo "setting set speech#rate=0.8;sound#volume=0.7;general#punctuation_level=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-daemon.sock
# Reset all settings to defaults
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
....
===== Clipboard Operations
....
# Place text into clipboard
echo "command clipboard This text will be copied to clipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command clipboard This text will be copied to clipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Export clipboard to file
echo "command exportclipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command exportclipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
....
===== Application Control
....
# Quit Fenrir
echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
....
==== Command Reference
@@ -1338,6 +1351,7 @@ echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-dea
* `+command say <text>+` - Speak the specified text
* `+command interrupt+` - Stop current speech
* `+command tempdisablespeech+` - Disable speech until next key press
* `+ls+` / `+list+` / `+command ls+` / `+command list+` - List registered Fenrir instances
*Clipboard Commands:*
@@ -1475,6 +1489,8 @@ By default we ship two sound packs.
. `+default-wav+` wav encoded, just for compatibility
Sound packs are located at /usr/share/sounds/fenrirscreenreader/
User sound packs in `+~/.local/stormux/fenrir/sounds/+` are loaded before
system sound packs.
Sound volume controls how loud the sounds for your selected sound pack
are.
@@ -1490,38 +1506,38 @@ Values: `+0.0+` is quietest, `+1.0+` is loudest.
The generic sound driver uses shell commands for play sound and
frequencies.
`+genericPlayFileCommand+` defines the command that is used to play a
`+generic_play_file_command+` defines the command that is used to play a
sound file.
....
generic_play_file_command=<your command for playing a file>
....
`+genericFrequencyCommand+` defines the command that is used playing
`+generic_frequency_command+` defines the command that is used playing
frequencies.
....
generic_frequency_command=<your command for playing a frequence>
....
The following variables are substituted in `+genericPlayFileCommand+`
and `+genericFrequencyCommand+`:
The following variables are substituted in `+generic_play_file_command+`
and `+generic_frequency_command+`:
* `+fenrirVolume+` = the current volume setting
* `+fenrirSoundFile+` = the sound file for an sound icon
* `+fenrirFrequence+` = the frequency to play
* `+fenrirDuration+` = the duration of the frequency
* `+fenrir_volume+` = the current volume setting
* `+fenrir_sound_file+` = the sound file for an sound icon
* `+fenrir_frequency+` = the frequency to play
* `+fenrir_duration+` = the duration of the frequency
Example genericPlayFileCommand (default)
Example generic_play_file_command (default)
....
generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile
generic_play_file_command=play -q -v fenrir_volume fenrir_sound_file
....
Example genericFrequencyCommand (default)
Example generic_frequency_command (default)
....
generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
generic_frequency_command=play -q -v fenrir_volume -n -c1 synth fenrir_duration sine fenrir_frequency
....
==== Speech
@@ -1535,8 +1551,14 @@ enabled=True
Values: on=`+True+`, off=`+False+`
# Select speech driver, options are speechdDriver (default),
genericDriver or espeakDriver: driver=speechdDriver #driver=espeakDriver
genericDriver, dectalkDriver, dectalkUsbDriver, litetalkDriver,
doubletalkDriver or tripletalkDriver: driver=speechdDriver
#driver=genericDriver
#driver=dectalkDriver
#driver=dectalkUsbDriver
#driver=litetalkDriver
#driver=doubletalkDriver
#driver=tripletalkDriver
This Selects the driver used to generate speech output.
@@ -1664,13 +1686,39 @@ the pico module:
language=de-DE
....
Serial hardware speech drivers use a serial device. Set an explicit path.
....
hardware_device=/dev/ttyACM0
hardware_device=/dev/ttyUSB0
hardware_device=/dev/ttyS0
....
Hardware speech drivers use 9600 baud by default.
....
hardware_baud_rate=9600
....
The `+doubletalkDriver+` targets DoubleTalk LT-style serial devices. It does
not support the internal DoubleTalk PC ISA card.
The `+dectalkUsbDriver+` autodetects the native Access Solutions DECtalk USB,
USB ID `+0dd0:2001+`. It requires PyUSB and a libusb backend and ignores the
serial device and baud-rate settings. From a source checkout, install the USB
dependencies with `+python3 -m pip install '.[dectalk_usb]'+`. A non-root
Fenrir process needs a local udev rule granting its account read/write access
to `+0dd0:2001+`; do not make the device world-writable. Other USB hardware
speech synthesizers are supported only when Linux exposes them as a serial tty
such as `+/dev/ttyACM0+` or `+/dev/ttyUSB0+`.
Read new text as it occurs auto_read_incoming=True Values: on=`+True+`,
off=`+False+`
==== Screen
The settings for screens, (TTY, PTY) are configured in the `+[screen]+`
section.
The settings for screen access are configured in the `+[screen]+`
section. `+vcsaDriver+` is used for Linux TTYs, and `+ptyDriver+` is
used by X11 terminal mode.
The driver to get the information from the screen:
@@ -1780,7 +1828,7 @@ keyboard_layout=desktop
....
Values: An absolute Path to a Keyboard definition file or a Filename
without extension located in `+/etc/fenrir/keyboard+`
without extension located in `+/etc/fenrirscreenreader/keyboard+`
Announce characters while typing.
@@ -1848,7 +1896,7 @@ the current punctuation and dict file in use:
punctuation_profile=default
....
Values: Text, see available profiles in `+/etc/fenrir/punctuation+` or
Values: Text, see available profiles in `+/etc/fenrirscreenreader/punctuation+` or
in `+sourceTree/config/punctuation+`
The current punctuation level in use:
@@ -1880,7 +1928,8 @@ link:#export clipboard to file[export clipboard to file]. The variable
`+$user+` is replaced by the current logged username.
....
clipboardExportPath=/tmp/fenrirClipboard
[clipboard]
clipboard_export_path=/tmp/fenrirClipboard
....
Values: Text, Systemfilepath
@@ -1888,6 +1937,7 @@ Values: Text, Systemfilepath
The number of available clipboards:
....
[clipboard]
number_of_clipboards=10
....
@@ -1954,19 +2004,20 @@ Values: Text, see aspell dictionary's.
Folder Path for your scripts "scriptKey" functionality:
....
scriptPath=/usr/share/fenrirscreenreader/scripts
script_path=/usr/share/fenrirscreenreader/scripts
....
Values: Text, Existing path on file system.
Values: Text, Existing path on file system. User scripts in
`+~/.local/stormux/fenrir/+` are loaded before this configured path.
Override commands or create new ones without changing the Fenrir
defaults:
....
commandPath=/usr/share/fenrirscreenreader/commands
command_path=/usr/share/fenrirscreenreader/commands
....
Values: Text, Existing path on file system. Subfolders in commandPath
Values: Text, Existing path on file system. Subfolders in command_path
are:
* `+commands+` = to create shortcut commands
@@ -2102,14 +2153,15 @@ Values: on=`+True+`, off=`+False+`
=== Scripting
Scripts can be in any language, bash, python, sh or others. Place your
scripts in the directory /usr/share/fenrirscreenreader/scripts/ (the
path is configurable in settings.conf). The script key is the
personal scripts in `+~/.local/stormux/fenrir/+`, or place system scripts in
the directory /usr/share/fenrirscreenreader/scripts/ (the path is configurable
in settings.conf). The script key is the
applications key. Usually this key can be found on the keyboard located
just left of the right most control key. When you name a script, the key
name appears in the script seperated by the sequence [.underline]#-#.
So, for example, if you have a python weather script you want assigned
to the script key plus the letter w you would name the script
/usr/share/fenrirscreenreader/scripts/weather[.underline]##-##key_w.py
~/.local/stormux/fenrir/weather[.underline]##-##key_w.py
Then, to access the script, simply press the script key and the letter
w. Scripts must be executable. So, make sure to chmod 755 your script
when you place it in the scripts directory. The script gets some
@@ -2233,13 +2285,13 @@ that shows the issue.
==== How-to create a debug file
. Delete old debug stuff +
`+sudo rm /var/log/fenrir.log+`
. Start fenrir in debug mode +
`+sudo fenrir -d+`
. Do your stuff to reproduce the problem
. Stop fenrir (`+fenrirKey + q+`)
the debug file is located in `+/var/log/fenrir.log+`
the debug file is located in `+/tmp/fenrir.log+`. If another Fenrir debug
instance is already using it, check `+/tmp/fenrir2.log+`,
`+/tmp/fenrir3.log+`, etc.
Please be as precise as possible to make it easy to solve the problem.
+98 -26
View File
@@ -38,6 +38,7 @@ Navigate the screen without moving the text cursor. Essential for examining cont
### Navigation (Desktop Layout)
- `Ctrl` - Stop speech (shut up)
- `Fenrir + H` - Tutorial mode
- `Fenrir + Ctrl + H` - Speech history
- `Fenrir + Q` - Quit Fenrir
- `Fenrir + Keypad 5` - Read current screen
- `Keypad 8` - Read current line
@@ -77,7 +78,13 @@ Navigate the screen without moving the text cursor. Essential for examining cont
## Configuration
### Settings File
Main configuration: `/etc/fenrir/settings/settings.conf`
Main configuration:
- **System-wide**: `/etc/fenrirscreenreader/settings/settings.conf`
- **User-local**: `~/.local/share/stormux/fenrirscreenreader/settings/settings.conf`
When Fenrir runs as a regular user, including `fenrir -x`, it loads the user-local settings file if it exists. If it does not exist, Fenrir falls back to the system settings file, but saving settings creates or updates the user-local file. When Fenrir runs as root, settings save to the system-wide file by default.
User sound themes are loaded before system themes from `~/.local/stormux/fenrir/sounds/`. User scripts are loaded before configured system scripts from `~/.local/stormux/fenrir/`.
### Key Sections
- `[speech]` - Speech synthesis settings
@@ -94,6 +101,9 @@ driver=speechdDriver
rate=0.5
pitch=0.5
volume=1.0
hardware_device=/dev/ttyS0
hardware_baud_rate=9600
history_size=50
[sound]
enabled=True
@@ -132,59 +142,73 @@ enable_command_remote=True # allow command execution
### Basic Usage with socat
#### Instance Discovery
```bash
# List registered Fenrir instances and their socket paths
echo "ls" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
In X terminal mode (`fenrir -x`), multiple Fenrir instances can run at the same
time. Each instance has a private socket at `/tmp/fenrirscreenreader-<pid>.sock`,
and one instance may also own the standard control socket. Use `ls` or
`command ls` on the standard socket to find the private socket for a specific
instance. Commands sent to the standard socket are handled by its owner when
possible; otherwise they are forwarded to a registered private socket,
preferring the sender's Fenrir ancestor when one can be found.
#### Speech Control
```bash
# Interrupt current speech
echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Speak custom text
echo "command say Hello, this is a test" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command say Hello, this is a test" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Temporarily disable speech
echo "command tempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command tempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
#### Settings Control
```bash
# Enable highlight tracking
echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Change speech parameters
echo "setting set speech#rate=0.8" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set speech#pitch=0.6" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set speech#volume=0.9" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set speech#rate=0.8" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set speech#pitch=0.6" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set speech#volume=0.9" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Change punctuation level (none/some/most/all)
echo "setting set general#punctuation_level=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set general#punctuation_level=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Voice and TTS control
echo "setting set speech#voice=en-us+f3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set speech#module=espeak-ng" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting set speech#voice=en-us+f3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set speech#module=espeak-ng" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Multiple settings at once
echo "setting set speech#rate=0.8;sound#volume=0.7;general#punctuation_level=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-daemon.sock
# Reset all settings
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Save settings
echo "setting save" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting saveas /tmp/my-settings.conf" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "setting save" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting saveas /tmp/my-settings.conf" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
#### Clipboard Operations
```bash
# Add text to clipboard
echo "command clipboard Text to copy" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command clipboard Text to copy" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Export clipboard to file
echo "command exportclipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command exportclipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
#### Application Control
```bash
# Quit Fenrir
echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
### Command Reference
@@ -193,6 +217,7 @@ echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-dea
- `command say <text>` - Speak text
- `command interrupt` - Stop speech
- `command tempdisablespeech` - Disable until next key
- `ls` / `list` / `command ls` / `command list` - List registered Fenrir instances
**Settings Commands:**
- `setting set <section>#<key>=<value>` - Change setting
@@ -219,7 +244,7 @@ echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-dea
#### Bash Helper Function
```bash
fenrir_say() {
echo "command say $1" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
echo "command say $1" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
}
# Usage
@@ -232,7 +257,7 @@ import socket
import os
def send_fenrir_command(command):
socket_path = "/tmp/fenrirscreenreader-deamon.sock"
socket_path = "/tmp/fenrirscreenreader-daemon.sock"
if os.path.exists(socket_path):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
@@ -287,6 +312,9 @@ Fenrir automatically detects and provides audio feedback for progress indicators
- **Automatic**: Works with downloads, compilations, installations
- **Remote control**: Enable via socket commands
Fenrir detects stable progress structures rather than application-specific
status formats, which change too frequently to support reliably.
### Spell Checking
- `Fenrir + S` - Spell check current word
- `Fenrir + S S` - Add word to dictionary
@@ -311,6 +339,25 @@ Fenrir automatically detects and provides audio feedback for progress indicators
### Speech Drivers
- **speechdDriver** - Speech-dispatcher (recommended)
- **genericDriver** - Command-line TTS (espeak, etc.)
- **dectalkDriver** - Serial DECtalk-compatible hardware speech
- **dectalkUsbDriver** - Native Access Solutions DECtalk USB (`0dd0:2001`)
- **litetalkDriver** - Serial LiteTalk-compatible hardware speech
- **doubletalkDriver** - Serial DoubleTalk LT-compatible hardware speech
- **tripletalkDriver** - Serial TripleTalk-compatible hardware speech
For serial hardware speech, set `speech#hardware_device` to an explicit path.
RPITalk gadget mode usually appears as `/dev/ttyACM0`; USB serial adapters
usually appear as `/dev/ttyUSB0`; built-in serial ports may be `/dev/ttyS0`.
The default baud rate is `9600`. `doubletalkDriver` targets
DoubleTalk LT-style serial devices, not the internal DoubleTalk PC ISA card.
The `dectalkUsbDriver` autodetects the native Access Solutions DECtalk USB and
ignores both serial settings. It requires PyUSB and a libusb backend. From a
source checkout, install them with
`python3 -m pip install '.[dectalk_usb]'`. A non-root Fenrir process also needs
a local udev rule granting its account read/write access to USB ID
`0dd0:2001`; do not make the device world-writable. USB TripleTalk devices work
only if Linux exposes them as a serial tty such as `/dev/ttyACM0` or
`/dev/ttyUSB0`.
### Sound Drivers
- **genericDriver** - Sox-based (default)
@@ -318,7 +365,7 @@ Fenrir automatically detects and provides audio feedback for progress indicators
### Input Drivers
- **evdevDriver** - Linux evdev (recommended for Linux)
- **ptyDriver** - Terminal emulation (cross-platform)
- **x11Driver** - X11 terminal-scoped input for `fenrir -x`
### Screen Drivers
- **vcsaDriver** - Linux VCSA devices (TTY)
@@ -341,11 +388,35 @@ fenrir [OPTIONS]
- `-o, --options SECTION#SETTING=VALUE;..` - Override settings
- `-d, --debug` - Enable debug mode
- `-p, --print` - Print debug to screen
- `-e, --emulated-pty` - PTY emulation for desktop use
- `-E, --emulated-evdev` - PTY + evdev emulation
- `-F, --force-all-screens` - Ignore ignoreScreen setting
- `-x, --x11` - PTY + X11 keyboard input scoped to the terminal window
- `--x11-window-id WINDOWID` - X11 window id to use for `--x11` terminal mode
- `-F, --force-all-screens` - Ignore ignore_screen setting
- `-i, -I, --ignore-screen SCREEN` - Ignore specific screen(s), can be used multiple times
### X11 Terminal Mode
Use `fenrir -x` to run Fenrir in an X11 terminal as a normal user:
```bash
fenrir -x
```
This mode uses PTY screen monitoring and X11 keyboard input. It is intended for GUI terminals such as xterm and VTE-based terminals, while keeping Fenrir key handling scoped to that terminal window. Desktop screen readers keep their global bindings outside the focused terminal.
Fenrir normally detects the target terminal from `WINDOWID` or the active X11 window. If needed, pass a window id explicitly:
```bash
fenrir -x --x11-window-id 0x123456
```
X11 terminal mode uses the same keyboard layout files as TTY Fenrir. Supported Fenrir keys include numpad Insert/`KEY_KP0`, CapsLock/`KEY_CAPSLOCK`, Insert/`KEY_INSERT`, and Super/Windows/`KEY_META`. Compose script keys are exposed as `KEY_COMPOSE`.
In this mode Fenrir runs as a regular user. Settings are loaded from `~/.local/share/stormux/fenrirscreenreader/settings/settings.conf` when present, otherwise from `/etc/fenrirscreenreader/settings/settings.conf`; saving settings creates or updates the user-local file. User sound themes and scripts are read first from `~/.local/stormux/fenrir/sounds/` and `~/.local/stormux/fenrir/`.
This mode requires `python-xlib`.
For a dedicated PTY/terminal screen reader, see TDSR: https://github.com/tspivey/tdsr
## Troubleshooting
### No Speech
@@ -366,7 +437,7 @@ fenrir [OPTIONS]
### Debug Mode
```bash
sudo fenrir -f -d
# Debug output goes to /var/log/fenrir.log
# Debug output goes to /tmp/fenrir.log
```
## Getting Help
@@ -379,5 +450,6 @@ sudo fenrir -f -d
## See Also
- [README.md](../README.md) - Installation and basic setup
- [diff_review_mode.md](./diff_review_mode.md) - Diff review workflow and key bindings
- [settings.conf](../config/settings/settings.conf) - Configuration reference
- `man fenrir` - Manual page
- `man fenrir` - Manual page
+54 -31
View File
@@ -857,31 +857,35 @@ Values: ''0.0'' is quietest, ''1.0'' is loudest.
=== Generic Driver ===
The generic sound driver uses shell commands for play sound and frequencies.
''genericPlayFileCommand'' defines the command that is used to play a sound file.
''generic_play_file_command'' defines the command that is used to play a sound file.
generic_play_file_command=<your command for playing a file>
''genericFrequencyCommand'' defines the command that is used playing frequencies.
''generic_frequency_command'' defines the command that is used playing frequencies.
generic_frequency_command=<your command for playing a frequence>
The following variables are substituted in ''genericPlayFileCommand'' and ''genericFrequencyCommand'':
* ''fenrirVolume'' = the current volume setting
* ''fenrirSoundFile'' = the sound file for an sound icon
* ''fenrirFrequence'' = the frequency to play
* ''fenrirDuration'' = the duration of the frequency
The following variables are substituted in ''generic_play_file_command'' and ''generic_frequency_command'':
* ''fenrir_volume'' = the current volume setting
* ''fenrir_sound_file'' = the sound file for an sound icon
* ''fenrir_frequency'' = the frequency to play
* ''fenrir_duration'' = the duration of the frequency
Example genericPlayFileCommand (default)
generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile
Example genericFrequencyCommand (default)
generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
Example generic_play_file_command (default)
generic_play_file_command=play -q -v fenrir_volume fenrir_sound_file
Example generic_frequency_command (default)
generic_frequency_command=play -q -v fenrir_volume -n -c1 synth fenrir_duration sine fenrir_frequency
==== Speech ====
Speech is configured in section ''[speech]''.
Turn speech on or off:
enabled=True
Values: on=''True'', off=''False''
# Select speech driver, options are speechdDriver (default), genericDriver or espeakDriver:
# Select speech driver, options are speechdDriver (default), genericDriver, dectalkDriver, dectalkUsbDriver, litetalkDriver, doubletalkDriver or tripletalkDriver:
driver=speechdDriver
#driver=espeakDriver
#driver=genericDriver
#driver=dectalkDriver
#driver=dectalkUsbDriver
#driver=litetalkDriver
#driver=doubletalkDriver
#driver=tripletalkDriver
Select the driver used to generate speech output.
@@ -890,7 +894,11 @@ Select the driver used to generate speech output.
Available Drivers:
* ''genericDriver'' using the generic driver, for Fenrir <1.5 this is not available
* ''speechdDriver'' using speech-dispatcher, for Fenrir <1.5 just use ''speechd''
* ''espeakDriver'' using the espeak directly, for Fenrir <1.5 just use ''espeak''
* ''dectalkDriver'' using DECtalk-compatible serial hardware or RPITalk
* ''dectalkUsbDriver'' using the native Access Solutions DECtalk USB
* ''litetalkDriver'' using LiteTalk-compatible serial hardware or RPITalk
* ''doubletalkDriver'' using DoubleTalk LT-compatible serial hardware
* ''tripletalkDriver'' using TripleTalk-compatible serial hardware
The rate selects how fast Fenrir will speak.
rate=0.65
@@ -921,6 +929,17 @@ Select the language you want Fenrir to use.
language=english-us
Values: Text, see your TTS synths documentation what is available.
Serial hardware speech drivers use a serial device. Set an explicit path.
hardware_device=/dev/ttyACM0
hardware_device=/dev/ttyUSB0
hardware_device=/dev/ttyS0
Hardware speech drivers use 9600 baud by default.
hardware_baud_rate=9600
The doubletalkDriver targets DoubleTalk LT-style serial devices. It does not support the internal DoubleTalk PC ISA card.
The dectalkUsbDriver autodetects the native Access Solutions DECtalk USB, USB ID 0dd0:2001. It requires PyUSB and a libusb backend and ignores the serial device and baud-rate settings. Other USB hardware speech synthesizers are supported only when Linux exposes them as a serial tty such as /dev/ttyACM0 or /dev/ttyUSB0.
Read new text as it occurs
auto_read_incoming=True
Values: on=''True'', off=''False''
@@ -928,22 +947,22 @@ Values: on=''True'', off=''False''
=== Generic Driver ===
The generic speech driver uses shell commands for speech synthisus.
''genericSpeechCommand'' defines the command that is executed for creating speech
The following variables are substituted in ''genericSpeechCommand'':
* ''FenrirText'' = is the text that should be spoken
* ''fenrirModule'' = may be the speech module like used in speech-dispatcher, not every TTY needs this
* ''fenrirLanguage'' = the language to speak in
* ''fenrirVoice'' = is the current voice that should be used
* ''fenrirVolume'' = is replaced with the current volume
* ''fenrirPitch'' = is replaced with the current pitch
* ''fenrirRate'' = is replaced with the current speed (speech rate)
''generic_speech_command'' defines the command that is executed for creating speech
The following variables are substituted in ''generic_speech_command'':
* ''fenrir_text'' = is the text that should be spoken
* ''fenrir_module'' = may be the speech module like used in speech-dispatcher, not every TTY needs this
* ''fenrir_language'' = the language to speak in
* ''fenrir_voice'' = is the current voice that should be used
* ''fenrir_volume'' = is replaced with the current volume
* ''fenrir_pitch'' = is replaced with the current pitch
* ''fenrir_rate'' = is replaced with the current speed (speech rate)
Example genericSpeechCommand (default):
generic_speech_command=espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice "fenrirText"
Example generic_speech_command (default):
generic_speech_command=espeak -a fenrir_volume -s fenrir_rate -p fenrir_pitch -v fenrir_voice "fenrir_text"
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 generic_speech_command. They are needed to calculate the abstract range in volume, rate and pitch 0.0 - 1.0.
FenrirMinVolume=0
fenrir_min_volume=0
fenrir_max_volume=200
fenrir_min_pitch=0
fenrir_max_pitch=99
@@ -1013,7 +1032,8 @@ Values: Integer,
* ''0'' = display size
* ''>0'' number of cells
==== Screen ====
The settings for screens, (TTY, PTY) are configured in the ''[screen]'' section.
The settings for screen access are configured in the ''[screen]'' section.
''vcsaDriver'' is used for Linux TTYs, and ''ptyDriver'' is used by X11 terminal mode.
The driver to get the information from the screen:
driver=vcsaDriver
@@ -1115,10 +1135,12 @@ Values: on=''True'', off=''False''
Specify the path where the clipboard should be exported to.
See [[#export clipboard to file|export clipboard to file]].
The variable ''$user'' is replaced by the current logged username.
clipboardExportPath=/tmp/fenrirClipboard
[clipboard]
clipboard_export_path=/tmp/fenrirClipboard
Values: Text, Systemfilepath
The number of available clipboards:
[clipboard]
number_of_clipboards=10
Values: Integer, 1 - 999
@@ -1297,10 +1319,11 @@ Please report Bugs and feature requests to:
for bugs please provide a [[#Howto create a debug file|debug]] file that shows the issue.
==== How-to create a debug file ====
- Delete old debug stuff\\ ''sudo rm /var/log/fenrir.log''
- Start fenrir in debug mode\\ ''sudo fenrir -d''
- Do your stuff to reproduce the problem
- Stop fenrir (''fenrirKey + q'')
the debug file is located in ''/var/log/fenrir.log''
the debug file is located in ''/tmp/fenrir.log''. If another Fenrir debug
instance is already using it, check ''/tmp/fenrir2.log'',
''/tmp/fenrir3.log'', etc.
Please be as precise as possible to make it easy to solve the problem.
-12
View File
@@ -744,7 +744,6 @@ msgid "Script file is not executable"
msgstr "Файл скрипта не исполняемый"
#: ../src/fenrirscreenreader/commands/commands/temp_disable_speech.py:17
#: ../src/fenrirscreenreader/commands/onByteInput/15000-enable_temp_speech.py:17
#: ../src/fenrirscreenreader/commands/onKeyInput/15000-enable_temp_speech.py:17
msgid "disables speech until next keypress"
msgstr "Отключить речь пока не нажата следующая клавиша"
@@ -898,7 +897,6 @@ msgid "speech disabled"
msgstr "Речь выключена"
#: ../src/fenrirscreenreader/commands/commands/toggle_speech.py:25
#: ../src/fenrirscreenreader/commands/onByteInput/15000-enable_temp_speech.py:24
#: ../src/fenrirscreenreader/commands/onKeyInput/15000-enable_temp_speech.py:28
msgid "speech enabled"
msgstr "Речь включена"
@@ -1089,15 +1087,6 @@ msgstr "Мерцание"
msgid "default"
msgstr "По умолчанию"
#: ../src/fenrirscreenreader/core/byteManager.py:103
#: ../src/fenrirscreenreader/core/byteManager.py:105
msgid "Sticky Mode On"
msgstr "Режим залипания включен"
#: ../src/fenrirscreenreader/core/byteManager.py:109
msgid "bypass"
msgstr ""
#: ../src/fenrirscreenreader/core/fenrirManager.py:26
msgid "Start Fenrir"
msgstr "fenrir запущен"
@@ -1216,4 +1205,3 @@ msgstr "Меню"
#: ../src/fenrirscreenreader/core/vmenuManager.py:234
msgid "Action"
msgstr "Действие"
-1
View File
@@ -1,7 +1,6 @@
daemonize
dbus-python
evdev
pexpect
pyenchant
pyperclip
pyte
+4 -1
View File
@@ -103,9 +103,12 @@ setup(
"pyudev>=0.21.0",
"setuptools",
"setproctitle",
"pexpect",
"pyte>=0.7.0",
],
extras_require={
"dectalk_usb": ["pyusb>=1.2.1"],
"x11": ["python-xlib>=0.33"],
},
)
if not forceSettingsFlag:
+19 -11
View File
@@ -41,7 +41,7 @@ def create_argument_parser():
argumentParser.add_argument(
'-s', '--setting',
metavar='SETTING-FILE',
default='/etc/fenrir/settings/settings.conf',
default=None,
help='Path to custom settings file'
)
argumentParser.add_argument(
@@ -61,14 +61,15 @@ def create_argument_parser():
help='Print debug messages to screen'
)
argumentParser.add_argument(
'-e', '--emulated-pty',
'-x', '--x11',
action='store_true',
help='Use PTY emulation with escape sequences for input (enables desktop/X/Wayland usage)'
help='Use PTY emulation with X11 keyboard input scoped to the terminal window'
)
argumentParser.add_argument(
'-E', '--emulated-evdev',
action='store_true',
help='Use PTY emulation with evdev for input (single instance)'
'--x11-window-id',
metavar='WINDOWID',
default='',
help='X11 window id to use for --x11 terminal mode'
)
argumentParser.add_argument(
'-F',
@@ -91,8 +92,8 @@ def validate_arguments(cliArgs):
if option and ('#' not in option or '=' not in option):
return False, f"Invalid option format: {option}\nExpected format: SECTION#SETTING=VALUE"
if cliArgs.emulated_pty and cliArgs.emulated_evdev:
return False, "Cannot use both --emulated-pty and --emulated-evdev simultaneously"
if cliArgs.x11_window_id and not cliArgs.x11:
return False, "--x11-window-id requires --x11"
return True, None
@@ -105,14 +106,17 @@ def run_fenrir():
fenrirApp.proceed()
except Exception as e:
print(f"Error starting Fenrir: {e}", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("Interrupted", file=sys.stderr)
sys.exit(1)
finally:
if fenrirApp and hasattr(fenrirApp, 'cleanup_on_error'):
try:
fenrirApp.cleanup_on_error()
except Exception as cleanup_error:
print(
f"Error during cleanup: {cleanup_error}", file=sys.stderr)
sys.exit(1)
finally:
if fenrirApp:
del fenrirApp
# Clean up PID file if it exists
@@ -124,6 +128,10 @@ def run_fenrir():
pass
def should_run_foreground(cliArgs):
return cliArgs.foreground or cliArgs.x11
def main():
global cliArgs
argumentParser = create_argument_parser()
@@ -135,7 +143,7 @@ def main():
argumentParser.error(errorMsg)
sys.exit(1)
if cliArgs.foreground or cliArgs.emulated_pty:
if should_run_foreground(cliArgs):
# Run directly in foreground
run_fenrir()
else:
@@ -13,6 +13,8 @@ from fenrirscreenreader.core.i18n import _
class command:
help_visible = False
def __init__(self):
pass
@@ -22,7 +24,7 @@ class command:
self.env["runtime"]["MemoryManager"].add_index_list(
"clipboardHistory",
self.env["runtime"]["SettingsManager"].get_setting_as_int(
"general", "number_of_clipboards"
"clipboard", "number_of_clipboards"
),
)
@@ -34,6 +34,7 @@ class command:
module = self.env["commandBuffer"]["lastTestedModule"]
voice = self.env["commandBuffer"]["lastTestedVoice"]
language = self.env["commandBuffer"].get("lastTestedLanguage")
self.env["runtime"]["OutputManager"].present_text(
f"Applying {voice} from {module}", interrupt=True
@@ -46,6 +47,7 @@ class command:
old_driver = SettingsManager.get_setting("speech", "driver")
old_module = SettingsManager.get_setting("speech", "module")
old_voice = SettingsManager.get_setting("speech", "voice")
old_language = SettingsManager.get_setting("speech", "language")
try:
# Apply new settings to runtime only (use set_setting to update
@@ -55,6 +57,10 @@ class command:
)
SettingsManager.set_setting("speech", "module", module)
SettingsManager.set_setting("speech", "voice", voice)
if language:
SettingsManager.set_setting(
"speech", "language", language
)
# Apply to speech driver instance directly
if "SpeechDriver" in self.env["runtime"]:
@@ -62,6 +68,8 @@ class command:
# Set the module and voice on the driver instance
SpeechDriver.set_module(module)
if language:
SpeechDriver.set_language(language)
SpeechDriver.set_voice(voice)
self.env["runtime"]["OutputManager"].present_text(
@@ -77,6 +85,9 @@ class command:
SettingsManager.set_setting("speech", "driver", old_driver)
SettingsManager.set_setting("speech", "module", old_module)
SettingsManager.set_setting("speech", "voice", old_voice)
SettingsManager.set_setting(
"speech", "language", old_language
)
self.env["runtime"]["OutputManager"].present_text(
f"Failed to apply voice, reverted: {str(e)}",
@@ -1,105 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import os
from fenrirscreenreader.core import debug
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 _("cycles between available keyboard layouts")
def get_available_layouts(self):
"""Get list of available keyboard layout files"""
layouts = []
# Check standard locations for keyboard layouts
settings_root = "/etc/fenrirscreenreader/"
if not os.path.exists(settings_root):
# Fallback to source directory
import fenrirscreenreader
fenrir_path = os.path.dirname(fenrirscreenreader.__file__)
settings_root = fenrir_path + "/../../config/"
keyboard_path = settings_root + "keyboard/"
if os.path.exists(keyboard_path):
for file in os.listdir(keyboard_path):
if (
file.endswith(".conf")
and not file.startswith("__")
and not file.lower().startswith("pty")
):
layout_name = file.replace(".conf", "")
if layout_name not in layouts:
layouts.append(layout_name)
# Ensure we have at least basic layouts
if not layouts:
layouts = ["desktop", "laptop"]
else:
layouts.sort()
return layouts
def run(self):
current_layout = self.env["runtime"]["SettingsManager"].get_setting(
"keyboard", "keyboard_layout"
)
# Extract layout name from full path if needed
if "/" in current_layout:
current_layout = os.path.basename(current_layout).replace(
".conf", ""
)
# Get available layouts
available_layouts = self.get_available_layouts()
# Find next layout in cycle
try:
current_index = available_layouts.index(current_layout)
next_index = (current_index + 1) % len(available_layouts)
except ValueError:
# If current layout not found, start from beginning
next_index = 0
next_layout = available_layouts[next_index]
# Update setting and reload shortcuts
self.env["runtime"]["SettingsManager"].set_setting(
"keyboard", "keyboard_layout", next_layout
)
# Reload shortcuts with new layout
try:
self.env["runtime"]["InputManager"].reload_shortcuts()
self.env["runtime"]["OutputManager"].present_text(
_("Switched to {} keyboard layout").format(next_layout),
interrupt=True,
)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
"Error reloading shortcuts: " + str(e), debug.DebugLevel.ERROR
)
self.env["runtime"]["OutputManager"].present_text(
_("Error switching keyboard layout"), interrupt=True
)
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("speak diff review key help")
def run(self):
self.env["runtime"]["DiffReviewManager"].present_key_help()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to next added diff line")
def run(self):
self.env["runtime"]["DiffReviewManager"].next_added()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to next character in diff line")
def run(self):
self.env["runtime"]["DiffReviewManager"].next_char()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to next diff file section")
def run(self):
self.env["runtime"]["DiffReviewManager"].next_file_section()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to next diff hunk")
def run(self):
self.env["runtime"]["DiffReviewManager"].next_hunk()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to next diff line")
def run(self):
self.env["runtime"]["DiffReviewManager"].next_line()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to next removed diff line")
def run(self):
self.env["runtime"]["DiffReviewManager"].next_removed()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to next word in diff line")
def run(self):
self.env["runtime"]["DiffReviewManager"].next_word()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to previous added diff line")
def run(self):
self.env["runtime"]["DiffReviewManager"].prev_added()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to previous character in diff line")
def run(self):
self.env["runtime"]["DiffReviewManager"].prev_char()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to previous diff file section")
def run(self):
self.env["runtime"]["DiffReviewManager"].prev_file_section()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to previous diff hunk")
def run(self):
self.env["runtime"]["DiffReviewManager"].prev_hunk()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to previous diff line")
def run(self):
self.env["runtime"]["DiffReviewManager"].prev_line()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to previous removed diff line")
def run(self):
self.env["runtime"]["DiffReviewManager"].prev_removed()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("move to previous word in diff line")
def run(self):
self.env["runtime"]["DiffReviewManager"].prev_word()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("speak diff summary")
def run(self):
self.env["runtime"]["DiffReviewManager"].present_summary()
def set_callback(self, callback):
pass
@@ -0,0 +1,32 @@
#!/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 _("exit diff review mode")
def run(self):
if not self.env["runtime"]["DiffReviewManager"].is_active():
return
self.env["runtime"]["DiffReviewManager"].disable_mode()
self.env["runtime"]["DiffReviewManager"].present_mode_message(
_("Diff review mode disabled."),
)
def set_callback(self, callback):
pass
@@ -26,7 +26,7 @@ class command:
def run(self):
clipboard_file_path = self.env["runtime"][
"SettingsManager"
].get_setting("general", "clipboard_export_path")
].get_setting("clipboard", "clipboard_export_path")
clipboard_file_path = clipboard_file_path.replace(
"$user", self.env["general"]["curr_user"]
)
@@ -4,33 +4,35 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import _thread
import importlib
import os
import pyperclip
from fenrirscreenreader.core import debug
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.utils import x_clipboard
def write_clipboard(clipboard):
"""Write the graphical clipboard without accessing Fenrir state."""
return x_clipboard.write_text(clipboard, scan_displays=True)
class command:
def __init__(self):
pass
self._task_ids = set()
def initialize(self, environment, script_path=""):
self.env = environment
self.script_path = script_path
def shutdown(self):
pass
task_manager = self.env["runtime"].get("BackgroundTaskManager")
if task_manager:
for task_id in self._task_ids:
task_manager.cancel_task(task_id)
self._task_ids.clear()
def get_description(self):
return _("Export current fenrir clipboard to X or GUI clipboard")
def run(self):
_thread.start_new_thread(self._thread_run, ())
def _thread_run(self):
try:
# Check if clipboard is empty
if self.env["runtime"]["MemoryManager"].is_index_list_empty(
@@ -46,52 +48,43 @@ class command:
"MemoryManager"
].get_index_list_element("clipboardHistory")
# Remember original display environment variable if it exists
original_display = os.environ.get("DISPLAY", "")
success = False
# Try different display options
for i in range(10):
display = f":{i}"
try:
# Set display environment variable
os.environ["DISPLAY"] = display
# Attempt to set clipboard content
# Weird workaround for some distros
importlib.reload(pyperclip)
pyperclip.copy(clipboard)
# If we get here without exception, we found a working
# display
success = True
break
except Exception:
# Failed for this display, try next one
continue
# Restore original display setting
if original_display:
os.environ["DISPLAY"] = original_display
else:
os.environ.pop("DISPLAY", None)
# Notify the user of the result
if success:
self.env["runtime"]["OutputManager"].present_text(
_("exported to the X session."), interrupt=True
)
else:
self.env["runtime"]["OutputManager"].present_text(
_(
"failed to export to X clipboard. No available display "
"found."
),
interrupt=True,
)
task_id = self.env["runtime"][
"BackgroundTaskManager"
].submit_task(
write_clipboard,
lambda result: self._handle_result(clipboard, result),
clipboard,
)
if task_id is not None:
self._task_ids.add(task_id)
except Exception as e:
self.env["runtime"]["OutputManager"].present_text(
str(e), sound_icon="", interrupt=False
)
def _handle_result(self, clipboard, result):
self._task_ids.discard(result.get("task_id"))
if result.get("succeeded") and result.get("value"):
sync_manager = self.env["runtime"].get("ClipboardSyncManager")
if sync_manager:
sync_manager.mark_written_to_x(clipboard)
self.env["runtime"]["OutputManager"].present_text(
_("exported to the X session."), interrupt=True
)
return
if not result.get("succeeded") and result.get("error"):
self.env["runtime"]["DebugManager"].write_debug_out(
"Clipboard export failed: " + result["error"],
debug.DebugLevel.ERROR,
)
self.env["runtime"]["OutputManager"].present_text(
_(
"failed to export to X clipboard. No available display found."
),
interrupt=True,
)
def set_callback(self, callback):
pass
@@ -22,7 +22,10 @@ class command:
return _("sends the following keypress to the terminal or application")
def run(self):
self.env["input"]["key_forward"] = 3
if self.env["runtime"]["InputManager"].no_key_pressed():
self.env["input"]["key_forward"] = 1
else:
self.env["input"]["key_forward"] = -1
self.env["runtime"]["OutputManager"].present_text(
_("Forward next keypress"), interrupt=True
)
@@ -26,7 +26,7 @@ class command:
def run(self):
clipboard_file_path = self.env["runtime"][
"SettingsManager"
].get_setting("general", "clipboard_export_path")
].get_setting("clipboard", "clipboard_export_path")
clipboard_file_path = clipboard_file_path.replace(
"$user", self.env["general"]["curr_user"]
)
@@ -4,84 +4,68 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import _thread
import importlib
import os
import pyperclip
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.utils import x_clipboard
def read_clipboard():
"""Read the graphical clipboard without accessing Fenrir state."""
return x_clipboard.read_text(scan_displays=True)
class command:
def __init__(self):
pass
self._task_ids = set()
def initialize(self, environment, script_path=""):
self.env = environment
self.script_path = script_path
def shutdown(self):
pass
task_manager = self.env["runtime"].get("BackgroundTaskManager")
if task_manager:
for task_id in self._task_ids:
task_manager.cancel_task(task_id)
self._task_ids.clear()
def get_description(self):
return _("imports the graphical clipboard to Fenrir's clipboard")
def run(self):
_thread.start_new_thread(self._thread_run, ())
task_id = self.env["runtime"]["BackgroundTaskManager"].submit_task(
read_clipboard, self._handle_result
)
if task_id is not None:
self._task_ids.add(task_id)
def _thread_run(self):
try:
# Remember original display environment variable if it exists
original_display = os.environ.get("DISPLAY", "")
clipboard_content = None
# Try different display options
for i in range(10):
display = f":{i}"
try:
# Set display environment variable
os.environ["DISPLAY"] = display
# Attempt to get clipboard content
# Weird workaround for some distros
importlib.reload(pyperclip)
clipboard_content = pyperclip.paste()
# If we get here without exception, we found a working
# display
if clipboard_content:
break
except Exception:
# Failed for this display, try next one
continue
# Restore original display setting
if original_display:
os.environ["DISPLAY"] = original_display
else:
os.environ.pop("DISPLAY", None)
# Process the clipboard content if we found any
if clipboard_content and isinstance(clipboard_content, str):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", clipboard_content
)
self.env["runtime"]["OutputManager"].present_text(
"Import to Clipboard",
sound_icon="CopyToClipboard",
interrupt=True,
)
self.env["runtime"]["OutputManager"].present_text(
clipboard_content, sound_icon="", interrupt=False
)
else:
self.env["runtime"]["OutputManager"].present_text(
"No text found in clipboard or no accessible display",
interrupt=True,
)
except Exception as e:
def _handle_result(self, result):
self._task_ids.discard(result.get("task_id"))
clipboard_content = result.get("value")
if not result.get("succeeded"):
self.env["runtime"]["OutputManager"].present_text(
str(e), sound_icon="", interrupt=False
result.get("error", "Clipboard import failed"),
sound_icon="",
interrupt=False,
)
return
if not clipboard_content or not isinstance(clipboard_content, str):
self.env["runtime"]["OutputManager"].present_text(
"No text found in clipboard or no accessible display",
interrupt=True,
)
return
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", clipboard_content
)
self.env["runtime"]["OutputManager"].present_text(
"Import to Clipboard",
sound_icon="CopyToClipboard",
interrupt=True,
)
self.env["runtime"]["OutputManager"].present_text(
clipboard_content, sound_icon="", interrupt=False
)
def set_callback(self, callback):
pass
@@ -18,7 +18,7 @@ class command:
self.env["runtime"]["MemoryManager"].add_index_list(
"clipboardHistory",
self.env["runtime"]["SettingsManager"].get_setting_as_int(
"general", "number_of_clipboards"
"clipboard", "number_of_clipboards"
),
)
@@ -38,7 +38,7 @@ class command:
self.env["screen"]["new_content_text"],
)
if curr_word.isspace():
if not curr_word or curr_word.isspace():
self.env["runtime"]["OutputManager"].present_text(
_("blank"), interrupt=True, flush=False
)
@@ -20,7 +20,7 @@ class command:
pass
def get_description(self):
return _("Phonetically spells the next word and moves review to it")
return _("Spells the current word")
def run(self):
self.env["runtime"][
@@ -29,30 +29,28 @@ class command:
(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
next_word,
curr_word,
end_of_screen,
line_break,
) = word_utils.get_next_word(
) = word_utils.get_current_word(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
self.env["screen"]["new_content_text"],
)
if next_word.isspace():
if not curr_word or curr_word.isspace():
self.env["runtime"]["OutputManager"].present_text(
_("blank"), interrupt=True, flush=False
)
else:
first_sequence = True
for c in next_word:
curr_char = char_utils.get_phonetic(c)
self.env["runtime"]["OutputManager"].present_text(
for index, curr_char in enumerate(curr_word):
char_utils.present_char_for_review(
self.env,
curr_char,
interrupt=first_sequence,
interrupt=index == 0,
announce_capital=True,
flush=False,
)
first_sequence = False
if end_of_screen:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "end_of_screen"
@@ -1,65 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.utils import char_utils
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _(
"phonetically presents the next character and set review to it"
)
def run(self):
self.env["runtime"][
"CursorManager"
].enter_review_mode_curr_text_cursor()
(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
next_char,
end_of_screen,
line_break,
) = char_utils.get_next_char(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
self.env["screen"]["new_content_text"],
)
next_char = char_utils.get_phonetic(next_char)
self.env["runtime"]["OutputManager"].present_text(
next_char, interrupt=True, announce_capital=True, flush=False
)
if end_of_screen:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "end_of_screen"
):
self.env["runtime"]["OutputManager"].present_text(
_("end of screen"),
interrupt=True,
sound_icon="EndOfScreen",
)
if line_break:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "line_break"
):
self.env["runtime"]["OutputManager"].present_text(
_("line break"), interrupt=False, sound_icon="EndOfLine"
)
def set_callback(self, callback):
pass
@@ -112,9 +112,9 @@ class command:
"review", "end_of_screen"
):
self.env["runtime"]["OutputManager"].present_text(
_("end of screen"),
_("start of screen"),
interrupt=True,
sound_icon="EndOfScreen",
sound_icon="StartOfScreen",
)
if line_break:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
@@ -1,65 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.utils import char_utils
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _(
"phonetically presents the previous character and set review to it"
)
def run(self):
self.env["runtime"][
"CursorManager"
].enter_review_mode_curr_text_cursor()
(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
prev_char,
end_of_screen,
line_break,
) = char_utils.get_prev_char(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
self.env["screen"]["new_content_text"],
)
prev_char = char_utils.get_phonetic(prev_char)
self.env["runtime"]["OutputManager"].present_text(
prev_char, interrupt=True, announce_capital=True, flush=False
)
if end_of_screen:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "end_of_screen"
):
self.env["runtime"]["OutputManager"].present_text(
_("end of screen"),
interrupt=True,
sound_icon="EndOfScreen",
)
if line_break:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "line_break"
):
self.env["runtime"]["OutputManager"].present_text(
_("line break"), interrupt=False, sound_icon="EndOfLine"
)
def set_callback(self, callback):
pass
@@ -50,9 +50,9 @@ class command:
"review", "end_of_screen"
):
self.env["runtime"]["OutputManager"].present_text(
_("end of screen"),
_("start of screen"),
interrupt=True,
sound_icon="EndOfScreen",
sound_icon="StartOfScreen",
)
def set_callback(self, callback):
@@ -95,9 +95,9 @@ class command:
"review", "end_of_screen"
):
self.env["runtime"]["OutputManager"].present_text(
_("end of screen"),
_("start of screen"),
interrupt=False,
sound_icon="EndOfScreen",
sound_icon="StartOfScreen",
)
if line_break:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
@@ -1,76 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.utils import char_utils
from fenrirscreenreader.utils import word_utils
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _(
"Phonetically spells the previous word and moves review to it"
)
def run(self):
self.env["runtime"][
"CursorManager"
].enter_review_mode_curr_text_cursor()
(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
prev_word,
end_of_screen,
line_break,
) = word_utils.get_prev_word(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
self.env["screen"]["new_content_text"],
)
if prev_word.isspace():
self.env["runtime"]["OutputManager"].present_text(
_("blank"), interrupt=True, flush=False
)
else:
first_sequence = True
for c in prev_word:
curr_char = char_utils.get_phonetic(c)
self.env["runtime"]["OutputManager"].present_text(
curr_char,
interrupt=first_sequence,
announce_capital=True,
flush=False,
)
first_sequence = False
if end_of_screen:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "end_of_screen"
):
self.env["runtime"]["OutputManager"].present_text(
_("end of screen"),
interrupt=True,
sound_icon="EndOfScreen",
)
if line_break:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "line_break"
):
self.env["runtime"]["OutputManager"].present_text(
_("line break"), interrupt=False, sound_icon="EndOfLine"
)
def set_callback(self, callback):
pass
@@ -50,9 +50,9 @@ class command:
"review", "end_of_screen"
):
self.env["runtime"]["OutputManager"].present_text(
_("end of screen"),
_("start of screen"),
interrupt=True,
sound_icon="EndOfScreen",
sound_icon="StartOfScreen",
)
if line_break:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
@@ -22,10 +22,7 @@ class command:
return _("Saves your current Fenrir settings so they are the default.")
def run(self):
settings_file = self.env["runtime"][
"SettingsManager"
].get_settings_file()
self.env["runtime"]["SettingsManager"].save_settings(settings_file)
self.env["runtime"]["SettingsManager"].save_settings()
self.env["runtime"]["OutputManager"].present_text(
_("Settings saved."), interrupt=True
)
@@ -0,0 +1,27 @@
#!/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 _("opens speech history")
def run(self):
self.env["runtime"]["SpeechHistoryManager"].open_history()
def set_callback(self, callback):
pass
@@ -4,25 +4,42 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import _thread
import os
import subprocess
from subprocess import PIPE
from subprocess import Popen
from fenrirscreenreader.core.i18n import _
def run_script(script_path, current_user):
"""Run an external command without accessing Fenrir's shared state."""
process = subprocess.Popen(
[script_path, current_user],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
stdout, stderr = process.communicate()
return {
"return_code": process.returncode,
"stdout": stdout,
"stderr": stderr,
}
class command:
def __init__(self):
pass
self._task_ids = set()
def initialize(self, environment, script_path=""):
self.env = environment
self.script_path = script_path
def shutdown(self):
pass
task_manager = self.env["runtime"].get("BackgroundTaskManager")
if task_manager:
for task_id in self._task_ids:
task_manager.cancel_task(task_id)
self._task_ids.clear()
def get_description(self):
return _("script: {0} fullpath: {1}").format(
@@ -49,29 +66,39 @@ class command:
interrupt=False,
)
return
_thread.start_new_thread(self._thread_run, ())
def _thread_run(self):
try:
callstring = (
self.script_path + " " + self.env["general"]["curr_user"]
)
p = Popen(callstring, stdout=PIPE, stderr=PIPE, shell=True)
stdout, stderr = p.communicate()
stdout = stdout.decode("utf-8")
stderr = stderr.decode("utf-8")
self.env["runtime"]["OutputManager"].interrupt_output()
if stderr != "":
self.env["runtime"]["OutputManager"].present_text(
str(stderr), sound_icon="", interrupt=False
)
if stdout != "":
self.env["runtime"]["OutputManager"].present_text(
str(stdout), sound_icon="", interrupt=False
)
except Exception as e:
task_id = self.env["runtime"][
"BackgroundTaskManager"
].submit_external_task(
run_script,
self._handle_result,
self.script_path,
self.env["general"]["curr_user"],
)
if task_id is not None:
self._task_ids.add(task_id)
def _handle_result(self, result):
self._task_ids.discard(result.get("task_id"))
if not result.get("succeeded"):
self.env["runtime"]["OutputManager"].present_text(
str(e), sound_icon="", interrupt=False
result.get("error", _("Script failed")),
sound_icon="",
interrupt=False,
)
return
output = result.get("value") or {}
stderr = output.get("stderr", "")
stdout = output.get("stdout", "")
self.env["runtime"]["OutputManager"].interrupt_output()
if stderr:
self.env["runtime"]["OutputManager"].present_text(
stderr, sound_icon="", interrupt=False
)
if stdout:
self.env["runtime"]["OutputManager"].present_text(
stdout, sound_icon="", interrupt=False
)
def set_callback(self, callback):
@@ -0,0 +1,27 @@
#!/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 _("toggle diff review mode")
def run(self):
self.env["runtime"]["DiffReviewManager"].toggle_mode()
def set_callback(self, callback):
pass
@@ -19,19 +19,47 @@ class command:
pass
def get_description(self):
self.env["runtime"]["HelpManager"].toggle_tutorial_mode()
return _(
"Exiting tutorial mode. To enter tutorial mode again press Fenrir+f1"
)
return _("enter or leave tutorial mode")
def run(self):
self.env["runtime"]["HelpManager"].toggle_tutorial_mode()
if self.env["runtime"]["HelpManager"].is_tutorial_mode():
help_manager = self.env["runtime"]["HelpManager"]
was_active = help_manager.is_tutorial_mode()
help_manager.toggle_tutorial_mode()
is_active = help_manager.is_tutorial_mode()
if was_active and is_active:
self.env["runtime"]["OutputManager"].present_text(
_(
"Entering tutorial mode. In this mode commands are described but not "
"executed. You can move through the list of commands with the up and "
"down arrow keys. To Exit tutorial mode press Fenrir+f1."
"Unable to exit tutorial mode because exclusive keyboard "
"capture could not be released. Press Fenrir+F1 or Escape "
"to try again."
),
interrupt=True,
)
elif is_active:
self.env["runtime"]["OutputManager"].present_text(
_(
"Entering tutorial mode. In this mode commands are "
"described but not executed. Use up and down to browse "
"actions and left and right to switch between Active "
"actions, Unbound actions, and Plugins. Press Space to "
"repeat the current item. To exit tutorial mode press "
"Fenrir+F1 or Escape. Active actions."
),
interrupt=True,
)
if self.env["runtime"]["HelpManager"].is_capture_degraded():
self.env["runtime"]["OutputManager"].present_text(
_(
"Warning: full keyboard capture is unavailable. "
"Unbound keys may reach the active application."
),
interrupt=False,
)
else:
self.env["runtime"]["OutputManager"].present_text(
_(
"Exiting tutorial mode. To enter tutorial mode again "
"press Fenrir+F1"
),
interrupt=True,
)
@@ -19,7 +19,6 @@ class command:
pass
def get_description(self):
self.env["runtime"]["VmenuManager"].toggle_vmenu_mode()
return _("Entering or Leaving v menu mode.")
def run(self):
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import subprocess
import time
from fenrirscreenreader.core.i18n import _
@@ -8,11 +7,14 @@ from fenrirscreenreader.core.i18n import _
class command:
def __init__(self):
pass
self._request_generation = 0
self._loading = False
def initialize(self, environment):
self.env = environment
self.testMessage = "This is a voice test. The quick brown fox jumps over the lazy dog."
self.test_message = (
"This is a voice test. The quick brown fox jumps over the lazy dog."
)
self.modules = []
self.voices = []
self.module_index = 0
@@ -22,7 +24,9 @@ class command:
self.lastAnnounceTime = 0
def shutdown(self):
pass
self._request_generation += 1
self._loading = False
self._leave_voice_browser(False)
def get_description(self):
return "Interactive voice browser with arrow key navigation"
@@ -36,34 +40,59 @@ class command:
"Starting voice browser", interrupt=True
)
# Load modules
self.modules = self.get_speechd_modules()
if not self.modules:
if self._loading:
return
self._loading = True
self._request_generation += 1
generation = self._request_generation
self.env["runtime"]["SpeechDiscoveryManager"].request_modules(
lambda modules, error: self._modules_ready(
generation, modules, error
)
)
def _modules_ready(self, generation, modules, error):
if generation != self._request_generation:
return
if error or not modules:
self._loading = False
self.env["runtime"]["OutputManager"].present_text(
"No speech modules found", interrupt=True
)
self.env["runtime"]["OutputManager"].play_sound("Error")
return
# Set current module
self.modules = modules
current_module = self.env["runtime"]["SettingsManager"].get_setting(
"speech", "module"
)
if current_module and current_module in self.modules:
self.module_index = self.modules.index(current_module)
self._request_current_module_voices(generation, True)
# Load voices
self.load_voices_for_current_module()
def _request_current_module_voices(self, generation, enter_browser=False):
module = self.modules[self.module_index]
self.env["runtime"]["OutputManager"].present_text(
f"Loading voices for {module}", interrupt=True
)
self.env["runtime"]["SpeechDiscoveryManager"].request_voices(
module,
lambda _module, voices, error: self._voices_ready(
generation, voices, error, enter_browser
),
)
# Set current voice
def _voices_ready(self, generation, voices, error, enter_browser):
if generation != self._request_generation:
return
self._loading = False
self.voices = voices if not error else []
self.voice_index = 0
current_voice = self.env["runtime"]["SettingsManager"].get_setting(
"speech", "voice"
)
if current_voice and current_voice in self.voices:
self.voice_index = self.voices.index(current_voice)
# Enter browser mode
self.enter_voice_browser()
if enter_browser:
self.enter_voice_browser()
self.announce_current_selection()
def enter_voice_browser(self):
@@ -112,6 +141,9 @@ class command:
def exit_voice_browser(self):
"""Exit voice browser and restore normal key bindings"""
self._leave_voice_browser(True)
def _leave_voice_browser(self, announce):
if not self.browserActive:
return
@@ -124,16 +156,10 @@ class command:
if "voiceBrowserInstance" in self.env["runtime"]:
del self.env["runtime"]["voiceBrowserInstance"]
self.env["runtime"]["OutputManager"].present_text(
"Voice browser exited", interrupt=True
)
def load_voices_for_current_module(self):
"""Load voices for current module"""
if self.module_index < len(self.modules):
module = self.modules[self.module_index]
self.voices = self.get_module_voices(module)
self.voice_index = 0 # Reset to first voice when changing modules
if announce:
self.env["runtime"]["OutputManager"].present_text(
"Voice browser exited", interrupt=True
)
def announce_current_selection(self):
"""Announce current module and voice"""
@@ -148,7 +174,7 @@ class command:
module = self.modules[self.module_index]
if self.voices and self.voice_index < len(self.voices):
voice = self.voices[self.voice_index]
voice = self.voices[self.voice_index].split("|", 1)[0]
self.env["runtime"]["OutputManager"].present_text(
f"{module}: {voice} ({self.voice_index + 1}/{len(self.voices)})",
interrupt=True,
@@ -173,16 +199,19 @@ class command:
self.announce_current_selection()
def next_module(self):
"""Move to next module"""
self.module_index = (self.module_index + 1) % len(self.modules)
self.load_voices_for_current_module()
self.announce_current_selection()
self._change_module(1)
def prev_module(self):
"""Move to previous module"""
self.module_index = (self.module_index - 1) % len(self.modules)
self.load_voices_for_current_module()
self.announce_current_selection()
self._change_module(-1)
def _change_module(self, offset):
if self._loading or not self.modules:
return
self._loading = True
self._request_generation += 1
generation = self._request_generation
self.module_index = (self.module_index + offset) % len(self.modules)
self._request_current_module_voices(generation)
def test_voice(self):
"""Test current voice"""
@@ -193,15 +222,38 @@ class command:
return
module = self.modules[self.module_index]
voice = self.voices[self.voice_index]
encoded_voice = self.voices[self.voice_index]
voice_name = encoded_voice.split("|", 1)[0]
self.env["runtime"]["OutputManager"].present_text(
"Testing...", interrupt=True
)
if self.preview_voice(module, voice):
# Store for apply command
self._request_generation += 1
generation = self._request_generation
self._loading = True
self.env["runtime"]["SpeechDiscoveryManager"].request_voice_test(
module,
voice_name,
self.test_message,
lambda succeeded, error: self._voice_test_ready(
generation, module, encoded_voice, succeeded, error
),
)
def _voice_test_ready(
self, generation, module, voice, succeeded, _error
):
if generation != self._request_generation:
return
self._loading = False
if succeeded:
self.env["commandBuffer"]["lastTestedModule"] = module
self.env["commandBuffer"]["lastTestedVoice"] = voice
voice_name, separator, language = voice.partition("|")
self.env["commandBuffer"]["lastTestedVoice"] = voice_name
if separator:
self.env["commandBuffer"]["lastTestedLanguage"] = language
else:
self.env["commandBuffer"].pop("lastTestedLanguage", None)
self.env["runtime"]["OutputManager"].play_sound("Accept")
else:
self.env["runtime"]["OutputManager"].play_sound("Error")
@@ -212,13 +264,17 @@ class command:
return
module = self.modules[self.module_index]
voice = self.voices[self.voice_index]
voice, separator, language = self.voices[self.voice_index].partition(
"|"
)
try:
SettingsManager = self.env["runtime"]["SettingsManager"]
SettingsManager.settings["speech"]["driver"] = "speechdDriver"
SettingsManager.settings["speech"]["module"] = module
SettingsManager.settings["speech"]["voice"] = voice
if separator:
SettingsManager.settings["speech"]["language"] = language
if "SpeechDriver" in self.env["runtime"]:
SpeechDriver = self.env["runtime"]["SpeechDriver"]
@@ -236,66 +292,5 @@ class command:
)
self.env["runtime"]["OutputManager"].play_sound("Error")
def preview_voice(self, module, voice):
"""Test voice with spd-say"""
try:
cmd = ["spd-say", "-o", module, "-y", voice, self.testMessage]
result = subprocess.run(cmd, timeout=10)
return result.returncode == 0
except Exception:
return False
def get_speechd_modules(self):
"""Get available speech modules"""
try:
result = subprocess.run(
["spd-say", "-O"], capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
lines = result.stdout.strip().split("\n")
return [line.strip() for line in lines[1:] if line.strip()]
except Exception:
pass
return []
def get_module_voices(self, module):
"""Get voices for module"""
try:
result = subprocess.run(
["spd-say", "-o", module, "-L"],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
lines = result.stdout.strip().split("\n")
voices = []
for line in lines[1:]:
if not line.strip():
continue
if module.lower() == "espeak-ng":
voice = self.process_espeak_voice(line)
if voice:
voices.append(voice)
else:
voices.append(line.strip())
return voices
except Exception:
pass
return []
def process_espeak_voice(self, voiceLine):
"""Process espeak voice format"""
parts = [p for p in voiceLine.split() if p]
if len(parts) < 2:
return None
lang_code = parts[-2].lower()
variant = parts[-1].lower()
return (
f"{lang_code}+{variant}"
if variant and variant != "none"
else lang_code
)
def set_callback(self, callback):
pass
@@ -1,225 +1,160 @@
#!/usr/bin/env python3
import subprocess
import threading
import time
from fenrirscreenreader.core import debug
from fenrirscreenreader.core.i18n import _
class command:
def __init__(self):
pass
self._request_generation = 0
self._loading = False
def initialize(self, environment):
self.env = environment
self.testMessage = (
self.test_message = (
"Voice test: The quick brown fox jumps over the lazy dog."
)
def shutdown(self):
pass
self._request_generation += 1
self._loading = False
def get_description(self):
return "Safe voice browser - cycles through voices without hanging"
return _("browse and test Speech Dispatcher voices")
def run(self):
try:
if self._loading:
return
self._loading = True
self._request_generation += 1
generation = self._request_generation
self.env["runtime"]["OutputManager"].present_text(
_("Loading speech modules"), interrupt=True
)
self.env["runtime"]["SpeechDiscoveryManager"].request_modules(
lambda modules, error: self._modules_ready(
generation, modules, error
)
)
def _modules_ready(self, generation, modules, error):
if generation != self._request_generation:
return
if error or not modules:
self._loading = False
self._report_error(_("No speech modules found"), error)
return
module_index = self.env["commandBuffer"].get(
"safeBrowserModuleIndex", 0
)
if module_index >= len(modules):
module_index = 0
module = modules[module_index]
self.env["runtime"]["OutputManager"].present_text(
_("Loading voices for {module}").format(module=module),
interrupt=True,
)
self.env["runtime"]["SpeechDiscoveryManager"].request_voices(
module,
lambda _module, voices, voice_error: self._voices_ready(
generation,
modules,
module_index,
voices,
voice_error,
),
)
def _voices_ready(
self, generation, modules, module_index, voices, error
):
if generation != self._request_generation:
return
if error or not voices:
self._loading = False
next_module = (module_index + 1) % len(modules)
self.env["commandBuffer"]["safeBrowserModuleIndex"] = next_module
self.env["commandBuffer"]["safeBrowserVoiceIndex"] = 0
self._report_error(_("No voices found"), error)
return
voice_index = self.env["commandBuffer"].get(
"safeBrowserVoiceIndex", 0
)
if voice_index >= len(voices):
voice_index = 0
module = modules[module_index]
voice = voices[voice_index]
voice_name = voice.split("|", 1)[0]
self.env["runtime"]["OutputManager"].present_text(
_("Module: {module}, voice: {voice}").format(
module=module, voice=voice_name
),
interrupt=True,
)
self.env["runtime"]["OutputManager"].present_text(
_("Testing voice"), interrupt=True
)
self.env["runtime"]["SpeechDiscoveryManager"].request_voice_test(
module,
voice_name,
self.test_message,
lambda succeeded, test_error: self._test_ready(
generation,
modules,
module_index,
voices,
voice_index,
succeeded,
test_error,
),
)
def _test_ready(
self,
generation,
modules,
module_index,
voices,
voice_index,
succeeded,
error,
):
if generation != self._request_generation:
return
self._loading = False
module = modules[module_index]
voice_name, separator, language = voices[voice_index].partition("|")
if succeeded:
self.env["commandBuffer"]["lastTestedModule"] = module
self.env["commandBuffer"]["lastTestedVoice"] = voice_name
if separator:
self.env["commandBuffer"]["lastTestedLanguage"] = language
else:
self.env["commandBuffer"].pop("lastTestedLanguage", None)
self.env["runtime"]["OutputManager"].present_text(
"Starting safe voice browser", interrupt=True
_("Voice test completed successfully"), interrupt=True
)
else:
self._report_error(_("Voice test failed"), error)
# Get modules with timeout protection
modules = self.get_speechd_modules_with_timeout()
if not modules:
self.env["runtime"]["OutputManager"].present_text(
"No speech modules found", interrupt=True
)
return
voice_index += 1
if voice_index >= len(voices):
voice_index = 0
module_index = (module_index + 1) % len(modules)
self.env["commandBuffer"]["safeBrowserModuleIndex"] = module_index
self.env["commandBuffer"]["safeBrowserVoiceIndex"] = voice_index
# Get current position from commandBuffer or start fresh
module_index = self.env["commandBuffer"].get(
"safeBrowserModuleIndex", 0
def _report_error(self, message, detail):
if detail:
self.env["runtime"]["DebugManager"].write_debug_out(
f"voice_browser_safe: {detail}",
debug.DebugLevel.ERROR,
)
voice_index = self.env["commandBuffer"].get(
"safeBrowserVoiceIndex", 0
)
# Ensure valid module index
if module_index >= len(modules):
module_index = 0
current_module = modules[module_index]
self.env["runtime"]["OutputManager"].present_text(
f"Loading voices for {current_module}...", interrupt=True
)
# Get voices with timeout protection
voices = self.get_module_voices_with_timeout(current_module)
if not voices:
self.env["runtime"]["OutputManager"].present_text(
f"No voices in {current_module}, trying next module",
interrupt=True,
)
module_index = (module_index + 1) % len(modules)
self.env["commandBuffer"][
"safeBrowserModuleIndex"
] = module_index
self.env["commandBuffer"]["safeBrowserVoiceIndex"] = 0
return
# Ensure valid voice index
if voice_index >= len(voices):
voice_index = 0
current_voice = voices[voice_index]
# Announce current selection
self.env["runtime"]["OutputManager"].present_text(
f"Module: {current_module} ({module_index + 1}/{len(modules)})",
interrupt=True,
)
self.env["runtime"]["OutputManager"].present_text(
f"Voice: {current_voice} ({voice_index + 1}/{len(voices)})",
interrupt=True,
)
# Test voice in background thread to avoid blocking
self.env["runtime"]["OutputManager"].present_text(
"Testing voice...", interrupt=True
)
# Use threading to prevent freezing
test_thread = threading.Thread(
target=self.test_voice_async,
args=(current_module, current_voice),
)
test_thread.daemon = True
test_thread.start()
# Store tested voice for apply command
self.env["commandBuffer"]["lastTestedModule"] = current_module
self.env["commandBuffer"]["lastTestedVoice"] = current_voice
# Advance to next voice for next run
voice_index += 1
if voice_index >= len(voices):
voice_index = 0
module_index = (module_index + 1) % len(modules)
# Store position for next run
self.env["commandBuffer"]["safeBrowserModuleIndex"] = module_index
self.env["commandBuffer"]["safeBrowserVoiceIndex"] = voice_index
# Give instructions
self.env["runtime"]["OutputManager"].present_text(
"Run again for next voice, or use apply voice command",
interrupt=True,
)
except Exception as e:
self.env["runtime"]["OutputManager"].present_text(
f"Voice browser error: {str(e)}", interrupt=True
)
self.env["runtime"]["OutputManager"].play_sound("Error")
def test_voice_async(self, module, voice):
"""Test voice in background thread to avoid blocking"""
try:
# Run with strict timeout
cmd = ["spd-say", "-o", module, "-y", voice, self.testMessage]
result = subprocess.run(cmd, timeout=5, capture_output=True)
# Schedule success sound for main thread
if result.returncode == 0:
# We can't call OutputManager from background thread safely
# So we'll just let the main thread handle feedback
pass
except subprocess.TimeoutExpired:
# Voice test timed out - this is okay, don't crash
pass
except Exception:
# Any other error - also okay, don't crash
pass
def get_speechd_modules_with_timeout(self):
"""Get speech modules with timeout protection"""
try:
result = subprocess.run(
["spd-say", "-O"], capture_output=True, text=True, timeout=3
)
if result.returncode == 0:
lines = result.stdout.strip().split("\n")
modules = [line.strip() for line in lines[1:] if line.strip()]
# Limit to first 10 modules to prevent overload
return modules[:10]
except subprocess.TimeoutExpired:
self.env["runtime"]["OutputManager"].present_text(
"Module detection timed out", interrupt=True
)
except Exception as e:
self.env["runtime"]["OutputManager"].present_text(
f"Module detection failed: {str(e)}", interrupt=True
)
return []
def get_module_voices_with_timeout(self, module):
"""Get voices with timeout and limits"""
try:
result = subprocess.run(
["spd-say", "-o", module, "-L"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0:
lines = result.stdout.strip().split("\n")
voices = []
for line in lines[1:]:
if not line.strip():
continue
if module.lower() == "espeak-ng":
voice = self.process_espeak_voice(line)
if voice:
voices.append(voice)
else:
voices.append(line.strip())
# Limit voice count to prevent memory issues
if len(voices) > 1000:
self.env["runtime"]["OutputManager"].present_text(
f"found {len(voices)} voices, limiting to first 1000",
interrupt=True,
)
voices = voices[:1000]
return voices
except subprocess.TimeoutExpired:
self.env["runtime"]["OutputManager"].present_text(
f"Voice detection for {module} timed out", interrupt=True
)
except Exception as e:
self.env["runtime"]["OutputManager"].present_text(
f"Voice detection failed: {str(e)}", interrupt=True
)
return []
def process_espeak_voice(self, voiceLine):
"""Process espeak voice format"""
try:
parts = [p for p in voiceLine.split() if p]
if len(parts) < 2:
return None
lang_code = parts[-2].lower()
variant = parts[-1].lower()
return (
f"{lang_code}+{variant}"
if variant and variant != "none"
else lang_code
)
except Exception:
return None
self.env["runtime"]["OutputManager"].present_text(
message, interrupt=True
)
def set_callback(self, callback):
pass
@@ -0,0 +1,30 @@
#!/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 _("show next help category")
def run(self):
text = self.env["runtime"]["HelpManager"].next_help_section()
self.env["runtime"]["OutputManager"].present_text(
text, interrupt=True
)
def set_callback(self, callback):
pass
@@ -0,0 +1,30 @@
#!/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 _("show previous help category")
def run(self):
text = self.env["runtime"]["HelpManager"].prev_help_section()
self.env["runtime"]["OutputManager"].present_text(
text, interrupt=True
)
def set_callback(self, callback):
pass
@@ -1,55 +0,0 @@
#!/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 ""
def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"keyboard", "interrupt_on_key_press"
):
return
if self.env["runtime"]["InputManager"].no_key_pressed():
return
if self.env["runtime"]["ScreenManager"].is_screen_change():
return
if len(self.env["input"]["curr_input"]) <= len(
self.env["input"]["prev_input"]
):
return
# if the filter is set
if (
self.env["runtime"]["SettingsManager"]
.get_setting("keyboard", "interrupt_on_key_press_filter")
.strip()
!= ""
):
filter_list = (
self.env["runtime"]["SettingsManager"]
.get_setting("keyboard", "interrupt_on_key_press_filter")
.split(",")
)
for curr_key in self.env["input"]["curr_input"]:
if curr_key not in filter_list:
return
self.env["runtime"]["OutputManager"].interrupt_output()
def set_callback(self, callback):
pass
@@ -1,41 +0,0 @@
#!/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 _("disables speech until next keypress")
def run(self):
if not self.env["commandBuffer"]["enableSpeechOnKeypress"]:
return
self.env["runtime"]["SettingsManager"].set_setting(
"speech",
"enabled",
str(self.env["commandBuffer"]["enableSpeechOnKeypress"]),
)
self.env["commandBuffer"]["enableSpeechOnKeypress"] = False
# Also disable prompt watching since speech was manually re-enabled
if "silenceUntilPrompt" in self.env["commandBuffer"]:
self.env["commandBuffer"]["silenceUntilPrompt"] = False
self.env["runtime"]["OutputManager"].present_text(
_("speech enabled"), sound_icon="SpeechOn", interrupt=True
)
def set_callback(self, callback):
pass
@@ -42,19 +42,10 @@ class command:
)
if x_move > 3:
return
if self.env["runtime"]["InputManager"].get_shortcut_type() in ["KEY"]:
if self.env["runtime"][
"InputManager"
].get_last_deepest_input() in [["KEY_TAB"]]:
return
elif self.env["runtime"]["InputManager"].get_shortcut_type() in [
"BYTE"
]:
if self.env["runtime"]["ByteManager"].get_last_byte_key() in [
b" ",
b"\t",
]:
return
if self.env["runtime"][
"InputManager"
].get_last_deepest_input() in [["KEY_TAB"]]:
return
# detect deletion or chilling
if (
self.env["screen"]["new_cursor"]["x"]
@@ -81,11 +81,10 @@ class command:
if curr_char.isspace():
# Only announce spaces during pure navigation (arrow keys)
# Check if this is really navigation by looking at input history
if self.env["runtime"]["InputManager"].get_shortcut_type() in [
"KEY"
] and self.env["runtime"]["InputManager"].get_last_deepest_input()[
0
] in [
last_input = self.env["runtime"][
"InputManager"
].get_last_deepest_input()
if last_input and last_input[0] in [
"KEY_LEFT",
"KEY_RIGHT",
"KEY_UP",
@@ -0,0 +1,44 @@
#!/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.tabCompletionManager import TabCompletionManager
class command:
def __init__(self):
self.manager = TabCompletionManager()
def initialize(self, environment):
self.env = environment
self.manager.initialize(environment)
def shutdown(self):
pass
def get_description(self):
return _("Announces tab completions on cursor movement")
def run(self):
text = self.manager.process_update()
if not text:
return
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(
text,
interrupt=do_interrupt,
announce_capital=True,
flush=False,
)
def set_callback(self, callback):
pass
@@ -0,0 +1,95 @@
#!/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 _("Announces large text insertions at the cursor")
def run(self):
if self.env["runtime"]["ScreenManager"].is_screen_change():
return
if not self.env["runtime"]["ScreenManager"].is_delta():
return
if not self.env["screen"].get("new_delta_is_typing", False):
return
x_move = (
self.env["screen"]["new_cursor"]["x"]
- self.env["screen"]["old_cursor"]["x"]
)
if x_move < 5:
return
if self._is_recent_tab_input():
return
delta_text = self.env["screen"]["new_delta"]
if not self._matches_cursor_insert(x_move, delta_text):
return
curr_delta = delta_text
if (
len(curr_delta.strip()) != len(curr_delta)
and curr_delta.strip() != ""
):
curr_delta = curr_delta.strip()
if not curr_delta:
return
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(
curr_delta,
interrupt=do_interrupt,
announce_capital=True,
flush=False,
)
def _is_recent_tab_input(self):
input_manager = self.env["runtime"].get("InputManager")
if not input_manager:
return False
if input_manager.get_last_deepest_input() in [["KEY_TAB"]]:
return True
last_event = input_manager.get_last_event()
if not last_event or last_event.get("event_name") != "KEY_TAB":
return False
try:
return time.time() - input_manager.get_last_input_time() <= 0.5
except Exception:
return False
def _matches_cursor_insert(self, x_move, delta_text):
if not delta_text or "\n" in delta_text:
return False
delta_len = len(delta_text)
if x_move == delta_len:
return True
if abs(x_move - delta_len) <= 2:
return True
return delta_len > 10 and abs(x_move - delta_len) <= (delta_len * 0.2)
def set_callback(self, callback):
pass
@@ -1,170 +0,0 @@
#!/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
# 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):
pass
def get_description(self):
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):
"""Enhanced tab completion detection with improved reliability"""
# Basic cursor movement check (preserve original logic)
x_move = (
self.env["screen"]["new_cursor"]["x"]
- self.env["screen"]["old_cursor"]["x"]
)
if x_move <= 0:
return
# Enhanced tab input detection with persistence
tab_detected = self._is_recent_tab_input()
# Fallback for non-tab movements (preserve original thresholds)
if not tab_detected:
if x_move < 5:
return
# Screen delta availability check
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
delta_text = self.env["screen"]["new_delta"]
# Enhanced correlation checking with flexible matching
if not self._is_flexible_completion_match(x_move, delta_text):
# Additional pattern-based validation for edge cases
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()
# 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):
pass
@@ -1,128 +0,0 @@
#!/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
@@ -5,11 +5,17 @@
# By Chrys, Storm Dragon, and contributors.
import datetime
import fcntl
import os
import tempfile
import time
from fenrirscreenreader.core.i18n import _
ANNOUNCEMENT_LOCK_TIMEOUT_SEC = 5.0
class command:
def __init__(self):
pass
@@ -26,6 +32,49 @@ class command:
def get_description(self):
return "No Description found"
def _get_announcement_lock_path(self):
return os.path.join(
tempfile.gettempdir(),
"fenrirscreenreader-time-announcement.lock",
)
def _read_announcement_lock_slot(self, lock_file):
lock_file.seek(0)
lock_content = lock_file.readline().strip().split()
if len(lock_content) < 2:
return ""
return lock_content[1]
def _claim_announcement_lock(self, announcement_slot):
now = time.time()
lock_path = self._get_announcement_lock_path()
try:
lock_fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o666)
except OSError:
return False
try:
os.chmod(lock_path, 0o666)
except OSError:
pass
with os.fdopen(lock_fd, "r+", encoding="utf-8") as lock_file:
try:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
lock_slot = self._read_announcement_lock_slot(lock_file)
lock_stat = os.fstat(lock_file.fileno())
if lock_slot == announcement_slot:
return False
lock_file.seek(0)
lock_file.truncate()
lock_file.write(f"{os.getpid()} {announcement_slot} {now}\n")
lock_file.flush()
os.fsync(lock_file.fileno())
return True
finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"time", "enabled"
@@ -50,6 +99,7 @@ class command:
if delay_sec > 0:
if int((now - self.last_time).total_seconds()) < delay_sec:
return
announcement_slot = f"delay:{int(now.timestamp()) // delay_sec}"
else:
# should announce?
if not str(now.minute).zfill(2) in on_minutes:
@@ -58,6 +108,7 @@ class command:
if now.hour == self.last_time.hour:
if now.minute == self.last_time.minute:
return
announcement_slot = f"minute:{datetime.datetime.strftime(now, '%Y%m%d%H%M')}"
date_format = self.env["runtime"]["SettingsManager"].get_setting(
"general", "date_format"
@@ -78,6 +129,10 @@ class command:
if not (present_date or present_time):
return
if not self._claim_announcement_lock(announcement_slot):
self.last_time = now
return
time_format = self.env["runtime"]["SettingsManager"].get_setting(
"general", "time_format"
)
@@ -48,7 +48,7 @@ class command:
for curr_key in self.env["input"]["curr_input"]:
if curr_key not in filter_list:
return
self.env["runtime"]["OutputManager"].interrupt_output()
self.env["runtime"]["OutputManager"].interrupt_output_async()
def set_callback(self, callback):
pass
@@ -0,0 +1,29 @@
#!/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.tabCompletionManager import TabCompletionManager
class command:
def __init__(self):
self.manager = TabCompletionManager()
def initialize(self, environment):
self.env = environment
self.manager.initialize(environment)
def shutdown(self):
pass
def get_description(self):
return _("Tracks tab keypresses for completion announcements")
def run(self):
self.manager.capture_if_tab()
def set_callback(self, callback):
pass
@@ -27,6 +27,11 @@ class command:
== self.env["input"]["new_caps_lock"]
):
return
current_input = self.env["input"]["curr_input"]
previous_input = self.env["input"]["prev_input"]
relevant_input = current_input or previous_input
if "KEY_CAPSLOCK" not in relevant_input:
return
if self.env["input"]["new_caps_lock"]:
self.env["runtime"]["OutputManager"].present_text(
_("Capslock on"), interrupt=True
@@ -27,6 +27,11 @@ class command:
== self.env["input"]["new_scroll_lock"]
):
return
current_input = self.env["input"]["curr_input"]
previous_input = self.env["input"]["prev_input"]
relevant_input = current_input or previous_input
if "KEY_SCROLLLOCK" not in relevant_input:
return
if self.env["input"]["new_scroll_lock"]:
self.env["runtime"]["OutputManager"].present_text(
_("Scrolllock on"), interrupt=True
@@ -28,15 +28,21 @@ class command:
# Only announce numlock changes if an actual numlock key was pressed
# AND the LED state actually changed (some numpads send spurious NUMLOCK events)
current_input = self.env["input"]["curr_input"]
previous_input = self.env["input"]["prev_input"]
relevant_input = current_input or previous_input
# 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))
relevant_input and
"KEY_NUMLOCK" in relevant_input and
not any(
key.startswith("KEY_KP")
for key in relevant_input
if isinstance(key, str)
)
)
if is_genuine_numlock:
@@ -41,20 +41,11 @@ class command:
== self.env["runtime"]["ScreenManager"].get_rows() - 1
):
return
if self.env["runtime"]["InputManager"].get_shortcut_type() in ["KEY"]:
if not (
self.env["runtime"]["InputManager"].get_last_deepest_input()
in [["KEY_UP"], ["KEY_DOWN"]]
):
return
elif self.env["runtime"]["InputManager"].get_shortcut_type() in [
"BYTE"
]:
if not (
self.env["runtime"]["ByteManager"].get_last_byte_key()
in [b"^[[A", b"^[[B"]
):
return
if not (
self.env["runtime"]["InputManager"].get_last_deepest_input()
in [["KEY_UP"], ["KEY_DOWN"]]
):
return
# Get the current cursor's line from both old and new content
prev_line = self.env["screen"]["old_content_text"].split("\n")[
@@ -0,0 +1,44 @@
#!/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.tabCompletionManager import TabCompletionManager
class command:
def __init__(self):
self.manager = TabCompletionManager()
def initialize(self, environment):
self.env = environment
self.manager.initialize(environment)
def shutdown(self):
pass
def get_description(self):
return _("Announces visible tab completion results")
def run(self):
text = self.manager.process_update()
if not text:
return
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(
text,
interrupt=do_interrupt,
announce_capital=True,
flush=False,
)
def set_callback(self, callback):
pass
@@ -36,6 +36,7 @@ class command:
# screen changes
elif (
self.env["screen"]["new_delta"]
and not self.env["screen"].get("new_delta_is_typing", False)
and self.is_real_progress_update()
):
self.detect_progress(self.env["screen"]["new_delta"])
@@ -78,18 +79,24 @@ class command:
# updates
delta_text = self.env["screen"]["new_delta"]
delta_length = len(delta_text)
explicit_progress_lines = self.get_explicit_progress_lines(delta_text)
if (
delta_length > 200
): # 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
): # Allow longer progress lines such as terminal status output
if not explicit_progress_lines:
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:
if (
"\n" in delta_text
and delta_length > 50
and not explicit_progress_lines
):
self.env["runtime"]["DebugManager"].write_debug_out(
f"Progress filter: multiline delta ({delta_length} chars)",
debug.DebugLevel.INFO,
@@ -106,6 +113,84 @@ class command:
return True
def get_curl_classic_percentage(self, text):
"""Return progress from a structurally valid curl classic row."""
import re
size_field = r"\d+(?:\.\d+)?(?:[kKmMgGtT](?:i?[bB])?)?"
time_field = r"(?:(?:\d+:)?\d{2}:\d{2}|--:--:--)"
curl_classic_match = re.match(
rf"^\s*(\d+)\s+{size_field}\s+"
rf"(\d+)\s+{size_field}\s+"
rf"(\d+)\s+{size_field}\s+"
rf"{size_field}\s+{size_field}\s+"
rf"{time_field}\s+{time_field}\s+{time_field}\s+"
rf"{size_field}(?=\s|$)",
text,
)
if not curl_classic_match:
return None
total_percentage = int(curl_classic_match.group(1))
received_percentage = int(curl_classic_match.group(2))
uploaded_percentage = int(curl_classic_match.group(3))
if not all(
0 <= percentage <= 100
for percentage in (
total_percentage,
received_percentage,
uploaded_percentage,
)
):
return None
if total_percentage != received_percentage:
return None
return float(total_percentage)
def is_explicit_progress_delta(self, text):
"""Allow long single-line deltas that still look like progress output."""
import re
if "\n" in text or self.contains_url(text):
return False
if self.get_curl_classic_percentage(text) is not None:
return True
has_percentage = re.search(r"(^|\s)\d+(?:\.\d+)?\s*%", text)
if not has_percentage:
return self.has_progress_status_fields(text)
bracketed_bar = re.search(
r"\[(?=[#=*>█▉▊▋▌▍▎▏▒▓░.\s-]{2,}\])"
r"(?=[^\]]*[#=*>█▉▊▋▌▍▎▏▒▓░])"
r"[#=*>█▉▊▋▌▍▎▏▒▓░.\s-]+\]",
text,
)
pipe_bar = re.search(
r"\|(?=[#=*>█▉▊▋▌▍▎▏▒▓░.\s-]{2,}\|)"
r"(?=[^|]*[#=*>█▉▊▋▌▍▎▏▒▓░])"
r"[#=*>█▉▊▋▌▍▎▏▒▓░.\s-]+\|",
text,
)
transfer_rate = re.search(
r"\b\d+(?:\.\d+)?\s*[kKmMgGtT](?:i?B)?/s\b",
text,
)
return bool(
bracketed_bar
or pipe_bar
or transfer_rate
)
def get_explicit_progress_lines(self, text):
"""Return structurally explicit progress rows from a screen delta."""
return [
line
for line in text.splitlines()
if self.is_explicit_progress_delta(line)
]
def reset_progress_state(self):
"""Reset progress state when a prompt is detected, allowing new progress operations to start fresh"""
self.env["runtime"]["DebugManager"].write_debug_out(
@@ -119,6 +204,15 @@ class command:
import re
import time
if "\n" in text:
explicit_progress_lines = self.get_explicit_progress_lines(text)
if not explicit_progress_lines:
return
# Parallel transfer tools commonly repaint several package rows,
# followed by an aggregate row. One tone per update is enough, and
# the final explicit row is normally that aggregate.
text = explicit_progress_lines[-1]
current_time = time.time()
# Debug: Print what we're checking
@@ -172,34 +266,21 @@ class command:
return
# Pattern 1a2: Curl classic progress format (percentage without % symbol)
# Extract percentage from curl's classic format
curl_classic_match = re.search(
r"^\s*(\d+)\s+\d+[kMGT]?\s+(\d+)\s+\d+[kMGT]?\s+\d+\s+\d+\s+\d+[kMGT]?\s+\d+\s+\d+:\d+:\d+\s+\d+:\d+:\d+\s+\d+:\d+:\d+\s+\d+[kMGT]?\s*$", text
)
if curl_classic_match:
# Use the first percentage (total progress)
percentage = float(curl_classic_match.group(1))
if 0 <= percentage <= 100:
percentage = self.get_curl_classic_percentage(text)
if percentage is not None:
self.env["runtime"]["DebugManager"].write_debug_out(
"found curl classic percentage: " + str(percentage),
debug.DebugLevel.INFO,
)
if percentage != self.env["commandBuffer"]["lastProgressValue"]:
self.env["runtime"]["DebugManager"].write_debug_out(
"found curl classic percentage: " + str(percentage),
"Playing tone for curl: " + str(percentage),
debug.DebugLevel.INFO,
)
if (
percentage
!= self.env["commandBuffer"]["lastProgressValue"]
):
self.env["runtime"]["DebugManager"].write_debug_out(
"Playing tone for curl: " + str(percentage),
debug.DebugLevel.INFO,
)
self.play_progress_tone(percentage)
self.env["commandBuffer"][
"lastProgressValue"
] = percentage
self.env["commandBuffer"][
"lastProgressTime"
] = current_time
return
self.play_progress_tone(percentage)
self.env["commandBuffer"]["lastProgressValue"] = percentage
self.env["commandBuffer"]["lastProgressTime"] = current_time
return
# Pattern 1b: Time/token activity (not percentage-based, so use single
# beep)
@@ -219,8 +300,20 @@ class command:
pacman_match = re.search(
r"\d+(?:\.\d+)?\s+[kKmMgGtT]iB\s+\d+(?:\.\d+)?\s+[kKmMgGtT]iB/s\s+\d+:\d+", text
)
# Pattern 1g: Key/value status lines from ffmpeg-like terminal tools.
# These do not expose a percentage in the status line, so treat them as
# activity rather than guessing progress from elapsed time.
status_fields_match = self.has_progress_status_fields(text)
if time_match or token_match or dd_match or curl_match or transfer_match or pacman_match:
if (
time_match
or token_match
or dd_match
or curl_match
or transfer_match
or pacman_match
or status_fields_match
):
# For non-percentage progress, use a single activity beep every 2
# seconds
if (
@@ -305,43 +398,41 @@ class command:
self.env["commandBuffer"]["lastProgressTime"] = current_time
return
# Pattern 6: Claude Code working indicators (various symbols + activity text + "esc/ctrl+c to interrupt")
# Matches any: [symbol] [Task description] (... to interrupt ...)
# Pattern 6: Interruptible terminal activity indicators
# 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.*\)',
# Keep this structural rather than adding application-specific formats,
# which change too frequently to support reliably.
interruptible_activity_match = re.search(
r'[*✢✽✶✻·•◦○●◆]\s+\w+.*?(?:…\s*)?\(.*(?:esc|ctrl\+c) to interrupt.*\)',
text,
re.IGNORECASE,
)
if claude_progress_match:
if not interruptible_activity_match:
interruptible_activity_match = re.search(
(
r'^\s*(?:thinking|working|processing|analyzing|searching|'
r'reading|writing|planning|running|executing|updating|'
r'building|installing|compiling|downloading|reviewing|'
r'generating|responding|applying|fixing|editing|creating|'
r'preparing|checking|opening|loading|fetching|retrieving|'
r'scanning|indexing|summarizing)\b.*'
r'\([^)]*(?:esc|ctrl\+c) to interrupt[^)]*\)\s*$'
),
text,
re.IGNORECASE,
)
if interruptible_activity_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
self.env["runtime"]["DebugManager"].write_debug_out(
"Playing Claude Code activity beep",
"Playing interruptible 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 (•/◦ ...)
# Pattern 6b: Bullet/white bullet activity lines (•/◦ ...)
bullet_activity_match = re.search(
(
r'^\s*[•◦]\s+.*(?:…|\.{3,}|\b(?:thinking|working|processing|'
@@ -572,5 +663,48 @@ class command:
return True
return False
def has_progress_status_fields(self, text):
"""Recognize dense key=value status lines from terminal tools."""
import re
if "\n" in text or self.contains_url(text):
return False
fields = re.findall(r"\b([a-zA-Z][a-zA-Z0-9_-]*)=\s*\S+", text)
if len(fields) < 3:
return False
field_names = {field.lower() for field in fields}
progress_fields = {
"frame",
"fps",
"size",
"time",
"bitrate",
"speed",
"elapsed",
"dup",
"drop",
}
if len(field_names & progress_fields) < 3:
return False
has_time_field = bool(
re.search(
r"\b(?:time|elapsed|eta)=\s*\d+(?::\d{2}){1,2}(?:\.\d+)?\b",
text,
re.IGNORECASE,
)
)
has_rate_field = bool(
re.search(
r"\b(?:speed|fps|bitrate)=\s*[\d.]+[a-zA-Z/]*\b",
text,
re.IGNORECASE,
)
)
return has_time_field and has_rate_field
def set_callback(self, callback):
pass
@@ -4,6 +4,8 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import difflib
import re
import time
from fenrirscreenreader.core.i18n import _
@@ -15,6 +17,8 @@ class command:
self._batched_text = []
self._last_flush_time = 0
self._in_flood_mode = False
self._pending_focus_text = ""
self._pending_focus_time = 0
def initialize(self, environment):
self.env = environment
@@ -111,6 +115,131 @@ class command:
return False
def _normalize_line(self, line):
return " ".join(line.split())
def _is_focus_control_line(self, text):
stripped = self._normalize_line(text)
if not stripped:
return False
if re.fullmatch(r"<\s*[A-Za-z][A-Za-z0-9 _.-]{0,30}\s*>", stripped):
return True
return stripped.lower() in {
"ok",
"cancel",
"yes",
"no",
"retry",
"abort",
"ignore",
"continue",
}
def _is_transient_focus_only_update(self, delta_text):
delta_lines = [
self._normalize_line(line)
for line in delta_text.splitlines()
if line.strip()
]
if len(delta_lines) != 1:
return False
if not self._is_focus_control_line(delta_lines[0]):
return False
screen_lines = [
self._normalize_line(line)
for line in self.env["screen"]["new_content_text"].split("\n")
if line.strip()
]
return screen_lines == delta_lines
def _store_pending_focus_text(self, delta_text):
delta_lines = [
self._normalize_line(line)
for line in delta_text.splitlines()
if line.strip()
]
self._pending_focus_text = "\n".join(delta_lines)
self._pending_focus_time = time.time()
def _take_pending_focus_text(self):
if not self._pending_focus_text:
return ""
if time.time() - self._pending_focus_time > 0.5:
self._pending_focus_text = ""
self._pending_focus_time = 0
return ""
pending = self._pending_focus_text
self._pending_focus_text = ""
self._pending_focus_time = 0
return pending
def _is_subsequence(self, subset_lines, source_lines):
subset_index = 0
for source_line in source_lines:
if source_line == subset_lines[subset_index]:
subset_index += 1
if subset_index == len(subset_lines):
return True
return False
def _prefer_inserted_lower_screen_text(self, delta_text):
delta_lines = [
line for line in delta_text.splitlines() if line.strip() != ""
]
if len(delta_lines) < 2:
return delta_text
old_lines = self.env["screen"]["old_content_text"].split("\n")
new_lines = self.env["screen"]["new_content_text"].split("\n")
if len(old_lines) != len(new_lines) or len(new_lines) < 4:
return delta_text
normalized_old_lines = [
self._normalize_line(line) for line in old_lines
]
normalized_new_lines = [
self._normalize_line(line) for line in new_lines
]
matcher = difflib.SequenceMatcher(
None, normalized_old_lines, normalized_new_lines, autojunk=False
)
top_lines_changed = False
inserted_lines = []
lower_screen_start = max(2, len(new_lines) // 2)
for tag, old_start, old_end, new_start, new_end in matcher.get_opcodes():
if tag in {"replace", "delete"} and old_start < 2:
top_lines_changed = True
if tag != "insert" or new_start < lower_screen_start:
continue
inserted_lines.extend(
line for line in new_lines[new_start:new_end] if line.strip() != ""
)
if not top_lines_changed or not inserted_lines:
return delta_text
if all(
self._is_focus_control_line(line) for line in inserted_lines
):
return delta_text
normalized_delta_lines = [
self._normalize_line(line) for line in delta_lines
]
normalized_inserted_lines = [
self._normalize_line(line) for line in inserted_lines
]
if not self._is_subsequence(
normalized_inserted_lines, normalized_delta_lines
):
return delta_text
if normalized_delta_lines == normalized_inserted_lines:
return delta_text
return "\n".join(inserted_lines)
def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"speech", "auto_read_incoming"
@@ -119,11 +248,15 @@ class command:
# is there something to read?
if not self.env["runtime"]["ScreenManager"].is_delta(ignoreSpace=True):
return
if self.env["screen"].get("new_delta_is_typing", False):
return
delta_text = self.env["screen"]["new_delta"]
if self.env["runtime"]["ScreenManager"].is_screen_change():
self._reset_flood_state()
self._pending_focus_text = ""
self._pending_focus_time = 0
# Skip if tab completion already handled this delta
if self._was_handled_by_tab_completion(delta_text):
@@ -146,6 +279,10 @@ class command:
if "\n" not in delta_text:
return
if self._is_transient_focus_only_update(delta_text):
self._store_pending_focus_text(delta_text)
return
rapid = self._is_rapid_updates()
high_volume = self._is_high_volume(delta_text)
@@ -169,6 +306,10 @@ class command:
return
# print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta']))
delta_text = self._prefer_inserted_lower_screen_text(delta_text)
pending_focus_text = self._take_pending_focus_text()
if pending_focus_text:
delta_text = "\n".join([delta_text, pending_focus_text])
self.env["runtime"]["OutputManager"].present_text(
delta_text, interrupt=False, flush=False
)
@@ -0,0 +1,27 @@
#!/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 _("closes speech history")
def run(self):
self.env["runtime"]["SpeechHistoryManager"].close_history()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("copies current speech history item to the clipboard")
def run(self):
self.env["runtime"]["SpeechHistoryManager"].copy_current_to_clipboard()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("speaks current speech history item")
def run(self):
self.env["runtime"]["SpeechHistoryManager"].present_current()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("selects the next speech history item")
def run(self):
self.env["runtime"]["SpeechHistoryManager"].next_entry()
def set_callback(self, callback):
pass
@@ -0,0 +1,27 @@
#!/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 _("selects the previous speech history item")
def run(self):
self.env["runtime"]["SpeechHistoryManager"].prev_entry()
def set_callback(self, callback):
pass
@@ -127,6 +127,8 @@ class config_command:
self.config.set("speech", "rate", "0.75")
self.config.set("speech", "pitch", "0.5")
self.config.set("speech", "volume", "1.0")
self.config.set("speech", "hardware_device", "/dev/ttyS0")
self.config.set("speech", "hardware_baud_rate", "9600")
self.config.add_section("sound")
self.config.set("sound", "driver", "genericDriver")
@@ -108,6 +108,8 @@ class command(config_command):
"rate": "0.5",
"pitch": "0.5",
"volume": "1.0",
"hardware_device": "/dev/ttyS0",
"hardware_baud_rate": "9600",
"auto_read_incoming": "True",
}
@@ -139,7 +141,12 @@ class command(config_command):
self.config["general"] = {
"punctuation_level": "some",
"debug_level": "0",
}
# Basic clipboard defaults
self.config["clipboard"] = {
"number_of_clipboards": "50",
"clipboard_export_path": "/tmp/fenrirClipboard",
}
# Write the configuration
@@ -37,7 +37,7 @@ class command(config_command):
self.present_text(f"Current screen driver: {current_description}")
# Cycle through the available drivers
drivers = ["vcsaDriver", "ptyDriver", "dummyDriver", "debugDriver"]
drivers = ["vcsaDriver", "ptyDriver", "dummyDriver"]
try:
current_index = drivers.index(current_driver)
next_index = (current_index + 1) % len(drivers)
@@ -22,23 +22,24 @@ class command(config_command):
def run(self):
current_theme = self.get_setting("sound", "theme", "default")
current_theme_name = os.path.basename(
os.path.normpath(current_theme)
)
# Present current theme
self.present_text(f"Current sound theme: {current_theme}")
# Look for available sound themes
sound_paths = [
"/usr/share/sounds",
"/usr/share/fenrirscreenreader/sounds",
os.path.expanduser("~/.local/share/fenrirscreenreader/sounds"),
]
sound_paths = self.env[
"runtime"
]["SettingsManager"].get_sound_theme_roots()
available_themes = self.get_available_themes(sound_paths)
if len(available_themes) > 1:
# For this implementation, cycle through available themes
try:
current_index = available_themes.index(current_theme)
current_index = available_themes.index(current_theme_name)
next_index = (current_index + 1) % len(available_themes)
new_theme = available_themes[next_index]
except ValueError:
@@ -29,12 +29,7 @@ class command:
self.env["runtime"]["OutputManager"].present_text(
"Okay, loading the information about Nano.", interrupt=True
)
if self.env["runtime"]["InputManager"].get_shortcut_type() in ["KEY"]:
self.env["runtime"]["InputManager"].send_keys(self.key_macro)
elif self.env["runtime"]["InputManager"].get_shortcut_type() in [
"BYTE"
]:
self.env["runtime"]["ByteManager"].send_bytes(self.byteMakro)
self.env["runtime"]["InputManager"].send_keys(self.key_macro)
def set_callback(self, callback):
pass
@@ -29,12 +29,7 @@ class command:
self.env["runtime"]["OutputManager"].present_text(
"Okay, you will now be asked to save your work.", interrupt=True
)
if self.env["runtime"]["InputManager"].get_shortcut_type() in ["KEY"]:
self.env["runtime"]["InputManager"].send_keys(self.key_macro)
elif self.env["runtime"]["InputManager"].get_shortcut_type() in [
"BYTE"
]:
self.env["runtime"]["ByteManager"].send_bytes(self.byteMakro)
self.env["runtime"]["InputManager"].send_keys(self.key_macro)
def set_callback(self, callback):
pass
@@ -18,7 +18,6 @@ class command:
# self.key_macro = [[1,'KEY_LEFTCTRL'],[1,'KEY_O'],[0.05,'SLEEP'],[0,'KEY_O'],[0,'KEY_LEFTCTRL']]
# self.key_macro = [[1,'KEY_LEFTSHIFT'],[1,'KEY_LEFTCTRL'],[1,'KEY_N'],[0.05,'SLEEP'],[0,'KEY_N'],[0,'KEY_LEFTCTRL'],[0,'KEY_LEFTSHIFT']]
self.key_macro = []
self.byteMakro = []
def shutdown(self):
pass
@@ -27,12 +26,7 @@ class command:
return "No description found"
def run(self):
if self.env["runtime"]["InputManager"].get_shortcut_type() in ["KEY"]:
self.env["runtime"]["InputManager"].send_keys(self.key_macro)
elif self.env["runtime"]["InputManager"].get_shortcut_type() in [
"BYTE"
]:
self.env["runtime"]["ByteManager"].send_bytes(self.byteMakro)
self.env["runtime"]["InputManager"].send_keys(self.key_macro)
def set_callback(self, callback):
pass

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