@crossbind/extension-clipboard 0.0.0-stage → 2.0.0-beta.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Buğra Sarı and crossbind contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,38 @@
1
- # Temporary Holding Version
1
+ # @crossbind/extension-clipboard
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The system clipboard's text, read and written from an app's page on macOS, iOS, Android, Windows and Linux. The page
4
+ has the web's `navigator.clipboard` as well, which reads on its user's gesture alone; this reads and writes at any time
5
+ the system lets an app.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npx crossbind add @crossbind/extension-clipboard
11
+ ```
12
+
13
+ That installs it with the app's package manager and lists it in the app's `crossbind.config.js`, whose host builds its
14
+ native code in:
15
+
16
+ ```js
17
+ import clipboard from '@crossbind/extension-clipboard/crossbind.extension.js';
18
+
19
+ export default {
20
+ target: { runtimeEnv: 'app' },
21
+ extensions: [clipboard()],
22
+ };
23
+ ```
24
+
25
+ ## Use
26
+
27
+ ```js
28
+ import { readText, writeText } from '@crossbind/extension-clipboard';
29
+
30
+ await writeText('Copied from the app');
31
+ const text = await readText(); // '' when the clipboard holds no text
32
+ ```
33
+
34
+ ## Platforms
35
+
36
+ - iOS asks its user before an app reads what another app copied.
37
+ - Android lets the app in front alone read the clipboard, and from Android 13 tells its user when one does.
38
+ - Windows waits a moment for another app that holds the clipboard, then rejects.
@@ -0,0 +1,16 @@
1
+ // The clipboard extension as an app's crossbind.config.js lists it: extensions: [clipboard()].
2
+ export default function clipboard() {
3
+ return {
4
+ name: 'clipboard',
5
+ contract: 1,
6
+ root: import.meta.dirname,
7
+ app: {
8
+ sources: ['native/clipboard.cpp'],
9
+ macos: { sources: ['native/darwin.mm'] },
10
+ ios: { sources: ['native/ios.mm'] },
11
+ linux: { sources: ['native/linux.cpp'] },
12
+ windows: { sources: ['native/windows.cpp'] },
13
+ android: { sources: ['native/android.cpp'], java: 'native/android', class: 'dev.crossbind.extension.clipboard.ClipboardExtension' },
14
+ },
15
+ };
16
+ }
package/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /** The text the system clipboard holds, or '' when it holds none. iOS asks its user before an app reads what another app copied. */
2
+ export declare function readText(): Promise<string>;
3
+
4
+ /** Puts text on the system clipboard, in place of what it held. */
5
+ export declare function writeText(text: string): Promise<void>;
package/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // The system clipboard's text, from an app's page. The page has the web's navigator.clipboard as well, which reads on its
2
+ // user's gesture alone; these read and write at any time the system lets an app.
3
+ import { invoke } from 'crossbind/app';
4
+
5
+ // The text the clipboard holds, or '' when it holds none.
6
+ export const readText = async () => invoke('clipboard.readText');
7
+
8
+ // Puts text on the clipboard, in place of what it held.
9
+ export const writeText = async (text) => {
10
+ if (typeof text !== 'string') throw new TypeError('crossbind: clipboard.writeText takes a string.');
11
+ return invoke('clipboard.writeText', { text });
12
+ };
@@ -0,0 +1,32 @@
1
+ package dev.crossbind.extension.clipboard;
2
+
3
+ import android.app.Activity;
4
+ import android.content.ClipData;
5
+ import android.content.ClipboardManager;
6
+
7
+ import dev.crossbind.app.Extension;
8
+ import dev.crossbind.app.ExtensionHost;
9
+
10
+ import org.json.JSONObject;
11
+
12
+ // The system clipboard's text. Android lets the app in front alone read it, and from Android 13 tells its user when one
13
+ // does.
14
+ public final class ClipboardExtension implements Extension {
15
+ @Override
16
+ public void call(ExtensionHost host, String function, JSONObject options, Call call) {
17
+ Activity activity = host.activity();
18
+ ClipboardManager board = activity.getSystemService(ClipboardManager.class);
19
+ switch (function) {
20
+ case "readText" -> {
21
+ ClipData clip = board.getPrimaryClip();
22
+ CharSequence text = clip == null || clip.getItemCount() == 0 ? null : clip.getItemAt(0).coerceToText(activity);
23
+ call.resolve(text == null ? "" : text.toString());
24
+ }
25
+ case "writeText" -> {
26
+ board.setPrimaryClip(ClipData.newPlainText("", options.optString("text")));
27
+ call.resolve(null);
28
+ }
29
+ default -> call.reject("crossbind: the clipboard extension's Java has no function " + function);
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,26 @@
1
+ #include "clipboard.h"
2
+
3
+ #include <optional>
4
+ #include <utility>
5
+
6
+ namespace crossbind::clipboard {
7
+ namespace {
8
+
9
+ // The extension's Java answers the call.
10
+ void viaJava(const std::string &function, Value::Object options, Call call) {
11
+ android::callJava("clipboard", function, std::move(options), [call = std::move(call)](std::optional<std::string> failure, Value result) {
12
+ if (failure) {
13
+ call.reject(*failure);
14
+ } else {
15
+ call.resolve(std::move(result));
16
+ }
17
+ });
18
+ }
19
+
20
+ } // namespace
21
+
22
+ void readText(Call call) { viaJava("readText", {}, std::move(call)); }
23
+
24
+ void writeText(Call call, std::string text) { viaJava("writeText", {{"text", std::move(text)}}, std::move(call)); }
25
+
26
+ } // namespace crossbind::clipboard
@@ -0,0 +1,15 @@
1
+ // The system clipboard's text, which the page reads and writes through @crossbind/extension-clipboard.
2
+ #include "clipboard.h"
3
+
4
+ #include <stdexcept>
5
+ #include <utility>
6
+
7
+ void crossbind_extension_clipboard(crossbind::Registry &registry) {
8
+ using crossbind::Call;
9
+ registry.function("clipboard.readText", [](Call call) { crossbind::clipboard::readText(std::move(call)); });
10
+ registry.function("clipboard.writeText", [](Call call) {
11
+ const crossbind::Value &text = call.options()["text"];
12
+ if (!text.isString()) throw std::invalid_argument("crossbind: clipboard.writeText takes a string");
13
+ crossbind::clipboard::writeText(call, text.asString());
14
+ });
15
+ }
@@ -0,0 +1,14 @@
1
+ #pragma once
2
+
3
+ // The clipboard each platform has. Each is called on the module's thread and answers the call: readText with the text
4
+ // the clipboard holds, or "" when it holds none, and writeText once the clipboard holds the text.
5
+ #include "crossbind/extension.h"
6
+
7
+ #include <string>
8
+
9
+ namespace crossbind::clipboard {
10
+
11
+ void readText(Call call);
12
+ void writeText(Call call, std::string text);
13
+
14
+ } // namespace crossbind::clipboard
@@ -0,0 +1,33 @@
1
+ #import <Cocoa/Cocoa.h>
2
+
3
+ #include "clipboard.h"
4
+
5
+ namespace crossbind::clipboard {
6
+ namespace {
7
+
8
+ // The text whole, a null in it as well.
9
+ std::string utf8(NSString *text) {
10
+ NSData *data = [text dataUsingEncoding:NSUTF8StringEncoding];
11
+ return data ? std::string(static_cast<const char *>(data.bytes), data.length) : std::string();
12
+ }
13
+
14
+ } // namespace
15
+
16
+ void readText(Call call) {
17
+ onMainThread([call] { call.resolve(utf8([NSPasteboard.generalPasteboard stringForType:NSPasteboardTypeString])); });
18
+ }
19
+
20
+ void writeText(Call call, std::string text) {
21
+ onMainThread([call, text] {
22
+ NSString *string = [[NSString alloc] initWithBytes:text.data() length:text.size() encoding:NSUTF8StringEncoding];
23
+ NSPasteboard *board = NSPasteboard.generalPasteboard;
24
+ [board clearContents];
25
+ if (string && [board setString:string forType:NSPasteboardTypeString]) {
26
+ call.resolve();
27
+ } else {
28
+ call.reject("crossbind: the clipboard took no text");
29
+ }
30
+ });
31
+ }
32
+
33
+ } // namespace crossbind::clipboard
package/native/ios.mm ADDED
@@ -0,0 +1,33 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ #include "clipboard.h"
4
+
5
+ namespace crossbind::clipboard {
6
+ namespace {
7
+
8
+ // The text whole, a null in it as well.
9
+ std::string utf8(NSString *text) {
10
+ NSData *data = [text dataUsingEncoding:NSUTF8StringEncoding];
11
+ return data ? std::string(static_cast<const char *>(data.bytes), data.length) : std::string();
12
+ }
13
+
14
+ } // namespace
15
+
16
+ // iOS asks its user first when the text is another app's.
17
+ void readText(Call call) {
18
+ onMainThread([call] { call.resolve(utf8(UIPasteboard.generalPasteboard.string)); });
19
+ }
20
+
21
+ void writeText(Call call, std::string text) {
22
+ onMainThread([call, text] {
23
+ NSString *string = [[NSString alloc] initWithBytes:text.data() length:text.size() encoding:NSUTF8StringEncoding];
24
+ if (!string) {
25
+ call.reject("crossbind: the clipboard took no text");
26
+ return;
27
+ }
28
+ UIPasteboard.generalPasteboard.string = string;
29
+ call.resolve();
30
+ });
31
+ }
32
+
33
+ } // namespace crossbind::clipboard
@@ -0,0 +1,26 @@
1
+ #include "clipboard.h"
2
+
3
+ #include <gtk/gtk.h>
4
+
5
+ #include <memory>
6
+
7
+ namespace crossbind::clipboard {
8
+
9
+ // GTK asks the clipboard's owner for its text, which may be another app, and answers on the main loop.
10
+ void readText(Call call) {
11
+ onMainThread([call] {
12
+ gtk_clipboard_request_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), [](GtkClipboard *, const gchar *text, gpointer data) {
13
+ const std::unique_ptr<Call> asked(static_cast<Call *>(data));
14
+ asked->resolve(std::string(text ? text : ""));
15
+ }, new Call(call));
16
+ });
17
+ }
18
+
19
+ void writeText(Call call, std::string text) {
20
+ onMainThread([call, text] {
21
+ gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), text.data(), static_cast<gint>(text.size()));
22
+ call.resolve();
23
+ });
24
+ }
25
+
26
+ } // namespace crossbind::clipboard
@@ -0,0 +1,97 @@
1
+ #include "clipboard.h"
2
+
3
+ #include <windows.h>
4
+
5
+ #include <cstring>
6
+
7
+ namespace crossbind::clipboard {
8
+ namespace {
9
+
10
+ // Another app may hold the clipboard for a moment, as a clipboard manager does after each copy.
11
+ constexpr int OPEN_ATTEMPTS = 5;
12
+ constexpr DWORD OPEN_WAIT_MS = 5;
13
+
14
+ std::wstring wide(const std::string &text) {
15
+ if (text.empty()) return {};
16
+ const int size = MultiByteToWideChar(CP_UTF8, 0, text.data(), static_cast<int>(text.size()), nullptr, 0);
17
+ std::wstring result(static_cast<size_t>(size), L'\0');
18
+ MultiByteToWideChar(CP_UTF8, 0, text.data(), static_cast<int>(text.size()), result.data(), size);
19
+ return result;
20
+ }
21
+
22
+ std::string utf8(const wchar_t *text) {
23
+ if (!text || !*text) return {};
24
+ const int size = WideCharToMultiByte(CP_UTF8, 0, text, -1, nullptr, 0, nullptr, nullptr);
25
+ std::string result(static_cast<size_t>(size - 1), '\0');
26
+ WideCharToMultiByte(CP_UTF8, 0, text, -1, result.data(), size, nullptr, nullptr);
27
+ return result;
28
+ }
29
+
30
+ // The window owns what the app puts on the clipboard.
31
+ bool openClipboard() {
32
+ for (int attempt = 0; attempt < OPEN_ATTEMPTS; attempt += 1) {
33
+ if (OpenClipboard(static_cast<HWND>(appWindow()))) return true;
34
+ Sleep(OPEN_WAIT_MS);
35
+ }
36
+ return false;
37
+ }
38
+
39
+ // UTF-16 with its null, in memory the clipboard owns once it takes it.
40
+ HGLOBAL memoryOf(const std::wstring &text) {
41
+ const size_t bytes = (text.size() + 1) * sizeof(wchar_t);
42
+ HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, bytes);
43
+ void *at = memory ? GlobalLock(memory) : nullptr;
44
+ if (!at) {
45
+ if (memory) GlobalFree(memory);
46
+ return nullptr;
47
+ }
48
+ std::memcpy(at, text.c_str(), bytes);
49
+ GlobalUnlock(memory);
50
+ return memory;
51
+ }
52
+
53
+ } // namespace
54
+
55
+ void readText(Call call) {
56
+ onMainThread([call] {
57
+ if (!openClipboard()) {
58
+ call.reject("crossbind: another app holds the clipboard");
59
+ return;
60
+ }
61
+ std::string text;
62
+ if (HANDLE data = GetClipboardData(CF_UNICODETEXT)) {
63
+ if (const auto *chars = static_cast<const wchar_t *>(GlobalLock(data))) {
64
+ text = utf8(chars);
65
+ GlobalUnlock(data);
66
+ }
67
+ }
68
+ CloseClipboard();
69
+ call.resolve(text);
70
+ });
71
+ }
72
+
73
+ void writeText(Call call, std::string text) {
74
+ onMainThread([call, text] {
75
+ HGLOBAL memory = memoryOf(wide(text));
76
+ if (!memory) {
77
+ call.reject("crossbind: no memory for the clipboard's text");
78
+ return;
79
+ }
80
+ if (!openClipboard()) {
81
+ GlobalFree(memory);
82
+ call.reject("crossbind: another app holds the clipboard");
83
+ return;
84
+ }
85
+ EmptyClipboard();
86
+ const bool isTaken = SetClipboardData(CF_UNICODETEXT, memory) != nullptr;
87
+ CloseClipboard();
88
+ if (!isTaken) {
89
+ GlobalFree(memory);
90
+ call.reject("crossbind: the clipboard took no text");
91
+ return;
92
+ }
93
+ call.resolve();
94
+ });
95
+ }
96
+
97
+ } // namespace crossbind::clipboard
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@crossbind/extension-clipboard",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "2.0.0-beta.69",
4
+ "description": "crossbind app extension: the system clipboard's text, read and written whenever the system lets an app.",
5
+ "homepage": "https://github.com/crossbind/crossbind/tree/main/extensions/clipboard#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/crossbind/crossbind.git"
9
+ },
10
+ "license": "MIT",
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./index.d.ts",
15
+ "default": "./index.js"
16
+ },
17
+ "./crossbind.extension.js": "./crossbind.extension.js"
18
+ },
19
+ "files": [
20
+ "crossbind.extension.js",
21
+ "index.d.ts",
22
+ "index.js",
23
+ "native"
24
+ ],
25
+ "crossbind": {
26
+ "extension": "./crossbind.extension.js"
27
+ },
28
+ "peerDependencies": {
29
+ "crossbind": "^2.0.0-beta.69"
30
+ },
31
+ "devDependencies": {
32
+ "crossbind": "^2.0.0-beta.69"
33
+ },
34
+ "keywords": [
35
+ "crossbind-extension",
36
+ "crossbind",
37
+ "system",
38
+ "clipboard",
39
+ "app"
40
+ ]
6
41
  }