Fix X11 key injection modifier release handling

This commit is contained in:
Thales Maciel 2026-02-27 15:22:54 -03:00
parent 993f51712b
commit 98b13d1069
2 changed files with 54 additions and 5 deletions

View file

@ -437,11 +437,17 @@ class X11Adapter:
if keycode == 0: if keycode == 0:
continue continue
keycodes.append(keycode) keycodes.append(keycode)
for code in keycodes: pressed: list[int] = []
xtest.fake_input(dpy, X.KeyPress, code) try:
for code in reversed(keycodes): for code in keycodes:
xtest.fake_input(dpy, X.KeyRelease, code) xtest.fake_input(dpy, X.KeyPress, code)
dpy.flush() pressed.append(code)
finally:
for code in reversed(pressed):
xtest.fake_input(dpy, X.KeyRelease, code)
# Ensure all synthetic key events are processed before the display
# may be closed by callers.
dpy.sync()
def _keysym_for_char(self, ch: str) -> tuple[int | None, bool]: def _keysym_for_char(self, ch: str) -> tuple[int | None, bool]:
if ch.isupper(): if ch.isupper():

View file

@ -65,6 +65,49 @@ class DesktopX11Tests(unittest.TestCase):
display_obj.close.assert_called_once_with() display_obj.close.assert_called_once_with()
@patch("desktop_x11.xtest.fake_input")
def test_send_combo_releases_keys_and_syncs(self, fake_input):
adapter = self._adapter()
dpy = MagicMock()
dpy.keysym_to_keycode.side_effect = [10, 20, 30]
adapter._send_combo(dpy, ["Control_L", "Shift_L", "v"])
expected_calls = [
((dpy, 2, 10),),
((dpy, 2, 20),),
((dpy, 2, 30),),
((dpy, 3, 30),),
((dpy, 3, 20),),
((dpy, 3, 10),),
]
self.assertEqual(fake_input.call_args_list, expected_calls)
dpy.sync.assert_called_once_with()
@patch("desktop_x11.xtest.fake_input")
def test_send_combo_releases_pressed_keys_when_press_fails(self, fake_input):
adapter = self._adapter()
dpy = MagicMock()
dpy.keysym_to_keycode.side_effect = [10, 20]
def _fake_input(_dpy, event_type, code):
if event_type == 2 and code == 20:
raise RuntimeError("press failed")
return None
fake_input.side_effect = _fake_input
with self.assertRaisesRegex(RuntimeError, "press failed"):
adapter._send_combo(dpy, ["Control_L", "v"])
expected_calls = [
((dpy, 2, 10),),
((dpy, 2, 20),),
((dpy, 3, 10),),
]
self.assertEqual(fake_input.call_args_list, expected_calls)
dpy.sync.assert_called_once_with()
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()