mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Compare commits
5 Commits
claude/fix
...
claude/cli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b54b778c7 | ||
|
|
03ce58e50d | ||
|
|
c46a41e5c5 | ||
|
|
65e14fb144 | ||
|
|
7b34cd465f |
@@ -29,6 +29,9 @@ src/bun.js/bindings/c-bindings.cpp
|
||||
src/bun.js/bindings/CallSite.cpp
|
||||
src/bun.js/bindings/CallSitePrototype.cpp
|
||||
src/bun.js/bindings/CatchScopeBinding.cpp
|
||||
src/bun.js/bindings/ClipboardDarwin.cpp
|
||||
src/bun.js/bindings/ClipboardLinux.cpp
|
||||
src/bun.js/bindings/ClipboardWindows.cpp
|
||||
src/bun.js/bindings/CodeCoverage.cpp
|
||||
src/bun.js/bindings/ConsoleObject.cpp
|
||||
src/bun.js/bindings/Cookie.cpp
|
||||
@@ -69,6 +72,7 @@ src/bun.js/bindings/JSBufferEncodingType.cpp
|
||||
src/bun.js/bindings/JSBufferList.cpp
|
||||
src/bun.js/bindings/JSBundlerPlugin.cpp
|
||||
src/bun.js/bindings/JSBunRequest.cpp
|
||||
src/bun.js/bindings/JSClipboard.cpp
|
||||
src/bun.js/bindings/JSCommonJSExtensions.cpp
|
||||
src/bun.js/bindings/JSCommonJSModule.cpp
|
||||
src/bun.js/bindings/JSCTaskScheduler.cpp
|
||||
|
||||
@@ -172,6 +172,7 @@ src/bun.js/bindings/JSArray.zig
|
||||
src/bun.js/bindings/JSArrayIterator.zig
|
||||
src/bun.js/bindings/JSBigInt.zig
|
||||
src/bun.js/bindings/JSCell.zig
|
||||
src/bun.js/bindings/JSClipboard.zig
|
||||
src/bun.js/bindings/JSErrorCode.zig
|
||||
src/bun.js/bindings/JSFunction.zig
|
||||
src/bun.js/bindings/JSGlobalObject.zig
|
||||
|
||||
@@ -468,6 +468,7 @@ pub const Run = struct {
|
||||
|
||||
bun.api.napi.fixDeadCodeElimination();
|
||||
bun.crash_handler.fixDeadCodeElimination();
|
||||
@import("bun.js/bindings/JSClipboard.zig").fixDeadCodeElimination();
|
||||
vm.globalExit();
|
||||
}
|
||||
|
||||
|
||||
85
src/bun.js/bindings/Clipboard.h
Normal file
85
src/bun.js/bindings/Clipboard.h
Normal file
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "root.h"
|
||||
#include <wtf/Vector.h>
|
||||
#include <wtf/text/WTFString.h>
|
||||
#include <wtf/text/CString.h>
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
|
||||
namespace Bun {
|
||||
namespace Clipboard {
|
||||
|
||||
using namespace WTF;
|
||||
|
||||
enum class ErrorType {
|
||||
None,
|
||||
NotSupported,
|
||||
AccessDenied,
|
||||
PlatformError
|
||||
};
|
||||
|
||||
struct Error {
|
||||
ErrorType type = ErrorType::None;
|
||||
String message;
|
||||
int code = 0;
|
||||
};
|
||||
|
||||
// Supported clipboard data types
|
||||
enum class DataType {
|
||||
Text,
|
||||
HTML,
|
||||
RTF,
|
||||
Image,
|
||||
Files
|
||||
};
|
||||
|
||||
struct ClipboardData {
|
||||
DataType type;
|
||||
Vector<uint8_t> data;
|
||||
String mimeType;
|
||||
};
|
||||
|
||||
// Async callback signature: (Error, Vector<ClipboardData>)
|
||||
using ReadCallback = std::function<void(Error, Vector<ClipboardData>)>;
|
||||
using WriteCallback = std::function<void(Error)>;
|
||||
|
||||
// Platform-specific implementations
|
||||
Error writeText(const String& text);
|
||||
Error writeHTML(const String& html);
|
||||
Error writeRTF(const String& rtf);
|
||||
Error writeImage(const Vector<uint8_t>& imageData, const String& mimeType);
|
||||
|
||||
std::optional<String> readText(Error& error);
|
||||
std::optional<String> readHTML(Error& error);
|
||||
std::optional<String> readRTF(Error& error);
|
||||
std::optional<Vector<uint8_t>> readImage(Error& error, String& mimeType);
|
||||
|
||||
// Async versions for thread pool execution
|
||||
void writeTextAsync(const String& text, WriteCallback callback);
|
||||
void writeHTMLAsync(const String& html, WriteCallback callback);
|
||||
void writeRTFAsync(const String& rtf, WriteCallback callback);
|
||||
void writeImageAsync(const Vector<uint8_t>& imageData, const String& mimeType, WriteCallback callback);
|
||||
|
||||
void readTextAsync(ReadCallback callback);
|
||||
void readHTMLAsync(ReadCallback callback);
|
||||
void readRTFAsync(ReadCallback callback);
|
||||
void readImageAsync(ReadCallback callback);
|
||||
|
||||
// Internal async task implementations
|
||||
void executeWriteTextAsync(const String& text, WriteCallback callback);
|
||||
void executeWriteHTMLAsync(const String& html, WriteCallback callback);
|
||||
void executeWriteRTFAsync(const String& rtf, WriteCallback callback);
|
||||
void executeWriteImageAsync(const Vector<uint8_t>& imageData, const String& mimeType, WriteCallback callback);
|
||||
|
||||
void executeReadTextAsync(ReadCallback callback);
|
||||
void executeReadHTMLAsync(ReadCallback callback);
|
||||
void executeReadRTFAsync(ReadCallback callback);
|
||||
void executeReadImageAsync(ReadCallback callback);
|
||||
|
||||
// Check if clipboard operations are supported
|
||||
bool isSupported();
|
||||
Vector<DataType> getSupportedTypes();
|
||||
|
||||
} // namespace Clipboard
|
||||
} // namespace Bun
|
||||
206
src/bun.js/bindings/ClipboardAsync.cpp.bak
Normal file
206
src/bun.js/bindings/ClipboardAsync.cpp.bak
Normal file
@@ -0,0 +1,206 @@
|
||||
#include "root.h"
|
||||
#include "Clipboard.h"
|
||||
#include <wtf/text/WTFString.h>
|
||||
#include <wtf/Vector.h>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
|
||||
namespace Bun {
|
||||
namespace Clipboard {
|
||||
|
||||
using namespace WTF;
|
||||
|
||||
// Async task structures
|
||||
struct WriteTextTask {
|
||||
String text;
|
||||
WriteCallback callback;
|
||||
};
|
||||
|
||||
struct WriteHTMLTask {
|
||||
String html;
|
||||
WriteCallback callback;
|
||||
};
|
||||
|
||||
struct WriteRTFTask {
|
||||
String rtf;
|
||||
WriteCallback callback;
|
||||
};
|
||||
|
||||
struct WriteImageTask {
|
||||
Vector<uint8_t> imageData;
|
||||
String mimeType;
|
||||
WriteCallback callback;
|
||||
};
|
||||
|
||||
struct ReadTextTask {
|
||||
ReadCallback callback;
|
||||
};
|
||||
|
||||
struct ReadHTMLTask {
|
||||
ReadCallback callback;
|
||||
};
|
||||
|
||||
struct ReadRTFTask {
|
||||
ReadCallback callback;
|
||||
};
|
||||
|
||||
struct ReadImageTask {
|
||||
ReadCallback callback;
|
||||
};
|
||||
|
||||
// Thread pool execution functions
|
||||
void executeWriteTextAsync(const String& text, WriteCallback callback)
|
||||
{
|
||||
std::thread([text = String(text), callback = std::move(callback)]() {
|
||||
Error error = writeText(text);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void executeWriteHTMLAsync(const String& html, WriteCallback callback)
|
||||
{
|
||||
std::thread([html = String(html), callback = std::move(callback)]() {
|
||||
Error error = writeHTML(html);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void executeWriteRTFAsync(const String& rtf, WriteCallback callback)
|
||||
{
|
||||
std::thread([rtf = String(rtf), callback = std::move(callback)]() {
|
||||
Error error = writeRTF(rtf);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void executeWriteImageAsync(const Vector<uint8_t>& imageData, const String& mimeType, WriteCallback callback)
|
||||
{
|
||||
std::thread([imageData, mimeType = String(mimeType), callback = std::move(callback)]() {
|
||||
Error error = writeImage(imageData, mimeType);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void executeReadTextAsync(ReadCallback callback)
|
||||
{
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
auto text = readText(error);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (text.has_value()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::Text;
|
||||
clipData.mimeType = "text/plain"_s;
|
||||
auto textUtf8 = text->utf8();
|
||||
clipData.data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(textUtf8.data()), textUtf8.length()));
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void executeReadHTMLAsync(ReadCallback callback)
|
||||
{
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
auto html = readHTML(error);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (html.has_value()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::HTML;
|
||||
clipData.mimeType = "text/html"_s;
|
||||
auto htmlUtf8 = html->utf8();
|
||||
clipData.data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(htmlUtf8.data()), htmlUtf8.length()));
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void executeReadRTFAsync(ReadCallback callback)
|
||||
{
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
auto rtf = readRTF(error);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (rtf.has_value()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::RTF;
|
||||
clipData.mimeType = "text/rtf"_s;
|
||||
auto rtfUtf8 = rtf->utf8();
|
||||
clipData.data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(rtfUtf8.data()), rtfUtf8.length()));
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void executeReadImageAsync(ReadCallback callback)
|
||||
{
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
String mimeType;
|
||||
auto imageData = readImage(error, mimeType);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (imageData.has_value()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::Image;
|
||||
clipData.mimeType = mimeType;
|
||||
clipData.data = WTFMove(*imageData);
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
// Public async interface functions
|
||||
void writeTextAsync(const String& text, WriteCallback callback)
|
||||
{
|
||||
executeWriteTextAsync(text, std::move(callback));
|
||||
}
|
||||
|
||||
void writeHTMLAsync(const String& html, WriteCallback callback)
|
||||
{
|
||||
executeWriteHTMLAsync(html, std::move(callback));
|
||||
}
|
||||
|
||||
void writeRTFAsync(const String& rtf, WriteCallback callback)
|
||||
{
|
||||
executeWriteRTFAsync(rtf, std::move(callback));
|
||||
}
|
||||
|
||||
void writeImageAsync(const Vector<uint8_t>& imageData, const String& mimeType, WriteCallback callback)
|
||||
{
|
||||
executeWriteImageAsync(imageData, mimeType, std::move(callback));
|
||||
}
|
||||
|
||||
void readTextAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadTextAsync(std::move(callback));
|
||||
}
|
||||
|
||||
void readHTMLAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadHTMLAsync(std::move(callback));
|
||||
}
|
||||
|
||||
void readRTFAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadRTFAsync(std::move(callback));
|
||||
}
|
||||
|
||||
void readImageAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadImageAsync(std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace Clipboard
|
||||
} // namespace Bun
|
||||
557
src/bun.js/bindings/ClipboardDarwin.cpp
Normal file
557
src/bun.js/bindings/ClipboardDarwin.cpp
Normal file
@@ -0,0 +1,557 @@
|
||||
#include "root.h"
|
||||
|
||||
#if OS(DARWIN)
|
||||
|
||||
#include "Clipboard.h"
|
||||
#include <dlfcn.h>
|
||||
#include <wtf/text/WTFString.h>
|
||||
#include <wtf/Vector.h>
|
||||
#include <wtf/NeverDestroyed.h>
|
||||
#include <thread>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
namespace Bun {
|
||||
namespace Clipboard {
|
||||
|
||||
using namespace WTF;
|
||||
|
||||
// AppKit C API function pointers loaded dynamically
|
||||
struct AppKitAPI {
|
||||
void* appkit_handle;
|
||||
void* foundation_handle;
|
||||
|
||||
// Function pointers for NSPasteboard C API
|
||||
void* (*NSPasteboardGeneralPasteboard)(void);
|
||||
int (*NSPasteboardClearContents)(void* pasteboard);
|
||||
int (*NSPasteboardSetStringForType)(void* pasteboard, CFStringRef string, CFStringRef type);
|
||||
int (*NSPasteboardSetDataForType)(void* pasteboard, CFDataRef data, CFStringRef type);
|
||||
CFStringRef (*NSPasteboardStringForType)(void* pasteboard, CFStringRef type);
|
||||
CFDataRef (*NSPasteboardDataForType)(void* pasteboard, CFStringRef type);
|
||||
|
||||
// Type constants
|
||||
CFStringRef NSPasteboardTypeString;
|
||||
CFStringRef NSPasteboardTypeHTML;
|
||||
CFStringRef NSPasteboardTypeRTF;
|
||||
CFStringRef NSPasteboardTypePNG;
|
||||
CFStringRef NSPasteboardTypeTIFF;
|
||||
|
||||
bool loaded;
|
||||
|
||||
AppKitAPI() : appkit_handle(nullptr), foundation_handle(nullptr), loaded(false) {}
|
||||
|
||||
bool load() {
|
||||
if (loaded) return true;
|
||||
|
||||
// Load Foundation framework
|
||||
foundation_handle = dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", RTLD_LAZY);
|
||||
if (!foundation_handle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load AppKit framework
|
||||
appkit_handle = dlopen("/System/Library/Frameworks/AppKit.framework/AppKit", RTLD_LAZY);
|
||||
if (!appkit_handle) {
|
||||
dlclose(foundation_handle);
|
||||
foundation_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load NSPasteboard C functions
|
||||
NSPasteboardGeneralPasteboard = (void*(*)(void))dlsym(appkit_handle, "NSPasteboardGeneralPasteboard");
|
||||
NSPasteboardClearContents = (int(*)(void*))dlsym(appkit_handle, "NSPasteboardClearContents");
|
||||
NSPasteboardSetStringForType = (int(*)(void*, CFStringRef, CFStringRef))dlsym(appkit_handle, "NSPasteboardSetStringForType");
|
||||
NSPasteboardSetDataForType = (int(*)(void*, CFDataRef, CFStringRef))dlsym(appkit_handle, "NSPasteboardSetDataForType");
|
||||
NSPasteboardStringForType = (CFStringRef(*)(void*, CFStringRef))dlsym(appkit_handle, "NSPasteboardStringForType");
|
||||
NSPasteboardDataForType = (CFDataRef(*)(void*, CFStringRef))dlsym(appkit_handle, "NSPasteboardDataForType");
|
||||
|
||||
// Verify we got the essential functions
|
||||
if (!NSPasteboardGeneralPasteboard || !NSPasteboardClearContents ||
|
||||
!NSPasteboardSetStringForType || !NSPasteboardStringForType) {
|
||||
dlclose(appkit_handle);
|
||||
dlclose(foundation_handle);
|
||||
appkit_handle = nullptr;
|
||||
foundation_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load type constants
|
||||
void* ptr;
|
||||
ptr = dlsym(appkit_handle, "NSPasteboardTypeString");
|
||||
if (ptr) NSPasteboardTypeString = *(CFStringRef*)ptr;
|
||||
|
||||
ptr = dlsym(appkit_handle, "NSPasteboardTypeHTML");
|
||||
if (ptr) NSPasteboardTypeHTML = *(CFStringRef*)ptr;
|
||||
|
||||
ptr = dlsym(appkit_handle, "NSPasteboardTypeRTF");
|
||||
if (ptr) NSPasteboardTypeRTF = *(CFStringRef*)ptr;
|
||||
|
||||
ptr = dlsym(appkit_handle, "NSPasteboardTypePNG");
|
||||
if (ptr) NSPasteboardTypePNG = *(CFStringRef*)ptr;
|
||||
|
||||
ptr = dlsym(appkit_handle, "NSPasteboardTypeTIFF");
|
||||
if (ptr) NSPasteboardTypeTIFF = *(CFStringRef*)ptr;
|
||||
|
||||
// Verify we have at least the string type
|
||||
if (!NSPasteboardTypeString) {
|
||||
dlclose(appkit_handle);
|
||||
dlclose(foundation_handle);
|
||||
appkit_handle = nullptr;
|
||||
foundation_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
~AppKitAPI() {
|
||||
if (appkit_handle) dlclose(appkit_handle);
|
||||
if (foundation_handle) dlclose(foundation_handle);
|
||||
}
|
||||
};
|
||||
|
||||
static AppKitAPI* getAppKitAPI() {
|
||||
static LazyNeverDestroyed<AppKitAPI> api;
|
||||
static std::once_flag onceFlag;
|
||||
std::call_once(onceFlag, [&] {
|
||||
api.construct();
|
||||
api->load();
|
||||
});
|
||||
return api->loaded ? &api.get() : nullptr;
|
||||
}
|
||||
|
||||
static void updateError(Error& err, const String& message) {
|
||||
err.type = ErrorType::PlatformError;
|
||||
err.message = message;
|
||||
err.code = -1;
|
||||
}
|
||||
|
||||
static CFStringRef createCFString(const String& str) {
|
||||
auto utf8 = str.utf8();
|
||||
return CFStringCreateWithBytes(kCFAllocatorDefault,
|
||||
reinterpret_cast<const UInt8*>(utf8.data()),
|
||||
utf8.length(),
|
||||
kCFStringEncodingUTF8,
|
||||
false);
|
||||
}
|
||||
|
||||
static String cfStringToWTFString(CFStringRef cfStr) {
|
||||
if (!cfStr) return String();
|
||||
|
||||
CFIndex length = CFStringGetLength(cfStr);
|
||||
CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
||||
|
||||
Vector<char> buffer(maxSize);
|
||||
if (CFStringGetCString(cfStr, buffer.data(), maxSize, kCFStringEncodingUTF8)) {
|
||||
return String::fromUTF8(buffer.data());
|
||||
}
|
||||
return String();
|
||||
}
|
||||
|
||||
// Public API implementations
|
||||
Error writeText(const String& text) {
|
||||
Error err;
|
||||
auto* api = getAppKitAPI();
|
||||
|
||||
if (!api) {
|
||||
updateError(err, "AppKit framework not available"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
void* pasteboard = api->NSPasteboardGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(err, "Could not access pasteboard"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
CFStringRef cfText = createCFString(text);
|
||||
if (!cfText) {
|
||||
updateError(err, "Failed to create CFString"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
api->NSPasteboardClearContents(pasteboard);
|
||||
int success = api->NSPasteboardSetStringForType(pasteboard, cfText, api->NSPasteboardTypeString);
|
||||
|
||||
CFRelease(cfText);
|
||||
|
||||
if (!success) {
|
||||
updateError(err, "Failed to write text to pasteboard"_s);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeHTML(const String& html) {
|
||||
Error err;
|
||||
auto* api = getAppKitAPI();
|
||||
|
||||
if (!api || !api->NSPasteboardTypeHTML || !api->NSPasteboardSetStringForType) {
|
||||
// Fall back to writing as plain text
|
||||
return writeText(html);
|
||||
}
|
||||
|
||||
void* pasteboard = api->NSPasteboardGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(err, "Could not access pasteboard"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
CFStringRef cfHtml = createCFString(html);
|
||||
if (!cfHtml) {
|
||||
updateError(err, "Failed to create CFString"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
api->NSPasteboardClearContents(pasteboard);
|
||||
int success = api->NSPasteboardSetStringForType(pasteboard, cfHtml, api->NSPasteboardTypeHTML);
|
||||
|
||||
CFRelease(cfHtml);
|
||||
|
||||
if (!success) {
|
||||
updateError(err, "Failed to write HTML to pasteboard"_s);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeRTF(const String& rtf) {
|
||||
Error err;
|
||||
auto* api = getAppKitAPI();
|
||||
|
||||
if (!api || !api->NSPasteboardTypeRTF || !api->NSPasteboardSetDataForType) {
|
||||
// Fall back to writing as plain text
|
||||
return writeText(rtf);
|
||||
}
|
||||
|
||||
void* pasteboard = api->NSPasteboardGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(err, "Could not access pasteboard"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
auto rtfData = rtf.utf8();
|
||||
CFDataRef cfData = CFDataCreate(kCFAllocatorDefault,
|
||||
reinterpret_cast<const UInt8*>(rtfData.data()),
|
||||
rtfData.length());
|
||||
if (!cfData) {
|
||||
updateError(err, "Failed to create CFData"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
api->NSPasteboardClearContents(pasteboard);
|
||||
int success = api->NSPasteboardSetDataForType(pasteboard, cfData, api->NSPasteboardTypeRTF);
|
||||
|
||||
CFRelease(cfData);
|
||||
|
||||
if (!success) {
|
||||
updateError(err, "Failed to write RTF to pasteboard"_s);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeImage(const Vector<uint8_t>& imageData, const String& mimeType) {
|
||||
Error err;
|
||||
auto* api = getAppKitAPI();
|
||||
|
||||
if (!api || !api->NSPasteboardSetDataForType) {
|
||||
updateError(err, "Image clipboard operations not supported"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
// Choose appropriate pasteboard type
|
||||
CFStringRef pasteboardType = nullptr;
|
||||
if (mimeType == "image/png"_s && api->NSPasteboardTypePNG) {
|
||||
pasteboardType = api->NSPasteboardTypePNG;
|
||||
} else if (mimeType == "image/tiff"_s && api->NSPasteboardTypeTIFF) {
|
||||
pasteboardType = api->NSPasteboardTypeTIFF;
|
||||
}
|
||||
|
||||
if (!pasteboardType) {
|
||||
updateError(err, "Unsupported image format for clipboard"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
void* pasteboard = api->NSPasteboardGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(err, "Could not access pasteboard"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
CFDataRef cfData = CFDataCreate(kCFAllocatorDefault, imageData.data(), imageData.size());
|
||||
if (!cfData) {
|
||||
updateError(err, "Failed to create CFData"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
api->NSPasteboardClearContents(pasteboard);
|
||||
int success = api->NSPasteboardSetDataForType(pasteboard, cfData, pasteboardType);
|
||||
|
||||
CFRelease(cfData);
|
||||
|
||||
if (!success) {
|
||||
updateError(err, "Failed to write image to pasteboard"_s);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
std::optional<String> readText(Error& error) {
|
||||
error = Error{};
|
||||
auto* api = getAppKitAPI();
|
||||
|
||||
if (!api) {
|
||||
updateError(error, "AppKit framework not available"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void* pasteboard = api->NSPasteboardGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(error, "Could not access pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
CFStringRef cfText = api->NSPasteboardStringForType(pasteboard, api->NSPasteboardTypeString);
|
||||
if (!cfText) {
|
||||
updateError(error, "No text found in pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
String result = cfStringToWTFString(cfText);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<String> readHTML(Error& error) {
|
||||
error = Error{};
|
||||
auto* api = getAppKitAPI();
|
||||
|
||||
if (!api || !api->NSPasteboardTypeHTML) {
|
||||
// Fall back to reading as plain text
|
||||
return readText(error);
|
||||
}
|
||||
|
||||
void* pasteboard = api->NSPasteboardGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(error, "Could not access pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
CFStringRef cfHtml = api->NSPasteboardStringForType(pasteboard, api->NSPasteboardTypeHTML);
|
||||
if (!cfHtml) {
|
||||
// Fall back to reading as plain text
|
||||
return readText(error);
|
||||
}
|
||||
|
||||
String result = cfStringToWTFString(cfHtml);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<String> readRTF(Error& error) {
|
||||
error = Error{};
|
||||
auto* api = getAppKitAPI();
|
||||
|
||||
if (!api || !api->NSPasteboardTypeRTF || !api->NSPasteboardDataForType) {
|
||||
// Fall back to reading as plain text
|
||||
return readText(error);
|
||||
}
|
||||
|
||||
void* pasteboard = api->NSPasteboardGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(error, "Could not access pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
CFDataRef cfData = api->NSPasteboardDataForType(pasteboard, api->NSPasteboardTypeRTF);
|
||||
if (!cfData) {
|
||||
// Fall back to reading as plain text
|
||||
return readText(error);
|
||||
}
|
||||
|
||||
const UInt8* bytes = CFDataGetBytePtr(cfData);
|
||||
CFIndex length = CFDataGetLength(cfData);
|
||||
|
||||
if (!bytes || !length) {
|
||||
updateError(error, "Invalid RTF data"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return String::fromUTF8(std::span<const char>(reinterpret_cast<const char*>(bytes), length));
|
||||
}
|
||||
|
||||
std::optional<Vector<uint8_t>> readImage(Error& error, String& mimeType) {
|
||||
error = Error{};
|
||||
auto* api = getAppKitAPI();
|
||||
|
||||
if (!api || !api->NSPasteboardDataForType) {
|
||||
updateError(error, "Image clipboard operations not supported"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void* pasteboard = api->NSPasteboardGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(error, "Could not access pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
CFDataRef imageData = nullptr;
|
||||
|
||||
// Try PNG first
|
||||
if (api->NSPasteboardTypePNG) {
|
||||
imageData = api->NSPasteboardDataForType(pasteboard, api->NSPasteboardTypePNG);
|
||||
if (imageData) {
|
||||
mimeType = "image/png"_s;
|
||||
}
|
||||
}
|
||||
|
||||
// Try TIFF if PNG not available
|
||||
if (!imageData && api->NSPasteboardTypeTIFF) {
|
||||
imageData = api->NSPasteboardDataForType(pasteboard, api->NSPasteboardTypeTIFF);
|
||||
if (imageData) {
|
||||
mimeType = "image/tiff"_s;
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageData) {
|
||||
updateError(error, "No image found in pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const UInt8* bytes = CFDataGetBytePtr(imageData);
|
||||
CFIndex length = CFDataGetLength(imageData);
|
||||
|
||||
if (!bytes || !length) {
|
||||
updateError(error, "Invalid image data"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Vector<uint8_t> result;
|
||||
result.append(std::span<const uint8_t>(bytes, length));
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isSupported() {
|
||||
return getAppKitAPI() != nullptr;
|
||||
}
|
||||
|
||||
Vector<DataType> getSupportedTypes() {
|
||||
Vector<DataType> types;
|
||||
auto* api = getAppKitAPI();
|
||||
if (api) {
|
||||
types.append(DataType::Text);
|
||||
if (api->NSPasteboardTypeHTML) types.append(DataType::HTML);
|
||||
if (api->NSPasteboardTypeRTF) types.append(DataType::RTF);
|
||||
if (api->NSPasteboardTypePNG || api->NSPasteboardTypeTIFF) types.append(DataType::Image);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
// Async implementations using std::thread
|
||||
void writeTextAsync(const String& text, WriteCallback callback) {
|
||||
std::thread([text = text.isolatedCopy(), callback = std::move(callback)]() {
|
||||
Error error = writeText(text);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void writeHTMLAsync(const String& html, WriteCallback callback) {
|
||||
std::thread([html = html.isolatedCopy(), callback = std::move(callback)]() {
|
||||
Error error = writeHTML(html);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void writeRTFAsync(const String& rtf, WriteCallback callback) {
|
||||
std::thread([rtf = rtf.isolatedCopy(), callback = std::move(callback)]() {
|
||||
Error error = writeRTF(rtf);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void writeImageAsync(const Vector<uint8_t>& imageData, const String& mimeType, WriteCallback callback) {
|
||||
std::thread([imageData, mimeType = mimeType.isolatedCopy(), callback = std::move(callback)]() {
|
||||
Error error = writeImage(imageData, mimeType);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void readTextAsync(ReadCallback callback) {
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
auto text = readText(error);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (text.has_value() && !text->isEmpty()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::Text;
|
||||
clipData.mimeType = "text/plain"_s;
|
||||
auto textUtf8 = text->utf8();
|
||||
clipData.data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(textUtf8.data()), textUtf8.length()));
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void readHTMLAsync(ReadCallback callback) {
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
auto html = readHTML(error);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (html.has_value() && !html->isEmpty()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::HTML;
|
||||
clipData.mimeType = "text/html"_s;
|
||||
auto htmlUtf8 = html->utf8();
|
||||
clipData.data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(htmlUtf8.data()), htmlUtf8.length()));
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void readRTFAsync(ReadCallback callback) {
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
auto rtf = readRTF(error);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (rtf.has_value() && !rtf->isEmpty()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::RTF;
|
||||
clipData.mimeType = "text/rtf"_s;
|
||||
auto rtfUtf8 = rtf->utf8();
|
||||
clipData.data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(rtfUtf8.data()), rtfUtf8.length()));
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void readImageAsync(ReadCallback callback) {
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
String mimeType;
|
||||
auto imageData = readImage(error, mimeType);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (imageData.has_value()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::Image;
|
||||
clipData.mimeType = mimeType;
|
||||
clipData.data = WTFMove(*imageData);
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
} // namespace Clipboard
|
||||
} // namespace Bun
|
||||
|
||||
#endif // OS(DARWIN)
|
||||
551
src/bun.js/bindings/ClipboardDarwin.cpp.bak
Normal file
551
src/bun.js/bindings/ClipboardDarwin.cpp.bak
Normal file
@@ -0,0 +1,551 @@
|
||||
#include "root.h"
|
||||
|
||||
#if OS(DARWIN)
|
||||
|
||||
#include "Clipboard.h"
|
||||
#include <dlfcn.h>
|
||||
#include <wtf/text/WTFString.h>
|
||||
#include <wtf/Vector.h>
|
||||
#include <wtf/NeverDestroyed.h>
|
||||
|
||||
// Forward declarations for AppKit types
|
||||
typedef struct objc_object NSObject;
|
||||
typedef struct objc_object NSPasteboard;
|
||||
typedef struct objc_object NSString;
|
||||
typedef struct objc_object NSData;
|
||||
typedef struct objc_object NSArray;
|
||||
typedef NSObject* id;
|
||||
typedef const struct objc_selector* SEL;
|
||||
typedef id (*IMP)(id, SEL, ...);
|
||||
|
||||
// Objective-C runtime functions
|
||||
extern "C" {
|
||||
id objc_getClass(const char* name);
|
||||
SEL sel_registerName(const char* str);
|
||||
id objc_msgSend(id theReceiver, SEL theSelector, ...);
|
||||
void* objc_getAssociatedObject(id object, const void* key);
|
||||
void objc_setAssociatedObject(id object, const void* key, id value, unsigned int policy);
|
||||
}
|
||||
|
||||
#define False 0
|
||||
#define True 1
|
||||
typedef signed char BOOL;
|
||||
|
||||
namespace Bun {
|
||||
namespace Clipboard {
|
||||
|
||||
using namespace WTF;
|
||||
|
||||
class AppKitFramework {
|
||||
public:
|
||||
void* handle;
|
||||
void* foundation_handle;
|
||||
|
||||
// Foundation classes and selectors
|
||||
id NSString_class;
|
||||
id NSData_class;
|
||||
id NSArray_class;
|
||||
id NSPasteboard_class;
|
||||
|
||||
// Selectors
|
||||
SEL stringWithUTF8String_sel;
|
||||
SEL UTF8String_sel;
|
||||
SEL length_sel;
|
||||
SEL dataWithBytes_length_sel;
|
||||
SEL bytes_sel;
|
||||
SEL generalPasteboard_sel;
|
||||
SEL clearContents_sel;
|
||||
SEL setString_forType_sel;
|
||||
SEL setData_forType_sel;
|
||||
SEL stringForType_sel;
|
||||
SEL dataForType_sel;
|
||||
SEL types_sel;
|
||||
SEL writeObjects_sel;
|
||||
SEL readObjectsForClasses_options_sel;
|
||||
SEL arrayWithObject_sel;
|
||||
|
||||
// Pasteboard type constants
|
||||
NSString* NSPasteboardTypeString;
|
||||
NSString* NSPasteboardTypeHTML;
|
||||
NSString* NSPasteboardTypeRTF;
|
||||
NSString* NSPasteboardTypePNG;
|
||||
NSString* NSPasteboardTypeTIFF;
|
||||
|
||||
AppKitFramework()
|
||||
: handle(nullptr)
|
||||
, foundation_handle(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
bool load()
|
||||
{
|
||||
if (handle && foundation_handle) return true;
|
||||
|
||||
// Load Foundation framework first
|
||||
foundation_handle = dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", RTLD_LAZY | RTLD_LOCAL);
|
||||
if (!foundation_handle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load AppKit framework
|
||||
handle = dlopen("/System/Library/Frameworks/AppKit.framework/AppKit", RTLD_LAZY | RTLD_LOCAL);
|
||||
if (!handle) {
|
||||
dlclose(foundation_handle);
|
||||
foundation_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!load_classes_and_selectors() || !load_constants()) {
|
||||
dlclose(handle);
|
||||
dlclose(foundation_handle);
|
||||
handle = nullptr;
|
||||
foundation_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool load_classes_and_selectors()
|
||||
{
|
||||
NSString_class = objc_getClass("NSString");
|
||||
NSData_class = objc_getClass("NSData");
|
||||
NSArray_class = objc_getClass("NSArray");
|
||||
NSPasteboard_class = objc_getClass("NSPasteboard");
|
||||
|
||||
if (!NSString_class || !NSData_class || !NSArray_class || !NSPasteboard_class) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stringWithUTF8String_sel = sel_registerName("stringWithUTF8String:");
|
||||
UTF8String_sel = sel_registerName("UTF8String");
|
||||
length_sel = sel_registerName("length");
|
||||
dataWithBytes_length_sel = sel_registerName("dataWithBytes:length:");
|
||||
bytes_sel = sel_registerName("bytes");
|
||||
generalPasteboard_sel = sel_registerName("generalPasteboard");
|
||||
clearContents_sel = sel_registerName("clearContents");
|
||||
setString_forType_sel = sel_registerName("setString:forType:");
|
||||
setData_forType_sel = sel_registerName("setData:forType:");
|
||||
stringForType_sel = sel_registerName("stringForType:");
|
||||
dataForType_sel = sel_registerName("dataForType:");
|
||||
types_sel = sel_registerName("types");
|
||||
writeObjects_sel = sel_registerName("writeObjects:");
|
||||
readObjectsForClasses_options_sel = sel_registerName("readObjectsForClasses:options:");
|
||||
arrayWithObject_sel = sel_registerName("arrayWithObject:");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool load_constants()
|
||||
{
|
||||
void* ptr;
|
||||
|
||||
ptr = dlsym(handle, "NSPasteboardTypeString");
|
||||
if (!ptr) return false;
|
||||
NSPasteboardTypeString = *(NSString**)ptr;
|
||||
|
||||
ptr = dlsym(handle, "NSPasteboardTypeHTML");
|
||||
if (!ptr) return false;
|
||||
NSPasteboardTypeHTML = *(NSString**)ptr;
|
||||
|
||||
ptr = dlsym(handle, "NSPasteboardTypeRTF");
|
||||
if (!ptr) return false;
|
||||
NSPasteboardTypeRTF = *(NSString**)ptr;
|
||||
|
||||
ptr = dlsym(handle, "NSPasteboardTypePNG");
|
||||
if (!ptr) return false;
|
||||
NSPasteboardTypePNG = *(NSString**)ptr;
|
||||
|
||||
ptr = dlsym(handle, "NSPasteboardTypeTIFF");
|
||||
if (!ptr) return false;
|
||||
NSPasteboardTypeTIFF = *(NSString**)ptr;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
static AppKitFramework* appKitFramework()
|
||||
{
|
||||
static LazyNeverDestroyed<AppKitFramework> framework;
|
||||
static std::once_flag onceFlag;
|
||||
std::call_once(onceFlag, [&] {
|
||||
framework.construct();
|
||||
if (!framework->load()) {
|
||||
// Framework failed to load, but object is still constructed
|
||||
}
|
||||
});
|
||||
return framework->handle ? &framework.get() : nullptr;
|
||||
}
|
||||
|
||||
static void updateError(Error& err, const String& message)
|
||||
{
|
||||
err.type = ErrorType::PlatformError;
|
||||
err.message = message;
|
||||
err.code = -1;
|
||||
}
|
||||
|
||||
static NSString* createNSString(const String& str)
|
||||
{
|
||||
auto* framework = appKitFramework();
|
||||
if (!framework) return nullptr;
|
||||
|
||||
auto utf8 = str.utf8();
|
||||
return (NSString*)objc_msgSend(framework->NSString_class, framework->stringWithUTF8String_sel, utf8.data());
|
||||
}
|
||||
|
||||
static String nsStringToWTFString(NSString* nsStr)
|
||||
{
|
||||
auto* framework = appKitFramework();
|
||||
if (!framework || !nsStr) return String();
|
||||
|
||||
const char* utf8Str = (const char*)objc_msgSend(nsStr, framework->UTF8String_sel);
|
||||
return String::fromUTF8(utf8Str);
|
||||
}
|
||||
|
||||
static NSPasteboard* getGeneralPasteboard()
|
||||
{
|
||||
auto* framework = appKitFramework();
|
||||
if (!framework) return nullptr;
|
||||
|
||||
return (NSPasteboard*)objc_msgSend(framework->NSPasteboard_class, framework->generalPasteboard_sel);
|
||||
}
|
||||
|
||||
Error writeText(const String& text)
|
||||
{
|
||||
Error err;
|
||||
|
||||
auto* framework = appKitFramework();
|
||||
if (!framework) {
|
||||
updateError(err, "AppKit framework not available"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
NSPasteboard* pasteboard = getGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(err, "Could not access pasteboard"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
NSString* nsText = createNSString(text);
|
||||
if (!nsText) {
|
||||
updateError(err, "Failed to create NSString"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
// Clear existing contents
|
||||
objc_msgSend(pasteboard, framework->clearContents_sel);
|
||||
|
||||
// Set string
|
||||
BOOL success = (BOOL)objc_msgSend(pasteboard, framework->setString_forType_sel, nsText, framework->NSPasteboardTypeString);
|
||||
|
||||
if (!success) {
|
||||
updateError(err, "Failed to write text to pasteboard"_s);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeHTML(const String& html)
|
||||
{
|
||||
Error err;
|
||||
|
||||
auto* framework = appKitFramework();
|
||||
if (!framework) {
|
||||
updateError(err, "AppKit framework not available"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
NSPasteboard* pasteboard = getGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(err, "Could not access pasteboard"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
NSString* nsHtml = createNSString(html);
|
||||
if (!nsHtml) {
|
||||
updateError(err, "Failed to create NSString"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
// Clear existing contents
|
||||
objc_msgSend(pasteboard, framework->clearContents_sel);
|
||||
|
||||
// Set HTML
|
||||
BOOL success = (BOOL)objc_msgSend(pasteboard, framework->setString_forType_sel, nsHtml, framework->NSPasteboardTypeHTML);
|
||||
|
||||
if (!success) {
|
||||
updateError(err, "Failed to write HTML to pasteboard"_s);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeRTF(const String& rtf)
|
||||
{
|
||||
Error err;
|
||||
|
||||
auto* framework = appKitFramework();
|
||||
if (!framework) {
|
||||
updateError(err, "AppKit framework not available"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
NSPasteboard* pasteboard = getGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(err, "Could not access pasteboard"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
auto rtfData = rtf.utf8();
|
||||
NSData* nsData = (NSData*)objc_msgSend(framework->NSData_class, framework->dataWithBytes_length_sel, rtfData.data(), rtfData.length());
|
||||
if (!nsData) {
|
||||
updateError(err, "Failed to create NSData"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
// Clear existing contents
|
||||
objc_msgSend(pasteboard, framework->clearContents_sel);
|
||||
|
||||
// Set RTF data
|
||||
BOOL success = (BOOL)objc_msgSend(pasteboard, framework->setData_forType_sel, nsData, framework->NSPasteboardTypeRTF);
|
||||
|
||||
if (!success) {
|
||||
updateError(err, "Failed to write RTF to pasteboard"_s);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeImage(const Vector<uint8_t>& imageData, const String& mimeType)
|
||||
{
|
||||
Error err;
|
||||
|
||||
auto* framework = appKitFramework();
|
||||
if (!framework) {
|
||||
updateError(err, "AppKit framework not available"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
NSPasteboard* pasteboard = getGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(err, "Could not access pasteboard"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
NSData* nsData = (NSData*)objc_msgSend(framework->NSData_class, framework->dataWithBytes_length_sel, imageData.data(), imageData.size());
|
||||
if (!nsData) {
|
||||
updateError(err, "Failed to create NSData"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
// Clear existing contents
|
||||
objc_msgSend(pasteboard, framework->clearContents_sel);
|
||||
|
||||
// Choose appropriate pasteboard type based on MIME type
|
||||
NSString* pasteboardType = framework->NSPasteboardTypePNG; // default
|
||||
if (mimeType == "image/tiff"_s) {
|
||||
pasteboardType = framework->NSPasteboardTypeTIFF;
|
||||
}
|
||||
|
||||
// Set image data
|
||||
BOOL success = (BOOL)objc_msgSend(pasteboard, framework->setData_forType_sel, nsData, pasteboardType);
|
||||
|
||||
if (!success) {
|
||||
updateError(err, "Failed to write image to pasteboard"_s);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
std::optional<String> readText(Error& error)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
auto* framework = appKitFramework();
|
||||
if (!framework) {
|
||||
updateError(error, "AppKit framework not available"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
NSPasteboard* pasteboard = getGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(error, "Could not access pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
NSString* text = (NSString*)objc_msgSend(pasteboard, framework->stringForType_sel, framework->NSPasteboardTypeString);
|
||||
if (!text) {
|
||||
updateError(error, "No text found in pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return nsStringToWTFString(text);
|
||||
}
|
||||
|
||||
std::optional<String> readHTML(Error& error)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
auto* framework = appKitFramework();
|
||||
if (!framework) {
|
||||
updateError(error, "AppKit framework not available"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
NSPasteboard* pasteboard = getGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(error, "Could not access pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
NSString* html = (NSString*)objc_msgSend(pasteboard, framework->stringForType_sel, framework->NSPasteboardTypeHTML);
|
||||
if (!html) {
|
||||
updateError(error, "No HTML found in pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return nsStringToWTFString(html);
|
||||
}
|
||||
|
||||
std::optional<String> readRTF(Error& error)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
auto* framework = appKitFramework();
|
||||
if (!framework) {
|
||||
updateError(error, "AppKit framework not available"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
NSPasteboard* pasteboard = getGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(error, "Could not access pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
NSData* rtfData = (NSData*)objc_msgSend(pasteboard, framework->dataForType_sel, framework->NSPasteboardTypeRTF);
|
||||
if (!rtfData) {
|
||||
updateError(error, "No RTF found in pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const void* bytes = objc_msgSend(rtfData, framework->bytes_sel);
|
||||
NSUInteger length = (NSUInteger)objc_msgSend(rtfData, framework->length_sel);
|
||||
|
||||
if (!bytes || !length) {
|
||||
updateError(error, "Invalid RTF data"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return String::fromUTF8(std::span<const char>(reinterpret_cast<const char*>(bytes), length));
|
||||
}
|
||||
|
||||
std::optional<Vector<uint8_t>> readImage(Error& error, String& mimeType)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
auto* framework = appKitFramework();
|
||||
if (!framework) {
|
||||
updateError(error, "AppKit framework not available"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
NSPasteboard* pasteboard = getGeneralPasteboard();
|
||||
if (!pasteboard) {
|
||||
updateError(error, "Could not access pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Try PNG first
|
||||
NSData* imageData = (NSData*)objc_msgSend(pasteboard, framework->dataForType_sel, framework->NSPasteboardTypePNG);
|
||||
if (imageData) {
|
||||
mimeType = "image/png"_s;
|
||||
} else {
|
||||
// Try TIFF
|
||||
imageData = (NSData*)objc_msgSend(pasteboard, framework->dataForType_sel, framework->NSPasteboardTypeTIFF);
|
||||
if (imageData) {
|
||||
mimeType = "image/tiff"_s;
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageData) {
|
||||
updateError(error, "No image found in pasteboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const void* bytes = objc_msgSend(imageData, framework->bytes_sel);
|
||||
NSUInteger length = (NSUInteger)objc_msgSend(imageData, framework->length_sel);
|
||||
|
||||
if (!bytes || !length) {
|
||||
updateError(error, "Invalid image data"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Vector<uint8_t> result;
|
||||
result.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(bytes), length));
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isSupported()
|
||||
{
|
||||
return appKitFramework() != nullptr;
|
||||
}
|
||||
|
||||
Vector<DataType> getSupportedTypes()
|
||||
{
|
||||
Vector<DataType> types;
|
||||
if (isSupported()) {
|
||||
types.append(DataType::Text);
|
||||
types.append(DataType::HTML);
|
||||
types.append(DataType::RTF);
|
||||
types.append(DataType::Image);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
// Async implementations - forward to common async implementation
|
||||
void writeTextAsync(const String& text, WriteCallback callback)
|
||||
{
|
||||
executeWriteTextAsync(text, std::move(callback));
|
||||
}
|
||||
|
||||
void writeHTMLAsync(const String& html, WriteCallback callback)
|
||||
{
|
||||
executeWriteHTMLAsync(html, std::move(callback));
|
||||
}
|
||||
|
||||
void writeRTFAsync(const String& rtf, WriteCallback callback)
|
||||
{
|
||||
executeWriteRTFAsync(rtf, std::move(callback));
|
||||
}
|
||||
|
||||
void writeImageAsync(const Vector<uint8_t>& imageData, const String& mimeType, WriteCallback callback)
|
||||
{
|
||||
executeWriteImageAsync(imageData, mimeType, std::move(callback));
|
||||
}
|
||||
|
||||
void readTextAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadTextAsync(std::move(callback));
|
||||
}
|
||||
|
||||
void readHTMLAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadHTMLAsync(std::move(callback));
|
||||
}
|
||||
|
||||
void readRTFAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadRTFAsync(std::move(callback));
|
||||
}
|
||||
|
||||
void readImageAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadImageAsync(std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace Clipboard
|
||||
} // namespace Bun
|
||||
|
||||
#endif // OS(DARWIN)
|
||||
429
src/bun.js/bindings/ClipboardLinux.cpp
Normal file
429
src/bun.js/bindings/ClipboardLinux.cpp
Normal file
@@ -0,0 +1,429 @@
|
||||
#include "root.h"
|
||||
#include "Clipboard.h"
|
||||
#include <wtf/text/WTFString.h>
|
||||
#include <wtf/Vector.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
|
||||
namespace Bun {
|
||||
namespace Clipboard {
|
||||
|
||||
using namespace WTF;
|
||||
|
||||
enum class ClipboardBackend {
|
||||
None,
|
||||
XClip, // For X11 environments
|
||||
WlClip // For Wayland environments
|
||||
};
|
||||
|
||||
static ClipboardBackend detected_backend = ClipboardBackend::None;
|
||||
static bool backend_detection_done = false;
|
||||
|
||||
static ClipboardBackend detectClipboardBackend() {
|
||||
if (backend_detection_done) return detected_backend;
|
||||
backend_detection_done = true;
|
||||
|
||||
// Check for Wayland first
|
||||
const char* wayland_display = getenv("WAYLAND_DISPLAY");
|
||||
if (wayland_display && strlen(wayland_display) > 0) {
|
||||
// Check if wl-copy is available
|
||||
if (system("which wl-copy > /dev/null 2>&1") == 0) {
|
||||
detected_backend = ClipboardBackend::WlClip;
|
||||
return detected_backend;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for X11
|
||||
const char* x11_display = getenv("DISPLAY");
|
||||
if (x11_display && strlen(x11_display) > 0) {
|
||||
// Check if xclip is available
|
||||
if (system("which xclip > /dev/null 2>&1") == 0) {
|
||||
detected_backend = ClipboardBackend::XClip;
|
||||
return detected_backend;
|
||||
}
|
||||
}
|
||||
|
||||
detected_backend = ClipboardBackend::None;
|
||||
return detected_backend;
|
||||
}
|
||||
|
||||
#if OS(LINUX)
|
||||
// Execute command using standard fork/exec - simpler and more reliable for clipboard operations
|
||||
static bool executeCommand(const std::vector<const char*>& args, const std::string& input = "", std::string* output = nullptr) {
|
||||
int input_pipe[2] = {-1, -1};
|
||||
int output_pipe[2] = {-1, -1};
|
||||
|
||||
// Create pipes if needed
|
||||
if (!input.empty() && pipe(input_pipe) == -1) {
|
||||
return false;
|
||||
}
|
||||
if (output && pipe(output_pipe) == -1) {
|
||||
if (input_pipe[0] != -1) {
|
||||
close(input_pipe[0]);
|
||||
close(input_pipe[1]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build argv
|
||||
std::vector<char*> argv;
|
||||
for (const char* arg : args) {
|
||||
argv.push_back(const_cast<char*>(arg));
|
||||
}
|
||||
argv.push_back(nullptr);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == -1) {
|
||||
// Fork failed
|
||||
if (input_pipe[0] != -1) {
|
||||
close(input_pipe[0]);
|
||||
close(input_pipe[1]);
|
||||
}
|
||||
if (output_pipe[0] != -1) {
|
||||
close(output_pipe[0]);
|
||||
close(output_pipe[1]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
// Child process
|
||||
if (!input.empty()) {
|
||||
dup2(input_pipe[0], STDIN_FILENO);
|
||||
close(input_pipe[0]);
|
||||
close(input_pipe[1]);
|
||||
}
|
||||
if (output) {
|
||||
dup2(output_pipe[1], STDOUT_FILENO);
|
||||
close(output_pipe[0]);
|
||||
close(output_pipe[1]);
|
||||
}
|
||||
|
||||
execvp(argv[0], argv.data());
|
||||
_exit(127); // execvp failed
|
||||
}
|
||||
|
||||
// Parent process - handle pipes
|
||||
if (!input.empty()) {
|
||||
close(input_pipe[0]);
|
||||
if (write(input_pipe[1], input.c_str(), input.length()) == -1) {
|
||||
// Handle write error - but continue
|
||||
}
|
||||
close(input_pipe[1]);
|
||||
}
|
||||
|
||||
if (output) {
|
||||
close(output_pipe[1]);
|
||||
char buffer[4096];
|
||||
ssize_t bytes_read;
|
||||
output->clear();
|
||||
while ((bytes_read = read(output_pipe[0], buffer, sizeof(buffer))) > 0) {
|
||||
output->append(buffer, bytes_read);
|
||||
}
|
||||
close(output_pipe[0]);
|
||||
}
|
||||
|
||||
// Wait for child process
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
#else
|
||||
// Fallback for non-Linux platforms - use basic system() calls
|
||||
static bool executeCommand(const std::vector<const char*>& args, const std::string& input = "", std::string* output = nullptr) {
|
||||
// This is a simplified fallback - not used on Linux
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Simple clipboard operations using external tools
|
||||
static bool writeToClipboard(const std::string& data, const std::string& mime_type = "text/plain") {
|
||||
ClipboardBackend backend = detectClipboardBackend();
|
||||
|
||||
switch (backend) {
|
||||
case ClipboardBackend::XClip: {
|
||||
std::vector<const char*> args;
|
||||
if (mime_type == "text/html") {
|
||||
args = {"xclip", "-selection", "clipboard", "-t", "text/html"};
|
||||
} else if (mime_type == "text/rtf") {
|
||||
args = {"xclip", "-selection", "clipboard", "-t", "text/rtf"};
|
||||
} else {
|
||||
args = {"xclip", "-selection", "clipboard"};
|
||||
}
|
||||
return executeCommand(args, data);
|
||||
}
|
||||
case ClipboardBackend::WlClip: {
|
||||
std::vector<const char*> args;
|
||||
if (mime_type == "text/html") {
|
||||
args = {"wl-copy", "-t", "text/html"};
|
||||
} else if (mime_type == "text/rtf") {
|
||||
args = {"wl-copy", "-t", "text/rtf"};
|
||||
} else {
|
||||
args = {"wl-copy"};
|
||||
}
|
||||
return executeCommand(args, data);
|
||||
}
|
||||
case ClipboardBackend::None:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static std::optional<std::string> readFromClipboard(const std::string& mime_type = "text/plain") {
|
||||
ClipboardBackend backend = detectClipboardBackend();
|
||||
std::string output;
|
||||
|
||||
switch (backend) {
|
||||
case ClipboardBackend::XClip: {
|
||||
std::vector<const char*> args;
|
||||
if (mime_type == "text/html") {
|
||||
args = {"xclip", "-selection", "clipboard", "-o", "-t", "text/html"};
|
||||
} else if (mime_type == "text/rtf") {
|
||||
args = {"xclip", "-selection", "clipboard", "-o", "-t", "text/rtf"};
|
||||
} else {
|
||||
args = {"xclip", "-selection", "clipboard", "-o"};
|
||||
}
|
||||
if (executeCommand(args, "", &output)) {
|
||||
return output;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClipboardBackend::WlClip: {
|
||||
std::vector<const char*> args;
|
||||
if (mime_type == "text/html") {
|
||||
args = {"wl-paste", "-t", "text/html"};
|
||||
} else if (mime_type == "text/rtf") {
|
||||
args = {"wl-paste", "-t", "text/rtf"};
|
||||
} else {
|
||||
args = {"wl-paste"};
|
||||
}
|
||||
if (executeCommand(args, "", &output)) {
|
||||
return output;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClipboardBackend::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Public API implementations
|
||||
Error writeText(const String& text) {
|
||||
Error err;
|
||||
auto utf8Data = text.utf8();
|
||||
std::string textData(utf8Data.data(), utf8Data.length());
|
||||
|
||||
bool success = writeToClipboard(textData, "text/plain");
|
||||
err.type = success ? ErrorType::None : ErrorType::PlatformError;
|
||||
if (!success) {
|
||||
err.message = "Failed to write text to clipboard"_s;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeHTML(const String& html) {
|
||||
Error err;
|
||||
auto utf8Data = html.utf8();
|
||||
std::string htmlData(utf8Data.data(), utf8Data.length());
|
||||
|
||||
bool success = writeToClipboard(htmlData, "text/html");
|
||||
err.type = success ? ErrorType::None : ErrorType::PlatformError;
|
||||
if (!success) {
|
||||
err.message = "Failed to write HTML to clipboard"_s;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeRTF(const String& rtf) {
|
||||
Error err;
|
||||
auto utf8Data = rtf.utf8();
|
||||
std::string rtfData(utf8Data.data(), utf8Data.length());
|
||||
|
||||
bool success = writeToClipboard(rtfData, "text/rtf");
|
||||
err.type = success ? ErrorType::None : ErrorType::PlatformError;
|
||||
if (!success) {
|
||||
err.message = "Failed to write RTF to clipboard"_s;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeImage(const Vector<uint8_t>& imageData, const String& mimeType) {
|
||||
Error err;
|
||||
err.type = ErrorType::NotSupported;
|
||||
err.message = "Image clipboard operations not yet implemented on Linux"_s;
|
||||
return err;
|
||||
}
|
||||
|
||||
std::optional<String> readText(Error& error) {
|
||||
auto result = readFromClipboard("text/plain");
|
||||
if (!result.has_value()) {
|
||||
error.type = ErrorType::PlatformError;
|
||||
error.message = "Failed to read text from clipboard"_s;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
error.type = ErrorType::None;
|
||||
return String::fromUTF8(std::span<const unsigned char>(reinterpret_cast<const unsigned char*>(result->c_str()), result->length()));
|
||||
}
|
||||
|
||||
std::optional<String> readHTML(Error& error) {
|
||||
auto result = readFromClipboard("text/html");
|
||||
if (!result.has_value()) {
|
||||
error.type = ErrorType::PlatformError;
|
||||
error.message = "Failed to read HTML from clipboard"_s;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
error.type = ErrorType::None;
|
||||
return String::fromUTF8(std::span<const unsigned char>(reinterpret_cast<const unsigned char*>(result->c_str()), result->length()));
|
||||
}
|
||||
|
||||
std::optional<String> readRTF(Error& error) {
|
||||
auto result = readFromClipboard("text/rtf");
|
||||
if (!result.has_value()) {
|
||||
error.type = ErrorType::PlatformError;
|
||||
error.message = "Failed to read RTF from clipboard"_s;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
error.type = ErrorType::None;
|
||||
return String::fromUTF8(std::span<const unsigned char>(reinterpret_cast<const unsigned char*>(result->c_str()), result->length()));
|
||||
}
|
||||
|
||||
std::optional<Vector<uint8_t>> readImage(Error& error, String& mimeType) {
|
||||
error.type = ErrorType::NotSupported;
|
||||
error.message = "Image clipboard operations not yet implemented on Linux"_s;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool isSupported() {
|
||||
return detectClipboardBackend() != ClipboardBackend::None;
|
||||
}
|
||||
|
||||
Vector<DataType> getSupportedTypes() {
|
||||
Vector<DataType> types;
|
||||
if (isSupported()) {
|
||||
types.append(DataType::Text);
|
||||
types.append(DataType::HTML);
|
||||
types.append(DataType::RTF);
|
||||
// Image support can be added later
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
// Async implementations using std::thread
|
||||
void writeTextAsync(const String& text, WriteCallback callback) {
|
||||
std::thread([text = text.isolatedCopy(), callback = std::move(callback)]() {
|
||||
Error error = writeText(text);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void writeHTMLAsync(const String& html, WriteCallback callback) {
|
||||
std::thread([html = html.isolatedCopy(), callback = std::move(callback)]() {
|
||||
Error error = writeHTML(html);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void writeRTFAsync(const String& rtf, WriteCallback callback) {
|
||||
std::thread([rtf = rtf.isolatedCopy(), callback = std::move(callback)]() {
|
||||
Error error = writeRTF(rtf);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void writeImageAsync(const Vector<uint8_t>& imageData, const String& mimeType, WriteCallback callback) {
|
||||
std::thread([imageData, mimeType = mimeType.isolatedCopy(), callback = std::move(callback)]() {
|
||||
Error error = writeImage(imageData, mimeType);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void readTextAsync(ReadCallback callback) {
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
auto text = readText(error);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (text.has_value() && !text->isEmpty()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::Text;
|
||||
clipData.mimeType = "text/plain"_s;
|
||||
auto textUtf8 = text->utf8();
|
||||
clipData.data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(textUtf8.data()), textUtf8.length()));
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void readHTMLAsync(ReadCallback callback) {
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
auto html = readHTML(error);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (html.has_value() && !html->isEmpty()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::HTML;
|
||||
clipData.mimeType = "text/html"_s;
|
||||
auto htmlUtf8 = html->utf8();
|
||||
clipData.data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(htmlUtf8.data()), htmlUtf8.length()));
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void readRTFAsync(ReadCallback callback) {
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
auto rtf = readRTF(error);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (rtf.has_value() && !rtf->isEmpty()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::RTF;
|
||||
clipData.mimeType = "text/rtf"_s;
|
||||
auto rtfUtf8 = rtf->utf8();
|
||||
clipData.data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(rtfUtf8.data()), rtfUtf8.length()));
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void readImageAsync(ReadCallback callback) {
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
String mimeType;
|
||||
auto imageData = readImage(error, mimeType);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (imageData.has_value()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::Image;
|
||||
clipData.mimeType = mimeType;
|
||||
clipData.data = WTFMove(*imageData);
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
} // namespace Clipboard
|
||||
} // namespace Bun
|
||||
521
src/bun.js/bindings/ClipboardLinux.cpp.bak
Normal file
521
src/bun.js/bindings/ClipboardLinux.cpp.bak
Normal file
@@ -0,0 +1,521 @@
|
||||
#include "root.h"
|
||||
|
||||
#if OS(LINUX)
|
||||
|
||||
#include "Clipboard.h"
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
#include <wtf/text/WTFString.h>
|
||||
#include <wtf/Vector.h>
|
||||
#include <wtf/NeverDestroyed.h>
|
||||
|
||||
// X11 and related types
|
||||
typedef struct _XDisplay Display;
|
||||
typedef unsigned long Window;
|
||||
typedef unsigned long Atom;
|
||||
typedef int Bool;
|
||||
typedef unsigned char* XPointer;
|
||||
|
||||
struct XEvent;
|
||||
|
||||
// Minimal definitions to avoid including X11 headers
|
||||
typedef struct {
|
||||
int type;
|
||||
unsigned long serial;
|
||||
Bool send_event;
|
||||
Display* display;
|
||||
Window owner;
|
||||
Window requestor;
|
||||
Atom selection;
|
||||
Atom target;
|
||||
Atom property;
|
||||
unsigned long time;
|
||||
} XSelectionRequestEvent;
|
||||
|
||||
typedef struct {
|
||||
int type;
|
||||
unsigned long serial;
|
||||
Bool send_event;
|
||||
Display* display;
|
||||
Window requestor;
|
||||
Atom selection;
|
||||
Atom target;
|
||||
Atom property;
|
||||
unsigned long time;
|
||||
} XSelectionEvent;
|
||||
|
||||
namespace Bun {
|
||||
namespace Clipboard {
|
||||
|
||||
using namespace WTF;
|
||||
|
||||
class X11Framework {
|
||||
public:
|
||||
void* x11_handle;
|
||||
|
||||
// X11 function pointers
|
||||
Display* (*XOpenDisplay)(const char* display_name);
|
||||
int (*XCloseDisplay)(Display* display);
|
||||
Window (*XDefaultRootWindow)(Display* display);
|
||||
Atom (*XInternAtom)(Display* display, const char* atom_name, Bool only_if_exists);
|
||||
char* (*XGetAtomName)(Display* display, Atom atom);
|
||||
int (*XSetSelectionOwner)(Display* display, Atom selection, Window owner, unsigned long time);
|
||||
Window (*XGetSelectionOwner)(Display* display, Atom selection);
|
||||
int (*XConvertSelection)(Display* display, Atom selection, Atom target, Atom property, Window requestor, unsigned long time);
|
||||
int (*XGetWindowProperty)(Display* display, Window w, Atom property, long long_offset, long long_length, Bool delete_prop, Atom req_type, Atom* actual_type_return, int* actual_format_return, unsigned long* nitems_return, unsigned long* bytes_after_return, unsigned char** prop_return);
|
||||
int (*XChangeProperty)(Display* display, Window w, Atom property, Atom type, int format, int mode, const unsigned char* data, int nelements);
|
||||
int (*XSendEvent)(Display* display, Window w, Bool propagate, long event_mask, XEvent* event_send);
|
||||
int (*XFlush)(Display* display);
|
||||
int (*XFree)(void* data);
|
||||
int (*XNextEvent)(Display* display, XEvent* event_return);
|
||||
int (*XPending)(Display* display);
|
||||
int (*XSelectInput)(Display* display, Window w, long event_mask);
|
||||
Window (*XCreateSimpleWindow)(Display* display, Window parent, int x, int y, unsigned int width, unsigned int height, unsigned int border_width, unsigned long border, unsigned long background);
|
||||
int (*XMapWindow)(Display* display, Window w);
|
||||
int (*XDestroyWindow)(Display* display, Window w);
|
||||
unsigned long (*XCurrentTime);
|
||||
|
||||
// Atoms we'll need
|
||||
Atom CLIPBOARD;
|
||||
Atom PRIMARY;
|
||||
Atom UTF8_STRING;
|
||||
Atom STRING;
|
||||
Atom TEXT;
|
||||
Atom TARGETS;
|
||||
Atom MULTIPLE;
|
||||
Atom TIMESTAMP;
|
||||
Atom text_html;
|
||||
Atom text_rtf;
|
||||
Atom image_png;
|
||||
|
||||
Display* display;
|
||||
Window window;
|
||||
|
||||
X11Framework()
|
||||
: x11_handle(nullptr)
|
||||
, display(nullptr)
|
||||
, window(0)
|
||||
{
|
||||
}
|
||||
|
||||
bool load()
|
||||
{
|
||||
if (x11_handle && display) return true;
|
||||
|
||||
x11_handle = dlopen("libX11.so.6", RTLD_LAZY | RTLD_LOCAL);
|
||||
if (!x11_handle) {
|
||||
x11_handle = dlopen("libX11.so", RTLD_LAZY | RTLD_LOCAL);
|
||||
if (!x11_handle) return false;
|
||||
}
|
||||
|
||||
if (!load_functions()) {
|
||||
dlclose(x11_handle);
|
||||
x11_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Open display
|
||||
display = XOpenDisplay(nullptr);
|
||||
if (!display) {
|
||||
dlclose(x11_handle);
|
||||
x11_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a simple window for clipboard operations
|
||||
Window root = XDefaultRootWindow(display);
|
||||
window = XCreateSimpleWindow(display, root, 0, 0, 1, 1, 0, 0, 0);
|
||||
if (!window) {
|
||||
XCloseDisplay(display);
|
||||
display = nullptr;
|
||||
dlclose(x11_handle);
|
||||
x11_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize atoms
|
||||
if (!init_atoms()) {
|
||||
XDestroyWindow(display, window);
|
||||
XCloseDisplay(display);
|
||||
display = nullptr;
|
||||
dlclose(x11_handle);
|
||||
x11_handle = nullptr;
|
||||
window = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
~X11Framework()
|
||||
{
|
||||
if (display) {
|
||||
if (window) {
|
||||
XDestroyWindow(display, window);
|
||||
}
|
||||
XCloseDisplay(display);
|
||||
}
|
||||
if (x11_handle) {
|
||||
dlclose(x11_handle);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
bool load_functions()
|
||||
{
|
||||
XOpenDisplay = (Display* (*)(const char*))dlsym(x11_handle, "XOpenDisplay");
|
||||
XCloseDisplay = (int (*)(Display*))dlsym(x11_handle, "XCloseDisplay");
|
||||
XDefaultRootWindow = (Window (*)(Display*))dlsym(x11_handle, "XDefaultRootWindow");
|
||||
XInternAtom = (Atom (*)(Display*, const char*, Bool))dlsym(x11_handle, "XInternAtom");
|
||||
XGetAtomName = (char* (*)(Display*, Atom))dlsym(x11_handle, "XGetAtomName");
|
||||
XSetSelectionOwner = (int (*)(Display*, Atom, Window, unsigned long))dlsym(x11_handle, "XSetSelectionOwner");
|
||||
XGetSelectionOwner = (Window (*)(Display*, Atom))dlsym(x11_handle, "XGetSelectionOwner");
|
||||
XConvertSelection = (int (*)(Display*, Atom, Atom, Atom, Window, unsigned long))dlsym(x11_handle, "XConvertSelection");
|
||||
XGetWindowProperty = (int (*)(Display*, Window, Atom, long, long, Bool, Atom, Atom*, int*, unsigned long*, unsigned long*, unsigned char**))dlsym(x11_handle, "XGetWindowProperty");
|
||||
XChangeProperty = (int (*)(Display*, Window, Atom, Atom, int, int, const unsigned char*, int))dlsym(x11_handle, "XChangeProperty");
|
||||
XSendEvent = (int (*)(Display*, Window, Bool, long, XEvent*))dlsym(x11_handle, "XSendEvent");
|
||||
XFlush = (int (*)(Display*))dlsym(x11_handle, "XFlush");
|
||||
XFree = (int (*)(void*))dlsym(x11_handle, "XFree");
|
||||
XNextEvent = (int (*)(Display*, XEvent*))dlsym(x11_handle, "XNextEvent");
|
||||
XPending = (int (*)(Display*))dlsym(x11_handle, "XPending");
|
||||
XSelectInput = (int (*)(Display*, Window, long))dlsym(x11_handle, "XSelectInput");
|
||||
XCreateSimpleWindow = (Window (*)(Display*, Window, int, int, unsigned int, unsigned int, unsigned int, unsigned long, unsigned long))dlsym(x11_handle, "XCreateSimpleWindow");
|
||||
XMapWindow = (int (*)(Display*, Window))dlsym(x11_handle, "XMapWindow");
|
||||
XDestroyWindow = (int (*)(Display*, Window))dlsym(x11_handle, "XDestroyWindow");
|
||||
|
||||
return XOpenDisplay && XCloseDisplay && XDefaultRootWindow && XInternAtom && XGetAtomName &&
|
||||
XSetSelectionOwner && XGetSelectionOwner && XConvertSelection && XGetWindowProperty &&
|
||||
XChangeProperty && XSendEvent && XFlush && XFree && XNextEvent && XPending &&
|
||||
XSelectInput && XCreateSimpleWindow && XMapWindow && XDestroyWindow;
|
||||
}
|
||||
|
||||
bool init_atoms()
|
||||
{
|
||||
CLIPBOARD = XInternAtom(display, "CLIPBOARD", False);
|
||||
PRIMARY = XInternAtom(display, "PRIMARY", False);
|
||||
UTF8_STRING = XInternAtom(display, "UTF8_STRING", False);
|
||||
STRING = XInternAtom(display, "STRING", False);
|
||||
TEXT = XInternAtom(display, "TEXT", False);
|
||||
TARGETS = XInternAtom(display, "TARGETS", False);
|
||||
MULTIPLE = XInternAtom(display, "MULTIPLE", False);
|
||||
TIMESTAMP = XInternAtom(display, "TIMESTAMP", False);
|
||||
text_html = XInternAtom(display, "text/html", False);
|
||||
text_rtf = XInternAtom(display, "text/rtf", False);
|
||||
image_png = XInternAtom(display, "image/png", False);
|
||||
|
||||
return CLIPBOARD && PRIMARY && UTF8_STRING && STRING && TEXT &&
|
||||
TARGETS && MULTIPLE && TIMESTAMP && text_html && text_rtf && image_png;
|
||||
}
|
||||
};
|
||||
|
||||
static X11Framework* x11Framework()
|
||||
{
|
||||
static LazyNeverDestroyed<X11Framework> framework;
|
||||
static std::once_flag onceFlag;
|
||||
std::call_once(onceFlag, [&] {
|
||||
framework.construct();
|
||||
if (!framework->load()) {
|
||||
// Framework failed to load, but object is still constructed
|
||||
}
|
||||
});
|
||||
return framework->display ? &framework.get() : nullptr;
|
||||
}
|
||||
|
||||
static void updateError(Error& err, const String& message)
|
||||
{
|
||||
err.type = ErrorType::PlatformError;
|
||||
err.message = message;
|
||||
err.code = -1;
|
||||
}
|
||||
|
||||
static bool setClipboardData(const Vector<uint8_t>& data, Atom target)
|
||||
{
|
||||
auto* framework = x11Framework();
|
||||
if (!framework) return false;
|
||||
|
||||
// Set the data as a property on our window
|
||||
Atom property = framework->XInternAtom(framework->display, "BUN_CLIPBOARD_DATA", False);
|
||||
framework->XChangeProperty(framework->display, framework->window, property, target, 8,
|
||||
0 /* PropModeReplace */, data.data(), data.size());
|
||||
|
||||
// Take ownership of the clipboard
|
||||
framework->XSetSelectionOwner(framework->display, framework->CLIPBOARD, framework->window, 0 /* CurrentTime */);
|
||||
|
||||
// Verify we own it
|
||||
Window owner = framework->XGetSelectionOwner(framework->display, framework->CLIPBOARD);
|
||||
framework->XFlush(framework->display);
|
||||
|
||||
return owner == framework->window;
|
||||
}
|
||||
|
||||
static std::optional<Vector<uint8_t>> getClipboardData(Atom target)
|
||||
{
|
||||
auto* framework = x11Framework();
|
||||
if (!framework) return std::nullopt;
|
||||
|
||||
// Check if anyone owns the clipboard
|
||||
Window owner = framework->XGetSelectionOwner(framework->display, framework->CLIPBOARD);
|
||||
if (owner == 0) return std::nullopt; // No owner
|
||||
|
||||
// Request the selection
|
||||
Atom property = framework->XInternAtom(framework->display, "BUN_CLIPBOARD_PROPERTY", False);
|
||||
framework->XConvertSelection(framework->display, framework->CLIPBOARD, target, property,
|
||||
framework->window, 0 /* CurrentTime */);
|
||||
framework->XFlush(framework->display);
|
||||
|
||||
// Wait for the SelectionNotify event (simplified approach)
|
||||
// In a real implementation, you'd need proper event handling
|
||||
// For now, just try to read the property immediately
|
||||
usleep(10000); // Small delay to allow the selection to be converted
|
||||
|
||||
Atom actual_type;
|
||||
int actual_format;
|
||||
unsigned long nitems;
|
||||
unsigned long bytes_after;
|
||||
unsigned char* prop = nullptr;
|
||||
|
||||
int result = framework->XGetWindowProperty(framework->display, framework->window, property,
|
||||
0, 65536, True, 0 /* AnyPropertyType */,
|
||||
&actual_type, &actual_format, &nitems,
|
||||
&bytes_after, &prop);
|
||||
|
||||
if (result != 0 /* Success */ || !prop || nitems == 0) {
|
||||
if (prop) framework->XFree(prop);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Vector<uint8_t> data;
|
||||
data.append(std::span<const uint8_t>(prop, nitems));
|
||||
framework->XFree(prop);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
Error writeText(const String& text)
|
||||
{
|
||||
Error err;
|
||||
|
||||
auto* framework = x11Framework();
|
||||
if (!framework) {
|
||||
updateError(err, "X11 not available"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
auto textUtf8 = text.utf8();
|
||||
Vector<uint8_t> data;
|
||||
data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(textUtf8.data()), textUtf8.length()));
|
||||
|
||||
if (!setClipboardData(data, framework->UTF8_STRING)) {
|
||||
updateError(err, "Failed to set clipboard text"_s);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeHTML(const String& html)
|
||||
{
|
||||
Error err;
|
||||
|
||||
auto* framework = x11Framework();
|
||||
if (!framework) {
|
||||
updateError(err, "X11 not available"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
auto htmlUtf8 = html.utf8();
|
||||
Vector<uint8_t> data;
|
||||
data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(htmlUtf8.data()), htmlUtf8.length()));
|
||||
|
||||
if (!setClipboardData(data, framework->text_html)) {
|
||||
updateError(err, "Failed to set clipboard HTML"_s);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeRTF(const String& rtf)
|
||||
{
|
||||
Error err;
|
||||
|
||||
auto* framework = x11Framework();
|
||||
if (!framework) {
|
||||
updateError(err, "X11 not available"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
auto rtfUtf8 = rtf.utf8();
|
||||
Vector<uint8_t> data;
|
||||
data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(rtfUtf8.data()), rtfUtf8.length()));
|
||||
|
||||
if (!setClipboardData(data, framework->text_rtf)) {
|
||||
updateError(err, "Failed to set clipboard RTF"_s);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeImage(const Vector<uint8_t>& imageData, const String& mimeType)
|
||||
{
|
||||
Error err;
|
||||
|
||||
auto* framework = x11Framework();
|
||||
if (!framework) {
|
||||
updateError(err, "X11 not available"_s);
|
||||
return err;
|
||||
}
|
||||
|
||||
Atom target = framework->image_png; // Default to PNG
|
||||
if (mimeType == "image/png"_s) {
|
||||
target = framework->image_png;
|
||||
}
|
||||
|
||||
if (!setClipboardData(imageData, target)) {
|
||||
updateError(err, "Failed to set clipboard image"_s);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
std::optional<String> readText(Error& error)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
auto* framework = x11Framework();
|
||||
if (!framework) {
|
||||
updateError(error, "X11 not available"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto data = getClipboardData(framework->UTF8_STRING);
|
||||
if (!data) {
|
||||
// Try STRING format as fallback
|
||||
data = getClipboardData(framework->STRING);
|
||||
if (!data) {
|
||||
updateError(error, "No text found in clipboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
return String::fromUTF8(std::span<const char>(reinterpret_cast<const char*>(data->data()), data->size()));
|
||||
}
|
||||
|
||||
std::optional<String> readHTML(Error& error)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
auto* framework = x11Framework();
|
||||
if (!framework) {
|
||||
updateError(error, "X11 not available"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto data = getClipboardData(framework->text_html);
|
||||
if (!data) {
|
||||
updateError(error, "No HTML found in clipboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return String::fromUTF8(std::span<const char>(reinterpret_cast<const char*>(data->data()), data->size()));
|
||||
}
|
||||
|
||||
std::optional<String> readRTF(Error& error)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
auto* framework = x11Framework();
|
||||
if (!framework) {
|
||||
updateError(error, "X11 not available"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto data = getClipboardData(framework->text_rtf);
|
||||
if (!data) {
|
||||
updateError(error, "No RTF found in clipboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return String::fromUTF8(std::span<const char>(reinterpret_cast<const char*>(data->data()), data->size()));
|
||||
}
|
||||
|
||||
std::optional<Vector<uint8_t>> readImage(Error& error, String& mimeType)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
auto* framework = x11Framework();
|
||||
if (!framework) {
|
||||
updateError(error, "X11 not available"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto data = getClipboardData(framework->image_png);
|
||||
if (data) {
|
||||
mimeType = "image/png"_s;
|
||||
return data;
|
||||
}
|
||||
|
||||
updateError(error, "No image found in clipboard"_s);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool isSupported()
|
||||
{
|
||||
return x11Framework() != nullptr;
|
||||
}
|
||||
|
||||
Vector<DataType> getSupportedTypes()
|
||||
{
|
||||
Vector<DataType> types;
|
||||
if (isSupported()) {
|
||||
types.append(DataType::Text);
|
||||
types.append(DataType::HTML);
|
||||
types.append(DataType::RTF);
|
||||
types.append(DataType::Image);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
// Async implementations - forward to common async implementation
|
||||
void writeTextAsync(const String& text, WriteCallback callback)
|
||||
{
|
||||
executeWriteTextAsync(text, std::move(callback));
|
||||
}
|
||||
|
||||
void writeHTMLAsync(const String& html, WriteCallback callback)
|
||||
{
|
||||
executeWriteHTMLAsync(html, std::move(callback));
|
||||
}
|
||||
|
||||
void writeRTFAsync(const String& rtf, WriteCallback callback)
|
||||
{
|
||||
executeWriteRTFAsync(rtf, std::move(callback));
|
||||
}
|
||||
|
||||
void writeImageAsync(const Vector<uint8_t>& imageData, const String& mimeType, WriteCallback callback)
|
||||
{
|
||||
executeWriteImageAsync(imageData, mimeType, std::move(callback));
|
||||
}
|
||||
|
||||
void readTextAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadTextAsync(std::move(callback));
|
||||
}
|
||||
|
||||
void readHTMLAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadHTMLAsync(std::move(callback));
|
||||
}
|
||||
|
||||
void readRTFAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadRTFAsync(std::move(callback));
|
||||
}
|
||||
|
||||
void readImageAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadImageAsync(std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace Clipboard
|
||||
} // namespace Bun
|
||||
|
||||
#endif // OS(LINUX)
|
||||
541
src/bun.js/bindings/ClipboardWindows.cpp
Normal file
541
src/bun.js/bindings/ClipboardWindows.cpp
Normal file
@@ -0,0 +1,541 @@
|
||||
#include "root.h"
|
||||
|
||||
#if OS(WINDOWS)
|
||||
|
||||
#include "Clipboard.h"
|
||||
#include <windows.h>
|
||||
#include <wtf/text/WTFString.h>
|
||||
#include <wtf/Vector.h>
|
||||
#include <wtf/text/StringView.h>
|
||||
#include <thread>
|
||||
|
||||
namespace Bun {
|
||||
namespace Clipboard {
|
||||
|
||||
using namespace WTF;
|
||||
|
||||
static void updateError(Error& err, const String& message, DWORD code = 0)
|
||||
{
|
||||
err.type = ErrorType::PlatformError;
|
||||
err.message = message;
|
||||
err.code = static_cast<int>(code);
|
||||
}
|
||||
|
||||
class WindowsClipboard {
|
||||
public:
|
||||
static bool open()
|
||||
{
|
||||
return OpenClipboard(nullptr) != 0;
|
||||
}
|
||||
|
||||
static void close()
|
||||
{
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
static bool clear()
|
||||
{
|
||||
return EmptyClipboard() != 0;
|
||||
}
|
||||
};
|
||||
|
||||
Error writeText(const String& text)
|
||||
{
|
||||
Error err;
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(err, "Failed to open clipboard"_s, GetLastError());
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::clear()) {
|
||||
updateError(err, "Failed to clear clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
// Convert to UTF-16
|
||||
auto textSize = (text.length() + 1) * sizeof(wchar_t);
|
||||
|
||||
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, textSize);
|
||||
if (!hGlobal) {
|
||||
updateError(err, "Failed to allocate memory"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
wchar_t* buffer = static_cast<wchar_t*>(GlobalLock(hGlobal));
|
||||
if (!buffer) {
|
||||
updateError(err, "Failed to lock memory"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
// Copy UTF-16 data
|
||||
Vector<UChar> characters = text.charactersWithNullTermination();
|
||||
memcpy(buffer, characters.data(), text.length() * sizeof(UChar));
|
||||
buffer[text.length()] = L'\0';
|
||||
GlobalUnlock(hGlobal);
|
||||
|
||||
if (!SetClipboardData(CF_UNICODETEXT, hGlobal)) {
|
||||
updateError(err, "Failed to set clipboard data"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeHTML(const String& html)
|
||||
{
|
||||
Error err;
|
||||
|
||||
// Register CF_HTML format if not already registered
|
||||
static UINT CF_HTML = RegisterClipboardFormat(L"HTML Format");
|
||||
if (!CF_HTML) {
|
||||
updateError(err, "Failed to register HTML clipboard format"_s, GetLastError());
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(err, "Failed to open clipboard"_s, GetLastError());
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::clear()) {
|
||||
updateError(err, "Failed to clear clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
// Create CF_HTML format
|
||||
auto htmlUtf8 = html.utf8();
|
||||
String htmlHeader = makeString(
|
||||
"Version:0.9\r\n"
|
||||
"StartHTML:0000000105\r\n"
|
||||
"EndHTML:"_s, String::number(105 + htmlUtf8.length()), "\r\n"
|
||||
"StartFragment:0000000105\r\n"
|
||||
"EndFragment:"_s, String::number(105 + htmlUtf8.length()), "\r\n"
|
||||
"<html><body><!--StartFragment-->"_s
|
||||
);
|
||||
String htmlFooter = "<!--EndFragment--></body></html>"_s;
|
||||
|
||||
auto fullHtml = makeString(htmlHeader, String::fromUTF8(htmlUtf8.data()), htmlFooter);
|
||||
auto fullHtmlUtf8 = fullHtml.utf8();
|
||||
|
||||
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, fullHtmlUtf8.length() + 1);
|
||||
if (!hGlobal) {
|
||||
updateError(err, "Failed to allocate memory"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
char* buffer = static_cast<char*>(GlobalLock(hGlobal));
|
||||
if (!buffer) {
|
||||
updateError(err, "Failed to lock memory"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
memcpy(buffer, fullHtmlUtf8.data(), fullHtmlUtf8.length());
|
||||
buffer[fullHtmlUtf8.length()] = '\0';
|
||||
GlobalUnlock(hGlobal);
|
||||
|
||||
if (!SetClipboardData(CF_HTML, hGlobal)) {
|
||||
updateError(err, "Failed to set HTML clipboard data"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeRTF(const String& rtf)
|
||||
{
|
||||
Error err;
|
||||
|
||||
// Register RTF format if not already registered
|
||||
static UINT CF_RTF = RegisterClipboardFormat(L"Rich Text Format");
|
||||
if (!CF_RTF) {
|
||||
updateError(err, "Failed to register RTF clipboard format"_s, GetLastError());
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(err, "Failed to open clipboard"_s, GetLastError());
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::clear()) {
|
||||
updateError(err, "Failed to clear clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
auto rtfUtf8 = rtf.utf8();
|
||||
|
||||
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, rtfUtf8.length() + 1);
|
||||
if (!hGlobal) {
|
||||
updateError(err, "Failed to allocate memory"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
char* buffer = static_cast<char*>(GlobalLock(hGlobal));
|
||||
if (!buffer) {
|
||||
updateError(err, "Failed to lock memory"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
memcpy(buffer, rtfUtf8.data(), rtfUtf8.length());
|
||||
buffer[rtfUtf8.length()] = '\0';
|
||||
GlobalUnlock(hGlobal);
|
||||
|
||||
if (!SetClipboardData(CF_RTF, hGlobal)) {
|
||||
updateError(err, "Failed to set RTF clipboard data"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeImage(const Vector<uint8_t>& imageData, const String& mimeType)
|
||||
{
|
||||
Error err;
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(err, "Failed to open clipboard"_s, GetLastError());
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::clear()) {
|
||||
updateError(err, "Failed to clear clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, imageData.size());
|
||||
if (!hGlobal) {
|
||||
updateError(err, "Failed to allocate memory"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
void* buffer = GlobalLock(hGlobal);
|
||||
if (!buffer) {
|
||||
updateError(err, "Failed to lock memory"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
memcpy(buffer, imageData.data(), imageData.size());
|
||||
GlobalUnlock(hGlobal);
|
||||
|
||||
// Use DIB format for generic image data
|
||||
UINT format = CF_DIB;
|
||||
if (mimeType == "image/png"_s) {
|
||||
// Register PNG format
|
||||
static UINT CF_PNG = RegisterClipboardFormat(L"PNG");
|
||||
if (CF_PNG) format = CF_PNG;
|
||||
}
|
||||
|
||||
if (!SetClipboardData(format, hGlobal)) {
|
||||
updateError(err, "Failed to set image clipboard data"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
std::optional<String> readText(Error& error)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(error, "Failed to open clipboard"_s, GetLastError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
HANDLE hData = GetClipboardData(CF_UNICODETEXT);
|
||||
if (!hData) {
|
||||
updateError(error, "No text found in clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
wchar_t* buffer = static_cast<wchar_t*>(GlobalLock(hData));
|
||||
if (!buffer) {
|
||||
updateError(error, "Failed to lock clipboard data"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
String text = String(std::span<const UChar>(reinterpret_cast<const UChar*>(buffer), wcslen(buffer)));
|
||||
GlobalUnlock(hData);
|
||||
WindowsClipboard::close();
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
std::optional<String> readHTML(Error& error)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
static UINT CF_HTML = RegisterClipboardFormat(L"HTML Format");
|
||||
if (!CF_HTML) {
|
||||
updateError(error, "Failed to register HTML clipboard format"_s, GetLastError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(error, "Failed to open clipboard"_s, GetLastError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
HANDLE hData = GetClipboardData(CF_HTML);
|
||||
if (!hData) {
|
||||
updateError(error, "No HTML found in clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
char* buffer = static_cast<char*>(GlobalLock(hData));
|
||||
if (!buffer) {
|
||||
updateError(error, "Failed to lock clipboard data"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
String html = String::fromUTF8(buffer);
|
||||
GlobalUnlock(hData);
|
||||
WindowsClipboard::close();
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
std::optional<String> readRTF(Error& error)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
static UINT CF_RTF = RegisterClipboardFormat(L"Rich Text Format");
|
||||
if (!CF_RTF) {
|
||||
updateError(error, "Failed to register RTF clipboard format"_s, GetLastError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(error, "Failed to open clipboard"_s, GetLastError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
HANDLE hData = GetClipboardData(CF_RTF);
|
||||
if (!hData) {
|
||||
updateError(error, "No RTF found in clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
char* buffer = static_cast<char*>(GlobalLock(hData));
|
||||
if (!buffer) {
|
||||
updateError(error, "Failed to lock clipboard data"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
String rtf = String::fromUTF8(buffer);
|
||||
GlobalUnlock(hData);
|
||||
WindowsClipboard::close();
|
||||
|
||||
return rtf;
|
||||
}
|
||||
|
||||
std::optional<Vector<uint8_t>> readImage(Error& error, String& mimeType)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(error, "Failed to open clipboard"_s, GetLastError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Try PNG format first
|
||||
static UINT CF_PNG = RegisterClipboardFormat(L"PNG");
|
||||
HANDLE hData = nullptr;
|
||||
|
||||
if (CF_PNG) {
|
||||
hData = GetClipboardData(CF_PNG);
|
||||
if (hData) {
|
||||
mimeType = "image/png"_s;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hData) {
|
||||
// Try DIB format
|
||||
hData = GetClipboardData(CF_DIB);
|
||||
if (hData) {
|
||||
mimeType = "image/bmp"_s;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hData) {
|
||||
updateError(error, "No image found in clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void* buffer = GlobalLock(hData);
|
||||
if (!buffer) {
|
||||
updateError(error, "Failed to lock clipboard data"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
SIZE_T dataSize = GlobalSize(hData);
|
||||
Vector<uint8_t> result;
|
||||
result.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(buffer), dataSize));
|
||||
|
||||
GlobalUnlock(hData);
|
||||
WindowsClipboard::close();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isSupported()
|
||||
{
|
||||
return true; // Windows clipboard is always available
|
||||
}
|
||||
|
||||
Vector<DataType> getSupportedTypes()
|
||||
{
|
||||
Vector<DataType> types;
|
||||
types.append(DataType::Text);
|
||||
types.append(DataType::HTML);
|
||||
types.append(DataType::RTF);
|
||||
types.append(DataType::Image);
|
||||
return types;
|
||||
}
|
||||
|
||||
// Async implementations using std::thread - consistent with other platforms
|
||||
void writeTextAsync(const String& text, WriteCallback callback) {
|
||||
std::thread([text = text.isolatedCopy(), callback = std::move(callback)]() {
|
||||
Error error = writeText(text);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void writeHTMLAsync(const String& html, WriteCallback callback) {
|
||||
std::thread([html = html.isolatedCopy(), callback = std::move(callback)]() {
|
||||
Error error = writeHTML(html);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void writeRTFAsync(const String& rtf, WriteCallback callback) {
|
||||
std::thread([rtf = rtf.isolatedCopy(), callback = std::move(callback)]() {
|
||||
Error error = writeRTF(rtf);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void writeImageAsync(const Vector<uint8_t>& imageData, const String& mimeType, WriteCallback callback) {
|
||||
std::thread([imageData, mimeType = mimeType.isolatedCopy(), callback = std::move(callback)]() {
|
||||
Error error = writeImage(imageData, mimeType);
|
||||
callback(error);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void readTextAsync(ReadCallback callback) {
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
auto text = readText(error);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (text.has_value() && !text->isEmpty()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::Text;
|
||||
clipData.mimeType = "text/plain"_s;
|
||||
auto textUtf8 = text->utf8();
|
||||
clipData.data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(textUtf8.data()), textUtf8.length()));
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void readHTMLAsync(ReadCallback callback) {
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
auto html = readHTML(error);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (html.has_value() && !html->isEmpty()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::HTML;
|
||||
clipData.mimeType = "text/html"_s;
|
||||
auto htmlUtf8 = html->utf8();
|
||||
clipData.data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(htmlUtf8.data()), htmlUtf8.length()));
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void readRTFAsync(ReadCallback callback) {
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
auto rtf = readRTF(error);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (rtf.has_value() && !rtf->isEmpty()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::RTF;
|
||||
clipData.mimeType = "text/rtf"_s;
|
||||
auto rtfUtf8 = rtf->utf8();
|
||||
clipData.data.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(rtfUtf8.data()), rtfUtf8.length()));
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void readImageAsync(ReadCallback callback) {
|
||||
std::thread([callback = std::move(callback)]() {
|
||||
Error error;
|
||||
String mimeType;
|
||||
auto imageData = readImage(error, mimeType);
|
||||
Vector<ClipboardData> data;
|
||||
|
||||
if (imageData.has_value()) {
|
||||
ClipboardData clipData;
|
||||
clipData.type = DataType::Image;
|
||||
clipData.mimeType = mimeType;
|
||||
clipData.data = WTFMove(*imageData);
|
||||
data.append(WTFMove(clipData));
|
||||
}
|
||||
|
||||
callback(error, WTFMove(data));
|
||||
}).detach();
|
||||
}
|
||||
|
||||
} // namespace Clipboard
|
||||
} // namespace Bun
|
||||
|
||||
#endif // OS(WINDOWS)
|
||||
476
src/bun.js/bindings/ClipboardWindows.cpp.bak
Normal file
476
src/bun.js/bindings/ClipboardWindows.cpp.bak
Normal file
@@ -0,0 +1,476 @@
|
||||
#include "root.h"
|
||||
|
||||
#if OS(WINDOWS)
|
||||
|
||||
#include "Clipboard.h"
|
||||
#include <windows.h>
|
||||
#include <wtf/text/WTFString.h>
|
||||
#include <wtf/Vector.h>
|
||||
#include <wtf/text/StringView.h>
|
||||
|
||||
namespace Bun {
|
||||
namespace Clipboard {
|
||||
|
||||
using namespace WTF;
|
||||
|
||||
static void updateError(Error& err, const String& message, DWORD code = 0)
|
||||
{
|
||||
err.type = ErrorType::PlatformError;
|
||||
err.message = message;
|
||||
err.code = static_cast<int>(code);
|
||||
}
|
||||
|
||||
class WindowsClipboard {
|
||||
public:
|
||||
static bool open()
|
||||
{
|
||||
return OpenClipboard(nullptr) != 0;
|
||||
}
|
||||
|
||||
static void close()
|
||||
{
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
static bool clear()
|
||||
{
|
||||
return EmptyClipboard() != 0;
|
||||
}
|
||||
};
|
||||
|
||||
Error writeText(const String& text)
|
||||
{
|
||||
Error err;
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(err, "Failed to open clipboard"_s, GetLastError());
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::clear()) {
|
||||
updateError(err, "Failed to clear clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
// Convert to UTF-16
|
||||
auto textSize = (text.length() + 1) * sizeof(wchar_t);
|
||||
|
||||
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, textSize);
|
||||
if (!hGlobal) {
|
||||
updateError(err, "Failed to allocate memory"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
wchar_t* buffer = static_cast<wchar_t*>(GlobalLock(hGlobal));
|
||||
if (!buffer) {
|
||||
updateError(err, "Failed to lock memory"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
// Copy UTF-16 data
|
||||
Vector<UChar> characters = text.charactersWithNullTermination();
|
||||
memcpy(buffer, characters.data(), text.length() * sizeof(UChar));
|
||||
buffer[text.length()] = L'\0';
|
||||
GlobalUnlock(hGlobal);
|
||||
|
||||
if (!SetClipboardData(CF_UNICODETEXT, hGlobal)) {
|
||||
updateError(err, "Failed to set clipboard data"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeHTML(const String& html)
|
||||
{
|
||||
Error err;
|
||||
|
||||
// Register CF_HTML format if not already registered
|
||||
static UINT CF_HTML = RegisterClipboardFormat(L"HTML Format");
|
||||
if (!CF_HTML) {
|
||||
updateError(err, "Failed to register HTML clipboard format"_s, GetLastError());
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(err, "Failed to open clipboard"_s, GetLastError());
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::clear()) {
|
||||
updateError(err, "Failed to clear clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
// Create CF_HTML format
|
||||
auto htmlUtf8 = html.utf8();
|
||||
String htmlHeader = makeString(
|
||||
"Version:0.9\r\n"
|
||||
"StartHTML:0000000105\r\n"
|
||||
"EndHTML:"_s, String::number(105 + htmlUtf8.length()), "\r\n"
|
||||
"StartFragment:0000000105\r\n"
|
||||
"EndFragment:"_s, String::number(105 + htmlUtf8.length()), "\r\n"
|
||||
"<html><body><!--StartFragment-->"_s
|
||||
);
|
||||
String htmlFooter = "<!--EndFragment--></body></html>"_s;
|
||||
|
||||
auto fullHtml = makeString(htmlHeader, String::fromUTF8(htmlUtf8.data()), htmlFooter);
|
||||
auto fullHtmlUtf8 = fullHtml.utf8();
|
||||
|
||||
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, fullHtmlUtf8.length() + 1);
|
||||
if (!hGlobal) {
|
||||
updateError(err, "Failed to allocate memory"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
char* buffer = static_cast<char*>(GlobalLock(hGlobal));
|
||||
if (!buffer) {
|
||||
updateError(err, "Failed to lock memory"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
memcpy(buffer, fullHtmlUtf8.data(), fullHtmlUtf8.length());
|
||||
buffer[fullHtmlUtf8.length()] = '\0';
|
||||
GlobalUnlock(hGlobal);
|
||||
|
||||
if (!SetClipboardData(CF_HTML, hGlobal)) {
|
||||
updateError(err, "Failed to set HTML clipboard data"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeRTF(const String& rtf)
|
||||
{
|
||||
Error err;
|
||||
|
||||
// Register RTF format if not already registered
|
||||
static UINT CF_RTF = RegisterClipboardFormat(L"Rich Text Format");
|
||||
if (!CF_RTF) {
|
||||
updateError(err, "Failed to register RTF clipboard format"_s, GetLastError());
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(err, "Failed to open clipboard"_s, GetLastError());
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::clear()) {
|
||||
updateError(err, "Failed to clear clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
auto rtfUtf8 = rtf.utf8();
|
||||
|
||||
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, rtfUtf8.length() + 1);
|
||||
if (!hGlobal) {
|
||||
updateError(err, "Failed to allocate memory"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
char* buffer = static_cast<char*>(GlobalLock(hGlobal));
|
||||
if (!buffer) {
|
||||
updateError(err, "Failed to lock memory"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
memcpy(buffer, rtfUtf8.data(), rtfUtf8.length());
|
||||
buffer[rtfUtf8.length()] = '\0';
|
||||
GlobalUnlock(hGlobal);
|
||||
|
||||
if (!SetClipboardData(CF_RTF, hGlobal)) {
|
||||
updateError(err, "Failed to set RTF clipboard data"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
Error writeImage(const Vector<uint8_t>& imageData, const String& mimeType)
|
||||
{
|
||||
Error err;
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(err, "Failed to open clipboard"_s, GetLastError());
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::clear()) {
|
||||
updateError(err, "Failed to clear clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, imageData.size());
|
||||
if (!hGlobal) {
|
||||
updateError(err, "Failed to allocate memory"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
void* buffer = GlobalLock(hGlobal);
|
||||
if (!buffer) {
|
||||
updateError(err, "Failed to lock memory"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
memcpy(buffer, imageData.data(), imageData.size());
|
||||
GlobalUnlock(hGlobal);
|
||||
|
||||
// Use DIB format for generic image data
|
||||
UINT format = CF_DIB;
|
||||
if (mimeType == "image/png"_s) {
|
||||
// Register PNG format
|
||||
static UINT CF_PNG = RegisterClipboardFormat(L"PNG");
|
||||
if (CF_PNG) format = CF_PNG;
|
||||
}
|
||||
|
||||
if (!SetClipboardData(format, hGlobal)) {
|
||||
updateError(err, "Failed to set image clipboard data"_s, GetLastError());
|
||||
GlobalFree(hGlobal);
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
WindowsClipboard::close();
|
||||
return err;
|
||||
}
|
||||
|
||||
std::optional<String> readText(Error& error)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(error, "Failed to open clipboard"_s, GetLastError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
HANDLE hData = GetClipboardData(CF_UNICODETEXT);
|
||||
if (!hData) {
|
||||
updateError(error, "No text found in clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
wchar_t* buffer = static_cast<wchar_t*>(GlobalLock(hData));
|
||||
if (!buffer) {
|
||||
updateError(error, "Failed to lock clipboard data"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
String text = String(std::span<const UChar>(reinterpret_cast<const UChar*>(buffer), wcslen(buffer)));
|
||||
GlobalUnlock(hData);
|
||||
WindowsClipboard::close();
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
std::optional<String> readHTML(Error& error)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
static UINT CF_HTML = RegisterClipboardFormat(L"HTML Format");
|
||||
if (!CF_HTML) {
|
||||
updateError(error, "Failed to register HTML clipboard format"_s, GetLastError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(error, "Failed to open clipboard"_s, GetLastError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
HANDLE hData = GetClipboardData(CF_HTML);
|
||||
if (!hData) {
|
||||
updateError(error, "No HTML found in clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
char* buffer = static_cast<char*>(GlobalLock(hData));
|
||||
if (!buffer) {
|
||||
updateError(error, "Failed to lock clipboard data"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
String html = String::fromUTF8(buffer);
|
||||
GlobalUnlock(hData);
|
||||
WindowsClipboard::close();
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
std::optional<String> readRTF(Error& error)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
static UINT CF_RTF = RegisterClipboardFormat(L"Rich Text Format");
|
||||
if (!CF_RTF) {
|
||||
updateError(error, "Failed to register RTF clipboard format"_s, GetLastError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(error, "Failed to open clipboard"_s, GetLastError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
HANDLE hData = GetClipboardData(CF_RTF);
|
||||
if (!hData) {
|
||||
updateError(error, "No RTF found in clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
char* buffer = static_cast<char*>(GlobalLock(hData));
|
||||
if (!buffer) {
|
||||
updateError(error, "Failed to lock clipboard data"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
String rtf = String::fromUTF8(buffer);
|
||||
GlobalUnlock(hData);
|
||||
WindowsClipboard::close();
|
||||
|
||||
return rtf;
|
||||
}
|
||||
|
||||
std::optional<Vector<uint8_t>> readImage(Error& error, String& mimeType)
|
||||
{
|
||||
error = Error {};
|
||||
|
||||
if (!WindowsClipboard::open()) {
|
||||
updateError(error, "Failed to open clipboard"_s, GetLastError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Try PNG format first
|
||||
static UINT CF_PNG = RegisterClipboardFormat(L"PNG");
|
||||
HANDLE hData = nullptr;
|
||||
|
||||
if (CF_PNG) {
|
||||
hData = GetClipboardData(CF_PNG);
|
||||
if (hData) {
|
||||
mimeType = "image/png"_s;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hData) {
|
||||
// Try DIB format
|
||||
hData = GetClipboardData(CF_DIB);
|
||||
if (hData) {
|
||||
mimeType = "image/bmp"_s;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hData) {
|
||||
updateError(error, "No image found in clipboard"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void* buffer = GlobalLock(hData);
|
||||
if (!buffer) {
|
||||
updateError(error, "Failed to lock clipboard data"_s, GetLastError());
|
||||
WindowsClipboard::close();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
SIZE_T dataSize = GlobalSize(hData);
|
||||
Vector<uint8_t> result;
|
||||
result.append(std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(buffer), dataSize));
|
||||
|
||||
GlobalUnlock(hData);
|
||||
WindowsClipboard::close();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isSupported()
|
||||
{
|
||||
return true; // Windows clipboard is always available
|
||||
}
|
||||
|
||||
Vector<DataType> getSupportedTypes()
|
||||
{
|
||||
Vector<DataType> types;
|
||||
types.append(DataType::Text);
|
||||
types.append(DataType::HTML);
|
||||
types.append(DataType::RTF);
|
||||
types.append(DataType::Image);
|
||||
return types;
|
||||
}
|
||||
|
||||
// Async implementations - forward to common async implementation
|
||||
void writeTextAsync(const String& text, WriteCallback callback)
|
||||
{
|
||||
executeWriteTextAsync(text, std::move(callback));
|
||||
}
|
||||
|
||||
void writeHTMLAsync(const String& html, WriteCallback callback)
|
||||
{
|
||||
executeWriteHTMLAsync(html, std::move(callback));
|
||||
}
|
||||
|
||||
void writeRTFAsync(const String& rtf, WriteCallback callback)
|
||||
{
|
||||
executeWriteRTFAsync(rtf, std::move(callback));
|
||||
}
|
||||
|
||||
void writeImageAsync(const Vector<uint8_t>& imageData, const String& mimeType, WriteCallback callback)
|
||||
{
|
||||
executeWriteImageAsync(imageData, mimeType, std::move(callback));
|
||||
}
|
||||
|
||||
void readTextAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadTextAsync(std::move(callback));
|
||||
}
|
||||
|
||||
void readHTMLAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadHTMLAsync(std::move(callback));
|
||||
}
|
||||
|
||||
void readRTFAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadRTFAsync(std::move(callback));
|
||||
}
|
||||
|
||||
void readImageAsync(ReadCallback callback)
|
||||
{
|
||||
executeReadImageAsync(std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace Clipboard
|
||||
} // namespace Bun
|
||||
|
||||
#endif // OS(WINDOWS)
|
||||
322
src/bun.js/bindings/JSClipboard.cpp
Normal file
322
src/bun.js/bindings/JSClipboard.cpp
Normal file
@@ -0,0 +1,322 @@
|
||||
#include "ErrorCode.h"
|
||||
#include "root.h"
|
||||
#include "Clipboard.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
#include <JavaScriptCore/JSCJSValue.h>
|
||||
#include <JavaScriptCore/JSObject.h>
|
||||
#include <JavaScriptCore/JSPromise.h>
|
||||
#include <JavaScriptCore/JSString.h>
|
||||
#include <JavaScriptCore/ObjectConstructor.h>
|
||||
#include <JavaScriptCore/Error.h>
|
||||
#include <JavaScriptCore/ErrorInstance.h>
|
||||
#include <JavaScriptCore/Identifier.h>
|
||||
#include <wtf/text/WTFString.h>
|
||||
#include <wtf/text/CString.h>
|
||||
#include <mutex>
|
||||
#include "ObjectBindings.h"
|
||||
|
||||
namespace Bun {
|
||||
|
||||
using namespace JSC;
|
||||
using namespace WTF;
|
||||
|
||||
// Options struct that will be passed through the threadpool
|
||||
struct ClipboardJobOptions {
|
||||
WTF_MAKE_STRUCT_TZONE_ALLOCATED(ClipboardJobOptions);
|
||||
|
||||
enum Operation {
|
||||
READ_TEXT = 0,
|
||||
WRITE_TEXT = 1,
|
||||
READ_HTML = 2,
|
||||
WRITE_HTML = 3
|
||||
};
|
||||
|
||||
Operation op;
|
||||
CString text; // UTF-8 encoded, thread-safe (only for WRITE operations)
|
||||
CString mimeType; // MIME type for operations
|
||||
|
||||
// Results (filled in by threadpool)
|
||||
Clipboard::Error error;
|
||||
std::optional<String> resultText;
|
||||
|
||||
ClipboardJobOptions(Operation op, CString&& text = CString(), CString&& mimeType = CString())
|
||||
: op(op)
|
||||
, text(text)
|
||||
, mimeType(mimeType)
|
||||
{
|
||||
}
|
||||
|
||||
~ClipboardJobOptions()
|
||||
{
|
||||
if (text.length() > 0) {
|
||||
memsetSpan(text.mutableSpan(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
static ClipboardJobOptions* fromJS(JSGlobalObject* globalObject, ArgList args, Operation operation)
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
String text;
|
||||
String mimeType = "text/plain"_s; // default
|
||||
|
||||
if (operation == WRITE_TEXT || operation == WRITE_HTML) {
|
||||
// Write operations need text content
|
||||
if (args.size() < 1) {
|
||||
Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "Expected text content"_s);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
JSValue textValue = args.at(0);
|
||||
// Convert any value to string as per Web API spec
|
||||
text = textValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, nullptr);
|
||||
|
||||
if (operation == WRITE_HTML) {
|
||||
mimeType = "text/html"_s;
|
||||
}
|
||||
} else if (operation == READ_HTML) {
|
||||
mimeType = "text/html"_s;
|
||||
} else {
|
||||
// READ_TEXT or other read operations might have optional type parameter
|
||||
if (args.size() > 0) {
|
||||
JSValue typeValue = args.at(0);
|
||||
if (typeValue.isString()) {
|
||||
mimeType = typeValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RELEASE_AND_RETURN(scope, new ClipboardJobOptions(operation, text.utf8(), mimeType.utf8()));
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Thread pool function - runs on a background thread
|
||||
void Bun__ClipboardJobOptions__runTask(ClipboardJobOptions* opts, JSGlobalObject* globalObject)
|
||||
{
|
||||
switch (opts->op) {
|
||||
case ClipboardJobOptions::READ_TEXT: {
|
||||
auto result = Clipboard::readText(opts->error);
|
||||
if (result.has_value()) {
|
||||
opts->resultText = result.value();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ClipboardJobOptions::WRITE_TEXT:
|
||||
opts->error = Clipboard::writeText(String::fromUTF8(opts->text.data()));
|
||||
break;
|
||||
|
||||
case ClipboardJobOptions::READ_HTML: {
|
||||
auto result = Clipboard::readHTML(opts->error);
|
||||
if (result.has_value()) {
|
||||
opts->resultText = result.value();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ClipboardJobOptions::WRITE_HTML:
|
||||
opts->error = Clipboard::writeHTML(String::fromUTF8(opts->text.data()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Runs on the main thread after threadpool completes - resolves the promise
|
||||
void Bun__ClipboardJobOptions__runFromJS(ClipboardJobOptions* opts, JSGlobalObject* global, EncodedJSValue promiseValue)
|
||||
{
|
||||
auto& vm = global->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
JSPromise* promise = jsCast<JSPromise*>(JSValue::decode(promiseValue));
|
||||
|
||||
if (opts->error.type != Clipboard::ErrorType::None) {
|
||||
String errorMessage = opts->error.message;
|
||||
if (errorMessage.isEmpty()) {
|
||||
errorMessage = "Clipboard operation failed"_s;
|
||||
}
|
||||
promise->reject(global, createError(global, errorMessage));
|
||||
} else {
|
||||
// Success cases
|
||||
switch (opts->op) {
|
||||
case ClipboardJobOptions::READ_TEXT:
|
||||
case ClipboardJobOptions::READ_HTML:
|
||||
if (opts->resultText.has_value()) {
|
||||
promise->resolve(global, jsString(vm, opts->resultText.value()));
|
||||
} else {
|
||||
promise->resolve(global, jsEmptyString(vm));
|
||||
}
|
||||
break;
|
||||
|
||||
case ClipboardJobOptions::WRITE_TEXT:
|
||||
case ClipboardJobOptions::WRITE_HTML:
|
||||
promise->resolve(global, jsUndefined());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Bun__ClipboardJobOptions__deinit(ClipboardJobOptions* opts)
|
||||
{
|
||||
delete opts;
|
||||
}
|
||||
|
||||
// Zig binding exports
|
||||
void Bun__Clipboard__scheduleJob(JSGlobalObject* global, ClipboardJobOptions* opts, EncodedJSValue promise);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsClipboardReadText, (JSGlobalObject* globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
auto* options = ClipboardJobOptions::fromJS(globalObject, ArgList(callFrame), ClipboardJobOptions::READ_TEXT);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
ASSERT(options);
|
||||
|
||||
JSPromise* promise = JSPromise::create(vm, globalObject->promiseStructure());
|
||||
Bun__Clipboard__scheduleJob(globalObject, options, JSValue::encode(promise));
|
||||
|
||||
return JSValue::encode(promise);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsClipboardWriteText, (JSGlobalObject* globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
if (callFrame->argumentCount() < 1) {
|
||||
Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "clipboard.writeText requires text content"_s);
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
auto* options = ClipboardJobOptions::fromJS(globalObject, ArgList(callFrame), ClipboardJobOptions::WRITE_TEXT);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
ASSERT(options);
|
||||
|
||||
JSPromise* promise = JSPromise::create(vm, globalObject->promiseStructure());
|
||||
Bun__Clipboard__scheduleJob(globalObject, options, JSValue::encode(promise));
|
||||
|
||||
return JSValue::encode(promise);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsClipboardRead, (JSGlobalObject* globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
// Default to reading text, but check the type parameter
|
||||
ClipboardJobOptions::Operation operation = ClipboardJobOptions::READ_TEXT;
|
||||
|
||||
if (callFrame->argumentCount() > 0) {
|
||||
JSValue typeValue = callFrame->uncheckedArgument(0);
|
||||
if (typeValue.isString()) {
|
||||
String type = typeValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
|
||||
|
||||
if (type == "text/html"_s) {
|
||||
operation = ClipboardJobOptions::READ_HTML;
|
||||
} else if (type != "text/plain"_s) {
|
||||
throwTypeError(globalObject, scope, makeString("Unsupported clipboard type: "_s, type));
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto* options = ClipboardJobOptions::fromJS(globalObject, ArgList(callFrame), operation);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
ASSERT(options);
|
||||
|
||||
JSPromise* promise = JSPromise::create(vm, globalObject->promiseStructure());
|
||||
Bun__Clipboard__scheduleJob(globalObject, options, JSValue::encode(promise));
|
||||
|
||||
return JSValue::encode(promise);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsClipboardWrite, (JSGlobalObject* globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
if (callFrame->argumentCount() < 1) {
|
||||
throwTypeError(globalObject, scope, "clipboard.write() requires at least one argument"_s);
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
auto data = callFrame->uncheckedArgument(0);
|
||||
if (!data.isObject()) {
|
||||
throwTypeError(globalObject, scope, "clipboard.write() expects an array of ClipboardItem objects"_s);
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
auto* object = asObject(data);
|
||||
|
||||
// Handle array of ClipboardItems
|
||||
if (isArray(globalObject, object)) {
|
||||
auto firstItem = object->getIndex(globalObject, 0);
|
||||
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
|
||||
|
||||
if (firstItem.isObject()) {
|
||||
object = asObject(firstItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract text/plain or text/html from the ClipboardItem
|
||||
auto textPlainValue = object->get(globalObject, Identifier::fromString(vm, "text/plain"_s));
|
||||
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
|
||||
|
||||
auto textHtmlValue = object->get(globalObject, Identifier::fromString(vm, "text/html"_s));
|
||||
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
|
||||
|
||||
ClipboardJobOptions* options = nullptr;
|
||||
|
||||
if (!textPlainValue.isUndefined() && textPlainValue.isString()) {
|
||||
// Handle text/plain
|
||||
String text = textPlainValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
|
||||
|
||||
options = new ClipboardJobOptions(ClipboardJobOptions::WRITE_TEXT, text.utf8());
|
||||
} else if (!textHtmlValue.isUndefined() && textHtmlValue.isString()) {
|
||||
// Handle text/html
|
||||
String html = textHtmlValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined()));
|
||||
|
||||
options = new ClipboardJobOptions(ClipboardJobOptions::WRITE_HTML, html.utf8());
|
||||
} else {
|
||||
throwTypeError(globalObject, scope, "No supported clipboard data types found"_s);
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
ASSERT(options);
|
||||
JSPromise* promise = JSPromise::create(vm, globalObject->promiseStructure());
|
||||
Bun__Clipboard__scheduleJob(globalObject, options, JSValue::encode(promise));
|
||||
|
||||
return JSValue::encode(promise);
|
||||
}
|
||||
|
||||
JSObject* createClipboardObject(JSGlobalObject* lexicalGlobalObject)
|
||||
{
|
||||
VM& vm = lexicalGlobalObject->vm();
|
||||
|
||||
JSObject* clipboardObject = constructEmptyObject(lexicalGlobalObject, lexicalGlobalObject->objectPrototype(), 4);
|
||||
|
||||
clipboardObject->putDirect(vm, Identifier::fromString(vm, "read"_s),
|
||||
JSFunction::create(vm, lexicalGlobalObject, 1, "read"_s, jsClipboardRead, ImplementationVisibility::Public));
|
||||
|
||||
clipboardObject->putDirect(vm, Identifier::fromString(vm, "write"_s),
|
||||
JSFunction::create(vm, lexicalGlobalObject, 1, "write"_s, jsClipboardWrite, ImplementationVisibility::Public));
|
||||
|
||||
clipboardObject->putDirect(vm, Identifier::fromString(vm, "writeText"_s),
|
||||
JSFunction::create(vm, lexicalGlobalObject, 1, "writeText"_s, jsClipboardWriteText, ImplementationVisibility::Public));
|
||||
|
||||
clipboardObject->putDirect(vm, Identifier::fromString(vm, "readText"_s),
|
||||
JSFunction::create(vm, lexicalGlobalObject, 0, "readText"_s, jsClipboardReadText, ImplementationVisibility::Public));
|
||||
|
||||
return clipboardObject;
|
||||
}
|
||||
|
||||
} // namespace Bun
|
||||
12
src/bun.js/bindings/JSClipboard.h
Normal file
12
src/bun.js/bindings/JSClipboard.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "root.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
#include <JavaScriptCore/JSGlobalObject.h>
|
||||
#include <JavaScriptCore/JSObject.h>
|
||||
|
||||
namespace Bun {
|
||||
|
||||
JSC::JSObject* createClipboardObject(JSC::JSGlobalObject* lexicalGlobalObject);
|
||||
|
||||
} // namespace Bun
|
||||
86
src/bun.js/bindings/JSClipboard.zig
Normal file
86
src/bun.js/bindings/JSClipboard.zig
Normal file
@@ -0,0 +1,86 @@
|
||||
pub const ClipboardJob = struct {
|
||||
vm: *jsc.VirtualMachine,
|
||||
task: jsc.WorkPoolTask,
|
||||
any_task: jsc.AnyTask,
|
||||
poll: Async.KeepAlive = .{},
|
||||
promise: jsc.Strong,
|
||||
|
||||
ctx: *ClipboardJobOptions,
|
||||
|
||||
// Opaque pointer to C++ ClipboardJobOptions struct
|
||||
const ClipboardJobOptions = opaque {
|
||||
pub extern fn Bun__ClipboardJobOptions__runTask(ctx: *ClipboardJobOptions, global: *jsc.JSGlobalObject) void;
|
||||
pub extern fn Bun__ClipboardJobOptions__runFromJS(ctx: *ClipboardJobOptions, global: *jsc.JSGlobalObject, promise: jsc.JSValue) void;
|
||||
pub extern fn Bun__ClipboardJobOptions__deinit(ctx: *ClipboardJobOptions) void;
|
||||
};
|
||||
|
||||
pub fn create(global: *jsc.JSGlobalObject, ctx: *ClipboardJobOptions, promise: jsc.JSValue) *ClipboardJob {
|
||||
const vm = global.bunVM();
|
||||
const job = bun.new(ClipboardJob, .{
|
||||
.vm = vm,
|
||||
.task = .{
|
||||
.callback = &runTask,
|
||||
},
|
||||
.any_task = undefined,
|
||||
.ctx = ctx,
|
||||
.promise = jsc.Strong.create(promise, global),
|
||||
});
|
||||
job.any_task = jsc.AnyTask.New(ClipboardJob, &runFromJS).init(job);
|
||||
return job;
|
||||
}
|
||||
|
||||
pub fn runTask(task: *jsc.WorkPoolTask) void {
|
||||
const job: *ClipboardJob = @fieldParentPtr("task", task);
|
||||
var vm = job.vm;
|
||||
defer vm.enqueueTaskConcurrent(jsc.ConcurrentTask.create(job.any_task.task()));
|
||||
|
||||
ClipboardJobOptions.Bun__ClipboardJobOptions__runTask(job.ctx, vm.global);
|
||||
}
|
||||
|
||||
pub fn runFromJS(this: *ClipboardJob) void {
|
||||
defer this.deinit();
|
||||
const vm = this.vm;
|
||||
|
||||
if (vm.isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promise = this.promise.get();
|
||||
if (promise == .zero) return;
|
||||
|
||||
ClipboardJobOptions.Bun__ClipboardJobOptions__runFromJS(this.ctx, vm.global, promise);
|
||||
}
|
||||
|
||||
fn deinit(this: *ClipboardJob) void {
|
||||
ClipboardJobOptions.Bun__ClipboardJobOptions__deinit(this.ctx);
|
||||
this.poll.unref(this.vm);
|
||||
this.promise.deinit();
|
||||
bun.destroy(this);
|
||||
}
|
||||
|
||||
pub fn schedule(this: *ClipboardJob) void {
|
||||
this.poll.ref(this.vm);
|
||||
jsc.WorkPool.schedule(&this.task);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function for C++ to call with opaque pointer
|
||||
export fn Bun__Clipboard__scheduleJob(global: *jsc.JSGlobalObject, options: *ClipboardJob.ClipboardJobOptions, promise: jsc.JSValue) void {
|
||||
const job = ClipboardJob.create(global, options, promise.withAsyncContextIfNeeded(global));
|
||||
job.schedule();
|
||||
}
|
||||
|
||||
// Prevent dead code elimination
|
||||
pub fn fixDeadCodeElimination() void {
|
||||
std.mem.doNotOptimizeAway(&Bun__Clipboard__scheduleJob);
|
||||
}
|
||||
|
||||
comptime {
|
||||
_ = &fixDeadCodeElimination;
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
const Async = bun.Async;
|
||||
const jsc = bun.jsc;
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "root.h"
|
||||
|
||||
#include "ZigGlobalObject.h"
|
||||
#include "JSClipboard.h"
|
||||
#include "helpers.h"
|
||||
#include "JavaScriptCore/ArgList.h"
|
||||
#include "JavaScriptCore/JSImmutableButterfly.h"
|
||||
@@ -3123,6 +3124,11 @@ void GlobalObject::finishCreation(VM& vm)
|
||||
#endif
|
||||
|
||||
obj->putDirect(init.vm, hardwareConcurrencyIdentifier, JSC::jsNumber(cpuCount));
|
||||
|
||||
// Add clipboard API
|
||||
JSC::JSObject* clipboardObj = Bun::createClipboardObject(init.owner);
|
||||
obj->putDirect(init.vm, JSC::Identifier::fromString(init.vm, "clipboard"_s), clipboardObj);
|
||||
|
||||
init.set(obj);
|
||||
});
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ pub const JSRef = @import("./bindings/JSRef.zig").JSRef;
|
||||
pub const JSString = @import("./bindings/JSString.zig").JSString;
|
||||
pub const JSUint8Array = @import("./bindings/JSUint8Array.zig").JSUint8Array;
|
||||
pub const JSBigInt = @import("./bindings/JSBigInt.zig").JSBigInt;
|
||||
pub const JSClipboard = @import("./bindings/JSClipboard.zig");
|
||||
pub const RefString = @import("./jsc/RefString.zig");
|
||||
pub const ScriptExecutionStatus = @import("./bindings/ScriptExecutionStatus.zig").ScriptExecutionStatus;
|
||||
pub const SourceType = @import("./bindings/SourceType.zig").SourceType;
|
||||
|
||||
48
test-clipboard-linux.sh
Executable file
48
test-clipboard-linux.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to run clipboard tests on Linux with xvfb
|
||||
# This ensures clipboard utilities work in headless environments
|
||||
|
||||
set -e
|
||||
|
||||
# Find an available display number
|
||||
DISPLAY_NUM=99
|
||||
while [ -f "/tmp/.X${DISPLAY_NUM}-lock" ]; do
|
||||
DISPLAY_NUM=$((DISPLAY_NUM + 1))
|
||||
if [ $DISPLAY_NUM -gt 110 ]; then
|
||||
echo "Error: No available display found"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Start xvfb if DISPLAY is not set or if we're in CI
|
||||
if [ -z "$DISPLAY" ] || [ -n "$CI" ]; then
|
||||
echo "Starting xvfb on display :${DISPLAY_NUM}..."
|
||||
export DISPLAY=:${DISPLAY_NUM}
|
||||
Xvfb :${DISPLAY_NUM} -screen 0 1024x768x24 -ac +extension GLX +render -noreset > /dev/null 2>&1 &
|
||||
XVFB_PID=$!
|
||||
|
||||
# Wait for xvfb to start
|
||||
sleep 3
|
||||
|
||||
# Verify xvfb is running
|
||||
if ! kill -0 $XVFB_PID 2>/dev/null; then
|
||||
echo "Error: Failed to start xvfb"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to cleanup on exit
|
||||
cleanup() {
|
||||
if [ -n "$XVFB_PID" ]; then
|
||||
echo "Stopping xvfb..."
|
||||
kill $XVFB_PID 2>/dev/null || true
|
||||
wait $XVFB_PID 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
else
|
||||
echo "Using existing DISPLAY=$DISPLAY"
|
||||
fi
|
||||
|
||||
echo "Running clipboard tests with DISPLAY=$DISPLAY..."
|
||||
exec "$@"
|
||||
149
test/js/web/clipboard.test.ts
Normal file
149
test/js/web/clipboard.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("navigator.clipboard exists", () => {
|
||||
expect(navigator.clipboard).toBeDefined();
|
||||
expect(typeof navigator.clipboard).toBe("object");
|
||||
});
|
||||
|
||||
test("navigator.clipboard has required methods", () => {
|
||||
expect(typeof navigator.clipboard.readText).toBe("function");
|
||||
expect(typeof navigator.clipboard.writeText).toBe("function");
|
||||
expect(typeof navigator.clipboard.read).toBe("function");
|
||||
expect(typeof navigator.clipboard.write).toBe("function");
|
||||
});
|
||||
|
||||
test("writeText and readText work with strings", async () => {
|
||||
const testText = "Hello from Bun clipboard test!";
|
||||
|
||||
// Write text to clipboard
|
||||
await navigator.clipboard.writeText(testText);
|
||||
|
||||
// Read it back
|
||||
const result = await navigator.clipboard.readText();
|
||||
expect(result).toBe(testText);
|
||||
});
|
||||
|
||||
test("writeText handles empty string", async () => {
|
||||
await navigator.clipboard.writeText("");
|
||||
const result = await navigator.clipboard.readText();
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
test("writeText handles unicode characters", async () => {
|
||||
const unicodeText = "Hello 世界 🌍 Bun! 🚀";
|
||||
|
||||
await navigator.clipboard.writeText(unicodeText);
|
||||
const result = await navigator.clipboard.readText();
|
||||
expect(result).toBe(unicodeText);
|
||||
});
|
||||
|
||||
test("write and read work with ClipboardItem containing text", async () => {
|
||||
const testText = "ClipboardItem test text";
|
||||
|
||||
const clipboardItem = {
|
||||
"text/plain": testText
|
||||
};
|
||||
|
||||
await navigator.clipboard.write([clipboardItem]);
|
||||
const result = await navigator.clipboard.read("text/plain");
|
||||
expect(result).toBe(testText);
|
||||
});
|
||||
|
||||
test("write and read work with HTML content", async () => {
|
||||
const testHTML = "<p>Hello <strong>HTML</strong> clipboard!</p>";
|
||||
|
||||
const clipboardItem = {
|
||||
"text/html": testHTML
|
||||
};
|
||||
|
||||
await navigator.clipboard.write([clipboardItem]);
|
||||
const result = await navigator.clipboard.read("text/html");
|
||||
expect(result).toBe(testHTML);
|
||||
});
|
||||
|
||||
test("writeText returns a Promise", () => {
|
||||
const promise = navigator.clipboard.writeText("test");
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
return promise; // Let test wait for completion
|
||||
});
|
||||
|
||||
test("readText returns a Promise", () => {
|
||||
const promise = navigator.clipboard.readText();
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
return promise; // Let test wait for completion
|
||||
});
|
||||
|
||||
test("write handles invalid arguments gracefully", async () => {
|
||||
try {
|
||||
// @ts-expect-error - testing invalid arguments
|
||||
await navigator.clipboard.write();
|
||||
expect.unreachable("Should have thrown an error");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TypeError);
|
||||
}
|
||||
});
|
||||
|
||||
test("writeText handles non-string arguments", async () => {
|
||||
// Should convert to string
|
||||
// @ts-expect-error - testing type coercion
|
||||
await navigator.clipboard.writeText(123);
|
||||
const result = await navigator.clipboard.readText();
|
||||
expect(result).toBe("123");
|
||||
});
|
||||
|
||||
test("multiple write/read operations work correctly", async () => {
|
||||
const texts = ["First text", "Second text", "Third text"];
|
||||
|
||||
for (const text of texts) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
const result = await navigator.clipboard.readText();
|
||||
expect(result).toBe(text);
|
||||
}
|
||||
});
|
||||
|
||||
test("concurrent clipboard operations work", async () => {
|
||||
// Test that concurrent operations don't crash - results may vary due to race conditions
|
||||
const operations = Array.from({ length: 3 }, (_, i) =>
|
||||
navigator.clipboard.writeText(`Concurrent text ${i}`)
|
||||
);
|
||||
|
||||
// Just ensure all operations complete without errors
|
||||
await Promise.all(operations);
|
||||
|
||||
// The final result should be one of the concurrent texts
|
||||
const finalResult = await navigator.clipboard.readText();
|
||||
expect(finalResult.startsWith("Concurrent text")).toBe(true);
|
||||
});
|
||||
|
||||
test("clipboard persists between operations", async () => {
|
||||
const testText = "Persistent clipboard text " + Date.now(); // Make unique to avoid interference
|
||||
|
||||
await navigator.clipboard.writeText(testText);
|
||||
|
||||
// Wait a bit
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
const result = await navigator.clipboard.readText();
|
||||
expect(result).toBe(testText);
|
||||
});
|
||||
|
||||
test("read with unsupported type shows appropriate error", async () => {
|
||||
try {
|
||||
// @ts-expect-error - testing unsupported type
|
||||
await navigator.clipboard.read("application/unsupported-type");
|
||||
expect.unreachable("Should have thrown an error");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TypeError);
|
||||
expect(error.message).toContain("Unsupported clipboard type");
|
||||
}
|
||||
});
|
||||
|
||||
// This test might be platform-specific and could be skipped on some systems
|
||||
test("clipboard handles large text content", async () => {
|
||||
const largeText = "A".repeat(10000); // 10KB of text
|
||||
|
||||
await navigator.clipboard.writeText(largeText);
|
||||
const result = await navigator.clipboard.readText();
|
||||
expect(result).toBe(largeText);
|
||||
expect(result.length).toBe(10000);
|
||||
});
|
||||
Reference in New Issue
Block a user