From 513e4749e3d899de70efd528e552504b5503da74 Mon Sep 17 00:00:00 2001 From: Anay Garodia <76836303+AnayGarodia@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:44:49 -0700 Subject: [PATCH] fix(xdotool): preserve input focus after injection (#564) --- src/vocalinux/text_injection/text_injector.py | 17 ++++++++++-- tests/test_text_injector.py | 26 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/vocalinux/text_injection/text_injector.py b/src/vocalinux/text_injection/text_injector.py index 3f5f682..4cef667 100644 --- a/src/vocalinux/text_injection/text_injector.py +++ b/src/vocalinux/text_injection/text_injector.py @@ -1004,10 +1004,23 @@ class TextInjector: logger.error("Text injection timed out on final attempt") raise - # Try to reset any stuck modifiers + # Release modifiers without sending Escape, which applications may + # interpret as a request to cancel or leave the focused input. try: subprocess.run( - ["xdotool", "key", "--clearmodifiers", "Escape"], + [ + "xdotool", + "keyup", + "--clearmodifiers", + "Control_L", + "Control_R", + "Shift_L", + "Shift_R", + "Alt_L", + "Alt_R", + "Super_L", + "Super_R", + ], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, diff --git a/tests/test_text_injector.py b/tests/test_text_injector.py index 707f9c7..888df5a 100644 --- a/tests/test_text_injector.py +++ b/tests/test_text_injector.py @@ -1141,6 +1141,32 @@ class TestTextInjectorEdgeCases(unittest.TestCase): if env_backup is not None: os.environ["DISPLAY"] = env_backup + def test_inject_with_xdotool_releases_modifiers_without_escape(self): + """The xdotool path must keep the target input focused after injection.""" + injector = TextInjector.__new__(TextInjector) + injector.environment = DesktopEnvironment.X11 + + injector._inject_with_xdotool("test") + + commands = [call.args[0] for call in self.mock_subprocess.call_args_list] + self.assertIn( + [ + "xdotool", + "keyup", + "--clearmodifiers", + "Control_L", + "Control_R", + "Shift_L", + "Shift_R", + "Alt_L", + "Alt_R", + "Super_L", + "Super_R", + ], + commands, + ) + self.assertFalse(any("Escape" in command for command in commands)) + def test_inject_with_wayland_tool_ydotool(self): """Test text injection with ydotool.""" with patch.dict("os.environ", {"XDG_SESSION_TYPE": "wayland"}):