From f1a8e6af2131d52957a45a957e53c415859b6816 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 29 May 2026 19:50:38 -0400 Subject: [PATCH] Fixed long standing bug where bottom of screen played for both top and bottom, found a couple other things that were off in the process. --- .../commands/commands/review_prev_char.py | 4 +- .../commands/review_prev_char_phonetic.py | 4 +- .../commands/commands/review_prev_line.py | 4 +- .../commands/commands/review_prev_word.py | 4 +- .../commands/review_prev_word_phonetic.py | 4 +- .../commands/commands/review_up.py | 4 +- src/fenrirscreenreader/fenrirVersion.py | 2 +- src/fenrirscreenreader/utils/char_utils.py | 2 - .../test_review_screen_boundary_sounds.py | 126 ++++++++++++++++++ 9 files changed, 139 insertions(+), 15 deletions(-) create mode 100644 tests/unit/test_review_screen_boundary_sounds.py diff --git a/src/fenrirscreenreader/commands/commands/review_prev_char.py b/src/fenrirscreenreader/commands/commands/review_prev_char.py index 7e441d2a..607c4c9a 100644 --- a/src/fenrirscreenreader/commands/commands/review_prev_char.py +++ b/src/fenrirscreenreader/commands/commands/review_prev_char.py @@ -112,9 +112,9 @@ class command: "review", "end_of_screen" ): self.env["runtime"]["OutputManager"].present_text( - _("end of screen"), + _("start of screen"), interrupt=True, - sound_icon="EndOfScreen", + sound_icon="StartOfScreen", ) if line_break: if self.env["runtime"]["SettingsManager"].get_setting_as_bool( diff --git a/src/fenrirscreenreader/commands/commands/review_prev_char_phonetic.py b/src/fenrirscreenreader/commands/commands/review_prev_char_phonetic.py index 295ebc19..805fca57 100644 --- a/src/fenrirscreenreader/commands/commands/review_prev_char_phonetic.py +++ b/src/fenrirscreenreader/commands/commands/review_prev_char_phonetic.py @@ -49,9 +49,9 @@ class command: "review", "end_of_screen" ): self.env["runtime"]["OutputManager"].present_text( - _("end of screen"), + _("start of screen"), interrupt=True, - sound_icon="EndOfScreen", + sound_icon="StartOfScreen", ) if line_break: if self.env["runtime"]["SettingsManager"].get_setting_as_bool( diff --git a/src/fenrirscreenreader/commands/commands/review_prev_line.py b/src/fenrirscreenreader/commands/commands/review_prev_line.py index 87d5fda9..c1c07df8 100644 --- a/src/fenrirscreenreader/commands/commands/review_prev_line.py +++ b/src/fenrirscreenreader/commands/commands/review_prev_line.py @@ -50,9 +50,9 @@ class command: "review", "end_of_screen" ): self.env["runtime"]["OutputManager"].present_text( - _("end of screen"), + _("start of screen"), interrupt=True, - sound_icon="EndOfScreen", + sound_icon="StartOfScreen", ) def set_callback(self, callback): diff --git a/src/fenrirscreenreader/commands/commands/review_prev_word.py b/src/fenrirscreenreader/commands/commands/review_prev_word.py index ae6b3b2b..36e0f62a 100644 --- a/src/fenrirscreenreader/commands/commands/review_prev_word.py +++ b/src/fenrirscreenreader/commands/commands/review_prev_word.py @@ -95,9 +95,9 @@ class command: "review", "end_of_screen" ): self.env["runtime"]["OutputManager"].present_text( - _("end of screen"), + _("start of screen"), interrupt=False, - sound_icon="EndOfScreen", + sound_icon="StartOfScreen", ) if line_break: if self.env["runtime"]["SettingsManager"].get_setting_as_bool( diff --git a/src/fenrirscreenreader/commands/commands/review_prev_word_phonetic.py b/src/fenrirscreenreader/commands/commands/review_prev_word_phonetic.py index eadca6da..0e9613f6 100644 --- a/src/fenrirscreenreader/commands/commands/review_prev_word_phonetic.py +++ b/src/fenrirscreenreader/commands/commands/review_prev_word_phonetic.py @@ -60,9 +60,9 @@ class command: "review", "end_of_screen" ): self.env["runtime"]["OutputManager"].present_text( - _("end of screen"), + _("start of screen"), interrupt=True, - sound_icon="EndOfScreen", + sound_icon="StartOfScreen", ) if line_break: if self.env["runtime"]["SettingsManager"].get_setting_as_bool( diff --git a/src/fenrirscreenreader/commands/commands/review_up.py b/src/fenrirscreenreader/commands/commands/review_up.py index d538ea75..b51c03b7 100644 --- a/src/fenrirscreenreader/commands/commands/review_up.py +++ b/src/fenrirscreenreader/commands/commands/review_up.py @@ -50,9 +50,9 @@ class command: "review", "end_of_screen" ): self.env["runtime"]["OutputManager"].present_text( - _("end of screen"), + _("start of screen"), interrupt=True, - sound_icon="EndOfScreen", + sound_icon="StartOfScreen", ) if line_break: if self.env["runtime"]["SettingsManager"].get_setting_as_bool( diff --git a/src/fenrirscreenreader/fenrirVersion.py b/src/fenrirscreenreader/fenrirVersion.py index a7fe09de..278b1cd9 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.05.24" +version = "2026.05.29" code_name = "master" diff --git a/src/fenrirscreenreader/utils/char_utils.py b/src/fenrirscreenreader/utils/char_utils.py index f1b92b33..4ed55164 100644 --- a/src/fenrirscreenreader/utils/char_utils.py +++ b/src/fenrirscreenreader/utils/char_utils.py @@ -47,7 +47,6 @@ def get_up_char(curr_x, curr_y, curr_text): curr_y -= 1 if curr_y < 0: curr_y = 0 - else: end_of_screen = True curr_char = "" if not end_of_screen: @@ -63,7 +62,6 @@ def get_down_char(curr_x, curr_y, curr_text): curr_y += 1 if curr_y >= len(wrapped_lines): curr_y = len(wrapped_lines) - 1 - else: end_of_screen = True curr_char = "" if not end_of_screen: diff --git a/tests/unit/test_review_screen_boundary_sounds.py b/tests/unit/test_review_screen_boundary_sounds.py new file mode 100644 index 00000000..a64a6ac3 --- /dev/null +++ b/tests/unit/test_review_screen_boundary_sounds.py @@ -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, + )