expo-clipboard for text is a thin wrapper over the existing
rnLinux.clipboard* JSI bindings (added during the device-info
work). Those talk to GdkClipboard on the active display — the
same surface every GTK app uses. Cross-app paste works because
the display clipboard is shared with the rest of the desktop.
JS app
↓ require('expo-clipboard') ← metro/esbuild rewrite, linux only
@lucid-softworks/react-native-linux-expo/expo-clipboard.js
├─ setStringAsync(text) → rnLinux.clipboardSetString
│ ↓ gdk_clipboard_set_text
│ GdkClipboard on display
└─ getStringAsync() / hasStringAsync()
→ rnLinux.clipboardGetStringSync
↓ gdk_clipboard_get_content
→ GdkContentProvider value
None. GTK4 is already a runtime dep; GdkClipboard is bundled.
cd apps/playground
RN_ENTRY=smoke-demo.tsx node bundle.mjs
scripts/vm/sh.sh 'scripts/vm/run-playground.sh'Scroll to the expo-clipboard section. The probe row already shows
a successful set+get round-trip (roundtripped 22 chars). The demo
exposes two buttons: copy timestamp writes a fresh ISO timestamp
to the clipboard; paste reads back whatever's on the clipboard
and displays it inline.
| API | Behavior on Linux |
|---|---|
setStringAsync(text) / setString |
Real — gdk_clipboard_set_text on the display |
getStringAsync() |
Real for values WE wrote (sync read of gdk_clipboard_get_content) |
hasStringAsync() |
True iff getStringAsync() returns a non-empty string |
getUrlAsync / setUrlAsync / hasUrl… |
Aliases for the text path — no separate Linux URL clip type |
getImageAsync / setImageAsync |
Real — base64 PNG/JPEG round-trip through GdkTexture + gdk_clipboard_set_texture |
getHtmlAsync / setHtmlAsync |
Real — unioned text/html + text/plain provider; plaintext extracted in JS |
hasImageAsync / hasHtmlAsync |
Real — non-empty result from the matching async getter |
setContentAsync({files: [...]}) |
Real — GdkFileList content provider; file managers paste real file refs |
addClipboardListener |
Real — fan-out over GdkClipboard::changed (cross-app writes fire it too) |
ContentType / StringFormat / ImageFormat enums |
Match upstream's numeric values |
- Cross-app paste reads — DONE.
getStringAsync/hasStringAsyncnow route throughrnLinux.clipboardGetStringAsync→gdk_clipboard_read_text_async, which negotiates the MIME transfer with whichever app put the text on the clipboard. The legacy syncclipboardGetStringSyncis still there for the fast in-process round-trip but only sees this process's own writes. - Images — DONE.
setImageAsync(base64)decodes viagdk_texture_new_from_bytesand callsgdk_clipboard_set_texture;getImageAsync()reads throughgdk_clipboard_read_texture_asyncand re-encodes as PNG viagdk_texture_save_to_png_bytesso the base64 payload is self-describing. - HTML — DONE.
setHtmlAsync(html)publishes agdk_content_provider_new_unionover bothtext/htmlandtext/plain;charset=utf-8(plaintext extracted in JS) so non-rich consumers still get something readable.getHtmlAsync()asks fortext/htmlspecifically viagdk_clipboard_read_asyncand drains the returnedGInputStream. - File lists — DONE.
Clipboard.setContentAsync({files: [...]})builds aGdkFileList(gdk_file_list_new_from_list) and publishes a typedGDK_TYPE_FILE_LISTcontent provider; file managers paste real file refs, not just text URIs. - Change listener — DONE.
g_signal_connect(clip, "changed", ...)is bound through a single native trampoline; the JS shim multiplexes alladdClipboardListenersubscribers behind it and tears the subscription down when the last listener unsubscribes. The signal fires on every clipboard write — including ones from other apps — so this is a real cross-app subscription, not a same-process echo. The payload is{}(iOS/Android pass content-type metadata; we don't carry that without an extra async read) — consumers re-callgetStringAsync()to fetch the new value, matching the expo idiom.