diff --git a/config/settings/settings.conf b/config/settings/settings.conf index 2d906906..23f7ddfa 100644 --- a/config/settings/settings.conf +++ b/config/settings/settings.conf @@ -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 diff --git a/docs/fenrir.adoc b/docs/fenrir.adoc index 7a2a182c..e0b85a4a 100644 --- a/docs/fenrir.adoc +++ b/docs/fenrir.adoc @@ -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= .... -`+genericFrequencyCommand+` defines the command that is used playing +`+generic_frequency_command+` defines the command that is used playing frequencies. .... generic_frequency_command= .... -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 diff --git a/docs/user.txt b/docs/user.txt index 420d743c..f5a5c2de 100644 --- a/docs/user.txt +++ b/docs/user.txt @@ -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= -''genericFrequencyCommand'' defines the command that is used playing frequencies. +''generic_frequency_command'' defines the command that is used playing frequencies. generic_frequency_command= -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 diff --git a/src/fenrirscreenreader/core/commandData.py b/src/fenrirscreenreader/core/commandData.py index b5f88fe4..50bd32e9 100644 --- a/src/fenrirscreenreader/core/commandData.py +++ b/src/fenrirscreenreader/core/commandData.py @@ -22,4 +22,7 @@ command_info = { # 'curr_command': '', "lastCommandExecutionTime": time.time(), "lastCommandRequestTime": time.time(), + "lastCommand": "", + "lastCommandSection": "", + "lastCommandRunTime": 0, } diff --git a/src/fenrirscreenreader/core/commandManager.py b/src/fenrirscreenreader/core/commandManager.py index 25387e88..38f9d585 100644 --- a/src/fenrirscreenreader/core/commandManager.py +++ b/src/fenrirscreenreader/core/commandManager.py @@ -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, diff --git a/src/fenrirscreenreader/core/settingsData.py b/src/fenrirscreenreader/core/settingsData.py index 99b8e82b..e6bef9b8 100644 --- a/src/fenrirscreenreader/core/settingsData.py +++ b/src/fenrirscreenreader/core/settingsData.py @@ -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, diff --git a/src/fenrirscreenreader/fenrirVersion.py b/src/fenrirscreenreader/fenrirVersion.py index 3f4944d9..310e9e7a 100644 --- a/src/fenrirscreenreader/fenrirVersion.py +++ b/src/fenrirscreenreader/fenrirVersion.py @@ -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" diff --git a/src/fenrirscreenreader/screenDriver/ptyDriver.py b/src/fenrirscreenreader/screenDriver/ptyDriver.py index 2a55ea10..0de1b0fc 100644 --- a/src/fenrirscreenreader/screenDriver/ptyDriver.py +++ b/src/fenrirscreenreader/screenDriver/ptyDriver.py @@ -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 diff --git a/src/fenrirscreenreader/soundDriver/genericDriver.py b/src/fenrirscreenreader/soundDriver/genericDriver.py index ab7a362e..f2b84042 100644 --- a/src/fenrirscreenreader/soundDriver/genericDriver.py +++ b/src/fenrirscreenreader/soundDriver/genericDriver.py @@ -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 = "" diff --git a/src/fenrirscreenreader/speechDriver/genericDriver.py b/src/fenrirscreenreader/speechDriver/genericDriver.py index 0a4a88c5..8fbe4028 100644 --- a/src/fenrirscreenreader/speechDriver/genericDriver.py +++ b/src/fenrirscreenreader/speechDriver/genericDriver.py @@ -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( diff --git a/tests/unit/test_generic_sound_driver.py b/tests/unit/test_generic_sound_driver.py new file mode 100644 index 00000000..040e476d --- /dev/null +++ b/tests/unit/test_generic_sound_driver.py @@ -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]) diff --git a/tests/unit/test_generic_speech_driver.py b/tests/unit/test_generic_speech_driver.py new file mode 100644 index 00000000..315212ad --- /dev/null +++ b/tests/unit/test_generic_speech_driver.py @@ -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) diff --git a/tests/unit/test_pty_terminal_sequences.py b/tests/unit/test_pty_terminal_sequences.py index 915bc997..706412f1 100644 --- a/tests/unit/test_pty_terminal_sequences.py +++ b/tests/unit/test_pty_terminal_sequences.py @@ -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()