Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions app/compose_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,15 +878,16 @@ def _on_paste_clicked(self) -> None:
text = self.txtOutput.toPlainText()
if not text.strip():
return
self.close()
try:
self._paste_service.paste(text, force_autopaste=True)
except PasteServiceError as exc:
self._show_status(t("compose.status.error").format(message=str(exc)), error=True)
return
except Exception as exc: # pragma: no cover - defensive
try:
self._paste_service.paste(text, force_autopaste=True, restore_clipboard=False)
except TypeError:
self._paste_service.paste(text, force_autopaste=True)
except (PasteServiceError, Exception) as exc: # pragma: no cover - defensive
logger.error("Failed to paste from compose window: %s", exc)
self.show()
self._show_status(t("compose.status.error").format(message=str(exc)), error=True)
return
self.close()

def closeEvent(self, event: QCloseEvent) -> None:
# A hidden modeless window must never remain an implicit routing target.
Expand Down
28 changes: 21 additions & 7 deletions app/paste_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,19 @@ def __init__(self, autopaste: bool = True, key_delay_ms: int = _KEY_DELAY_MS) ->
self.key_delay_ms = max(0, int(key_delay_ms))
self._ydotool_uses_legacy_keys: Optional[bool] = None

def paste(self, text: str, force_autopaste: Optional[bool] = None) -> None:
def paste(
self,
text: str,
force_autopaste: Optional[bool] = None,
restore_clipboard: bool = True,
) -> None:
"""Text ins Clipboard schreiben und optional einfuegen.

Args:
text: Der einzufuegende Text.
force_autopaste: Optionaler Override fuer den Auto-Paste-Schritt.
restore_clipboard: Ob nach erfolgreichem Auto-Paste der urspruengliche
Clipboard-Inhalt wiederhergestellt werden soll.

Raises:
PasteServiceError: Wenn wl-copy nicht gefunden oder hart fehlschlaegt.
Expand All @@ -140,14 +147,15 @@ def paste(self, text: str, force_autopaste: Optional[bool] = None) -> None:
do_autopaste = False

logger.info("Paste request: %d chars, autopaste=%s", len(text), do_autopaste)
previous_clipboard = self._read_clipboard() if do_autopaste else None
previous_clipboard = self._read_clipboard() if (do_autopaste and restore_clipboard) else None
self._copy_to_clipboard(text)

if do_autopaste and self._ydotool_paste():
# Pause, damit die Ziel-App den eingefuegten Text uebernehmen kann
time.sleep(_RESTORE_DELAY)
self._cleanup_copyq(text)
self._restore_clipboard(previous_clipboard)
if restore_clipboard:
time.sleep(_RESTORE_DELAY)
self._cleanup_copyq(text)
self._restore_clipboard(previous_clipboard)

def clipboard_only(self, text: str) -> None:
"""Nur Clipboard, kein ydotool -- fuer Faelle wo Auto-Paste unterwuenscht."""
Expand Down Expand Up @@ -196,8 +204,14 @@ def _read_clipboard(self) -> Optional[str]:
try:
stdout, _ = process.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
process.kill()
process.communicate()
try:
process.kill()
except OSError:
pass
try:
process.communicate(timeout=0.2)
except (subprocess.TimeoutExpired, OSError, ValueError):
pass
return None
if process.returncode != 0:
return None
Expand Down
15 changes: 15 additions & 0 deletions tests/test_paste_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,21 @@ def test_paste_restores_empty_previous_clipboard_and_cleans_up_copyq(self):
restore_mock.assert_called_once_with("")
cleanup_mock.assert_called_once_with("neu")

def test_paste_does_not_read_or_restore_when_restore_clipboard_false(self):
service = PasteService(autopaste=True)
with patch("app.paste_service.shutil.which", return_value="/usr/bin/ydotool"):
with patch.object(service, "_read_clipboard") as read_mock:
with patch.object(service, "_copy_to_clipboard") as copy_mock:
with patch.object(service, "_ydotool_paste", return_value=True) as paste_mock:
with patch.object(service, "_restore_clipboard") as restore_mock:
with patch.object(service, "_cleanup_copyq") as cleanup_mock:
service.paste("neu", force_autopaste=True, restore_clipboard=False)
read_mock.assert_not_called()
copy_mock.assert_called_once_with("neu")
paste_mock.assert_called_once_with()
restore_mock.assert_not_called()
cleanup_mock.assert_not_called()

def test_clipboard_only_does_not_restore_or_cleanup(self):
service = PasteService(autopaste=True)
with patch.object(service, "_read_clipboard") as read_mock:
Expand Down
Loading