Skip to content

PasteService-Hardening: Terminal-Paste, Clipboard-Restore, CopyQ-Cleanup - #38

Merged
TimInTech merged 5 commits into
mainfrom
feat/paste-service-hardening
Jul 1, 2026
Merged

PasteService-Hardening: Terminal-Paste, Clipboard-Restore, CopyQ-Cleanup#38
TimInTech merged 5 commits into
mainfrom
feat/paste-service-hardening

Conversation

@TimInTech

Copy link
Copy Markdown
Owner

Summary

  • Detect the active window and send Ctrl+Shift+V in known terminal emulators instead of Ctrl+V, which is usually a no-op or copy there (X11 only, clean fallback on Wayland/missing xdotool).
  • Save the clipboard content before autopaste and restore it afterwards, so dictation no longer clobbers whatever the user had copied.
  • Opportunistically remove the transient pasted text from CopyQ's history (if CopyQ is installed) so it doesn't clutter the clipboard manager.

Implements the roadmap item "PasteService-Hardening — Clipboard-Restore + Terminal-Paste" (Project #10), in three independently reviewed commits.

Test plan

  • pytest tests/test_paste_service.py tests/test_state_machine.py -v — all green
  • pytest -q (full suite) — 356 passed, 70 skipped, 0 failed
  • Manual smoke test on a real X11/Wayland session: dictate into a terminal (Ctrl+Shift+V fires) and into a GUI app (Ctrl+V fires, clipboard restored afterwards)

TimInTech added 4 commits July 2, 2026 00:00
xdotool-based active-window detection lets PasteService send Ctrl+Shift+V
in known terminal emulators instead of Ctrl+V, which is usually a no-op
or copy there. X11-only, falls back cleanly on Wayland/missing xdotool.

First step of the PasteService-Hardening roadmap item (clipboard-restore
and CopyQ-cleanup follow separately).
Reads the previous clipboard content before writing the transcribed
text, then restores it once ydotool has delivered the paste keystrokes.
Wayland (wl-paste) and X11 (xclip -o) are supported; restore is
best-effort and never blocks the main paste path on failure.

Only applies to the autopaste flow — clipboard_only() leaves the
transcribed text in the clipboard for manual paste, as intended.

Second step of the PasteService-Hardening roadmap item (CopyQ cleanup
follows separately).
Popen() without check=True never raises CalledProcessError, so that
branch in _read_clipboard() was dead. Also documents why paste() and
_ydotool_paste() each sleep _PASTE_DELAY for a different reason, so a
future reader doesn't mistake it for an accidental double wait.
After a successful autopaste-and-restore cycle, remove the transient
pasted text from CopyQ's clipboard history if CopyQ is installed and
its top entry matches exactly (trailing newline tolerant). No hard
CopyQ dependency: missing binary, timeouts, and other errors are
swallowed and never affect the main paste path.

Third and final step of the PasteService-Hardening roadmap item.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f95e72eeb6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/paste_service.py
Comment on lines +147 to +148
self._restore_clipboard(previous_clipboard)
self._cleanup_copyq(text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clean CopyQ before restoring the old clipboard

When CopyQ is running and there was previous clipboard text, _restore_clipboard() writes that old value back first, which CopyQ records or moves to history index 0. _cleanup_copyq() then only reads/removes index 0, so it sees the restored value instead of the newly pasted text and leaves the transcript in CopyQ history; clean the pasted entry before restoring, or locate the matching history item after restore.

Useful? React with 👍 / 👎.

run_mock.side_effect = side_effect
svc.paste("hallo welt", force_autopaste=True)
assert run_mock.call_count == 2
assert run_mock.call_count == 3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mock the saved clipboard before expecting restore

With this test's mocks, _read_clipboard() is not mocked and the real wl-paste/xclip -o probe commonly returns None in headless CI; then _restore_clipboard(None) is a no-op and subprocess.run is only called for the clipboard write and ydotool (I reproduced 2 calls with the same mocks). This assertion is therefore host-dependent unless the test stubs _read_clipboard to return saved text before expecting the restore call.

Useful? React with 👍 / 👎.

test_force_autopaste_override_enables_ydotool asserted call_count==3
but only mocked subprocess.run, not the subprocess.Popen used by
_read_clipboard(). On a desktop with wl-copy/xclip actually installed
this reads the real clipboard and the assertion holds; in CI, where
those binaries are absent, _read_clipboard() returns None and the
restore step never fires a subprocess.run call, dropping the count to
2 and failing the build.

Mock _read_clipboard() directly so the test no longer depends on
whichever clipboard tooling happens to be present on the runner.
@TimInTech
TimInTech merged commit 34ca51a into main Jul 1, 2026
7 checks passed
@TimInTech
TimInTech deleted the feat/paste-service-hardening branch July 1, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant