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":