chore: vendor clipboard into go package

This commit is contained in:
adamdottv 2025-07-08 18:48:40 -05:00
parent da3df51316
commit 39bcba85a9
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
34 changed files with 2883 additions and 0 deletions

View 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']

View 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.24.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
packages/tui/clipboard/.gitignore vendored Normal file
View 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/

View file

@ -0,0 +1,13 @@
# Copyright 2021 The golang.design Initiative Authors.
# All rights reserved. Use of this source code is governed
# by a MIT license that can be found in the LICENSE file.
#
# Written by Changkun Ou <changkun.de>
FROM golang:1.24
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" ]

View file

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

View file

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

View file

@ -0,0 +1,155 @@
// 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")
errNoCgo = errors.New("clipboard: cannot use when CGO_ENABLED=0")
)
// 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)
}

View file

@ -0,0 +1,80 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build android
#include <android/log.h>
#include <jni.h>
#include <stdlib.h>
#include <string.h>
#define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, \
"GOLANG.DESIGN/X/CLIPBOARD", __VA_ARGS__)
static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
jmethodID m = (*env)->GetMethodID(env, clazz, name, sig);
if (m == 0) {
(*env)->ExceptionClear(env);
LOG_FATAL("cannot find method %s %s", name, sig);
return 0;
}
return m;
}
jobject get_clipboard(uintptr_t jni_env, uintptr_t ctx) {
JNIEnv *env = (JNIEnv*)jni_env;
jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx);
jmethodID getSystemService = find_method(env, ctxClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
jstring service = (*env)->NewStringUTF(env, "clipboard");
jobject ret = (jobject)(*env)->CallObjectMethod(env, (jobject)ctx, getSystemService, service);
jthrowable err = (*env)->ExceptionOccurred(env);
if (err != NULL) {
LOG_FATAL("cannot find clipboard");
(*env)->ExceptionClear(env);
return NULL;
}
return ret;
}
char *clipboard_read_string(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) {
JNIEnv *env = (JNIEnv*)jni_env;
jobject mgr = get_clipboard(jni_env, ctx);
if (mgr == NULL) {
return NULL;
}
jclass mgrClass = (*env)->GetObjectClass(env, mgr);
jmethodID getText = find_method(env, mgrClass, "getText", "()Ljava/lang/CharSequence;");
jobject content = (jstring)(*env)->CallObjectMethod(env, mgr, getText);
if (content == NULL) {
return NULL;
}
jclass clzCharSequence = (*env)->GetObjectClass(env, content);
jmethodID toString = (*env)->GetMethodID(env, clzCharSequence, "toString", "()Ljava/lang/String;");
jobject s = (*env)->CallObjectMethod(env, content, toString);
const char *chars = (*env)->GetStringUTFChars(env, s, NULL);
char *copy = strdup(chars);
(*env)->ReleaseStringUTFChars(env, s, chars);
return copy;
}
void clipboard_write_string(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *str) {
JNIEnv *env = (JNIEnv*)jni_env;
jobject mgr = get_clipboard(jni_env, ctx);
if (mgr == NULL) {
return;
}
jclass mgrClass = (*env)->GetObjectClass(env, mgr);
jmethodID setText = find_method(env, mgrClass, "setText", "(Ljava/lang/CharSequence;)V");
(*env)->CallVoidMethod(env, mgr, setText, (*env)->NewStringUTF(env, str));
}

View file

