2 Commits

Author SHA1 Message Date
Storm Dragon 447e2cc896 Missed one commented setting. 2026-06-18 10:51:44 -04:00
Storm Dragon 2b7d205f06 Fixed some half-completed work in generic drivers. Minor -x bug fixes. 2026-06-18 10:35:48 -04:00
13 changed files with 426 additions and 120 deletions
+15 -15
View File
@@ -19,14 +19,14 @@ volume=0.7
# shell commands for generic sound driver
# the folowing variable are substituted
# fenrirVolume = the current volume setting
# fenrirSoundFile = the soundfile for an soundicon
# fenrirFrequence = the frequency to play
# fenrirDuration = the duration of the frequency
# fenrir_volume = the current volume setting
# fenrir_sound_file = the soundfile for an soundicon
# fenrir_frequency = the frequency to play
# fenrir_duration = the duration of the frequency
# the following command is used to play a soundfile
generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile
generic_play_file_command=play -q -v fenrir_volume fenrir_sound_file
#the following command is used to generate a frequency beep
generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
generic_frequency_command=play -q -v fenrir_volume -n -c1 synth fenrir_duration sine fenrir_frequency
# Enable progress bar monitoring with ascending tones by default
progress_monitoring=True
@@ -114,17 +114,17 @@ max_batch_lines=100
# Only enable flood control if this many new lines appear in the window
flood_line_threshold=500
# genericSpeechCommand is the command that is executed for talking
# generic_speech_command is the command that is executed for talking
# the following variables are replaced with values
# fenrirText = is the text that should be spoken
# fenrirModule = may be the speech module like used in speech-dispatcher, not every TTY need this
# fenrirLanguage = the language
# fenrirVoice = is the current voice that should be used. Set the voice variable above.
# fenrir_text = is the text that should be spoken
# fenrir_module = may be the speech module like used in speech-dispatcher, not every TTY need this
# fenrir_language = the language
# fenrir_voice = is the current voice that should be used. Set the voice variable above.
# the current volume, pitch and rate is calculated like this
# value = min + settingValue * (min - max )
# fenrirVolume = is replaced with the current volume
# fenrirPitch = is replaced with the current pitch
# fenrirRate = is replaced with the current speed (speech rate)
# fenrir_volume = is replaced with the current volume
# fenrir_pitch = is replaced with the current pitch
# fenrir_rate = is replaced with the current speed (speech rate)
generic_speech_command=espeak-ng -a fenrir_volume -s fenrir_rate -p fenrir_pitch -v fenrir_voice -- "fenrir_text"
# min and max values of the TTS system that is used in generic_speech_command
@@ -201,7 +201,7 @@ new_line_pause=True
number_of_clipboards=50
# used path for "export_clipboard_to_file"
# $user is replaced by username
#clipboardExportPath=/home/$user/fenrirClipboard
#clipboard_export_path=/home/$user/fenrirClipboard
clipboard_export_path=/tmp/fenrirClipboard
# Convert text emoticons like :) to descriptive text (e.g., "smiling face")
emoticons=True
+12 -12
View File
@@ -1506,38 +1506,38 @@ Values: `+0.0+` is quietest, `+1.0+` is loudest.
The generic sound driver uses shell commands for play sound and
frequencies.
`+genericPlayFileCommand+` defines the command that is used to play a
`+generic_play_file_command+` defines the command that is used to play a
sound file.
....
generic_play_file_command=<your command for playing a file>
....
`+genericFrequencyCommand+` defines the command that is used playing
`+generic_frequency_command+` defines the command that is used playing
frequencies.
....
generic_frequency_command=<your command for playing a frequence>
....
The following variables are substituted in `+genericPlayFileCommand+`
and `+genericFrequencyCommand+`:
The following variables are substituted in `+generic_play_file_command+`
and `+generic_frequency_command+`:
* `+fenrirVolume+` = the current volume setting
* `+fenrirSoundFile+` = the sound file for an sound icon
* `+fenrirFrequence+` = the frequency to play
* `+fenrirDuration+` = the duration of the frequency
* `+fenrir_volume+` = the current volume setting
* `+fenrir_sound_file+` = the sound file for an sound icon
* `+fenrir_frequency+` = the frequency to play
* `+fenrir_duration+` = the duration of the frequency
Example genericPlayFileCommand (default)
Example generic_play_file_command (default)
....
generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile
generic_play_file_command=play -q -v fenrir_volume fenrir_sound_file
....
Example genericFrequencyCommand (default)
Example generic_frequency_command (default)
....
generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
generic_frequency_command=play -q -v fenrir_volume -n -c1 synth fenrir_duration sine fenrir_frequency
....
==== Speech
+24 -24
View File
@@ -857,21 +857,21 @@ Values: ''0.0'' is quietest, ''1.0'' is loudest.
=== Generic Driver ===
The generic sound driver uses shell commands for play sound and frequencies.
''genericPlayFileCommand'' defines the command that is used to play a sound file.
''generic_play_file_command'' defines the command that is used to play a sound file.
generic_play_file_command=<your command for playing a file>
''genericFrequencyCommand'' defines the command that is used playing frequencies.
''generic_frequency_command'' defines the command that is used playing frequencies.
generic_frequency_command=<your command for playing a frequence>
The following variables are substituted in ''genericPlayFileCommand'' and ''genericFrequencyCommand'':
* ''fenrirVolume'' = the current volume setting
* ''fenrirSoundFile'' = the sound file for an sound icon
* ''fenrirFrequence'' = the frequency to play
* ''fenrirDuration'' = the duration of the frequency
The following variables are substituted in ''generic_play_file_command'' and ''generic_frequency_command'':
* ''fenrir_volume'' = the current volume setting
* ''fenrir_sound_file'' = the sound file for an sound icon
* ''fenrir_frequency'' = the frequency to play
* ''fenrir_duration'' = the duration of the frequency
Example genericPlayFileCommand (default)
generic_play_file_command=play -q -v fenrirVolume fenrirSoundFile
Example genericFrequencyCommand (default)
generic_frequency_command=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence
Example generic_play_file_command (default)
generic_play_file_command=play -q -v fenrir_volume fenrir_sound_file
Example generic_frequency_command (default)
generic_frequency_command=play -q -v fenrir_volume -n -c1 synth fenrir_duration sine fenrir_frequency
==== Speech ====
Speech is configured in section ''[speech]''.
Turn speech on or off:
@@ -945,22 +945,22 @@ Values: on=''True'', off=''False''
=== Generic Driver ===
The generic speech driver uses shell commands for speech synthisus.
''genericSpeechCommand'' defines the command that is executed for creating speech
The following variables are substituted in ''genericSpeechCommand'':
* ''FenrirText'' = is the text that should be spoken
* ''fenrirModule'' = may be the speech module like used in speech-dispatcher, not every TTY needs this
* ''fenrirLanguage'' = the language to speak in
* ''fenrirVoice'' = is the current voice that should be used
* ''fenrirVolume'' = is replaced with the current volume
* ''fenrirPitch'' = is replaced with the current pitch
* ''fenrirRate'' = is replaced with the current speed (speech rate)
''generic_speech_command'' defines the command that is executed for creating speech
The following variables are substituted in ''generic_speech_command'':
* ''fenrir_text'' = is the text that should be spoken
* ''fenrir_module'' = may be the speech module like used in speech-dispatcher, not every TTY needs this
* ''fenrir_language'' = the language to speak in
* ''fenrir_voice'' = is the current voice that should be used
* ''fenrir_volume'' = is replaced with the current volume
* ''fenrir_pitch'' = is replaced with the current pitch
* ''fenrir_rate'' = is replaced with the current speed (speech rate)
Example genericSpeechCommand (default):
generic_speech_command=espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice "fenrirText"
Example generic_speech_command (default):
generic_speech_command=espeak -a fenrir_volume -s fenrir_rate -p fenrir_pitch -v fenrir_voice "fenrir_text"
These are the minimum and maximum values of the TTS system used in genericSpeechCommand. They are needed to calculate the abstract range in volume, rate and pitch 0.0 - 1.0.
These are the minimum and maximum values of the TTS system used in generic_speech_command. They are needed to calculate the abstract range in volume, rate and pitch 0.0 - 1.0.
FenrirMinVolume=0
fenrir_min_volume=0
fenrir_max_volume=200
fenrir_min_pitch=0
fenrir_max_pitch=99
@@ -22,4 +22,7 @@ command_info = {
# 'curr_command': '',
"lastCommandExecutionTime": time.time(),
"lastCommandRequestTime": time.time(),
"lastCommand": "",
"lastCommandSection": "",
"lastCommandRunTime": 0,
}
@@ -474,6 +474,10 @@ class CommandManager:
def run_command(self, command, section="commands"):
if self.command_exists(command, section):
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(
"run_command command:" + section + "." + command,
debug.DebugLevel.INFO,
+11 -3
View File
@@ -12,8 +12,13 @@ settings_data = {
"driver": "genericDriver",
"theme": "default",
"volume": 1.0,
"generic_play_file_command": "play -q -v fenrirVolume fenrirSoundFile",
"generic_frequency_command": "play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence",
"generic_play_file_command": (
"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,
},
"speech": {
@@ -38,7 +43,10 @@ settings_data = {
"batch_flush_interval": 0.5,
"max_batch_lines": 100,
"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_max_volume": 200,
"fenrir_min_pitch": 0,
+2 -2
View File
@@ -4,5 +4,5 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
version = "2026.06.01"
code_name = "master"
version = "2026.06.18"
code_name = "testing"
@@ -304,6 +304,11 @@ class driver(screenDriver):
"keyboard", "interrupt_on_key_press_filter"
).strip():
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()
def start_stdin_interrupt_thread(self):
@@ -335,10 +340,43 @@ class driver(screenDriver):
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.interrupt_output_on_stdin_input(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
@@ -26,15 +26,15 @@ class driver(sound_driver):
Attributes:
proc: Currently running subprocess for sound playback
soundFileCommand (str): Command template for playing sound files
frequenceCommand (str): Command template for generating frequencies
sound_file_command (str): Command template for playing sound files
frequency_command (str): Command template for generating frequencies
"""
def __init__(self):
sound_driver.__init__(self)
self.proc = None
self.soundType = ""
self.soundFileCommand = ""
self.frequenceCommand = ""
self.sound_type = ""
self.sound_file_command = ""
self.frequency_command = ""
def initialize(self, environment):
"""Initialize the generic sound driver.
@@ -46,17 +46,20 @@ class driver(sound_driver):
environment: Fenrir environment dictionary with settings
"""
self.env = environment
self.soundFileCommand = self.env["runtime"][
self.sound_file_command = self.env["runtime"][
"SettingsManager"
].get_setting("sound", "generic_play_file_command")
self.frequenceCommand = self.env["runtime"][
self.frequency_command = self.env["runtime"][
"SettingsManager"
].get_setting("sound", "generic_frequency_command")
if self.soundFileCommand == "":
self.soundFileCommand = "play -q -v fenrirVolume fenrirSoundFile"
if self.frequenceCommand == "":
self.frequenceCommand = (
"play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence"
if self.sound_file_command == "":
self.sound_file_command = (
"play -q -v fenrir_volume fenrir_sound_file"
)
if self.frequency_command == "":
self.frequency_command = (
"play -q -v fenrir_volume -n -c1 synth "
"fenrir_duration sine fenrir_frequency"
)
self._initialized = True
@@ -75,22 +78,22 @@ class driver(sound_driver):
return
if interrupt:
self.cancel()
popen_frequence_command = shlex.split(self.frequenceCommand)
for idx, word in enumerate(popen_frequence_command):
popen_frequency_command = shlex.split(self.frequency_command)
for idx, word in enumerate(popen_frequency_command):
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("fenrirFrequence", str(frequence))
popen_frequence_command[idx] = word
word = word.replace("fenrir_duration", str(duration))
word = word.replace("fenrir_frequency", str(frequence))
popen_frequency_command[idx] = word
self.proc = subprocess.Popen(
popen_frequence_command,
popen_frequency_command,
stdin=None,
stdout=None,
stderr=None,
shell=False,
)
self.soundType = "frequence"
self.sound_type = "frequence"
def play_sound_file(self, file_path, interrupt=True):
"""Play a sound file.
@@ -108,13 +111,13 @@ class driver(sound_driver):
if not os.path.isfile(file_path) or ".." in file_path:
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):
word = word.replace("fenrirVolume", str(self.volume))
word = word.replace("fenrirSoundFile", shlex.quote(str(file_path)))
word = word.replace("fenrir_volume", str(self.volume))
word = word.replace("fenrir_sound_file", str(file_path))
popen_sound_file_command[idx] = word
self.proc = subprocess.Popen(popen_sound_file_command, shell=False)
self.soundType = "file"
self.sound_type = "file"
def cancel(self):
"""Cancel currently playing sound.
@@ -123,9 +126,9 @@ class driver(sound_driver):
"""
if not self._initialized:
return
if self.soundType == "":
if self.sound_type == "":
return
if self.soundType == "file":
if self.sound_type == "file":
self.proc.kill()
try:
# Wait for process to finish to prevent zombies
@@ -134,7 +137,7 @@ class driver(sound_driver):
pass # Process already terminated
except Exception as e:
pass # Handle any other wait errors
if self.soundType == "frequence":
if self.sound_type == "frequence":
self.proc.kill()
try:
# Wait for process to finish to prevent zombies
@@ -143,4 +146,4 @@ class driver(sound_driver):
pass # Process already terminated
except Exception as e:
pass # Handle any other wait errors
self.soundType = ""
self.sound_type = ""
@@ -30,49 +30,52 @@ class driver(speech_driver):
def __init__(self):
speech_driver.__init__(self)
self.proc = None
self.speechThread = Thread(target=self.worker)
self.speech_thread = Thread(target=self.worker)
self.lock = Lock()
self.textQueue = SpeakQueue()
self.text_queue = SpeakQueue()
def initialize(self, environment):
self.env = environment
self.minVolume = self.env["runtime"][
self.min_volume = self.env["runtime"][
"SettingsManager"
].get_setting_as_int("speech", "fenrir_min_volume")
self.maxVolume = self.env["runtime"][
self.max_volume = self.env["runtime"][
"SettingsManager"
].get_setting_as_int("speech", "fenrir_max_volume")
self.minPitch = self.env["runtime"][
self.min_pitch = self.env["runtime"][
"SettingsManager"
].get_setting_as_int("speech", "fenrir_min_pitch")
self.maxPitch = self.env["runtime"][
self.max_pitch = self.env["runtime"][
"SettingsManager"
].get_setting_as_int("speech", "fenrir_max_pitch")
self.minRate = self.env["runtime"][
self.min_rate = self.env["runtime"][
"SettingsManager"
].get_setting_as_int("speech", "fenrir_min_rate")
self.maxRate = self.env["runtime"][
self.max_rate = self.env["runtime"][
"SettingsManager"
].get_setting_as_int("speech", "fenrir_max_rate")
self.speechCommand = self.env["runtime"][
self.speech_command = self.env["runtime"][
"SettingsManager"
].get_setting("speech", "generic_speech_command")
if self.speechCommand == "":
self.speechCommand = 'espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice -- "fenrirText"'
if self.speech_command == "":
self.speech_command = (
"espeak -a fenrir_volume -s fenrir_rate -p fenrir_pitch "
'-v fenrir_voice -- "fenrir_text"'
)
if False: # for debugging overwrite here
# self.speechCommand = 'spd-say --wait -r 100 -i 100 "fenrirText"'
self.speechCommand = 'flite -t "fenrirText"'
# self.speech_command = 'spd-say --wait -r 100 -i 100 "fenrir_text"'
self.speech_command = 'flite -t "fenrir_text"'
self._is_initialized = True
if self._is_initialized:
self.speechThread.start()
self.speech_thread.start()
def shutdown(self):
if not self._is_initialized:
return
self.cancel()
self.textQueue.put(-1)
self.text_queue.put(-1)
def speak(self, text, queueable=True, ignore_punctuation=False):
if not self._is_initialized:
@@ -88,7 +91,7 @@ class driver(speech_driver):
"language": self.language,
"voice": self.voice,
}
self.textQueue.put(utterance.copy())
self.text_queue.put(utterance.copy())
def cancel(self):
if not self._is_initialized:
@@ -129,7 +132,7 @@ class driver(speech_driver):
def clear_buffer(self):
if not self._is_initialized:
return
self.textQueue.clear()
self.text_queue.clear()
def set_voice(self, voice):
if not self._is_initialized:
@@ -140,13 +143,13 @@ class driver(speech_driver):
if not self._is_initialized:
return
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):
if not self._is_initialized:
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):
if not self._is_initialized:
@@ -162,12 +165,29 @@ class driver(speech_driver):
if not self._is_initialized:
return
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):
while True:
utterance = self.textQueue.get()
utterance = self.text_queue.get()
if isinstance(utterance, int):
if utterance == -1:
@@ -209,21 +229,7 @@ class driver(speech_driver):
if not isinstance(utterance["rate"], str):
utterance["rate"] = ""
popen_speech_command = shlex.split(self.speechCommand)
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
popen_speech_command = self._build_speech_command(utterance)
try:
self.env["runtime"]["DebugManager"].write_debug_out(
+114
View File
@@ -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])
+73
View File
@@ -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)
+57
View File
@@ -70,6 +70,7 @@ def test_pty_stdin_input_interrupts_output_when_all_keys_interrupt_enabled():
"runtime": {
"SettingsManager": settings_manager,
"OutputManager": output_manager,
"DebugManager": Mock(write_debug_out=Mock()),
}
}
@@ -254,6 +255,7 @@ def test_pty_stdin_input_honors_interrupt_disabled():
"runtime": {
"SettingsManager": settings_manager,
"OutputManager": output_manager,
"DebugManager": Mock(write_debug_out=Mock()),
}
}
@@ -273,6 +275,7 @@ def test_pty_stdin_input_leaves_filtered_interrupts_to_key_events():
"runtime": {
"SettingsManager": settings_manager,
"OutputManager": output_manager,
"DebugManager": Mock(write_debug_out=Mock()),
}
}
@@ -281,6 +284,60 @@ def test_pty_stdin_input_leaves_filtered_interrupts_to_key_events():
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
def test_pty_backspace_with_fenrir_key_synthesizes_shortcut_events():
pty_driver = PtyDriver()