diff --git a/src/cthulhu/input_event_manager.py b/src/cthulhu/input_event_manager.py index b64f420..8fe766c 100644 --- a/src/cthulhu/input_event_manager.py +++ b/src/cthulhu/input_event_manager.py @@ -477,28 +477,49 @@ class InputEventManager: return None self._log_active_x11_window_for_xterm_check(window) + sawIdentifier = False for attrName in ("get_class_group_name", "get_class_instance_name", "get_name"): - if self._identifier_is_xterm(self._safe_call(window, attrName)): + value = self._safe_call(window, attrName) + if self._identifier_is_xterm(value): return True + if isinstance(value, str) and value.strip(): + sawIdentifier = True classGroup = self._safe_call(window, "get_class_group") if classGroup is not None: for attrName in ("get_name", "get_res_class"): - if self._identifier_is_xterm(self._safe_call(classGroup, attrName)): + value = self._safe_call(classGroup, attrName) + if self._identifier_is_xterm(value): return True + if isinstance(value, str) and value.strip(): + sawIdentifier = True try: pid = int(window.get_pid()) except Exception: pid = -1 if pid < 1: + if not sawIdentifier: + msg = ( + "INPUT EVENT MANAGER: XTerm matcher cannot identify active X11 window; " + "no usable identifiers or pid." + ) + debug.print_message(debug.LEVEL_INFO, msg, True) + return None return False try: with open(f"/proc/{pid}/cmdline", "rb") as cmdlineFile: executable = cmdlineFile.read().split(b"\0", 1)[0].decode(errors="ignore") except OSError: + if not sawIdentifier: + msg = ( + "INPUT EVENT MANAGER: XTerm matcher cannot identify active X11 window; " + "pid disappeared before cmdline lookup." + ) + debug.print_message(debug.LEVEL_INFO, msg, True) + return None return False return self._identifier_is_xterm(executable) @@ -635,15 +656,15 @@ class InputEventManager: ) -> bool: """Returns True when XTerm is active and Cthulhu lacks matching AT-SPI context.""" - if pendingFocus is not None: - msg = "INPUT EVENT MANAGER: XTerm matcher false; pending focus exists." - debug.print_message(debug.LEVEL_INFO, msg, True) - return False match = self._active_x11_window_xterm_match() tokens = ["INPUT EVENT MANAGER: XTerm matcher returned", match] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if match is True: return True + if pendingFocus is not None: + msg = "INPUT EVENT MANAGER: XTerm matcher false; pending focus exists." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False if match is None and self._scriptWithSuspendedGrabsForXterm is not None: msg = ( "INPUT EVENT MANAGER: Keeping XTerm key-grab suspension; " diff --git a/tests/test_input_event_manager_x11_focus_regressions.py b/tests/test_input_event_manager_x11_focus_regressions.py index 3b33cb2..f0b880f 100644 --- a/tests/test_input_event_manager_x11_focus_regressions.py +++ b/tests/test_input_event_manager_x11_focus_regressions.py @@ -279,6 +279,52 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase): activeScript.addKeyGrabs.assert_not_called() keyboardEvent.process.assert_not_called() + def test_xterm_pass_through_ignores_stale_pending_self_hosted_focus(self): + manager = input_event_manager.InputEventManager() + pendingFocus = object() + focusManager = mock.Mock() + focusManager.get_active_window.return_value = None + focusManager.get_locus_of_focus.return_value = None + scriptManager = mock.Mock() + activeScript = mock.Mock(app=None) + scriptManager.get_active_script.return_value = activeScript + keyboardEvent = mock.Mock() + + with ( + mock.patch.object(input_event_manager.cthulhu_state, "capturingKeys", False), + mock.patch.object(input_event_manager.cthulhu_state, "pendingSelfHostedFocus", pendingFocus), + mock.patch.object(input_event_manager.focus_manager, "get_manager", return_value=focusManager), + mock.patch.object(input_event_manager.script_manager, "get_manager", return_value=scriptManager), + mock.patch.object(input_event_manager.input_event, "KeyboardEvent", return_value=keyboardEvent), + mock.patch.object(manager, "_active_x11_window_xterm_match", return_value=True), + mock.patch.object(input_event_manager.debug, "print_message"), + mock.patch.object(input_event_manager.debug, "print_tokens"), + ): + result = manager.process_keyboard_event( + mock.Mock(), + True, + 90, + 65438, + 0, + "KP_Insert", + ) + + self.assertFalse(result) + activeScript.removeKeyGrabs.assert_called_once_with() + keyboardEvent.process.assert_not_called() + + def test_pending_self_hosted_focus_blocks_unknown_xterm_match(self): + manager = input_event_manager.InputEventManager() + + with ( + mock.patch.object(manager, "_active_x11_window_xterm_match", return_value=None), + mock.patch.object(input_event_manager.debug, "print_message"), + mock.patch.object(input_event_manager.debug, "print_tokens"), + ): + result = manager._should_pass_through_for_active_xterm(None, None, object()) + + self.assertFalse(result) + def test_xterm_grabs_restore_when_active_window_is_positively_not_xterm(self): manager = input_event_manager.InputEventManager() focusManager = mock.Mock() @@ -317,6 +363,24 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase): activeScript.addKeyGrabs.assert_called_once_with() keyboardEvent.process.assert_called_once_with() + def test_unidentified_active_x11_window_is_unknown_not_non_xterm(self): + manager = input_event_manager.InputEventManager() + window = mock.Mock() + window.get_class_group_name.return_value = None + window.get_class_instance_name.return_value = None + window.get_name.return_value = None + window.get_class_group.return_value = None + window.get_pid.return_value = -1 + + with ( + mock.patch.object(manager, "_get_active_x11_window", return_value=window), + mock.patch.object(input_event_manager.debug, "print_message"), + mock.patch.object(input_event_manager.debug, "print_tokens"), + ): + result = manager._active_x11_window_xterm_match() + + self.assertIsNone(result) + def test_finds_focused_atspi_window_for_active_x11_pid(self): manager = input_event_manager.InputEventManager() app = object()