Skip to content

Replace the whole clipboard on a terminal copy and drop the text snapshot - #687

Merged
coneilen merged 2 commits into
mainfrom
coneilen-investigate-clipboard-format-preservatio
Oct 10, 2026
Merged

coneilen merged 2 commits into
mainfrom
coneilen-investigate-clipboard-format-preservatio

Conversation

@coneilen

@coneilen coneilen commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A terminal copy on Windows now replaces the whole clipboard with plain Unicode text and no longer reads the previous clipboard text first. The new text is staged in a clipboard block before the clipboard is opened, so running out of memory cannot happen after the clipboard has been emptied.

The previous behavior (#684) snapshotted CF_UNICODETEXT before EmptyClipboard and put it back if the publish failed. An investigation (probes run on a private window station, never the interactive clipboard) found that this costs more than it protects:

  • GetClipboardData on a delayed-render owner has no timeout and renders on the caller's thread while the clipboard lock is held (1.5 s in a synthetic same-thread owner), on every copy, to protect data the user is about to discard.
  • Restoring only the text drops the history and cloud opt-out formats (ExcludeClipboardContentFromMonitorProcessing, CanIncludeInClipboardHistory, CanUploadToCloudClipboard), so a failed copy could republish text that was excluded from history and sync.
  • Preserving or restoring the other formats (replace only the text formats without emptying, snapshot every format, or the OLE clipboard) was evaluated and rejected: keeping other formats gives a stale mixed clipboard, a snapshot forces rendering and cannot copy GDI handles, and the OLE clipboard cannot serve plain Win32 sources after the swap.

macOS does clearContents then setString, so replacing every format matches it.

Changes

  • Clipboard.zig: remove the snapshot, the restore and the "previous text unreadable" refusal. writeTextWith stages the block first (stage), opens and empties the clipboard, then publishes. The caller owns the block until publish succeeds and frees it exactly once on every failure path; after success Windows owns it.
  • App.zig: terminalCopyFailureStatus drops the kept, lost and unreadable wording. A refused publish says the clipboard may now be empty; a staging failure says the clipboard was left unchanged.
  • Tests: the stand-in clipboard counts opens, empties, publishes, closes and frees and tracks which staged blocks the caller still owns. New tests: staging failure makes zero open, empty and publish calls; the text is staged before the clipboard is opened; open failure, empty failure and publish failure each free the block exactly once; publish success never frees. The stand-in also enforces Win32 call order (empty fails unless the clipboard is open, publish fails unless it is open and emptied, closing and reopening reset ownership), with tests that a wrong-order call fails and that a copy which skips emptying is a write failure that frees its block. Removed the tests of the deleted snapshot and restore behavior.
  • README and investigation/ui-parity-matrix.md: a successful copy replaces every format; a refused publish can leave the clipboard empty; RDP and Dev Box copy and paste (rdpclip) is not verified. The ledger row stays Partial.

Test plan

RED: zig test src\Clipboard.zig (pinned 0.15.2, new staging and ownership tests run against the old snapshot implementation) -> 16 passed; 6 failed, e.g. expected error.OutOfMemory, found void; frees expected 1, found 0; stages expected 1, found 0; ClipboardPreviousTextUnreadable on a copy over unreadable old text. The tests compiled and ran, so this is a behavioral failure, not a compile error.
GREEN: zig test src\Clipboard.zig (same flags and tests, new implementation) -> All 16 tests passed (final head, including the stand-in ordering tests added after review).
REGRESSION: zig test src\App.zig with --test-filter "terminal copy" (full link flags) -> All 3 tests passed, including the live terminal copy test that drives a refused write through a stand-in.

Baseline before the change was 15 passed for src\Clipboard.zig (14 of its own plus the imported Win32 test); it is 16 now (15 plus the imported test) because the snapshot and restore tests were removed and the new ownership and ordering tests added.

Not tested: the real Windows clipboard. No test or probe in this change touches it; NativeApi.stage, publish and free (GlobalAlloc, GlobalLock, SetClipboardData, GlobalFree) compile but are not executed. Remote Desktop and Dev Box copy and paste through rdpclip is not verified, and neither is Clipboard History or cloud sync behavior. Paste still calls GetClipboardData and keeps its delayed-render exposure; that is a separate, acknowledged limitation.

Checklist

  • I have read the Contributing Guidelines
  • I have signed off my commits (git commit -s) per the DCO
  • Tests pass locally (make test)
  • Code follows the existing style (make check)
  • I added the test/contract before the implementation and observed the intended RED failure

The two unchecked boxes are the macOS targets, which this Windows-only change does not build. The Windows Zig tests above were run locally instead.

coneilen and others added 2 commits October 10, 2026 09:18
…shot

Stage the new text in a clipboard block before the clipboard is opened and
free it exactly once on every failure, so running out of memory can no longer
happen after the clipboard is emptied. Remove the pre-empty CF_UNICODETEXT
snapshot and restore: reading another program's delayed-render text has no
timeout and can hang the UI thread, and restoring only the text would drop the
history and cloud opt-out formats. A refused publish is now an ordinary write
failure whose status says the clipboard may be empty. Document that a copy
replaces every format, as on macOS, and that RDP copy and paste is unverified.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Colin Neilens <coneilen@microsoft.com>
Empty fails unless the clipboard is open, publish fails unless it is open and emptied, and open and close reset ownership. Adds tests that a wrong-order call fails and that a copy which skips emptying is a write failure that frees its block.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Colin Neilens <coneilen@microsoft.com>
@coneilen
coneilen merged commit 8556c0a into main Oct 10, 2026
25 checks passed
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