Compare commits

...

6 Commits

Author SHA1 Message Date
autofix-ci[bot]
13675d7928 [autofix.ci] apply automated fixes 2025-09-25 07:55:12 +00:00
Claude Bot
a596dc12b4 fix: Cross-platform compatibility for clipboard API
- Fix Windows API calls using proper extern declarations
- Fix UTF-16 conversion function names for Zig compatibility
- All platforms now compile successfully (Windows, macOS, Linux)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 07:51:16 +00:00
autofix-ci[bot]
1a1b75dec0 [autofix.ci] apply automated fixes 2025-09-25 07:34:41 +00:00
Claude Bot
d5700a23f8 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>
2025-09-25 07:32:32 +00:00
autofix-ci[bot]
acadbae579 [autofix.ci] apply automated fixes 2025-09-25 06:59:10 +00:00
Claude Bot
e3eccdf5ee feat: add experimental clipboard API
- Adds Bun.clipboard.writeText() and Bun.clipboard.readText()
- Pure Zig implementation (~170 lines)
- Windows: Direct Win32 API calls
- macOS: Uses pbcopy/pbpaste via std.process.Child
- Linux: Uses xclip or wl-clipboard via std.process.Child
- Returns promises for consistency with web Clipboard API
2025-09-25 06:57:18 +00:00
7 changed files with 246 additions and 0 deletions

17
packages/bun-types/clipboard.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
declare module "bun" {
interface Clipboard {
/**
* Writes text to the system clipboard
* @param text The text to write to the clipboard
*/
writeText(text: string): void;
/**
* Reads text from the system clipboard
* @returns The clipboard text
*/
readText(): string;
}
const clipboard: Clipboard;
}

View File

@@ -23,6 +23,7 @@
/// <reference path="./experimental.d.ts" />
/// <reference path="./sql.d.ts" />
/// <reference path="./security.d.ts" />
/// <reference path="./clipboard.d.ts" />
/// <reference path="./bun.ns.d.ts" />

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);
@@ -128,6 +129,7 @@ pub const BunObject = struct {
@export(&BunObject.TOML, .{ .name = lazyPropertyCallbackName("TOML") });
@export(&BunObject.YAML, .{ .name = lazyPropertyCallbackName("YAML") });
@export(&BunObject.clipboard, .{ .name = lazyPropertyCallbackName("clipboard") });
@export(&BunObject.Glob, .{ .name = lazyPropertyCallbackName("Glob") });
@export(&BunObject.Transpiler, .{ .name = lazyPropertyCallbackName("Transpiler") });
@export(&BunObject.argv, .{ .name = lazyPropertyCallbackName("argv") });
@@ -1273,6 +1275,22 @@ pub fn getYAMLObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSVa
return YAMLObject.create(globalThis);
}
pub fn getClipboardObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
const clipboard = @import("./clipboard.zig");
const obj = jsc.JSValue.createEmptyObject(globalThis, 2);
obj.put(
globalThis,
jsc.ZigString.static("writeText"),
jsc.createCallback(globalThis, jsc.ZigString.static("writeText"), 1, clipboard.writeText),
);
obj.put(
globalThis,
jsc.ZigString.static("readText"),
jsc.createCallback(globalThis, jsc.ZigString.static("readText"), 0, clipboard.readText),
);
return obj;
}
pub fn getGlobConstructor(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
return jsc.API.Glob.js.getConstructor(globalThis);
}

View File

