summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-07-08 18:48:40 -0500
committeradamdottv <[email protected]>2025-07-08 18:48:40 -0500
commit39bcba85a9e9270f8f6734dcd227dd821b5931e0 (patch)
tree93cbc11fc16b85aaaa93c271637d2a51e781352e
parentda3df51316eb6e56835d0c22547c6dece6a791db (diff)
downloadopencode-39bcba85a9e9270f8f6734dcd227dd821b5931e0.tar.gz
opencode-39bcba85a9e9270f8f6734dcd227dd821b5931e0.zip
chore: vendor clipboard into go package
-rw-r--r--packages/tui/clipboard/.github/FUNDING.yml12
-rw-r--r--packages/tui/clipboard/.github/workflows/clipboard.yml71
-rw-r--r--packages/tui/clipboard/.gitignore15
-rw-r--r--packages/tui/clipboard/Dockerfile13
-rw-r--r--packages/tui/clipboard/LICENSE21
-rw-r--r--packages/tui/clipboard/README.md162
-rw-r--r--packages/tui/clipboard/clipboard.go155
-rw-r--r--packages/tui/clipboard/clipboard_android.c80
-rw-r--r--packages/tui/clipboard/clipboard_android.go102
-rw-r--r--packages/tui/clipboard/clipboard_darwin.go266
-rw-r--r--packages/tui/clipboard/clipboard_ios.go80
-rw-r--r--packages/tui/clipboard/clipboard_ios.m20
-rw-r--r--packages/tui/clipboard/clipboard_linux.go277
-rw-r--r--packages/tui/clipboard/clipboard_nocgo.go25
-rw-r--r--packages/tui/clipboard/clipboard_test.go336
-rw-r--r--packages/tui/clipboard/clipboard_windows.go551
-rw-r--r--packages/tui/clipboard/cmd/gclip-gui/AndroidManifest.xml33
-rw-r--r--packages/tui/clipboard/cmd/gclip-gui/README.md31
-rw-r--r--packages/tui/clipboard/cmd/gclip-gui/main.go236
-rw-r--r--packages/tui/clipboard/cmd/gclip/README.md40
-rw-r--r--packages/tui/clipboard/cmd/gclip/main.go131
-rw-r--r--packages/tui/clipboard/example_test.go56
-rw-r--r--packages/tui/clipboard/export_test.go14
-rw-r--r--packages/tui/clipboard/go.mod13
-rw-r--r--packages/tui/clipboard/go.sum8
-rwxr-xr-xpackages/tui/clipboard/test_linux_nocgo.sh109
-rw-r--r--packages/tui/clipboard/tests/Makefile15
-rwxr-xr-xpackages/tui/clipboard/tests/test-docker.sh11
-rw-r--r--packages/tui/clipboard/tests/testdata/android.pngbin0 -> 109241 bytes
-rw-r--r--packages/tui/clipboard/tests/testdata/clipboard.pngbin0 -> 21310 bytes
-rw-r--r--packages/tui/clipboard/tests/testdata/darwin.pngbin0 -> 118106 bytes
-rw-r--r--packages/tui/clipboard/tests/testdata/ios.pngbin0 -> 47419 bytes
-rw-r--r--packages/tui/clipboard/tests/testdata/linux.pngbin0 -> 28976 bytes
-rw-r--r--packages/tui/clipboard/tests/testdata/windows.pngbin0 -> 20532 bytes
34 files changed, 2883 insertions, 0 deletions
diff --git a/packages/tui/clipboard/.github/FUNDING.yml b/packages/tui/clipboard/.github/FUNDING.yml
new file mode 100644
index 000000000..2957eb0ea
--- /dev/null
+++ b/packages/tui/clipboard/.github/FUNDING.yml
@@ -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']
diff --git a/packages/tui/clipboard/.github/workflows/clipboard.yml b/packages/tui/clipboard/.github/workflows/clipboard.yml
new file mode 100644
index 000000000..38441335d
--- /dev/null
+++ b/packages/tui/clipboard/.github/workflows/clipboard.yml
@@ -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 . \ No newline at end of file
diff --git a/packages/tui/clipboard/.gitignore b/packages/tui/clipboard/.gitignore
new file mode 100644
index 000000000..66fd13c90
--- /dev/null
+++ b/packages/tui/clipboard/.gitignore
@@ -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/
diff --git a/packages/tui/clipboard/Dockerfile b/packages/tui/clipboard/Dockerfile
new file mode 100644
index 000000000..b810346f3
--- /dev/null
+++ b/packages/tui/clipboard/Dockerfile
@@ -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" ]
diff --git a/packages/tui/clipboard/LICENSE b/packages/tui/clipboard/LICENSE
new file mode 100644
index 000000000..7f2bbe295
--- /dev/null
+++ b/packages/tui/clipboard/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Changkun Ou <[email protected]>
+
+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. \ No newline at end of file
diff --git a/packages/tui/clipboard/README.md b/packages/tui/clipboard/README.md
new file mode 100644
index 000000000..d71200703
--- /dev/null
+++ b/packages/tui/clipboard/README.md
@@ -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). \ No newline at end of file
diff --git a/packages/tui/clipboard/clipboard.go b/packages/tui/clipboard/clipboard.go
new file mode 100644
index 000000000..bd7c69843
--- /dev/null
+++ b/packages/tui/clipboard/clipboard.go
@@ -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)
+}
diff --git a/packages/tui/clipboard/clipboard_android.c b/packages/tui/clipboard/clipboard_android.c
new file mode 100644
index 000000000..9dc34f6d6
--- /dev/null
+++ b/packages/tui/clipboard/clipboard_android.c
@@ -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));
+}
diff --git a/packages/tui/clipboard/clipboard_android.go b/packages/tui/clipboard/clipboard_android.go
new file mode 100644
index 000000000..c9ce78fcb
--- /dev/null
+++ b/packages/tui/clipboard/clipboard_android.go
@@ -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
+}
diff --git a/packages/tui/clipboard/clipboard_darwin.go b/packages/tui/clipboard/clipboard_darwin.go
new file mode 100644
index 000000000..0b902403a
--- /dev/null
+++ b/packages/tui/clipboard/clipboard_darwin.go
@@ -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
+}
diff --git a/packages/tui/clipboard/clipboard_ios.go b/packages/tui/clipboard/clipboard_ios.go
new file mode 100644
index 000000000..e027b8c2b
--- /dev/null
+++ b/packages/tui/clipboard/clipboard_ios.go
@@ -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
+}
diff --git a/packages/tui/clipboard/clipboard_ios.m b/packages/tui/clipboard/clipboard_ios.m
new file mode 100644
index 000000000..15eb122bc
--- /dev/null
+++ b/packages/tui/clipboard/clipboard_ios.m
@@ -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];
+}
diff --git a/packages/tui/clipboard/clipboard_linux.go b/packages/tui/clipboard/clipboard_linux.go
new file mode 100644
index 000000000..218d0709d
--- /dev/null
+++ b/packages/tui/clipboard/clipboard_linux.go
@@ -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")
+}
diff --git a/packages/tui/clipboard/clipboard_nocgo.go b/packages/tui/clipboard/clipboard_nocgo.go
new file mode 100644
index 000000000..7b3e05f6c
--- /dev/null
+++ b/packages/tui/clipboard/clipboard_nocgo.go
@@ -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")
+}
diff --git a/packages/tui/clipboard/clipboard_test.go b/packages/tui/clipboard/clipboard_test.go
new file mode 100644
index 000000000..9dce284bd
--- /dev/null
+++ b/packages/tui/clipboard/clipboard_test.go
@@ -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)
+ })
+}
diff --git a/packages/tui/clipboard/clipboard_windows.go b/packages/tui/clipboard/clipboard_windows.go
new file mode 100644
index 000000000..bd042cda8
--- /dev/null
+++ b/packages/tui/clipboard/clipboard_windows.go
@@ -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")
+)
diff --git a/packages/tui/clipboard/cmd/gclip-gui/AndroidManifest.xml b/packages/tui/clipboard/cmd/gclip-gui/AndroidManifest.xml
new file mode 100644
index 000000000..63b0cba57
--- /dev/null
+++ b/packages/tui/clipboard/cmd/gclip-gui/AndroidManifest.xml
@@ -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>
diff --git a/packages/tui/clipboard/cmd/gclip-gui/README.md b/packages/tui/clipboard/cmd/gclip-gui/README.md
new file mode 100644
index 000000000..571604e7f
--- /dev/null
+++ b/packages/tui/clipboard/cmd/gclip-gui/README.md
@@ -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). \ No newline at end of file
diff --git a/packages/tui/clipboard/cmd/gclip-gui/main.go b/packages/tui/clipboard/cmd/gclip-gui/main.go
new file mode 100644
index 000000000..1bf46270c
--- /dev/null
+++ b/packages/tui/clipboard/cmd/gclip-gui/main.go
@@ -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()
+ }
+ }
+ })
+}
diff --git a/packages/tui/clipboard/cmd/gclip/README.md b/packages/tui/clipboard/cmd/gclip/README.md
new file mode 100644
index 000000000..d4ea0a6c0
--- /dev/null
+++ b/packages/tui/clipboard/cmd/gclip/README.md
@@ -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). \ No newline at end of file
diff --git a/packages/tui/clipboard/cmd/gclip/main.go b/packages/tui/clipboard/cmd/gclip/main.go
new file mode 100644
index 000000000..30d5714b3
--- /dev/null
+++ b/packages/tui/clipboard/cmd/gclip/main.go
@@ -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
+}
diff --git a/packages/tui/clipboard/example_test.go b/packages/tui/clipboard/example_test.go
new file mode 100644
index 000000000..72d613a80
--- /dev/null
+++ b/packages/tui/clipboard/example_test.go
@@ -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
+}
diff --git a/packages/tui/clipboard/export_test.go b/packages/tui/clipboard/export_test.go
new file mode 100644
index 000000000..69af20ace
--- /dev/null
+++ b/packages/tui/clipboard/export_test.go
@@ -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
+)
diff --git a/packages/tui/clipboard/go.mod b/packages/tui/clipboard/go.mod
new file mode 100644
index 000000000..ba0dd55b2
--- /dev/null
+++ b/packages/tui/clipboard/go.mod
@@ -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
+)
diff --git a/packages/tui/clipboard/go.sum b/packages/tui/clipboard/go.sum
new file mode 100644
index 000000000..a68e266bd
--- /dev/null
+++ b/packages/tui/clipboard/go.sum
@@ -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=
diff --git a/packages/tui/clipboard/test_linux_nocgo.sh b/packages/tui/clipboard/test_linux_nocgo.sh
new file mode 100755
index 000000000..877da2112
--- /dev/null
+++ b/packages/tui/clipboard/test_linux_nocgo.sh
@@ -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!" \ No newline at end of file
diff --git a/packages/tui/clipboard/tests/Makefile b/packages/tui/clipboard/tests/Makefile
new file mode 100644
index 000000000..809e2eaf5
--- /dev/null
+++ b/packages/tui/clipboard/tests/Makefile
@@ -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 \ No newline at end of file
diff --git a/packages/tui/clipboard/tests/test-docker.sh b/packages/tui/clipboard/tests/test-docker.sh
new file mode 100755
index 000000000..a17a6af75
--- /dev/null
+++ b/packages/tui/clipboard/tests/test-docker.sh
@@ -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 ./... \ No newline at end of file
diff --git a/packages/tui/clipboard/tests/testdata/android.png b/packages/tui/clipboard/tests/testdata/android.png
new file mode 100644
index 000000000..b309ce409
--- /dev/null
+++ b/packages/tui/clipboard/tests/testdata/android.png
Binary files differ
diff --git a/packages/tui/clipboard/tests/testdata/clipboard.png b/packages/tui/clipboard/tests/testdata/clipboard.png
new file mode 100644
index 000000000..cd3af8c10
--- /dev/null
+++ b/packages/tui/clipboard/tests/testdata/clipboard.png
Binary files differ
diff --git a/packages/tui/clipboard/tests/testdata/darwin.png b/packages/tui/clipboard/tests/testdata/darwin.png
new file mode 100644
index 000000000..b9ed2bff9
--- /dev/null
+++ b/packages/tui/clipboard/tests/testdata/darwin.png
Binary files differ
diff --git a/packages/tui/clipboard/tests/testdata/ios.png b/packages/tui/clipboard/tests/testdata/ios.png
new file mode 100644
index 000000000..ddc7a9d43
--- /dev/null
+++ b/packages/tui/clipboard/tests/testdata/ios.png
Binary files differ
diff --git a/packages/tui/clipboard/tests/testdata/linux.png b/packages/tui/clipboard/tests/testdata/linux.png
new file mode 100644
index 000000000..e15627351
--- /dev/null
+++ b/packages/tui/clipboard/tests/testdata/linux.png
Binary files differ
diff --git a/packages/tui/clipboard/tests/testdata/windows.png b/packages/tui/clipboard/tests/testdata/windows.png
new file mode 100644
index 000000000..dd25f31dc
--- /dev/null
+++ b/packages/tui/clipboard/tests/testdata/windows.png
Binary files differ