diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..25c3120
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.idea
+bin
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 01b5f8c..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# 默认忽略的文件
-/shelf/
-/workspace.xml
-# 基于编辑器的 HTTP 客户端请求
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
-# GitHub Copilot persisted chat sessions
-/copilot/chatSessions
diff --git a/.idea/clipboard.iml b/.idea/clipboard.iml
deleted file mode 100644
index 5d81a31..0000000
--- a/.idea/clipboard.iml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index f6f9c30..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/clipboard.go b/clipboard.go
index 6f34283..0d8c828 100644
--- a/clipboard.go
+++ b/clipboard.go
@@ -1,34 +1,13 @@
+//go:build windows
+
package clipboard
import (
"errors"
- "strings"
- "time"
)
-type FileType string
-
-const (
- Text FileType = "text"
- File FileType = "file"
- Image FileType = "image"
- HTML FileType = "html"
-)
-
-type Clipboard struct {
- winOriginTypes []string
- plateform string
- date time.Time
- secondaryOriType string
- secondaryType FileType
- secondaryData []byte
- secondarySize int
-
- primaryOriType string
- primaryType FileType
- primaryData []byte
- primarySize int
- hash string
+func Init() error {
+ return nil
}
func Set(types FileType, data []byte) error {
@@ -40,7 +19,7 @@ func Set(types FileType, data []byte) error {
case Image:
return AutoSetter("PNG", data)
case HTML:
- return AutoSetter("HTML Format", data)
+ return AutoSetter("HTML format", data)
}
return errors.New("not support type:" + string(types))
}
@@ -48,119 +27,3 @@ func Set(types FileType, data []byte) error {
func SetOrigin(types string, data []byte) error {
return AutoSetter(types, data)
}
-
-func (c *Clipboard) PrimaryType() FileType {
- return c.primaryType
-}
-
-func (c *Clipboard) AvailableTypes() []FileType {
- var res = make([]FileType, 0, 2)
- if c.primaryType != "" {
- res = append(res, c.primaryType)
- }
- if c.secondaryType != "" {
- res = append(res, c.secondaryType)
- }
- return res
-}
-
-func (c *Clipboard) IsText() bool {
- return c.primaryType == Text || c.secondaryType == Text
-}
-
-func (c *Clipboard) Text() string {
- if c.primaryType == Text {
- return string(c.primaryData)
- }
- if c.secondaryType == Text {
- return string(c.secondaryData)
- }
- return ""
-}
-
-func (c *Clipboard) TextSize() int {
- if c.primaryType == Text {
- return c.primarySize
- }
- if c.secondaryType == Text {
- return c.secondarySize
- }
- return 0
-}
-
-func (c *Clipboard) IsHTML() bool {
- return (c.primaryType == HTML || c.secondaryType == HTML) || c.IsText()
-}
-
-func (c *Clipboard) HTML() string {
- var htmlBytes []byte
- if c.primaryType == HTML {
- htmlBytes = c.primaryData
- } else if c.secondaryType == HTML {
- htmlBytes = c.secondaryData
- } else {
- return c.Text()
- }
- formats := strings.SplitN(string(htmlBytes), "\n", 7)
- if len(formats) < 7 {
- return string(htmlBytes)
- }
- return formats[6]
-}
-
-func (c *Clipboard) FilePaths() []string {
- if c.primaryType == File {
- return strings.Split(string(c.primaryData), "|")
- }
- if c.secondaryType == File {
- return strings.Split(string(c.secondaryData), "|")
- }
- return nil
-}
-
-func (c *Clipboard) IsFile() bool {
- return c.primaryType == File || c.secondaryType == File
-}
-
-func (c *Clipboard) FirstFilePath() string {
- if c.primaryType == File {
- return strings.Split(string(c.primaryData), "|")[0]
- }
- if c.secondaryType == File {
- return strings.Split(string(c.secondaryData), "|")[0]
- }
- return ""
-}
-
-func (c *Clipboard) Image() []byte {
- if c.primaryType == Image {
- return c.primaryData
- }
- if c.secondaryType == Image {
- return c.secondaryData
- }
- return nil
-}
-
-func (c *Clipboard) ImageSize() int {
- if c.primaryType == Image {
- return c.primarySize
- }
- if c.secondaryType == Image {
- return c.secondarySize
- }
- return 0
-
-}
-
-func (c *Clipboard) IsImage() bool {
- return c.primaryType == Image || c.secondaryType == Image
-}
-
-func (c *Clipboard) PrimaryTypeSize() int {
- return c.primarySize
-}
-
-func (c *Clipboard) SecondaryTypeSize() int {
- return c.secondarySize
-}
diff --git a/clipboard_android.c b/clipboard_android.c
new file mode 100644
index 0000000..9dc34f6
--- /dev/null
+++ b/clipboard_android.c
@@ -0,0 +1,80 @@
+// Copyright 2021 The golang.design Initiative Authors.
+// All rights reserved. Use of this source code is governed
+// by a MIT license that can be found in the LICENSE file.
+//
+// Written by Changkun Ou
+
+//go:build android
+
+#include
+#include
+#include
+#include
+
+#define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, \
+ "GOLANG.DESIGN/X/CLIPBOARD", __VA_ARGS__)
+
+static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
+ jmethodID m = (*env)->GetMethodID(env, clazz, name, sig);
+ if (m == 0) {
+ (*env)->ExceptionClear(env);
+ LOG_FATAL("cannot find method %s %s", name, sig);
+ return 0;
+ }
+ return m;
+}
+
+jobject get_clipboard(uintptr_t jni_env, uintptr_t ctx) {
+ JNIEnv *env = (JNIEnv*)jni_env;
+ jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx);
+ jmethodID getSystemService = find_method(env, ctxClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
+
+ jstring service = (*env)->NewStringUTF(env, "clipboard");
+ jobject ret = (jobject)(*env)->CallObjectMethod(env, (jobject)ctx, getSystemService, service);
+ jthrowable err = (*env)->ExceptionOccurred(env);
+
+ if (err != NULL) {
+ LOG_FATAL("cannot find clipboard");
+ (*env)->ExceptionClear(env);
+ return NULL;
+ }
+ return ret;
+}
+
+char *clipboard_read_string(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) {
+ JNIEnv *env = (JNIEnv*)jni_env;
+ jobject mgr = get_clipboard(jni_env, ctx);
+ if (mgr == NULL) {
+ return NULL;
+ }
+
+ jclass mgrClass = (*env)->GetObjectClass(env, mgr);
+ jmethodID getText = find_method(env, mgrClass, "getText", "()Ljava/lang/CharSequence;");
+
+ jobject content = (jstring)(*env)->CallObjectMethod(env, mgr, getText);
+ if (content == NULL) {
+ return NULL;
+ }
+
+ jclass clzCharSequence = (*env)->GetObjectClass(env, content);
+ jmethodID toString = (*env)->GetMethodID(env, clzCharSequence, "toString", "()Ljava/lang/String;");
+ jobject s = (*env)->CallObjectMethod(env, content, toString);
+
+ const char *chars = (*env)->GetStringUTFChars(env, s, NULL);
+ char *copy = strdup(chars);
+ (*env)->ReleaseStringUTFChars(env, s, chars);
+ return copy;
+}
+
+void clipboard_write_string(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *str) {
+ JNIEnv *env = (JNIEnv*)jni_env;
+ jobject mgr = get_clipboard(jni_env, ctx);
+ if (mgr == NULL) {
+ return;
+ }
+
+ jclass mgrClass = (*env)->GetObjectClass(env, mgr);
+ jmethodID setText = find_method(env, mgrClass, "setText", "(Ljava/lang/CharSequence;)V");
+
+ (*env)->CallVoidMethod(env, mgr, setText, (*env)->NewStringUTF(env, str));
+}
diff --git a/clipboard_android.go b/clipboard_android.go
new file mode 100644
index 0000000..808b8f5
--- /dev/null
+++ b/clipboard_android.go
@@ -0,0 +1,102 @@
+// Copyright 2021 The golang.design Initiative Authors.
+// All rights reserved. Use of this source code is governed
+// by a MIT license that can be found in the LICENSE file.
+//
+// Written by Changkun Ou
+
+//go:build android
+
+package clipboard
+
+/*
+#cgo LDFLAGS: -landroid -llog
+
+#include
+char *clipboard_read_string(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx);
+void clipboard_write_string(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *str);
+
+*/
+import "C"
+import (
+ "bytes"
+ "context"
+ "time"
+ "unsafe"
+
+ "golang.org/x/mobile/app"
+)
+
+func initialize() error { return nil }
+
+func read(t format) (buf []byte, err error) {
+ switch t {
+ case fmtText:
+ s := ""
+ if err := app.RunOnJVM(func(vm, env, ctx uintptr) error {
+ cs := C.clipboard_read_string(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx))
+ if cs == nil {
+ return nil
+ }
+
+ s = C.GoString(cs)
+ C.free(unsafe.Pointer(cs))
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ return []byte(s), nil
+ case fmtImage:
+ return nil, errUnsupported
+ default:
+ return nil, errUnsupported
+ }
+}
+
+// write writes the given data to clipboard and
+// returns true if success or false if failed.
+func write(t format, buf []byte) (<-chan struct{}, error) {
+ done := make(chan struct{}, 1)
+ switch t {
+ case fmtText:
+ cs := C.CString(string(buf))
+ defer C.free(unsafe.Pointer(cs))
+
+ if err := app.RunOnJVM(func(vm, env, ctx uintptr) error {
+ C.clipboard_write_string(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), cs)
+ done <- struct{}{}
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ return done, nil
+ case fmtImage:
+ return nil, errUnsupported
+ default:
+ return nil, errUnsupported
+ }
+}
+
+func watch(ctx context.Context, t format) <-chan []byte {
+ recv := make(chan []byte, 1)
+ ti := time.NewTicker(time.Second)
+ last := doRead(t)
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ close(recv)
+ return
+ case <-ti.C:
+ b := doRead(t)
+ if b == nil {
+ continue
+ }
+ if bytes.Compare(last, b) != 0 {
+ recv <- b
+ last = b
+ }
+ }
+ }
+ }()
+ return recv
+}
diff --git a/clipboard_darwin.go b/clipboard_darwin.go
new file mode 100644
index 0000000..aae9a46
--- /dev/null
+++ b/clipboard_darwin.go
@@ -0,0 +1,122 @@
+// Copyright 2021 The golang.design Initiative Authors.
+// All rights reserved. Use of this source code is governed
+// by a MIT license that can be found in the LICENSE file.
+//
+// Written by Changkun Ou
+
+//go:build darwin && !ios
+
+package clipboard
+
+/*
+#cgo CFLAGS: -x objective-c
+#cgo LDFLAGS: -framework Foundation -framework Cocoa
+#import
+#import
+
+unsigned int clipboard_read_string(void **out);
+unsigned int clipboard_read_image(void **out);
+int clipboard_write_string(const void *bytes, NSInteger n);
+int clipboard_write_image(const void *bytes, NSInteger n);
+NSInteger clipboard_change_count();
+*/
+import "C"
+import (
+ "context"
+ "time"
+ "unsafe"
+)
+
+func initialize() error { return nil }
+
+func read(t format) (buf []byte, err error) {
+ var (
+ data unsafe.Pointer
+ n C.uint
+ )
+ switch t {
+ case fmtText:
+ n = C.clipboard_read_string(&data)
+ case fmtImage:
+ n = C.clipboard_read_image(&data)
+ }
+ if data == nil {
+ return nil, errUnavailable
+ }
+ defer C.free(unsafe.Pointer(data))
+ if n == 0 {
+ return nil, nil
+ }
+ return C.GoBytes(data, C.int(n)), nil
+}
+
+// write writes the given data to clipboard and
+// returns true if success or false if failed.
+func write(t format, buf []byte) (<-chan struct{}, error) {
+ var ok C.int
+ switch t {
+ case fmtText:
+ if len(buf) == 0 {
+ ok = C.clipboard_write_string(unsafe.Pointer(nil), 0)
+ } else {
+ ok = C.clipboard_write_string(unsafe.Pointer(&buf[0]),
+ C.NSInteger(len(buf)))
+ }
+ case fmtImage:
+ if len(buf) == 0 {
+ ok = C.clipboard_write_image(unsafe.Pointer(nil), 0)
+ } else {
+ ok = C.clipboard_write_image(unsafe.Pointer(&buf[0]),
+ C.NSInteger(len(buf)))
+ }
+ default:
+ return nil, errUnsupported
+ }
+ if ok != 0 {
+ return nil, errUnavailable
+ }
+
+ // use unbuffered data to prevent goroutine leak
+ changed := make(chan struct{}, 1)
+ cnt := C.long(C.clipboard_change_count())
+ go func() {
+ for {
+ // not sure if we are too slow or the user too fast :)
+ time.Sleep(time.Second)
+ cur := C.long(C.clipboard_change_count())
+ if cnt != cur {
+ changed <- struct{}{}
+ close(changed)
+ return
+ }
+ }
+ }()
+ return changed, nil
+}
+
+func watch(ctx context.Context, t format) <-chan []byte {
+ recv := make(chan []byte, 1)
+ // not sure if we are too slow or the user too fast :)
+ ti := time.NewTicker(time.Second)
+ lastCount := C.long(C.clipboard_change_count())
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ close(recv)
+ return
+ case <-ti.C:
+ this := C.long(C.clipboard_change_count())
+ if lastCount != this {
+ b := doRead(t)
+ if b == nil {
+ continue
+ }
+ recv <- b
+ lastCount = this
+ }
+ }
+ }
+ }()
+ return recv
+}
diff --git a/clipboard_darwin.m b/clipboard_darwin.m
new file mode 100644
index 0000000..177e771
--- /dev/null
+++ b/clipboard_darwin.m
@@ -0,0 +1,62 @@
+// Copyright 2021 The golang.design Initiative Authors.
+// All rights reserved. Use of this source code is governed
+// by a MIT license that can be found in the LICENSE file.
+//
+// Written by Changkun Ou
+
+//go:build darwin && !ios
+
+// Interact with NSPasteboard using Objective-C
+// https://developer.apple.com/documentation/appkit/nspasteboard?language=objc
+
+#import
+#import
+
+unsigned int clipboard_read_string(void **out) {
+ NSPasteboard * pasteboard = [NSPasteboard generalPasteboard];
+ NSData *data = [pasteboard dataForType:NSPasteboardTypeString];
+ if (data == nil) {
+ return 0;
+ }
+ NSUInteger siz = [data length];
+ *out = malloc(siz);
+ [data getBytes: *out length: siz];
+ return siz;
+}
+
+unsigned int clipboard_read_image(void **out) {
+ NSPasteboard * pasteboard = [NSPasteboard generalPasteboard];
+ NSData *data = [pasteboard dataForType:NSPasteboardTypePNG];
+ if (data == nil) {
+ return 0;
+ }
+ NSUInteger siz = [data length];
+ *out = malloc(siz);
+ [data getBytes: *out length: siz];
+ return siz;
+}
+
+int clipboard_write_string(const void *bytes, NSInteger n) {
+ NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
+ NSData *data = [NSData dataWithBytes: bytes length: n];
+ [pasteboard clearContents];
+ BOOL ok = [pasteboard setData: data forType:NSPasteboardTypeString];
+ if (!ok) {
+ return -1;
+ }
+ return 0;
+}
+int clipboard_write_image(const void *bytes, NSInteger n) {
+ NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
+ NSData *data = [NSData dataWithBytes: bytes length: n];
+ [pasteboard clearContents];
+ BOOL ok = [pasteboard setData: data forType:NSPasteboardTypePNG];
+ if (!ok) {
+ return -1;
+ }
+ return 0;
+}
+
+NSInteger clipboard_change_count() {
+ return [[NSPasteboard generalPasteboard] changeCount];
+}
diff --git a/clipboard_ios.go b/clipboard_ios.go
new file mode 100644
index 0000000..638912d
--- /dev/null
+++ b/clipboard_ios.go
@@ -0,0 +1,80 @@
+// Copyright 2021 The golang.design Initiative Authors.
+// All rights reserved. Use of this source code is governed
+// by a MIT license that can be found in the LICENSE file.
+//
+// Written by Changkun Ou
+
+//go:build ios
+
+package clipboard
+
+/*
+#cgo CFLAGS: -x objective-c
+#cgo LDFLAGS: -framework Foundation -framework UIKit -framework MobileCoreServices
+
+#import
+void clipboard_write_string(char *s);
+char *clipboard_read_string();
+*/
+import "C"
+import (
+ "bytes"
+ "context"
+ "time"
+ "unsafe"
+)
+
+func initialize() error { return nil }
+
+func read(t format) (buf []byte, err error) {
+ switch t {
+ case FmtText:
+ return []byte(C.GoString(C.clipboard_read_string())), nil
+ case FmtImage:
+ return nil, errUnsupported
+ default:
+ return nil, errUnsupported
+ }
+}
+
+// SetContent sets the clipboard content for iOS
+func write(t format, buf []byte) (<-chan struct{}, error) {
+ done := make(chan struct{}, 1)
+ switch t {
+ case FmtText:
+ cs := C.CString(string(buf))
+ defer C.free(unsafe.Pointer(cs))
+
+ C.clipboard_write_string(cs)
+ return done, nil
+ case FmtImage:
+ return nil, errUnsupported
+ default:
+ return nil, errUnsupported
+ }
+}
+
+func watch(ctx context.Context, t format) <-chan []byte {
+ recv := make(chan []byte, 1)
+ ti := time.NewTicker(time.Second)
+ last := doRead(t)
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ close(recv)
+ return
+ case <-ti.C:
+ b := doRead(t)
+ if b == nil {
+ continue
+ }
+ if bytes.Compare(last, b) != 0 {
+ recv <- b
+ last = b
+ }
+ }
+ }
+ }()
+ return recv
+}
diff --git a/clipboard_ios.m b/clipboard_ios.m
new file mode 100644
index 0000000..15eb122
--- /dev/null
+++ b/clipboard_ios.m
@@ -0,0 +1,20 @@
+// Copyright 2021 The golang.design Initiative Authors.
+// All rights reserved. Use of this source code is governed
+// by a MIT license that can be found in the LICENSE file.
+//
+// Written by Changkun Ou
+
+//go:build ios
+
+#import
+#import
+
+void clipboard_write_string(char *s) {
+ NSString *value = [NSString stringWithUTF8String:s];
+ [[UIPasteboard generalPasteboard] setString:value];
+}
+
+char *clipboard_read_string() {
+ NSString *str = [[UIPasteboard generalPasteboard] string];
+ return (char *)[str UTF8String];
+}
diff --git a/clipboard_linux.c b/clipboard_linux.c
new file mode 100644
index 0000000..d23934b
--- /dev/null
+++ b/clipboard_linux.c
@@ -0,0 +1,263 @@
+// Copyright 2021 The golang.design Initiative Authors.
+// All rights reserved. Use of this source code is governed
+// by a MIT license that can be found in the LICENSE file.
+//
+// Written by Changkun Ou
+
+//go:build linux && !android
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// syncStatus is a function from the Go side.
+extern void syncStatus(uintptr_t handle, int status);
+
+void *libX11;
+
+Display* (*P_XOpenDisplay)(int);
+void (*P_XCloseDisplay)(Display*);
+Window (*P_XDefaultRootWindow)(Display*);
+Window (*P_XCreateSimpleWindow)(Display*, Window, int, int, int, int, int, int, int);
+Atom (*P_XInternAtom)(Display*, char*, int);
+void (*P_XSetSelectionOwner)(Display*, Atom, Window, unsigned long);
+Window (*P_XGetSelectionOwner)(Display*, Atom);
+void (*P_XNextEvent)(Display*, XEvent*);
+int (*P_XChangeProperty)(Display*, Window, Atom, Atom, int, int, unsigned char*, int);
+void (*P_XSendEvent)(Display*, Window, int, long , XEvent*);
+int (*P_XGetWindowProperty) (Display*, Window, Atom, long, long, Bool, Atom, Atom*, int*, unsigned long *, unsigned long *, unsigned char **);
+void (*P_XFree) (void*);
+void (*P_XDeleteProperty) (Display*, Window, Atom);
+void (*P_XConvertSelection)(Display*, Atom, Atom, Atom, Window, Time);
+
+int initX11() {
+ if (libX11) {
+ return 1;
+ }
+ libX11 = dlopen("libX11.so", RTLD_LAZY);
+ if (!libX11) {
+ return 0;
+ }
+ P_XOpenDisplay = (Display* (*)(int)) dlsym(libX11, "XOpenDisplay");
+ P_XCloseDisplay = (void (*)(Display*)) dlsym(libX11, "XCloseDisplay");
+ P_XDefaultRootWindow = (Window (*)(Display*)) dlsym(libX11, "XDefaultRootWindow");
+ P_XCreateSimpleWindow = (Window (*)(Display*, Window, int, int, int, int, int, int, int)) dlsym(libX11, "XCreateSimpleWindow");
+ P_XInternAtom = (Atom (*)(Display*, char*, int)) dlsym(libX11, "XInternAtom");
+ P_XSetSelectionOwner = (void (*)(Display*, Atom, Window, unsigned long)) dlsym(libX11, "XSetSelectionOwner");
+ P_XGetSelectionOwner = (Window (*)(Display*, Atom)) dlsym(libX11, "XGetSelectionOwner");
+ P_XNextEvent = (void (*)(Display*, XEvent*)) dlsym(libX11, "XNextEvent");
+ P_XChangeProperty = (int (*)(Display*, Window, Atom, Atom, int, int, unsigned char*, int)) dlsym(libX11, "XChangeProperty");
+ P_XSendEvent = (void (*)(Display*, Window, int, long , XEvent*)) dlsym(libX11, "XSendEvent");
+ P_XGetWindowProperty = (int (*)(Display*, Window, Atom, long, long, Bool, Atom, Atom*, int*, unsigned long *, unsigned long *, unsigned char **)) dlsym(libX11, "XGetWindowProperty");
+ P_XFree = (void (*)(void*)) dlsym(libX11, "XFree");
+ P_XDeleteProperty = (void (*)(Display*, Window, Atom)) dlsym(libX11, "XDeleteProperty");
+ P_XConvertSelection = (void (*)(Display*, Atom, Atom, Atom, Window, Time)) dlsym(libX11, "XConvertSelection");
+ return 1;
+}
+
+int clipboard_test() {
+ if (!initX11()) {
+ return -1;
+ }
+
+ Display* d = NULL;
+ for (int i = 0; i < 42; i++) {
+ d = (*P_XOpenDisplay)(0);
+ if (d == NULL) {
+ continue;
+ }
+ break;
+ }
+ if (d == NULL) {
+ return -1;
+ }
+ (*P_XCloseDisplay)(d);
+ return 0;
+}
+
+// clipboard_write writes the given buf of size n as type typ.
+// if start is provided, the value of start will be changed to 1 to indicate
+// if the write is availiable for reading.
+int clipboard_write(char *typ, unsigned char *buf, size_t n, uintptr_t handle) {
+ if (!initX11()) {
+ return -1;
+ }
+
+ Display* d = NULL;
+ for (int i = 0; i < 42; i++) {
+ d = (*P_XOpenDisplay)(0);
+ if (d == NULL) {
+ continue;
+ }
+ break;
+ }
+ if (d == NULL) {
+ syncStatus(handle, -1);
+ return -1;
+ }
+ Window w = (*P_XCreateSimpleWindow)(d, (*P_XDefaultRootWindow)(d), 0, 0, 1, 1, 0, 0, 0);
+
+ // Use False because these may not available for the first time.
+ Atom sel = (*P_XInternAtom)(d, "CLIPBOARD", 0);
+ Atom atomString = (*P_XInternAtom)(d, "UTF8_STRING", 0);
+ Atom atomImage = (*P_XInternAtom)(d, "image/png", 0);
+ Atom targetsAtom = (*P_XInternAtom)(d, "TARGETS", 0);
+
+ // Use True to makesure the requested type is a valid type.
+ Atom target = (*P_XInternAtom)(d, typ, 1);
+ if (target == None) {
+ (*P_XCloseDisplay)(d);
+ syncStatus(handle, -2);
+ return -2;
+ }
+
+ (*P_XSetSelectionOwner)(d, sel, w, CurrentTime);
+ if ((*P_XGetSelectionOwner)(d, sel) != w) {
+ (*P_XCloseDisplay)(d);
+ syncStatus(handle, -3);
+ return -3;
+ }
+
+ XEvent event;
+ XSelectionRequestEvent* xsr;
+ int notified = 0;
+ for (;;) {
+ if (notified == 0) {
+ syncStatus(handle, 1); // notify Go side
+ notified = 1;
+ }
+
+ (*P_XNextEvent)(d, &event);
+ switch (event.type) {
+ case SelectionClear:
+ // For debugging:
+ // printf("x11write: lost ownership of clipboard selection.\n");
+ // fflush(stdout);
+ (*P_XCloseDisplay)(d);
+ return 0;
+ case SelectionNotify:
+ // For debugging:
+ // printf("x11write: notify.\n");
+ // fflush(stdout);
+ break;
+ case SelectionRequest:
+ if (event.xselectionrequest.selection != sel) {
+ break;
+ }
+
+ XSelectionRequestEvent * xsr = &event.xselectionrequest;
+ XSelectionEvent ev = {0};
+ int R = 0;
+
+ ev.type = SelectionNotify;
+ ev.display = xsr->display;
+ ev.requestor = xsr->requestor;
+ ev.selection = xsr->selection;
+ ev.time = xsr->time;
+ ev.target = xsr->target;
+ ev.property = xsr->property;
+
+ if (ev.target == atomString && ev.target == target) {
+ R = (*P_XChangeProperty)(ev.display, ev.requestor, ev.property,
+ atomString, 8, PropModeReplace, buf, n);
+ } else if (ev.target == atomImage && ev.target == target) {
+ R = (*P_XChangeProperty)(ev.display, ev.requestor, ev.property,
+ atomImage, 8, PropModeReplace, buf, n);
+ } else if (ev.target == targetsAtom) {
+ // Reply atoms for supported targets, other clients should
+ // request the clipboard again and obtain the data if their
+ // implementation is correct.
+ Atom targets[] = { atomString, atomImage };
+ R = (*P_XChangeProperty)(ev.display, ev.requestor, ev.property,
+ XA_ATOM, 32, PropModeReplace,
+ (unsigned char *)&targets, sizeof(targets)/sizeof(Atom));
+ } else {
+ ev.property = None;
+ }
+
+ if ((R & 2) == 0) (*P_XSendEvent)(d, ev.requestor, 0, 0, (XEvent *)&ev);
+ break;
+ }
+ }
+}
+
+// read_data reads the property of a selection if the target atom matches
+// the actual atom.
+unsigned long read_data(XSelectionEvent *sev, Atom sel, Atom prop, Atom target, char **buf) {
+ if (!initX11()) {
+ return -1;
+ }
+
+ unsigned char *data;
+ Atom actual;
+ int format;
+ unsigned long n = 0;
+ unsigned long size = 0;
+ if (sev->property == None || sev->selection != sel || sev->property != prop) {
+ return 0;
+ }
+
+ int ret = (*P_XGetWindowProperty)(sev->display, sev->requestor, sev->property,
+ 0L, (~0L), 0, AnyPropertyType, &actual, &format, &size, &n, &data);
+ if (ret != Success) {
+ return 0;
+ }
+
+ if (actual == target && buf != NULL) {
+ *buf = (char *)malloc(size * sizeof(char));
+ memcpy(*buf, data, size*sizeof(char));
+ }
+ (*P_XFree)(data);
+ (*P_XDeleteProperty)(sev->display, sev->requestor, sev->property);
+ return size * sizeof(char);
+}
+
+// clipboard_read reads the clipboard selection in given format typ.
+// the read bytes are written into buf and returns the size of the buffer.
+//
+// The caller of this function should responsible for the free of the buf.
+unsigned long clipboard_read(char* typ, char **buf) {
+ if (!initX11()) {
+ return -1;
+ }
+
+ Display* d = NULL;
+ for (int i = 0; i < 42; i++) {
+ d = (*P_XOpenDisplay)(0);
+ if (d == NULL) {
+ continue;
+ }
+ break;
+ }
+ if (d == NULL) {
+ return -1;
+ }
+
+ Window w = (*P_XCreateSimpleWindow)(d, (*P_XDefaultRootWindow)(d), 0, 0, 1, 1, 0, 0, 0);
+
+ // Use False because these may not available for the first time.
+ Atom sel = (*P_XInternAtom)(d, "CLIPBOARD", False);
+ Atom prop = (*P_XInternAtom)(d, "GOLANG_DESIGN_DATA", False);
+
+ // Use True to makesure the requested type is a valid type.
+ Atom target = (*P_XInternAtom)(d, typ, True);
+ if (target == None) {
+ (*P_XCloseDisplay)(d);
+ return -2;
+ }
+
+ (*P_XConvertSelection)(d, sel, target, prop, w, CurrentTime);
+ XEvent event;
+ for (;;) {
+ (*P_XNextEvent)(d, &event);
+ if (event.type != SelectionNotify) continue;
+ break;
+ }
+ unsigned long n = read_data((XSelectionEvent *)&event.xselection, sel, prop, target, buf);
+ (*P_XCloseDisplay)(d);
+ return n;
+}
diff --git a/clipboard_linux.go b/clipboard_linux.go
new file mode 100644
index 0000000..fbd5b14
--- /dev/null
+++ b/clipboard_linux.go
@@ -0,0 +1,172 @@
+// Copyright 2021 The golang.design Initiative Authors.
+// All rights reserved. Use of this source code is governed
+// by a MIT license that can be found in the LICENSE file.
+//
+// Written by Changkun Ou
+
+//go:build linux && !android
+
+package clipboard
+
+/*
+#cgo LDFLAGS: -ldl
+#include
+#include
+#include
+#include
+
+int clipboard_test();
+int clipboard_write(
+ char* typ,
+ unsigned char* buf,
+ size_t n,
+ uintptr_t handle
+);
+unsigned long clipboard_read(char* typ, char **out);
+*/
+import "C"
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "os"
+ "runtime"
+ "runtime/cgo"
+ "time"
+ "unsafe"
+)
+
+var helpmsg = `%w: Failed to initialize the X11 display, and the clipboard package
+will not work properly. Install the following dependency may help:
+
+ apt install -y libx11-dev
+
+If the clipboard package is in an environment without a frame buffer,
+such as a cloud server, it may also be necessary to install xvfb:
+
+ apt install -y xvfb
+
+and initialize a virtual frame buffer:
+
+ Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
+ export DISPLAY=:99.0
+
+Then this package should be ready to use.
+`
+
+func initialize() error {
+ ok := C.clipboard_test()
+ if ok != 0 {
+ return fmt.Errorf(helpmsg, errUnavailable)
+ }
+ return nil
+}
+
+func read(t format) (buf []byte, err error) {
+ switch t {
+ case fmtText:
+ return readc("UTF8_STRING")
+ case fmtImage:
+ return readc("image/png")
+ }
+ return nil, errUnsupported
+}
+
+func readc(t string) ([]byte, error) {
+ ct := C.CString(t)
+ defer C.free(unsafe.Pointer(ct))
+
+ var data *C.char
+ n := C.clipboard_read(ct, &data)
+ switch C.long(n) {
+ case -1:
+ return nil, errUnavailable
+ case -2:
+ return nil, errUnsupported
+ }
+ if data == nil {
+ return nil, errUnavailable
+ }
+ defer C.free(unsafe.Pointer(data))
+ switch {
+ case n == 0:
+ return nil, nil
+ default:
+ return C.GoBytes(unsafe.Pointer(data), C.int(n)), nil
+ }
+}
+
+// write writes the given data to clipboard and
+// returns true if success or false if failed.
+func write(t format, buf []byte) (<-chan struct{}, error) {
+ var s string
+ switch t {
+ case fmtText:
+ s = "UTF8_STRING"
+ case fmtImage:
+ s = "image/png"
+ }
+
+ start := make(chan int)
+ done := make(chan struct{}, 1)
+
+ go func() { // serve as a daemon until the ownership is terminated.
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ cs := C.CString(s)
+ defer C.free(unsafe.Pointer(cs))
+
+ h := cgo.NewHandle(start)
+ var ok C.int
+ if len(buf) == 0 {
+ ok = C.clipboard_write(cs, nil, 0, C.uintptr_t(h))
+ } else {
+ ok = C.clipboard_write(cs, (*C.uchar)(unsafe.Pointer(&(buf[0]))), C.size_t(len(buf)), C.uintptr_t(h))
+ }
+ if ok != C.int(0) {
+ fmt.Fprintf(os.Stderr, "write failed with status: %d\n", int(ok))
+ }
+ done <- struct{}{}
+ close(done)
+ }()
+
+ status := <-start
+ if status < 0 {
+ return nil, errUnavailable
+ }
+ // wait until enter event loop
+ return done, nil
+}
+
+func watch(ctx context.Context, t format) <-chan []byte {
+ recv := make(chan []byte, 1)
+ ti := time.NewTicker(time.Second)
+ last := doRead(t)
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ close(recv)
+ return
+ case <-ti.C:
+ b := doRead(t)
+ if b == nil {
+ continue
+ }
+ if !bytes.Equal(last, b) {
+ recv <- b
+ last = b
+ }
+ }
+ }
+ }()
+ return recv
+}
+
+//export syncStatus
+func syncStatus(h uintptr, val int) {
+ v := cgo.Handle(h).Value().(chan int)
+ v <- val
+ cgo.Handle(h).Delete()
+}
diff --git a/clipboard_nocgo.go b/clipboard_nocgo.go
new file mode 100644
index 0000000..0e6689d
--- /dev/null
+++ b/clipboard_nocgo.go
@@ -0,0 +1,25 @@
+//go:build !windows && !cgo
+
+package clipboard
+
+import "context"
+
+func initialize() error {
+ return errNoCgo
+}
+
+func read(t format) (buf []byte, err error) {
+ panic("clipboard: cannot use when CGO_ENABLED=0")
+}
+
+func readc(t string) ([]byte, error) {
+ panic("clipboard: cannot use when CGO_ENABLED=0")
+}
+
+func write(t format, buf []byte) (<-chan struct{}, error) {
+ panic("clipboard: cannot use when CGO_ENABLED=0")
+}
+
+func watch(ctx context.Context, t format) <-chan []byte {
+ panic("clipboard: cannot use when CGO_ENABLED=0")
+}
diff --git a/clipboard_nowindows.go b/clipboard_nowindows.go
new file mode 100644
index 0000000..c2c9644
--- /dev/null
+++ b/clipboard_nowindows.go
@@ -0,0 +1,284 @@
+//go:build !windows
+
+// Copyright 2021 The golang.design Initiative Authors.
+// All rights reserved. Use of this source code is governed
+// by a MIT license that can be found in the LICENSE file.
+//
+// Written by Changkun Ou
+
+/*
+Package clipboard provides cross platform clipboard access and supports
+macOS/Linux/Windows/Android/iOS platform. Before interacting with the
+clipboard, one must call Init to assert if it is possible to use this
+package:
+
+ err := clipboard.Init()
+ if err != nil {
+ panic(err)
+ }
+
+The most common operations are `doRead` and `doWrite`. To use them:
+
+ // write/read text format data of the clipboard, and
+ // the byte buffer regarding the text are UTF8 encoded.
+ clipboard.doWrite(clipboard.FmtText, []byte("text data"))
+ clipboard.doRead(clipboard.FmtText)
+
+ // write/read image format data of the clipboard, and
+ // the byte buffer regarding the image are PNG encoded.
+ clipboard.doWrite(clipboard.FmtImage, []byte("image data"))
+ clipboard.doRead(clipboard.FmtImage)
+
+Note that read/write regarding image format assumes that the bytes are
+PNG encoded since it serves the alpha blending purpose that might be
+used in other graphical software.
+
+In addition, `clipboard.doWrite` returns a channel that can receive an
+empty struct as a signal, which indicates the corresponding write call
+to the clipboard is outdated, meaning the clipboard has been overwritten
+by others and the previously written data is lost. For instance:
+
+ changed := clipboard.doWrite(clipboard.FmtText, []byte("text data"))
+
+ select {
+ case <-changed:
+ println(`"text data" is no longer available from clipboard.`)
+ }
+
+You can ignore the returning channel if you don't need this type of
+notification. Furthermore, when you need more than just knowing whether
+clipboard data is changed, use the watcher API:
+
+ ch := clipboard.doWatchWatch(context.TODO(), clipboard.FmtText)
+ for data := range ch {
+ // print out clipboard data whenever it is changed
+ println(string(data))
+ }
+*/
+package clipboard // import "golang.design/x/clipboard"
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "runtime"
+ "sync"
+ "sync/atomic"
+ "time"
+)
+
+var (
+ // activate only for running tests.
+ debug = false
+ errUnavailable = errors.New("clipboard unavailable")
+ errUnsupported = errors.New("unsupported format")
+ errNoCgo = errors.New("clipboard: cannot use when CGO_ENABLED=0")
+)
+
+var (
+ // Due to the limitation on operating systems (such as darwin),
+ // concurrent read can even cause panic, use a global lock to
+ // guarantee one read at a time.
+ lock = sync.Mutex{}
+ initOnce sync.Once
+ initError error
+)
+
+// Init initializes the clipboard package. It returns an error
+// if the clipboard is not available to use. This may happen if the
+// target system lacks required dependency, such as libx11-dev in X11
+// environment. For example,
+//
+// err := clipboard.Init()
+// if err != nil {
+// panic(err)
+// }
+//
+// If Init returns an error, any subsequent doRead/doWrite/doWatchWatch call
+// may result in an unrecoverable panic.
+func Init() error {
+ initOnce.Do(func() {
+ initError = initialize()
+ })
+ return initError
+}
+
+// doRead returns a chunk of bytes of the clipboard data if it presents
+// in the desired format t presents. Otherwise, it returns nil.
+func doRead(t format) []byte {
+ lock.Lock()
+ defer lock.Unlock()
+
+ buf, err := read(t)
+ if err != nil {
+ if debug {
+ fmt.Fprintf(os.Stderr, "read clipboard err: %v\n", err)
+ }
+ return nil
+ }
+ return buf
+}
+
+// doWrite writes a given buffer to the clipboard in a specified format.
+// doWrite returned a receive-only channel can receive an empty struct
+// as a signal, which indicates the clipboard has been overwritten from
+// this write.
+// If format t indicates an image, then the given buf assumes
+// the image data is PNG encoded.
+func doWrite(t format, buf []byte) <-chan struct{} {
+ lock.Lock()
+ defer lock.Unlock()
+
+ changed, err := write(t, buf)
+ if err != nil {
+ if debug {
+ fmt.Fprintf(os.Stderr, "write to clipboard err: %v\n", err)
+ }
+ return nil
+ }
+ return changed
+}
+
+// doWatch returns a receive-only channel that received the clipboard data
+// whenever any change of clipboard data in the desired format happens.
+//
+// The returned channel will be closed if the given context is canceled.
+func doWatch(ctx context.Context, t format) <-chan []byte {
+ return watch(ctx, t)
+}
+
+func Get() (Clipboard, error) {
+ txt := doRead(0)
+ img := doRead(1)
+ if txt == nil && img == nil {
+ return Clipboard{}, errUnavailable
+ }
+ res := Clipboard{
+ winOriginTypes: nil,
+ platform: runtime.GOOS,
+ date: time.Now(),
+ }
+ if txt != nil && img == nil {
+ res.primaryType = Text
+ res.primaryData = txt
+ res.primarySize = len(txt)
+ }
+ if img != nil && txt == nil {
+ res.primaryType = Image
+ res.primaryData = img
+ res.primarySize = len(img)
+ }
+ res.primaryType = Text
+ res.primaryData = txt
+ res.primarySize = len(txt)
+ res.secondaryType = Image
+ res.secondaryData = img
+ res.secondarySize = len(img)
+ return res, nil
+}
+
+func GetMeta() (Clipboard, error) {
+ return Get()
+}
+
+func AutoFetcher(uFormat string) (interface{}, error) {
+ return nil, fmt.Errorf("This platform does not support AutoFetcher")
+}
+
+func SetOrigin(types string, data []byte) error {
+ return fmt.Errorf("This platform does not support SetOrigin")
+}
+
+func Set(types FileType, data []byte) error {
+ ft := 0
+ switch types {
+ case Text:
+ ft = 0
+ case File:
+ ft = 1
+ default:
+ return errors.New("not support type:" + string(types))
+ }
+ res := doWrite(format(ft), data)
+ if res == nil {
+ return fmt.Errorf("This platform does not support Set for type:" + string(types))
+ }
+ select {
+ case <-res:
+ return nil
+ case <-time.After(10 * time.Second):
+ return fmt.Errorf("Set operation timeout for type:" + string(types))
+ }
+}
+
+func ClipSize(uFormat string) (int, error) {
+ return 0, fmt.Errorf("This platform does not support ClipSize")
+}
+
+func AutoSetter(uFormat string, data interface{}) error {
+ return fmt.Errorf("This platform does not support AutoSetter")
+}
+
+var isListening uint32
+var stopSignal chan struct{}
+
+func Listen(onlyMeta bool) (<-chan Clipboard, error) {
+ if atomic.LoadUint32(&isListening) != 0 {
+ return nil, errors.New("Already listening")
+ }
+ atomic.StoreUint32(&isListening, 1)
+ ctx, stopFn := context.WithCancel(context.Background())
+ res := make(chan Clipboard, 10) // 增大缓冲区
+ stopSignal = make(chan struct{})
+ go func() {
+ txt := doWatch(ctx, 0)
+ img := doWatch(ctx, 1)
+ for {
+ select {
+ case <-stopSignal:
+ stopFn()
+ atomic.StoreUint32(&isListening, 0)
+ close(res)
+ close(stopSignal)
+ return
+ case t, ok := <-txt:
+ if !ok {
+ continue
+ }
+ res <- Clipboard{
+ platform: runtime.GOOS,
+ date: time.Now(),
+ primaryType: Text,
+ primaryData: t,
+ primarySize: len(t),
+ }
+ case i, ok := <-img:
+ if !ok {
+ continue
+ }
+ res <- Clipboard{
+ platform: runtime.GOOS,
+ date: time.Now(),
+ primaryType: Image,
+ primaryData: i,
+ primarySize: len(i),
+ }
+ }
+ }
+ }()
+ return res, nil
+}
+
+func StopListen() error {
+ if atomic.LoadUint32(&isListening) == 0 {
+ return nil
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("StopListen panic:", r)
+ }
+ }()
+ close(stopSignal)
+ return nil
+}
diff --git a/clipboard_test.go b/clipboard_test.go
index 78779af..4624d97 100644
--- a/clipboard_test.go
+++ b/clipboard_test.go
@@ -2,10 +2,7 @@ package clipboard
import (
"b612.me/win32api"
- "encoding/binary"
"fmt"
- "os"
- "syscall"
"testing"
"time"
)
@@ -19,7 +16,7 @@ func TestGet(t *testing.T) {
for {
select {
case cb := <-lsn:
- fmt.Println(cb.plateform)
+ fmt.Println(cb.platform)
fmt.Println(cb.winOriginTypes)
fmt.Println(cb.AvailableTypes())
if cb.IsText() {
@@ -49,7 +46,7 @@ func TestGetMeta(t *testing.T) {
for {
select {
case cb := <-lsn:
- fmt.Println(cb.plateform)
+ fmt.Println(cb.platform)
fmt.Println(cb.winOriginTypes)
fmt.Println(cb.AvailableTypes())
fmt.Println(cb.primarySize)
@@ -62,37 +59,3 @@ func TestGetMeta(t *testing.T) {
}
}
}
-
-func TestAutoSetter(t *testing.T) {
- //samp := "天狼星、测试,123.hello.love.what??"
- /*
- err := AutoSetter("File", []string{"C:\\Users\\Starainrt\\Desktop\\haruhi.jpg"})
- if err != nil {
- t.Fatal(err)
- }
- */
- f, err := os.ReadFile("C:\\Users\\Starainrt\\Desktop\\60.png")
- if err != nil {
- t.Fatal(err)
- }
- err = AutoSetter("Image", f)
- if err != nil {
- t.Fatal(err)
- }
-}
-
-func TestSetTextOrigin(t *testing.T) {
- samp := "天狼星、测试,123.hello.love.what"
- u, err := syscall.UTF16FromString(samp)
- if err != nil {
- t.Fatal(err)
- }
- b := make([]byte, 2*len(u))
- for i, v := range u {
- binary.LittleEndian.PutUint16(b[i*2:], v)
- }
- err = setClipboardData(win32api.CF_UNICODETEXT, b, nil)
- if err != nil {
- t.Fatal(err)
- }
-}
diff --git a/clipboard_windows.go b/clipboard_windows.go
index 7fd94db..ab68e90 100644
--- a/clipboard_windows.go
+++ b/clipboard_windows.go
@@ -2,10 +2,30 @@ package clipboard
import (
"b612.me/win32api"
+ "errors"
"fmt"
"runtime"
+ "sort"
+ "sync"
+ "time"
)
+var (
+ clipboardWorker chan func()
+ workerReady sync.WaitGroup
+)
+
+func init() {
+ // 创建专门的工作线程来处理剪贴板操作
+ clipboardWorker = make(chan func(), 10)
+ go func() {
+ runtime.LockOSThread() // 永久锁定这个线程
+ for fn := range clipboardWorker {
+ fn()
+ }
+ }()
+}
+
var winformat = map[win32api.DWORD]string{
1: "CF_TEXT",
2: "CF_BITMAP",
@@ -37,10 +57,11 @@ var formatRank = map[string]int{
"CF_DIBV5": 2,
//"CF_DIB": 2,
"PNG": 2,
- "HTML Format": 3,
+ "HTML format": 3,
"CF_HDROP": 4,
}
+// 公开的Get和GetMeta函数
func Get() (Clipboard, error) {
return innerGetClipboard(true)
}
@@ -52,108 +73,212 @@ func GetMeta() (Clipboard, error) {
func innerGetClipboard(withFetch bool) (Clipboard, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
- var tmpData interface{}
- var c = Clipboard{
- plateform: "windows",
+
+ c := Clipboard{
+ platform: "windows",
+ date: time.Now(),
}
+
err := win32api.OpenClipboard(0)
if err != nil {
return c, fmt.Errorf("OpenClipboard error: %v", err)
}
defer win32api.CloseClipboard()
+
formats, err := win32api.GetUpdatedClipboardFormatsAll()
if err != nil {
return c, fmt.Errorf("GetUpdatedClipboardFormatsAll error: %v", err)
}
- var firstFormatName, secondFormatName string
- var firstFormat, secondFormat int = 65535, 65535
+
+ type formatEntry struct {
+ name string
+ rank int
+ }
+
+ var rankedFormats []formatEntry
for _, format := range formats {
- if d, ok := winformat[format]; ok {
- if formatRank[d] > 0 {
- if formatRank[d] < firstFormat {
- secondFormat = firstFormat
- secondFormatName = firstFormatName
- firstFormat = formatRank[d]
- firstFormatName = d
- } else if formatRank[d] != firstFormat && formatRank[d] < secondFormat {
- secondFormat = formatRank[d]
- secondFormatName = d
- }
- }
- c.winOriginTypes = append(c.winOriginTypes, d)
- continue
- }
- d, e := win32api.GetClipboardFormatName(format)
- if e != nil {
- continue
- }
- if formatRank[d] > 0 {
- if formatRank[d] < firstFormat {
- secondFormat = firstFormat
- secondFormatName = firstFormatName
- firstFormat = formatRank[d]
- firstFormatName = d
- } else if formatRank[d] != firstFormat && formatRank[d] < secondFormat {
- secondFormat = formatRank[d]
- secondFormatName = d
+ var name string
+ var ok bool
+ if name, ok = winformat[format]; !ok {
+ name, err = win32api.GetClipboardFormatName(format)
+ if err != nil {
+ continue
}
}
- c.winOriginTypes = append(c.winOriginTypes, d)
- }
+ c.winOriginTypes = append(c.winOriginTypes, name)
- c.primaryOriType = firstFormatName
- switch c.primaryOriType {
- case "CF_UNICODETEXT":
- c.primaryType = Text
- case "HTML Format":
- c.primaryType = HTML
- case "PNG", "CF_DIBV5", "CF_DIB":
- c.primaryType = Image
- case "CF_HDROP":
- c.primaryType = File
- }
- if withFetch {
- tmpData, err = AutoFetcher(firstFormatName)
- if err != nil {
- return c, fmt.Errorf("AutoFetcher error: %v", err)
- }
- c.primaryData = tmpData.([]byte)
- c.primarySize = len(c.primaryData)
- } else {
- c.primarySize, err = ClipSize(firstFormatName)
- if err != nil {
- return c, fmt.Errorf("ClipSize error: %v", err)
+ rank := formatRank[name]
+ if rank > 0 {
+ rankedFormats = append(rankedFormats, formatEntry{name: name, rank: rank})
}
}
- if secondFormatName != "" {
- switch secondFormatName {
- case "CF_UNICODETEXT":
- c.secondaryType = Text
- case "HTML Format":
- c.secondaryType = HTML
- case "PNG", "CF_DIBV5", "CF_DIB":
- c.secondaryType = Image
- case "CF_HDROP":
- c.secondaryType = File
- }
- c.secondaryOriType = secondFormatName
- if withFetch {
- tmpData, err = AutoFetcher(secondFormatName)
- if err != nil {
- return c, fmt.Errorf("AutoFetcher error: %v", err)
- }
- c.secondaryData = tmpData.([]byte)
- c.secondarySize = len(c.secondaryData)
- } else {
- c.secondarySize, err = ClipSize(secondFormatName)
- if err != nil {
- return c, fmt.Errorf("ClipSize error: %v", err)
- }
- }
+ sort.Slice(rankedFormats, func(i, j int) bool {
+ return rankedFormats[i].rank < rankedFormats[j].rank
+ })
+
+ var primaryName, secondaryName string
+ if len(rankedFormats) > 0 {
+ primaryName = rankedFormats[0].name
+ }
+ if len(rankedFormats) > 1 {
+ secondaryName = rankedFormats[1].name
+ }
+
+ if primaryName == "" {
+ return c, errors.New("no supported primary format found in clipboard")
+ }
+
+ err = setClipTypeAndData(&c, primaryName, true, withFetch)
+ if err != nil {
+ return c, err
+ }
+
+ if secondaryName != "" {
+ err = setClipTypeAndData(&c, secondaryName, false, withFetch)
if err != nil {
- return c, fmt.Errorf("AutoFetcher error: %v", err)
+ return c, err
}
}
+
return c, nil
}
+
+func getClipboardWithRetry() (Clipboard, error) {
+ return innerGetClipboardSafe(true)
+}
+
+func getMetaWithRetry() (Clipboard, error) {
+ return innerGetClipboardSafe(false)
+}
+
+// 安全的剪贴板读取(已在工作线程中)
+func innerGetClipboardSafe(withFetch bool) (Clipboard, error) {
+ c := Clipboard{
+ platform: "windows",
+ date: time.Now(),
+ }
+
+ // 尝试打开剪贴板,带超时
+ var err error
+ for i := 0; i < 5; i++ {
+ err = win32api.OpenClipboard(0)
+ if err == nil {
+ break
+ }
+ time.Sleep(5 * time.Millisecond)
+ }
+ if err != nil {
+ return c, fmt.Errorf("OpenClipboard error after retries: %v", err)
+ }
+ defer win32api.CloseClipboard()
+
+ // 快速获取格式
+ formats, err := win32api.GetUpdatedClipboardFormatsAll()
+ if err != nil {
+ return c, fmt.Errorf("GetUpdatedClipboardFormatsAll error: %v", err)
+ }
+
+ type formatEntry struct {
+ name string
+ rank int
+ }
+
+ var rankedFormats []formatEntry
+ for _, format := range formats {
+ var name string
+ var ok bool
+ if name, ok = winformat[format]; !ok {
+ name, _ = win32api.GetClipboardFormatName(format)
+ if name == "" {
+ continue
+ }
+ }
+ c.winOriginTypes = append(c.winOriginTypes, name)
+
+ rank := formatRank[name]
+ if rank > 0 {
+ rankedFormats = append(rankedFormats, formatEntry{name: name, rank: rank})
+ }
+ }
+
+ sort.Slice(rankedFormats, func(i, j int) bool {
+ return rankedFormats[i].rank < rankedFormats[j].rank
+ })
+
+ var primaryName, secondaryName string
+ if len(rankedFormats) > 0 {
+ primaryName = rankedFormats[0].name
+ }
+ if len(rankedFormats) > 1 {
+ secondaryName = rankedFormats[1].name
+ }
+
+ if primaryName == "" {
+ return c, errors.New("no supported primary format found")
+ }
+
+ err = setClipTypeAndData(&c, primaryName, true, withFetch)
+ if err != nil {
+ return c, err
+ }
+
+ if secondaryName != "" {
+ setClipTypeAndData(&c, secondaryName, false, withFetch)
+ }
+
+ return c, nil
+}
+
+func setClipTypeAndData(c *Clipboard, formatName string, isPrimary bool, withFetch bool) error {
+ var typ FileType
+ switch formatName {
+ case "CF_UNICODETEXT":
+ typ = Text
+ case "HTML format":
+ typ = HTML
+ case "PNG", "CF_DIBV5", "CF_DIB":
+ typ = Image
+ case "CF_HDROP":
+ typ = File
+ default:
+ return fmt.Errorf("unsupported format: %s", formatName)
+ }
+
+ if isPrimary {
+ c.primaryOriType = formatName
+ c.primaryType = typ
+ } else {
+ c.secondaryOriType = formatName
+ c.secondaryType = typ
+ }
+
+ if withFetch {
+ tmpData, err := AutoFetcher(formatName)
+ if err != nil {
+ return fmt.Errorf("AutoFetcher error: %v", err)
+ }
+ data, ok := tmpData.([]byte)
+ if !ok {
+ return fmt.Errorf("unexpected data type from AutoFetcher: %T", tmpData)
+ }
+ if isPrimary {
+ c.primaryData = data
+ c.primarySize = len(data)
+ } else {
+ c.secondaryData = data
+ c.secondarySize = len(data)
+ }
+ } else {
+ size, err := ClipSize(formatName)
+ if err != nil {
+ return fmt.Errorf("ClipSize error: %v", err)
+ }
+ if isPrimary {
+ c.primarySize = size
+ } else {
+ c.secondarySize = size
+ }
+ }
+ return nil
+}
diff --git a/def.go b/def.go
new file mode 100644
index 0000000..31bfc39
--- /dev/null
+++ b/def.go
@@ -0,0 +1,158 @@
+package clipboard
+
+import (
+ "strings"
+ "time"
+)
+
+type FileType string
+
+const (
+ Text FileType = "text"
+ File FileType = "file"
+ Image FileType = "image"
+ HTML FileType = "html"
+)
+
+type Clipboard struct {
+ winOriginTypes []string
+ platform string
+ date time.Time
+ secondaryOriType string
+ secondaryType FileType
+ secondaryData []byte
+ secondarySize int
+
+ primaryOriType string
+ primaryType FileType
+ primaryData []byte
+ primarySize int
+ hash string
+}
+
+// format represents the format of clipboard data.
+type format int
+
+// All sorts of supported clipboard data
+const (
+ // FmtText indicates plain text clipboard format
+ fmtText format = iota
+ // FmtImage indicates image/png clipboard format
+ fmtImage
+)
+
+func (c *Clipboard) PrimaryType() FileType {
+ return c.primaryType
+}
+
+func (c *Clipboard) AvailableTypes() []FileType {
+ var res = make([]FileType, 0, 2)
+ if c.primaryType != "" {
+ res = append(res, c.primaryType)
+ }
+ if c.secondaryType != "" {
+ res = append(res, c.secondaryType)
+ }
+ return res
+}
+
+func (c *Clipboard) IsText() bool {
+ return c.primaryType == Text || c.secondaryType == Text
+}
+
+func (c *Clipboard) Text() string {
+ if c.primaryType == Text {
+ return string(c.primaryData)
+ }
+ if c.secondaryType == Text {
+ return string(c.secondaryData)
+ }
+ return ""
+}
+
+func (c *Clipboard) TextSize() int {
+ if c.primaryType == Text {
+ return c.primarySize
+ }
+ if c.secondaryType == Text {
+ return c.secondarySize
+ }
+ return 0
+}
+
+func (c *Clipboard) IsHTML() bool {
+ return (c.primaryType == HTML || c.secondaryType == HTML) || c.IsText()
+}
+
+func (c *Clipboard) HTML() string {
+ var htmlBytes []byte
+ if c.primaryType == HTML {
+ htmlBytes = c.primaryData
+ } else if c.secondaryType == HTML {
+ htmlBytes = c.secondaryData
+ } else {
+ return c.Text()
+ }
+ formats := strings.SplitN(string(htmlBytes), "\n", 7)
+ if len(formats) < 7 {
+ return string(htmlBytes)
+ }
+ return formats[6]
+}
+
+func (c *Clipboard) FilePaths() []string {
+ if c.primaryType == File {
+ return strings.Split(string(c.primaryData), "|")
+ }
+ if c.secondaryType == File {
+ return strings.Split(string(c.secondaryData), "|")
+ }
+ return nil
+}
+
+func (c *Clipboard) IsFile() bool {
+ return c.primaryType == File || c.secondaryType == File
+}
+
+func (c *Clipboard) FirstFilePath() string {
+ if c.primaryType == File {
+ return strings.Split(string(c.primaryData), "|")[0]
+ }
+ if c.secondaryType == File {
+ return strings.Split(string(c.secondaryData), "|")[0]
+ }
+ return ""
+}
+
+func (c *Clipboard) Image() []byte {
+ if c.primaryType == Image {
+ return c.primaryData
+ }
+ if c.secondaryType == Image {
+ return c.secondaryData
+ }
+ return nil
+}
+
+func (c *Clipboard) ImageSize() int {
+ if c.primaryType == Image {
+ return c.primarySize
+ }
+ if c.secondaryType == Image {
+ return c.secondarySize
+ }
+ return 0
+
+}
+
+func (c *Clipboard) IsImage() bool {
+ return c.primaryType == Image || c.secondaryType == Image
+}
+
+func (c *Clipboard) PrimaryTypeSize() int {
+ return c.primarySize
+}
+
+func (c *Clipboard) SecondaryTypeSize() int {
+ return c.secondarySize
+}
diff --git a/go.mod b/go.mod
index a226e3f..0a79d67 100644
--- a/go.mod
+++ b/go.mod
@@ -1,10 +1,16 @@
module b612.me/clipboard
-go 1.21.2
+go 1.24.0
+
+toolchain go1.24.5
require (
- b612.me/win32api v0.0.0-20240402021613-0959dfb96afa
- golang.org/x/image v0.15.0
+ b612.me/win32api v0.0.3
+ golang.org/x/image v0.32.0
+ golang.org/x/mobile v0.0.0-20251021151156-188f512ec823
)
-require golang.org/x/sys v0.18.0 // indirect
+require (
+ golang.org/x/exp/shiny v0.0.0-20251023183803-a4bb9ffd2546 // indirect
+ golang.org/x/sys v0.38.0 // indirect
+)
diff --git a/go.sum b/go.sum
index 8ce4fb1..b80ddce 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,11 @@
-b612.me/win32api v0.0.0-20240402021613-0959dfb96afa h1:BsFIbLbjQqq9Yuh+eWs7JmmXcw2RKerP1NT7X8+GKR4=
-b612.me/win32api v0.0.0-20240402021613-0959dfb96afa/go.mod h1:sj66sFJDKElEjOR+0YhdSW6b4kq4jsXu4T5/Hnpyot0=
-golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
-golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+b612.me/win32api v0.0.3 h1:TfINlv9BBmWC/YbkJ0MTpN1NzTFPnnTGB5Dux6iRWIA=
+b612.me/win32api v0.0.3/go.mod h1:sj66sFJDKElEjOR+0YhdSW6b4kq4jsXu4T5/Hnpyot0=
+golang.org/x/exp/shiny v0.0.0-20251023183803-a4bb9ffd2546 h1:x6e614Gmc2aX69sL3tI7s5hsUgZmGp/38/Wjb90khW8=
+golang.org/x/exp/shiny v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:QMAAUorQ8fzCK0C6mr4X4XV9BEp7Al6+jlejJvfYKw4=
+golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
+golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
+golang.org/x/mobile v0.0.0-20251021151156-188f512ec823 h1:M0DtBf/UvJoTH+tk6tgHT2NVxNEJCYhVu1g/xeD+GEk=
+golang.org/x/mobile v0.0.0-20251021151156-188f512ec823/go.mod h1:3QSlP0AtP6HPTLbsxfgfefGN76jpIB9yBsMqB8UY37I=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
diff --git a/listener_windows.go b/listener_windows.go
index a88a805..1a3df26 100644
--- a/listener_windows.go
+++ b/listener_windows.go
@@ -1,11 +1,17 @@
+//go:build windows
+
package clipboard
import (
"b612.me/win32api"
"errors"
"fmt"
+ "runtime"
+ "sync"
"sync/atomic"
+ "syscall"
"time"
+ "unsafe"
)
var stopSign chan struct{}
@@ -71,7 +77,6 @@ func fetchListener(hWnd win32api.HWND, res chan struct{}) {
}
}
}
-*/
func PauseListen() error {
if atomic.LoadUint32(&isListening) == 0 {
@@ -89,6 +94,7 @@ func RecoverListen() error {
return nil
}
+
func StopListen() error {
defer func() {
if r := recover(); r != nil {
@@ -102,6 +108,7 @@ func StopListen() error {
return nil
}
+
func Listen(onlyMeta bool) (<-chan Clipboard, error) {
if atomic.LoadUint32(&isListening) != 0 {
return nil, errors.New("Already listening")
@@ -154,3 +161,247 @@ func Listen(onlyMeta bool) (<-chan Clipboard, error) {
}()
return res, nil
}
+*/
+
+const WM_USER_STOP = win32api.WM_USER + 1
+
+var clipboardChannel chan Clipboard
+var onlyMetaGlobal bool
+
+var hwndGlobal win32api.HWND
+
+var lastSequence win32api.DWORD
+var sequenceMux sync.Mutex
+
+func clipboardWndProc(hWnd win32api.HWND, uMsg win32api.UINT, wParam win32api.WPARAM, lParam win32api.LPARAM) win32api.LRESULT {
+ switch uMsg {
+ case 0x031D: // WM_CLIPBOARDUPDATE
+ // 标记需要检查剪贴板
+ go checkClipboard(false)
+ return 0
+ case WM_USER_STOP:
+ win32api.PostQuitMessage(0)
+ return 0
+ case 0x0002: // WM_DESTROY
+ win32api.PostQuitMessage(0)
+ return 0
+ }
+ return win32api.DefWindowProc(hWnd, uMsg, wParam, lParam)
+}
+
+// 检查剪贴板是否真的有变化
+func checkClipboard(ignoreMessage bool) {
+ if atomic.LoadUint32(&isListening) != 1 {
+ return
+ }
+
+ sequenceMux.Lock()
+ defer sequenceMux.Unlock()
+
+ // 获取当前剪贴板序列号
+ var currentSeq win32api.DWORD
+ runInWorker(func() {
+ currentSeq, _ = win32api.GetClipboardSequenceNumber()
+ })
+
+ // 如果序列号没有变化,跳过
+ if !ignoreMessage && currentSeq == lastSequence {
+ return
+ }
+ lastSequence = currentSeq
+
+ // 尝试读取剪贴板,带重试机制
+ var cb Clipboard
+ var err error
+ maxRetries := 3
+ retryDelay := 10 * time.Millisecond
+
+ for i := 0; i < maxRetries; i++ {
+ runInWorker(func() {
+ if !onlyMetaGlobal {
+ cb, err = getClipboardWithRetry()
+ } else {
+ cb, err = getMetaWithRetry()
+ }
+ })
+
+ if err == nil {
+ break
+ }
+
+ // 如果剪贴板被占用,稍等再试
+ if i < maxRetries-1 {
+ time.Sleep(retryDelay)
+ retryDelay *= 2 // 指数退避
+ }
+ }
+
+ if err == nil && clipboardChannel != nil {
+ select {
+ case clipboardChannel <- cb:
+ default:
+ // channel满了,移除最旧的
+ select {
+ case <-clipboardChannel:
+ clipboardChannel <- cb
+ default:
+ }
+ }
+ }
+}
+
+func handleClipboardUpdate() {
+ if atomic.LoadUint32(&isListening) != 1 {
+ return
+ }
+
+ var cb Clipboard
+ var err error
+ if !onlyMetaGlobal {
+ cb, err = Get()
+ } else {
+ cb, err = GetMeta()
+ }
+ if err == nil && clipboardChannel != nil {
+ select {
+ case clipboardChannel <- cb:
+ default:
+ // channel满了,丢弃旧数据
+ }
+ }
+}
+
+func Listen(onlyMeta bool) (<-chan Clipboard, error) {
+ if atomic.LoadUint32(&isListening) != 0 {
+ return nil, errors.New("Already listening")
+ }
+ atomic.StoreUint32(&isListening, 1)
+
+ res := make(chan Clipboard, 10) // 增大缓冲区
+ clipboardChannel = res
+ onlyMetaGlobal = onlyMeta
+ stopSign = make(chan struct{})
+
+ // 初始化序列号
+ runInWorker(func() {
+ lastSequence, _ = win32api.GetClipboardSequenceNumber()
+ })
+
+ go func() {
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ defer func() {
+ if hwndGlobal != 0 {
+ win32api.RemoveClipboardFormatListener(hwndGlobal)
+ win32api.DestroyWindow(hwndGlobal)
+ hwndGlobal = 0
+ }
+ atomic.StoreUint32(&isListening, 0)
+ close(res)
+ clipboardChannel = nil
+ }()
+
+ hInstance, err := win32api.GetModuleHandle(nil)
+ if err != nil {
+ return
+ }
+
+ className := fmt.Sprintf("ClipListener_%d", time.Now().UnixNano())
+
+ var wc win32api.WNDCLASSEX
+ wc.CbSize = win32api.UINT(unsafe.Sizeof(wc))
+ wc.Style = 0
+ wc.LpfnWndProc = syscall.NewCallback(clipboardWndProc)
+ wc.CbClsExtra = 0
+ wc.CbWndExtra = 0
+ wc.HInstance = hInstance
+ wc.HIcon = 0
+ wc.HCursor = 0
+ wc.HbrBackground = 0
+ wc.LpszMenuName = nil
+ wc.LpszClassName = syscall.StringToUTF16Ptr(className)
+ wc.HIconSm = 0
+
+ _, err = win32api.RegisterClassEx(&wc)
+ if err != nil {
+ return
+ }
+
+ hwnd, err := win32api.CreateWindowEx(
+ 0, className, "", 0,
+ 0, 0, 0, 0,
+ 0, 0, hInstance, unsafe.Pointer(nil))
+ if err != nil {
+ return
+ }
+ hwndGlobal = hwnd
+
+ ok, err := win32api.AddClipboardFormatListener(hwnd)
+ if err != nil || !ok {
+ win32api.DestroyWindow(hwnd)
+ return
+ }
+ go func() {
+ ticker := time.NewTicker(5000 * time.Millisecond)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-stopSign:
+ return
+ case <-ticker.C:
+ // 定期检查,防止遗漏
+ go checkClipboard(false)
+ }
+ }
+ }()
+ // 监听停止信号
+ go func() {
+ <-stopSign
+ if hwndGlobal != 0 {
+ win32api.PostMessage(hwndGlobal, WM_USER_STOP, 0, 0)
+ }
+ }()
+
+ // 消息循环
+ for {
+ var msg win32api.MSG
+ ret, err := win32api.GetMessage(&msg, 0, 0, 0)
+
+ if ret == 0 {
+ // 收到WM_QUIT消息,正常退出
+ break
+ }
+
+ if ret == 0xFFFFFFFF {
+ // GetMessage出错
+ if err != nil {
+ fmt.Printf("GetMessage error: %v\n", err)
+ }
+ continue
+ }
+
+ win32api.TranslateMessage(&msg)
+ win32api.DispatchMessage(&msg)
+ }
+ }()
+
+ return res, nil
+}
+
+func StopListen() {
+ if atomic.LoadUint32(&isListening) == 1 && stopSign != nil {
+ close(stopSign)
+ }
+}
+
+// 在工作线程中执行剪贴板操作
+func runInWorker(fn func()) {
+ done := make(chan struct{})
+ clipboardWorker <- func() {
+ fn()
+ close(done)
+ }
+ <-done
+}
diff --git a/readhandle_windows.go b/readhandle_windows.go
index 6519c47..30697cd 100644
--- a/readhandle_windows.go
+++ b/readhandle_windows.go
@@ -48,8 +48,8 @@ func AutoFetcher(uFormat string) (interface{}, error) {
switch uFormat {
case "CF_TEXT", "CF_UNICODETEXT":
return fetchClipboardData(win32api.CF_UNICODETEXT, textFetcher)
- case "HTML Format":
- return fetchClipboardData(win32api.RegisterClipboardFormat("HTML Format"), nil)
+ case "HTML format":
+ return fetchClipboardData(win32api.RegisterClipboardFormat("HTML format"), nil)
case "CF_HDROP":
return fetchClipboardData(win32api.CF_HDROP, filedropFetcher)
case "CF_DIBV5":