Improve socket handling for -x spawned fenrir instances.

This commit is contained in:
Storm Dragon
2026-05-07 23:24:54 -04:00
parent 0273f9b956
commit 8638bca1d5
53 changed files with 794 additions and 1072 deletions
+28 -19
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
@@ -134,8 +129,8 @@ Settings are located in:
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
@@ -266,7 +261,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-deamon.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,6 +275,19 @@ 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-deamon.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. 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
@@ -406,6 +415,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:**
@@ -711,8 +723,6 @@ 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
@@ -723,9 +733,6 @@ 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
@@ -753,6 +760,8 @@ X11 terminal mode uses the normal keyboard layout files, including desktop and l
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
+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():
+1 -6
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
@@ -36,7 +32,7 @@ To change keyboard layout, edit `/etc/fenrir/settings/settings.conf`:
```ini
[keyboard]
keyboardLayout=desktop # or laptop, nvda-desktop, nvda-laptop, pty, pty2
keyboardLayout=desktop # or laptop
```
## Available Key Constants
@@ -582,4 +578,3 @@ BTN_TRIGGER_HAPPY37
BTN_TRIGGER_HAPPY38
BTN_TRIGGER_HAPPY39
BTN_TRIGGER_HAPPY40
-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
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
+3 -5
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
@@ -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
+22 -20
View File
@@ -13,13 +13,14 @@ 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 ]
.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.
@@ -53,14 +54,6 @@ Enable debug mode. Debug information will be logged to /var/log/fenrir.log.
.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.
@@ -234,14 +227,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,10 +258,6 @@ 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.
@@ -300,6 +287,19 @@ enableCommandRemote=True
.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-deamon.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.
.TP
.B Basic Speech Control:
.EX
@@ -355,6 +355,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:
@@ -516,4 +518,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
+17 -8
View File
@@ -1214,12 +1214,6 @@ 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.
@@ -1277,6 +1271,19 @@ 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-deamon.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.
===== Basic Speech Control
....
@@ -1338,6 +1345,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:*
@@ -1669,8 +1677,9 @@ 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:
+16 -3
View File
@@ -132,6 +132,18 @@ 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-deamon.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.
#### Speech Control
```bash
# Interrupt current speech
@@ -193,6 +205,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
@@ -318,7 +331,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,8 +354,6 @@ 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
- `-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 ignoreScreen setting
@@ -368,6 +379,8 @@ X11 terminal mode uses the same keyboard layout files as TTY Fenrir. Supported F
This mode requires `python-xlib`.
For a dedicated PTY/terminal screen reader, see TDSR: https://github.com/tspivey/tdsr
## Troubleshooting
### No Speech
+2 -1
View File
@@ -1013,7 +1013,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
-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
-1
View File
@@ -103,7 +103,6 @@ setup(
"pyudev>=0.21.0",
"setuptools",
"setproctitle",
"pexpect",
"pyte>=0.7.0",
],
extras_require={
+3 -18
View File
@@ -60,16 +60,6 @@ def create_argument_parser():
action='store_true',
help='Print debug messages to screen'
)
argumentParser.add_argument(
'-e', '--emulated-pty',
action='store_true',
help='Use PTY emulation with escape sequences for input (enables desktop/X/Wayland usage)'
)
argumentParser.add_argument(
'-E', '--emulated-evdev',
action='store_true',
help='Use PTY emulation with evdev for input (single instance)'
)
argumentParser.add_argument(
'-x', '--x11',
action='store_true',
@@ -102,13 +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"
emulated_modes = [
cliArgs.emulated_pty,
cliArgs.emulated_evdev,
cliArgs.x11,
]
if sum(bool(mode) for mode in emulated_modes) > 1:
return False, "Cannot combine --emulated-pty, --emulated-evdev, and --x11"
if cliArgs.x11_window_id and not cliArgs.x11:
return False, "--x11-window-id requires --x11"
return True, None
@@ -141,7 +126,7 @@ def run_fenrir():
def should_run_foreground(cliArgs):
return cliArgs.foreground or cliArgs.emulated_pty or cliArgs.x11
return cliArgs.foreground or cliArgs.x11
def main():
@@ -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",
@@ -33,19 +33,10 @@ class command:
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
if (self.env["runtime"]["InputManager"].get_last_deepest_input()
in [["KEY_TAB"]]):
tab_detected = True
self.env["commandBuffer"]["tabCompletion"]["lastTabTime"] = current_time
# Check if tab was pressed recently (200ms window)
if not tab_detected:
@@ -5,11 +5,16 @@
# By Chrys, Storm Dragon, and contributors.
import datetime
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 +31,59 @@ class command:
def get_description(self):
return "No Description found"
def _get_announcement_lock_path(self):
return os.path.join(
tempfile.gettempdir(),
f"fenrirscreenreader-{os.getuid()}-time-announcement.lock",
)
def _try_create_announcement_lock(self, announcement_slot, now):
lock_path = self._get_announcement_lock_path()
try:
lock_fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
except FileExistsError:
return False
with os.fdopen(lock_fd, "w", encoding="utf-8") as lock_file:
lock_file.write(f"{os.getpid()} {announcement_slot} {now}\n")
return True
def _read_announcement_lock_slot(self, lock_path):
with open(lock_path, "r", encoding="utf-8") as lock_file:
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()
if self._try_create_announcement_lock(announcement_slot, now):
return True
lock_path = self._get_announcement_lock_path()
try:
lock_slot = self._read_announcement_lock_slot(lock_path)
lock_stat = os.stat(lock_path)
except FileNotFoundError:
return self._try_create_announcement_lock(announcement_slot, now)
except OSError:
return False
if lock_slot == announcement_slot:
return False
if not lock_slot and now - lock_stat.st_mtime < ANNOUNCEMENT_LOCK_TIMEOUT_SEC:
return False
try:
os.unlink(lock_path)
except FileNotFoundError:
pass
except OSError:
return False
return self._try_create_announcement_lock(announcement_slot, now)
def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"time", "enabled"
@@ -50,6 +108,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 +117,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 +138,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"
)
@@ -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")[
@@ -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)
@@ -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
-224
View File
@@ -1,224 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import inspect
import os
import re
import time
from fenrirscreenreader.core import debug
from fenrirscreenreader.core.eventData import FenrirEventType
from fenrirscreenreader.core.i18n import _
currentdir = os.path.dirname(
os.path.realpath(os.path.abspath(inspect.getfile(inspect.currentframe())))
)
fenrir_path = os.path.dirname(currentdir)
class ByteManager:
def __init__(self):
self.switchCtrlModeOnce = 0
self.controlMode = True
self.repeat = 1
self.lastInputTime = time.time()
self.lastByteKey = b""
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def unify_escape_seq(self, escapeSequence):
converted_escape_sequence = escapeSequence
if converted_escape_sequence[0] == 27:
converted_escape_sequence = b"^[" + converted_escape_sequence[1:]
if len(converted_escape_sequence) > 1:
if (
converted_escape_sequence[0] == 94
and converted_escape_sequence[1] == 91
):
converted_escape_sequence = (
b"^[" + converted_escape_sequence[2:]
)
return converted_escape_sequence
def handle_byte_stream(self, event_data, sep=b"\x1b"):
buffer = event_data
# handle prefix
end_index = buffer.find(sep)
if end_index > 0:
curr_sequence = buffer[:end_index]
buffer = buffer[end_index:]
self.handle_single_byte_sequence(curr_sequence)
# special handlig for none found (performance)
elif end_index == -1:
self.handle_single_byte_sequence(buffer)
return
# handle outstanding sequence
while buffer != b"":
end_index = buffer[len(sep) :].find(sep)
if end_index == -1:
curr_sequence = buffer
buffer = b""
else:
curr_sequence = buffer[: end_index + len(sep)]
buffer = buffer[end_index + len(sep) :]
self.handle_single_byte_sequence(curr_sequence)
def handle_byte_input(self, event_data):
if not event_data:
return
if event_data == b"":
return
try:
self.env["runtime"]["DebugManager"].write_debug_out(
"handle_byte_input " + event_data.decode("utf8"),
debug.DebugLevel.INFO,
)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
"ByteManager handle_byte_input: Error decoding byte data: "
+ str(e),
debug.DebugLevel.ERROR,
)
self.handle_byte_stream(event_data)
def handle_single_byte_sequence(self, event_data):
converted_escape_sequence = self.unify_escape_seq(event_data)
if self.switchCtrlModeOnce > 0:
self.switchCtrlModeOnce -= 1
is_control_mode = False
if (
self.controlMode
and not self.switchCtrlModeOnce == 1
or not self.controlMode
):
is_control_mode = self.handle_control_mode(event_data)
is_command = False
if (
self.controlMode
and not self.switchCtrlModeOnce == 1
or not self.controlMode
and self.switchCtrlModeOnce == 1
):
if self.lastByteKey == converted_escape_sequence:
if time.time() - self.lastInputTime <= self.env["runtime"][
"SettingsManager"
].get_setting_as_float("keyboard", "double_tap_timeout"):
self.repeat += 1
shortcut_data = b""
for i in range(self.repeat):
shortcut_data = shortcut_data + converted_escape_sequence
is_command = self.detect_byte_command(shortcut_data)
# fall back to single stroke - do we want this?
if not is_command:
is_command = self.detect_byte_command(
converted_escape_sequence
)
self.repeat = 1
if not (is_command or is_control_mode):
self.env["runtime"]["ScreenManager"].inject_text_to_screen(
event_data
)
if not is_command:
self.repeat = 1
self.lastByteKey = converted_escape_sequence
self.lastInputTime = time.time()
def get_last_byte_key(self):
return self.lastByteKey
def handle_control_mode(self, escapeSequence):
converted_escape_sequence = self.unify_escape_seq(escapeSequence)
if converted_escape_sequence == b"^[R":
self.controlMode = not self.controlMode
self.switchCtrlModeOnce = 0
if self.controlMode:
self.env["runtime"]["OutputManager"].present_text(
_("Sticky Mode On"),
sound_icon="Accept",
interrupt=True,
flush=True,
)
else:
self.env["runtime"]["OutputManager"].present_text(
_("Sticky Mode On"),
sound_icon="Cancel",
interrupt=True,
flush=True,
)
return True
if converted_escape_sequence == b"^[:":
self.switchCtrlModeOnce = 2
self.env["runtime"]["OutputManager"].present_text(
_("bypass"), sound_icon="PTYBypass", interrupt=True, flush=True
)
return True
return False
def send_bytes(self, byteMacro):
pass
def detect_byte_command(self, escapeSequence):
converted_escape_sequence = self.unify_escape_seq(escapeSequence)
command = self.env["runtime"]["InputManager"].get_command_for_shortcut(
converted_escape_sequence
)
if command != "":
self.env["runtime"]["EventManager"].put_to_event_queue(
FenrirEventType.execute_command, command
)
command = ""
return True
return False
def load_byte_shortcuts(
self, kb_config_path=fenrir_path + "/../../config/keyboard/pty.conf"
):
kb_config = open(kb_config_path, "r")
while True:
line = kb_config.readline()
if not line:
break
line = line.replace("\n", "")
if line.replace(" ", "") == "":
continue
if line.replace(" ", "").startswith("#"):
continue
if line.count("=") != 1:
continue
values = line.split("=")
clean_shortcut = bytes(values[0], "UTF-8")
repeat = 1
if len(clean_shortcut) > 2:
if chr(clean_shortcut[1]) == ",":
try:
repeat = int(chr(clean_shortcut[0]))
clean_shortcut = clean_shortcut[2:]
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
"ByteManager load_byte_shortcuts: Error parsing repeat count: "
+ str(e),
debug.DebugLevel.ERROR,
)
repeat = 1
clean_shortcut = clean_shortcut
shortcut = b""
for i in range(repeat):
shortcut += clean_shortcut
command_name = values[1].upper()
self.env["bindings"][shortcut] = command_name
self.env["runtime"]["DebugManager"].write_debug_out(
"Byte Shortcut: " + str(shortcut) + " command:" + command_name,
debug.DebugLevel.INFO,
on_any_level=True,
)
kb_config.close()
+1 -2
View File
@@ -18,8 +18,7 @@ class FenrirEventType(Enum):
screen_changed = 5
heart_beat = 6
execute_command = 7
byte_input = 8
remote_incomming = 9
remote_incomming = 8
def __int__(self):
return self.value
@@ -61,8 +61,6 @@ class EventManager:
self.env["runtime"]["FenrirManager"].handle_heart_beat(event)
elif event["Type"] == FenrirEventType.execute_command:
self.env["runtime"]["FenrirManager"].handle_execute_command(event)
elif event["Type"] == FenrirEventType.byte_input:
self.env["runtime"]["FenrirManager"].handle_byte_input(event)
elif event["Type"] == FenrirEventType.remote_incomming:
self.env["runtime"]["FenrirManager"].handle_remote_incomming(event)
+13 -15
View File
@@ -10,6 +10,7 @@ import sys
import time
from fenrirscreenreader.core import debug
from fenrirscreenreader.core import remoteInstanceRegistry
from fenrirscreenreader.core import settingsManager
from fenrirscreenreader.core.eventData import FenrirEventType
from fenrirscreenreader.core.i18n import _
@@ -132,16 +133,6 @@ class FenrirManager:
"onKeyInput"
)
def handle_byte_input(self, event):
if not event["data"] or event["data"] == b"":
return
self.environment["runtime"]["ByteManager"].handle_byte_input(
event["data"]
)
self.environment["runtime"]["CommandManager"].execute_default_trigger(
"onByteInput"
)
def handle_execute_command(self, event):
if not event["data"] or event["data"] == "":
return
@@ -456,6 +447,7 @@ class FenrirManager:
# Clean up socket files that might not be removed by the driver
try:
socket_file = None
screen_driver = None
if (
"runtime" in self.environment
and "SettingsManager" in self.environment["runtime"]
@@ -466,14 +458,19 @@ class FenrirManager:
].get_setting("remote", "socket_file")
except Exception:
pass # Use default socket file path
try:
screen_driver = self.environment["runtime"][
"SettingsManager"
].get_setting("screen", "driver")
except Exception:
pass
if not socket_file:
# Use default socket file paths
socket_file = "/tmp/fenrirscreenreader-deamon.sock"
if os.path.exists(socket_file):
os.unlink(socket_file)
if screen_driver == "vcsaDriver":
socket_file = "/tmp/fenrirscreenreader-deamon.sock"
if os.path.exists(socket_file):
os.unlink(socket_file)
# Also try PID-based socket file
pid_socket_file = (
"/tmp/fenrirscreenreader-"
+ str(os.getpid())
@@ -483,6 +480,7 @@ class FenrirManager:
os.unlink(pid_socket_file)
elif os.path.exists(socket_file):
os.unlink(socket_file)
remoteInstanceRegistry.remove_instance()
except Exception:
pass # Ignore errors during socket cleanup
@@ -14,7 +14,6 @@ general_data = {
"managerList": [
"AttributeManager",
"PunctuationManager",
"ByteManager",
"CursorManager",
"ApplicationManager",
"CommandManager",
@@ -40,7 +39,6 @@ general_data = {
"commandFolderList": [
"commands",
"onKeyInput",
"onByteInput",
"onCursorChange",
"onScreenUpdate",
"onScreenChanged",
@@ -15,7 +15,6 @@ class InputDriver:
def initialize(self, environment):
self.env = environment
self.env["runtime"]["InputManager"].set_shortcut_type("KEY")
self._is_initialized = True
def shutdown(self):
@@ -21,17 +21,9 @@ fenrir_path = os.path.dirname(currentdir)
class InputManager:
def __init__(self):
self.shortcutType = "KEY"
self.executeDeviceGrab = False
self.lastDetectedDevices = None
def set_shortcut_type(self, shortcutType="KEY"):
if shortcutType in ["KEY", "BYTE"]:
self.shortcutType = shortcutType
def get_shortcut_type(self):
return self.shortcutType
def initialize(self, environment):
self.env = environment
self.env["runtime"]["SettingsManager"].load_driver(
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import json
import os
import tempfile
import time
INSTANCE_TIMEOUT_SEC = 30.0
def get_registry_dir():
return os.path.join(
tempfile.gettempdir(),
f"fenrirscreenreader-instances-{os.getuid()}",
)
def get_instance_file(pid=None):
if pid is None:
pid = os.getpid()
return os.path.join(get_registry_dir(), f"{pid}.json")
def write_instance(instance_data):
registry_dir = get_registry_dir()
os.makedirs(registry_dir, mode=0o700, exist_ok=True)
os.chmod(registry_dir, 0o700)
instance_data["updated_at"] = time.time()
instance_path = get_instance_file(instance_data.get("pid"))
with open(instance_path, "w", encoding="utf-8") as instance_file:
json.dump(instance_data, instance_file, sort_keys=True)
instance_file.write("\n")
def remove_instance(pid=None):
try:
os.unlink(get_instance_file(pid))
except FileNotFoundError:
pass
def process_exists(pid):
try:
os.kill(pid, 0)
return True
except OSError:
return False
def list_instances():
registry_dir = get_registry_dir()
instances = []
try:
instance_files = os.listdir(registry_dir)
except FileNotFoundError:
return instances
now = time.time()
for instance_name in instance_files:
instance_path = os.path.join(registry_dir, instance_name)
try:
with open(instance_path, "r", encoding="utf-8") as instance_file:
instance_data = json.load(instance_file)
pid = int(instance_data.get("pid", 0))
updated_at = float(instance_data.get("updated_at", 0))
except (OSError, ValueError, TypeError, json.JSONDecodeError):
continue
if not pid or not process_exists(pid) or now - updated_at > INSTANCE_TIMEOUT_SEC:
try:
os.unlink(instance_path)
except OSError:
pass
continue
instances.append(instance_data)
return sorted(instances, key=lambda instance: int(instance.get("pid", 0)))
@@ -26,14 +26,21 @@ command interrupt
"""
import hashlib
import os
import tempfile
import time
from fenrirscreenreader.core import debug
from fenrirscreenreader.core import remoteInstanceRegistry
from fenrirscreenreader.core.eventData import FenrirEventType
from fenrirscreenreader.core.i18n import _
REMOTE_COMMAND_LOCK_TIMEOUT_SEC = 2.0
REMOTE_COMMAND_LOCK_PREFIX = f"fenrirscreenreader-{os.getuid()}-remote-"
class RemoteManager:
def __init__(self):
# command controll
@@ -48,6 +55,8 @@ class RemoteManager:
self.resetWindowConst = "RESETWINDOW"
self.setClipboardConst = "CLIPBOARD "
self.exportClipboardConst = "EXPORTCLIPBOARD"
self.listInstancesConst = "LS"
self.listInstancesLongConst = "LIST"
# setting controll
self.settingConst = "SETTING "
self.setSettingConst = "SET "
@@ -208,6 +217,14 @@ class RemoteManager:
"success": True,
"message": "Clipboard exported to file",
}
elif upper_command_text in (
self.listInstancesConst,
self.listInstancesLongConst,
):
return {
"success": True,
"message": self.list_instances(),
}
else:
return {
"success": False,
@@ -257,6 +274,102 @@ class RemoteManager:
self.set_clipboard(parameter_text)
elif upper_command_text.startswith(self.exportClipboardConst):
self.export_clipboard()
elif upper_command_text in (
self.listInstancesConst,
self.listInstancesLongConst,
):
return
def _get_remote_command_lock_path(self, event_data):
event_hash = hashlib.sha256(event_data.encode("utf-8")).hexdigest()
return os.path.join(
tempfile.gettempdir(),
f"{REMOTE_COMMAND_LOCK_PREFIX}{event_hash}.lock",
)
def _cleanup_stale_remote_command_locks(self, now):
try:
lock_files = os.listdir(tempfile.gettempdir())
except OSError:
return
for lock_file in lock_files:
if not lock_file.startswith(REMOTE_COMMAND_LOCK_PREFIX):
continue
lock_path = os.path.join(tempfile.gettempdir(), lock_file)
try:
lock_age = now - os.stat(lock_path).st_mtime
if lock_age > REMOTE_COMMAND_LOCK_TIMEOUT_SEC:
os.unlink(lock_path)
except OSError:
pass
def _try_create_remote_command_lock(self, lock_path, now):
try:
lock_fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
except FileExistsError:
return False
with os.fdopen(lock_fd, "w", encoding="utf-8") as lock_file:
lock_file.write(f"{os.getpid()} {now}\n")
return True
def _claim_remote_command(self, event_data):
lock_path = self._get_remote_command_lock_path(event_data)
now = time.time()
self._cleanup_stale_remote_command_locks(now)
if self._try_create_remote_command_lock(lock_path, now):
return True
try:
with open(lock_path, "r", encoding="utf-8") as lock_file:
lock_parts = lock_file.readline().strip().split()
lock_pid = int(lock_parts[0]) if lock_parts else 0
lock_stat = os.stat(lock_path)
except FileNotFoundError:
return self._try_create_remote_command_lock(lock_path, now)
except (OSError, ValueError):
return False
if lock_pid == os.getpid():
return True
if now - lock_stat.st_mtime < REMOTE_COMMAND_LOCK_TIMEOUT_SEC:
return False
try:
os.unlink(lock_path)
except FileNotFoundError:
pass
except OSError:
return False
return self._try_create_remote_command_lock(lock_path, now)
def list_instances(self):
instances = remoteInstanceRegistry.list_instances()
if not instances:
return "No Fenrir instances registered"
lines = []
for instance in instances:
socket_files = ", ".join(instance.get("socket_files", []))
x11_window_id = instance.get("x11_window_id") or "none"
main_socket = "yes" if instance.get("main_socket") else "no"
lines.append(
"pid={pid} ppid={ppid} screen={screen} keyboard={keyboard} "
"main_socket={main_socket} x11_window_id={x11_window_id} "
"sockets={sockets}".format(
pid=instance.get("pid", ""),
ppid=instance.get("ppid", ""),
screen=instance.get("screen_driver", ""),
keyboard=instance.get("keyboard_driver", ""),
main_socket=main_socket,
x11_window_id=x11_window_id,
sockets=socket_files,
)
)
return "\n".join(lines)
def temp_disable_speech(self):
self.env["runtime"]["OutputManager"].temp_disable_speech()
@@ -381,6 +494,14 @@ class RemoteManager:
)
try:
if upper_event_data in (
self.listInstancesConst,
self.listInstancesLongConst,
):
return {
"success": True,
"message": self.list_instances(),
}
if upper_event_data.startswith(self.settingConst):
settings_text = event_data[len(self.settingConst) :]
return self.handle_settings_change_with_response(settings_text)
@@ -406,6 +527,9 @@ class RemoteManager:
debug.DebugLevel.INFO,
)
if not self._claim_remote_command(event_data):
return
if upper_event_data.startswith(self.settingConst):
settings_text = event_data[len(self.settingConst) :]
self.handle_settings_change(settings_text)
+27 -89
View File
@@ -11,7 +11,6 @@ from configparser import ConfigParser
from fenrirscreenreader.core import applicationManager
from fenrirscreenreader.core import attributeManager
from fenrirscreenreader.core import barrierManager
from fenrirscreenreader.core import byteManager
from fenrirscreenreader.core import commandManager
from fenrirscreenreader.core import cursorManager
from fenrirscreenreader.core import debug
@@ -410,9 +409,7 @@ class SettingsManager:
if setting == "driver":
valid_drivers = [
"evdevDriver",
"ptyDriver",
"x11Driver",
"atspiDriver",
"dummyDriver",
]
if value not in valid_drivers:
@@ -496,18 +493,6 @@ class SettingsManager:
if cliArgs.print:
self.set_setting("general", "debug_level", 3)
self.set_setting("general", "debug_mode", "PRINT")
if cliArgs.emulated_pty:
# Set PTY driver settings
pty_settings = {
"screen": {"driver": "ptyDriver"},
"keyboard": {"driver": "ptyDriver", "keyboard_layout": "pty"}
}
for section, settings in pty_settings.items():
for key, value in settings.items():
self.set_setting(section, key, value)
if cliArgs.emulated_evdev:
self.set_setting("screen", "driver", "ptyDriver")
self.set_setting("keyboard", "driver", "evdevDriver")
if cliArgs.x11:
self.set_setting("screen", "driver", "ptyDriver")
self.set_setting("keyboard", "driver", "x11Driver")
@@ -631,9 +616,6 @@ class SettingsManager:
environment["runtime"]["OutputManager"] = outputManager.OutputManager()
environment["runtime"]["OutputManager"].initialize(environment)
environment["runtime"]["ByteManager"] = byteManager.ByteManager()
environment["runtime"]["ByteManager"].initialize(environment)
environment["runtime"]["InputManager"] = inputManager.InputManager()
environment["runtime"]["InputManager"].initialize(environment)
@@ -656,89 +638,45 @@ class SettingsManager:
] = diffReviewManager.DiffReviewManager()
environment["runtime"]["DiffReviewManager"].initialize(environment)
if environment["runtime"]["InputManager"].get_shortcut_type() == "KEY":
if not os.path.exists(
self.get_setting("keyboard", "keyboard_layout")
if not os.path.exists(
self.get_setting("keyboard", "keyboard_layout")
):
if os.path.exists(
settings_root
+ "keyboard/"
+ self.get_setting("keyboard", "keyboard_layout")
):
if os.path.exists(
self.set_setting(
"keyboard",
"keyboard_layout",
settings_root
+ "keyboard/"
+ self.get_setting("keyboard", "keyboard_layout")
):
self.set_setting(
"keyboard",
"keyboard_layout",
settings_root
+ "keyboard/"
+ self.get_setting("keyboard", "keyboard_layout"),
)
environment["runtime"]["InputManager"].load_shortcuts(
self.get_setting("keyboard", "keyboard_layout")
)
if os.path.exists(
settings_root
+ "keyboard/"
+ self.get_setting("keyboard", "keyboard_layout")
+ ".conf"
):
self.set_setting(
"keyboard",
"keyboard_layout",
settings_root
+ "keyboard/"
+ self.get_setting("keyboard", "keyboard_layout")
+ ".conf",
)
environment["runtime"]["InputManager"].load_shortcuts(
self.get_setting("keyboard", "keyboard_layout")
)
else:
+ self.get_setting("keyboard", "keyboard_layout"),
)
environment["runtime"]["InputManager"].load_shortcuts(
self.get_setting("keyboard", "keyboard_layout")
)
elif (
environment["runtime"]["InputManager"].get_shortcut_type()
== "BYTE"
):
if not os.path.exists(
self.get_setting("keyboard", "keyboard_layout")
if os.path.exists(
settings_root
+ "keyboard/"
+ self.get_setting("keyboard", "keyboard_layout")
+ ".conf"
):
if os.path.exists(
self.set_setting(
"keyboard",
"keyboard_layout",
settings_root
+ "keyboard/"
+ self.get_setting("keyboard", "keyboard_layout")
):
self.set_setting(
"keyboard",
"keyboard_layout",
settings_root
+ "keyboard/"
+ self.get_setting("keyboard", "keyboard_layout"),
)
environment["runtime"]["ByteManager"].load_byte_shortcuts(
self.get_setting("keyboard", "keyboard_layout")
)
if os.path.exists(
settings_root
+ "keyboard/"
+ self.get_setting("keyboard", "keyboard_layout")
+ ".conf"
):
self.set_setting(
"keyboard",
"keyboard_layout",
settings_root
+ "keyboard/"
+ self.get_setting("keyboard", "keyboard_layout")
+ ".conf",
)
environment["runtime"]["ByteManager"].load_byte_shortcuts(
self.get_setting("keyboard", "keyboard_layout")
)
else:
environment["runtime"]["ByteManager"].load_byte_shortcuts(
+ ".conf",
)
environment["runtime"]["InputManager"].load_shortcuts(
self.get_setting("keyboard", "keyboard_layout")
)
else:
environment["runtime"]["InputManager"].load_shortcuts(
self.get_setting("keyboard", "keyboard_layout")
)
environment["runtime"]["CursorManager"] = cursorManager.CursorManager()
environment["runtime"]["CursorManager"].initialize(environment)
+2 -6
View File
@@ -33,9 +33,7 @@ class VmenuManager:
self.env = environment
# use default path
self.defaultVMenuPath = (
fenrir_path
+ "/commands/vmenu-profiles/"
+ self.env["runtime"]["InputManager"].get_shortcut_type()
fenrir_path + "/commands/vmenu-profiles/KEY"
)
# if there is no user configuration
if (
@@ -49,9 +47,7 @@ class VmenuManager:
].get_setting("menu", "vmenu_path")
if not self.defaultVMenuPath.endswith("/"):
self.defaultVMenuPath += "/"
self.defaultVMenuPath += self.env["runtime"][
"InputManager"
].get_shortcut_type()
self.defaultVMenuPath += "KEY"
self.create_menu_tree()
self.closeAfterAction = False
@@ -16,7 +16,6 @@ class driver(inputDriver):
def initialize(self, environment):
self.env = environment
self.env["runtime"]["InputManager"].set_shortcut_type("KEY")
self._initialized = True
print("Input Debug Driver: Initialized")
@@ -87,7 +87,6 @@ class driver(inputDriver):
if libraries are not available.
"""
self.env = environment
self.env["runtime"]["InputManager"].set_shortcut_type("KEY")
global _evdevAvailable
global _udevAvailable
global _evdevAvailableError
@@ -1,215 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import time
from fenrirscreenreader.core import debug
from fenrirscreenreader.core.inputDriver import InputDriver as inputDriver
class driver(inputDriver):
"""PTY (Pseudo-terminal) input driver for Fenrir screen reader.
This driver provides input handling for terminal emulation environments
where direct device access (evdev) is not available or appropriate.
It uses byte-based input processing instead of key event processing.
This is primarily used when running Fenrir in terminal emulators,
desktop environments, or other contexts where traditional TTY device
access is not available.
Features:
- Byte-based input processing
- Terminal emulation compatibility
- Simplified input handling for non-TTY environments
"""
def __init__(self):
self._is_initialized = False
inputDriver.__init__(self)
def initialize(self, environment):
"""Initialize the PTY input driver.
Sets the input manager to use byte-based shortcuts instead of
key-based shortcuts, enabling proper operation in terminal
emulation environments.
Args:
environment: Fenrir environment dictionary
Returns:
bool: True if initialization successful, False otherwise
"""
try:
if environment is None:
raise ValueError("Environment cannot be None")
self.env = environment
# Validate required managers are available
if "runtime" not in self.env:
raise ValueError("Runtime environment missing")
if "InputManager" not in self.env["runtime"]:
raise ValueError("InputManager not available")
self.env["runtime"]["InputManager"].set_shortcut_type("BYTE")
self._is_initialized = True
self.env["runtime"]["DebugManager"].write_debug_out(
"PTY inputDriver: Initialized with byte-based shortcuts",
debug.DebugLevel.INFO
)
return True
except Exception as e:
# Log error if possible, otherwise fallback to print
try:
if hasattr(self, 'env') and self.env and "runtime" in self.env:
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY inputDriver: Initialization failed: {e}",
debug.DebugLevel.ERROR
)
else:
print(f"PTY inputDriver initialization error: {e}")
except:
print(f"PTY inputDriver initialization error: {e}")
self._is_initialized = False
return False
def shutdown(self):
"""Shutdown the PTY input driver.
Performs cleanup operations when the driver is being stopped.
For PTY driver, this involves cleaning up any resources and
logging the shutdown.
"""
if not self._is_initialized:
return
try:
self.env["runtime"]["DebugManager"].write_debug_out(
"PTY inputDriver: Shutting down",
debug.DebugLevel.INFO
)
except Exception as e:
# Fallback logging if debug manager is unavailable
print(f"PTY inputDriver shutdown error: {e}")
finally:
self._is_initialized = False
def get_input_event(self):
"""Get input event from PTY.
For PTY driver, input events are handled through the byte-based
shortcut system rather than direct device events. This method
returns None as PTY input is processed through the screen driver
and InputManager's byte processing.
Returns:
None: PTY driver uses byte-based processing, not event-based
"""
return None
def is_device_connected(self):
"""Check if PTY input device is connected.
For PTY driver, the "device" is the terminal interface itself,
which is considered connected if the driver is initialized.
Returns:
bool: True if driver is initialized, False otherwise
"""
return self._is_initialized
def get_device_name(self):
"""Get the name of the PTY input device.
Returns:
str: Human-readable name of the PTY input device
"""
return "PTY (Pseudo-terminal) Input"
def grab_devices(self, grab=True):
"""Grab or release input devices.
For PTY driver, device grabbing is not applicable since input
is processed through terminal emulation rather than direct
device access.
Args:
grab (bool): Whether to grab (True) or release (False) devices
Returns:
bool: Always returns True for PTY driver (no-op success)
"""
if not self._is_initialized:
return False
action = "grab" if grab else "release"
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY inputDriver: {action} devices (no-op for PTY)",
debug.DebugLevel.INFO
)
return True
def has_device_detection(self):
"""Check if driver supports device detection.
PTY driver does not support dynamic device detection since
it operates on the terminal interface directly.
Returns:
bool: Always False for PTY driver
"""
return False
def get_device_list(self):
"""Get list of available input devices.
For PTY driver, there is only one logical device - the terminal
interface itself.
Returns:
list: Single-item list containing PTY device info
"""
if not self._is_initialized:
return []
return [{
'name': 'PTY Terminal',
'path': '/dev/pts/*',
'type': 'terminal',
'connected': True
}]
def get_led_state(self, led_mask=None):
"""Get LED state information.
PTY driver cannot access LED states since it operates through
terminal emulation rather than direct hardware access.
Args:
led_mask: LED mask parameter (ignored for PTY)
Returns:
dict: Empty dict (no LED access for PTY)
"""
return {}
def set_led_state(self, led_dict):
"""Set LED states.
PTY driver cannot control LEDs since it operates through
terminal emulation rather than direct hardware access.
Args:
led_dict (dict): LED state dictionary (ignored for PTY)
Returns:
bool: Always False (LED control not supported)
"""
return False
@@ -160,7 +160,6 @@ class driver(inputDriver):
def initialize(self, environment):
self.env = environment
self.env["runtime"]["InputManager"].set_shortcut_type("KEY")
if display is None:
self.fail_startup("python-xlib is not available: " + str(_x_error))
self.display = display.Display()
+148 -52
View File
@@ -8,15 +8,22 @@ import os
import os.path
import select
import socket
import time
from fenrirscreenreader.core import debug
from fenrirscreenreader.core import remoteInstanceRegistry
from fenrirscreenreader.core.eventData import FenrirEventType
from fenrirscreenreader.core.remoteDriver import RemoteDriver as remoteDriver
MAIN_SOCKET_FILE = "/tmp/fenrirscreenreader-deamon.sock"
class driver(remoteDriver):
def __init__(self):
remoteDriver.__init__(self)
self.fenrirSocks = []
self.socket_files = []
def initialize(self, environment):
self.env = environment
@@ -26,9 +33,7 @@ class driver(remoteDriver):
self.watch_dog, multiprocess=False
)
def watch_dog(self, active, event_queue):
# echo "command say this is a test" | socat -
# UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
def _get_configured_socket_file(self):
socket_file = ""
try:
socket_file = self.env["runtime"]["SettingsManager"].get_setting(
@@ -40,62 +45,153 @@ class driver(remoteDriver):
+ str(e),
debug.DebugLevel.ERROR,
)
if socket_file == "":
if (
self.env["runtime"]["SettingsManager"].get_setting(
"screen", "driver"
)
== "vcsaDriver"
):
socket_file = "/tmp/fenrirscreenreader-deamon.sock"
else:
socket_file = (
"/tmp/fenrirscreenreader-" + str(os.getppid()) + ".sock"
)
return socket_file
def _get_socket_candidates(self):
configured_socket_file = self._get_configured_socket_file()
if configured_socket_file:
return [(configured_socket_file, False)]
screen_driver = self.env["runtime"]["SettingsManager"].get_setting(
"screen", "driver"
)
if screen_driver == "vcsaDriver":
return [(MAIN_SOCKET_FILE, False)]
private_socket_file = (
"/tmp/fenrirscreenreader-" + str(os.getpid()) + ".sock"
)
return [(private_socket_file, False), (MAIN_SOCKET_FILE, True)]
def _is_socket_active(self, socket_file):
test_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
test_sock.settimeout(0.2)
test_sock.connect(socket_file)
return True
except OSError:
return False
finally:
test_sock.close()
def _bind_socket(self, socket_file, optional):
if os.path.exists(socket_file):
if optional and self._is_socket_active(socket_file):
return None
os.unlink(socket_file)
self.fenrirSock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.fenrirSock.bind(socket_file)
os.chmod(socket_file, 0o666) # Allow all users to read/write
self.fenrirSock.listen(1)
fenrir_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
fenrir_sock.bind(socket_file)
os.chmod(socket_file, 0o666) # Allow all users to read/write
fenrir_sock.listen(1)
except OSError:
fenrir_sock.close()
if optional:
return None
raise
return fenrir_sock
def _register_instance(self):
settings_manager = self.env["runtime"]["SettingsManager"]
instance_data = {
"pid": os.getpid(),
"ppid": os.getppid(),
"socket_files": self.socket_files,
"main_socket": MAIN_SOCKET_FILE in self.socket_files,
"screen_driver": settings_manager.get_setting("screen", "driver"),
"keyboard_driver": settings_manager.get_setting("keyboard", "driver"),
"x11_window_id": settings_manager.get_setting(
"keyboard", "x11_window_id"
),
"created_at": time.time(),
}
remoteInstanceRegistry.write_instance(instance_data)
def _cleanup(self):
for fenrir_sock in self.fenrirSocks:
try:
fenrir_sock.close()
except OSError:
pass
self.fenrirSocks = []
for socket_file in self.socket_files:
try:
if os.path.exists(socket_file):
os.unlink(socket_file)
except OSError:
pass
self.socket_files = []
remoteInstanceRegistry.remove_instance()
def _handle_client(self, client_sock, event_queue):
try:
rawdata = client_sock.recv(8129)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
"unixDriver watch_dog: Error receiving data from client: "
+ str(e),
debug.DebugLevel.ERROR,
)
rawdata = b""
try:
data = rawdata.decode("utf-8").rstrip().lstrip()
upper_data = data.upper()
if upper_data in ("LS", "LIST", "COMMAND LS", "COMMAND LIST"):
response = self.env["runtime"][
"RemoteManager"
].handle_remote_incomming_with_response(data)
client_sock.sendall((response["message"] + "\n").encode("utf-8"))
return
event_queue.put(
{
"Type": FenrirEventType.remote_incomming,
"data": data,
}
)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
"unixDriver watch_dog: Error decoding/queuing data: "
+ str(e),
debug.DebugLevel.ERROR,
)
def watch_dog(self, active, event_queue):
# echo "command say this is a test" | socat -
# UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
for socket_file, optional in self._get_socket_candidates():
fenrir_sock = self._bind_socket(socket_file, optional)
if fenrir_sock is None:
continue
self.fenrirSocks.append(fenrir_sock)
self.socket_files.append(socket_file)
if not self.fenrirSocks:
return
self._register_instance()
last_register = time.time()
while active.value:
if time.time() - last_register > 10.0:
self._register_instance()
last_register = time.time()
# Check if the client is still connected and if data is available:
try:
r, _, _ = select.select([self.fenrirSock], [], [], 0.8)
r, _, _ = select.select(self.fenrirSocks, [], [], 0.8)
except select.error:
break
if r == []:
continue
if self.fenrirSock in r:
client_sock, client_addr = self.fenrirSock.accept()
for fenrir_sock in r:
client_sock, client_addr = fenrir_sock.accept()
# Ensure client socket is always closed to prevent resource
# leaks
try:
try:
rawdata = client_sock.recv(8129)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
"unixDriver watch_dog: Error receiving data from "
"client: "
+ str(e),
debug.DebugLevel.ERROR,
)
rawdata = b"" # Set default empty data if recv fails
try:
data = rawdata.decode("utf-8").rstrip().lstrip()
event_queue.put(
{
"Type": FenrirEventType.remote_incomming,
"data": data,
}
)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
"unixDriver watch_dog: Error decoding/queuing data: "
+ str(e),
debug.DebugLevel.ERROR,
)
self._handle_client(client_sock, event_queue)
finally:
# Always close client socket, even if data processing fails
try:
@@ -106,8 +202,8 @@ class driver(remoteDriver):
+ str(e),
debug.DebugLevel.ERROR,
)
if self.fenrirSock:
self.fenrirSock.close()
self.fenrirSock = None
if os.path.exists(socket_file):
os.unlink(socket_file)
self._cleanup()
def shutdown(self):
self._cleanup()
remoteDriver.shutdown(self)
@@ -254,9 +254,6 @@ class driver(screenDriver):
# Load configurable timeouts from settings
self._load_pty_settings()
self.shortcutType = self.env["runtime"][
"InputManager"
].get_shortcut_type()
self.env["runtime"]["ProcessManager"].add_custom_event_thread(
self.terminal_emulation
)
@@ -403,31 +400,23 @@ class driver(screenDriver):
}
)
break
if self.shortcutType == "KEY":
try:
self.inject_text_to_screen(msg_bytes)
except Exception as e:
self.env["runtime"][
"DebugManager"
].write_debug_out(
"ptyDriver getInputData: Error injecting text to screen: "
+ str(e),
debug.DebugLevel.ERROR,
)
event_queue.put(
{
"Type": FenrirEventType.stop_main_loop,
"data": None,
}
)
break
else:
try:
self.inject_text_to_screen(msg_bytes)
except Exception as e:
self.env["runtime"][
"DebugManager"
].write_debug_out(
"ptyDriver getInputData: Error injecting text to screen: "
+ str(e),
debug.DebugLevel.ERROR,
)
event_queue.put(
{
"Type": FenrirEventType.byte_input,
"data": msg_bytes,
"Type": FenrirEventType.stop_main_loop,
"data": None,
}
)
break
# output
if self.p_out in r:
try:
+62
View File
@@ -216,6 +216,68 @@ class TestRemoteDataFormat:
assert result["success"] is False
assert "Unknown command format" in result["message"]
def test_list_instances_top_level_command(self, mock_environment):
"""Test listing registered Fenrir instances."""
self.manager.initialize(mock_environment)
with patch(
"fenrirscreenreader.core.remoteManager.remoteInstanceRegistry.list_instances",
return_value=[
{
"pid": 123,
"ppid": 100,
"screen_driver": "ptyDriver",
"keyboard_driver": "x11Driver",
"main_socket": True,
"x11_window_id": "0x123",
"socket_files": [
"/tmp/fenrirscreenreader-123.sock",
"/tmp/fenrirscreenreader-deamon.sock",
],
}
],
):
result = self.manager.handle_remote_incomming_with_response("ls")
assert result["success"] is True
assert "pid=123" in result["message"]
assert "x11_window_id=0x123" in result["message"]
assert "/tmp/fenrirscreenreader-123.sock" in result["message"]
def test_remote_incoming_suppresses_command_claimed_by_other_instance(
self, mock_environment, tmp_path
):
"""Test untargeted duplicate remote commands only run in one process."""
self.manager.initialize(mock_environment)
lock_path = tmp_path / "remote-command.lock"
lock_path.write_text("999999 1\n")
with patch.object(
self.manager,
"_get_remote_command_lock_path",
return_value=str(lock_path),
):
self.manager.handle_remote_incomming("command say duplicated")
mock_environment["runtime"]["OutputManager"].speak_text.assert_not_called()
def test_remote_incoming_allows_same_instance_repeat(
self, mock_environment, tmp_path
):
"""Test repeated direct commands to one instance are not suppressed."""
self.manager.initialize(mock_environment)
lock_path = tmp_path / "remote-command.lock"
with patch.object(
self.manager,
"_get_remote_command_lock_path",
return_value=str(lock_path),
):
self.manager.handle_remote_incomming("command say repeat")
self.manager.handle_remote_incomming("command say repeat")
assert mock_environment["runtime"]["OutputManager"].speak_text.call_count == 2
@pytest.mark.integration
@pytest.mark.remote
+22
View File
@@ -0,0 +1,22 @@
import pytest
from fenrirscreenreader.screenDriver.ptyDriver import Terminal
class DummyProcessInput:
def __init__(self):
self.data = []
def write(self, data):
self.data.append(data)
@pytest.mark.unit
def test_csi_sequences_with_intermediate_characters_do_not_render_final_byte():
terminal = Terminal(10, 3, DummyProcessInput())
terminal.feed(b"\x1b[2026$p\x1b[2048$pX")
screen = terminal.get_screen_content()
assert screen["text"].splitlines()[0] == "X "
assert "p" not in screen["text"]
+5 -2
View File
@@ -136,11 +136,14 @@ class TestDriverValidation:
"""Keyboard driver should only accept whitelisted values."""
# Valid drivers
self.manager._validate_setting_value("keyboard", "driver", "evdevDriver")
self.manager._validate_setting_value("keyboard", "driver", "ptyDriver")
self.manager._validate_setting_value("keyboard", "driver", "x11Driver")
self.manager._validate_setting_value("keyboard", "driver", "atspiDriver")
self.manager._validate_setting_value("keyboard", "driver", "dummyDriver")
with pytest.raises(ValueError, match="Invalid input driver"):
self.manager._validate_setting_value("keyboard", "driver", "ptyDriver")
with pytest.raises(ValueError, match="Invalid input driver"):
self.manager._validate_setting_value("keyboard", "driver", "atspiDriver")
# Invalid driver
with pytest.raises(ValueError, match="Invalid input driver"):
self.manager._validate_setting_value("keyboard", "driver", "badDriver")
+83
View File
@@ -0,0 +1,83 @@
"""
Unit tests for automatic time announcement behavior.
"""
import datetime
import importlib.util
from pathlib import Path
from unittest.mock import Mock
def _load_time_module():
module_path = (
Path(__file__).resolve().parents[2]
/ "src"
/ "fenrirscreenreader"
/ "commands"
/ "onHeartBeat"
/ "76000-time.py"
)
spec = importlib.util.spec_from_file_location("fenrir_time_command", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _create_environment(output_manager):
now = datetime.datetime.now()
settings_manager = Mock()
settings_manager.get_setting.side_effect = lambda section, setting: {
("time", "on_minutes"): str(now.minute).zfill(2),
("general", "date_format"): "%A, %B %d, %Y",
("general", "time_format"): "%I:%M%P",
}[(section, setting)]
settings_manager.get_setting_as_int.side_effect = lambda section, setting: {
("time", "delay_sec"): 0,
}[(section, setting)]
settings_manager.get_setting_as_bool.side_effect = lambda section, setting: {
("time", "enabled"): True,
("time", "present_date"): True,
("time", "present_time"): True,
("time", "interrupt"): False,
("time", "announce"): True,
}[(section, setting)]
return {
"runtime": {
"OutputManager": output_manager,
"SettingsManager": settings_manager,
}
}
def test_time_announcement_lock_suppresses_second_instance(tmp_path):
time_module = _load_time_module()
lock_path = tmp_path / "time-announcement.lock"
first_output = Mock(
interrupt_output=Mock(),
play_sound_icon=Mock(),
present_text=Mock(),
)
second_output = Mock(
interrupt_output=Mock(),
play_sound_icon=Mock(),
present_text=Mock(),
)
first_command = time_module.command()
second_command = time_module.command()
first_command.initialize(_create_environment(first_output))
second_command.initialize(_create_environment(second_output))
first_command._get_announcement_lock_path = lambda: str(lock_path)
second_command._get_announcement_lock_path = lambda: str(lock_path)
first_command.last_time -= datetime.timedelta(hours=1)
second_command.last_time -= datetime.timedelta(hours=1)
first_command.run()
second_command.run()
first_output.play_sound_icon.assert_called_once_with("announce")
first_output.present_text.assert_called()
second_output.interrupt_output.assert_not_called()
second_output.play_sound_icon.assert_not_called()
second_output.present_text.assert_not_called()
+21 -5
View File
@@ -30,14 +30,17 @@ def test_x11_mode_runs_in_foreground():
@pytest.mark.unit
def test_x11_mode_rejects_other_emulated_modes():
def test_removed_emulated_pty_flag_is_rejected():
fenrir = load_fenrir_entrypoint()
args = fenrir.create_argument_parser().parse_args(["-x", "-e"])
with pytest.raises(SystemExit):
fenrir.create_argument_parser().parse_args(["-e"])
is_valid, error = fenrir.validate_arguments(args)
assert is_valid is False
assert "--x11" in error
@pytest.mark.unit
def test_removed_emulated_evdev_flag_is_rejected():
fenrir = load_fenrir_entrypoint()
with pytest.raises(SystemExit):
fenrir.create_argument_parser().parse_args(["-E"])
@pytest.mark.unit
@@ -50,6 +53,19 @@ def test_x11_cli_accepts_window_id():
assert args.x11_window_id == "0x123"
@pytest.mark.unit
def test_x11_window_id_requires_x11_mode():
fenrir = load_fenrir_entrypoint()
args = fenrir.create_argument_parser().parse_args(
["--x11-window-id", "0x123"]
)
is_valid, error = fenrir.validate_arguments(args)
assert is_valid is False
assert "--x11-window-id requires --x11" == error
@pytest.mark.unit
def test_x11_key_name_mapping_for_keypad_and_capslock():
x11 = X11Driver()
-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 ""
@@ -1216,4 +1205,3 @@ msgstr ""
#: ../src/fenrirscreenreader\core\vmenuManager.py:234
msgid "Action"
msgstr ""