Skip to content

feat(clipboard): expose clipboard text to JavaScript - #121

Merged
remorses merged 2 commits into
remorses:mainfrom
Cyberistic:feat/clipboard
Oct 31, 2026
Merged

remorses merged 2 commits into
remorses:mainfrom
Cyberistic:feat/clipboard

Conversation

@Cyberistic

@Cyberistic Cyberistic commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What

Apps can now read and write the OS clipboard. The runtime already had logic for
copy/paste inside native elements.

  • GpuixRenderer.writeToClipboard(text: string): void
  • GpuixRenderer.readFromClipboard(): string | null
  • copyText(text: string): void exported from @gpuix/react

here is some AI slop:

Why sync

The issue asked for an async read if macOS reads asynchronously. It does not in
this submodule:

  • crates/gpui/src/platform.rs:326 — Platform::read_from_clipboard_async
    defaults to Task::ready(Ok(self.read_from_clipboard())); the only override
    in the tree is gpui_web.
  • crates/gpui_macos/src/platform.rs:1347 — MacPlatform::read_from_clipboard
    reads NSPasteboard under a mutex, with no main-thread hop and no Task.
  • custom_elements/input.rs paste already reads this way.

So a Promise would fake an async read that does not exist, and blocking on a
Task from the app thread would be worse. The napi call is synchronous:

  • macOS runs it on the app thread through GPUI_APP (with_app).
  • Windows/Linux/FreeBSD post a UiCommand and wait on a sync_channel for
    the UI thread's answer, the same pattern cycleDebugFrameOverlay() uses. The
    UI thread never blocks; only the JS thread waits, with the existing 2 s
    timeout.
  • Terminal writes OSC 52 and keeps a local copy; there is no clipboard read
    protocol, so a read returns only text this app wrote.
  • Browser has no binding (out of scope), and copyText throws.

Semantics

  • Text only: ClipboardItem::new_string. Metadata/multi-entry is not exposed.
  • An empty string is a real entry, not a no-op, and reads back as ''.
  • readFromClipboard() returns null for an empty clipboard, image-only, or
    file-only content. A file list is never flattened into paths.
  • Reading preserves an empty string. ClipboardItem::text() returns None for
    empty text, so crate::clipboard::text joins string entries itself.
  • input.rs's paste helper moved to crate::clipboard::paste_text, so paste
    and the binding share one definition and one test module.

The runtime already owned the OS clipboard for copy and paste, but an app had
no way to call it. Add `writeToClipboard(text)` and `readFromClipboard()` to
`GpuixRenderer`, and `copyText(text)` to `@gpuix/react`.

Both calls are synchronous. Every desktop GPUI platform reads and writes the
platform clipboard directly (`Platform::read_from_clipboard_async` is only
overridden by the browser), so a promise would be a fake of an async read that
does not exist. On Windows and Linux the call runs on the UI thread and only
the JavaScript thread waits, the same path `cycleDebugFrameOverlay()` uses.

Text only. An empty string is a real entry, so `copyText('')` clears the
clipboard instead of no-opping, and reads back as `''`. `readFromClipboard()`
returns null when the clipboard holds no text, and a file list never becomes
paths.

Reading keeps an empty string, which `ClipboardItem::text()` cannot express.
`input.rs`'s paste helper moves to `crate::clipboard` so paste and the binding
share one definition.

The test renderer wraps `VisualTestPlatform`, whose clipboard is in memory, so
the tests never touch the system clipboard.
@Cyberistic
Cyberistic marked this pull request as ready for review October 25, 2026 04:20
`docs/gpuix-capabilities.md` repeated the README section and added a host
capability table that nothing keeps in sync with the renderers. README is
the API contract, so the clipboard semantics stay there. Also corrects the
claim that no UI thread is involved: on Windows and Linux the call runs on
the UI thread while JavaScript waits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@remorses
remorses merged commit c2c52c7 into remorses:main Oct 31, 2026
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.

2 participants