@ -0,0 +1,102 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build android
package clipboard
/*
#cgo LDFLAGS: -landroid -llog
#include <stdlib.h>
char *clipboard_read_string(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx);
void clipboard_write_string(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *str);
*/
import "C"
import (
"bytes"
"context"
"time"
"unsafe"
"golang.org/x/mobile/app"
)
func initialize() error { return nil }
func read(t Format) (buf []byte, err error) {
switch t {
case FmtText:
s := ""
if err := app.RunOnJVM(func(vm, env, ctx uintptr) error {
cs := C.clipboard_read_string(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx))
if cs == nil {
return nil
}
s = C.GoString(cs)
C.free(unsafe.Pointer(cs))
return nil
}); err != nil {
return nil, err
}
return []byte(s), nil
case FmtImage:
return nil, errUnsupported
default:
return nil, errUnsupported
}
}
// write writes the given data to clipboard and
// returns true if success or false if failed.
func write(t Format, buf []byte) (<-chan struct{}, error) {
done := make(chan struct{}, 1)
switch t {
case FmtText:
cs := C.CString(string(buf))
defer C.free(unsafe.Pointer(cs))
if err := app.RunOnJVM(func(vm, env, ctx uintptr) error {
C.clipboard_write_string(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), cs)
done <- struct{}{}
return nil
}); err != nil {
return nil, err
}
return done, nil
case FmtImage:
return nil, errUnsupported
default:
return nil, errUnsupported
}
}
func watch(ctx context.Context, t Format) <-chan []byte {
recv := make(chan []byte, 1)
ti := time.NewTicker(time.Second)
last := Read(t)
go func() {
for {
select {
case <-ctx.Done():
close(recv)
return
case <-ti.C:
b := Read(t)
if b == nil {
continue
}
if bytes.Compare(last, b) != 0 {
recv <- b
last = b
}
}
}
}()
return recv
}

View file

