Skip to content

feat(clipboard): add rich clipboard module with macOS backend#211

Open
kdroidFilter wants to merge 8 commits intomainfrom
feat/clipboard-module
Open

feat(clipboard): add rich clipboard module with macOS backend#211
kdroidFilter wants to merge 8 commits intomainfrom
feat/clipboard-module

Conversation

@kdroidFilter
Copy link
Copy Markdown
Collaborator

Summary

  • clipboard-common — suspend Clipboard façade with reads/writes for text, HTML, RTF, PNG images and file lists; write { } DSL for atomic multi-format publication; watch(): Flow<ClipboardEvent> reactive watcher. Backends discovered via ServiceLoader, no-op fallback when none are on the classpath.
  • clipboard-macosNSPasteboard JNI backend. Single NSPasteboardItem for multi-format writes, PNG + TIFF dual publication for web/AppKit compatibility, modern NSURL reads with legacy NSFilenamesPboardType fallback, HTML BOM stripping, NSPasteboard.accessBehavior guarded via respondsToSelector: (macOS 15.4+).
  • macOS 15.4+ privacy — watcher and availableFormats() never read payload bytes, so they stay outside the scope of the new pasteboard-privacy prompt. AccessBehavior API exposes the new policy.
  • Example app — new Clipboard tab with live watcher, multi-format read/write, image preview, file list, and event log.
  • Tabs refactor — the flat tab row is grouped into 5 parent tabs (Home / UI / Notifications / Launcher / System) with custom Popup-based dropdowns styled to match the existing chip tabs (no Material DropdownMenu).
  • Docsdocs/runtime/clipboard-common.md + docs/runtime/clipboard-macos.md, mkdocs.yml nav, docs/runtime/index.md, docs/llms.txt, docs/roadmap.md, and README.md all updated.

Test plan

  • ./gradlew :clipboard-common:compileKotlin :clipboard-macos:compileKotlin :example:compileKotlin — passes on JDK 21
  • ./gradlew :clipboard-common:detekt :clipboard-common:ktlintCheck :clipboard-macos:detekt :clipboard-macos:ktlintCheck :example:ktlintCheck — all green
  • Native build.sh produces both darwin-aarch64 and darwin-x64 dylibs exporting all 11 Java_... symbols
  • mkdocs build processes both new pages without warnings
  • Manual: ./gradlew run → switch to System → Clipboard, exercise each read/write button, observe the live watcher firing on external copies
  • CI: add macOS build matrix entry in build-natives.yaml and verify/download steps in the 6 consumer workflows before publishing (follow-up task)

- clipboard-common: Clipboard façade with suspend read/write for text, HTML,
  RTF, images (PNG bytes) and file lists; ClipboardWriteScope DSL for atomic
  multi-format writes; Flow<ClipboardEvent> watcher (metadata only, safe under
  macOS 15.4+ pasteboard privacy); ServiceLoader-based backend SPI with
  NoOpBackend fallback.
- clipboard-macos: NSPasteboard JNI backend. Single NSPasteboardItem for
  multi-format writes, PNG + TIFF on images for web/AppKit compatibility,
  modern NSURL reads with legacy NSFilenamesPboardType fallback, HTML BOM
  stripping, NSPasteboard.accessBehavior guarded via respondsToSelector.
- Example: new Clipboard tab with live watcher + multi-format read/write.
- Docs: runtime/clipboard-common.md + runtime/clipboard-macos.md, mkdocs nav,
  llms.txt, roadmap, README updated.
- UI: parent tabs grouped with per-group dropdown menus (custom Popup styled
  to match the tab chips, no Material DropdownMenu) to tame the flat tab row.
- Clipboard.accessBehavior: AccessBehavior? reads NSPasteboard.accessBehavior,
  null when the host OS has no privacy model.
- Clipboard.isAccessBehaviorSupported gates UI based on respondsToSelector:
  probing for both getter and setter.
- Native nativeGetAccessBehavior + nativeIsAccessBehaviorSupported in the JNI
  bridge; both guarded via NSInvocation + respondsToSelector.
- Example Clipboard tab: new "Privacy — macOS 15.4+" card showing the current
  policy with chips for the three values; disabled chips + explanatory copy on
  older macOS / other OSes.
- Docs updated (runtime/clipboard-common.md, runtime/clipboard-macos.md).
…elector

The respondsToSelector: probe for -accessBehavior / -setAccessBehavior: was
returning NO on macOS Tahoe under the unsigned JVM, causing the example's
Privacy card to claim the API was unsupported even though the runtime does
expose it. Switch to [NSProcessInfo isOperatingSystemAtLeastVersion:] for the
support flag — more reliable, independent of ObjC metadata visibility. The
selector probes on get/set remain as a safety net.
Implement full clipboard support for Linux across both X11 (XCB + XFixes)
and Wayland (wl-clipboard delegation). X11 backend handles format negotiation,
INCR protocol for large transfers, change detection via XFixes. Wayland
delegates to wl-copy/wl-paste with atomic format selection. Platform
detection and fallback logic ensures compatibility across session types,
including Xwayland on Wayland hosts.
- Add MIME type negotiation: try image/png then jpeg/webp/bmp/gif/tiff
- Fix stream-drain race in runCaptureBytes: join reader thread fully after
  process exit (was capped at 200ms, could truncate large images)
- Add fallback: read image files directly from disk when clipboard contains
  file URIs (Nautilus copy on .png files)
- Bump image read timeout to 5 seconds (screenshots may take time)
- Demo: display image thumbnails inline in the event log when images are
  copied or read
- Add WaylandImageSmokeTest integration test
X11 ICCCM compliance (#4/#5):
- SetSelectionOwner now uses real server timestamp via PropertyNotify probe,
  not XCB_CURRENT_TIME (violates ICCCM §2.1). Added get_server_timestamp_locked()
  which fires a zero-byte ChangeProperty to trigger timestamp event.
- TIMESTAMP replies now return g_own_ts (real value) instead of truncated 0.
- Verified: xclip -o -t TIMESTAMP returns non-zero after our clipboard write.

INCR cleanup (#3):
- On INCR read timeout, delete property to unblock sender waiting for
  PropertyNotify=Delete (ICCCM compliant termination).

Process lifecycle (#7):
- Wayland: runCaptureBytes, runSilently, writeBytes now escalate to
  destroyForcibly() if SIGTERM doesn't terminate after 500ms grace.

AccessBehavior mapping (#12):
- Kotlin: explicit when() mapping (0→AlwaysAllow, 1→AskEveryTime, 2→AlwaysDeny)
  instead of ordinal/entries.getOrNull (fragile with future macOS versions).
- ObjC: validate input 0..2 on set; return -1 if get() returns out-of-range.

Documentation & robustness (#13, #1):
- Clipboard.watch() doc: clarify poll interval is always honored; source of
  counter differs by backend (Mach IPC / XFixes / wl-paste).
- Re-check isActive after slow availableFormats() to avoid emitting to
  cancelled flow.

Added X11TimestampSmokeTest to verify real timestamps are used.
Implements full Win32 clipboard support via JNI (user32, gdi32, gdiplus):
- Reads/writes: text (CF_UNICODETEXT), HTML (CF_HTML wrapper), RTF,
  images (PNG + CF_DIBV5 with Chromium alpha sanitization), file lists (CF_HDROP)
- Change detection via GetClipboardSequenceNumber (no message pump needed)
- Open-clipboard retry loop (5 × 10ms) for rdpclip.exe contention
- GDI+ transcoding: PNG ↔ DIB via synthetic BMP-in-memory
- x64 + ARM64 DLLs built via MSVC (build.bat)
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