@@ -0,0 +1,179 @@
pub fn writeText(globalObject: *JSGlobalObject, callframe: *jsc.CallFrame) JSError!JSValue {
const args = callframe.argumentsAsArray(1);
if (args.len < 1 or !args[0].isString()) {
return globalObject.throw("writeText requires a string argument", .{});
}
const text = try args[0].toSlice(globalObject, bun.default_allocator);
defer text.deinit();
writeTextNative(text.slice()) catch |err| {
return globalObject.throw("Failed to write to clipboard: {s}", .{@errorName(err)});
};
return .js_undefined;
}
pub fn readText(globalObject: *JSGlobalObject, _: *jsc.CallFrame) JSError!JSValue {
const text = readTextNative(bun.default_allocator) catch |err| {
return globalObject.throw("Failed to read from clipboard: {s}", .{@errorName(err)});
};
defer bun.default_allocator.free(text);
return ZigString.fromUTF8(text).toJS(globalObject);
}
fn writeTextNative(text: []const u8) !void {
if (comptime Environment.isWindows) {
return writeTextWindows(text);
} else if (comptime Environment.isMac) {
return writeTextDarwin(text);
} else {
return writeTextLinux(text);
}
}
fn readTextNative(allocator: std.mem.Allocator) ![]u8 {
if (comptime Environment.isWindows) {
return readTextWindows(allocator);
} else if (comptime Environment.isMac) {
return readTextDarwin(allocator);
} else {
return readTextLinux(allocator);
}
}
// Windows implementation using Win32 APIs
const windows = if (builtin.os.tag == .windows) @import("std").os.windows else undefined;
const GMEM_MOVEABLE = 0x0002;
const CF_UNICODETEXT = 13;
extern "user32" fn OpenClipboard(?*anyopaque) callconv(windows.WINAPI) windows.BOOL;
extern "user32" fn CloseClipboard() callconv(windows.WINAPI) windows.BOOL;
extern "user32" fn EmptyClipboard() callconv(windows.WINAPI) windows.BOOL;
extern "user32" fn SetClipboardData(format: u32, mem: ?windows.HANDLE) callconv(windows.WINAPI) ?windows.HANDLE;
extern "user32" fn GetClipboardData(format: u32) callconv(windows.WINAPI) ?windows.HANDLE;
extern "kernel32" fn GlobalAlloc(flags: u32, bytes: usize) callconv(windows.WINAPI) ?windows.HANDLE;
extern "kernel32" fn GlobalLock(mem: ?windows.HANDLE) callconv(windows.WINAPI) ?*anyopaque;
extern "kernel32" fn GlobalUnlock(mem: ?windows.HANDLE) callconv(windows.WINAPI) windows.BOOL;
fn writeTextWindows(text: []const u8) !void {
// Open clipboard
if (OpenClipboard(null) == 0) return error.OpenFailed;
defer _ = CloseClipboard();
_ = EmptyClipboard();
// Convert UTF-8 to UTF-16
const len = std.unicode.calcUtf16LeLen(text) catch return error.InvalidUtf8;
const size = (len + 1) * 2;
const handle = GlobalAlloc(GMEM_MOVEABLE, size) orelse return error.AllocFailed;
const ptr = @as([*]u16, @ptrCast(@alignCast(GlobalLock(handle) orelse return error.LockFailed)));
defer _ = GlobalUnlock(handle);
_ = try std.unicode.utf8ToUtf16Le(ptr[0..len], text);
ptr[len] = 0;
if (SetClipboardData(CF_UNICODETEXT, handle) == null) return error.SetFailed;
}
fn readTextWindows(allocator: std.mem.Allocator) ![]u8 {
if (OpenClipboard(null) == 0) return error.OpenFailed;
defer _ = CloseClipboard();
const handle = GetClipboardData(CF_UNICODETEXT) orelse return allocator.dupe(u8, "");
const ptr = @as([*:0]const u16, @ptrCast(@alignCast(GlobalLock(handle) orelse return error.LockFailed)));
defer _ = GlobalUnlock(handle);
const len = std.mem.len(ptr);
const result = std.unicode.utf16LeToUtf8Alloc(allocator, ptr[0..len]) catch return error.InvalidUtf16;
return result;
}
// macOS implementation using pbcopy/pbpaste
fn writeTextDarwin(text: []const u8) !void {
var child = std.process.Child.init(&.{"pbcopy"}, bun.default_allocator);
child.stdin_behavior = .Pipe;
try child.spawn();
try child.stdin.?.writeAll(text);
child.stdin.?.close();
const term = try child.wait();
if (term.Exited != 0) return error.Failed;
}
fn readTextDarwin(allocator: std.mem.Allocator) ![]u8 {
const result = try std.process.Child.run(.{
.allocator = allocator,
.argv = &.{"pbpaste"},
});
defer allocator.free(result.stderr);
if (result.term.Exited != 0) {
allocator.free(result.stdout);
return error.Failed;
}
return result.stdout;
}
// Linux implementation using xclip/wl-clipboard
fn writeTextLinux(text: []const u8) !void {
// Try wl-copy first for Wayland
var child = std.process.Child.init(&.{"wl-copy"}, bun.default_allocator);
child.stdin_behavior = .Pipe;
child.stderr_behavior = .Ignore;
if (child.spawn()) |_| {
child.stdin.?.writeAll(text) catch {};
child.stdin.?.close();
const term = try child.wait();
if (term.Exited == 0) return;
} else |_| {}
// Fallback to xclip for X11
child = std.process.Child.init(&.{ "xclip", "-selection", "clipboard" }, bun.default_allocator);
child.stdin_behavior = .Pipe;
try child.spawn();
try child.stdin.?.writeAll(text);
child.stdin.?.close();
const term = try child.wait();
if (term.Exited != 0) return error.Failed;
}
fn readTextLinux(allocator: std.mem.Allocator) ![]u8 {
// Try wl-paste first for Wayland
if (std.process.Child.run(.{
.allocator = allocator,
.argv = &.{"wl-paste"},
})) |result| {
defer allocator.free(result.stderr);
if (result.term.Exited == 0) return result.stdout;
allocator.free(result.stdout);
} else |_| {}
// Fallback to xclip for X11
const result = try std.process.Child.run(.{
.allocator = allocator,
.argv = &.{ "xclip", "-selection", "clipboard", "-o" },
});
defer allocator.free(result.stderr);
if (result.term.Exited != 0) {
allocator.free(result.stdout);
return error.Failed;
}
return result.stdout;
}
// Imports at the bottom (Zig style in Bun codebase)
const builtin = @import("builtin");
const std = @import("std");
const bun = @import("bun");
const Environment = bun.Environment;
const JSError = bun.JSError;
const jsc = bun.jsc;
const JSGlobalObject = jsc.JSGlobalObject;
const JSValue = jsc.JSValue;
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

@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test";
test("Bun.clipboard exists", () => {
expect(Bun.clipboard).toBeDefined();
expect(typeof Bun.clipboard.writeText).toBe("function");
expect(typeof Bun.clipboard.readText).toBe("function");
});
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", () => {
Bun.clipboard.writeText("");
const result = Bun.clipboard.readText();
expect(result).toBe("");
});
test("handles unicode", () => {
const text = "Hello 世界 🚀";
Bun.clipboard.writeText(text);
const result = Bun.clipboard.readText();
expect(result).toBe(text);
});
});