@ -0,0 +1,266 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build darwin && !ios
package clipboard
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"time"
)
var (
lastChangeCount int64
changeCountMu sync.Mutex
)
func initialize() error { return nil }
func read(t Format) (buf []byte, err error) {
switch t {
case FmtText:
return readText()
case FmtImage:
return readImage()
default:
return nil, errUnsupported
}
}
func readText() ([]byte, error) {
// Check if clipboard contains string data
checkScript := `
try
set clipboardTypes to (clipboard info)
repeat with aType in clipboardTypes
if (first item of aType) is string then
return "hastext"
end if
end repeat
return "notext"
on error
return "error"
end try
`
cmd := exec.Command("osascript", "-e", checkScript)
checkOut, err := cmd.Output()
if err != nil {
return nil, errUnavailable
}
checkOut = bytes.TrimSpace(checkOut)
if !bytes.Equal(checkOut, []byte("hastext")) {
return nil, errUnavailable
}
// Now get the actual text
cmd = exec.Command("osascript", "-e", "get the clipboard")
out, err := cmd.Output()
if err != nil {
return nil, errUnavailable
}
// Remove trailing newline that osascript adds
out = bytes.TrimSuffix(out, []byte("\n"))
// If clipboard was set to empty string, return nil
if len(out) == 0 {
return nil, nil
}
return out, nil
}
func readImage() ([]byte, error) {
// AppleScript to read image data from clipboard as base64
script := `
try
set theData to the clipboard as «class PNGf»
return theData
on error
return ""
end try
`
cmd := exec.Command("osascript", "-e", script)
out, err := cmd.Output()
if err != nil {
return nil, errUnavailable
}
// Check if we got any data
out = bytes.TrimSpace(out)
if len(out) == 0 {
return nil, errUnavailable
}
// The output is in hex format (e.g., «data PNGf89504E...»)
// We need to extract and convert it
outStr := string(out)
if !strings.HasPrefix(outStr, "«data PNGf") || !strings.HasSuffix(outStr, "»") {
return nil, errUnavailable
}
// Extract hex data
hexData := strings.TrimPrefix(outStr, "«data PNGf")
hexData = strings.TrimSuffix(hexData, "»")
// Convert hex to bytes
buf := make([]byte, len(hexData)/2)
for i := 0; i < len(hexData); i += 2 {
b, err := strconv.ParseUint(hexData[i:i+2], 16, 8)
if err != nil {
return nil, errUnavailable
}
buf[i/2] = byte(b)
}
return buf, 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 err error
switch t {
case FmtText:
err = writeText(buf)
case FmtImage:
err = writeImage(buf)
default:
return nil, errUnsupported
}
if err != nil {
return nil, err
}
// Update change count
changeCountMu.Lock()
lastChangeCount++
currentCount := lastChangeCount
changeCountMu.Unlock()
// use unbuffered channel to prevent goroutine leak
changed := make(chan struct{}, 1)
go func() {
for {
time.Sleep(time.Second)
changeCountMu.Lock()
if lastChangeCount != currentCount {
changeCountMu.Unlock()
changed <- struct{}{}
close(changed)
return
}
changeCountMu.Unlock()
}
}()
return changed, nil
}
func writeText(buf []byte) error {
if len(buf) == 0 {
// Clear clipboard
script := `set the clipboard to ""`
cmd := exec.Command("osascript", "-e", script)
if err := cmd.Run(); err != nil {
return errUnavailable
}
return nil
}
// Escape the text for AppleScript
text := string(buf)
text = strings.ReplaceAll(text, "\\", "\\\\")
text = strings.ReplaceAll(text, "\"", "\\\"")
script := fmt.Sprintf(`set the clipboard to "%s"`, text)
cmd := exec.Command("osascript", "-e", script)
if err := cmd.Run(); err != nil {
return errUnavailable
}
return nil
}
func writeImage(buf []byte) error {
if len(buf) == 0 {
// Clear clipboard
script := `set the clipboard to ""`
cmd := exec.Command("osascript", "-e", script)
if err := cmd.Run(); err != nil {
return errUnavailable
}
return nil
}
// Create a temporary file to store the PNG data
tmpFile, err := os.CreateTemp("", "clipboard*.png")
if err != nil {
return errUnavailable
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.Write(buf); err != nil {
tmpFile.Close()
return errUnavailable
}
tmpFile.Close()
// Use osascript to set clipboard to the image file
script := fmt.Sprintf(`
set theFile to POSIX file "%s"
set theImage to read theFile as «class PNGf»
set the clipboard to theImage
`, tmpFile.Name())
cmd := exec.Command("osascript", "-e", script)
if err := cmd.Run(); err != nil {
return errUnavailable
}
return nil
}
func watch(ctx context.Context, t Format) <-chan []byte {
recv := make(chan []byte, 1)
ti := time.NewTicker(time.Second)
// Get initial clipboard content
var lastContent []byte
if b := Read(t); b != nil {
lastContent = make([]byte, len(b))
copy(lastContent, b)
}
go func() {
defer close(recv)
defer ti.Stop()
for {
select {
case <-ctx.Done():
return
case <-ti.C:
b := Read(t)
if b == nil {
continue
}
// Check if content changed
if !bytes.Equal(lastContent, b) {
recv <- b
lastContent = make([]byte, len(b))
copy(lastContent, b)
}
}
}
}()
return recv
}

View file

@ -0,0 +1,80 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build ios
package clipboard
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework UIKit -framework MobileCoreServices
#import <stdlib.h>
void clipboard_write_string(char *s);
char *clipboard_read_string();
*/
import "C"
import (
"bytes"
"context"
"time"
"unsafe"
)
func initialize() error { return nil }
func read(t Format) (buf []byte, err error) {
switch t {
case FmtText:
return []byte(C.GoString(C.clipboard_read_string())), nil
case FmtImage:
return nil, errUnsupported
default:
return nil, errUnsupported
}
}
// SetContent sets the clipboard content for iOS
func write(t Format, buf []byte) (<-chan struct{}, error) {
done := make(chan struct{}, 1)
switch t {
case FmtText:
cs := C.CString(string(buf))
defer C.free(unsafe.Pointer(cs))
C.clipboard_write_string(cs)
return done, nil
case FmtImage:
return nil, errUnsupported
default:
return nil, errUnsupported
}
}
func watch(ctx context.Context, t Format) <-chan []byte {
recv := make(chan []byte, 1)
ti := time.NewTicker(time.Second)
last := Read(t)
go func() {
for {
select {
case <-ctx.Done():
close(recv)
return
case <-ti.C:
b := Read(t)
if b == nil {
continue
}
if bytes.Compare(last, b) != 0 {
recv <- b
last = b
}
}
}
}()
return recv
}

View file

@ -0,0 +1,20 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build ios
#import <UIKit/UIKit.h>
#import <MobileCoreServices/MobileCoreServices.h>
void clipboard_write_string(char *s) {
NSString *value = [NSString stringWithUTF8String:s];
[[UIPasteboard generalPasteboard] setString:value];
}
char *clipboard_read_string() {
NSString *str = [[UIPasteboard generalPasteboard] string];
return (char *)[str UTF8String];
}

View file

@ -0,0 +1,277 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build linux && !android
package clipboard
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"strings"
"sync"
"time"
)
var (
// Clipboard tools in order of preference
clipboardTools = []struct {
name string
readCmd []string
writeCmd []string
readImg []string
writeImg []string
available bool
}{
{
name: "xclip",
readCmd: []string{"xclip", "-selection", "clipboard", "-o"},
writeCmd: []string{"xclip", "-selection", "clipboard"},
readImg: []string{"xclip", "-selection", "clipboard", "-t", "image/png", "-o"},
writeImg: []string{"xclip", "-selection", "clipboard", "-t", "image/png"},
},
{
name: "xsel",
readCmd: []string{"xsel", "--clipboard", "--output"},
writeCmd: []string{"xsel", "--clipboard", "--input"},
readImg: []string{"xsel", "--clipboard", "--output"},
writeImg: []string{"xsel", "--clipboard", "--input"},
},
{
name: "wl-clipboard",
readCmd: []string{"wl-paste", "-n"},
writeCmd: []string{"wl-copy"},
readImg: []string{"wl-paste", "-t", "image/png", "-n"},
writeImg: []string{"wl-copy", "-t", "image/png"},
},
}
selectedTool int = -1
toolMutex sync.Mutex
lastChangeTime time.Time
changeTimeMu sync.Mutex
)
func initialize() error {
toolMutex.Lock()
defer toolMutex.Unlock()
if selectedTool >= 0 {
return nil // Already initialized
}
// Check which clipboard tool is available
for i, tool := range clipboardTools {
cmd := exec.Command("which", tool.name)
if err := cmd.Run(); err == nil {
clipboardTools[i].available = true
if selectedTool < 0 {
selectedTool = i
}
}
}
if selectedTool < 0 {
return fmt.Errorf(`%w: No clipboard utility found. Install one of the following:
For X11 systems:
apt install -y xclip
# or
apt install -y xsel
For Wayland systems:
apt install -y wl-clipboard
If running in a headless environment, you may also need:
apt install -y xvfb
# and run:
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
export DISPLAY=:99.0`, errUnavailable)
}
return nil
}
func read(t Format) (buf []byte, err error) {
toolMutex.Lock()
tool := clipboardTools[selectedTool]
toolMutex.Unlock()
switch t {
case FmtText:
return readText(tool)
case FmtImage:
return readImage(tool)
default:
return nil, errUnsupported
}
}
func readText(tool struct {
name string
readCmd []string
writeCmd []string
readImg []string
writeImg []string
available bool
}) ([]byte, error) {
// First check if clipboard contains text
cmd := exec.Command(tool.readCmd[0], tool.readCmd[1:]...)
out, err := cmd.Output()
if err != nil {
// Check if it's because clipboard contains non-text data
if tool.name == "xclip" {
// xclip returns error when clipboard doesn't contain requested type
checkCmd := exec.Command("xclip", "-selection", "clipboard", "-t", "TARGETS", "-o")
targets, _ := checkCmd.Output()
if bytes.Contains(targets, []byte("image/png")) && !bytes.Contains(targets, []byte("UTF8_STRING")) {
return nil, errUnavailable
}
}
return nil, errUnavailable
}
return out, nil
}
func readImage(tool struct {
name string
readCmd []string
writeCmd []string
readImg []string
writeImg []string
available bool
}) ([]byte, error) {
if tool.name == "xsel" {
// xsel doesn't support image types well, return error
return nil, errUnavailable
}
cmd := exec.Command(tool.readImg[0], tool.readImg[1:]...)
out, err := cmd.Output()
if err != nil {
return nil, errUnavailable
}
// Verify it's PNG data
if len(out) < 8 || !bytes.Equal(out[:8], []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}) {
return nil, errUnavailable
}
return out, nil
}
func write(t Format, buf []byte) (<-chan struct{}, error) {
toolMutex.Lock()
tool := clipboardTools[selectedTool]
toolMutex.Unlock()
var cmd *exec.Cmd
switch t {
case FmtText:
if len(buf) == 0 {
// Write empty string
cmd = exec.Command(tool.writeCmd[0], tool.writeCmd[1:]...)
cmd.Stdin = bytes.NewReader([]byte{})
} else {
cmd = exec.Command(tool.writeCmd[0], tool.writeCmd[1:]...)
cmd.Stdin = bytes.NewReader(buf)
}
case FmtImage:
if tool.name == "xsel" {
// xsel doesn't support image types well
return nil, errUnavailable
}
if len(buf) == 0 {
// Clear clipboard
cmd = exec.Command(tool.writeCmd[0], tool.writeCmd[1:]...)
cmd.Stdin = bytes.NewReader([]byte{})
} else {
cmd = exec.Command(tool.writeImg[0], tool.writeImg[1:]...)
cmd.Stdin = bytes.NewReader(buf)
}
default:
return nil, errUnsupported
}
if err := cmd.Run(); err != nil {
return nil, errUnavailable
}
// Update change time
changeTimeMu.Lock()
lastChangeTime = time.Now()
currentTime := lastChangeTime
changeTimeMu.Unlock()
// Create change notification channel
changed := make(chan struct{}, 1)
go func() {
for {
time.Sleep(time.Second)
changeTimeMu.Lock()
if !lastChangeTime.Equal(currentTime) {
changeTimeMu.Unlock()
changed <- struct{}{}
close(changed)
return
}
changeTimeMu.Unlock()
}
}()
return changed, nil
}
func watch(ctx context.Context, t Format) <-chan []byte {
recv := make(chan []byte, 1)
ti := time.NewTicker(time.Second)
// Get initial clipboard content
var lastContent []byte
if b := Read(t); b != nil {
lastContent = make([]byte, len(b))
copy(lastContent, b)
}
go func() {
defer close(recv)
defer ti.Stop()
for {
select {
case <-ctx.Done():
return
case <-ti.C:
b := Read(t)
if b == nil {
continue
}
// Check if content changed
if !bytes.Equal(lastContent, b) {
recv <- b
lastContent = make([]byte, len(b))
copy(lastContent, b)
}
}
}
}()
return recv
}
// Helper function to check clipboard content type for xclip
func getClipboardTargets() []string {
cmd := exec.Command("xclip", "-selection", "clipboard", "-t", "TARGETS", "-o")
out, err := cmd.Output()
if err != nil {
return nil
}
return strings.Split(string(out), "\n")
}

