From 3f25e5bf869d70a06afab64ca7812930f06e4ed5 Mon Sep 17 00:00:00 2001 From: adamdottv <2363879+adamdottv@users.noreply.github.com> Date: Wed, 9 Jul 2025 04:55:19 -0500 Subject: chore: internal clipboard package --- packages/tui/clipboard/.github/FUNDING.yml | 12 - .../tui/clipboard/.github/workflows/clipboard.yml | 71 --- packages/tui/clipboard/.gitignore | 15 - packages/tui/clipboard/Dockerfile | 13 - packages/tui/clipboard/LICENSE | 21 - packages/tui/clipboard/README.md | 162 ------ packages/tui/clipboard/clipboard.go | 155 ------ packages/tui/clipboard/clipboard_android.c | 80 --- packages/tui/clipboard/clipboard_android.go | 102 ---- packages/tui/clipboard/clipboard_darwin.go | 266 ---------- packages/tui/clipboard/clipboard_ios.go | 80 --- packages/tui/clipboard/clipboard_ios.m | 20 - packages/tui/clipboard/clipboard_linux.go | 276 ----------- packages/tui/clipboard/clipboard_nocgo.go | 25 - packages/tui/clipboard/clipboard_test.go | 336 ------------- packages/tui/clipboard/clipboard_windows.go | 551 --------------------- .../clipboard/cmd/gclip-gui/AndroidManifest.xml | 33 -- packages/tui/clipboard/cmd/gclip-gui/README.md | 31 -- packages/tui/clipboard/cmd/gclip-gui/main.go | 236 --------- packages/tui/clipboard/cmd/gclip/README.md | 40 -- packages/tui/clipboard/cmd/gclip/main.go | 131 ----- packages/tui/clipboard/example_test.go | 56 --- packages/tui/clipboard/export_test.go | 14 - packages/tui/clipboard/go.mod | 13 - packages/tui/clipboard/go.sum | 8 - packages/tui/clipboard/test_linux_nocgo.sh | 109 ---- packages/tui/clipboard/tests/Makefile | 15 - packages/tui/clipboard/tests/test-docker.sh | 11 - packages/tui/clipboard/tests/testdata/android.png | Bin 109241 -> 0 bytes .../tui/clipboard/tests/testdata/clipboard.png | Bin 21310 -> 0 bytes packages/tui/clipboard/tests/testdata/darwin.png | Bin 118106 -> 0 bytes packages/tui/clipboard/tests/testdata/ios.png | Bin 47419 -> 0 bytes packages/tui/clipboard/tests/testdata/linux.png | Bin 28976 -> 0 bytes packages/tui/clipboard/tests/testdata/windows.png | Bin 20532 -> 0 bytes packages/tui/cmd/opencode/main.go | 2 +- packages/tui/go.mod | 10 +- packages/tui/go.sum | 4 - packages/tui/internal/app/app.go | 2 +- packages/tui/internal/clipboard/clipboard.go | 155 ++++++ .../tui/internal/clipboard/clipboard_darwin.go | 266 ++++++++++ packages/tui/internal/clipboard/clipboard_linux.go | 276 +++++++++++ packages/tui/internal/clipboard/clipboard_nocgo.go | 25 + .../tui/internal/clipboard/clipboard_windows.go | 551 +++++++++++++++++++++ packages/tui/internal/components/chat/editor.go | 2 +- packages/tui/opencode | Bin 0 -> 24791266 bytes 45 files changed, 1278 insertions(+), 2897 deletions(-) delete mode 100644 packages/tui/clipboard/.github/FUNDING.yml delete mode 100644 packages/tui/clipboard/.github/workflows/clipboard.yml delete mode 100644 packages/tui/clipboard/.gitignore delete mode 100644 packages/tui/clipboard/Dockerfile delete mode 100644 packages/tui/clipboard/LICENSE delete mode 100644 packages/tui/clipboard/README.md delete mode 100644 packages/tui/clipboard/clipboard.go delete mode 100644 packages/tui/clipboard/clipboard_android.c delete mode 100644 packages/tui/clipboard/clipboard_android.go delete mode 100644 packages/tui/clipboard/clipboard_darwin.go delete mode 100644 packages/tui/clipboard/clipboard_ios.go delete mode 100644 packages/tui/clipboard/clipboard_ios.m delete mode 100644 packages/tui/clipboard/clipboard_linux.go delete mode 100644 packages/tui/clipboard/clipboard_nocgo.go delete mode 100644 packages/tui/clipboard/clipboard_test.go delete mode 100644 packages/tui/clipboard/clipboard_windows.go delete mode 100644 packages/tui/clipboard/cmd/gclip-gui/AndroidManifest.xml delete mode 100644 packages/tui/clipboard/cmd/gclip-gui/README.md delete mode 100644 packages/tui/clipboard/cmd/gclip-gui/main.go delete mode 100644 packages/tui/clipboard/cmd/gclip/README.md delete mode 100644 packages/tui/clipboard/cmd/gclip/main.go delete mode 100644 packages/tui/clipboard/example_test.go delete mode 100644 packages/tui/clipboard/export_test.go delete mode 100644 packages/tui/clipboard/go.mod delete mode 100644 packages/tui/clipboard/go.sum delete mode 100755 packages/tui/clipboard/test_linux_nocgo.sh delete mode 100644 packages/tui/clipboard/tests/Makefile delete mode 100755 packages/tui/clipboard/tests/test-docker.sh delete mode 100644 packages/tui/clipboard/tests/testdata/android.png delete mode 100644 packages/tui/clipboard/tests/testdata/clipboard.png delete mode 100644 packages/tui/clipboard/tests/testdata/darwin.png delete mode 100644 packages/tui/clipboard/tests/testdata/ios.png delete mode 100644 packages/tui/clipboard/tests/testdata/linux.png delete mode 100644 packages/tui/clipboard/tests/testdata/windows.png create mode 100644 packages/tui/internal/clipboard/clipboard.go create mode 100644 packages/tui/internal/clipboard/clipboard_darwin.go create mode 100644 packages/tui/internal/clipboard/clipboard_linux.go create mode 100644 packages/tui/internal/clipboard/clipboard_nocgo.go create mode 100644 packages/tui/internal/clipboard/clipboard_windows.go create mode 100755 packages/tui/opencode diff --git a/packages/tui/clipboard/.github/FUNDING.yml b/packages/tui/clipboard/.github/FUNDING.yml deleted file mode 100644 index 2957eb0ea..000000000 --- a/packages/tui/clipboard/.github/FUNDING.yml +++ /dev/null @@ -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'] diff --git a/packages/tui/clipboard/.github/workflows/clipboard.yml b/packages/tui/clipboard/.github/workflows/clipboard.yml deleted file mode 100644 index 38441335d..000000000 --- a/packages/tui/clipboard/.github/workflows/clipboard.yml +++ /dev/null @@ -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 - -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 deleted file mode 100644 index 66fd13c90..000000000 --- a/packages/tui/clipboard/.gitignore +++ /dev/null @@ -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/ diff --git a/packages/tui/clipboard/Dockerfile b/packages/tui/clipboard/Dockerfile deleted file mode 100644 index b810346f3..000000000 --- a/packages/tui/clipboard/Dockerfile +++ /dev/null @@ -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 - -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 deleted file mode 100644 index 7f2bbe295..000000000 --- a/packages/tui/clipboard/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Changkun Ou - -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 deleted file mode 100644 index d71200703..000000000 --- a/packages/tui/clipboard/README.md +++ /dev/null @@ -1,162 +0,0 @@ -# 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 ] - -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). \ No newline at end of file diff --git a/packages/tui/clipboard/clipboard.go b/packages/tui/clipboard/clipboard.go deleted file mode 100644 index bd7c69843..000000000 --- a/packages/tui/clipboard/clipboard.go +++ /dev/null @@ -1,155 +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 - -/* -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 deleted file mode 100644 index 9dc34f6d6..000000000 --- a/packages/tui/clipboard/clipboard_android.c +++ /dev/null @@ -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 - -//go:build android - -#include -#include -#include -#include - -#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 deleted file mode 100644 index c9ce78fcb..000000000 --- a/packages/tui/clipboard/clipboard_android.go +++ /dev/null @@ -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 - -//go:build android - -package clipboard - -/* -#cgo LDFLAGS: -landroid -llog - -#include -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 deleted file mode 100644 index 0b902403a..000000000 --- a/packages/tui/clipboard/clipboard_darwin.go +++ /dev/null @@ -1,266 +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 - -//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 deleted file mode 100644 index e027b8c2b..000000000 --- a/packages/tui/clipboard/clipboard_ios.go +++ /dev/null @@ -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 - -//go:build ios - -package clipboard - -/* -#cgo CFLAGS: -x objective-c -#cgo LDFLAGS: -framework Foundation -framework UIKit -framework MobileCoreServices - -#import -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 deleted file mode 100644 index 15eb122bc..000000000 --- a/packages/tui/clipboard/clipboard_ios.m +++ /dev/null @@ -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 - -//go:build ios - -#import -#import - -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 deleted file mode 100644 index ca4a8db75..000000000 --- a/packages/tui/clipboard/clipboard_linux.go +++ /dev/null @@ -1,276 +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 - -//go:build linux && !android - -package clipboard - -import ( - "bytes" - "context" - "fmt" - "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 deleted file mode 100644 index 7b3e05f6c..000000000 --- a/packages/tui/clipboard/clipboard_nocgo.go +++ /dev/null @@ -1,25 +0,0 @@ -//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 deleted file mode 100644 index 9dce284bd..000000000 --- a/packages/tui/clipboard/clipboard_test.go +++ /dev/null @@ -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 - -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 deleted file mode 100644 index bd042cda8..000000000 --- a/packages/tui/clipboard/clipboard_windows.go +++ /dev/null @@ -1,551 +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 - -//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 deleted file mode 100644 index 63b0cba57..000000000 --- a/packages/tui/clipboard/cmd/gclip-gui/AndroidManifest.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/packages/tui/clipboard/cmd/gclip-gui/README.md b/packages/tui/clipboard/cmd/gclip-gui/README.md deleted file mode 100644 index 571604e7f..000000000 --- a/packages/tui/clipboard/cmd/gclip-gui/README.md +++ /dev/null @@ -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 | -|:-----:|:---:|:-------:|:-------:|:-----:| -|![](../../tests/testdata/darwin.png)|![](../../tests/testdata/ios.png)|![](../../tests/testdata/windows.png)|![](../../tests/testdata/android.png)|![](../../tests/testdata/linux.png)| - -## License - -MIT | © 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 deleted file mode 100644 index 1bf46270c..000000000 --- a/packages/tui/clipboard/cmd/gclip-gui/main.go +++ /dev/null @@ -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 - -//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 deleted file mode 100644 index d4ea0a6c0..000000000 --- a/packages/tui/clipboard/cmd/gclip/README.md +++ /dev/null @@ -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 ] -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). \ No newline at end of file diff --git a/packages/tui/clipboard/cmd/gclip/main.go b/packages/tui/clipboard/cmd/gclip/main.go deleted file mode 100644 index 30d5714b3..000000000 --- a/packages/tui/clipboard/cmd/gclip/main.go +++ /dev/null @@ -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 - -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 ] - -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 deleted file mode 100644 index 72d613a80..000000000 --- a/packages/tui/clipboard/example_test.go +++ /dev/null @@ -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 - -//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 deleted file mode 100644 index 69af20ace..000000000 --- a/packages/tui/clipboard/export_test.go +++ /dev/null @@ -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 - -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 deleted file mode 100644 index ba0dd55b2..000000000 --- a/packages/tui/clipboard/go.mod +++ /dev/null @@ -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 -) diff --git a/packages/tui/clipboard/go.sum b/packages/tui/clipboard/go.sum deleted file mode 100644 index a68e266bd..000000000 --- a/packages/tui/clipboard/go.sum +++ /dev/null @@ -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= diff --git a/packages/tui/clipboard/test_linux_nocgo.sh b/packages/tui/clipboard/test_linux_nocgo.sh deleted file mode 100755 index 877da2112..000000000 --- a/packages/tui/clipboard/test_linux_nocgo.sh +++ /dev/null @@ -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!" \ No newline at end of file diff --git a/packages/tui/clipboard/tests/Makefile b/packages/tui/clipboard/tests/Makefile deleted file mode 100644 index 809e2eaf5..000000000 --- a/packages/tui/clipboard/tests/Makefile +++ /dev/null @@ -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 - -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 deleted file mode 100755 index a17a6af75..000000000 --- a/packages/tui/clipboard/tests/test-docker.sh +++ /dev/null @@ -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 - -# 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 deleted file mode 100644 index b309ce409..000000000 Binary files a/packages/tui/clipboard/tests/testdata/android.png and /dev/null differ diff --git a/packages/tui/clipboard/tests/testdata/clipboard.png b/packages/tui/clipboard/tests/testdata/clipboard.png deleted file mode 100644 index cd3af8c10..000000000 Binary files a/packages/tui/clipboard/tests/testdata/clipboard.png and /dev/null differ diff --git a/packages/tui/clipboard/tests/testdata/darwin.png b/packages/tui/clipboard/tests/testdata/darwin.png deleted file mode 100644 index b9ed2bff9..000000000 Binary files a/packages/tui/clipboard/tests/testdata/darwin.png and /dev/null differ diff --git a/packages/tui/clipboard/tests/testdata/ios.png b/packages/tui/clipboard/tests/testdata/ios.png deleted file mode 100644 index ddc7a9d43..000000000 Binary files a/packages/tui/clipboard/tests/testdata/ios.png and /dev/null differ diff --git a/packages/tui/clipboard/tests/testdata/linux.png b/packages/tui/clipboard/tests/testdata/linux.png deleted file mode 100644 index e15627351..000000000 Binary files a/packages/tui/clipboard/tests/testdata/linux.png and /dev/null differ diff --git a/packages/tui/clipboard/tests/testdata/windows.png b/packages/tui/clipboard/tests/testdata/windows.png deleted file mode 100644 index dd25f31dc..000000000 Binary files a/packages/tui/clipboard/tests/testdata/windows.png and /dev/null differ diff --git a/packages/tui/cmd/opencode/main.go b/packages/tui/cmd/opencode/main.go index ada1880d1..0c8f70c8b 100644 --- a/packages/tui/cmd/opencode/main.go +++ b/packages/tui/cmd/opencode/main.go @@ -13,8 +13,8 @@ import ( "github.com/sst/opencode-sdk-go" "github.com/sst/opencode-sdk-go/option" "github.com/sst/opencode/internal/app" + "github.com/sst/opencode/internal/clipboard" "github.com/sst/opencode/internal/tui" - "golang.design/x/clipboard" ) var Version = "dev" diff --git a/packages/tui/go.mod b/packages/tui/go.mod index 08bc1c589..12dd3f88e 100644 --- a/packages/tui/go.mod +++ b/packages/tui/go.mod @@ -17,14 +17,11 @@ require ( github.com/muesli/termenv v0.16.0 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 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 ) -replace ( - github.com/sst/opencode-sdk-go => ./sdk - golang.design/x/clipboard => ./clipboard -) +replace github.com/sst/opencode-sdk-go => ./sdk 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/sjson v1.2.5 // 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/tools v0.34.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/yuin/goldmark v1.7.8 // 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/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect diff --git a/packages/tui/go.sum b/packages/tui/go.sum index 969cf1beb..9eab3f577 100644 --- a/packages/tui/go.sum +++ b/packages/tui/go.sum @@ -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/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/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/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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 83934343e..a70b3ca64 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -12,13 +12,13 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode/internal/clipboard" "github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/components/toast" "github.com/sst/opencode/internal/config" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" - "golang.design/x/clipboard" ) type App struct { diff --git a/packages/tui/internal/clipboard/clipboard.go b/packages/tui/internal/clipboard/clipboard.go new file mode 100644 index 000000000..70e05bd29 --- /dev/null +++ b/packages/tui/internal/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 + +/* +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 ( + "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/internal/clipboard/clipboard_darwin.go b/packages/tui/internal/clipboard/clipboard_darwin.go new file mode 100644 index 000000000..ead6811f1 --- /dev/null +++ b/packages/tui/internal/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 + +//go:build darwin + +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/internal/clipboard/clipboard_linux.go b/packages/tui/internal/clipboard/clipboard_linux.go new file mode 100644 index 000000000..ca8a3bc6a --- /dev/null +++ b/packages/tui/internal/clipboard/clipboard_linux.go @@ -0,0 +1,276 @@ +// 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 + +//go:build linux + +package clipboard + +import ( + "bytes" + "context" + "fmt" + "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/internal/clipboard/clipboard_nocgo.go b/packages/tui/internal/clipboard/clipboard_nocgo.go new file mode 100644 index 000000000..7b3e05f6c --- /dev/null +++ b/packages/tui/internal/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/internal/clipboard/clipboard_windows.go b/packages/tui/internal/clipboard/clipboard_windows.go new file mode 100644 index 000000000..bd042cda8 --- /dev/null +++ b/packages/tui/internal/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 + +//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/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index 071f22d0e..6053c9c01 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -15,13 +15,13 @@ import ( "github.com/google/uuid" "github.com/sst/opencode-sdk-go" "github.com/sst/opencode/internal/app" + "github.com/sst/opencode/internal/clipboard" "github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/components/dialog" "github.com/sst/opencode/internal/components/textarea" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" - "golang.design/x/clipboard" ) type EditorComponent interface { diff --git a/packages/tui/opencode b/packages/tui/opencode new file mode 100755 index 000000000..cdaf172d2 Binary files /dev/null and b/packages/tui/opencode differ -- cgit v1.2.3