Compare commits
18 Commits
ce43d64e77
...
testing
| Author | SHA1 | Date | |
|---|---|---|---|
| 447e2cc896 | |||
| 2b7d205f06 | |||
| fd5fe5b328 | |||
| 4ed3f4d6ab | |||
| 2cb83632f9 | |||
| 0c4fe50606 | |||
| 15f2435749 | |||
| 3897b63068 | |||
| f1a8e6af21 | |||
| bd54ec0edb | |||
| b9518f52ec | |||
| c143c9a561 | |||
| 7e2f927596 | |||
| 788e678ed6 | |||
| ea89e90c2f | |||
| 89b85c6f17 | |||
| 1f7aa99cc0 | |||
| e8bc34eaf5 |
@@ -653,6 +653,10 @@ Building...
|
|||||||
- **Non-blocking**: Progress tones don't interrupt speech or other functionality
|
- **Non-blocking**: Progress tones don't interrupt speech or other functionality
|
||||||
- **Configurable**: Can be enabled/disabled as needed
|
- **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
|
### Usage Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -736,7 +740,7 @@ send_fenrir_command("setting set speech#rate=0.9")
|
|||||||
|
|
||||||
**Commands not working:**
|
**Commands not working:**
|
||||||
- Verify `enable_command_remote=True` in settings
|
- Verify `enable_command_remote=True` in settings
|
||||||
- Check Fenrir debug logs: `/var/log/fenrir.log`
|
- Check Fenrir debug logs: `/tmp/fenrir.log`
|
||||||
- Test with simple command: `echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock`
|
- Test with simple command: `echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock`
|
||||||
|
|
||||||
## Command Line Options
|
## Command Line Options
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ https://git.stormux.org/storm/fenrir/issues
|
|||||||
|
|
||||||
For bugs, please provide a debug file that shows the issue.
|
For bugs, please provide a debug file that shows the issue.
|
||||||
How to create a debug file:
|
How to create a debug file:
|
||||||
1. first delete old stuff:
|
1. start fenrir in debug mode
|
||||||
sudo rm /var/log/fenrir.log
|
|
||||||
2. start fenrir in debug mode
|
|
||||||
sudo fenrir -d
|
sudo fenrir -d
|
||||||
<do your stuff>
|
<do your stuff>
|
||||||
3.
|
2.
|
||||||
stop fenrir (fenrirKey + q)
|
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.
|
please be as precise as possible to make it easy to solve the problem.
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ volume=0.7
|
|||||||
|
|
||||||
# shell commands for generic sound driver
|
# shell commands for generic sound driver
|
||||||
# the folowing variable are substituted
|
# the folowing variable are substituted
|
||||||
# fenrirVolume = the current volume setting
|
# fenrir_volume = the current volume setting
|
||||||
# fenrirSoundFile = the soundfile for an soundicon
|
# fenrir_sound_file = the soundfile for an soundicon
|
||||||
# fenrirFrequence = the frequency to play
|
# fenrir_frequency = the frequency to play
|
||||||
# fenrirDuration = the duration of the frequency
|
# fenrir_duration = the duration of the frequency
|
||||||
# the following command is used to play a soundfile
|
# 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
|
#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
|
# Enable progress bar monitoring with ascending tones by default
|
||||||
progress_monitoring=True
|
progress_monitoring=True
|
||||||
@@ -114,17 +114,17 @@ max_batch_lines=100
|
|||||||
# Only enable flood control if this many new lines appear in the window
|
# Only enable flood control if this many new lines appear in the window
|
||||||
flood_line_threshold=500
|
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
|
# the following variables are replaced with values
|
||||||
# fenrirText = is the text that should be spoken
|
# fenrir_text = is the text that should be spoken
|
||||||
# fenrirModule = may be the speech module like used in speech-dispatcher, not every TTY need this
|
# fenrir_module = may be the speech module like used in speech-dispatcher, not every TTY need this
|
||||||
# fenrirLanguage = the language
|
# fenrir_language = the language
|
||||||
# fenrirVoice = is the current voice that should be used. Set the voice variable above.
|
# 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
|
# the current volume, pitch and rate is calculated like this
|
||||||
# value = min + settingValue * (min - max )
|
# value = min + settingValue * (min - max )
|
||||||
# fenrirVolume = is replaced with the current volume
|
# fenrir_volume = is replaced with the current volume
|
||||||
# fenrirPitch = is replaced with the current pitch
|
# fenrir_pitch = is replaced with the current pitch
|
||||||
# fenrirRate = is replaced with the current speed (speech rate)
|
# 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"
|
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
|
# min and max values of the TTS system that is used in generic_speech_command
|
||||||
@@ -183,7 +183,8 @@ double_tap_timeout=0.2
|
|||||||
# The default is 0, no logging.
|
# The default is 0, no logging.
|
||||||
debug_level=0
|
debug_level=0
|
||||||
# debugMode sets where the debug output should send to:
|
# debugMode sets where the debug output should send to:
|
||||||
# debugMode=File writes to 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
|
# debugMode=Print just prints on the screen
|
||||||
debug_mode=File
|
debug_mode=File
|
||||||
debug_file=
|
debug_file=
|
||||||
@@ -200,7 +201,7 @@ new_line_pause=True
|
|||||||
number_of_clipboards=50
|
number_of_clipboards=50
|
||||||
# used path for "export_clipboard_to_file"
|
# used path for "export_clipboard_to_file"
|
||||||
# $user is replaced by username
|
# $user is replaced by username
|
||||||
#clipboardExportPath=/home/$user/fenrirClipboard
|
#clipboard_export_path=/home/$user/fenrirClipboard
|
||||||
clipboard_export_path=/tmp/fenrirClipboard
|
clipboard_export_path=/tmp/fenrirClipboard
|
||||||
# Convert text emoticons like :) to descriptive text (e.g., "smiling face")
|
# Convert text emoticons like :) to descriptive text (e.g., "smiling face")
|
||||||
emoticons=True
|
emoticons=True
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ sudo ./fenrir -f -d -p
|
|||||||
|
|
||||||
# Debug output goes to:
|
# Debug output goes to:
|
||||||
# - Console (with -p flag)
|
# - Console (with -p flag)
|
||||||
# - /var/log/fenrir.log
|
# - /tmp/fenrir.log
|
||||||
```
|
```
|
||||||
|
|
||||||
## Creating Commands
|
## Creating Commands
|
||||||
|
|||||||
+4
-2
@@ -50,7 +50,9 @@ Multiple settings can be separated by semicolons.
|
|||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BR \-d ", " \-\-debug
|
.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
|
.TP
|
||||||
.BR \-p ", " \-\-print
|
.BR \-p ", " \-\-print
|
||||||
@@ -476,7 +478,7 @@ User sound themes
|
|||||||
User scripts
|
User scripts
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B /var/log/fenrir.log
|
.B /tmp/fenrir.log
|
||||||
Debug log file
|
Debug log file
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
|
|||||||
+15
-15
@@ -1506,38 +1506,38 @@ Values: `+0.0+` is quietest, `+1.0+` is loudest.
|
|||||||
The generic sound driver uses shell commands for play sound and
|
The generic sound driver uses shell commands for play sound and
|
||||||
frequencies.
|
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.
|
sound file.
|
||||||
|
|
||||||
....
|
....
|
||||||
generic_play_file_command=<your command for playing a 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.
|
frequencies.
|
||||||
|
|
||||||
....
|
....
|
||||||
generic_frequency_command=<your command for playing a frequence>
|
generic_frequency_command=<your command for playing a frequence>
|
||||||
....
|
....
|
||||||
|
|
||||||
The following variables are substituted in `+genericPlayFileCommand+`
|
The following variables are substituted in `+generic_play_file_command+`
|
||||||
and `+genericFrequencyCommand+`:
|
and `+generic_frequency_command+`:
|
||||||
|
|
||||||
* `+fenrirVolume+` = the current volume setting
|
* `+fenrir_volume+` = the current volume setting
|
||||||
* `+fenrirSoundFile+` = the sound file for an sound icon
|
* `+fenrir_sound_file+` = the sound file for an sound icon
|
||||||
* `+fenrirFrequence+` = the frequency to play
|
* `+fenrir_frequency+` = the frequency to play
|
||||||
* `+fenrirDuration+` = the duration of the frequency
|
* `+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
|
==== Speech
|
||||||
@@ -2276,13 +2276,13 @@ that shows the issue.
|
|||||||
|
|
||||||
==== How-to create a debug file
|
==== How-to create a debug file
|
||||||
|
|
||||||
. Delete old debug stuff +
|
|
||||||
`+sudo rm /var/log/fenrir.log+`
|
|
||||||
. Start fenrir in debug mode +
|
. Start fenrir in debug mode +
|
||||||
`+sudo fenrir -d+`
|
`+sudo fenrir -d+`
|
||||||
. Do your stuff to reproduce the problem
|
. Do your stuff to reproduce the problem
|
||||||
. Stop fenrir (`+fenrirKey + q+`)
|
. 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.
|
Please be as precise as possible to make it easy to solve the problem.
|
||||||
|
|||||||
+4
-1
@@ -312,6 +312,9 @@ Fenrir automatically detects and provides audio feedback for progress indicators
|
|||||||
- **Automatic**: Works with downloads, compilations, installations
|
- **Automatic**: Works with downloads, compilations, installations
|
||||||
- **Remote control**: Enable via socket commands
|
- **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
|
### Spell Checking
|
||||||
- `Fenrir + S` - Spell check current word
|
- `Fenrir + S` - Spell check current word
|
||||||
- `Fenrir + S S` - Add word to dictionary
|
- `Fenrir + S S` - Add word to dictionary
|
||||||
@@ -428,7 +431,7 @@ For a dedicated PTY/terminal screen reader, see TDSR: https://github.com/tspivey
|
|||||||
### Debug Mode
|
### Debug Mode
|
||||||
```bash
|
```bash
|
||||||
sudo fenrir -f -d
|
sudo fenrir -f -d
|
||||||
# Debug output goes to /var/log/fenrir.log
|
# Debug output goes to /tmp/fenrir.log
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting Help
|
## Getting Help
|
||||||
|
|||||||
+27
-26
@@ -857,21 +857,21 @@ Values: ''0.0'' is quietest, ''1.0'' is loudest.
|
|||||||
=== Generic Driver ===
|
=== Generic Driver ===
|
||||||
The generic sound driver uses shell commands for play sound and frequencies.
|
The generic sound driver uses shell commands for play sound and frequencies.
|
||||||
|
|
||||||
''genericPlayFileCommand'' defines the command that is used to play a sound file.
|
''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>
|
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>
|
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
|
* ''fenrir_volume'' = the current volume setting
|
||||||
* ''fenrirSoundFile'' = the sound file for an sound icon
|
* ''fenrir_sound_file'' = the sound file for an sound icon
|
||||||
* ''fenrirFrequence'' = the frequency to play
|
* ''fenrir_frequency'' = the frequency to play
|
||||||
* ''fenrirDuration'' = the duration of the frequency
|
* ''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 ====
|
==== Speech ====
|
||||||
Speech is configured in section ''[speech]''.
|
Speech is configured in section ''[speech]''.
|
||||||
Turn speech on or off:
|
Turn speech on or off:
|
||||||
@@ -945,22 +945,22 @@ Values: on=''True'', off=''False''
|
|||||||
=== Generic Driver ===
|
=== Generic Driver ===
|
||||||
The generic speech driver uses shell commands for speech synthisus.
|
The generic speech driver uses shell commands for speech synthisus.
|
||||||
|
|
||||||
''genericSpeechCommand'' defines the command that is executed for creating speech
|
''generic_speech_command'' defines the command that is executed for creating speech
|
||||||
The following variables are substituted in ''genericSpeechCommand'':
|
The following variables are substituted in ''generic_speech_command'':
|
||||||
* ''FenrirText'' = is the text that should be spoken
|
* ''fenrir_text'' = is the text that should be spoken
|
||||||
* ''fenrirModule'' = may be the speech module like used in speech-dispatcher, not every TTY needs this
|
* ''fenrir_module'' = may be the speech module like used in speech-dispatcher, not every TTY needs this
|
||||||
* ''fenrirLanguage'' = the language to speak in
|
* ''fenrir_language'' = the language to speak in
|
||||||
* ''fenrirVoice'' = is the current voice that should be used
|
* ''fenrir_voice'' = is the current voice that should be used
|
||||||
* ''fenrirVolume'' = is replaced with the current volume
|
* ''fenrir_volume'' = is replaced with the current volume
|
||||||
* ''fenrirPitch'' = is replaced with the current pitch
|
* ''fenrir_pitch'' = is replaced with the current pitch
|
||||||
* ''fenrirRate'' = is replaced with the current speed (speech rate)
|
* ''fenrir_rate'' = is replaced with the current speed (speech rate)
|
||||||
|
|
||||||
Example genericSpeechCommand (default):
|
Example generic_speech_command (default):
|
||||||
generic_speech_command=espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice "fenrirText"
|
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_max_volume=200
|
||||||
fenrir_min_pitch=0
|
fenrir_min_pitch=0
|
||||||
fenrir_max_pitch=99
|
fenrir_max_pitch=99
|
||||||
@@ -1315,10 +1315,11 @@ Please report Bugs and feature requests to:
|
|||||||
|
|
||||||
for bugs please provide a [[#Howto create a debug file|debug]] file that shows the issue.
|
for bugs please provide a [[#Howto create a debug file|debug]] file that shows the issue.
|
||||||
==== How-to create a debug file ====
|
==== How-to create a debug file ====
|
||||||
- Delete old debug stuff\\ ''sudo rm /var/log/fenrir.log''
|
|
||||||
- Start fenrir in debug mode\\ ''sudo fenrir -d''
|
- Start fenrir in debug mode\\ ''sudo fenrir -d''
|
||||||
- Do your stuff to reproduce the problem
|
- Do your stuff to reproduce the problem
|
||||||
- Stop fenrir (''fenrirKey + q'')
|
- 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.
|
Please be as precise as possible to make it easy to solve the problem.
|
||||||
|
|||||||
@@ -112,9 +112,9 @@ class command:
|
|||||||
"review", "end_of_screen"
|
"review", "end_of_screen"
|
||||||
):
|
):
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
_("end of screen"),
|
_("start of screen"),
|
||||||
interrupt=True,
|
interrupt=True,
|
||||||
sound_icon="EndOfScreen",
|
sound_icon="StartOfScreen",
|
||||||
)
|
)
|
||||||
if line_break:
|
if line_break:
|
||||||
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ class command:
|
|||||||
"review", "end_of_screen"
|
"review", "end_of_screen"
|
||||||
):
|
):
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
_("end of screen"),
|
_("start of screen"),
|
||||||
interrupt=True,
|
interrupt=True,
|
||||||
sound_icon="EndOfScreen",
|
sound_icon="StartOfScreen",
|
||||||
)
|
)
|
||||||
if line_break:
|
if line_break:
|
||||||
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ class command:
|
|||||||
"review", "end_of_screen"
|
"review", "end_of_screen"
|
||||||
):
|
):
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
_("end of screen"),
|
_("start of screen"),
|
||||||
interrupt=True,
|
interrupt=True,
|
||||||
sound_icon="EndOfScreen",
|
sound_icon="StartOfScreen",
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_callback(self, callback):
|
def set_callback(self, callback):
|
||||||
|
|||||||
@@ -95,9 +95,9 @@ class command:
|
|||||||
"review", "end_of_screen"
|
"review", "end_of_screen"
|
||||||
):
|
):
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
_("end of screen"),
|
_("start of screen"),
|
||||||
interrupt=False,
|
interrupt=False,
|
||||||
sound_icon="EndOfScreen",
|
sound_icon="StartOfScreen",
|
||||||
)
|
)
|
||||||
if line_break:
|
if line_break:
|
||||||
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ class command:
|
|||||||
"review", "end_of_screen"
|
"review", "end_of_screen"
|
||||||
):
|
):
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
_("end of screen"),
|
_("start of screen"),
|
||||||
interrupt=True,
|
interrupt=True,
|
||||||
sound_icon="EndOfScreen",
|
sound_icon="StartOfScreen",
|
||||||
)
|
)
|
||||||
if line_break:
|
if line_break:
|
||||||
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ class command:
|
|||||||
"review", "end_of_screen"
|
"review", "end_of_screen"
|
||||||
):
|
):
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
_("end of screen"),
|
_("start of screen"),
|
||||||
interrupt=True,
|
interrupt=True,
|
||||||
sound_icon="EndOfScreen",
|
sound_icon="StartOfScreen",
|
||||||
)
|
)
|
||||||
if line_break:
|
if line_break:
|
||||||
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ class command:
|
|||||||
delta_length = len(delta_text)
|
delta_length = len(delta_text)
|
||||||
if (
|
if (
|
||||||
delta_length > 200
|
delta_length > 200
|
||||||
): # Allow longer progress lines like Claude Code's status
|
): # Allow longer progress lines such as terminal status output
|
||||||
|
if not self.is_explicit_progress_delta(delta_text):
|
||||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
f"Progress filter: delta too long ({delta_length})",
|
f"Progress filter: delta too long ({delta_length})",
|
||||||
debug.DebugLevel.INFO,
|
debug.DebugLevel.INFO,
|
||||||
@@ -107,6 +108,25 @@ class command:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
has_percentage = re.search(r"(^|\s)\d+(?:\.\d+)?\s*%", text)
|
||||||
|
if not has_percentage:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return bool(
|
||||||
|
re.search(
|
||||||
|
r"[|\[\]#=*>█▉▊▋▌▍▎▏▒▓░]"
|
||||||
|
r"|\b\d+(?:\.\d+)?\s*[kKmMgGtT](?:i?B)?/s\b",
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def reset_progress_state(self):
|
def reset_progress_state(self):
|
||||||
"""Reset progress state when a prompt is detected, allowing new progress operations to start fresh"""
|
"""Reset progress state when a prompt is detected, allowing new progress operations to start fresh"""
|
||||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
@@ -306,43 +326,27 @@ class command:
|
|||||||
self.env["commandBuffer"]["lastProgressTime"] = current_time
|
self.env["commandBuffer"]["lastProgressTime"] = current_time
|
||||||
return
|
return
|
||||||
|
|
||||||
# Pattern 6: Claude Code working indicators (various symbols + activity text + "esc/ctrl+c to interrupt")
|
# Pattern 6: Interruptible terminal activity indicators
|
||||||
# Matches any: [symbol] [Task description]… (... to interrupt ...)
|
# Matches any: [symbol] [Task description][…] (... to interrupt ...)
|
||||||
# Symbols include: * ✢ ✽ ✶ ✻ · • ◦ ○ ● ◆ and similar decorative characters
|
# Symbols include: * ✢ ✽ ✶ ✻ · • ◦ ○ ● ◆ and similar decorative characters
|
||||||
# Example: ✽ Reviewing script for issues… (ctrl+c to interrupt · 33s · ↑ 1.6k tokens · thought for 4s)
|
# Keep this structural rather than adding application-specific formats,
|
||||||
claude_progress_match = re.search(
|
# which change too frequently to support reliably.
|
||||||
r'[*✢✽✶✻·•◦○●◆]\s+\w+.*?…\s*\(.*(?:esc|ctrl\+c) to interrupt.*\)',
|
interruptible_activity_match = re.search(
|
||||||
|
r'[*✢✽✶✻·•◦○●◆]\s+\w+.*?(?:…\s*)?\(.*(?:esc|ctrl\+c) to interrupt.*\)',
|
||||||
text,
|
text,
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
if claude_progress_match:
|
if interruptible_activity_match:
|
||||||
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
|
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
|
||||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
"Playing Claude Code activity beep",
|
"Playing interruptible activity beep",
|
||||||
debug.DebugLevel.INFO,
|
debug.DebugLevel.INFO,
|
||||||
)
|
)
|
||||||
self.play_activity_beep()
|
self.play_activity_beep()
|
||||||
self.env["commandBuffer"]["lastProgressTime"] = current_time
|
self.env["commandBuffer"]["lastProgressTime"] = current_time
|
||||||
return
|
return
|
||||||
|
|
||||||
# Pattern 6b: Claude Code tool invocation indicators (● Tool Name(...))
|
# Pattern 6b: Bullet/white bullet activity lines (•/◦ ...)
|
||||||
# Example: ● Web Search("query here")
|
|
||||||
tool_invocation_match = re.search(
|
|
||||||
r'[●○◉•◦]\s+(?:Web\s*Search|Read|Write|Edit|Bash|Glob|Grep|Task|WebFetch)\s*\(',
|
|
||||||
text,
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
if tool_invocation_match:
|
|
||||||
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
|
|
||||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
||||||
"Playing Claude Code tool invocation beep",
|
|
||||||
debug.DebugLevel.INFO,
|
|
||||||
)
|
|
||||||
self.play_activity_beep()
|
|
||||||
self.env["commandBuffer"]["lastProgressTime"] = current_time
|
|
||||||
return
|
|
||||||
|
|
||||||
# Pattern 6c: Bullet/white bullet activity lines (•/◦ ...)
|
|
||||||
bullet_activity_match = re.search(
|
bullet_activity_match = re.search(
|
||||||
(
|
(
|
||||||
r'^\s*[•◦]\s+.*(?:…|\.{3,}|\b(?:thinking|working|processing|'
|
r'^\s*[•◦]\s+.*(?:…|\.{3,}|\b(?:thinking|working|processing|'
|
||||||
|
|||||||
@@ -22,4 +22,7 @@ command_info = {
|
|||||||
# 'curr_command': '',
|
# 'curr_command': '',
|
||||||
"lastCommandExecutionTime": time.time(),
|
"lastCommandExecutionTime": time.time(),
|
||||||
"lastCommandRequestTime": time.time(),
|
"lastCommandRequestTime": time.time(),
|
||||||
|
"lastCommand": "",
|
||||||
|
"lastCommandSection": "",
|
||||||
|
"lastCommandRunTime": 0,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -474,6 +474,10 @@ class CommandManager:
|
|||||||
def run_command(self, command, section="commands"):
|
def run_command(self, command, section="commands"):
|
||||||
if self.command_exists(command, section):
|
if self.command_exists(command, section):
|
||||||
try:
|
try:
|
||||||
|
command_time = time.time()
|
||||||
|
self.env["commandInfo"]["lastCommand"] = command
|
||||||
|
self.env["commandInfo"]["lastCommandSection"] = section
|
||||||
|
self.env["commandInfo"]["lastCommandRunTime"] = command_time
|
||||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
"run_command command:" + section + "." + command,
|
"run_command command:" + section + "." + command,
|
||||||
debug.DebugLevel.INFO,
|
debug.DebugLevel.INFO,
|
||||||
|
|||||||
@@ -3,24 +3,22 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import fcntl
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from fenrirscreenreader.core import debug
|
from fenrirscreenreader.core import debug
|
||||||
|
|
||||||
|
|
||||||
class DebugManager:
|
class DebugManager:
|
||||||
|
DEFAULT_LOG_DIR = "/tmp"
|
||||||
|
DEFAULT_LOG_BASENAME = "fenrir"
|
||||||
|
DEFAULT_LOG_EXTENSION = ".log"
|
||||||
|
|
||||||
def __init__(self, file_name=""):
|
def __init__(self, file_name=""):
|
||||||
self._file = None
|
self._file = None
|
||||||
self._fileOpened = False
|
self._fileOpened = False
|
||||||
self._fileName = (
|
|
||||||
"/tmp/fenrir_"
|
|
||||||
+ str(os.getpid())
|
|
||||||
+ "_"
|
|
||||||
+ str(datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S"))
|
|
||||||
+ ".log"
|
|
||||||
)
|
|
||||||
if file_name != "":
|
|
||||||
self._fileName = file_name
|
self._fileName = file_name
|
||||||
|
self._useDefaultLogName = file_name == ""
|
||||||
|
|
||||||
def initialize(self, environment):
|
def initialize(self, environment):
|
||||||
self.env = environment
|
self.env = environment
|
||||||
@@ -39,6 +37,10 @@ class DebugManager:
|
|||||||
self._fileOpened = False
|
self._fileOpened = False
|
||||||
if file_name != "":
|
if file_name != "":
|
||||||
self._fileName = file_name
|
self._fileName = file_name
|
||||||
|
self._useDefaultLogName = False
|
||||||
|
if self._useDefaultLogName:
|
||||||
|
self._open_default_debug_file()
|
||||||
|
return
|
||||||
if self._fileName != "":
|
if self._fileName != "":
|
||||||
directory = os.path.dirname(self._fileName)
|
directory = os.path.dirname(self._fileName)
|
||||||
if not os.path.exists(directory):
|
if not os.path.exists(directory):
|
||||||
@@ -51,6 +53,43 @@ class DebugManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
def _open_default_debug_file(self):
|
||||||
|
pathlib.Path(self.DEFAULT_LOG_DIR).mkdir(parents=True, exist_ok=True)
|
||||||
|
log_number = 1
|
||||||
|
while True:
|
||||||
|
log_file = self._default_log_file_name(log_number)
|
||||||
|
try:
|
||||||
|
fd = os.open(
|
||||||
|
log_file,
|
||||||
|
os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW,
|
||||||
|
0o644,
|
||||||
|
)
|
||||||
|
file_obj = os.fdopen(fd, "a")
|
||||||
|
fcntl.flock(file_obj.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
file_obj.seek(0)
|
||||||
|
file_obj.truncate()
|
||||||
|
os.chmod(log_file, 0o644)
|
||||||
|
self._file = file_obj
|
||||||
|
self._fileName = log_file
|
||||||
|
self._fileOpened = True
|
||||||
|
return
|
||||||
|
except BlockingIOError:
|
||||||
|
try:
|
||||||
|
file_obj.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
log_number += 1
|
||||||
|
except OSError as e:
|
||||||
|
print(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _default_log_file_name(self, log_number):
|
||||||
|
suffix = "" if log_number == 1 else str(log_number)
|
||||||
|
return os.path.join(
|
||||||
|
self.DEFAULT_LOG_DIR,
|
||||||
|
self.DEFAULT_LOG_BASENAME + suffix + self.DEFAULT_LOG_EXTENSION,
|
||||||
|
)
|
||||||
|
|
||||||
def write_debug_out(
|
def write_debug_out(
|
||||||
self, text, level=debug.DebugLevel.DEACTIVE, on_any_level=False
|
self, text, level=debug.DebugLevel.DEACTIVE, on_any_level=False
|
||||||
):
|
):
|
||||||
@@ -120,3 +159,4 @@ class DebugManager:
|
|||||||
def set_debug_file(self, file_name):
|
def set_debug_file(self, file_name):
|
||||||
self.close_debug_file()
|
self.close_debug_file()
|
||||||
self._fileName = file_name
|
self._fileName = file_name
|
||||||
|
self._useDefaultLogName = file_name == ""
|
||||||
|
|||||||
@@ -311,7 +311,10 @@ class FenrirManager:
|
|||||||
self.singleKeyCommand = True
|
self.singleKeyCommand = True
|
||||||
elif (
|
elif (
|
||||||
(
|
(
|
||||||
self.environment["runtime"]["DiffReviewManager"].is_active()
|
self.environment["runtime"]["VmenuManager"].get_active()
|
||||||
|
or self.environment["runtime"][
|
||||||
|
"DiffReviewManager"
|
||||||
|
].is_active()
|
||||||
or self.environment["runtime"][
|
or self.environment["runtime"][
|
||||||
"SpeechHistoryManager"
|
"SpeechHistoryManager"
|
||||||
].is_active()
|
].is_active()
|
||||||
|
|||||||
@@ -12,8 +12,13 @@ settings_data = {
|
|||||||
"driver": "genericDriver",
|
"driver": "genericDriver",
|
||||||
"theme": "default",
|
"theme": "default",
|
||||||
"volume": 1.0,
|
"volume": 1.0,
|
||||||
"generic_play_file_command": "play -q -v fenrirVolume fenrirSoundFile",
|
"generic_play_file_command": (
|
||||||
"generic_frequency_command": "play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence",
|
"play -q -v fenrir_volume fenrir_sound_file"
|
||||||
|
),
|
||||||
|
"generic_frequency_command": (
|
||||||
|
"play -q -v fenrir_volume -n -c1 synth "
|
||||||
|
"fenrir_duration sine fenrir_frequency"
|
||||||
|
),
|
||||||
"progress_monitoring": True,
|
"progress_monitoring": True,
|
||||||
},
|
},
|
||||||
"speech": {
|
"speech": {
|
||||||
@@ -38,7 +43,10 @@ settings_data = {
|
|||||||
"batch_flush_interval": 0.5,
|
"batch_flush_interval": 0.5,
|
||||||
"max_batch_lines": 100,
|
"max_batch_lines": 100,
|
||||||
"flood_line_threshold": 500,
|
"flood_line_threshold": 500,
|
||||||
"generic_speech_command": 'espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice "fenrirText"',
|
"generic_speech_command": (
|
||||||
|
"espeak -a fenrir_volume -s fenrir_rate -p fenrir_pitch "
|
||||||
|
'-v fenrir_voice "fenrir_text"'
|
||||||
|
),
|
||||||
"fenrir_min_volume": 0,
|
"fenrir_min_volume": 0,
|
||||||
"fenrir_max_volume": 200,
|
"fenrir_max_volume": 200,
|
||||||
"fenrir_min_pitch": 0,
|
"fenrir_min_pitch": 0,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
from argparse import Namespace
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
|
||||||
from fenrirscreenreader.core import applicationManager
|
from fenrirscreenreader.core import applicationManager
|
||||||
@@ -67,6 +68,15 @@ class SettingsManager:
|
|||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def format_cli_args(self, cliArgs):
|
||||||
|
if cliArgs is None:
|
||||||
|
return "{}"
|
||||||
|
if isinstance(cliArgs, Namespace):
|
||||||
|
args = vars(cliArgs)
|
||||||
|
else:
|
||||||
|
args = vars(cliArgs) if hasattr(cliArgs, "__dict__") else {}
|
||||||
|
return str({key: args[key] for key in sorted(args)})
|
||||||
|
|
||||||
def get_binding_backup(self):
|
def get_binding_backup(self):
|
||||||
return self.bindingsBackup.copy()
|
return self.bindingsBackup.copy()
|
||||||
|
|
||||||
@@ -644,6 +654,11 @@ class SettingsManager:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
environment["runtime"]["DebugManager"].initialize(environment)
|
environment["runtime"]["DebugManager"].initialize(environment)
|
||||||
|
environment["runtime"]["DebugManager"].write_debug_out(
|
||||||
|
"Fenrir startup CLI arguments: " + self.format_cli_args(cliArgs),
|
||||||
|
debug.DebugLevel.INFO,
|
||||||
|
on_any_level=True,
|
||||||
|
)
|
||||||
|
|
||||||
if cliArgs.force_all_screens:
|
if cliArgs.force_all_screens:
|
||||||
environment["runtime"]["force_all_screens"] = True
|
environment["runtime"]["force_all_screens"] = True
|
||||||
|
|||||||
@@ -132,13 +132,6 @@ class TabCompletionManager:
|
|||||||
if candidate_text:
|
if candidate_text:
|
||||||
return self._clean_text(candidate_text)
|
return self._clean_text(candidate_text)
|
||||||
|
|
||||||
delta_text = self.env["screen"]["new_delta"]
|
|
||||||
if (
|
|
||||||
delta_text
|
|
||||||
and not self.env["screen"].get("new_delta_is_typing", False)
|
|
||||||
):
|
|
||||||
return self._clean_text(delta_text)
|
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _get_cursor_line_inserted_text(
|
def _get_cursor_line_inserted_text(
|
||||||
@@ -184,26 +177,19 @@ class TabCompletionManager:
|
|||||||
return "".join(inserted_parts)
|
return "".join(inserted_parts)
|
||||||
|
|
||||||
def _get_candidate_text(self, old_lines, new_lines, cursor_y):
|
def _get_candidate_text(self, old_lines, new_lines, cursor_y):
|
||||||
if len(old_lines) != len(new_lines):
|
|
||||||
return self._get_inserted_lines(old_lines, new_lines, cursor_y)
|
|
||||||
|
|
||||||
changed_lines = []
|
|
||||||
old_cursor_line = (
|
old_cursor_line = (
|
||||||
old_lines[cursor_y].strip() if cursor_y < len(old_lines) else ""
|
old_lines[cursor_y].strip() if cursor_y < len(old_lines) else ""
|
||||||
)
|
)
|
||||||
for index, old_line in enumerate(old_lines):
|
return self._get_inserted_lines(
|
||||||
if index == cursor_y:
|
old_lines,
|
||||||
continue
|
new_lines,
|
||||||
if index < len(new_lines) and old_line != new_lines[index]:
|
self.env["screen"]["new_cursor"]["y"],
|
||||||
if new_lines[index].strip() == old_cursor_line:
|
old_cursor_line,
|
||||||
continue
|
|
||||||
changed_lines.append(new_lines[index])
|
|
||||||
|
|
||||||
return "\n".join(
|
|
||||||
line.rstrip() for line in changed_lines if line.strip()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_inserted_lines(self, old_lines, new_lines, cursor_y):
|
def _get_inserted_lines(
|
||||||
|
self, old_lines, new_lines, new_cursor_y, old_cursor_line
|
||||||
|
):
|
||||||
matcher = difflib.SequenceMatcher(
|
matcher = difflib.SequenceMatcher(
|
||||||
None, old_lines, new_lines, autojunk=False
|
None, old_lines, new_lines, autojunk=False
|
||||||
)
|
)
|
||||||
@@ -217,10 +203,15 @@ class TabCompletionManager:
|
|||||||
) in matcher.get_opcodes():
|
) in matcher.get_opcodes():
|
||||||
if tag not in ["insert", "replace"]:
|
if tag not in ["insert", "replace"]:
|
||||||
continue
|
continue
|
||||||
if new_end <= cursor_y:
|
if new_start > new_cursor_y:
|
||||||
|
continue
|
||||||
|
if tag == "replace" and any(
|
||||||
|
line.strip() for line in old_lines[old_start:old_end]
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
for line in new_lines[new_start:new_end]:
|
for line in new_lines[new_start:new_end]:
|
||||||
if line.strip():
|
stripped_line = line.strip()
|
||||||
|
if stripped_line and stripped_line != old_cursor_line:
|
||||||
inserted_lines.append(line.rstrip())
|
inserted_lines.append(line.rstrip())
|
||||||
return "\n".join(inserted_lines)
|
return "\n".join(inserted_lines)
|
||||||
|
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
# Fenrir TTY screen reader
|
# Fenrir TTY screen reader
|
||||||
# By Chrys, Storm Dragon, and contributors.
|
# By Chrys, Storm Dragon, and contributors.
|
||||||
|
|
||||||
version = "2026.05.23"
|
version = "2026.06.18"
|
||||||
code_name = "testing"
|
code_name = "testing"
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ class driver(inputDriver):
|
|||||||
self.fenrir_keys = set()
|
self.fenrir_keys = set()
|
||||||
self.failed_grabs = 0
|
self.failed_grabs = 0
|
||||||
self.modifier_state = 0
|
self.modifier_state = 0
|
||||||
|
self.modifier_interrupt_state = 0
|
||||||
|
|
||||||
def initialize(self, environment):
|
def initialize(self, environment):
|
||||||
self.env = environment
|
self.env = environment
|
||||||
@@ -194,6 +195,7 @@ class driver(inputDriver):
|
|||||||
)
|
)
|
||||||
self.num_lock_mask = self.find_num_lock_mask()
|
self.num_lock_mask = self.find_num_lock_mask()
|
||||||
self.refresh_modifier_state()
|
self.refresh_modifier_state()
|
||||||
|
self.modifier_interrupt_state = self.modifier_state
|
||||||
self.refresh_interesting_keys()
|
self.refresh_interesting_keys()
|
||||||
self.refresh_grabs(force=True)
|
self.refresh_grabs(force=True)
|
||||||
self.env["runtime"]["ProcessManager"].add_custom_event_thread(
|
self.env["runtime"]["ProcessManager"].add_custom_event_thread(
|
||||||
@@ -274,6 +276,7 @@ class driver(inputDriver):
|
|||||||
while active.value:
|
while active.value:
|
||||||
try:
|
try:
|
||||||
self.refresh_grabs()
|
self.refresh_grabs()
|
||||||
|
self.poll_modifier_interrupt_keys()
|
||||||
if not self.display.pending_events():
|
if not self.display.pending_events():
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
continue
|
continue
|
||||||
@@ -371,6 +374,56 @@ class driver(inputDriver):
|
|||||||
"event_x_time": getattr(event, "time", X.CurrentTime),
|
"event_x_time": getattr(event, "time", X.CurrentTime),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def poll_modifier_interrupt_keys(self):
|
||||||
|
if not self.active or not self.should_poll_modifier_interrupt_keys():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
pointer = self.root.query_pointer()
|
||||||
|
current_state = getattr(pointer, "mask", 0)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
previous_state = self.modifier_interrupt_state
|
||||||
|
self.modifier_interrupt_state = current_state
|
||||||
|
self.modifier_state = current_state
|
||||||
|
for key_name, modifier_mask in self.interrupt_modifier_masks():
|
||||||
|
if current_state & modifier_mask and not previous_state & modifier_mask:
|
||||||
|
self.interrupt_output_on_modifier_key(key_name)
|
||||||
|
|
||||||
|
def should_poll_modifier_interrupt_keys(self):
|
||||||
|
try:
|
||||||
|
settings_manager = self.env["runtime"]["SettingsManager"]
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
if not settings_manager.get_setting_as_bool(
|
||||||
|
"keyboard", "interrupt_on_key_press"
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
return (
|
||||||
|
settings_manager.get_setting(
|
||||||
|
"keyboard", "interrupt_on_key_press_filter"
|
||||||
|
).strip()
|
||||||
|
== ""
|
||||||
|
)
|
||||||
|
|
||||||
|
def interrupt_modifier_masks(self):
|
||||||
|
return [
|
||||||
|
("KEY_CTRL", X.ControlMask),
|
||||||
|
("KEY_SHIFT", X.ShiftMask),
|
||||||
|
("KEY_ALT", X.Mod1Mask),
|
||||||
|
]
|
||||||
|
|
||||||
|
def interrupt_output_on_modifier_key(self, key_name):
|
||||||
|
try:
|
||||||
|
self.env["runtime"]["OutputManager"].interrupt_output_async()
|
||||||
|
except Exception as e:
|
||||||
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
|
"x11Driver modifier interrupt failed for "
|
||||||
|
+ key_name
|
||||||
|
+ ": "
|
||||||
|
+ str(e),
|
||||||
|
debug.DebugLevel.ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
def refresh_modifier_state(self):
|
def refresh_modifier_state(self):
|
||||||
try:
|
try:
|
||||||
pointer = self.root.query_pointer()
|
pointer = self.root.query_pointer()
|
||||||
|
|||||||
@@ -304,6 +304,11 @@ class driver(screenDriver):
|
|||||||
"keyboard", "interrupt_on_key_press_filter"
|
"keyboard", "interrupt_on_key_press_filter"
|
||||||
).strip():
|
).strip():
|
||||||
return
|
return
|
||||||
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
|
"ptyDriver interrupt_output_on_stdin_input: "
|
||||||
|
+ repr(msg_bytes),
|
||||||
|
debug.DebugLevel.INFO,
|
||||||
|
)
|
||||||
self.start_stdin_interrupt_thread()
|
self.start_stdin_interrupt_thread()
|
||||||
|
|
||||||
def start_stdin_interrupt_thread(self):
|
def start_stdin_interrupt_thread(self):
|
||||||
@@ -333,10 +338,112 @@ class driver(screenDriver):
|
|||||||
def handle_stdin_input(self, msg_bytes, event_queue):
|
def handle_stdin_input(self, msg_bytes, event_queue):
|
||||||
if self.synthesize_backspace_shortcut(msg_bytes, event_queue):
|
if self.synthesize_backspace_shortcut(msg_bytes, event_queue):
|
||||||
return
|
return
|
||||||
|
if self.handle_vmenu_stdin_input(msg_bytes, event_queue):
|
||||||
|
return
|
||||||
|
if self.suppress_recent_review_stdin_tail(msg_bytes):
|
||||||
|
return
|
||||||
self.record_stdin_keypress(msg_bytes)
|
self.record_stdin_keypress(msg_bytes)
|
||||||
self.interrupt_output_on_stdin_input(msg_bytes)
|
self.interrupt_output_on_stdin_input(msg_bytes)
|
||||||
self.inject_text_to_screen(msg_bytes)
|
self.inject_text_to_screen(msg_bytes)
|
||||||
|
|
||||||
|
def suppress_recent_review_stdin_tail(self, msg_bytes):
|
||||||
|
if not self.is_keyboard_control_sequence(msg_bytes):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
command_info = self.env["commandInfo"]
|
||||||
|
last_command = command_info.get("lastCommand", "")
|
||||||
|
last_section = command_info.get("lastCommandSection", "")
|
||||||
|
last_run_time = command_info.get("lastCommandRunTime", 0)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
if last_section != "commands" or not last_command.startswith("REVIEW_"):
|
||||||
|
return False
|
||||||
|
if time.time() - last_run_time > 0.8:
|
||||||
|
return False
|
||||||
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
|
"ptyDriver suppressing recent review stdin tail: "
|
||||||
|
+ repr(msg_bytes),
|
||||||
|
debug.DebugLevel.INFO,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_keyboard_control_sequence(self, msg_bytes):
|
||||||
|
if not msg_bytes:
|
||||||
|
return False
|
||||||
|
if msg_bytes.startswith(b"\x1b"):
|
||||||
|
return True
|
||||||
|
if len(msg_bytes) == 1:
|
||||||
|
value = msg_bytes[0]
|
||||||
|
return value < 32 and msg_bytes not in [b"\t", b"\n", b"\r"]
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle_vmenu_stdin_input(self, msg_bytes, event_queue):
|
||||||
|
if not self.is_vmenu_active():
|
||||||
|
return False
|
||||||
|
key_name = self.vmenu_stdin_key_name(msg_bytes)
|
||||||
|
if key_name and not self.vmenu_key_already_handled(key_name):
|
||||||
|
self.queue_keypress(key_name, event_queue)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_vmenu_active(self):
|
||||||
|
try:
|
||||||
|
return self.env["runtime"]["VmenuManager"].get_active()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def vmenu_stdin_key_name(self, msg_bytes):
|
||||||
|
key_map = {
|
||||||
|
b"\x1b": "KEY_ESC",
|
||||||
|
b"\x1b[A": "KEY_UP",
|
||||||
|
b"\x1b[B": "KEY_DOWN",
|
||||||
|
b"\x1b[C": "KEY_RIGHT",
|
||||||
|
b"\x1b[D": "KEY_LEFT",
|
||||||
|
b"\x1b[5~": "KEY_PAGEUP",
|
||||||
|
b"\x1b[6~": "KEY_PAGEDOWN",
|
||||||
|
b"\r": "KEY_ENTER",
|
||||||
|
b"\n": "KEY_ENTER",
|
||||||
|
b" ": "KEY_SPACE",
|
||||||
|
}
|
||||||
|
if msg_bytes in key_map:
|
||||||
|
return key_map[msg_bytes]
|
||||||
|
if len(msg_bytes) != 1:
|
||||||
|
return None
|
||||||
|
char = chr(msg_bytes[0])
|
||||||
|
if "a" <= char <= "z" or "A" <= char <= "Z":
|
||||||
|
return "KEY_" + char.upper()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def vmenu_key_already_handled(self, key_name):
|
||||||
|
try:
|
||||||
|
return key_name in self.env["input"]["curr_input"]
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def queue_keypress(self, key_name, event_queue):
|
||||||
|
event_time = time.time()
|
||||||
|
for event_state in [1, 0]:
|
||||||
|
try:
|
||||||
|
event_queue.put(
|
||||||
|
{
|
||||||
|
"Type": FenrirEventType.keyboard_input,
|
||||||
|
"data": {
|
||||||
|
"event_name": key_name,
|
||||||
|
"event_value": 0,
|
||||||
|
"event_sec": int(event_time),
|
||||||
|
"event_usec": int((event_time % 1) * 1000000),
|
||||||
|
"event_state": event_state,
|
||||||
|
"event_type": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
block=False,
|
||||||
|
)
|
||||||
|
except Full:
|
||||||
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
|
"ptyDriver queue_keypress: Event queue full, dropping "
|
||||||
|
+ key_name,
|
||||||
|
debug.DebugLevel.WARNING,
|
||||||
|
)
|
||||||
|
|
||||||
def record_stdin_keypress(self, msg_bytes):
|
def record_stdin_keypress(self, msg_bytes):
|
||||||
if msg_bytes != b"\t":
|
if msg_bytes != b"\t":
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ class driver(sound_driver):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
proc: Currently running subprocess for sound playback
|
proc: Currently running subprocess for sound playback
|
||||||
soundFileCommand (str): Command template for playing sound files
|
sound_file_command (str): Command template for playing sound files
|
||||||
frequenceCommand (str): Command template for generating frequencies
|
frequency_command (str): Command template for generating frequencies
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
sound_driver.__init__(self)
|
sound_driver.__init__(self)
|
||||||
self.proc = None
|
self.proc = None
|
||||||
self.soundType = ""
|
self.sound_type = ""
|
||||||
self.soundFileCommand = ""
|
self.sound_file_command = ""
|
||||||
self.frequenceCommand = ""
|
self.frequency_command = ""
|
||||||
|
|
||||||
def initialize(self, environment):
|
def initialize(self, environment):
|
||||||
"""Initialize the generic sound driver.
|
"""Initialize the generic sound driver.
|
||||||
@@ -46,17 +46,20 @@ class driver(sound_driver):
|
|||||||
environment: Fenrir environment dictionary with settings
|
environment: Fenrir environment dictionary with settings
|
||||||
"""
|
"""
|
||||||
self.env = environment
|
self.env = environment
|
||||||
self.soundFileCommand = self.env["runtime"][
|
self.sound_file_command = self.env["runtime"][
|
||||||
"SettingsManager"
|
"SettingsManager"
|
||||||
].get_setting("sound", "generic_play_file_command")
|
].get_setting("sound", "generic_play_file_command")
|
||||||
self.frequenceCommand = self.env["runtime"][
|
self.frequency_command = self.env["runtime"][
|
||||||
"SettingsManager"
|
"SettingsManager"
|
||||||
].get_setting("sound", "generic_frequency_command")
|
].get_setting("sound", "generic_frequency_command")
|
||||||
if self.soundFileCommand == "":
|
if self.sound_file_command == "":
|
||||||
self.soundFileCommand = "play -q -v fenrirVolume fenrirSoundFile"
|
self.sound_file_command = (
|
||||||
if self.frequenceCommand == "":
|
"play -q -v fenrir_volume fenrir_sound_file"
|
||||||
self.frequenceCommand = (
|
)
|
||||||
"play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence"
|
if self.frequency_command == "":
|
||||||
|
self.frequency_command = (
|
||||||
|
"play -q -v fenrir_volume -n -c1 synth "
|
||||||
|
"fenrir_duration sine fenrir_frequency"
|
||||||
)
|
)
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
@@ -75,22 +78,22 @@ class driver(sound_driver):
|
|||||||
return
|
return
|
||||||
if interrupt:
|
if interrupt:
|
||||||
self.cancel()
|
self.cancel()
|
||||||
popen_frequence_command = shlex.split(self.frequenceCommand)
|
popen_frequency_command = shlex.split(self.frequency_command)
|
||||||
for idx, word in enumerate(popen_frequence_command):
|
for idx, word in enumerate(popen_frequency_command):
|
||||||
word = word.replace(
|
word = word.replace(
|
||||||
"fenrirVolume", str(self.volume * adjust_volume)
|
"fenrir_volume", str(self.volume * adjust_volume)
|
||||||
)
|
)
|
||||||
word = word.replace("fenrirDuration", str(duration))
|
word = word.replace("fenrir_duration", str(duration))
|
||||||
word = word.replace("fenrirFrequence", str(frequence))
|
word = word.replace("fenrir_frequency", str(frequence))
|
||||||
popen_frequence_command[idx] = word
|
popen_frequency_command[idx] = word
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
popen_frequence_command,
|
popen_frequency_command,
|
||||||
stdin=None,
|
stdin=None,
|
||||||
stdout=None,
|
stdout=None,
|
||||||
stderr=None,
|
stderr=None,
|
||||||
shell=False,
|
shell=False,
|
||||||
)
|
)
|
||||||
self.soundType = "frequence"
|
self.sound_type = "frequence"
|
||||||
|
|
||||||
def play_sound_file(self, file_path, interrupt=True):
|
def play_sound_file(self, file_path, interrupt=True):
|
||||||
"""Play a sound file.
|
"""Play a sound file.
|
||||||
@@ -108,13 +111,13 @@ class driver(sound_driver):
|
|||||||
|
|
||||||
if not os.path.isfile(file_path) or ".." in file_path:
|
if not os.path.isfile(file_path) or ".." in file_path:
|
||||||
return
|
return
|
||||||
popen_sound_file_command = shlex.split(self.soundFileCommand)
|
popen_sound_file_command = shlex.split(self.sound_file_command)
|
||||||
for idx, word in enumerate(popen_sound_file_command):
|
for idx, word in enumerate(popen_sound_file_command):
|
||||||
word = word.replace("fenrirVolume", str(self.volume))
|
word = word.replace("fenrir_volume", str(self.volume))
|
||||||
word = word.replace("fenrirSoundFile", shlex.quote(str(file_path)))
|
word = word.replace("fenrir_sound_file", str(file_path))
|
||||||
popen_sound_file_command[idx] = word
|
popen_sound_file_command[idx] = word
|
||||||
self.proc = subprocess.Popen(popen_sound_file_command, shell=False)
|
self.proc = subprocess.Popen(popen_sound_file_command, shell=False)
|
||||||
self.soundType = "file"
|
self.sound_type = "file"
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
"""Cancel currently playing sound.
|
"""Cancel currently playing sound.
|
||||||
@@ -123,9 +126,9 @@ class driver(sound_driver):
|
|||||||
"""
|
"""
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
return
|
return
|
||||||
if self.soundType == "":
|
if self.sound_type == "":
|
||||||
return
|
return
|
||||||
if self.soundType == "file":
|
if self.sound_type == "file":
|
||||||
self.proc.kill()
|
self.proc.kill()
|
||||||
try:
|
try:
|
||||||
# Wait for process to finish to prevent zombies
|
# Wait for process to finish to prevent zombies
|
||||||
@@ -134,7 +137,7 @@ class driver(sound_driver):
|
|||||||
pass # Process already terminated
|
pass # Process already terminated
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass # Handle any other wait errors
|
pass # Handle any other wait errors
|
||||||
if self.soundType == "frequence":
|
if self.sound_type == "frequence":
|
||||||
self.proc.kill()
|
self.proc.kill()
|
||||||
try:
|
try:
|
||||||
# Wait for process to finish to prevent zombies
|
# Wait for process to finish to prevent zombies
|
||||||
@@ -143,4 +146,4 @@ class driver(sound_driver):
|
|||||||
pass # Process already terminated
|
pass # Process already terminated
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass # Handle any other wait errors
|
pass # Handle any other wait errors
|
||||||
self.soundType = ""
|
self.sound_type = ""
|
||||||
|
|||||||
@@ -30,49 +30,52 @@ class driver(speech_driver):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
speech_driver.__init__(self)
|
speech_driver.__init__(self)
|
||||||
self.proc = None
|
self.proc = None
|
||||||
self.speechThread = Thread(target=self.worker)
|
self.speech_thread = Thread(target=self.worker)
|
||||||
self.lock = Lock()
|
self.lock = Lock()
|
||||||
self.textQueue = SpeakQueue()
|
self.text_queue = SpeakQueue()
|
||||||
|
|
||||||
def initialize(self, environment):
|
def initialize(self, environment):
|
||||||
self.env = environment
|
self.env = environment
|
||||||
self.minVolume = self.env["runtime"][
|
self.min_volume = self.env["runtime"][
|
||||||
"SettingsManager"
|
"SettingsManager"
|
||||||
].get_setting_as_int("speech", "fenrir_min_volume")
|
].get_setting_as_int("speech", "fenrir_min_volume")
|
||||||
self.maxVolume = self.env["runtime"][
|
self.max_volume = self.env["runtime"][
|
||||||
"SettingsManager"
|
"SettingsManager"
|
||||||
].get_setting_as_int("speech", "fenrir_max_volume")
|
].get_setting_as_int("speech", "fenrir_max_volume")
|
||||||
self.minPitch = self.env["runtime"][
|
self.min_pitch = self.env["runtime"][
|
||||||
"SettingsManager"
|
"SettingsManager"
|
||||||
].get_setting_as_int("speech", "fenrir_min_pitch")
|
].get_setting_as_int("speech", "fenrir_min_pitch")
|
||||||
self.maxPitch = self.env["runtime"][
|
self.max_pitch = self.env["runtime"][
|
||||||
"SettingsManager"
|
"SettingsManager"
|
||||||
].get_setting_as_int("speech", "fenrir_max_pitch")
|
].get_setting_as_int("speech", "fenrir_max_pitch")
|
||||||
self.minRate = self.env["runtime"][
|
self.min_rate = self.env["runtime"][
|
||||||
"SettingsManager"
|
"SettingsManager"
|
||||||
].get_setting_as_int("speech", "fenrir_min_rate")
|
].get_setting_as_int("speech", "fenrir_min_rate")
|
||||||
self.maxRate = self.env["runtime"][
|
self.max_rate = self.env["runtime"][
|
||||||
"SettingsManager"
|
"SettingsManager"
|
||||||
].get_setting_as_int("speech", "fenrir_max_rate")
|
].get_setting_as_int("speech", "fenrir_max_rate")
|
||||||
|
|
||||||
self.speechCommand = self.env["runtime"][
|
self.speech_command = self.env["runtime"][
|
||||||
"SettingsManager"
|
"SettingsManager"
|
||||||
].get_setting("speech", "generic_speech_command")
|
].get_setting("speech", "generic_speech_command")
|
||||||
if self.speechCommand == "":
|
if self.speech_command == "":
|
||||||
self.speechCommand = 'espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice -- "fenrirText"'
|
self.speech_command = (
|
||||||
|
"espeak -a fenrir_volume -s fenrir_rate -p fenrir_pitch "
|
||||||
|
'-v fenrir_voice -- "fenrir_text"'
|
||||||
|
)
|
||||||
if False: # for debugging overwrite here
|
if False: # for debugging overwrite here
|
||||||
# self.speechCommand = 'spd-say --wait -r 100 -i 100 "fenrirText"'
|
# self.speech_command = 'spd-say --wait -r 100 -i 100 "fenrir_text"'
|
||||||
self.speechCommand = 'flite -t "fenrirText"'
|
self.speech_command = 'flite -t "fenrir_text"'
|
||||||
|
|
||||||
self._is_initialized = True
|
self._is_initialized = True
|
||||||
if self._is_initialized:
|
if self._is_initialized:
|
||||||
self.speechThread.start()
|
self.speech_thread.start()
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
if not self._is_initialized:
|
if not self._is_initialized:
|
||||||
return
|
return
|
||||||
self.cancel()
|
self.cancel()
|
||||||
self.textQueue.put(-1)
|
self.text_queue.put(-1)
|
||||||
|
|
||||||
def speak(self, text, queueable=True, ignore_punctuation=False):
|
def speak(self, text, queueable=True, ignore_punctuation=False):
|
||||||
if not self._is_initialized:
|
if not self._is_initialized:
|
||||||
@@ -88,7 +91,7 @@ class driver(speech_driver):
|
|||||||
"language": self.language,
|
"language": self.language,
|
||||||
"voice": self.voice,
|
"voice": self.voice,
|
||||||
}
|
}
|
||||||
self.textQueue.put(utterance.copy())
|
self.text_queue.put(utterance.copy())
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
if not self._is_initialized:
|
if not self._is_initialized:
|
||||||
@@ -129,7 +132,7 @@ class driver(speech_driver):
|
|||||||
def clear_buffer(self):
|
def clear_buffer(self):
|
||||||
if not self._is_initialized:
|
if not self._is_initialized:
|
||||||
return
|
return
|
||||||
self.textQueue.clear()
|
self.text_queue.clear()
|
||||||
|
|
||||||
def set_voice(self, voice):
|
def set_voice(self, voice):
|
||||||
if not self._is_initialized:
|
if not self._is_initialized:
|
||||||
@@ -140,13 +143,13 @@ class driver(speech_driver):
|
|||||||
if not self._is_initialized:
|
if not self._is_initialized:
|
||||||
return
|
return
|
||||||
self.pitch = str(
|
self.pitch = str(
|
||||||
self.minPitch + pitch * (self.maxPitch - self.minPitch)
|
self.min_pitch + pitch * (self.max_pitch - self.min_pitch)
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_rate(self, rate):
|
def set_rate(self, rate):
|
||||||
if not self._is_initialized:
|
if not self._is_initialized:
|
||||||
return
|
return
|
||||||
self.rate = str(self.minRate + rate * (self.maxRate - self.minRate))
|
self.rate = str(self.min_rate + rate * (self.max_rate - self.min_rate))
|
||||||
|
|
||||||
def set_module(self, module):
|
def set_module(self, module):
|
||||||
if not self._is_initialized:
|
if not self._is_initialized:
|
||||||
@@ -162,12 +165,29 @@ class driver(speech_driver):
|
|||||||
if not self._is_initialized:
|
if not self._is_initialized:
|
||||||
return
|
return
|
||||||
self.volume = str(
|
self.volume = str(
|
||||||
self.minVolume + volume * (self.maxVolume - self.minVolume)
|
self.min_volume + volume * (self.max_volume - self.min_volume)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _build_speech_command(self, utterance):
|
||||||
|
replacements = {
|
||||||
|
"fenrir_volume": str(utterance["volume"]),
|
||||||
|
"fenrir_module": str(utterance["module"]),
|
||||||
|
"fenrir_language": str(utterance["language"]),
|
||||||
|
"fenrir_voice": str(utterance["voice"]),
|
||||||
|
"fenrir_pitch": str(utterance["pitch"]),
|
||||||
|
"fenrir_rate": str(utterance["rate"]),
|
||||||
|
"fenrir_text": shlex.quote(str(utterance["text"])),
|
||||||
|
}
|
||||||
|
speech_command = shlex.split(self.speech_command)
|
||||||
|
for idx, word in enumerate(speech_command):
|
||||||
|
for placeholder, value in replacements.items():
|
||||||
|
word = word.replace(placeholder, value)
|
||||||
|
speech_command[idx] = word
|
||||||
|
return speech_command
|
||||||
|
|
||||||
def worker(self):
|
def worker(self):
|
||||||
while True:
|
while True:
|
||||||
utterance = self.textQueue.get()
|
utterance = self.text_queue.get()
|
||||||
|
|
||||||
if isinstance(utterance, int):
|
if isinstance(utterance, int):
|
||||||
if utterance == -1:
|
if utterance == -1:
|
||||||
@@ -209,21 +229,7 @@ class driver(speech_driver):
|
|||||||
if not isinstance(utterance["rate"], str):
|
if not isinstance(utterance["rate"], str):
|
||||||
utterance["rate"] = ""
|
utterance["rate"] = ""
|
||||||
|
|
||||||
popen_speech_command = shlex.split(self.speechCommand)
|
popen_speech_command = self._build_speech_command(utterance)
|
||||||
for idx, word in enumerate(popen_speech_command):
|
|
||||||
word = word.replace("fenrirVolume", str(utterance["volume"]))
|
|
||||||
word = word.replace("fenrirModule", str(utterance["module"]))
|
|
||||||
word = word.replace(
|
|
||||||
"fenrirLanguage", str(utterance["language"])
|
|
||||||
)
|
|
||||||
word = word.replace("fenrirVoice", str(utterance["voice"]))
|
|
||||||
word = word.replace("fenrirPitch", str(utterance["pitch"]))
|
|
||||||
word = word.replace("fenrirRate", str(utterance["rate"]))
|
|
||||||
# Properly quote text to prevent command injection
|
|
||||||
word = word.replace(
|
|
||||||
"fenrirText", shlex.quote(str(utterance["text"]))
|
|
||||||
)
|
|
||||||
popen_speech_command[idx] = word
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ def get_up_char(curr_x, curr_y, curr_text):
|
|||||||
curr_y -= 1
|
curr_y -= 1
|
||||||
if curr_y < 0:
|
if curr_y < 0:
|
||||||
curr_y = 0
|
curr_y = 0
|
||||||
else:
|
|
||||||
end_of_screen = True
|
end_of_screen = True
|
||||||
curr_char = ""
|
curr_char = ""
|
||||||
if not end_of_screen:
|
if not end_of_screen:
|
||||||
@@ -63,7 +62,6 @@ def get_down_char(curr_x, curr_y, curr_text):
|
|||||||
curr_y += 1
|
curr_y += 1
|
||||||
if curr_y >= len(wrapped_lines):
|
if curr_y >= len(wrapped_lines):
|
||||||
curr_y = len(wrapped_lines) - 1
|
curr_y = len(wrapped_lines) - 1
|
||||||
else:
|
|
||||||
end_of_screen = True
|
end_of_screen = True
|
||||||
curr_char = ""
|
curr_char = ""
|
||||||
if not end_of_screen:
|
if not end_of_screen:
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
from fenrirscreenreader.core.debugManager import DebugManager
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_debug_file_uses_flat_name(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.setattr(DebugManager, "DEFAULT_LOG_DIR", str(tmp_path))
|
||||||
|
manager = DebugManager()
|
||||||
|
try:
|
||||||
|
manager.open_debug_file()
|
||||||
|
|
||||||
|
assert manager.get_debug_file() == str(tmp_path / "fenrir.log")
|
||||||
|
assert (tmp_path / "fenrir.log").exists()
|
||||||
|
finally:
|
||||||
|
manager.close_debug_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_debug_file_uses_next_number_when_locked(
|
||||||
|
tmp_path, monkeypatch
|
||||||
|
):
|
||||||
|
monkeypatch.setattr(DebugManager, "DEFAULT_LOG_DIR", str(tmp_path))
|
||||||
|
first_manager = DebugManager()
|
||||||
|
second_manager = DebugManager()
|
||||||
|
try:
|
||||||
|
first_manager.open_debug_file()
|
||||||
|
second_manager.open_debug_file()
|
||||||
|
|
||||||
|
assert first_manager.get_debug_file() == str(tmp_path / "fenrir.log")
|
||||||
|
assert second_manager.get_debug_file() == str(
|
||||||
|
tmp_path / "fenrir2.log"
|
||||||
|
)
|
||||||
|
assert (tmp_path / "fenrir2.log").exists()
|
||||||
|
finally:
|
||||||
|
second_manager.close_debug_file()
|
||||||
|
first_manager.close_debug_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_debug_file_reuses_unlocked_flat_name(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.setattr(DebugManager, "DEFAULT_LOG_DIR", str(tmp_path))
|
||||||
|
first_manager = DebugManager()
|
||||||
|
second_manager = DebugManager()
|
||||||
|
try:
|
||||||
|
first_manager.open_debug_file()
|
||||||
|
first_manager.close_debug_file()
|
||||||
|
second_manager.open_debug_file()
|
||||||
|
|
||||||
|
assert second_manager.get_debug_file() == str(tmp_path / "fenrir.log")
|
||||||
|
finally:
|
||||||
|
second_manager.close_debug_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_debug_file_uses_exact_path(tmp_path):
|
||||||
|
debug_file = tmp_path / "custom.log"
|
||||||
|
manager = DebugManager(str(debug_file))
|
||||||
|
try:
|
||||||
|
manager.open_debug_file()
|
||||||
|
|
||||||
|
assert manager.get_debug_file() == str(debug_file)
|
||||||
|
assert debug_file.exists()
|
||||||
|
finally:
|
||||||
|
manager.close_debug_file()
|
||||||
@@ -22,6 +22,7 @@ def test_speech_history_plain_key_modal_command_is_dispatched():
|
|||||||
)
|
)
|
||||||
speech_history_manager = Mock(is_active=Mock(return_value=True))
|
speech_history_manager = Mock(is_active=Mock(return_value=True))
|
||||||
diff_review_manager = Mock(is_active=Mock(return_value=False))
|
diff_review_manager = Mock(is_active=Mock(return_value=False))
|
||||||
|
vmenu_manager = Mock(get_active=Mock(return_value=False))
|
||||||
|
|
||||||
manager.environment = {
|
manager.environment = {
|
||||||
"input": {
|
"input": {
|
||||||
@@ -32,6 +33,7 @@ def test_speech_history_plain_key_modal_command_is_dispatched():
|
|||||||
"runtime": {
|
"runtime": {
|
||||||
"InputManager": input_manager,
|
"InputManager": input_manager,
|
||||||
"EventManager": event_manager,
|
"EventManager": event_manager,
|
||||||
|
"VmenuManager": vmenu_manager,
|
||||||
"DiffReviewManager": diff_review_manager,
|
"DiffReviewManager": diff_review_manager,
|
||||||
"SpeechHistoryManager": speech_history_manager,
|
"SpeechHistoryManager": speech_history_manager,
|
||||||
},
|
},
|
||||||
@@ -42,3 +44,43 @@ def test_speech_history_plain_key_modal_command_is_dispatched():
|
|||||||
event_manager.put_to_event_queue.assert_called_once_with(
|
event_manager.put_to_event_queue.assert_called_once_with(
|
||||||
FenrirEventType.execute_command, "SPEECH_HISTORY_PREV"
|
FenrirEventType.execute_command, "SPEECH_HISTORY_PREV"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_vmenu_plain_key_modal_command_is_dispatched():
|
||||||
|
manager = FenrirManager.__new__(FenrirManager)
|
||||||
|
manager.modifierInput = False
|
||||||
|
manager.singleKeyCommand = False
|
||||||
|
manager.command = ""
|
||||||
|
|
||||||
|
event_manager = Mock(put_to_event_queue=Mock())
|
||||||
|
input_manager = Mock(
|
||||||
|
is_key_press=Mock(return_value=False),
|
||||||
|
no_key_pressed=Mock(return_value=False),
|
||||||
|
get_curr_shortcut=Mock(return_value=str([1, ["KEY_UP"]])),
|
||||||
|
get_command_for_shortcut=Mock(return_value="PREV_VMENU_ENTRY"),
|
||||||
|
)
|
||||||
|
vmenu_manager = Mock(get_active=Mock(return_value=True))
|
||||||
|
speech_history_manager = Mock(is_active=Mock(return_value=False))
|
||||||
|
diff_review_manager = Mock(is_active=Mock(return_value=False))
|
||||||
|
|
||||||
|
manager.environment = {
|
||||||
|
"input": {
|
||||||
|
"key_forward": 0,
|
||||||
|
"prev_input": ["KEY_UP"],
|
||||||
|
"curr_input": ["KEY_UP"],
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"InputManager": input_manager,
|
||||||
|
"EventManager": event_manager,
|
||||||
|
"VmenuManager": vmenu_manager,
|
||||||
|
"DiffReviewManager": diff_review_manager,
|
||||||
|
"SpeechHistoryManager": speech_history_manager,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.detect_shortcut_command()
|
||||||
|
|
||||||
|
event_manager.put_to_event_queue.assert_called_once_with(
|
||||||
|
FenrirEventType.execute_command, "PREV_VMENU_ENTRY"
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
from configparser import ConfigParser
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from fenrirscreenreader.core.settingsData import settings_data
|
||||||
|
from fenrirscreenreader.soundDriver import genericDriver
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsManager:
|
||||||
|
def __init__(self, settings):
|
||||||
|
self.settings = settings
|
||||||
|
|
||||||
|
def get_setting(self, section, setting):
|
||||||
|
return self.settings[setting]
|
||||||
|
|
||||||
|
|
||||||
|
def _sound_driver(settings):
|
||||||
|
sound_driver = genericDriver.driver()
|
||||||
|
sound_driver.initialize(
|
||||||
|
{"runtime": {"SettingsManager": SettingsManager(settings)}}
|
||||||
|
)
|
||||||
|
sound_driver.set_volume(0.75)
|
||||||
|
return sound_driver
|
||||||
|
|
||||||
|
|
||||||
|
def _configured_sound_settings():
|
||||||
|
config = ConfigParser(interpolation=None)
|
||||||
|
config.read(Path(__file__).parents[2] / "config/settings/settings.conf")
|
||||||
|
return {
|
||||||
|
"generic_play_file_command": config.get(
|
||||||
|
"sound", "generic_play_file_command"
|
||||||
|
),
|
||||||
|
"generic_frequency_command": config.get(
|
||||||
|
"sound", "generic_frequency_command"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_play_frequency_replaces_snake_case_placeholders(monkeypatch):
|
||||||
|
popen = Mock()
|
||||||
|
monkeypatch.setattr(genericDriver.subprocess, "Popen", popen)
|
||||||
|
sound_driver = _sound_driver(
|
||||||
|
{
|
||||||
|
"generic_play_file_command": "player fenrir_sound_file",
|
||||||
|
"generic_frequency_command": (
|
||||||
|
"tone --volume fenrir_volume --duration fenrir_duration "
|
||||||
|
"--frequency fenrir_frequency"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
sound_driver.play_frequence(
|
||||||
|
440, 0.25, adjust_volume=0.5, interrupt=False
|
||||||
|
)
|
||||||
|
|
||||||
|
assert popen.call_args.args[0] == [
|
||||||
|
"tone",
|
||||||
|
"--volume",
|
||||||
|
"0.375",
|
||||||
|
"--duration",
|
||||||
|
"0.25",
|
||||||
|
"--frequency",
|
||||||
|
"440",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_play_sound_file_replaces_snake_case_placeholders(
|
||||||
|
monkeypatch, tmp_path
|
||||||
|
):
|
||||||
|
popen = Mock()
|
||||||
|
monkeypatch.setattr(genericDriver.subprocess, "Popen", popen)
|
||||||
|
sound_file = tmp_path / "sound file.ogg"
|
||||||
|
sound_file.write_bytes(b"")
|
||||||
|
sound_driver = _sound_driver(
|
||||||
|
{
|
||||||
|
"generic_play_file_command": (
|
||||||
|
"player --volume fenrir_volume fenrir_sound_file"
|
||||||
|
),
|
||||||
|
"generic_frequency_command": "tone fenrir_frequency",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
sound_driver.play_sound_file(str(sound_file), interrupt=False)
|
||||||
|
|
||||||
|
assert popen.call_args.args[0] == [
|
||||||
|
"player",
|
||||||
|
"--volume",
|
||||||
|
"0.75",
|
||||||
|
str(sound_file),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"settings",
|
||||||
|
[settings_data["sound"], _configured_sound_settings()],
|
||||||
|
)
|
||||||
|
def test_default_sound_commands_use_supported_placeholders(
|
||||||
|
monkeypatch, tmp_path, settings
|
||||||
|
):
|
||||||
|
popen = Mock()
|
||||||
|
monkeypatch.setattr(genericDriver.subprocess, "Popen", popen)
|
||||||
|
sound_file = tmp_path / "sound.ogg"
|
||||||
|
sound_file.write_bytes(b"")
|
||||||
|
sound_driver = _sound_driver(settings)
|
||||||
|
|
||||||
|
sound_driver.play_frequence(
|
||||||
|
440, 0.25, adjust_volume=0.5, interrupt=False
|
||||||
|
)
|
||||||
|
sound_driver.play_sound_file(str(sound_file), interrupt=False)
|
||||||
|
|
||||||
|
for call in popen.call_args_list:
|
||||||
|
assert not any("fenrir_" in argument for argument in call.args[0])
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
from configparser import ConfigParser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from fenrirscreenreader.core.settingsData import settings_data
|
||||||
|
from fenrirscreenreader.speechDriver import genericDriver
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_speech_command_replaces_snake_case_placeholders():
|
||||||
|
speech_driver = genericDriver.driver()
|
||||||
|
speech_driver.speech_command = (
|
||||||
|
"synth --volume fenrir_volume --module fenrir_module "
|
||||||
|
"--language fenrir_language --voice fenrir_voice "
|
||||||
|
"--pitch fenrir_pitch --rate fenrir_rate fenrir_text"
|
||||||
|
)
|
||||||
|
utterance = {
|
||||||
|
"volume": "150",
|
||||||
|
"module": "module-name",
|
||||||
|
"language": "en-US",
|
||||||
|
"voice": "voice-name",
|
||||||
|
"pitch": "50",
|
||||||
|
"rate": "240",
|
||||||
|
"text": "hello",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert speech_driver._build_speech_command(utterance) == [
|
||||||
|
"synth",
|
||||||
|
"--volume",
|
||||||
|
"150",
|
||||||
|
"--module",
|
||||||
|
"module-name",
|
||||||
|
"--language",
|
||||||
|
"en-US",
|
||||||
|
"--voice",
|
||||||
|
"voice-name",
|
||||||
|
"--pitch",
|
||||||
|
"50",
|
||||||
|
"--rate",
|
||||||
|
"240",
|
||||||
|
"hello",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _configured_speech_command():
|
||||||
|
config = ConfigParser(interpolation=None)
|
||||||
|
config.read(Path(__file__).parents[2] / "config/settings/settings.conf")
|
||||||
|
return config.get("speech", "generic_speech_command")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"speech_command",
|
||||||
|
[
|
||||||
|
settings_data["speech"]["generic_speech_command"],
|
||||||
|
_configured_speech_command(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_default_speech_commands_use_supported_placeholders(speech_command):
|
||||||
|
speech_driver = genericDriver.driver()
|
||||||
|
speech_driver.speech_command = speech_command
|
||||||
|
utterance = {
|
||||||
|
"volume": "150",
|
||||||
|
"module": "",
|
||||||
|
"language": "en-US",
|
||||||
|
"voice": "voice-name",
|
||||||
|
"pitch": "50",
|
||||||
|
"rate": "240",
|
||||||
|
"text": "hello",
|
||||||
|
}
|
||||||
|
|
||||||
|
built_command = speech_driver._build_speech_command(utterance)
|
||||||
|
|
||||||
|
assert not any("fenrir_" in argument for argument in built_command)
|
||||||
@@ -39,3 +39,104 @@ def test_progress_detector_skips_typing_delta():
|
|||||||
|
|
||||||
command.is_real_progress_update.assert_not_called()
|
command.is_real_progress_update.assert_not_called()
|
||||||
command.detect_progress.assert_not_called()
|
command.detect_progress.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_progress_detector_allows_long_tqdm_transfer_delta():
|
||||||
|
progress_module = _load_progress_module()
|
||||||
|
command = progress_module.command()
|
||||||
|
sample = (
|
||||||
|
"88%|"
|
||||||
|
"████████████████████████████████████████████████████████████████"
|
||||||
|
"████████████████████████████████████████████████████████████████"
|
||||||
|
"████████████████████████████████████████████████████████████████"
|
||||||
|
"█████████████████████████▊ "
|
||||||
|
"| 843M/954M [00:54<00:07, 15.2MB/s]"
|
||||||
|
)
|
||||||
|
command.env = {
|
||||||
|
"commandBuffer": {"progress_monitoring": True},
|
||||||
|
"runtime": {
|
||||||
|
"DebugManager": Mock(write_debug_out=Mock()),
|
||||||
|
"ScreenManager": Mock(is_screen_change=Mock(return_value=False)),
|
||||||
|
"CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)),
|
||||||
|
},
|
||||||
|
"screen": {
|
||||||
|
"new_delta": sample,
|
||||||
|
"new_content_text": sample,
|
||||||
|
"old_cursor": {"x": 0, "y": 0},
|
||||||
|
"new_cursor": {"x": 0, "y": 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert len(sample) > 200
|
||||||
|
assert command.is_real_progress_update()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_progress_detector_beeps_for_long_tqdm_transfer_delta():
|
||||||
|
progress_module = _load_progress_module()
|
||||||
|
command = progress_module.command()
|
||||||
|
sample = (
|
||||||
|
"90%|"
|
||||||
|
"████████████████████████████████████████████████████████████████"
|
||||||
|
"████████████████████████████████████████████████████████████████"
|
||||||
|
"████████████████████████████████████████████████████████████████"
|
||||||
|
"█████████████████████████████████████▍ "
|
||||||
|
"| 856M/954M [00:56<00:14, 6.78MB/s]"
|
||||||
|
)
|
||||||
|
command.env = {
|
||||||
|
"commandBuffer": {
|
||||||
|
"progress_monitoring": True,
|
||||||
|
"lastProgressValue": -1,
|
||||||
|
"lastProgressTime": 0,
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"DebugManager": Mock(write_debug_out=Mock()),
|
||||||
|
"ScreenManager": Mock(is_screen_change=Mock(return_value=False)),
|
||||||
|
"CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)),
|
||||||
|
},
|
||||||
|
"screen": {
|
||||||
|
"new_delta": sample,
|
||||||
|
"new_delta_is_typing": False,
|
||||||
|
"new_content_text": sample,
|
||||||
|
"old_cursor": {"x": 0, "y": 0},
|
||||||
|
"new_cursor": {"x": 0, "y": 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
command.play_progress_tone = Mock()
|
||||||
|
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
command.play_progress_tone.assert_called_once_with(90.0)
|
||||||
|
assert command.env["commandBuffer"]["lastProgressValue"] == 90.0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_progress_detector_beeps_for_interruptible_status_without_ellipsis():
|
||||||
|
progress_module = _load_progress_module()
|
||||||
|
command = progress_module.command()
|
||||||
|
sample = "◦ Files: (1m 04s • esc to interrupt)"
|
||||||
|
command.env = {
|
||||||
|
"commandBuffer": {
|
||||||
|
"progress_monitoring": True,
|
||||||
|
"lastProgressValue": -1,
|
||||||
|
"lastProgressTime": 0,
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"DebugManager": Mock(write_debug_out=Mock()),
|
||||||
|
"ScreenManager": Mock(is_screen_change=Mock(return_value=False)),
|
||||||
|
"CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)),
|
||||||
|
},
|
||||||
|
"screen": {
|
||||||
|
"new_delta": sample,
|
||||||
|
"new_delta_is_typing": False,
|
||||||
|
"new_content_text": sample,
|
||||||
|
"old_cursor": {"x": 0, "y": 0},
|
||||||
|
"new_cursor": {"x": 0, "y": 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
command.play_activity_beep = Mock()
|
||||||
|
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
command.play_activity_beep.assert_called_once_with()
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ def test_pty_stdin_input_interrupts_output_when_all_keys_interrupt_enabled():
|
|||||||
"runtime": {
|
"runtime": {
|
||||||
"SettingsManager": settings_manager,
|
"SettingsManager": settings_manager,
|
||||||
"OutputManager": output_manager,
|
"OutputManager": output_manager,
|
||||||
|
"DebugManager": Mock(write_debug_out=Mock()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +161,90 @@ def test_pty_plain_stdin_does_not_record_tab_keypress():
|
|||||||
pty_driver.inject_text_to_screen.assert_called_once_with(b"a")
|
pty_driver.inject_text_to_screen.assert_called_once_with(b"a")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("sequence", "key_name"),
|
||||||
|
[
|
||||||
|
(b"\x1b[A", "KEY_UP"),
|
||||||
|
(b"\x1b[B", "KEY_DOWN"),
|
||||||
|
(b"\x1b[C", "KEY_RIGHT"),
|
||||||
|
(b"\x1b[D", "KEY_LEFT"),
|
||||||
|
(b"\x1b[5~", "KEY_PAGEUP"),
|
||||||
|
(b"\x1b[6~", "KEY_PAGEDOWN"),
|
||||||
|
(b"\x1b", "KEY_ESC"),
|
||||||
|
(b"\r", "KEY_ENTER"),
|
||||||
|
(b" ", "KEY_SPACE"),
|
||||||
|
(b"a", "KEY_A"),
|
||||||
|
(b"Z", "KEY_Z"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_pty_vmenu_stdin_is_consumed_and_synthesizes_key_events(
|
||||||
|
sequence,
|
||||||
|
key_name,
|
||||||
|
):
|
||||||
|
pty_driver = PtyDriver()
|
||||||
|
event_queue = Mock()
|
||||||
|
settings_manager = Mock()
|
||||||
|
settings_manager.get_setting_as_bool.return_value = False
|
||||||
|
pty_driver.env = {
|
||||||
|
"input": {"curr_input": []},
|
||||||
|
"runtime": {
|
||||||
|
"DebugManager": Mock(write_debug_out=Mock()),
|
||||||
|
"SettingsManager": settings_manager,
|
||||||
|
"VmenuManager": Mock(get_active=Mock(return_value=True)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pty_driver.inject_text_to_screen = Mock()
|
||||||
|
|
||||||
|
pty_driver.handle_stdin_input(sequence, event_queue)
|
||||||
|
|
||||||
|
pty_driver.inject_text_to_screen.assert_not_called()
|
||||||
|
assert event_queue.put.call_count == 2
|
||||||
|
first_event = event_queue.put.call_args_list[0].args[0]
|
||||||
|
second_event = event_queue.put.call_args_list[1].args[0]
|
||||||
|
assert first_event["Type"] == FenrirEventType.keyboard_input
|
||||||
|
assert first_event["data"]["event_name"] == key_name
|
||||||
|
assert first_event["data"]["event_state"] == 1
|
||||||
|
assert second_event["data"]["event_name"] == key_name
|
||||||
|
assert second_event["data"]["event_state"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_pty_vmenu_unknown_stdin_is_consumed_without_injection():
|
||||||
|
pty_driver = PtyDriver()
|
||||||
|
event_queue = Mock()
|
||||||
|
pty_driver.env = {
|
||||||
|
"input": {"curr_input": []},
|
||||||
|
"runtime": {
|
||||||
|
"VmenuManager": Mock(get_active=Mock(return_value=True)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pty_driver.inject_text_to_screen = Mock()
|
||||||
|
|
||||||
|
pty_driver.handle_stdin_input(b"\x1b[1;5A", event_queue)
|
||||||
|
|
||||||
|
pty_driver.inject_text_to_screen.assert_not_called()
|
||||||
|
event_queue.put.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_pty_vmenu_stdin_does_not_duplicate_current_x11_key():
|
||||||
|
pty_driver = PtyDriver()
|
||||||
|
event_queue = Mock()
|
||||||
|
pty_driver.env = {
|
||||||
|
"input": {"curr_input": ["KEY_RIGHT"]},
|
||||||
|
"runtime": {
|
||||||
|
"VmenuManager": Mock(get_active=Mock(return_value=True)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pty_driver.inject_text_to_screen = Mock()
|
||||||
|
|
||||||
|
pty_driver.handle_stdin_input(b"\x1b[C", event_queue)
|
||||||
|
|
||||||
|
pty_driver.inject_text_to_screen.assert_not_called()
|
||||||
|
event_queue.put.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_pty_stdin_input_honors_interrupt_disabled():
|
def test_pty_stdin_input_honors_interrupt_disabled():
|
||||||
pty_driver = PtyDriver()
|
pty_driver = PtyDriver()
|
||||||
@@ -170,6 +255,7 @@ def test_pty_stdin_input_honors_interrupt_disabled():
|
|||||||
"runtime": {
|
"runtime": {
|
||||||
"SettingsManager": settings_manager,
|
"SettingsManager": settings_manager,
|
||||||
"OutputManager": output_manager,
|
"OutputManager": output_manager,
|
||||||
|
"DebugManager": Mock(write_debug_out=Mock()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +275,7 @@ def test_pty_stdin_input_leaves_filtered_interrupts_to_key_events():
|
|||||||
"runtime": {
|
"runtime": {
|
||||||
"SettingsManager": settings_manager,
|
"SettingsManager": settings_manager,
|
||||||
"OutputManager": output_manager,
|
"OutputManager": output_manager,
|
||||||
|
"DebugManager": Mock(write_debug_out=Mock()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +284,60 @@ def test_pty_stdin_input_leaves_filtered_interrupts_to_key_events():
|
|||||||
output_manager.interrupt_output.assert_not_called()
|
output_manager.interrupt_output.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_pty_recent_review_escape_tail_is_consumed_without_interrupt_or_injection():
|
||||||
|
pty_driver = PtyDriver()
|
||||||
|
event_queue = Mock()
|
||||||
|
settings_manager = Mock()
|
||||||
|
settings_manager.get_setting_as_bool.return_value = True
|
||||||
|
settings_manager.get_setting.return_value = ""
|
||||||
|
output_manager = Mock()
|
||||||
|
pty_driver.env = {
|
||||||
|
"commandInfo": {
|
||||||
|
"lastCommand": "REVIEW_PREV_LINE",
|
||||||
|
"lastCommandSection": "commands",
|
||||||
|
"lastCommandRunTime": time.time(),
|
||||||
|
},
|
||||||
|
"input": {"curr_input": []},
|
||||||
|
"runtime": {
|
||||||
|
"DebugManager": Mock(write_debug_out=Mock()),
|
||||||
|
"OutputManager": output_manager,
|
||||||
|
"SettingsManager": settings_manager,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pty_driver.inject_text_to_screen = Mock()
|
||||||
|
|
||||||
|
pty_driver.handle_stdin_input(b"\x1b[7~", event_queue)
|
||||||
|
|
||||||
|
output_manager.interrupt_output.assert_not_called()
|
||||||
|
pty_driver.inject_text_to_screen.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_pty_recent_review_does_not_consume_plain_text_input():
|
||||||
|
pty_driver = PtyDriver()
|
||||||
|
event_queue = Mock()
|
||||||
|
settings_manager = Mock()
|
||||||
|
settings_manager.get_setting_as_bool.return_value = False
|
||||||
|
pty_driver.env = {
|
||||||
|
"commandInfo": {
|
||||||
|
"lastCommand": "REVIEW_PREV_LINE",
|
||||||
|
"lastCommandSection": "commands",
|
||||||
|
"lastCommandRunTime": time.time(),
|
||||||
|
},
|
||||||
|
"input": {"curr_input": []},
|
||||||
|
"runtime": {
|
||||||
|
"DebugManager": Mock(write_debug_out=Mock()),
|
||||||
|
"SettingsManager": settings_manager,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pty_driver.inject_text_to_screen = Mock()
|
||||||
|
|
||||||
|
pty_driver.handle_stdin_input(b"a", event_queue)
|
||||||
|
|
||||||
|
pty_driver.inject_text_to_screen.assert_called_once_with(b"a")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_pty_backspace_with_fenrir_key_synthesizes_shortcut_events():
|
def test_pty_backspace_with_fenrir_key_synthesizes_shortcut_events():
|
||||||
pty_driver = PtyDriver()
|
pty_driver = PtyDriver()
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from fenrirscreenreader.utils import char_utils
|
||||||
|
|
||||||
|
|
||||||
|
COMMANDS_DIR = (
|
||||||
|
Path(__file__).resolve().parents[2]
|
||||||
|
/ "src"
|
||||||
|
/ "fenrirscreenreader"
|
||||||
|
/ "commands"
|
||||||
|
/ "commands"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_command(name):
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
f"fenrir_{name}", COMMANDS_DIR / f"{name}.py"
|
||||||
|
)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module.command()
|
||||||
|
|
||||||
|
|
||||||
|
def build_environment(cursor):
|
||||||
|
settings_manager = Mock()
|
||||||
|
settings_manager.get_setting_as_bool.return_value = True
|
||||||
|
output_manager = Mock()
|
||||||
|
cursor_manager = Mock()
|
||||||
|
cursor_manager.enter_review_mode_curr_text_cursor.return_value = None
|
||||||
|
cursor_manager.get_review_or_text_cursor.return_value = cursor
|
||||||
|
|
||||||
|
return {
|
||||||
|
"punctuation": {"PUNCTDICT": {" ": "space"}},
|
||||||
|
"screen": {
|
||||||
|
"newCursorReview": cursor.copy(),
|
||||||
|
"new_cursor": cursor.copy(),
|
||||||
|
"new_content_text": "abc\ndef",
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"AttributeManager": Mock(has_attributes=Mock(return_value=False)),
|
||||||
|
"CursorManager": cursor_manager,
|
||||||
|
"OutputManager": output_manager,
|
||||||
|
"SettingsManager": settings_manager,
|
||||||
|
"TableManager": Mock(is_table_mode=Mock(return_value=False)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(name, cursor):
|
||||||
|
env = build_environment(cursor)
|
||||||
|
command = load_command(name)
|
||||||
|
command.initialize(env)
|
||||||
|
command.run()
|
||||||
|
return env["runtime"]["OutputManager"]
|
||||||
|
|
||||||
|
|
||||||
|
def boundary_call(output_manager):
|
||||||
|
return output_manager.present_text.call_args_list[-1]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_previous_line_uses_start_of_screen_sound_at_top():
|
||||||
|
output_manager = run_command("review_prev_line", {"x": 0, "y": 0})
|
||||||
|
|
||||||
|
call = boundary_call(output_manager)
|
||||||
|
assert call.args[0] == "start of screen"
|
||||||
|
assert call.kwargs["sound_icon"] == "StartOfScreen"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_next_line_uses_end_of_screen_sound_at_bottom():
|
||||||
|
output_manager = run_command("review_next_line", {"x": 0, "y": 1})
|
||||||
|
|
||||||
|
call = boundary_call(output_manager)
|
||||||
|
assert call.args[0] == "end of screen"
|
||||||
|
assert call.kwargs["sound_icon"] == "EndOfScreen"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_previous_character_uses_start_of_screen_sound_at_top_left():
|
||||||
|
output_manager = run_command("review_prev_char", {"x": 0, "y": 0})
|
||||||
|
|
||||||
|
call = boundary_call(output_manager)
|
||||||
|
assert call.args[0] == "start of screen"
|
||||||
|
assert call.kwargs["sound_icon"] == "StartOfScreen"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_next_character_uses_end_of_screen_sound_at_bottom_right():
|
||||||
|
output_manager = run_command("review_next_char", {"x": 2, "y": 1})
|
||||||
|
|
||||||
|
call = boundary_call(output_manager)
|
||||||
|
assert call.args[0] == "end of screen"
|
||||||
|
assert call.kwargs["sound_icon"] == "EndOfScreen"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_vertical_character_navigation_reports_boundaries_only_at_edges():
|
||||||
|
assert char_utils.get_up_char(0, 1, "abc\ndef") == (
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"a",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
assert char_utils.get_up_char(0, 0, "abc\ndef") == (
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"",
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
assert char_utils.get_down_char(0, 0, "abc\ndef") == (
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
"d",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
assert char_utils.get_down_char(0, 1, "abc\ndef") == (
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
"",
|
||||||
|
True,
|
||||||
|
)
|
||||||
@@ -5,6 +5,8 @@ Tests the _validate_setting_value method to ensure proper input validation
|
|||||||
for all configurable settings that could cause crashes or accessibility issues.
|
for all configurable settings that could cause crashes or accessibility issues.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -206,6 +208,30 @@ def test_focus_settings_define_tui_toggle():
|
|||||||
assert settings_data["focus"]["tui"] is False
|
assert settings_data["focus"]["tui"] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.settings
|
||||||
|
def test_format_cli_args_reports_startup_flags_in_stable_order():
|
||||||
|
manager = SettingsManager()
|
||||||
|
cli_args = Namespace(
|
||||||
|
debug=True,
|
||||||
|
foreground=False,
|
||||||
|
force_all_screens=False,
|
||||||
|
ignore_screen=["7"],
|
||||||
|
options="speech#rate=1.2",
|
||||||
|
print=False,
|
||||||
|
setting="/tmp/settings.conf",
|
||||||
|
x11=True,
|
||||||
|
x11_window_id="0x123",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert manager.format_cli_args(cli_args) == (
|
||||||
|
"{'debug': True, 'force_all_screens': False, 'foreground': False, "
|
||||||
|
"'ignore_screen': ['7'], 'options': 'speech#rate=1.2', "
|
||||||
|
"'print': False, 'setting': '/tmp/settings.conf', 'x11': True, "
|
||||||
|
"'x11_window_id': '0x123'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
@pytest.mark.settings
|
@pytest.mark.settings
|
||||||
class TestSettingsPathSelection:
|
class TestSettingsPathSelection:
|
||||||
|
|||||||
@@ -140,6 +140,93 @@ def test_candidate_list_speaks_visible_list_without_cursor_advance():
|
|||||||
assert manager.process_update() == "Documents/ Downloads/"
|
assert manager.process_update() == "Documents/ Downloads/"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_full_screen_scroll_speaks_only_inserted_candidate_list():
|
||||||
|
old_text = "\n".join(
|
||||||
|
[
|
||||||
|
"old top".ljust(30),
|
||||||
|
"old middle".ljust(30),
|
||||||
|
"old lower".ljust(30),
|
||||||
|
"old bottom".ljust(30),
|
||||||
|
"$ ./Toby".ljust(30),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
manager, env, _input_manager = _build_env(old_text, {"x": 8, "y": 4})
|
||||||
|
|
||||||
|
manager.capture_if_tab()
|
||||||
|
_set_screen_update(
|
||||||
|
env,
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"old middle".ljust(30),
|
||||||
|
"old lower".ljust(30),
|
||||||
|
"old bottom".ljust(30),
|
||||||
|
"TobyAccMod_V10-0.pk3 TobyConfig.ini".ljust(30),
|
||||||
|
"$ ./Toby".ljust(30),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
{"x": 8, "y": 4},
|
||||||
|
delta="\n".join(
|
||||||
|
[
|
||||||
|
"old middle",
|
||||||
|
"old lower",
|
||||||
|
"old bottom",
|
||||||
|
"TobyAccMod_V10-0.pk3 TobyConfig.ini",
|
||||||
|
"$ ./Toby",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert manager.process_update() == "TobyAccMod_V10-0.pk3 TobyConfig.ini"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_same_height_repaint_without_inserted_candidates_stays_silent():
|
||||||
|
old_text = "\n".join(
|
||||||
|
[
|
||||||
|
"old top".ljust(30),
|
||||||
|
"old middle".ljust(30),
|
||||||
|
"old lower".ljust(30),
|
||||||
|
"$ ./Toby".ljust(30),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
manager, env, _input_manager = _build_env(old_text, {"x": 8, "y": 3})
|
||||||
|
|
||||||
|
manager.capture_if_tab()
|
||||||
|
_set_screen_update(
|
||||||
|
env,
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"status line".ljust(30),
|
||||||
|
"old prompt history".ljust(30),
|
||||||
|
"unrelated output".ljust(30),
|
||||||
|
"$ ./Toby".ljust(30),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
{"x": 8, "y": 3},
|
||||||
|
delta="status line\nold prompt history\nunrelated output\n$ ./Toby",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert manager.process_update() == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_recent_tab_does_not_speak_delta_without_detected_completion():
|
||||||
|
old_text = "\n".join(["$ ./Toby".ljust(20), "".ljust(20)])
|
||||||
|
manager, env, _input_manager = _build_env(old_text, {"x": 8, "y": 0})
|
||||||
|
|
||||||
|
manager.capture_if_tab()
|
||||||
|
_set_screen_update(
|
||||||
|
env,
|
||||||
|
old_text,
|
||||||
|
{"x": 8, "y": 0},
|
||||||
|
delta="old unrelated screen content\n$ ./Toby",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert manager.process_update() == ""
|
||||||
|
assert env["commandBuffer"]["tabCompletion"]["pending"] is not None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_no_screen_change_stays_silent_and_keeps_pending_briefly():
|
def test_no_screen_change_stays_silent_and_keeps_pending_briefly():
|
||||||
manager, env, _input_manager = _build_env(
|
manager, env, _input_manager = _build_env(
|
||||||
|
|||||||
@@ -217,6 +217,47 @@ def test_x11_build_passive_grabs_for_fenrir_keys_and_shortcuts():
|
|||||||
assert ("KEY_BACKSPACE", X.Mod4Mask, True) in grabs
|
assert ("KEY_BACKSPACE", X.Mod4Mask, True) in grabs
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"modifier_mask",
|
||||||
|
[X.ControlMask, X.ShiftMask, X.Mod1Mask],
|
||||||
|
)
|
||||||
|
def test_x11_poll_modifier_interrupt_keys_interrupts_without_input_events(
|
||||||
|
modifier_mask,
|
||||||
|
):
|
||||||
|
x11 = X11Driver()
|
||||||
|
x11.active = True
|
||||||
|
x11.modifier_interrupt_state = 0
|
||||||
|
x11.modifier_state = 0
|
||||||
|
x11.root = Mock()
|
||||||
|
x11.root.query_pointer.return_value = Mock(mask=modifier_mask)
|
||||||
|
settings_manager = Mock()
|
||||||
|
settings_manager.get_setting_as_bool.return_value = True
|
||||||
|
settings_manager.get_setting.return_value = ""
|
||||||
|
output_manager = Mock()
|
||||||
|
x11.env = {
|
||||||
|
"input": {"event_buffer": []},
|
||||||
|
"runtime": {
|
||||||
|
"SettingsManager": settings_manager,
|
||||||
|
"OutputManager": output_manager,
|
||||||
|
"DebugManager": Mock(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
x11.poll_modifier_interrupt_keys()
|
||||||
|
|
||||||
|
output_manager.interrupt_output_async.assert_called_once()
|
||||||
|
assert x11.env["input"]["event_buffer"] == []
|
||||||
|
|
||||||
|
output_manager.interrupt_output_async.reset_mock()
|
||||||
|
x11.root.query_pointer.return_value = Mock(mask=0)
|
||||||
|
|
||||||
|
x11.poll_modifier_interrupt_keys()
|
||||||
|
|
||||||
|
output_manager.interrupt_output_async.assert_not_called()
|
||||||
|
assert x11.env["input"]["event_buffer"] == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_x11_optional_modifier_masks_can_exclude_numlock():
|
def test_x11_optional_modifier_masks_can_exclude_numlock():
|
||||||
x11 = X11Driver()
|
x11 = X11Driver()
|
||||||
|
|||||||
Reference in New Issue
Block a user