first useable version
This commit is contained in:
parent
0d790f2f68
commit
c4ecfd09ec
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.idea
|
||||||
|
bin
|
||||||
10
.idea/.gitignore
generated
vendored
10
.idea/.gitignore
generated
vendored
@ -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
11
.idea/clipboard.iml
generated
@ -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
8
.idea/modules.xml
generated
@ -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>
|
|
||||||
147
clipboard.go
147
clipboard.go
@ -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
80
clipboard_android.c
Normal 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
102
clipboard_android.go
Normal 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
122
clipboard_darwin.go
Normal 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
62
clipboard_darwin.m
Normal 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
80
clipboard_ios.go
Normal 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
20
clipboard_ios.m
Normal 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
263
clipboard_linux.c
Normal 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
172
clipboard_linux.go
Normal 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
25
clipboard_nocgo.go
Normal 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
284
clipboard_nowindows.go
Normal 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
|
||||||
|
}
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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
158
def.go
Normal 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
14
go.mod
@ -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
15
go.sum
@ -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=
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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":
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user