From 5dccdd27c48b0a2be5e838331947ba514898c933 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 12 May 2026 18:30:23 -0400 Subject: [PATCH] Another shot at squishing this bug. --- src/fenrirscreenreader/fenrirVersion.py | 2 +- .../inputDriver/x11Driver.py | 52 +++++++++++++++---- tests/unit/test_x11_terminal_mode.py | 39 ++++++++++++-- 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/src/fenrirscreenreader/fenrirVersion.py b/src/fenrirscreenreader/fenrirVersion.py index c78f44fd..3d1a410e 100644 --- a/src/fenrirscreenreader/fenrirVersion.py +++ b/src/fenrirscreenreader/fenrirVersion.py @@ -5,4 +5,4 @@ # By Chrys, Storm Dragon, and contributors. version = "2026.05.12" -code_name = "master" +code_name = "testing" diff --git a/src/fenrirscreenreader/inputDriver/x11Driver.py b/src/fenrirscreenreader/inputDriver/x11Driver.py index 8ab406ff..44c3bea3 100644 --- a/src/fenrirscreenreader/inputDriver/x11Driver.py +++ b/src/fenrirscreenreader/inputDriver/x11Driver.py @@ -157,6 +157,7 @@ class driver(inputDriver): self.interesting_keys = set() self.fenrir_keys = set() self.failed_grabs = 0 + self.modifier_state = 0 def initialize(self, environment): self.env = environment @@ -188,6 +189,7 @@ class driver(inputDriver): ) ) self.num_lock_mask = self.find_num_lock_mask() + self.refresh_modifier_state() self.refresh_interesting_keys() self.refresh_grabs(force=True) self.env["runtime"]["ProcessManager"].add_custom_event_thread( @@ -306,6 +308,7 @@ class driver(inputDriver): key_name = input_event["event_name"] if not self.should_emit_key(key_name): return + self.update_modifier_state_from_event(input_event) self.write_debug( "x11Driver key event " + key_name @@ -360,8 +363,41 @@ class driver(inputDriver): "event_usec": int((event_time % 1) * 1000000), "event_state": 1 if event.type == X.KeyPress else 0, "event_type": event.type, + "event_raw_state": getattr(event, "state", 0), } + def refresh_modifier_state(self): + try: + pointer = self.root.query_pointer() + self.modifier_state = getattr(pointer, "mask", 0) + except Exception: + self.modifier_state = 0 + + def update_modifier_state_from_event(self, input_event): + key_name = input_event["event_name"] + event_state = input_event["event_state"] + raw_state = input_event.get("event_raw_state", self.modifier_state) + self.modifier_state = raw_state + if key_name == "KEY_NUMLOCK": + self.update_lock_modifier_state(self.num_lock_mask, event_state) + elif key_name == "KEY_CAPSLOCK": + self.update_lock_modifier_state(X.LockMask, event_state) + else: + modifier_mask = self.modifier_masks.get(key_name, 0) + if modifier_mask: + if event_state == 1: + self.modifier_state |= modifier_mask + elif event_state == 0: + self.modifier_state &= ~modifier_mask + + def update_lock_modifier_state(self, modifier_mask, event_state): + if not modifier_mask or event_state != 1: + return + if self.modifier_state & modifier_mask: + self.modifier_state &= ~modifier_mask + else: + self.modifier_state |= modifier_mask + def keycode_to_key_name(self, keycode, state=0): key_names = [] for keysym_name in self.keycode_to_keysym_names(keycode): @@ -643,15 +679,13 @@ class driver(inputDriver): self.ungrab_all_devices() def get_led_state(self, led=0): - try: - pointer = self.root.query_pointer() - mask = getattr(pointer, "mask", 0) - if led == 0: - return bool(self.num_lock_mask and mask & self.num_lock_mask) - if led == 1: - return bool(mask & X.LockMask) - except Exception: - pass + if led == 0: + return bool( + self.num_lock_mask + and self.modifier_state & self.num_lock_mask + ) + if led == 1: + return bool(self.modifier_state & X.LockMask) return False def set_led_state(self, led_dict): diff --git a/tests/unit/test_x11_terminal_mode.py b/tests/unit/test_x11_terminal_mode.py index 3bf82249..55f2fdb3 100644 --- a/tests/unit/test_x11_terminal_mode.py +++ b/tests/unit/test_x11_terminal_mode.py @@ -201,16 +201,49 @@ def test_x11_optional_modifier_masks_can_exclude_numlock(): @pytest.mark.unit -def test_x11_get_led_state_reads_lock_modifiers_from_pointer_mask(): +def test_x11_get_led_state_reads_cached_lock_modifiers_without_x_round_trip(): x11 = X11Driver() x11.num_lock_mask = X.Mod2Mask - pointer = Mock(mask=X.Mod2Mask | X.LockMask) + x11.modifier_state = X.Mod2Mask | X.LockMask x11.root = Mock() - x11.root.query_pointer.return_value = pointer assert x11.get_led_state(0) is True assert x11.get_led_state(1) is True assert x11.get_led_state(2) is False + x11.root.query_pointer.assert_not_called() + + +@pytest.mark.unit +def test_x11_update_modifier_state_tracks_event_masks_and_lock_toggles(): + x11 = X11Driver() + x11.num_lock_mask = X.Mod2Mask + + x11.update_modifier_state_from_event( + { + "event_name": "KEY_KP8", + "event_state": 1, + "event_raw_state": X.Mod2Mask, + } + ) + assert x11.get_led_state(0) is True + + x11.update_modifier_state_from_event( + { + "event_name": "KEY_NUMLOCK", + "event_state": 1, + "event_raw_state": X.Mod2Mask, + } + ) + assert x11.get_led_state(0) is False + + x11.update_modifier_state_from_event( + { + "event_name": "KEY_CAPSLOCK", + "event_state": 1, + "event_raw_state": 0, + } + ) + assert x11.get_led_state(1) is True @pytest.mark.unit