Adding upstream version 0.7.0+dfsg.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
f170ee46ad
commit
79ead63b61
35 changed files with 2904 additions and 0 deletions
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: [changkun] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
71
.github/workflows/clipboard.yml
vendored
Normal file
71
.github/workflows/clipboard.yml
vendored
Normal file
|
@ -0,0 +1,71 @@
|
|||
# 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>
|
||||
|
||||
name: clipboard
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
platform_test:
|
||||
env:
|
||||
DISPLAY: ':0.0'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go: ['1.17.x', '1.18.x', '1.19.x', '1.20.x']
|
||||
steps:
|
||||
- name: Install and run dependencies (xvfb libx11-dev)
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y xvfb libx11-dev x11-utils libegl1-mesa-dev libgles2-mesa-dev
|
||||
Xvfb :0 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
# Wait for Xvfb
|
||||
MAX_ATTEMPTS=120 # About 60 seconds
|
||||
COUNT=0
|
||||
echo -n "Waiting for Xvfb to be ready..."
|
||||
while ! xdpyinfo -display "${DISPLAY}" >/dev/null 2>&1; do
|
||||
echo -n "."
|
||||
sleep 0.50s
|
||||
COUNT=$(( COUNT + 1 ))
|
||||
if [ "${COUNT}" -ge "${MAX_ATTEMPTS}" ]; then
|
||||
echo " Gave up waiting for X server on ${DISPLAY}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "Done - Xvfb is ready!"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
stable: 'false'
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Build (${{ matrix.go }})
|
||||
run: |
|
||||
go build -o gclip cmd/gclip/main.go
|
||||
go build -o gclip-gui cmd/gclip-gui/main.go
|
||||
|
||||
- name: Run Tests with CGO_ENABLED=1 (${{ matrix.go }})
|
||||
if: ${{ runner.os == 'Linux' || runner.os == 'macOS'}}
|
||||
run: |
|
||||
CGO_ENABLED=1 go test -v -covermode=atomic .
|
||||
|
||||
- name: Run Tests with CGO_ENABLED=0 (${{ matrix.go }})
|
||||
if: ${{ runner.os == 'Linux' || runner.os == 'macOS'}}
|
||||
run: |
|
||||
CGO_ENABLED=0 go test -v -covermode=atomic .
|
||||
|
||||
- name: Run Tests on Windows (${{ matrix.go }})
|
||||
if: ${{ runner.os == 'Windows'}}
|
||||
run: |
|
||||
go test -v -covermode=atomic .
|
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
13
Dockerfile
Normal file
13
Dockerfile
Normal 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
21
LICENSE
Normal 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
160
README.md
Normal file
|
@ -0,0 +1,160 @@
|
|||
# clipboard [](https://pkg.go.dev/golang.design/x/clipboard)  
|
||||
|
||||
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 | © 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de).
|
154
clipboard.go
Normal file
154
clipboard.go
Normal 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)
|
||||
}
|
81
clipboard_android.c
Normal file
81
clipboard_android.c
Normal file
|
@ -0,0 +1,81 @@
|
|||
// 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
|
||||
// +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));
|
||||
}
|
103
clipboard_android.go
Normal file
103
clipboard_android.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
// 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
|
||||
// +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
|
||||
}
|
123
clipboard_darwin.go
Normal file
123
clipboard_darwin.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
// 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
|
||||
// +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
|
||||
}
|
63
clipboard_darwin.m
Normal file
63
clipboard_darwin.m
Normal file
|
@ -0,0 +1,63 @@
|
|||
// 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
|
||||
// +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];
|
||||
}
|
81
clipboard_ios.go
Normal file
81
clipboard_ios.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
// 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
|
||||
// +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
|
||||
}
|
21
clipboard_ios.m
Normal file
21
clipboard_ios.m
Normal file
|
@ -0,0 +1,21 @@
|
|||
// 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
|
||||
// +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];
|
||||
}
|
264
clipboard_linux.c
Normal file
264
clipboard_linux.c
Normal file
|
@ -0,0 +1,264 @@
|
|||
// 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
|
||||
// +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;
|
||||
}
|
171
clipboard_linux.go
Normal file
171
clipboard_linux.go
Normal file
|
@ -0,0 +1,171 @@
|
|||
// 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
|
||||
// +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
|
||||
}
|
||||
|
||||
type syncChan struct {
|
||||
c chan int
|
||||
}
|
||||
|
||||
//export syncStatus
|
||||
func syncStatus(h uintptr, val int) {
|
||||
v := cgo.Handle(h).Value().(chan int)
|
||||
v <- val
|
||||
cgo.Handle(h).Delete()
|
||||
}
|
26
clipboard_nocgo.go
Normal file
26
clipboard_nocgo.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
//go:build !windows && !cgo
|
||||
// +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
341
clipboard_test.go
Normal 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
|
||||
}()
|
||||
clipboard.Read(clipboard.FmtImage)
|
||||
}()
|
||||
<-done
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestClipboardWriteEmpty(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")
|
||||
}
|
||||
}
|
||||
|
||||
chg1 := clipboard.Write(clipboard.FmtText, nil)
|
||||
if got := clipboard.Read(clipboard.FmtText); got != nil {
|
||||
t.Fatalf("write nil to clipboard should read nil, got: %v", string(got))
|
||||
}
|
||||
clipboard.Write(clipboard.FmtText, []byte(""))
|
||||
<-chg1
|
||||
|
||||
if got := clipboard.Read(clipboard.FmtText); string(got) != "" {
|
||||
t.Fatalf("write empty string to clipboard should read empty string, got: `%v`", string(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClipboardWatch(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")
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
|
||||
defer cancel()
|
||||
|
||||
// clear clipboard
|
||||
clipboard.Write(clipboard.FmtText, []byte(""))
|
||||
lastRead := clipboard.Read(clipboard.FmtText)
|
||||
|
||||
changed := clipboard.Watch(ctx, clipboard.FmtText)
|
||||
|
||||
want := []byte("golang.design/x/clipboard")
|
||||
go func(ctx context.Context) {
|
||||
t := time.NewTicker(time.Millisecond * 500)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
clipboard.Write(clipboard.FmtText, want)
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if string(lastRead) == "" {
|
||||
t.Fatalf("clipboard watch never receives a notification")
|
||||
}
|
||||
t.Log(string(lastRead))
|
||||
return
|
||||
case data, ok := <-changed:
|
||||
if !ok {
|
||||
if string(lastRead) == "" {
|
||||
t.Fatalf("clipboard watch never receives a notification")
|
||||
}
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(data, want) {
|
||||
t.Fatalf("received data from watch mismatch, want: %v, got %v", string(want), string(data))
|
||||
}
|
||||
lastRead = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkClipboard(b *testing.B) {
|
||||
b.Run("text", func(b *testing.B) {
|
||||
data := []byte("golang.design/x/clipboard")
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
clipboard.Write(clipboard.FmtText, data)
|
||||
_ = clipboard.Read(clipboard.FmtText)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestClipboardNoCgo(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 should always be tested")
|
||||
}
|
||||
|
||||
t.Run("Read", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
return
|
||||
}
|
||||
t.Fatalf("expect to fail when CGO_ENABLED=0")
|
||||
}()
|
||||
|
||||
clipboard.Read(clipboard.FmtText)
|
||||
})
|
||||
|
||||
t.Run("Write", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
return
|
||||
}
|
||||
t.Fatalf("expect to fail when CGO_ENABLED=0")
|
||||
}()
|
||||
|
||||
clipboard.Write(clipboard.FmtText, []byte("dummy"))
|
||||
})
|
||||
|
||||
t.Run("Watch", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
return
|
||||
}
|
||||
t.Fatalf("expect to fail when CGO_ENABLED=0")
|
||||
}()
|
||||
|
||||
clipboard.Watch(context.TODO(), clipboard.FmtText)
|
||||
})
|
||||
}
|
552
clipboard_windows.go
Normal file
552
clipboard_windows.go
Normal file
|
@ -0,0 +1,552 @@
|
|||
// 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 windows
|
||||
// +build windows
|
||||
|
||||
package clipboard
|
||||
|
||||
// Interacting with Clipboard on Windows:
|
||||
// https://docs.microsoft.com/zh-cn/windows/win32/dataxchg/using-the-clipboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/image/bmp"
|
||||
)
|
||||
|
||||
func initialize() error { return nil }
|
||||
|
||||
// readText reads the clipboard and returns the text data if presents.
|
||||
// The caller is responsible for opening/closing the clipboard before
|
||||
// calling this function.
|
||||
func readText() (buf []byte, err error) {
|
||||
hMem, _, err := getClipboardData.Call(cFmtUnicodeText)
|
||||
if hMem == 0 {
|
||||
return nil, err
|
||||
}
|
||||
p, _, err := gLock.Call(hMem)
|
||||
if p == 0 {
|
||||
return nil, err
|
||||
}
|
||||
defer gUnlock.Call(hMem)
|
||||
|
||||
// Find NUL terminator
|
||||
n := 0
|
||||
for ptr := unsafe.Pointer(p); *(*uint16)(ptr) != 0; n++ {
|
||||
ptr = unsafe.Pointer(uintptr(ptr) +
|
||||
unsafe.Sizeof(*((*uint16)(unsafe.Pointer(p)))))
|
||||
}
|
||||
|
||||
var s []uint16
|
||||
h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
|
||||
h.Data = p
|
||||
h.Len = n
|
||||
h.Cap = n
|
||||
return []byte(string(utf16.Decode(s))), nil
|
||||
}
|
||||
|
||||
// writeText writes given data to the clipboard. It is the caller's
|
||||
// responsibility for opening/closing the clipboard before calling
|
||||
// this function.
|
||||
func writeText(buf []byte) error {
|
||||
r, _, err := emptyClipboard.Call()
|
||||
if r == 0 {
|
||||
return fmt.Errorf("failed to clear clipboard: %w", err)
|
||||
}
|
||||
|
||||
// empty text, we are done here.
|
||||
if len(buf) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
s, err := syscall.UTF16FromString(string(buf))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert given string: %w", err)
|
||||
}
|
||||
|
||||
hMem, _, err := gAlloc.Call(gmemMoveable, uintptr(len(s)*int(unsafe.Sizeof(s[0]))))
|
||||
if hMem == 0 {
|
||||
return fmt.Errorf("failed to alloc global memory: %w", err)
|
||||
}
|
||||
|
||||
p, _, err := gLock.Call(hMem)
|
||||
if p == 0 {
|
||||
return fmt.Errorf("failed to lock global memory: %w", err)
|
||||
}
|
||||
defer gUnlock.Call(hMem)
|
||||
|
||||
// no return value
|
||||
memMove.Call(p, uintptr(unsafe.Pointer(&s[0])),
|
||||
uintptr(len(s)*int(unsafe.Sizeof(s[0]))))
|
||||
|
||||
v, _, err := setClipboardData.Call(cFmtUnicodeText, hMem)
|
||||
if v == 0 {
|
||||
gFree.Call(hMem)
|
||||
return fmt.Errorf("failed to set text to clipboard: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readImage reads the clipboard and returns PNG encoded image data
|
||||
// if presents. The caller is responsible for opening/closing the
|
||||
// clipboard before calling this function.
|
||||
func readImage() ([]byte, error) {
|
||||
hMem, _, err := getClipboardData.Call(cFmtDIBV5)
|
||||
if hMem == 0 {
|
||||
// second chance to try FmtDIB
|
||||
return readImageDib()
|
||||
}
|
||||
p, _, err := gLock.Call(hMem)
|
||||
if p == 0 {
|
||||
return nil, err
|
||||
}
|
||||
defer gUnlock.Call(hMem)
|
||||
|
||||
// inspect header information
|
||||
info := (*bitmapV5Header)(unsafe.Pointer(p))
|
||||
|
||||
// maybe deal with other formats?
|
||||
if info.BitCount != 32 {
|
||||
return nil, errUnsupported
|
||||
}
|
||||
|
||||
var data []byte
|
||||
sh := (*reflect.SliceHeader)(unsafe.Pointer(&data))
|
||||
sh.Data = uintptr(p)
|
||||
sh.Cap = int(info.Size + 4*uint32(info.Width)*uint32(info.Height))
|
||||
sh.Len = int(info.Size + 4*uint32(info.Width)*uint32(info.Height))
|
||||
img := image.NewRGBA(image.Rect(0, 0, int(info.Width), int(info.Height)))
|
||||
offset := int(info.Size)
|
||||
stride := int(info.Width)
|
||||
for y := 0; y < int(info.Height); y++ {
|
||||
for x := 0; x < int(info.Width); x++ {
|
||||
idx := offset + 4*(y*stride+x)
|
||||
xhat := (x + int(info.Width)) % int(info.Width)
|
||||
yhat := int(info.Height) - 1 - y
|
||||
r := data[idx+2]
|
||||
g := data[idx+1]
|
||||
b := data[idx+0]
|
||||
a := data[idx+3]
|
||||
img.SetRGBA(xhat, yhat, color.RGBA{r, g, b, a})
|
||||
}
|
||||
}
|
||||
// always use PNG encoding.
|
||||
var buf bytes.Buffer
|
||||
png.Encode(&buf, img)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func readImageDib() ([]byte, error) {
|
||||
const (
|
||||
fileHeaderLen = 14
|
||||
infoHeaderLen = 40
|
||||
cFmtDIB = 8
|
||||
)
|
||||
|
||||
hClipDat, _, err := getClipboardData.Call(cFmtDIB)
|
||||
if err != nil {
|
||||
return nil, errors.New("not dib format data: " + err.Error())
|
||||
}
|
||||
pMemBlk, _, err := gLock.Call(hClipDat)
|
||||
if pMemBlk == 0 {
|
||||
return nil, errors.New("failed to call global lock: " + err.Error())
|
||||
}
|
||||
defer gUnlock.Call(hClipDat)
|
||||
|
||||
bmpHeader := (*bitmapHeader)(unsafe.Pointer(pMemBlk))
|
||||
dataSize := bmpHeader.SizeImage + fileHeaderLen + infoHeaderLen
|
||||
|
||||
if bmpHeader.SizeImage == 0 && bmpHeader.Compression == 0 {
|
||||
iSizeImage := bmpHeader.Height * ((bmpHeader.Width*uint32(bmpHeader.BitCount)/8 + 3) &^ 3)
|
||||
dataSize += iSizeImage
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
binary.Write(buf, binary.LittleEndian, uint16('B')|(uint16('M')<<8))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(dataSize))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(0))
|
||||
const sizeof_colorbar = 0
|
||||
binary.Write(buf, binary.LittleEndian, uint32(fileHeaderLen+infoHeaderLen+sizeof_colorbar))
|
||||
j := 0
|
||||
for i := fileHeaderLen; i < int(dataSize); i++ {
|
||||
binary.Write(buf, binary.BigEndian, *(*byte)(unsafe.Pointer(pMemBlk + uintptr(j))))
|
||||
j++
|
||||
}
|
||||
return bmpToPng(buf)
|
||||
}
|
||||
|
||||
func bmpToPng(bmpBuf *bytes.Buffer) (buf []byte, err error) {
|
||||
var f bytes.Buffer
|
||||
original_image, err := bmp.Decode(bmpBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = png.Encode(&f, original_image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.Bytes(), nil
|
||||
}
|
||||
|
||||
func writeImage(buf []byte) error {
|
||||
r, _, err := emptyClipboard.Call()
|
||||
if r == 0 {
|
||||
return fmt.Errorf("failed to clear clipboard: %w", err)
|
||||
}
|
||||
|
||||
// empty text, we are done here.
|
||||
if len(buf) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
img, err := png.Decode(bytes.NewReader(buf))
|
||||
if err != nil {
|
||||
return fmt.Errorf("input bytes is not PNG encoded: %w", err)
|
||||
}
|
||||
|
||||
offset := unsafe.Sizeof(bitmapV5Header{})
|
||||
width := img.Bounds().Dx()
|
||||
height := img.Bounds().Dy()
|
||||
imageSize := 4 * width * height
|
||||
|
||||
data := make([]byte, int(offset)+imageSize)
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
idx := int(offset) + 4*(y*width+x)
|
||||
r, g, b, a := img.At(x, height-1-y).RGBA()
|
||||
data[idx+2] = uint8(r)
|
||||
data[idx+1] = uint8(g)
|
||||
data[idx+0] = uint8(b)
|
||||
data[idx+3] = uint8(a)
|
||||
}
|
||||
}
|
||||
|
||||
info := bitmapV5Header{}
|
||||
info.Size = uint32(offset)
|
||||
info.Width = int32(width)
|
||||
info.Height = int32(height)
|
||||
info.Planes = 1
|
||||
info.Compression = 0 // BI_RGB
|
||||
info.SizeImage = uint32(4 * info.Width * info.Height)
|
||||
info.RedMask = 0xff0000 // default mask
|
||||
info.GreenMask = 0xff00
|
||||
info.BlueMask = 0xff
|
||||
info.AlphaMask = 0xff000000
|
||||
info.BitCount = 32 // we only deal with 32 bpp at the moment.
|
||||
// Use calibrated RGB values as Go's image/png assumes linear color space.
|
||||
// Other options:
|
||||
// - LCS_CALIBRATED_RGB = 0x00000000
|
||||
// - LCS_sRGB = 0x73524742
|
||||
// - LCS_WINDOWS_COLOR_SPACE = 0x57696E20
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/eb4bbd50-b3ce-4917-895c-be31f214797f
|
||||
info.CSType = 0x73524742
|
||||
// Use GL_IMAGES for GamutMappingIntent
|
||||
// Other options:
|
||||
// - LCS_GM_ABS_COLORIMETRIC = 0x00000008
|
||||
// - LCS_GM_BUSINESS = 0x00000001
|
||||
// - LCS_GM_GRAPHICS = 0x00000002
|
||||
// - LCS_GM_IMAGES = 0x00000004
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/9fec0834-607d-427d-abd5-ab240fb0db38
|
||||
info.Intent = 4 // LCS_GM_IMAGES
|
||||
|
||||
infob := make([]byte, int(unsafe.Sizeof(info)))
|
||||
for i, v := range *(*[unsafe.Sizeof(info)]byte)(unsafe.Pointer(&info)) {
|
||||
infob[i] = v
|
||||
}
|
||||
copy(data[:], infob[:])
|
||||
|
||||
hMem, _, err := gAlloc.Call(gmemMoveable,
|
||||
uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
|
||||
if hMem == 0 {
|
||||
return fmt.Errorf("failed to alloc global memory: %w", err)
|
||||
}
|
||||
|
||||
p, _, err := gLock.Call(hMem)
|
||||
if p == 0 {
|
||||
return fmt.Errorf("failed to lock global memory: %w", err)
|
||||
}
|
||||
defer gUnlock.Call(hMem)
|
||||
|
||||
memMove.Call(p, uintptr(unsafe.Pointer(&data[0])),
|
||||
uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
|
||||
|
||||
v, _, err := setClipboardData.Call(cFmtDIBV5, hMem)
|
||||
if v == 0 {
|
||||
gFree.Call(hMem)
|
||||
return fmt.Errorf("failed to set text to clipboard: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func read(t Format) (buf []byte, err error) {
|
||||
// On Windows, OpenClipboard and CloseClipboard must be executed on
|
||||
// the same thread. Thus, lock the OS thread for further execution.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
var format uintptr
|
||||
switch t {
|
||||
case FmtImage:
|
||||
format = cFmtDIBV5
|
||||
case FmtText:
|
||||
fallthrough
|
||||
default:
|
||||
format = cFmtUnicodeText
|
||||
}
|
||||
|
||||
// check if clipboard is avaliable for the requested format
|
||||
r, _, err := isClipboardFormatAvailable.Call(format)
|
||||
if r == 0 {
|
||||
return nil, errUnavailable
|
||||
}
|
||||
|
||||
// try again until open clipboard successed
|
||||
for {
|
||||
r, _, _ = openClipboard.Call()
|
||||
if r == 0 {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
defer closeClipboard.Call()
|
||||
|
||||
switch format {
|
||||
case cFmtDIBV5:
|
||||
return readImage()
|
||||
case cFmtUnicodeText:
|
||||
fallthrough
|
||||
default:
|
||||
return readText()
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
errch := make(chan error)
|
||||
changed := make(chan struct{}, 1)
|
||||
go func() {
|
||||
// make sure GetClipboardSequenceNumber happens with
|
||||
// OpenClipboard on the same thread.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
for {
|
||||
r, _, _ := openClipboard.Call(0)
|
||||
if r == 0 {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// var param uintptr
|
||||
switch t {
|
||||
case FmtImage:
|
||||
err := writeImage(buf)
|
||||
if err != nil {
|
||||
errch <- err
|
||||
closeClipboard.Call()
|
||||
return
|
||||
}
|
||||
case FmtText:
|
||||
fallthrough
|
||||
default:
|
||||
// param = cFmtUnicodeText
|
||||
err := writeText(buf)
|
||||
if err != nil {
|
||||
errch <- err
|
||||
closeClipboard.Call()
|
||||
return
|
||||
}
|
||||
}
|
||||
// Close the clipboard otherwise other applications cannot
|
||||
// paste the data.
|
||||
closeClipboard.Call()
|
||||
|
||||
cnt, _, _ := getClipboardSequenceNumber.Call()
|
||||
errch <- nil
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
cur, _, _ := getClipboardSequenceNumber.Call()
|
||||
if cur != cnt {
|
||||
changed <- struct{}{}
|
||||
close(changed)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
err := <-errch
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
func watch(ctx context.Context, t Format) <-chan []byte {
|
||||
recv := make(chan []byte, 1)
|
||||
ready := make(chan struct{})
|
||||
go func() {
|
||||
// not sure if we are too slow or the user too fast :)
|
||||
ti := time.NewTicker(time.Second)
|
||||
cnt, _, _ := getClipboardSequenceNumber.Call()
|
||||
ready <- struct{}{}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
close(recv)
|
||||
return
|
||||
case <-ti.C:
|
||||
cur, _, _ := getClipboardSequenceNumber.Call()
|
||||
if cnt != cur {
|
||||
b := Read(t)
|
||||
if b == nil {
|
||||
continue
|
||||
}
|
||||
recv <- b
|
||||
cnt = cur
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
<-ready
|
||||
return recv
|
||||
}
|
||||
|
||||
const (
|
||||
cFmtBitmap = 2 // Win+PrintScreen
|
||||
cFmtUnicodeText = 13
|
||||
cFmtDIBV5 = 17
|
||||
// Screenshot taken from special shortcut is in different format (why??), see:
|
||||
// https://jpsoft.com/forums/threads/detecting-clipboard-format.5225/
|
||||
cFmtDataObject = 49161 // Shift+Win+s, returned from enumClipboardFormats
|
||||
gmemMoveable = 0x0002
|
||||
)
|
||||
|
||||
// BITMAPV5Header structure, see:
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
|
||||
type bitmapV5Header struct {
|
||||
Size uint32
|
||||
Width int32
|
||||
Height int32
|
||||
Planes uint16
|
||||
BitCount uint16
|
||||
Compression uint32
|
||||
SizeImage uint32
|
||||
XPelsPerMeter int32
|
||||
YPelsPerMeter int32
|
||||
ClrUsed uint32
|
||||
ClrImportant uint32
|
||||
RedMask uint32
|
||||
GreenMask uint32
|
||||
BlueMask uint32
|
||||
AlphaMask uint32
|
||||
CSType uint32
|
||||
Endpoints struct {
|
||||
CiexyzRed, CiexyzGreen, CiexyzBlue struct {
|
||||
CiexyzX, CiexyzY, CiexyzZ int32 // FXPT2DOT30
|
||||
}
|
||||
}
|
||||
GammaRed uint32
|
||||
GammaGreen uint32
|
||||
GammaBlue uint32
|
||||
Intent uint32
|
||||
ProfileData uint32
|
||||
ProfileSize uint32
|
||||
Reserved uint32
|
||||
}
|
||||
|
||||
type bitmapHeader struct {
|
||||
Size uint32
|
||||
Width uint32
|
||||
Height uint32
|
||||
PLanes uint16
|
||||
BitCount uint16
|
||||
Compression uint32
|
||||
SizeImage uint32
|
||||
XPelsPerMeter uint32
|
||||
YPelsPerMeter uint32
|
||||
ClrUsed uint32
|
||||
ClrImportant uint32
|
||||
}
|
||||
|
||||
// Calling a Windows DLL, see:
|
||||
// https://github.com/golang/go/wiki/WindowsDLLs
|
||||
var (
|
||||
user32 = syscall.MustLoadDLL("user32")
|
||||
// Opens the clipboard for examination and prevents other
|
||||
// applications from modifying the clipboard content.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-openclipboard
|
||||
openClipboard = user32.MustFindProc("OpenClipboard")
|
||||
// Closes the clipboard.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-closeclipboard
|
||||
closeClipboard = user32.MustFindProc("CloseClipboard")
|
||||
// Empties the clipboard and frees handles to data in the clipboard.
|
||||
// The function then assigns ownership of the clipboard to the
|
||||
// window that currently has the clipboard open.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-emptyclipboard
|
||||
emptyClipboard = user32.MustFindProc("EmptyClipboard")
|
||||
// Retrieves data from the clipboard in a specified format.
|
||||
// The clipboard must have been opened previously.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboarddata
|
||||
getClipboardData = user32.MustFindProc("GetClipboardData")
|
||||
// Places data on the clipboard in a specified clipboard format.
|
||||
// The window must be the current clipboard owner, and the
|
||||
// application must have called the OpenClipboard function. (When
|
||||
// responding to the WM_RENDERFORMAT message, the clipboard owner
|
||||
// must not call OpenClipboard before calling SetClipboardData.)
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata
|
||||
setClipboardData = user32.MustFindProc("SetClipboardData")
|
||||
// Determines whether the clipboard contains data in the specified format.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-isclipboardformatavailable
|
||||
isClipboardFormatAvailable = user32.MustFindProc("IsClipboardFormatAvailable")
|
||||
// Clipboard data formats are stored in an ordered list. To perform
|
||||
// an enumeration of clipboard data formats, you make a series of
|
||||
// calls to the EnumClipboardFormats function. For each call, the
|
||||
// format parameter specifies an available clipboard format, and the
|
||||
// function returns the next available clipboard format.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-isclipboardformatavailable
|
||||
enumClipboardFormats = user32.MustFindProc("EnumClipboardFormats")
|
||||
// Retrieves the clipboard sequence number for the current window station.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboardsequencenumber
|
||||
getClipboardSequenceNumber = user32.MustFindProc("GetClipboardSequenceNumber")
|
||||
// Registers a new clipboard format. This format can then be used as
|
||||
// a valid clipboard format.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformata
|
||||
registerClipboardFormatA = user32.MustFindProc("RegisterClipboardFormatA")
|
||||
|
||||
kernel32 = syscall.NewLazyDLL("kernel32")
|
||||
|
||||
// Locks a global memory object and returns a pointer to the first
|
||||
// byte of the object's memory block.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globallock
|
||||
gLock = kernel32.NewProc("GlobalLock")
|
||||
// Decrements the lock count associated with a memory object that was
|
||||
// allocated with GMEM_MOVEABLE. This function has no effect on memory
|
||||
// objects allocated with GMEM_FIXED.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalunlock
|
||||
gUnlock = kernel32.NewProc("GlobalUnlock")
|
||||
// Allocates the specified number of bytes from the heap.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalalloc
|
||||
gAlloc = kernel32.NewProc("GlobalAlloc")
|
||||
// Frees the specified global memory object and invalidates its handle.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalfree
|
||||
gFree = kernel32.NewProc("GlobalFree")
|
||||
memMove = kernel32.NewProc("RtlMoveMemory")
|
||||
)
|
33
cmd/gclip-gui/AndroidManifest.xml
Normal file
33
cmd/gclip-gui/AndroidManifest.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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>
|
||||
-->
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="design.golang.clipboard.gclip"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
|
||||
<!-- In order to access the clipboard, the application manifest must
|
||||
specify the permission requirement. See the following page for
|
||||
details.
|
||||
http://developer.android.com/guide/topics/manifest/manifest-intro.html#perms -->
|
||||
<uses-permission android:name="android.permission.CLIPBOARD" />
|
||||
|
||||
<application android:label="gclip" android:debuggable="true">
|
||||
<activity android:name="org.golang.app.GoNativeActivity"
|
||||
android:label="Gclip"
|
||||
android:configChanges="orientation|keyboardHidden">
|
||||
<meta-data android:name="android.app.lib_name" android:value="Gclip" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
31
cmd/gclip-gui/README.md
Normal file
31
cmd/gclip-gui/README.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
# gclip-gui
|
||||
|
||||
This is a very basic example for verification purpose that demonstrates
|
||||
how the [golang.design/x/clipboard](https://golang.design/x/clipboard)
|
||||
can interact with macOS/Linux/Windows/Android/iOS system clipboard.
|
||||
|
||||
The gclip GUI application writes a string to the system clipboard
|
||||
periodically then reads it back and renders it if possible.
|
||||
|
||||
Because of the system limitation, on mobile devices, only string data is
|
||||
supported at the moment. Hence, one must use clipboard.FmtText. Other supplied
|
||||
formats result in a panic.
|
||||
|
||||
This example is intentded as cross platform application. To build it, one
|
||||
must use [gomobile](https://golang.org/x/mobile). You may follow the instructions
|
||||
provided in the [GoMobile wiki](https://github.com/golang/go/wiki/Mobile) page.
|
||||
|
||||
|
||||
- For desktop: `go build -o gclip-gui`
|
||||
- For Android: `gomobile build -v -target=android -o gclip-gui.apk`
|
||||
- For iOS: `gomobile build -v -target=ios -bundleid design.golang.gclip-gui.app`
|
||||
|
||||
## Screenshots
|
||||
|
||||
| macOS | iOS | Windows | Android | Linux |
|
||||
|:-----:|:---:|:-------:|:-------:|:-----:|
|
||||
||||||
|
||||
|
||||
## License
|
||||
|
||||
MIT | © 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de).
|
239
cmd/gclip-gui/main.go
Normal file
239
cmd/gclip-gui/main.go
Normal file
|
@ -0,0 +1,239 @@
|
|||
// 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 || ios || linux || darwin || windows
|
||||
// +build android ios linux darwin windows
|
||||
|
||||
// This is a very basic example for verification purpose that
|
||||
// demonstrates how the golang.design/x/clipboard can interact
|
||||
// with macOS/Linux/Windows/Android/iOS system clipboard.
|
||||
//
|
||||
// The gclip GUI application writes a string to the system clipboard
|
||||
// periodically then reads it back and renders it if possible.
|
||||
//
|
||||
// Because of the system limitation, on mobile devices, only string
|
||||
// data is supported at the moment. Hence, one must use clipboard.FmtText.
|
||||
// Other supplied formats result in a panic.
|
||||
//
|
||||
// This example is intentded as cross platform application.
|
||||
// To build it, one must use gomobile (https://golang.org/x/mobile).
|
||||
// You may follow the instructions provided in the GoMobile's wiki page:
|
||||
// https://github.com/golang/go/wiki/Mobile.
|
||||
//
|
||||
// - For desktop:
|
||||
//
|
||||
// go build -o gclip-gui
|
||||
//
|
||||
// - For Android:
|
||||
//
|
||||
// gomobile build -v -target=android -o gclip-gui.apk
|
||||
//
|
||||
// - For iOS:
|
||||
//
|
||||
// gomobile build -v -target=ios -bundleid design.golang.gclip-gui.app
|
||||
//
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.design/x/clipboard"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
"golang.org/x/image/math/fixed"
|
||||
"golang.org/x/mobile/app"
|
||||
"golang.org/x/mobile/event/lifecycle"
|
||||
"golang.org/x/mobile/event/paint"
|
||||
"golang.org/x/mobile/event/size"
|
||||
"golang.org/x/mobile/exp/gl/glutil"
|
||||
"golang.org/x/mobile/geom"
|
||||
"golang.org/x/mobile/gl"
|
||||
)
|
||||
|
||||
type Label struct {
|
||||
sz size.Event
|
||||
images *glutil.Images
|
||||
m *glutil.Image
|
||||
drawer *font.Drawer
|
||||
|
||||
mu sync.Mutex
|
||||
data string
|
||||
}
|
||||
|
||||
func NewLabel(images *glutil.Images) *Label {
|
||||
return &Label{
|
||||
images: images,
|
||||
data: "Hello! Gclip.",
|
||||
drawer: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Label) SetLabel(s string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.data = s
|
||||
}
|
||||
|
||||
const (
|
||||
fontWidth = 5
|
||||
fontHeight = 7
|
||||
lineWidth = 100
|
||||
lineHeight = 120
|
||||
)
|
||||
|
||||
func (l *Label) Draw(sz size.Event) {
|
||||
l.mu.Lock()
|
||||
s := l.data
|
||||
l.mu.Unlock()
|
||||
imgW, imgH := lineWidth*basicfont.Face7x13.Width, lineHeight*basicfont.Face7x13.Height
|
||||
if sz.WidthPx == 0 && sz.HeightPx == 0 {
|
||||
return
|
||||
}
|
||||
if imgW > sz.WidthPx {
|
||||
imgW = sz.WidthPx
|
||||
}
|
||||
|
||||
if l.sz != sz {
|
||||
l.sz = sz
|
||||
if l.m != nil {
|
||||
l.m.Release()
|
||||
}
|
||||
l.m = l.images.NewImage(imgW, imgH)
|
||||
}
|
||||
// Clear the drawing image.
|
||||
for i := 0; i < len(l.m.RGBA.Pix); i++ {
|
||||
l.m.RGBA.Pix[i] = 0
|
||||
}
|
||||
|
||||
l.drawer = &font.Drawer{
|
||||
Dst: l.m.RGBA,
|
||||
Src: image.NewUniform(color.RGBA{0, 100, 125, 255}),
|
||||
Face: basicfont.Face7x13,
|
||||
Dot: fixed.P(5, 10),
|
||||
}
|
||||
l.drawer.DrawString(s)
|
||||
l.m.Upload()
|
||||
l.m.Draw(
|
||||
sz,
|
||||
geom.Point{X: 0, Y: 50},
|
||||
geom.Point{X: geom.Pt(imgW), Y: 50},
|
||||
geom.Point{X: 0, Y: geom.Pt(imgH)},
|
||||
l.m.RGBA.Bounds(),
|
||||
)
|
||||
}
|
||||
|
||||
func (l *Label) Release() {
|
||||
if l.m != nil {
|
||||
l.m.Release()
|
||||
l.m = nil
|
||||
l.images = nil
|
||||
}
|
||||
}
|
||||
|
||||
// GclipApp is the application instance.
|
||||
type GclipApp struct {
|
||||
app app.App
|
||||
|
||||
ctx gl.Context
|
||||
siz size.Event
|
||||
|
||||
images *glutil.Images
|
||||
l *Label
|
||||
|
||||
counter int
|
||||
}
|
||||
|
||||
// WatchClipboard watches the system clipboard every seconds.
|
||||
func (g *GclipApp) WatchClipboard() {
|
||||
go func() {
|
||||
tk := time.NewTicker(time.Second)
|
||||
for range tk.C {
|
||||
// Write something to the clipboard
|
||||
w := fmt.Sprintf("(gclip: %d)", g.counter)
|
||||
clipboard.Write(clipboard.FmtText, []byte(w))
|
||||
g.counter++
|
||||
log.Println(w)
|
||||
|
||||
// Read it back and render it, if possible.
|
||||
data := clipboard.Read(clipboard.FmtText)
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Set the current clipboard data as label content and render on the screen.
|
||||
r := fmt.Sprintf("clipboard: %s", string(data))
|
||||
g.l.SetLabel(r)
|
||||
g.app.Send(paint.Event{})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (g *GclipApp) OnStart(e lifecycle.Event) {
|
||||
g.ctx, _ = e.DrawContext.(gl.Context)
|
||||
g.images = glutil.NewImages(g.ctx)
|
||||
g.l = NewLabel(g.images)
|
||||
g.app.Send(paint.Event{})
|
||||
}
|
||||
|
||||
func (g *GclipApp) OnStop() {
|
||||
g.l.Release()
|
||||
g.images.Release()
|
||||
g.ctx = nil
|
||||
}
|
||||
|
||||
func (g *GclipApp) OnSize(size size.Event) {
|
||||
g.siz = size
|
||||
}
|
||||
|
||||
func (g *GclipApp) OnDraw() {
|
||||
if g.ctx == nil {
|
||||
return
|
||||
}
|
||||
defer g.app.Send(paint.Event{})
|
||||
defer g.app.Publish()
|
||||
g.ctx.ClearColor(0, 0, 0, 1)
|
||||
g.ctx.Clear(gl.COLOR_BUFFER_BIT)
|
||||
g.l.Draw(g.siz)
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := clipboard.Init()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
app.Main(func(a app.App) {
|
||||
gclip := GclipApp{app: a}
|
||||
gclip.app.Send(size.Event{WidthPx: 800, HeightPx: 500})
|
||||
gclip.WatchClipboard()
|
||||
for e := range gclip.app.Events() {
|
||||
switch e := gclip.app.Filter(e).(type) {
|
||||
case lifecycle.Event:
|
||||
switch e.Crosses(lifecycle.StageVisible) {
|
||||
case lifecycle.CrossOn:
|
||||
gclip.OnStart(e)
|
||||
case lifecycle.CrossOff:
|
||||
gclip.OnStop()
|
||||
os.Exit(0)
|
||||
}
|
||||
case size.Event:
|
||||
gclip.OnSize(e)
|
||||
case paint.Event:
|
||||
gclip.OnDraw()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
40
cmd/gclip/README.md
Normal file
40
cmd/gclip/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
# gclip
|
||||
|
||||
`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 &
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT | © 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de).
|
131
cmd/gclip/main.go
Normal file
131
cmd/gclip/main.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
// 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 main // go install golang.design/x/clipboard/cmd/gclip@latest
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, `gclip is a command that provides clipboard interaction.
|
||||
|
||||
usage: gclip [-copy|-paste] [-f <file>]
|
||||
|
||||
options:
|
||||
`)
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
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
|
||||
`)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
var (
|
||||
in = flag.Bool("copy", false, "copy data to clipboard")
|
||||
out = flag.Bool("paste", false, "paste data from clipboard")
|
||||
file = flag.String("f", "", "source or destination to a given file path")
|
||||
)
|
||||
|
||||
func init() {
|
||||
err := clipboard.Init()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if *out {
|
||||
if err := pst(); err != nil {
|
||||
usage()
|
||||
}
|
||||
return
|
||||
}
|
||||
if *in {
|
||||
if err := cpy(); err != nil {
|
||||
usage()
|
||||
}
|
||||
return
|
||||
}
|
||||
usage()
|
||||
}
|
||||
|
||||
func cpy() error {
|
||||
t := clipboard.FmtText
|
||||
ext := filepath.Ext(*file)
|
||||
|
||||
switch ext {
|
||||
case ".png":
|
||||
t = clipboard.FmtImage
|
||||
case ".txt":
|
||||
fallthrough
|
||||
default:
|
||||
t = clipboard.FmtText
|
||||
}
|
||||
|
||||
var (
|
||||
b []byte
|
||||
err error
|
||||
)
|
||||
if *file != "" {
|
||||
b, err = os.ReadFile(*file)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to read given file: %v", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
b, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to read from stdin: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until clipboard content has been changed.
|
||||
<-clipboard.Write(t, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func pst() (err error) {
|
||||
var b []byte
|
||||
|
||||
b = clipboard.Read(clipboard.FmtText)
|
||||
if b == nil {
|
||||
b = clipboard.Read(clipboard.FmtImage)
|
||||
}
|
||||
|
||||
if *file != "" && b != nil {
|
||||
err = os.WriteFile(*file, b, os.ModePerm)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to write data to file %s: %v", *file, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for len(b) > 0 {
|
||||
n, err := os.Stdout.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b = b[n:]
|
||||
}
|
||||
return nil
|
||||
}
|
57
example_test.go
Normal file
57
example_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
// 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 cgo
|
||||
// +build cgo
|
||||
|
||||
package clipboard_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
func ExampleWrite() {
|
||||
err := clipboard.Init()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
clipboard.Write(clipboard.FmtText, []byte("Hello, 世界"))
|
||||
// Output:
|
||||
}
|
||||
|
||||
func ExampleRead() {
|
||||
err := clipboard.Init()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(string(clipboard.Read(clipboard.FmtText)))
|
||||
// Output:
|
||||
// Hello, 世界
|
||||
}
|
||||
|
||||
func ExampleWatch() {
|
||||
err := clipboard.Init()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
|
||||
defer cancel()
|
||||
|
||||
changed := clipboard.Watch(context.Background(), clipboard.FmtText)
|
||||
go func(ctx context.Context) {
|
||||
clipboard.Write(clipboard.FmtText, []byte("你好,world"))
|
||||
}(ctx)
|
||||
fmt.Println(string(<-changed))
|
||||
// Output:
|
||||
// 你好,world
|
||||
}
|
13
export_test.go
Normal file
13
export_test.go
Normal 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>
|
||||
|
||||
package clipboard
|
||||
|
||||
// for debugging errors
|
||||
var (
|
||||
Debug = debug
|
||||
ErrUnavailable = errUnavailable
|
||||
)
|
13
go.mod
Normal file
13
go.mod
Normal file
|
@ -0,0 +1,13 @@
|
|||
module golang.design/x/clipboard
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
golang.org/x/image v0.6.0
|
||||
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
)
|
49
go.sum
Normal file
49
go.sum
Normal file
|
@ -0,0 +1,49 @@
|
|||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
|
||||
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4=
|
||||
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
15
tests/Makefile
Normal file
15
tests/Makefile
Normal file
|
@ -0,0 +1,15 @@
|
|||
# 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>
|
||||
|
||||
all: test
|
||||
|
||||
test:
|
||||
go test -v -count=1 -covermode=atomic ..
|
||||
|
||||
test-docker:
|
||||
docker build -t golang-design/x/clipboard ..
|
||||
docker run --rm --name cb golang-design/x/clipboard
|
||||
docker rmi golang-design/x/clipboard
|
11
tests/test-docker.sh
Executable file
11
tests/test-docker.sh
Executable file
|
@ -0,0 +1,11 @@
|
|||
# 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>
|
||||
|
||||
# require apt-get install xvfb
|
||||
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
export DISPLAY=:99.0
|
||||
|
||||
go test -v -covermode=atomic ./...
|
BIN
tests/testdata/android.png
vendored
Normal file
BIN
tests/testdata/android.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
BIN
tests/testdata/clipboard.png
vendored
Normal file
BIN
tests/testdata/clipboard.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
tests/testdata/darwin.png
vendored
Normal file
BIN
tests/testdata/darwin.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 115 KiB |
BIN
tests/testdata/ios.png
vendored
Normal file
BIN
tests/testdata/ios.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
tests/testdata/linux.png
vendored
Normal file
BIN
tests/testdata/linux.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
tests/testdata/windows.png
vendored
Normal file
BIN
tests/testdata/windows.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Loading…
Add table
Add a link
Reference in a new issue