first useable version

This commit is contained in:
兔子 2025-11-10 10:17:06 +08:00
parent 0d790f2f68
commit c4ecfd09ec
Signed by: b612
GPG Key ID: 99DD2222B612B612
22 changed files with 1860 additions and 306 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea
bin

10
.idea/.gitignore generated vendored
View File

@ -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

11
.idea/clipboard.iml generated
View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.idea/copilot/chatSessions" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/clipboard.iml" filepath="$PROJECT_DIR$/.idea/clipboard.iml" />
</modules>
</component>
</project>

View File

@ -1,34 +1,13 @@
//go:build windows
package clipboard package clipboard
import ( import (
"errors" "errors"
"strings"
"time"
) )
type FileType string func Init() error {
return nil
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 Set(types FileType, data []byte) error { func Set(types FileType, data []byte) error {
@ -40,7 +19,7 @@ func Set(types FileType, data []byte) error {
case Image: case Image:
return AutoSetter("PNG", data) return AutoSetter("PNG", data)
case HTML: case HTML:
return AutoSetter("HTML Format", data) return AutoSetter("HTML format", data)
} }
return errors.New("not support type:" + string(types)) 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 { func SetOrigin(types string, data []byte) error {
return AutoSetter(types, data) 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
}

80
clipboard_android.c Normal file
View File

@ -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 <changkun.de>
//go:build android
#include <android/log.h>
#include <jni.h>
#include <stdlib.h>
#include <string.h>
#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));
}

102
clipboard_android.go Normal file
View File

@ -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 <changkun.de>
//go:build android
package clipboard
/*
#cgo LDFLAGS: -landroid -llog
#include <stdlib.h>
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
}

122
clipboard_darwin.go Normal file
View File

@ -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 <changkun.de>
//go:build darwin && !ios
package clipboard
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
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
}

62
clipboard_darwin.m Normal file
View File

@ -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 <changkun.de>
//go:build darwin && !ios
// Interact with NSPasteboard using Objective-C
// https://developer.apple.com/documentation/appkit/nspasteboard?language=objc
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
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];
}

80
clipboard_ios.go Normal file
View File

@ -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 <changkun.de>
//go:build ios
package clipboard
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework UIKit -framework MobileCoreServices
#import <stdlib.h>
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
}

20
clipboard_ios.m Normal file
View File

@ -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 <changkun.de>
//go:build ios
#import <UIKit/UIKit.h>
#import <MobileCoreServices/MobileCoreServices.h>
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];
}

263
clipboard_linux.c Normal file
View File

@ -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 <changkun.de>
//go:build linux && !android
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <dlfcn.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
// 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;
}

172
clipboard_linux.go Normal file
View File

@ -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 <changkun.de>
//go:build linux && !android
package clipboard
/*
#cgo LDFLAGS: -ldl
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
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()
}

25
clipboard_nocgo.go Normal file
View File

@ -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")
}

284
clipboard_nowindows.go Normal file
View File

@ -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 <changkun.de>
/*
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
}

View File

@ -2,10 +2,7 @@ package clipboard
import ( import (
"b612.me/win32api" "b612.me/win32api"
"encoding/binary"
"fmt" "fmt"
"os"
"syscall"
"testing" "testing"
"time" "time"
) )
@ -19,7 +16,7 @@ func TestGet(t *testing.T) {
for { for {
select { select {
case cb := <-lsn: case cb := <-lsn:
fmt.Println(cb.plateform) fmt.Println(cb.platform)
fmt.Println(cb.winOriginTypes) fmt.Println(cb.winOriginTypes)
fmt.Println(cb.AvailableTypes()) fmt.Println(cb.AvailableTypes())
if cb.IsText() { if cb.IsText() {
@ -49,7 +46,7 @@ func TestGetMeta(t *testing.T) {
for { for {
select { select {
case cb := <-lsn: case cb := <-lsn:
fmt.Println(cb.plateform) fmt.Println(cb.platform)
fmt.Println(cb.winOriginTypes) fmt.Println(cb.winOriginTypes)
fmt.Println(cb.AvailableTypes()) fmt.Println(cb.AvailableTypes())
fmt.Println(cb.primarySize) 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)
}
}

View File

@ -2,10 +2,30 @@ package clipboard
import ( import (
"b612.me/win32api" "b612.me/win32api"
"errors"
"fmt" "fmt"
"runtime" "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{ var winformat = map[win32api.DWORD]string{
1: "CF_TEXT", 1: "CF_TEXT",
2: "CF_BITMAP", 2: "CF_BITMAP",
@ -37,10 +57,11 @@ var formatRank = map[string]int{
"CF_DIBV5": 2, "CF_DIBV5": 2,
//"CF_DIB": 2, //"CF_DIB": 2,
"PNG": 2, "PNG": 2,
"HTML Format": 3, "HTML format": 3,
"CF_HDROP": 4, "CF_HDROP": 4,
} }
// 公开的Get和GetMeta函数
func Get() (Clipboard, error) { func Get() (Clipboard, error) {
return innerGetClipboard(true) return innerGetClipboard(true)
} }
@ -52,108 +73,212 @@ func GetMeta() (Clipboard, error) {
func innerGetClipboard(withFetch bool) (Clipboard, error) { func innerGetClipboard(withFetch bool) (Clipboard, error) {
runtime.LockOSThread() runtime.LockOSThread()
defer runtime.UnlockOSThread() defer runtime.UnlockOSThread()
var tmpData interface{}
var c = Clipboard{ c := Clipboard{
plateform: "windows", platform: "windows",
date: time.Now(),
} }
err := win32api.OpenClipboard(0) err := win32api.OpenClipboard(0)
if err != nil { if err != nil {
return c, fmt.Errorf("OpenClipboard error: %v", err) return c, fmt.Errorf("OpenClipboard error: %v", err)
} }
defer win32api.CloseClipboard() defer win32api.CloseClipboard()
formats, err := win32api.GetUpdatedClipboardFormatsAll() formats, err := win32api.GetUpdatedClipboardFormatsAll()
if err != nil { if err != nil {
return c, fmt.Errorf("GetUpdatedClipboardFormatsAll error: %v", err) 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 { for _, format := range formats {
if d, ok := winformat[format]; ok { var name string
if formatRank[d] > 0 { var ok bool
if formatRank[d] < firstFormat { if name, ok = winformat[format]; !ok {
secondFormat = firstFormat name, err = win32api.GetClipboardFormatName(format)
secondFormatName = firstFormatName if err != nil {
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 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
}
}
c.winOriginTypes = append(c.winOriginTypes, d)
} }
c.winOriginTypes = append(c.winOriginTypes, name)
c.primaryOriType = firstFormatName rank := formatRank[name]
switch c.primaryOriType { if rank > 0 {
case "CF_UNICODETEXT": rankedFormats = append(rankedFormats, formatEntry{name: name, rank: rank})
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)
} }
} }
if secondFormatName != "" { sort.Slice(rankedFormats, func(i, j int) bool {
switch secondFormatName { return rankedFormats[i].rank < rankedFormats[j].rank
case "CF_UNICODETEXT": })
c.secondaryType = Text
case "HTML Format": var primaryName, secondaryName string
c.secondaryType = HTML if len(rankedFormats) > 0 {
case "PNG", "CF_DIBV5", "CF_DIB": primaryName = rankedFormats[0].name
c.secondaryType = Image
case "CF_HDROP":
c.secondaryType = File
} }
c.secondaryOriType = secondFormatName if len(rankedFormats) > 1 {
if withFetch { secondaryName = rankedFormats[1].name
tmpData, err = AutoFetcher(secondFormatName) }
if primaryName == "" {
return c, errors.New("no supported primary format found in clipboard")
}
err = setClipTypeAndData(&c, primaryName, true, withFetch)
if err != nil { if err != nil {
return c, fmt.Errorf("AutoFetcher error: %v", err) return c, err
} }
c.secondaryData = tmpData.([]byte)
c.secondarySize = len(c.secondaryData) if secondaryName != "" {
} else { err = setClipTypeAndData(&c, secondaryName, false, withFetch)
c.secondarySize, err = ClipSize(secondFormatName)
if err != nil { if err != nil {
return c, fmt.Errorf("ClipSize error: %v", err) return c, err
}
}
if err != nil {
return c, fmt.Errorf("AutoFetcher error: %v", err)
} }
} }
return c, nil 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
}

158
def.go Normal file
View File

@ -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
}

14
go.mod
View File

@ -1,10 +1,16 @@
module b612.me/clipboard module b612.me/clipboard
go 1.21.2 go 1.24.0
toolchain go1.24.5
require ( require (
b612.me/win32api v0.0.0-20240402021613-0959dfb96afa b612.me/win32api v0.0.3
golang.org/x/image v0.15.0 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
)

15
go.sum
View File

@ -1,6 +1,11 @@
b612.me/win32api v0.0.0-20240402021613-0959dfb96afa h1:BsFIbLbjQqq9Yuh+eWs7JmmXcw2RKerP1NT7X8+GKR4= b612.me/win32api v0.0.3 h1:TfINlv9BBmWC/YbkJ0MTpN1NzTFPnnTGB5Dux6iRWIA=
b612.me/win32api v0.0.0-20240402021613-0959dfb96afa/go.mod h1:sj66sFJDKElEjOR+0YhdSW6b4kq4jsXu4T5/Hnpyot0= b612.me/win32api v0.0.3/go.mod h1:sj66sFJDKElEjOR+0YhdSW6b4kq4jsXu4T5/Hnpyot0=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/exp/shiny v0.0.0-20251023183803-a4bb9ffd2546 h1:x6e614Gmc2aX69sL3tI7s5hsUgZmGp/38/Wjb90khW8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/exp/shiny v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:QMAAUorQ8fzCK0C6mr4X4XV9BEp7Al6+jlejJvfYKw4=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 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.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=

View File

@ -1,11 +1,17 @@
//go:build windows
package clipboard package clipboard
import ( import (
"b612.me/win32api" "b612.me/win32api"
"errors" "errors"
"fmt" "fmt"
"runtime"
"sync"
"sync/atomic" "sync/atomic"
"syscall"
"time" "time"
"unsafe"
) )
var stopSign chan struct{} var stopSign chan struct{}
@ -71,7 +77,6 @@ func fetchListener(hWnd win32api.HWND, res chan struct{}) {
} }
} }
} }
*/
func PauseListen() error { func PauseListen() error {
if atomic.LoadUint32(&isListening) == 0 { if atomic.LoadUint32(&isListening) == 0 {
@ -89,6 +94,7 @@ func RecoverListen() error {
return nil return nil
} }
func StopListen() error { func StopListen() error {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -102,6 +108,7 @@ func StopListen() error {
return nil return nil
} }
func Listen(onlyMeta bool) (<-chan Clipboard, error) { func Listen(onlyMeta bool) (<-chan Clipboard, error) {
if atomic.LoadUint32(&isListening) != 0 { if atomic.LoadUint32(&isListening) != 0 {
return nil, errors.New("Already listening") return nil, errors.New("Already listening")
@ -154,3 +161,247 @@ func Listen(onlyMeta bool) (<-chan Clipboard, error) {
}() }()
return res, nil 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
}

View File

@ -48,8 +48,8 @@ func AutoFetcher(uFormat string) (interface{}, error) {
switch uFormat { switch uFormat {
case "CF_TEXT", "CF_UNICODETEXT": case "CF_TEXT", "CF_UNICODETEXT":
return fetchClipboardData(win32api.CF_UNICODETEXT, textFetcher) return fetchClipboardData(win32api.CF_UNICODETEXT, textFetcher)
case "HTML Format": case "HTML format":
return fetchClipboardData(win32api.RegisterClipboardFormat("HTML Format"), nil) return fetchClipboardData(win32api.RegisterClipboardFormat("HTML format"), nil)
case "CF_HDROP": case "CF_HDROP":
return fetchClipboardData(win32api.CF_HDROP, filedropFetcher) return fetchClipboardData(win32api.CF_HDROP, filedropFetcher)
case "CF_DIBV5": case "CF_DIBV5":