mirror of
https://github.com/sst/opencode.git
synced 2025-08-04 05:28:16 +00:00
chore: internal clipboard package
This commit is contained in:
parent
67765fa47c
commit
3f25e5bf86
40 changed files with 8 additions and 1627 deletions
12
packages/tui/clipboard/.github/FUNDING.yml
vendored
12
packages/tui/clipboard/.github/FUNDING.yml
vendored
|
@ -1,12 +0,0 @@
|
||||||
# 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']
|
|
|
@ -1,71 +0,0 @@
|
||||||
# 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
15
packages/tui/clipboard/.gitignore
vendored
|
@ -1,15 +0,0 @@
|
||||||
# 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/
|
|
|
@ -1,13 +0,0 @@
|
||||||
# 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" ]
|
|
|
@ -1,21 +0,0 @@
|
||||||
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.
|
|
|
@ -1,162 +0,0 @@
|
||||||
# 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.
|
|
||||||
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 | © 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de).
|
|
|
@ -1,80 +0,0 @@
|
||||||
// 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));
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
// 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];
|
|
||||||
}
|
|
|
@ -1,336 +0,0 @@
|
||||||
// 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)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,31 +0,0 @@
|
||||||
# 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).
|
|
|
@ -1,236 +0,0 @@
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
# 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).
|
|
|
@ -1,131 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
// 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
|
|
||||||
)
|
|
|
@ -1,13 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,8 +0,0 @@
|
||||||
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=
|
|
|
@ -1,109 +0,0 @@
|
||||||
#!/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!"
|
|
|
@ -1,15 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,11 +0,0 @@
|
||||||
# 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
packages/tui/clipboard/tests/testdata/android.png
vendored
BIN
packages/tui/clipboard/tests/testdata/android.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 107 KiB |
BIN
packages/tui/clipboard/tests/testdata/clipboard.png
vendored
BIN
packages/tui/clipboard/tests/testdata/clipboard.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
BIN
packages/tui/clipboard/tests/testdata/darwin.png
vendored
BIN
packages/tui/clipboard/tests/testdata/darwin.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 115 KiB |
BIN
packages/tui/clipboard/tests/testdata/ios.png
vendored
BIN
packages/tui/clipboard/tests/testdata/ios.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
BIN
packages/tui/clipboard/tests/testdata/linux.png
vendored
BIN
packages/tui/clipboard/tests/testdata/linux.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 28 KiB |
BIN
packages/tui/clipboard/tests/testdata/windows.png
vendored
BIN
packages/tui/clipboard/tests/testdata/windows.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
|
@ -13,8 +13,8 @@ import (
|
||||||
"github.com/sst/opencode-sdk-go"
|
"github.com/sst/opencode-sdk-go"
|
||||||
"github.com/sst/opencode-sdk-go/option"
|
"github.com/sst/opencode-sdk-go/option"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/clipboard"
|
||||||
"github.com/sst/opencode/internal/tui"
|
"github.com/sst/opencode/internal/tui"
|
||||||
"golang.design/x/clipboard"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
|
@ -17,14 +17,11 @@ require (
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
||||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.8
|
github.com/sst/opencode-sdk-go v0.1.0-alpha.8
|
||||||
golang.design/x/clipboard v0.7.1
|
golang.org/x/image v0.28.0
|
||||||
rsc.io/qr v0.2.0
|
rsc.io/qr v0.2.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace github.com/sst/opencode-sdk-go => ./sdk
|
||||||
github.com/sst/opencode-sdk-go => ./sdk
|
|
||||||
golang.design/x/clipboard => ./clipboard
|
|
||||||
)
|
|
||||||
|
|
||||||
require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||||
|
|
||||||
|
@ -58,8 +55,6 @@ require (
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||||
golang.org/x/exp/shiny v0.0.0-20250620022241-b7579e27df2b // indirect
|
|
||||||
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect
|
|
||||||
golang.org/x/mod v0.25.0 // indirect
|
golang.org/x/mod v0.25.0 // indirect
|
||||||
golang.org/x/tools v0.34.0 // indirect
|
golang.org/x/tools v0.34.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
@ -87,7 +82,6 @@ require (
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
github.com/yuin/goldmark v1.7.8 // indirect
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||||
golang.org/x/image v0.28.0 // indirect
|
|
||||||
golang.org/x/net v0.41.0 // indirect
|
golang.org/x/net v0.41.0 // indirect
|
||||||
golang.org/x/sync v0.15.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
|
|
@ -216,12 +216,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
golang.org/x/exp/shiny v0.0.0-20250620022241-b7579e27df2b h1:zELBzk+7ERc6m8BxhzU2VYjp03wlEvi+cIgYQR5H3CI=
|
|
||||||
golang.org/x/exp/shiny v0.0.0-20250620022241-b7579e27df2b/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
|
|
||||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
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/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/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
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/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
|
|
@ -12,13 +12,13 @@ import (
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/sst/opencode-sdk-go"
|
"github.com/sst/opencode-sdk-go"
|
||||||
|
"github.com/sst/opencode/internal/clipboard"
|
||||||
"github.com/sst/opencode/internal/commands"
|
"github.com/sst/opencode/internal/commands"
|
||||||
"github.com/sst/opencode/internal/components/toast"
|
"github.com/sst/opencode/internal/components/toast"
|
||||||
"github.com/sst/opencode/internal/config"
|
"github.com/sst/opencode/internal/config"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/internal/util"
|
"github.com/sst/opencode/internal/util"
|
||||||
"golang.design/x/clipboard"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
|
|
|
@ -53,7 +53,7 @@ clipboard data is changed, use the watcher API:
|
||||||
println(string(data))
|
println(string(data))
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
package clipboard // import "golang.design/x/clipboard"
|
package clipboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -4,7 +4,7 @@
|
||||||
//
|
//
|
||||||
// Written by Changkun Ou <changkun.de>
|
// Written by Changkun Ou <changkun.de>
|
||||||
|
|
||||||
//go:build darwin && !ios
|
//go:build darwin
|
||||||
|
|
||||||
package clipboard
|
package clipboard
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
//
|
//
|
||||||
// Written by Changkun Ou <changkun.de>
|
// Written by Changkun Ou <changkun.de>
|
||||||
|
|
||||||
//go:build linux && !android
|
//go:build linux
|
||||||
|
|
||||||
package clipboard
|
package clipboard
|
||||||
|
|
|
@ -15,13 +15,13 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sst/opencode-sdk-go"
|
"github.com/sst/opencode-sdk-go"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/clipboard"
|
||||||
"github.com/sst/opencode/internal/commands"
|
"github.com/sst/opencode/internal/commands"
|
||||||
"github.com/sst/opencode/internal/components/dialog"
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
"github.com/sst/opencode/internal/components/textarea"
|
"github.com/sst/opencode/internal/components/textarea"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/internal/util"
|
"github.com/sst/opencode/internal/util"
|
||||||
"golang.design/x/clipboard"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type EditorComponent interface {
|
type EditorComponent interface {
|
||||||
|
|
BIN
packages/tui/opencode
Executable file
BIN
packages/tui/opencode
Executable file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue