From eeb7bd046ff4b9cc271be2361a07221560d4b295 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Wed, 8 Apr 2026 03:37:50 -0400 Subject: [PATCH] Hopefully fix some wayland keyboard stuff. --- src/cthulhu/learn_mode_presenter.py | 4 +- src/cthulhu/plugins/ByeCthulhu/plugin.py | 22 +++++----- src/cthulhu/scripts/default.py | 4 ++ tests/test_bye_cthulhu_regressions.py | 30 +++++++++++++ ...est_default_script_shutdown_regressions.py | 32 ++++++++++++++ tests/test_learn_mode_regressions.py | 44 +++++++++++++++++++ 6 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 tests/test_bye_cthulhu_regressions.py create mode 100644 tests/test_default_script_shutdown_regressions.py create mode 100644 tests/test_learn_mode_regressions.py diff --git a/src/cthulhu/learn_mode_presenter.py b/src/cthulhu/learn_mode_presenter.py index be72b1e..c8a32f3 100644 --- a/src/cthulhu/learn_mode_presenter.py +++ b/src/cthulhu/learn_mode_presenter.py @@ -172,7 +172,8 @@ class LearnModePresenter: and event.getHandler() is None: cthulhu_state.activeScript.phoneticSpellCurrentItem(event.event_string) - if event.event_string == "Escape": + key_name = event.keyval_name or event.event_string + if key_name == "Escape": self.quit(script=None, event=event) return True @@ -404,4 +405,3 @@ def getPresenter(): _presenter = LearnModePresenter(cthulhu.cthulhuApp) return _presenter - diff --git a/src/cthulhu/plugins/ByeCthulhu/plugin.py b/src/cthulhu/plugins/ByeCthulhu/plugin.py index d3174ea..95cb249 100644 --- a/src/cthulhu/plugins/ByeCthulhu/plugin.py +++ b/src/cthulhu/plugins/ByeCthulhu/plugin.py @@ -23,6 +23,7 @@ class ByeCthulhu(Plugin): super().__init__(*args, **kwargs) logger.info("ByeCthulhu plugin initialized") self._signal_handler_id = None + self._is_connected = False @cthulhu_hookimpl def activate(self, plugin=None): @@ -33,13 +34,14 @@ class ByeCthulhu(Plugin): logger.info("Activating ByeCthulhu plugin") try: - # Connect to the stop-application-completed signal - signal_manager = self.app.getSignalManager() - self._signal_handler_id = signal_manager.connectSignal( - "stop-application-completed", - self.process, - "default" # Add profile parameter - ) + if not self._is_connected: + signal_manager = self.app.getSignalManager() + self._signal_handler_id = signal_manager.connectSignal( + "stop-application-completed", + self.process, + "default" # Add profile parameter + ) + self._is_connected = True except Exception as e: logger.error(f"Error activating ByeCthulhu plugin: {e}") @@ -52,14 +54,14 @@ class ByeCthulhu(Plugin): logger.info("Deactivating ByeCthulhu plugin") try: - # Disconnect signal if we have an ID - if self._signal_handler_id is not None: + if self._is_connected and self._signal_handler_id is not None: signal_manager = self.app.getSignalManager() # Use disconnectSignalByFunction instead since disconnectSignal doesn't exist signal_manager.disconnectSignalByFunction( self.process ) self._signal_handler_id = None + self._is_connected = False except Exception as e: logger.error(f"Error deactivating ByeCthulhu plugin: {e}") @@ -72,4 +74,4 @@ class ByeCthulhu(Plugin): state.activeScript.presentationInterrupt() state.activeScript.presentMessage(messages.STOP_CTHULHU, resetStyles=False) except Exception as e: - logger.error(f"Error in ByeCthulhu process: {e}") \ No newline at end of file + logger.error(f"Error in ByeCthulhu process: {e}") diff --git a/src/cthulhu/scripts/default.py b/src/cthulhu/scripts/default.py index ba5c895..9ef5d85 100644 --- a/src/cthulhu/scripts/default.py +++ b/src/cthulhu/scripts/default.py @@ -597,6 +597,10 @@ class Script(script.Script): """ Removes this script's AT-SPI key grabs. """ msg = "DEFAULT: removing key grabs" debug.printMessage(debug.LEVEL_INFO, msg, True) + if cthulhu_state.device is None: + self.grab_ids = [] + self._modifierGrabIds = [] + return for id in self.grab_ids: cthulhu.removeKeyGrab(id) self.grab_ids = [] diff --git a/tests/test_bye_cthulhu_regressions.py b/tests/test_bye_cthulhu_regressions.py new file mode 100644 index 0000000..3a406ab --- /dev/null +++ b/tests/test_bye_cthulhu_regressions.py @@ -0,0 +1,30 @@ +import sys +import unittest +from pathlib import Path +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from cthulhu.plugins.ByeCthulhu.plugin import ByeCthulhu + + +class ByeCthulhuRegressionTests(unittest.TestCase): + def test_activate_only_connects_stop_signal_once(self): + plugin = ByeCthulhu() + signalManager = mock.Mock() + signalManager.connectSignal.return_value = 42 + plugin.app = mock.Mock() + plugin.app.getSignalManager.return_value = signalManager + + plugin.activate(plugin) + plugin.activate(plugin) + + signalManager.connectSignal.assert_called_once_with( + "stop-application-completed", + plugin.process, + "default", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_default_script_shutdown_regressions.py b/tests/test_default_script_shutdown_regressions.py new file mode 100644 index 0000000..aba083d --- /dev/null +++ b/tests/test_default_script_shutdown_regressions.py @@ -0,0 +1,32 @@ +import sys +import unittest +from pathlib import Path +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from cthulhu import cthulhu_state +from cthulhu.scripts import default + + +class DefaultScriptShutdownRegressionTests(unittest.TestCase): + def test_remove_key_grabs_clears_tracking_without_touching_dead_device(self): + script = object.__new__(default.Script) + script.grab_ids = [474, 475] + script._modifierGrabIds = [("Insert", 99)] + + with ( + mock.patch.object(cthulhu_state, "device", None), + mock.patch("cthulhu.scripts.default.cthulhu.removeKeyGrab") as removeKeyGrab, + mock.patch("cthulhu.scripts.default.input_event_manager.get_manager") as getManager, + ): + script.removeKeyGrabs() + + removeKeyGrab.assert_not_called() + getManager.assert_not_called() + self.assertEqual(script.grab_ids, []) + self.assertEqual(script._modifierGrabIds, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_learn_mode_regressions.py b/tests/test_learn_mode_regressions.py new file mode 100644 index 0000000..69336e7 --- /dev/null +++ b/tests/test_learn_mode_regressions.py @@ -0,0 +1,44 @@ +import sys +import unittest +from pathlib import Path +from unittest import mock + +import gi + +gi.require_version("Gdk", "3.0") + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from gi.repository import Gdk + +from cthulhu import cthulhu_state +from cthulhu import input_event +from cthulhu import learn_mode_presenter + + +class LearnModePresenterRegressionTests(unittest.TestCase): + def test_escape_keyval_exits_learn_mode_when_event_string_is_control_character(self): + presenter = learn_mode_presenter.LearnModePresenter(mock.Mock()) + presenter._is_active = True + + keyboardEvent = input_event.KeyboardEvent( + True, + 9, + Gdk.KEY_Escape, + 0, + "\x1b", + ) + + activeScript = mock.Mock() + + with mock.patch.object(cthulhu_state, "activeScript", activeScript): + result = presenter.handle_event(keyboardEvent) + + self.assertTrue(result) + self.assertFalse(presenter.is_active()) + activeScript.speakKeyEvent.assert_called_once_with(keyboardEvent) + activeScript.presentMessage.assert_called_once() + + +if __name__ == "__main__": + unittest.main()