Skip to content

feat(x11): opt-in capture-time X11 takeover so XWayland can paste images - #75

Merged
davidboulay merged 1 commit into
mainfrom
feat/x11-image-takeover-optin
Jul 31, 2026
Merged

feat(x11): opt-in capture-time X11 takeover so XWayland can paste images#75
davidboulay merged 1 commit into
mainfrom
feat/x11-image-takeover-optin

Conversation

@davidboulay

Copy link
Copy Markdown
Owner

No version bump. This wants testing on a real copy before it ships.

The gap

An image copied on Wayland can't be pasted into an XWayland app — Claude Desktop runs --ozone-platform=x11. Xwayland exports text selections to X11 but never image ones, and a wlr-data-control clip (what CosmicShot's wl-copy writes) never reaches X11 at all. So those apps aren't reading it slowly; they have nothing to read.

Recovering the same clip from the Clippy panel does paste — because that path makes our helper the X11 selection owner and it serves the bytes itself.

Why this was previously considered dead

_MIRROR_ON_CAPTURE existed to do exactly that at capture time. Shipped in 1.4.21, reverted in 1.4.22: cosmic-comp copies our X11 offer into the Wayland selection, Xwayland re-takes X11 ~35s later to proxy it, Xwayland can't serve image data, and both channels end up advertising an offer nothing backs. The comment concluded owning X11 was itself the problem.

What changed: that conclusion doesn't hold

Re-measured with the GTK consumer probe (not xclip, which lies about this):

t+8s   image/png: 22211  0.0s  decodes: true
t+46s  image/png: 22211  0.0s  decodes: true
t+85s  image/png: 22211  0.0s  decodes: true

The recover path publishes through the same helper and does not decay. So owning X11 isn't what breaks it. The difference is that a recover displaces the previous owner, while a capture-time grab races an app still holding the Wayland selection — two authorities for one selection, the same failure class the CosmicShot side deliberately avoided.

That's worth retesting rather than presuming dead.

The change

_MIRROR_ON_CAPTURE becomes the x11_image_takeover setting — default off, images only, still behind the existing _mirror_allowed() rate cap, still short-circuiting on our own echo by content hash.

// ~/.config/clippy/settings.json
"x11_image_takeover": true

Restart the daemon, copy an image in CosmicShot, paste into Claude Desktop.

Failure mode if the hypothesis is wrong: both channels go empty a few seconds after any image copy — clipboard dead system-wide, not just in XWayland apps. Turn it back off. That risk is why it's opt-in rather than a default.

Tests

scripts/x11_takeover_gate_test.py (new, in CI) pins what must not drift: ships off; off publishes nothing and still releases; on publishes and does not release; our own echo short-circuits before either path; the rate cap bounds republishes.

Mutation-checked — flipping the default to True, and ignoring the setting, each fail it:

mutation A: x11_image_takeover must default to False — True
mutation B: off must not publish — ['image/png']

Full suite green locally (8 scripts).

No version bump — this needs testing on a real copy before it ships.

An image copied on Wayland cannot be pasted into an XWayland app (Claude Desktop
runs --ozone-platform=x11). Xwayland exports text selections to X11 but never
image ones, and a wlr-data-control clip — what CosmicShot's wl-copy writes —
never reaches X11 at all, so those apps have nothing to read.

Recovering the same clip from the panel already works, because that path makes
our helper the selection owner. The disabled `_MIRROR_ON_CAPTURE` constant
existed to do the same at capture time; it shipped in 1.4.21 and was reverted in
1.4.22 after both channels were measured dead ~35s after a copy, and the comment
concluded that owning X11 was itself the problem.

Measured since, with the GTK consumer probe rather than xclip: the recover path
does **not** decay. 22211 bytes fetched in 0.0s at t+8s, t+46s and t+85s,
decoding fine each time, helper holding throughout. Owning X11 is therefore not
what breaks. The difference is that a recover displaces the previous owner,
while a capture-time grab races an app that still holds the Wayland selection —
two authorities for one selection, the same failure class the CosmicShot side
concluded not to create.

That makes capture-time takeover worth retesting rather than presuming dead, so
the constant becomes the `x11_image_takeover` setting: default off, images only,
still behind the existing `_mirror_allowed()` rate cap, still short-circuiting
on our own echo by content hash.

It is a hypothesis until someone copies from a real app with it on. If it
regresses, the symptom is both channels going empty a few seconds after any
image copy — turn it back off.

scripts/x11_takeover_gate_test.py (new, in CI) pins the parts that must not
drift: ships off, off publishes nothing and still releases, on publishes and
does not release, our own echo short-circuits before either, and the rate cap
bounds republishes. Mutation-checked — flipping the default to True, or ignoring
the setting, each fail it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@davidboulay
davidboulay merged commit 36b4e52 into main Jul 31, 2026
1 check 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