This commit is contained in:
Trevor Slocum 2024-10-11 19:54:04 -07:00
commit 5dd95c9434
32 changed files with 2749 additions and 0 deletions

13
Dockerfile Normal file
View file

@ -0,0 +1,13 @@
# 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>
FROM golang:1.17
RUN apt-get update && apt-get install -y \
xvfb libx11-dev libegl1-mesa-dev libgles2-mesa-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /app
COPY . .
CMD [ "sh", "-c", "./tests/test-docker.sh" ]

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Changkun Ou <contact@changkun.de>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

160
README.md Normal file
View file

@ -0,0 +1,160 @@
# clipboard [![PkgGoDev](https://pkg.go.dev/badge/golang.design/x/clipboard)](https://pkg.go.dev/golang.design/x/clipboard) ![](https://changkun.de/urlstat?mode=github&repo=golang-design/clipboard) ![clipboard](https://github.com/golang-design/clipboard/workflows/clipboard/badge.svg?branch=main)
Cross platform (macOS/Linux/Windows/Android/iOS) clipboard package in Go
```go
import "golang.design/x/clipboard"
```
## Features
- Cross platform supports: **macOS, Linux (X11), Windows, iOS, and Android**
- Copy/paste UTF-8 text
- Copy/paste PNG encoded images (Desktop-only)
- Command `gclip` as a demo application
- Mobile app `gclip-gui` as a demo application
## API Usage
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:
```go
// Init returns an error if the package is not ready for use.
err := clipboard.Init()
if err != nil {
panic(err)
}
```
The most common operations are `Read` and `Write`. To use them:
```go
// write/read text format data of the clipboard, and
// the byte buffer regarding the text are UTF8 encoded.
clipboard.Write(clipboard.FmtText, []byte("text data"))
clipboard.Read(clipboard.FmtText)
// write/read image format data of the clipboard, and
// the byte buffer regarding the image are PNG encoded.
clipboard.Write(clipboard.FmtImage, []byte("image data"))
clipboard.Read(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.Write` 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:
```go
changed := clipboard.Write(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:
```go
ch := clipboard.Watch(context.TODO(), clipboard.FmtText)
for data := range ch {
// print out clipboard data whenever it is changed
println(string(data))
}
```
## Demos
- A command line tool `gclip` for command line clipboard accesses, see document [here](./cmd/gclip/README.md).
- A GUI application `gclip-gui` for functionality verifications on mobile systems, see a document [here](./cmd/gclip-gui/README.md).
## Command Usage
`gclip` command offers the ability to interact with the system clipboard
from the shell. To install:
```bash
$ go install golang.design/x/clipboard/cmd/gclip@latest
```
```bash
$ gclip
gclip is a command that provides clipboard interaction.
usage: gclip [-copy|-paste] [-f <file>]
options:
-copy
copy data to clipboard
-f string
source or destination to a given file path
-paste
paste data from clipboard
examples:
gclip -paste paste from clipboard and prints the content
gclip -paste -f x.txt paste from clipboard and save as text to x.txt
gclip -paste -f x.png paste from clipboard and save as image to x.png
cat x.txt | gclip -copy copy content from x.txt to clipboard
gclip -copy -f x.txt copy content from x.txt to clipboard
gclip -copy -f x.png copy x.png as image data to clipboard
```
If `-copy` is used, the command will exit when the data is no longer
available from the clipboard. You can always send the command to the
background using a shell `&` operator, for example:
```bash
$ cat x.txt | gclip -copy &
```
## Platform Specific Details
This package spent efforts to provide cross platform abstraction regarding
accessing system clipboards, but here are a few details you might need to know.
### Dependency
- macOS: require Cgo, no dependency
- Linux: require X11 dev package. For instance, install `libx11-dev` or `xorg-dev` or `libX11-devel` to access X window system.
- Windows: no Cgo, no dependency
- iOS/Android: collaborate with [`gomobile`](https://golang.org/x/mobile)
### Screenshot
In general, when you need test your implementation regarding images,
There are system level shortcuts to put screenshot image into your system clipboard:
- On macOS, use `Ctrl+Shift+Cmd+4`
- On Linux/Ubuntu, use `Ctrl+Shift+PrintScreen`
- On Windows, use `Shift+Win+s`
As described in the API documentation, the package supports read/write
UTF8 encoded plain text or PNG encoded image data. Thus,
the other types of data are not supported yet, i.e. undefined behavior.
## Who is using this package?
The main purpose of building this package is to support the
[midgard](https://changkun.de/s/midgard) project, which offers
clipboard-based features like universal clipboard service that syncs
clipboard content across multiple systems, allocating public accessible
for clipboard content, etc.
To know more projects, check our [wiki](https://github.com/golang-design/clipboard/wiki) page.
## License
MIT | &copy; 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de).

154
clipboard.go Normal file
View file

@ -0,0 +1,154 @@
// 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 `Read` and `Write`. To use them:
// write/read text format data of the clipboard, and
// the byte buffer regarding the text are UTF8 encoded.
clipboard.Write(clipboard.FmtText, []byte("text data"))
clipboard.Read(clipboard.FmtText)
// write/read image format data of the clipboard, and
// the byte buffer regarding the image are PNG encoded.
clipboard.Write(clipboard.FmtImage, []byte("image data"))
clipboard.Read(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.Write` 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.Write(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.Watch(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"
"sync"
)
var (
// activate only for running tests.
debug = false
errUnavailable = errors.New("clipboard unavailable")
errUnsupported = errors.New("unsupported format")
)
// 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
)
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 Read/Write/Watch call
// may result in an unrecoverable panic.
func Init() error {
initOnce.Do(func() {
initError = initialize()
})
return initError
}
// Read returns a chunk of bytes of the clipboard data if it presents
// in the desired format t presents. Otherwise, it returns nil.
func Read(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
}
// Write writes a given buffer to the clipboard in a specified format.
// Write 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 Write(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
}
// Watch 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 Watch(ctx context.Context, t Format) <-chan []byte {
return watch(ctx, t)
}

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 := Read(t)
go func() {
for {
select {
case <-ctx.Done():
close(recv)
return
case <-ti.C:
b := Read(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 := Read(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 := Read(t)
go func() {
for {
select {
case <-ctx.Done():
close(recv)
return
case <-ti.C:
b := Read(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 readed bytes is 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;
}

166
clipboard_linux.go Normal file
View file

@ -0,0 +1,166 @@
// 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)
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 := Read(t)
go func() {
for {
select {
case <-ctx.Done():
close(recv)
return
case <-ti.C:
b := Read(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 {
panic("clipboard: cannot use when CGO_ENABLED=0")
}
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")
}

341
clipboard_test.go Normal file
View file

@ -0,0 +1,341 @@
// 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_test
import (
"bytes"
"context"
"errors"
"image/color"
"image/png"
"os"
"reflect"
"runtime"
"testing"
"time"
"golang.design/x/clipboard"
)
func init() {
clipboard.Debug = true
}
func TestClipboardInit(t *testing.T) {
t.Run("no-cgo", func(t *testing.T) {
if val, ok := os.LookupEnv("CGO_ENABLED"); !ok || val != "0" {
t.Skip("CGO_ENABLED is set to 1")
}
if runtime.GOOS == "windows" {
t.Skip("Windows does not need to check for cgo")
}
defer func() {
if r := recover(); r != nil {
return
}
t.Fatalf("expect to fail when CGO_ENABLED=0")
}()
clipboard.Init()
})
t.Run("with-cgo", func(t *testing.T) {
if val, ok := os.LookupEnv("CGO_ENABLED"); ok && val == "0" {
t.Skip("CGO_ENABLED is set to 0")
}
if runtime.GOOS != "linux" {
t.Skip("Only Linux may return error at the moment.")
}
if err := clipboard.Init(); err != nil && !errors.Is(err, clipboard.ErrUnavailable) {
t.Fatalf("expect ErrUnavailable, but got: %v", err)
}
})
}
func TestClipboard(t *testing.T) {
if runtime.GOOS != "windows" {
if val, ok := os.LookupEnv("CGO_ENABLED"); ok && val == "0" {
t.Skip("CGO_ENABLED is set to 0")
}
}
t.Run("image", func(t *testing.T) {
data, err := os.ReadFile("tests/testdata/clipboard.png")
if err != nil {
t.Fatalf("failed to read gold file: %v", err)
}
clipboard.Write(clipboard.FmtImage, data)
b := clipboard.Read(clipboard.FmtText)
if b != nil {
t.Fatalf("read clipboard that stores image data as text should fail, but got len: %d", len(b))
}
b = clipboard.Read(clipboard.FmtImage)
if b == nil {
t.Fatalf("read clipboard that stores image data as image should success, but got: nil")
}
img1, err := png.Decode(bytes.NewReader(data))
if err != nil {
t.Fatalf("write image is not png encoded: %v", err)
}
img2, err := png.Decode(bytes.NewReader(b))
if err != nil {
t.Fatalf("read image is not png encoded: %v", err)
}
w := img2.Bounds().Dx()
h := img2.Bounds().Dy()
incorrect := 0
for i := 0; i < w; i++ {
for j := 0; j < h; j++ {
wr, wg, wb, wa := img1.At(i, j).RGBA()
gr, gg, gb, ga := img2.At(i, j).RGBA()
want := color.RGBA{
R: uint8(wr),
G: uint8(wg),
B: uint8(wb),
A: uint8(wa),
}
got := color.RGBA{
R: uint8(gr),
G: uint8(gg),
B: uint8(gb),
A: uint8(ga),
}
if !reflect.DeepEqual(want, got) {
t.Logf("read data from clipbaord is inconsistent with previous written data, pix: (%d,%d), got: %+v, want: %+v", i, j, got, want)
incorrect++
}
}
}
if incorrect > 0 {
t.Fatalf("read data from clipboard contains too much inconsistent pixels to the previous written data, number of incorrect pixels: %v", incorrect)
}
})
t.Run("text", func(t *testing.T) {
data := []byte("golang.design/x/clipboard")
clipboard.Write(clipboard.FmtText, data)
b := clipboard.Read(clipboard.FmtImage)
if b != nil {
t.Fatalf("read clipboard that stores text data as image should fail, but got len: %d", len(b))
}
b = clipboard.Read(clipboard.FmtText)
if b == nil {
t.Fatal("read clipboard taht stores text data as text should success, but got: nil")
}
if !reflect.DeepEqual(data, b) {
t.Fatalf("read data from clipbaord is inconsistent with previous written data, got: %d, want: %d", len(b), len(data))
}
})
}
func TestClipboardMultipleWrites(t *testing.T) {
if runtime.GOOS != "windows" {
if val, ok := os.LookupEnv("CGO_ENABLED"); ok && val == "0" {
t.Skip("CGO_ENABLED is set to 0")
}
}
data, err := os.ReadFile("tests/testdata/clipboard.png")
if err != nil {
t.Fatalf("failed to read gold file: %v", err)
}
chg := clipboard.Write(clipboard.FmtImage, data)
data = []byte("golang.design/x/clipboard")
clipboard.Write(clipboard.FmtText, data)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
select {
case <-ctx.Done():
t.Fatalf("failed to receive clipboard change notification")
case _, ok := <-chg:
if !ok {
t.Fatalf("change channel is closed before receiving the changed clipboard data")
}
}
_, ok := <-chg
if ok {
t.Fatalf("changed channel should be closed after receiving the notification")
}
b := clipboard.Read(clipboard.FmtImage)
if b != nil {
t.Fatalf("read clipboard that should store text data as image should fail, but got: %d", len(b))
}
b = clipboard.Read(clipboard.FmtText)
if b == nil {
t.Fatalf("read clipboard that should store text data as text should success, got: nil")
}
if !reflect.DeepEqual(data, b) {
t.Fatalf("read data from clipbaord is inconsistent with previous write, want %s, got: %s", string(data), string(b))
}
}
func TestClipboardConcurrentRead(t *testing.T) {
if runtime.GOOS != "windows" {
if val, ok := os.LookupEnv("CGO_ENABLED"); ok && val == "0" {
t.Skip("CGO_ENABLED is set to 0")
}
}
// This test check that concurrent read/write to the clipboard does
// not cause crashes on some specific platform, such as macOS.
done := make(chan bool, 2)
go func() {
defer func() {
done <- true
}()
clipboard.Read(clipboard.FmtText)
}()
go func() {
defer func() {
done <- true
}()