feat: Add clipboard API with pure Zig implementation

- Implements Bun.clipboard.writeText() and Bun.clipboard.readText()
- Pure Zig implementation using native APIs (no C++)
- Windows: Win32 clipboard APIs
- macOS: pbcopy/pbpaste commands
- Linux: xclip/wl-clipboard support
- Synchronous API for simplicity
- Tests skip when no display is available

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2025-09-25 07:32:32 +00:00
parent acadbae579
commit d5700a23f8
6 changed files with 30 additions and 29 deletions

View File

@@ -3,15 +3,14 @@ declare module "bun" {
/**
* Writes text to the system clipboard
* @param text The text to write to the clipboard
* @returns A promise that resolves when the text is written
*/
writeText(text: string): Promise<void>;
writeText(text: string): void;
/**
* Reads text from the system clipboard
* @returns A promise that resolves with the clipboard text
* @returns The clipboard text
*/
readText(): Promise<string>;
readText(): string;
}
const clipboard: Clipboard;

View File

@@ -49,6 +49,7 @@ pub const BunObject = struct {
// --- Lazy property callbacks ---
pub const CryptoHasher = toJSLazyPropertyCallback(Crypto.CryptoHasher.getter);
pub const clipboard = toJSLazyPropertyCallback(Bun.getClipboardObject);
pub const CSRF = toJSLazyPropertyCallback(Bun.getCSRFObject);
pub const FFI = toJSLazyPropertyCallback(Bun.FFIObject.getter);
pub const FileSystemRouter = toJSLazyPropertyCallback(Bun.getFileSystemRouter);
@@ -63,7 +64,6 @@ pub const BunObject = struct {
pub const SHA512_256 = toJSLazyPropertyCallback(Crypto.SHA512_256.getter);
pub const TOML = toJSLazyPropertyCallback(Bun.getTOMLObject);
pub const YAML = toJSLazyPropertyCallback(Bun.getYAMLObject);
pub const clipboard = toJSLazyPropertyCallback(Bun.getClipboardObject);
pub const Transpiler = toJSLazyPropertyCallback(Bun.getTranspilerConstructor);
pub const argv = toJSLazyPropertyCallback(Bun.getArgv);
pub const cwd = toJSLazyPropertyCallback(Bun.getCWD);

View File

@@ -12,7 +12,7 @@ pub fn writeText(globalObject: *JSGlobalObject, callframe: *jsc.CallFrame) JSErr
return globalObject.throw("Failed to write to clipboard: {s}", .{@errorName(err)});
};
return JSPromise.resolvedPromiseValue(globalObject, .undefined);
return .js_undefined;
}
pub fn readText(globalObject: *JSGlobalObject, _: *jsc.CallFrame) JSError!JSValue {
@@ -21,7 +21,7 @@ pub fn readText(globalObject: *JSGlobalObject, _: *jsc.CallFrame) JSError!JSValu
};
defer bun.default_allocator.free(text);
return JSPromise.resolvedPromiseValue(globalObject, ZigString.fromUTF8(text).toJS(globalObject));
return ZigString.fromUTF8(text).toJS(globalObject);
}
fn writeTextNative(text: []const u8) !void {
@@ -156,14 +156,12 @@ fn readTextLinux(allocator: std.mem.Allocator) ![]u8 {
return result.stdout;
}
// Imports at the bottom (Zig style in Bun codebase)
const std = @import("std");
const bun = @import("bun");
const Environment = bun.Environment;
const JSError = bun.JSError;
const jsc = bun.jsc;
const JSError = jsc.JSError;
const JSGlobalObject = jsc.JSGlobalObject;
const JSPromise = jsc.JSPromise;
const JSValue = jsc.JSValue;
const ZigString = jsc.ZigString;
const ZigString = jsc.ZigString;

View File

@@ -3,6 +3,7 @@
// --- Getters ---
#define FOR_EACH_GETTER(macro) \
macro(clipboard) \
macro(CSRF) \
macro(CryptoHasher) \
macro(FFI) \

View File

@@ -707,6 +707,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
@begin bunObjectTable
$ constructBunShell DontDelete|PropertyCallback
ArrayBufferSink BunObject_lazyPropCb_wrap_ArrayBufferSink DontDelete|PropertyCallback
clipboard BunObject_lazyPropCb_wrap_clipboard DontDelete|PropertyCallback
Cookie constructCookieObject DontDelete|ReadOnly|PropertyCallback
CookieMap constructCookieMapObject DontDelete|ReadOnly|PropertyCallback
CryptoHasher BunObject_lazyPropCb_wrap_CryptoHasher DontDelete|PropertyCallback

View File

@@ -1,4 +1,4 @@
import { expect, test } from "bun:test";
import { describe, expect, test } from "bun:test";
test("Bun.clipboard exists", () => {
expect(Bun.clipboard).toBeDefined();
@@ -6,22 +6,24 @@ test("Bun.clipboard exists", () => {
expect(typeof Bun.clipboard.readText).toBe("function");
});
test("writeText and readText work", async () => {
const text = "Hello from Bun clipboard!";
await Bun.clipboard.writeText(text);
const result = await Bun.clipboard.readText();
expect(result).toBe(text);
});
describe.skipIf(!process.env.DISPLAY && process.platform === "linux")("clipboard operations", () => {
test("writeText and readText work", () => {
const text = "Hello from Bun clipboard!";
Bun.clipboard.writeText(text);
const result = Bun.clipboard.readText();
expect(result).toBe(text);
});
test("handles empty string", async () => {
await Bun.clipboard.writeText("");
const result = await Bun.clipboard.readText();
expect(result).toBe("");
});
test("handles empty string", () => {
Bun.clipboard.writeText("");
const result = Bun.clipboard.readText();
expect(result).toBe("");
});
test("handles unicode", async () => {
const text = "Hello 世界 🚀";
await Bun.clipboard.writeText(text);
const result = await Bun.clipboard.readText();
expect(result).toBe(text);
test("handles unicode", () => {
const text = "Hello 世界 🚀";
Bun.clipboard.writeText(text);
const result = Bun.clipboard.readText();
expect(result).toBe(text);
});
});