diff --git a/src/desktop_x11.py b/src/desktop_x11.py index b19f6e4..ba4bda6 100644 --- a/src/desktop_x11.py +++ b/src/desktop_x11.py @@ -258,23 +258,35 @@ class X11Adapter: def _paste_clipboard(self) -> None: dpy = display.Display() - self._send_combo(dpy, ["Control_L", "Shift_L", "v"]) + try: + self._send_combo(dpy, ["Control_L", "Shift_L", "v"]) + finally: + try: + dpy.close() + except Exception: + pass def _type_text(self, text: str) -> None: if not text: return dpy = display.Display() - for ch in text: - if ch == "\n": - self._send_combo(dpy, ["Return"]) - continue - keysym, needs_shift = self._keysym_for_char(ch) - if keysym is None: - continue - if needs_shift: - self._send_combo(dpy, ["Shift_L", keysym], already_keysym=True) - else: - self._send_combo(dpy, [keysym], already_keysym=True) + try: + for ch in text: + if ch == "\n": + self._send_combo(dpy, ["Return"]) + continue + keysym, needs_shift = self._keysym_for_char(ch) + if keysym is None: + continue + if needs_shift: + self._send_combo(dpy, ["Shift_L", keysym], already_keysym=True) + else: + self._send_combo(dpy, [keysym], already_keysym=True) + finally: + try: + dpy.close() + except Exception: + pass def _send_combo(self, dpy: display.Display, keys: Iterable[str | int], already_keysym: bool = False) -> None: keycodes: list[int] = [] diff --git a/tests/test_desktop_x11.py b/tests/test_desktop_x11.py new file mode 100644 index 0000000..7bf11c0 --- /dev/null +++ b/tests/test_desktop_x11.py @@ -0,0 +1,70 @@ +import sys +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "src" +if str(SRC) not in sys.path: + sys.path.insert(0, str(SRC)) + +from desktop_x11 import X11Adapter + + +class DesktopX11Tests(unittest.TestCase): + def _adapter(self) -> X11Adapter: + return object.__new__(X11Adapter) + + @patch("desktop_x11.display.Display") + def test_paste_clipboard_closes_display_on_success(self, display_cls): + adapter = self._adapter() + display_obj = MagicMock() + display_cls.return_value = display_obj + adapter._send_combo = MagicMock() + + adapter._paste_clipboard() + + adapter._send_combo.assert_called_once_with(display_obj, ["Control_L", "Shift_L", "v"]) + display_obj.close.assert_called_once_with() + + @patch("desktop_x11.display.Display") + def test_paste_clipboard_closes_display_on_send_error(self, display_cls): + adapter = self._adapter() + display_obj = MagicMock() + display_cls.return_value = display_obj + adapter._send_combo = MagicMock(side_effect=RuntimeError("boom")) + + with self.assertRaisesRegex(RuntimeError, "boom"): + adapter._paste_clipboard() + + display_obj.close.assert_called_once_with() + + @patch("desktop_x11.display.Display") + def test_type_text_closes_display_on_success(self, display_cls): + adapter = self._adapter() + display_obj = MagicMock() + display_cls.return_value = display_obj + adapter._send_combo = MagicMock() + adapter._keysym_for_char = MagicMock(return_value=(42, False)) + + adapter._type_text("a") + + adapter._send_combo.assert_called_once_with(display_obj, [42], already_keysym=True) + display_obj.close.assert_called_once_with() + + @patch("desktop_x11.display.Display") + def test_type_text_closes_display_on_send_error(self, display_cls): + adapter = self._adapter() + display_obj = MagicMock() + display_cls.return_value = display_obj + adapter._send_combo = MagicMock(side_effect=RuntimeError("boom")) + adapter._keysym_for_char = MagicMock(return_value=(42, False)) + + with self.assertRaisesRegex(RuntimeError, "boom"): + adapter._type_text("a") + + display_obj.close.assert_called_once_with() + + +if __name__ == "__main__": + unittest.main()