View file

@ -0,0 +1,25 @@
//go:build !windows && !darwin && !linux && !cgo
package clipboard
import "context"
func initialize() error {
return errNoCgo
}
func read(t Format) (buf []byte, err error) {
panic("clipboard: cannot use when CGO_ENABLED=0")
}
func readc(t string) ([]byte, error) {
panic("clipboard: cannot use when CGO_ENABLED=0")
}
func write(t Format, buf []byte) (<-chan struct{}, error) {
panic("clipboard: cannot use when CGO_ENABLED=0")
}
func watch(ctx context.Context, t Format) <-chan []byte {
panic("clipboard: cannot use when CGO_ENABLED=0")
}

View file

@ -0,0 +1,336 @@
// 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")
}
if err := clipboard.Init(); !errors.Is(err, clipboard.ErrCgoDisabled) {
t.Fatalf("expect ErrCgoDisabled, got: %v", err)
}
})
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)
})
}

View file

@ -0,0 +1,551 @@
// 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
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")
)

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

View 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 |
|:-----:|:---:|:-------:|:-------:|:-----:|
|![](../../tests/testdata/darwin.png)|![](../../tests/testdata/ios.png)|![](../../tests/testdata/windows.png)|![](../../tests/testdata/android.png)|![](../../tests/testdata/linux.png)|
## License
MIT | &copy; 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de).

