From 2d8980051faca48d77f76331101c894c65ca54ee Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Mon, 25 May 2026 23:06:07 -0400 Subject: [PATCH] Tighten terminal check for dropping. --- src/cthulhu/input_event_manager.py | 124 +++++++++++++----- ...put_event_manager_x11_focus_regressions.py | 91 +++++++++++++ 2 files changed, 183 insertions(+), 32 deletions(-) diff --git a/src/cthulhu/input_event_manager.py b/src/cthulhu/input_event_manager.py index 7f4f970..57b7e9c 100644 --- a/src/cthulhu/input_event_manager.py +++ b/src/cthulhu/input_event_manager.py @@ -429,41 +429,69 @@ class InputEventManager: identifier = os.path.basename(value.strip().lower()) return identifier == "xterm" - def _active_x11_window_is_xterm(self) -> bool: - """Returns True when the active X11 window appears to be XTerm.""" + @staticmethod + def _safe_call(window: Any, attrName: str) -> Optional[Any]: + """Returns attrName() for window, or None when unavailable.""" + + attr = getattr(window, attrName, None) + if not callable(attr): + return None + + try: + return attr() + except Exception: + return None + + def _log_active_x11_window_for_xterm_check(self, window: Any) -> None: + """Logs X11 window details used for XTerm pass-through decisions.""" + + classGroup = self._safe_call(window, "get_class_group") + classGroupName = None + classGroupResClass = None + if classGroup is not None: + classGroupName = self._safe_call(classGroup, "get_name") + classGroupResClass = self._safe_call(classGroup, "get_res_class") + + tokens = [ + "INPUT EVENT MANAGER: Active X11 window for XTerm check:", + "name", + self._safe_call(window, "get_name"), + "class group", + self._safe_call(window, "get_class_group_name") or classGroupName, + "class group res class", + classGroupResClass, + "class instance", + self._safe_call(window, "get_class_instance_name"), + "pid", + self._safe_call(window, "get_pid"), + ] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + def _active_x11_window_xterm_match(self) -> Optional[bool]: + """Returns whether the active X11 window is XTerm, or None when unknown.""" window = self._get_active_x11_window() if window is None: - return False + msg = "INPUT EVENT MANAGER: XTerm matcher cannot identify active X11 window." + debug.print_message(debug.LEVEL_INFO, msg, True) + return None + self._log_active_x11_window_for_xterm_check(window) for attrName in ("get_class_group_name", "get_class_instance_name", "get_name"): - attr = getattr(window, attrName, None) - if not callable(attr): - continue - try: - if self._identifier_is_xterm(attr()): - return True - except Exception: - continue + if self._identifier_is_xterm(self._safe_call(window, attrName)): + return True - getClassGroup = getattr(window, "get_class_group", None) - if callable(getClassGroup): - try: - classGroup = getClassGroup() - except Exception: - classGroup = None + classGroup = self._safe_call(window, "get_class_group") + if classGroup is not None: for attrName in ("get_name", "get_res_class"): - attr = getattr(classGroup, attrName, None) - if not callable(attr): - continue - try: - if self._identifier_is_xterm(attr()): - return True - except Exception: - continue + if self._identifier_is_xterm(self._safe_call(classGroup, attrName)): + return True - pid = self._get_active_x11_window_pid() + try: + pid = int(window.get_pid()) + except Exception: + pid = -1 if pid < 1: return False @@ -475,6 +503,11 @@ class InputEventManager: return self._identifier_is_xterm(executable) + def _active_x11_window_is_xterm(self) -> bool: + """Returns True when the active X11 window appears to be XTerm.""" + + return self._active_x11_window_xterm_match() is True + def _find_active_x11_atspi_window(self) -> Optional[Atspi.Accessible]: """Returns the focused AT-SPI window for the active X11 PID, if possible.""" @@ -589,8 +622,22 @@ class InputEventManager: """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 - return self._active_x11_window_is_xterm() + 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 match is None and self._scriptWithSuspendedGrabsForXterm is not None: + msg = ( + "INPUT EVENT MANAGER: Keeping XTerm key-grab suspension; " + "active X11 window is temporarily unknown." + ) + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + return False def _suspend_key_grabs_for_xterm(self) -> None: """Suspends active-script key grabs so XTerm/Fenrir can receive them.""" @@ -606,8 +653,11 @@ class InputEventManager: if not callable(removeGrabs): return - msg = "INPUT EVENT MANAGER: Removing active script key grabs while XTerm is focused." - debug.print_message(debug.LEVEL_INFO, msg, True) + tokens = [ + "INPUT EVENT MANAGER: Removing active script key grabs while XTerm is focused:", + script, + ] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) removeGrabs() self._scriptWithSuspendedGrabsForXterm = script @@ -619,15 +669,25 @@ class InputEventManager: if script is None: return - if script is not script_manager.get_manager().get_active_script(): + activeScript = script_manager.get_manager().get_active_script() + if script is not activeScript: + tokens = [ + "INPUT EVENT MANAGER: Not restoring XTerm-suspended key grabs; active script changed:", + script, + activeScript, + ] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return addGrabs = getattr(script, "addKeyGrabs", None) if not callable(addGrabs): return - msg = "INPUT EVENT MANAGER: Restoring active script key grabs after leaving XTerm." - debug.print_message(debug.LEVEL_INFO, msg, True) + tokens = [ + "INPUT EVENT MANAGER: Restoring active script key grabs after leaving XTerm:", + script, + ] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) addGrabs() # pylint: disable=too-many-arguments diff --git a/tests/test_input_event_manager_x11_focus_regressions.py b/tests/test_input_event_manager_x11_focus_regressions.py index 4958bea..75426b3 100644 --- a/tests/test_input_event_manager_x11_focus_regressions.py +++ b/tests/test_input_event_manager_x11_focus_regressions.py @@ -173,6 +173,97 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase): findActiveWindow.assert_not_called() keyboardEvent.process.assert_not_called() + def test_lxterminal_does_not_pass_through_as_xterm(self): + manager = input_event_manager.InputEventManager() + window = mock.Mock() + window.get_class_group_name.return_value = "lxterminal" + window.get_class_instance_name.return_value = "lxterminal" + window.get_name.return_value = "LXTerminal" + 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._should_pass_through_for_active_xterm(None, None, None) + + self.assertFalse(result) + + def test_xterm_pass_through_stays_active_when_window_lookup_temporarily_fails(self): + manager = input_event_manager.InputEventManager() + 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() + manager._scriptWithSuspendedGrabsForXterm = activeScript + + with ( + mock.patch.object(input_event_manager.cthulhu_state, "capturingKeys", False), + mock.patch.object(input_event_manager.cthulhu_state, "pendingSelfHostedFocus", None), + 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=None), + 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) + self.assertIs(manager._scriptWithSuspendedGrabsForXterm, activeScript) + activeScript.addKeyGrabs.assert_not_called() + keyboardEvent.process.assert_not_called() + + def test_xterm_grabs_restore_when_active_window_is_positively_not_xterm(self): + manager = input_event_manager.InputEventManager() + 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() + keyboardEvent.is_modifier_key.return_value = False + manager._scriptWithSuspendedGrabsForXterm = activeScript + + with ( + mock.patch.object(input_event_manager.cthulhu_state, "capturingKeys", False), + mock.patch.object(input_event_manager.cthulhu_state, "pendingSelfHostedFocus", None), + 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=False), + mock.patch.object(input_event_manager.AXUtilities, "can_be_active_window", return_value=True), + mock.patch.object(manager, "last_event_was_keyboard", return_value=False), + 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, + 36, + 65293, + 0, + "Return", + ) + + self.assertTrue(result) + self.assertIsNone(manager._scriptWithSuspendedGrabsForXterm) + activeScript.addKeyGrabs.assert_called_once_with() + keyboardEvent.process.assert_called_once_with() + def test_finds_focused_atspi_window_for_active_x11_pid(self): manager = input_event_manager.InputEventManager() app = object()