285 lines
7.1 KiB
Go
285 lines
7.1 KiB
Go
//go:build !windows
|
|
|
|
// Copyright 2021 The golang.design Initiative Authors.
|
|
// All rights reserved. Use of this source code is governed
|
|
// by a MIT license that can be found in the LICENSE file.
|
|
//
|
|
// Written by Changkun Ou <changkun.de>
|
|
|
|
/*
|
|
Package clipboard provides cross platform clipboard access and supports
|
|
macOS/Linux/Windows/Android/iOS platform. Before interacting with the
|
|
clipboard, one must call Init to assert if it is possible to use this
|
|
package:
|
|
|
|
err := clipboard.Init()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
The most common operations are `doRead` and `doWrite`. To use them:
|
|
|
|
// write/read text format data of the clipboard, and
|
|
// the byte buffer regarding the text are UTF8 encoded.
|
|
clipboard.doWrite(clipboard.FmtText, []byte("text data"))
|
|
clipboard.doRead(clipboard.FmtText)
|
|
|
|
// write/read image format data of the clipboard, and
|
|
// the byte buffer regarding the image are PNG encoded.
|
|
clipboard.doWrite(clipboard.FmtImage, []byte("image data"))
|
|
clipboard.doRead(clipboard.FmtImage)
|
|
|
|
Note that read/write regarding image format assumes that the bytes are
|
|
PNG encoded since it serves the alpha blending purpose that might be
|
|
used in other graphical software.
|
|
|
|
In addition, `clipboard.doWrite` returns a channel that can receive an
|
|
empty struct as a signal, which indicates the corresponding write call
|
|
to the clipboard is outdated, meaning the clipboard has been overwritten
|
|
by others and the previously written data is lost. For instance:
|
|
|
|
changed := clipboard.doWrite(clipboard.FmtText, []byte("text data"))
|
|
|
|
select {
|
|
case <-changed:
|
|
println(`"text data" is no longer available from clipboard.`)
|
|
}
|
|
|
|
You can ignore the returning channel if you don't need this type of
|
|
notification. Furthermore, when you need more than just knowing whether
|
|
clipboard data is changed, use the watcher API:
|
|
|
|
ch := clipboard.doWatchWatch(context.TODO(), clipboard.FmtText)
|
|
for data := range ch {
|
|
// print out clipboard data whenever it is changed
|
|
println(string(data))
|
|
}
|
|
*/
|
|
package clipboard // import "golang.design/x/clipboard"
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
// activate only for running tests.
|
|
debug = false
|
|
errUnavailable = errors.New("clipboard unavailable")
|
|
errUnsupported = errors.New("unsupported format")
|
|
errNoCgo = errors.New("clipboard: cannot use when CGO_ENABLED=0")
|
|
)
|
|
|
|
var (
|
|
// Due to the limitation on operating systems (such as darwin),
|
|
// concurrent read can even cause panic, use a global lock to
|
|
// guarantee one read at a time.
|
|
lock = sync.Mutex{}
|
|
initOnce sync.Once
|
|
initError error
|
|
)
|
|
|
|
// Init initializes the clipboard package. It returns an error
|
|
// if the clipboard is not available to use. This may happen if the
|
|
// target system lacks required dependency, such as libx11-dev in X11
|
|
// environment. For example,
|
|
//
|
|
// err := clipboard.Init()
|
|
// if err != nil {
|
|
// panic(err)
|
|
// }
|
|
//
|
|
// If Init returns an error, any subsequent doRead/doWrite/doWatchWatch call
|
|
// may result in an unrecoverable panic.
|
|
func Init() error {
|
|
initOnce.Do(func() {
|
|
initError = initialize()
|
|
})
|
|
return initError
|
|
}
|
|
|
|
// doRead returns a chunk of bytes of the clipboard data if it presents
|
|
// in the desired format t presents. Otherwise, it returns nil.
|
|
func doRead(t format) []byte {
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
buf, err := read(t)
|
|
if err != nil {
|
|
if debug {
|
|
fmt.Fprintf(os.Stderr, "read clipboard err: %v\n", err)
|
|
}
|
|
return nil
|
|
}
|
|
return buf
|
|
}
|
|
|
|
// doWrite writes a given buffer to the clipboard in a specified format.
|
|
// doWrite returned a receive-only channel can receive an empty struct
|
|
// as a signal, which indicates the clipboard has been overwritten from
|
|
// this write.
|
|
// If format t indicates an image, then the given buf assumes
|
|
// the image data is PNG encoded.
|
|
func doWrite(t format, buf []byte) <-chan struct{} {
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
changed, err := write(t, buf)
|
|
if err != nil {
|
|
if debug {
|
|
fmt.Fprintf(os.Stderr, "write to clipboard err: %v\n", err)
|
|
}
|
|
return nil
|
|
}
|
|
return changed
|
|
}
|
|
|
|
// doWatch returns a receive-only channel that received the clipboard data
|
|
// whenever any change of clipboard data in the desired format happens.
|
|
//
|
|
// The returned channel will be closed if the given context is canceled.
|
|
func doWatch(ctx context.Context, t format) <-chan []byte {
|
|
return watch(ctx, t)
|
|
}
|
|
|
|
func Get() (Clipboard, error) {
|
|
txt := doRead(0)
|
|
img := doRead(1)
|
|
if txt == nil && img == nil {
|
|
return Clipboard{}, errUnavailable
|
|
}
|
|
res := Clipboard{
|
|
winOriginTypes: nil,
|
|
platform: runtime.GOOS,
|
|
date: time.Now(),
|
|
}
|
|
if txt != nil && img == nil {
|
|
res.primaryType = Text
|
|
res.primaryData = txt
|
|
res.primarySize = len(txt)
|
|
}
|
|
if img != nil && txt == nil {
|
|
res.primaryType = Image
|
|
res.primaryData = img
|
|
res.primarySize = len(img)
|
|
}
|
|
res.primaryType = Text
|
|
res.primaryData = txt
|
|
res.primarySize = len(txt)
|
|
res.secondaryType = Image
|
|
res.secondaryData = img
|
|
res.secondarySize = len(img)
|
|
return res, nil
|
|
}
|
|
|
|
func GetMeta() (Clipboard, error) {
|
|
return Get()
|
|
}
|
|
|
|
func AutoFetcher(uFormat string) (interface{}, error) {
|
|
return nil, fmt.Errorf("This platform does not support AutoFetcher")
|
|
}
|
|
|
|
func SetOrigin(types string, data []byte) error {
|
|
return fmt.Errorf("This platform does not support SetOrigin")
|
|
}
|
|
|
|
func Set(types FileType, data []byte) error {
|
|
ft := 0
|
|
switch types {
|
|
case Text:
|
|
ft = 0
|
|
case File:
|
|
ft = 1
|
|
default:
|
|
return errors.New("not support type:" + string(types))
|
|
}
|
|
res := doWrite(format(ft), data)
|
|
if res == nil {
|
|
return fmt.Errorf("This platform does not support Set for type:" + string(types))
|
|
}
|
|
select {
|
|
case <-res:
|
|
return nil
|
|
case <-time.After(10 * time.Second):
|
|
return fmt.Errorf("Set operation timeout for type:" + string(types))
|
|
}
|
|
}
|
|
|
|
func ClipSize(uFormat string) (int, error) {
|
|
return 0, fmt.Errorf("This platform does not support ClipSize")
|
|
}
|
|
|
|
func AutoSetter(uFormat string, data interface{}) error {
|
|
return fmt.Errorf("This platform does not support AutoSetter")
|
|
}
|
|
|
|
var isListening uint32
|
|
var stopSignal chan struct{}
|
|
|
|
func Listen(onlyMeta bool) (<-chan Clipboard, error) {
|
|
if atomic.LoadUint32(&isListening) != 0 {
|
|
return nil, errors.New("Already listening")
|
|
}
|
|
atomic.StoreUint32(&isListening, 1)
|
|
ctx, stopFn := context.WithCancel(context.Background())
|
|
res := make(chan Clipboard, 10) // 增大缓冲区
|
|
stopSignal = make(chan struct{})
|
|
go func() {
|
|
txt := doWatch(ctx, 0)
|
|
img := doWatch(ctx, 1)
|
|
for {
|
|
select {
|
|
case <-stopSignal:
|
|
stopFn()
|
|
atomic.StoreUint32(&isListening, 0)
|
|
close(res)
|
|
close(stopSignal)
|
|
return
|
|
case t, ok := <-txt:
|
|
if !ok {
|
|
continue
|
|
}
|
|
res <- Clipboard{
|
|
platform: runtime.GOOS,
|
|
date: time.Now(),
|
|
primaryType: Text,
|
|
primaryData: t,
|
|
primarySize: len(t),
|
|
}
|
|
case i, ok := <-img:
|
|
if !ok {
|
|
continue
|
|
}
|
|
res <- Clipboard{
|
|
platform: runtime.GOOS,
|
|
date: time.Now(),
|
|
primaryType: Image,
|
|
primaryData: i,
|
|
primarySize: len(i),
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return res, nil
|
|
}
|
|
|
|
func StopListen() error {
|
|
if atomic.LoadUint32(&isListening) == 0 {
|
|
return nil
|
|
}
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
fmt.Println("StopListen panic:", r)
|
|
}
|
|
}()
|
|
close(stopSignal)
|
|
return nil
|
|
}
|