View file

@ -0,0 +1,236 @@
// 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
// 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 (
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()
}
}
})
}

View 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 | &copy; 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de).

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

View file

@ -0,0 +1,56 @@
// 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
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
}

View file

@ -0,0 +1,14 @@
// 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
ErrCgoDisabled = errNoCgo
)

View file

@ -0,0 +1,13 @@
module golang.design/x/clipboard
go 1.24
require (
golang.org/x/image v0.28.0
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f
)
require (
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 // indirect
golang.org/x/sys v0.33.0 // indirect
)

View file

@ -0,0 +1,8 @@
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 h1:Wdx0vgH5Wgsw+lF//LJKmWOJBLWX6nprsMqnf99rYDE=
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f h1:/n+PL2HlfqeSiDCuhdBbRNlGS/g2fM4OHufalHaTVG8=
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mTVAhA48LpfW/8fNR0ifStlH2axyfg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

View file

@ -0,0 +1,109 @@
#!/bin/bash
# Test script for Linux CGO-free clipboard implementation
echo "Testing Linux clipboard implementation without CGO..."
# Check for required tools
echo "Checking for clipboard tools..."
for tool in xclip xsel wl-copy; do
if command -v $tool &> /dev/null; then
echo "$tool is installed"
else
echo "$tool is not installed"
fi
done
# Create test program
cat > test_linux_clipboard.go << 'EOF'
package main
import (
"fmt"
"log"
"os"
"golang.design/x/clipboard"
)
func main() {
err := clipboard.Init()
if err != nil {
log.Fatal("Failed to initialize clipboard:", err)
}
// Test text
fmt.Println("\n=== Testing Text Clipboard ===")
testText := []byte("Hello from CGO-free Linux clipboard!")
clipboard.Write(clipboard.FmtText, testText)
fmt.Println("Wrote text:", string(testText))
readText := clipboard.Read(clipboard.FmtText)
fmt.Println("Read text:", string(readText))
if string(testText) == string(readText) {
fmt.Println("✓ Text clipboard test passed")
} else {
fmt.Println("✗ Text clipboard test failed")
}
// Test empty write
fmt.Println("\n=== Testing Empty Write ===")
clipboard.Write(clipboard.FmtText, []byte{})
emptyRead := clipboard.Read(clipboard.FmtText)
if emptyRead == nil || len(emptyRead) == 0 {
fmt.Println("✓ Empty write test passed")
} else {
fmt.Println("✗ Empty write test failed, got:", string(emptyRead))
}
// Test image if requested
if len(os.Args) > 1 && os.Args[1] == "image" {
fmt.Println("\n=== Testing Image Clipboard ===")
// Try to read test image
imageData, err := os.ReadFile("tests/testdata/clipboard.png")
if err != nil {
fmt.Println("Could not read test image:", err)
return
}
clipboard.Write(clipboard.FmtImage, imageData)
fmt.Println("Wrote image data, length:", len(imageData))
readImage := clipboard.Read(clipboard.FmtImage)
if readImage != nil {
fmt.Println("Read image data, length:", len(readImage))
if len(imageData) == len(readImage) {
fmt.Println("✓ Image clipboard test passed")
} else {
fmt.Println("✗ Image lengths don't match")
}
} else {
fmt.Println("✗ Failed to read image from clipboard")
}
// Test that reading text from image clipboard returns nil
textFromImage := clipboard.Read(clipboard.FmtText)
if textFromImage == nil {
fmt.Println("✓ Reading text from image clipboard correctly returned nil")
} else {
fmt.Println("✗ Reading text from image clipboard should return nil, got:", string(textFromImage))
}
}
}
EOF
# Run tests with CGO disabled
echo -e "\n=== Running with CGO_ENABLED=0 ==="
CGO_ENABLED=0 go run test_linux_clipboard.go
echo -e "\n=== Running with CGO_ENABLED=0 and image test ==="
CGO_ENABLED=0 go run test_linux_clipboard.go image
# Run actual tests
echo -e "\n=== Running go test with CGO_ENABLED=0 ==="
CGO_ENABLED=0 go test -v -run TestClipboard
# Clean up
rm -f test_linux_clipboard.go
echo -e "\nTest script completed!"

View 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

View 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 ./...

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB