From 2cb83632f9325730e53ddea87198d8b99eadbfa6 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 31 May 2026 22:36:35 -0400 Subject: [PATCH] Fixed keyboard handling regression. --- src/fenrirscreenreader/fenrirVersion.py | 2 +- .../inputDriver/x11Driver.py | 53 +++++++++++++++++++ tests/unit/test_x11_terminal_mode.py | 41 ++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/fenrirscreenreader/fenrirVersion.py b/src/fenrirscreenreader/fenrirVersion.py index 7528725b..de252211 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.30" +version = "2026.05.31" code_name = "testing" diff --git a/src/fenrirscreenreader/inputDriver/x11Driver.py b/src/fenrirscreenreader/inputDriver/x11Driver.py index 2f39b2be..7d6335e8 100644 --- a/src/fenrirscreenreader/inputDriver/x11Driver.py +++ b/src/fenrirscreenreader/inputDriver/x11Driver.py @@ -162,6 +162,7 @@ class driver(inputDriver): self.fenrir_keys = set() self.failed_grabs = 0 self.modifier_state = 0 + self.modifier_interrupt_state = 0 def initialize(self, environment): self.env = environment @@ -194,6 +195,7 @@ class driver(inputDriver): ) self.num_lock_mask = self.find_num_lock_mask() self.refresh_modifier_state() + self.modifier_interrupt_state = self.modifier_state self.refresh_interesting_keys() self.refresh_grabs(force=True) self.env["runtime"]["ProcessManager"].add_custom_event_thread( @@ -274,6 +276,7 @@ class driver(inputDriver): while active.value: try: self.refresh_grabs() + self.poll_modifier_interrupt_keys() if not self.display.pending_events(): time.sleep(0.01) continue @@ -371,6 +374,56 @@ class driver(inputDriver): "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): try: pointer = self.root.query_pointer() diff --git a/tests/unit/test_x11_terminal_mode.py b/tests/unit/test_x11_terminal_mode.py index c9233c76..9ca6d376 100644 --- a/tests/unit/test_x11_terminal_mode.py +++ b/tests/unit/test_x11_terminal_mode.py @@ -217,6 +217,47 @@ def test_x11_build_passive_grabs_for_fenrir_keys_and_shortcuts(): 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 def test_x11_optional_modifier_masks_can_exclude_numlock(): x11 = X11Driver()