From c143c9a5618eb1ea1d940de105e9a12cca771b44 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 24 May 2026 17:03:41 -0400 Subject: [PATCH] Found a vmenu bug in -x. I thought we were close to a new release... --- src/fenrirscreenreader/core/fenrirManager.py | 5 +- .../screenDriver/ptyDriver.py | 63 ++++++++++++++++++ .../test_fenrir_manager_speech_history.py | 42 ++++++++++++ tests/unit/test_pty_terminal_sequences.py | 66 +++++++++++++++++++ 4 files changed, 175 insertions(+), 1 deletion(-) diff --git a/src/fenrirscreenreader/core/fenrirManager.py b/src/fenrirscreenreader/core/fenrirManager.py index 6ae697f3..ed19624f 100644 --- a/src/fenrirscreenreader/core/fenrirManager.py +++ b/src/fenrirscreenreader/core/fenrirManager.py @@ -311,7 +311,10 @@ class FenrirManager: self.singleKeyCommand = True elif ( ( - self.environment["runtime"]["DiffReviewManager"].is_active() + self.environment["runtime"]["VmenuManager"].get_active() + or self.environment["runtime"][ + "DiffReviewManager" + ].is_active() or self.environment["runtime"][ "SpeechHistoryManager" ].is_active() diff --git a/src/fenrirscreenreader/screenDriver/ptyDriver.py b/src/fenrirscreenreader/screenDriver/ptyDriver.py index 1b0c5504..5574dab8 100644 --- a/src/fenrirscreenreader/screenDriver/ptyDriver.py +++ b/src/fenrirscreenreader/screenDriver/ptyDriver.py @@ -333,10 +333,73 @@ class driver(screenDriver): def handle_stdin_input(self, msg_bytes, event_queue): if self.synthesize_backspace_shortcut(msg_bytes, event_queue): return + if self.handle_vmenu_stdin_input(msg_bytes, event_queue): + return self.record_stdin_keypress(msg_bytes) self.interrupt_output_on_stdin_input(msg_bytes) self.inject_text_to_screen(msg_bytes) + 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: + 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 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): if msg_bytes != b"\t": return diff --git a/tests/unit/test_fenrir_manager_speech_history.py b/tests/unit/test_fenrir_manager_speech_history.py index a00f9d03..fd68ecc2 100644 --- a/tests/unit/test_fenrir_manager_speech_history.py +++ b/tests/unit/test_fenrir_manager_speech_history.py @@ -22,6 +22,7 @@ def test_speech_history_plain_key_modal_command_is_dispatched(): ) speech_history_manager = Mock(is_active=Mock(return_value=True)) diff_review_manager = Mock(is_active=Mock(return_value=False)) + vmenu_manager = Mock(get_active=Mock(return_value=False)) manager.environment = { "input": { @@ -32,6 +33,7 @@ def test_speech_history_plain_key_modal_command_is_dispatched(): "runtime": { "InputManager": input_manager, "EventManager": event_manager, + "VmenuManager": vmenu_manager, "DiffReviewManager": diff_review_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( 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" + ) diff --git a/tests/unit/test_pty_terminal_sequences.py b/tests/unit/test_pty_terminal_sequences.py index 8a2583d1..835c6907 100644 --- a/tests/unit/test_pty_terminal_sequences.py +++ b/tests/unit/test_pty_terminal_sequences.py @@ -160,6 +160,72 @@ def test_pty_plain_stdin_does_not_record_tab_keypress(): 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_stdin_input_honors_interrupt_disabled(): pty_driver = PtyDriver()