Skip to content

fix: map Linux clipboard format names to macOS UTIs (#8) - #9

Open
ihoka wants to merge 1 commit into
standardagents:mainfrom
ihoka:fix/linux-macos-clipboard-format-mapping
Open

ihoka wants to merge 1 commit into
standardagents:mainfrom
ihoka:fix/linux-macos-clipboard-format-mapping

Conversation

@ihoka

@ihoka ihoka commented Sep 4, 2026

Copy link
Copy Markdown

Problem

Fixes #8. Copying on a Linux (Wayland or X11) peer and pasting on macOS does nothing. The macOS daemon receives the clip but fails to apply it: the Linux side's format names — X11 selection atoms (STRING, TEXT, UTF8_STRING) and raw MIME types (text/plain, text/plain;charset=utf-8) — are handed to NSPasteboard verbatim as UTIs. Modern AppKit rejects each one ('STRING' is not a valid UTI string), the pasteboard ends up empty, and the daemon logs native clipboard was empty immediately after publishing.

macOS → Linux already works, because macOS emits public.* UTIs and wl-copy / X11 accept arbitrary MIME and atom strings.

Fix

Receive-side normalization in src/clipboard.rs, applied when publishing to the native clipboard:

  • publish_format(name, target) — for the NSPasteboard backend, maps known X11 atom / MIME names to their UTI equivalents (STRING/TEXT/UTF8_STRING/text/plain*public.utf8-plain-text, text/htmlpublic.html, image/pngpublic.png, …), passes strings that are already valid reverse-DNS UTIs through unchanged, and drops names it cannot represent (rather than letting AppKit log an invalid-UTI error for each). Non-macOS backends (wl-copy, X11) still receive every name verbatim — their current behaviour is correct.
  • generic_publish_entries() — runs the mapping across a clip's non-file representations and collapses the several names that normalize to one UTI into a single pasteboard entry. On a collision it keeps the bytes from the most UTF-8-authoritative source (public.utf8-plain-text > UTF8_STRING / …;charset=utf-8 > text/plain > TEXT > STRING), so non-ASCII text copied from an X11 app — whose STRING target is Latin-1 under ICCCM — is not published as mislabeled UTF-8.
  • A text/uri-list that resolves to no local files is still published verbatim on the non-macOS backends; it is only suppressed when a native file list was published in its place.

Incidental fix along the way: the old apply loop published a second file-format representation (e.g. text/uri-list alongside public.file-url) raw, adding a stale remote-path target next to the correct Files-derived one.

Scope / non-goals

  • Capture/send side and the wire format are untouched.
  • No reverse (UTI → MIME/atom) map for the X11/Wayland backends — the macOS → Linux direction already works.
  • The mapping is byte-preserving by design, so a source that offers only a legacy-encoded flavor (X11 STRING as Latin-1, or a UTF-16 text/html from Firefox on Linux) still lands those bytes under a UTF-8 UTI. Charset transcoding would be a separate change.

Testing

  • 18 new unit tests in src/clipboard.rs covering the mapping table (case-insensitive), UTI pass-through, the drop set, the Passthrough invariant, duplicate collapse with byte-level tie-break, and the uri-list passthrough gate. These run on both CI OS legs since the mapper is not cfg-gated.
  • tests/headless_x11.rs extended with an arbitrary-MIME-name round-trip assertion (Linux passthrough regression guard).
  • Manual pbpaste verification on a real Linux + macOS peer pair — to be added to this PR before it's ready for review.

🤖 Generated with Claude Code

Text and other non-file clips copied on a Linux (Wayland or X11) peer never
reached the macOS pasteboard. The Linux side sends X11 selection-atom names
(STRING, TEXT, UTF8_STRING) and raw MIME types (text/plain, text/html), and the
NSPasteboard backend handed them to AppKit verbatim as UTIs. AppKit rejects
every one, the pasteboard ends up empty, and the daemon logs "native clipboard
was empty immediately after publishing". The reverse direction already works
because macOS emits public.* UTIs and wl-copy/X11 accept arbitrary names.

Normalize representation type names on the receive/apply side:

- publish_format() maps known atom/MIME names to their UTI equivalents for the
  NSPasteboard backend, passes already-valid reverse-DNS UTIs through unchanged,
  and drops names AppKit cannot represent (rather than logging an invalid-UTI
  error for each). Non-macOS backends (wl-copy, X11) still receive every name
  verbatim.
- generic_publish_entries() applies the mapping across a clip and collapses the
  several names that normalize to one UTI into a single pasteboard entry. When
  they collide it keeps the bytes from the most UTF-8-authoritative source, so
  non-ASCII text from an X11 app whose STRING target is Latin-1 is not published
  as mislabeled UTF-8.
- A text/uri-list that resolves to no local files is still published verbatim on
  the non-macOS backends; it is only suppressed when a native file list was
  published in its place.

Also fixes an incidental bug: the old apply loop published a second file-format
representation (e.g. text/uri-list alongside public.file-url) raw, adding a
stale remote-path target next to the correct Files-derived one.

Capture/send and the wire format are untouched. Known limitation, left for a
follow-up: the mapping is byte-preserving by design, so a source that offers
only a legacy-encoded flavor (X11 STRING as Latin-1, or a UTF-16 text/html)
still lands those bytes under a UTF-8 UTI.

Fixes standardagents#8

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qd7JneqSJWFAbRHauiHDbR
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.

Linux → macOS: text clips never reach NSPasteboard (X11/Wayland format names sent as UTIs)

1 participant