├── tests ├── testdata │ ├── ios.png │ ├── linux.png │ ├── android.png │ ├── darwin.png │ ├── windows.png │ └── clipboard.png ├── test-docker.sh └── Makefile ├── go.mod ├── .gitignore ├── export_test.go ├── Dockerfile ├── clipboard_nocgo.go ├── clipboard_ios.m ├── go.sum ├── .github ├── FUNDING.yml └── workflows │ └── clipboard.yml ├── LICENSE ├── example_test.go ├── cmd ├── gclip-gui │ ├── AndroidManifest.xml │ ├── README.md │ └── main.go └── gclip │ ├── README.md │ └── main.go ├── clipboard_ios.go ├── clipboard_darwin.m ├── clipboard_android.go ├── clipboard_android.c ├── clipboard_darwin.go ├── clipboard_linux.go ├── clipboard.go ├── README.md ├── clipboard_test.go ├── clipboard_linux.c └── clipboard_windows.go /tests/testdata/ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-design/clipboard/HEAD/tests/testdata/ios.png -------------------------------------------------------------------------------- /tests/testdata/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-design/clipboard/HEAD/tests/testdata/linux.png -------------------------------------------------------------------------------- /tests/testdata/android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-design/clipboard/HEAD/tests/testdata/android.png -------------------------------------------------------------------------------- /tests/testdata/darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-design/clipboard/HEAD/tests/testdata/darwin.png -------------------------------------------------------------------------------- /tests/testdata/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-design/clipboard/HEAD/tests/testdata/windows.png -------------------------------------------------------------------------------- /tests/testdata/clipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-design/clipboard/HEAD/tests/testdata/clipboard.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module golang.design/x/clipboard 2 | 3 | go 1.24 4 | 5 | require ( 6 | golang.org/x/image v0.28.0 7 | golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f 8 | ) 9 | 10 | require ( 11 | golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 // indirect 12 | golang.org/x/sys v0.33.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /tests/test-docker.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The golang.design Initiative Authors. 2 | # All rights reserved. Use of this source code is governed 3 | # by a MIT license that can be found in the LICENSE file. 4 | # 5 | # Written by Changkun Ou 6 | 7 | # require apt-get install xvfb 8 | Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 9 | export DISPLAY=:99.0 10 | 11 | go test -v -covermode=atomic ./... -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | package clipboard 8 | 9 | // for debugging errors 10 | var ( 11 | Debug = debug 12 | ErrUnavailable = errUnavailable 13 | ErrCgoDisabled = errNoCgo 14 | ) 15 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The golang.design Initiative Authors. 2 | # All rights reserved. Use of this source code is governed 3 | # by a MIT license that can be found in the LICENSE file. 4 | # 5 | # Written by Changkun Ou 6 | 7 | all: test 8 | 9 | test: 10 | go test -v -count=1 -covermode=atomic .. 11 | 12 | test-docker: 13 | docker build -t golang-design/x/clipboard .. 14 | docker run --rm --name cb golang-design/x/clipboard 15 | docker rmi golang-design/x/clipboard -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The golang.design Initiative Authors. 2 | # All rights reserved. Use of this source code is governed 3 | # by a MIT license that can be found in the LICENSE file. 4 | # 5 | # Written by Changkun Ou 6 | 7 | FROM golang:1.24 8 | RUN apt-get update && apt-get install -y \ 9 | xvfb libx11-dev libegl1-mesa-dev libgles2-mesa-dev \ 10 | && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 11 | WORKDIR /app 12 | COPY . . 13 | CMD [ "sh", "-c", "./tests/test-docker.sh" ] 14 | -------------------------------------------------------------------------------- /clipboard_nocgo.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !cgo 2 | 3 | package clipboard 4 | 5 | import "context" 6 | 7 | func initialize() error { 8 | return errNoCgo 9 | } 10 | 11 | func read(t Format) (buf []byte, err error) { 12 | panic("clipboard: cannot use when CGO_ENABLED=0") 13 | } 14 | 15 | func readc(t string) ([]byte, error) { 16 | panic("clipboard: cannot use when CGO_ENABLED=0") 17 | } 18 | 19 | func write(t Format, buf []byte) (<-chan struct{}, error) { 20 | panic("clipboard: cannot use when CGO_ENABLED=0") 21 | } 22 | 23 | func watch(ctx context.Context, t Format) <-chan []byte { 24 | panic("clipboard: cannot use when CGO_ENABLED=0") 25 | } 26 | -------------------------------------------------------------------------------- /clipboard_ios.m: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | //go:build ios 8 | 9 | #import 10 | #import 11 | 12 | void clipboard_write_string(char *s) { 13 | NSString *value = [NSString stringWithUTF8String:s]; 14 | [[UIPasteboard generalPasteboard] setString:value]; 15 | } 16 | 17 | char *clipboard_read_string() { 18 | NSString *str = [[UIPasteboard generalPasteboard] string]; 19 | return (char *)[str UTF8String]; 20 | } 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 h1:Wdx0vgH5Wgsw+lF//LJKmWOJBLWX6nprsMqnf99rYDE= 2 | golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8= 3 | golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= 4 | golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= 5 | golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f h1:/n+PL2HlfqeSiDCuhdBbRNlGS/g2fM4OHufalHaTVG8= 6 | golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mTVAhA48LpfW/8fNR0ifStlH2axyfg= 7 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 8 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [changkun] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Changkun Ou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | //go:build cgo 8 | 9 | package clipboard_test 10 | 11 | import ( 12 | "context" 13 | "fmt" 14 | "time" 15 | 16 | "golang.design/x/clipboard" 17 | ) 18 | 19 | func ExampleWrite() { 20 | err := clipboard.Init() 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | clipboard.Write(clipboard.FmtText, []byte("Hello, 世界")) 26 | // Output: 27 | } 28 | 29 | func ExampleRead() { 30 | err := clipboard.Init() 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | fmt.Println(string(clipboard.Read(clipboard.FmtText))) 36 | // Output: 37 | // Hello, 世界 38 | } 39 | 40 | func ExampleWatch() { 41 | err := clipboard.Init() 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) 47 | defer cancel() 48 | 49 | changed := clipboard.Watch(context.Background(), clipboard.FmtText) 50 | go func(ctx context.Context) { 51 | clipboard.Write(clipboard.FmtText, []byte("你好,world")) 52 | }(ctx) 53 | fmt.Println(string(<-changed)) 54 | // Output: 55 | // 你好,world 56 | } 57 | -------------------------------------------------------------------------------- /cmd/gclip-gui/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /cmd/gclip/README.md: -------------------------------------------------------------------------------- 1 | # gclip 2 | 3 | `gclip` command offers the ability to interact with the system clipboard 4 | from the shell. To install: 5 | 6 | ```bash 7 | $ go install golang.design/x/clipboard/cmd/gclip@latest 8 | ``` 9 | 10 | ```bash 11 | $ gclip 12 | gclip is a command that provides clipboard interaction. 13 | usage: gclip [-copy|-paste] [-f ] 14 | options: 15 | -copy 16 | copy data to clipboard 17 | -f string 18 | source or destination to a given file path 19 | -paste 20 | paste data from clipboard 21 | examples: 22 | gclip -paste paste from clipboard and prints the content 23 | gclip -paste -f x.txt paste from clipboard and save as text to x.txt 24 | gclip -paste -f x.png paste from clipboard and save as image to x.png 25 | cat x.txt | gclip -copy copy content from x.txt to clipboard 26 | gclip -copy -f x.txt copy content from x.txt to clipboard 27 | gclip -copy -f x.png copy x.png as image data to clipboard 28 | ``` 29 | 30 | If `-copy` is used, the command will exit when the data is no longer 31 | available from the clipboard. You can always send the command to the 32 | background using a shell `&` operator, for example: 33 | 34 | ```bash 35 | $ cat x.txt | gclip -copy & 36 | ``` 37 | 38 | ## License 39 | 40 | MIT | © 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de). -------------------------------------------------------------------------------- /cmd/gclip-gui/README.md: -------------------------------------------------------------------------------- 1 | # gclip-gui 2 | 3 | This is a very basic example for verification purpose that demonstrates 4 | how the [golang.design/x/clipboard](https://golang.design/x/clipboard) 5 | can interact with macOS/Linux/Windows/Android/iOS system clipboard. 6 | 7 | The gclip GUI application writes a string to the system clipboard 8 | periodically then reads it back and renders it if possible. 9 | 10 | Because of the system limitation, on mobile devices, only string data is 11 | supported at the moment. Hence, one must use clipboard.FmtText. Other supplied 12 | formats result in a panic. 13 | 14 | This example is intentded as cross platform application. To build it, one 15 | must use [gomobile](https://golang.org/x/mobile). You may follow the instructions 16 | provided in the [GoMobile wiki](https://github.com/golang/go/wiki/Mobile) page. 17 | 18 | 19 | - For desktop: `go build -o gclip-gui` 20 | - For Android: `gomobile build -v -target=android -o gclip-gui.apk` 21 | - For iOS: `gomobile build -v -target=ios -bundleid design.golang.gclip-gui.app` 22 | 23 | ## Screenshots 24 | 25 | | macOS | iOS | Windows | Android | Linux | 26 | |:-----:|:---:|:-------:|:-------:|:-----:| 27 | |![](../../tests/testdata/darwin.png)|![](../../tests/testdata/ios.png)|![](../../tests/testdata/windows.png)|![](../../tests/testdata/android.png)|![](../../tests/testdata/linux.png)| 28 | 29 | ## License 30 | 31 | MIT | © 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de). -------------------------------------------------------------------------------- /clipboard_ios.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | //go:build ios 8 | 9 | package clipboard 10 | 11 | /* 12 | #cgo CFLAGS: -x objective-c 13 | #cgo LDFLAGS: -framework Foundation -framework UIKit -framework MobileCoreServices 14 | 15 | #import 16 | void clipboard_write_string(char *s); 17 | char *clipboard_read_string(); 18 | */ 19 | import "C" 20 | import ( 21 | "bytes" 22 | "context" 23 | "time" 24 | "unsafe" 25 | ) 26 | 27 | func initialize() error { return nil } 28 | 29 | func read(t Format) (buf []byte, err error) { 30 | switch t { 31 | case FmtText: 32 | return []byte(C.GoString(C.clipboard_read_string())), nil 33 | case FmtImage: 34 | return nil, errUnsupported 35 | default: 36 | return nil, errUnsupported 37 | } 38 | } 39 | 40 | // SetContent sets the clipboard content for iOS 41 | func write(t Format, buf []byte) (<-chan struct{}, error) { 42 | done := make(chan struct{}, 1) 43 | switch t { 44 | case FmtText: 45 | cs := C.CString(string(buf)) 46 | defer C.free(unsafe.Pointer(cs)) 47 | 48 | C.clipboard_write_string(cs) 49 | return done, nil 50 | case FmtImage: 51 | return nil, errUnsupported 52 | default: 53 | return nil, errUnsupported 54 | } 55 | } 56 | 57 | func watch(ctx context.Context, t Format) <-chan []byte { 58 | recv := make(chan []byte, 1) 59 | ti := time.NewTicker(time.Second) 60 | last := Read(t) 61 | go func() { 62 | for { 63 | select { 64 | case <-ctx.Done(): 65 | close(recv) 66 | return 67 | case <-ti.C: 68 | b := Read(t) 69 | if b == nil { 70 | continue 71 | } 72 | if bytes.Compare(last, b) != 0 { 73 | recv <- b 74 | last = b 75 | } 76 | } 77 | } 78 | }() 79 | return recv 80 | } 81 | -------------------------------------------------------------------------------- /clipboard_darwin.m: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | //go:build darwin && !ios 8 | 9 | // Interact with NSPasteboard using Objective-C 10 | // https://developer.apple.com/documentation/appkit/nspasteboard?language=objc 11 | 12 | #import 13 | #import 14 | 15 | unsigned int clipboard_read_string(void **out) { 16 | NSPasteboard * pasteboard = [NSPasteboard generalPasteboard]; 17 | NSData *data = [pasteboard dataForType:NSPasteboardTypeString]; 18 | if (data == nil) { 19 | return 0; 20 | } 21 | NSUInteger siz = [data length]; 22 | *out = malloc(siz); 23 | [data getBytes: *out length: siz]; 24 | return siz; 25 | } 26 | 27 | unsigned int clipboard_read_image(void **out) { 28 | NSPasteboard * pasteboard = [NSPasteboard generalPasteboard]; 29 | NSData *data = [pasteboard dataForType:NSPasteboardTypePNG]; 30 | if (data == nil) { 31 | return 0; 32 | } 33 | NSUInteger siz = [data length]; 34 | *out = malloc(siz); 35 | [data getBytes: *out length: siz]; 36 | return siz; 37 | } 38 | 39 | int clipboard_write_string(const void *bytes, NSInteger n) { 40 | NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; 41 | NSData *data = [NSData dataWithBytes: bytes length: n]; 42 | [pasteboard clearContents]; 43 | BOOL ok = [pasteboard setData: data forType:NSPasteboardTypeString]; 44 | if (!ok) { 45 | return -1; 46 | } 47 | return 0; 48 | } 49 | int clipboard_write_image(const void *bytes, NSInteger n) { 50 | NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; 51 | NSData *data = [NSData dataWithBytes: bytes length: n]; 52 | [pasteboard clearContents]; 53 | BOOL ok = [pasteboard setData: data forType:NSPasteboardTypePNG]; 54 | if (!ok) { 55 | return -1; 56 | } 57 | return 0; 58 | } 59 | 60 | NSInteger clipboard_change_count() { 61 | return [[NSPasteboard generalPasteboard] changeCount]; 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/clipboard.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The golang.design Initiative Authors. 2 | # All rights reserved. Use of this source code is governed 3 | # by a MIT license that can be found in the LICENSE file. 4 | # 5 | # Written by Changkun Ou 6 | 7 | name: clipboard 8 | 9 | on: 10 | push: 11 | branches: [ main ] 12 | pull_request: 13 | branches: [ main ] 14 | 15 | jobs: 16 | platform_test: 17 | env: 18 | DISPLAY: ':0.0' 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | os: [ubuntu-latest, macos-latest, windows-latest] 24 | go: ['1.24.x'] 25 | steps: 26 | - name: Install and run dependencies (xvfb libx11-dev) 27 | if: ${{ runner.os == 'Linux' }} 28 | run: | 29 | sudo apt update 30 | sudo apt install -y xvfb libx11-dev x11-utils libegl1-mesa-dev libgles2-mesa-dev 31 | Xvfb :0 -screen 0 1024x768x24 > /dev/null 2>&1 & 32 | # Wait for Xvfb 33 | MAX_ATTEMPTS=120 # About 60 seconds 34 | COUNT=0 35 | echo -n "Waiting for Xvfb to be ready..." 36 | while ! xdpyinfo -display "${DISPLAY}" >/dev/null 2>&1; do 37 | echo -n "." 38 | sleep 0.50s 39 | COUNT=$(( COUNT + 1 )) 40 | if [ "${COUNT}" -ge "${MAX_ATTEMPTS}" ]; then 41 | echo " Gave up waiting for X server on ${DISPLAY}" 42 | exit 1 43 | fi 44 | done 45 | echo "Done - Xvfb is ready!" 46 | 47 | - uses: actions/checkout@v2 48 | - uses: actions/setup-go@v2 49 | with: 50 | stable: 'false' 51 | go-version: ${{ matrix.go }} 52 | 53 | - name: Build (${{ matrix.go }}) 54 | run: | 55 | go build -o gclip cmd/gclip/main.go 56 | go build -o gclip-gui cmd/gclip-gui/main.go 57 | 58 | - name: Run Tests with CGO_ENABLED=1 (${{ matrix.go }}) 59 | if: ${{ runner.os == 'Linux' || runner.os == 'macOS'}} 60 | run: | 61 | CGO_ENABLED=1 go test -v -covermode=atomic . 62 | 63 | - name: Run Tests with CGO_ENABLED=0 (${{ matrix.go }}) 64 | if: ${{ runner.os == 'Linux' || runner.os == 'macOS'}} 65 | run: | 66 | CGO_ENABLED=0 go test -v -covermode=atomic . 67 | 68 | - name: Run Tests on Windows (${{ matrix.go }}) 69 | if: ${{ runner.os == 'Windows'}} 70 | run: | 71 | go test -v -covermode=atomic . -------------------------------------------------------------------------------- /clipboard_android.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | //go:build android 8 | 9 | package clipboard 10 | 11 | /* 12 | #cgo LDFLAGS: -landroid -llog 13 | 14 | #include 15 | char *clipboard_read_string(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx); 16 | void clipboard_write_string(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *str); 17 | 18 | */ 19 | import "C" 20 | import ( 21 | "bytes" 22 | "context" 23 | "time" 24 | "unsafe" 25 | 26 | "golang.org/x/mobile/app" 27 | ) 28 | 29 | func initialize() error { return nil } 30 | 31 | func read(t Format) (buf []byte, err error) { 32 | switch t { 33 | case FmtText: 34 | s := "" 35 | if err := app.RunOnJVM(func(vm, env, ctx uintptr) error { 36 | cs := C.clipboard_read_string(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx)) 37 | if cs == nil { 38 | return nil 39 | } 40 | 41 | s = C.GoString(cs) 42 | C.free(unsafe.Pointer(cs)) 43 | return nil 44 | }); err != nil { 45 | return nil, err 46 | } 47 | return []byte(s), nil 48 | case FmtImage: 49 | return nil, errUnsupported 50 | default: 51 | return nil, errUnsupported 52 | } 53 | } 54 | 55 | // write writes the given data to clipboard and 56 | // returns true if success or false if failed. 57 | func write(t Format, buf []byte) (<-chan struct{}, error) { 58 | done := make(chan struct{}, 1) 59 | switch t { 60 | case FmtText: 61 | cs := C.CString(string(buf)) 62 | defer C.free(unsafe.Pointer(cs)) 63 | 64 | if err := app.RunOnJVM(func(vm, env, ctx uintptr) error { 65 | C.clipboard_write_string(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), cs) 66 | done <- struct{}{} 67 | return nil 68 | }); err != nil { 69 | return nil, err 70 | } 71 | return done, nil 72 | case FmtImage: 73 | return nil, errUnsupported 74 | default: 75 | return nil, errUnsupported 76 | } 77 | } 78 | 79 | func watch(ctx context.Context, t Format) <-chan []byte { 80 | recv := make(chan []byte, 1) 81 | ti := time.NewTicker(time.Second) 82 | last := Read(t) 83 | go func() { 84 | for { 85 | select { 86 | case <-ctx.Done(): 87 | close(recv) 88 | return 89 | case <-ti.C: 90 | b := Read(t) 91 | if b == nil { 92 | continue 93 | } 94 | if bytes.Compare(last, b) != 0 { 95 | recv <- b 96 | last = b 97 | } 98 | } 99 | } 100 | }() 101 | return recv 102 | } 103 | -------------------------------------------------------------------------------- /clipboard_android.c: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | //go:build android 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, \ 15 | "GOLANG.DESIGN/X/CLIPBOARD", __VA_ARGS__) 16 | 17 | static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) { 18 | jmethodID m = (*env)->GetMethodID(env, clazz, name, sig); 19 | if (m == 0) { 20 | (*env)->ExceptionClear(env); 21 | LOG_FATAL("cannot find method %s %s", name, sig); 22 | return 0; 23 | } 24 | return m; 25 | } 26 | 27 | jobject get_clipboard(uintptr_t jni_env, uintptr_t ctx) { 28 | JNIEnv *env = (JNIEnv*)jni_env; 29 | jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx); 30 | jmethodID getSystemService = find_method(env, ctxClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); 31 | 32 | jstring service = (*env)->NewStringUTF(env, "clipboard"); 33 | jobject ret = (jobject)(*env)->CallObjectMethod(env, (jobject)ctx, getSystemService, service); 34 | jthrowable err = (*env)->ExceptionOccurred(env); 35 | 36 | if (err != NULL) { 37 | LOG_FATAL("cannot find clipboard"); 38 | (*env)->ExceptionClear(env); 39 | return NULL; 40 | } 41 | return ret; 42 | } 43 | 44 | char *clipboard_read_string(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) { 45 | JNIEnv *env = (JNIEnv*)jni_env; 46 | jobject mgr = get_clipboard(jni_env, ctx); 47 | if (mgr == NULL) { 48 | return NULL; 49 | } 50 | 51 | jclass mgrClass = (*env)->GetObjectClass(env, mgr); 52 | jmethodID getText = find_method(env, mgrClass, "getText", "()Ljava/lang/CharSequence;"); 53 | 54 | jobject content = (jstring)(*env)->CallObjectMethod(env, mgr, getText); 55 | if (content == NULL) { 56 | return NULL; 57 | } 58 | 59 | jclass clzCharSequence = (*env)->GetObjectClass(env, content); 60 | jmethodID toString = (*env)->GetMethodID(env, clzCharSequence, "toString", "()Ljava/lang/String;"); 61 | jobject s = (*env)->CallObjectMethod(env, content, toString); 62 | 63 | const char *chars = (*env)->GetStringUTFChars(env, s, NULL); 64 | char *copy = strdup(chars); 65 | (*env)->ReleaseStringUTFChars(env, s, chars); 66 | return copy; 67 | } 68 | 69 | void clipboard_write_string(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *str) { 70 | JNIEnv *env = (JNIEnv*)jni_env; 71 | jobject mgr = get_clipboard(jni_env, ctx); 72 | if (mgr == NULL) { 73 | return; 74 | } 75 | 76 | jclass mgrClass = (*env)->GetObjectClass(env, mgr); 77 | jmethodID setText = find_method(env, mgrClass, "setText", "(Ljava/lang/CharSequence;)V"); 78 | 79 | (*env)->CallVoidMethod(env, mgr, setText, (*env)->NewStringUTF(env, str)); 80 | } 81 | -------------------------------------------------------------------------------- /cmd/gclip/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | package main // go install golang.design/x/clipboard/cmd/gclip@latest 8 | 9 | import ( 10 | "flag" 11 | "fmt" 12 | "io" 13 | "os" 14 | "path/filepath" 15 | 16 | "golang.design/x/clipboard" 17 | ) 18 | 19 | func usage() { 20 | fmt.Fprintf(os.Stderr, `gclip is a command that provides clipboard interaction. 21 | 22 | usage: gclip [-copy|-paste] [-f ] 23 | 24 | options: 25 | `) 26 | flag.PrintDefaults() 27 | fmt.Fprintf(os.Stderr, ` 28 | examples: 29 | gclip -paste paste from clipboard and prints the content 30 | gclip -paste -f x.txt paste from clipboard and save as text to x.txt 31 | gclip -paste -f x.png paste from clipboard and save as image to x.png 32 | 33 | cat x.txt | gclip -copy copy content from x.txt to clipboard 34 | gclip -copy -f x.txt copy content from x.txt to clipboard 35 | gclip -copy -f x.png copy x.png as image data to clipboard 36 | `) 37 | os.Exit(2) 38 | } 39 | 40 | var ( 41 | in = flag.Bool("copy", false, "copy data to clipboard") 42 | out = flag.Bool("paste", false, "paste data from clipboard") 43 | file = flag.String("f", "", "source or destination to a given file path") 44 | ) 45 | 46 | func init() { 47 | err := clipboard.Init() 48 | if err != nil { 49 | panic(err) 50 | } 51 | } 52 | 53 | func main() { 54 | flag.Usage = usage 55 | flag.Parse() 56 | if *out { 57 | if err := pst(); err != nil { 58 | usage() 59 | } 60 | return 61 | } 62 | if *in { 63 | if err := cpy(); err != nil { 64 | usage() 65 | } 66 | return 67 | } 68 | usage() 69 | } 70 | 71 | func cpy() error { 72 | t := clipboard.FmtText 73 | ext := filepath.Ext(*file) 74 | 75 | switch ext { 76 | case ".png": 77 | t = clipboard.FmtImage 78 | case ".txt": 79 | fallthrough 80 | default: 81 | t = clipboard.FmtText 82 | } 83 | 84 | var ( 85 | b []byte 86 | err error 87 | ) 88 | if *file != "" { 89 | b, err = os.ReadFile(*file) 90 | if err != nil { 91 | fmt.Fprintf(os.Stderr, "failed to read given file: %v", err) 92 | return err 93 | } 94 | } else { 95 | b, err = io.ReadAll(os.Stdin) 96 | if err != nil { 97 | fmt.Fprintf(os.Stderr, "failed to read from stdin: %v", err) 98 | return err 99 | } 100 | } 101 | 102 | // Wait until clipboard content has been changed. 103 | <-clipboard.Write(t, b) 104 | return nil 105 | } 106 | 107 | func pst() (err error) { 108 | var b []byte 109 | 110 | b = clipboard.Read(clipboard.FmtText) 111 | if b == nil { 112 | b = clipboard.Read(clipboard.FmtImage) 113 | } 114 | 115 | if *file != "" && b != nil { 116 | err = os.WriteFile(*file, b, os.ModePerm) 117 | if err != nil { 118 | fmt.Fprintf(os.Stderr, "failed to write data to file %s: %v", *file, err) 119 | } 120 | return err 121 | } 122 | 123 | for len(b) > 0 { 124 | n, err := os.Stdout.Write(b) 125 | if err != nil { 126 | return err 127 | } 128 | b = b[n:] 129 | } 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /clipboard_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | //go:build darwin && !ios 8 | 9 | package clipboard 10 | 11 | /* 12 | #cgo CFLAGS: -x objective-c 13 | #cgo LDFLAGS: -framework Foundation -framework Cocoa 14 | #import 15 | #import 16 | 17 | unsigned int clipboard_read_string(void **out); 18 | unsigned int clipboard_read_image(void **out); 19 | int clipboard_write_string(const void *bytes, NSInteger n); 20 | int clipboard_write_image(const void *bytes, NSInteger n); 21 | NSInteger clipboard_change_count(); 22 | */ 23 | import "C" 24 | import ( 25 | "context" 26 | "time" 27 | "unsafe" 28 | ) 29 | 30 | func initialize() error { return nil } 31 | 32 | func read(t Format) (buf []byte, err error) { 33 | var ( 34 | data unsafe.Pointer 35 | n C.uint 36 | ) 37 | switch t { 38 | case FmtText: 39 | n = C.clipboard_read_string(&data) 40 | case FmtImage: 41 | n = C.clipboard_read_image(&data) 42 | } 43 | if data == nil { 44 | return nil, errUnavailable 45 | } 46 | defer C.free(unsafe.Pointer(data)) 47 | if n == 0 { 48 | return nil, nil 49 | } 50 | return C.GoBytes(data, C.int(n)), nil 51 | } 52 | 53 | // write writes the given data to clipboard and 54 | // returns true if success or false if failed. 55 | func write(t Format, buf []byte) (<-chan struct{}, error) { 56 | var ok C.int 57 | switch t { 58 | case FmtText: 59 | if len(buf) == 0 { 60 | ok = C.clipboard_write_string(unsafe.Pointer(nil), 0) 61 | } else { 62 | ok = C.clipboard_write_string(unsafe.Pointer(&buf[0]), 63 | C.NSInteger(len(buf))) 64 | } 65 | case FmtImage: 66 | if len(buf) == 0 { 67 | ok = C.clipboard_write_image(unsafe.Pointer(nil), 0) 68 | } else { 69 | ok = C.clipboard_write_image(unsafe.Pointer(&buf[0]), 70 | C.NSInteger(len(buf))) 71 | } 72 | default: 73 | return nil, errUnsupported 74 | } 75 | if ok != 0 { 76 | return nil, errUnavailable 77 | } 78 | 79 | // use unbuffered data to prevent goroutine leak 80 | changed := make(chan struct{}, 1) 81 | cnt := C.long(C.clipboard_change_count()) 82 | go func() { 83 | for { 84 | // not sure if we are too slow or the user too fast :) 85 | time.Sleep(time.Second) 86 | cur := C.long(C.clipboard_change_count()) 87 | if cnt != cur { 88 | changed <- struct{}{} 89 | close(changed) 90 | return 91 | } 92 | } 93 | }() 94 | return changed, nil 95 | } 96 | 97 | func watch(ctx context.Context, t Format) <-chan []byte { 98 | recv := make(chan []byte, 1) 99 | // not sure if we are too slow or the user too fast :) 100 | ti := time.NewTicker(time.Second) 101 | lastCount := C.long(C.clipboard_change_count()) 102 | go func() { 103 | for { 104 | select { 105 | case <-ctx.Done(): 106 | close(recv) 107 | return 108 | case <-ti.C: 109 | this := C.long(C.clipboard_change_count()) 110 | if lastCount != this { 111 | b := Read(t) 112 | if b == nil { 113 | continue 114 | } 115 | recv <- b 116 | lastCount = this 117 | } 118 | } 119 | } 120 | }() 121 | return recv 122 | } 123 | -------------------------------------------------------------------------------- /clipboard_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | //go:build linux && !android 8 | 9 | package clipboard 10 | 11 | /* 12 | #cgo LDFLAGS: -ldl 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | int clipboard_test(); 19 | int clipboard_write( 20 | char* typ, 21 | unsigned char* buf, 22 | size_t n, 23 | uintptr_t handle 24 | ); 25 | unsigned long clipboard_read(char* typ, char **out); 26 | */ 27 | import "C" 28 | import ( 29 | "bytes" 30 | "context" 31 | "fmt" 32 | "os" 33 | "runtime" 34 | "runtime/cgo" 35 | "time" 36 | "unsafe" 37 | ) 38 | 39 | var helpmsg = `%w: Failed to initialize the X11 display, and the clipboard package 40 | will not work properly. Install the following dependency may help: 41 | 42 | apt install -y libx11-dev 43 | 44 | If the clipboard package is in an environment without a frame buffer, 45 | such as a cloud server, it may also be necessary to install xvfb: 46 | 47 | apt install -y xvfb 48 | 49 | and initialize a virtual frame buffer: 50 | 51 | Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 52 | export DISPLAY=:99.0 53 | 54 | Then this package should be ready to use. 55 | ` 56 | 57 | func initialize() error { 58 | ok := C.clipboard_test() 59 | if ok != 0 { 60 | return fmt.Errorf(helpmsg, errUnavailable) 61 | } 62 | return nil 63 | } 64 | 65 | func read(t Format) (buf []byte, err error) { 66 | switch t { 67 | case FmtText: 68 | return readc("UTF8_STRING") 69 | case FmtImage: 70 | return readc("image/png") 71 | } 72 | return nil, errUnsupported 73 | } 74 | 75 | func readc(t string) ([]byte, error) { 76 | ct := C.CString(t) 77 | defer C.free(unsafe.Pointer(ct)) 78 | 79 | var data *C.char 80 | n := C.clipboard_read(ct, &data) 81 | switch C.long(n) { 82 | case -1: 83 | return nil, errUnavailable 84 | case -2: 85 | return nil, errUnsupported 86 | } 87 | if data == nil { 88 | return nil, errUnavailable 89 | } 90 | defer C.free(unsafe.Pointer(data)) 91 | switch { 92 | case n == 0: 93 | return nil, nil 94 | default: 95 | return C.GoBytes(unsafe.Pointer(data), C.int(n)), nil 96 | } 97 | } 98 | 99 | // write writes the given data to clipboard and 100 | // returns true if success or false if failed. 101 | func write(t Format, buf []byte) (<-chan struct{}, error) { 102 | var s string 103 | switch t { 104 | case FmtText: 105 | s = "UTF8_STRING" 106 | case FmtImage: 107 | s = "image/png" 108 | } 109 | 110 | start := make(chan int) 111 | done := make(chan struct{}, 1) 112 | 113 | go func() { // serve as a daemon until the ownership is terminated. 114 | runtime.LockOSThread() 115 | defer runtime.UnlockOSThread() 116 | 117 | cs := C.CString(s) 118 | defer C.free(unsafe.Pointer(cs)) 119 | 120 | h := cgo.NewHandle(start) 121 | var ok C.int 122 | if len(buf) == 0 { 123 | ok = C.clipboard_write(cs, nil, 0, C.uintptr_t(h)) 124 | } else { 125 | ok = C.clipboard_write(cs, (*C.uchar)(unsafe.Pointer(&(buf[0]))), C.size_t(len(buf)), C.uintptr_t(h)) 126 | } 127 | if ok != C.int(0) { 128 | fmt.Fprintf(os.Stderr, "write failed with status: %d\n", int(ok)) 129 | } 130 | done <- struct{}{} 131 | close(done) 132 | }() 133 | 134 | status := <-start 135 | if status < 0 { 136 | return nil, errUnavailable 137 | } 138 | // wait until enter event loop 139 | return done, nil 140 | } 141 | 142 | func watch(ctx context.Context, t Format) <-chan []byte { 143 | recv := make(chan []byte, 1) 144 | ti := time.NewTicker(time.Second) 145 | last := Read(t) 146 | go func() { 147 | for { 148 | select { 149 | case <-ctx.Done(): 150 | close(recv) 151 | return 152 | case <-ti.C: 153 | b := Read(t) 154 | if b == nil { 155 | continue 156 | } 157 | if !bytes.Equal(last, b) { 158 | recv <- b 159 | last = b 160 | } 161 | } 162 | } 163 | }() 164 | return recv 165 | } 166 | 167 | //export syncStatus 168 | func syncStatus(h uintptr, val int) { 169 | v := cgo.Handle(h).Value().(chan int) 170 | v <- val 171 | cgo.Handle(h).Delete() 172 | } 173 | -------------------------------------------------------------------------------- /clipboard.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | /* 8 | Package clipboard provides cross platform clipboard access and supports 9 | macOS/Linux/Windows/Android/iOS platform. Before interacting with the 10 | clipboard, one must call Init to assert if it is possible to use this 11 | package: 12 | 13 | err := clipboard.Init() 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | The most common operations are `Read` and `Write`. To use them: 19 | 20 | // write/read text format data of the clipboard, and 21 | // the byte buffer regarding the text are UTF8 encoded. 22 | clipboard.Write(clipboard.FmtText, []byte("text data")) 23 | clipboard.Read(clipboard.FmtText) 24 | 25 | // write/read image format data of the clipboard, and 26 | // the byte buffer regarding the image are PNG encoded. 27 | clipboard.Write(clipboard.FmtImage, []byte("image data")) 28 | clipboard.Read(clipboard.FmtImage) 29 | 30 | Note that read/write regarding image format assumes that the bytes are 31 | PNG encoded since it serves the alpha blending purpose that might be 32 | used in other graphical software. 33 | 34 | In addition, `clipboard.Write` returns a channel that can receive an 35 | empty struct as a signal, which indicates the corresponding write call 36 | to the clipboard is outdated, meaning the clipboard has been overwritten 37 | by others and the previously written data is lost. For instance: 38 | 39 | changed := clipboard.Write(clipboard.FmtText, []byte("text data")) 40 | 41 | select { 42 | case <-changed: 43 | println(`"text data" is no longer available from clipboard.`) 44 | } 45 | 46 | You can ignore the returning channel if you don't need this type of 47 | notification. Furthermore, when you need more than just knowing whether 48 | clipboard data is changed, use the watcher API: 49 | 50 | ch := clipboard.Watch(context.TODO(), clipboard.FmtText) 51 | for data := range ch { 52 | // print out clipboard data whenever it is changed 53 | println(string(data)) 54 | } 55 | */ 56 | package clipboard // import "golang.design/x/clipboard" 57 | 58 | import ( 59 | "context" 60 | "errors" 61 | "fmt" 62 | "os" 63 | "sync" 64 | ) 65 | 66 | var ( 67 | // activate only for running tests. 68 | debug = false 69 | errUnavailable = errors.New("clipboard unavailable") 70 | errUnsupported = errors.New("unsupported format") 71 | errNoCgo = errors.New("clipboard: cannot use when CGO_ENABLED=0") 72 | ) 73 | 74 | // Format represents the format of clipboard data. 75 | type Format int 76 | 77 | // All sorts of supported clipboard data 78 | const ( 79 | // FmtText indicates plain text clipboard format 80 | FmtText Format = iota 81 | // FmtImage indicates image/png clipboard format 82 | FmtImage 83 | ) 84 | 85 | var ( 86 | // Due to the limitation on operating systems (such as darwin), 87 | // concurrent read can even cause panic, use a global lock to 88 | // guarantee one read at a time. 89 | lock = sync.Mutex{} 90 | initOnce sync.Once 91 | initError error 92 | ) 93 | 94 | // Init initializes the clipboard package. It returns an error 95 | // if the clipboard is not available to use. This may happen if the 96 | // target system lacks required dependency, such as libx11-dev in X11 97 | // environment. For example, 98 | // 99 | // err := clipboard.Init() 100 | // if err != nil { 101 | // panic(err) 102 | // } 103 | // 104 | // If Init returns an error, any subsequent Read/Write/Watch call 105 | // may result in an unrecoverable panic. 106 | func Init() error { 107 | initOnce.Do(func() { 108 | initError = initialize() 109 | }) 110 | return initError 111 | } 112 | 113 | // Read returns a chunk of bytes of the clipboard data if it presents 114 | // in the desired format t presents. Otherwise, it returns nil. 115 | func Read(t Format) []byte { 116 | lock.Lock() 117 | defer lock.Unlock() 118 | 119 | buf, err := read(t) 120 | if err != nil { 121 | if debug { 122 | fmt.Fprintf(os.Stderr, "read clipboard err: %v\n", err) 123 | } 124 | return nil 125 | } 126 | return buf 127 | } 128 | 129 | // Write writes a given buffer to the clipboard in a specified format. 130 | // Write returned a receive-only channel can receive an empty struct 131 | // as a signal, which indicates the clipboard has been overwritten from 132 | // this write. 133 | // If format t indicates an image, then the given buf assumes 134 | // the image data is PNG encoded. 135 | func Write(t Format, buf []byte) <-chan struct{} { 136 | lock.Lock() 137 | defer lock.Unlock() 138 | 139 | changed, err := write(t, buf) 140 | if err != nil { 141 | if debug { 142 | fmt.Fprintf(os.Stderr, "write to clipboard err: %v\n", err) 143 | } 144 | return nil 145 | } 146 | return changed 147 | } 148 | 149 | // Watch returns a receive-only channel that received the clipboard data 150 | // whenever any change of clipboard data in the desired format happens. 151 | // 152 | // The returned channel will be closed if the given context is canceled. 153 | func Watch(ctx context.Context, t Format) <-chan []byte { 154 | return watch(ctx, t) 155 | } 156 | -------------------------------------------------------------------------------- /cmd/gclip-gui/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | //go:build android || ios || linux || darwin || windows 8 | 9 | // This is a very basic example for verification purpose that 10 | // demonstrates how the golang.design/x/clipboard can interact 11 | // with macOS/Linux/Windows/Android/iOS system clipboard. 12 | // 13 | // The gclip GUI application writes a string to the system clipboard 14 | // periodically then reads it back and renders it if possible. 15 | // 16 | // Because of the system limitation, on mobile devices, only string 17 | // data is supported at the moment. Hence, one must use clipboard.FmtText. 18 | // Other supplied formats result in a panic. 19 | // 20 | // This example is intentded as cross platform application. 21 | // To build it, one must use gomobile (https://golang.org/x/mobile). 22 | // You may follow the instructions provided in the GoMobile's wiki page: 23 | // https://github.com/golang/go/wiki/Mobile. 24 | // 25 | // - For desktop: 26 | // 27 | // go build -o gclip-gui 28 | // 29 | // - For Android: 30 | // 31 | // gomobile build -v -target=android -o gclip-gui.apk 32 | // 33 | // - For iOS: 34 | // 35 | // gomobile build -v -target=ios -bundleid design.golang.gclip-gui.app 36 | // 37 | package main 38 | 39 | import ( 40 | "fmt" 41 | "image" 42 | "image/color" 43 | "log" 44 | "os" 45 | "sync" 46 | "time" 47 | 48 | "golang.design/x/clipboard" 49 | 50 | "golang.org/x/image/font" 51 | "golang.org/x/image/font/basicfont" 52 | "golang.org/x/image/math/fixed" 53 | "golang.org/x/mobile/app" 54 | "golang.org/x/mobile/event/lifecycle" 55 | "golang.org/x/mobile/event/paint" 56 | "golang.org/x/mobile/event/size" 57 | "golang.org/x/mobile/exp/gl/glutil" 58 | "golang.org/x/mobile/geom" 59 | "golang.org/x/mobile/gl" 60 | ) 61 | 62 | type Label struct { 63 | sz size.Event 64 | images *glutil.Images 65 | m *glutil.Image 66 | drawer *font.Drawer 67 | 68 | mu sync.Mutex 69 | data string 70 | } 71 | 72 | func NewLabel(images *glutil.Images) *Label { 73 | return &Label{ 74 | images: images, 75 | data: "Hello! Gclip.", 76 | drawer: nil, 77 | } 78 | } 79 | 80 | func (l *Label) SetLabel(s string) { 81 | l.mu.Lock() 82 | defer l.mu.Unlock() 83 | 84 | l.data = s 85 | } 86 | 87 | const ( 88 | lineWidth = 100 89 | lineHeight = 120 90 | ) 91 | 92 | func (l *Label) Draw(sz size.Event) { 93 | l.mu.Lock() 94 | s := l.data 95 | l.mu.Unlock() 96 | imgW, imgH := lineWidth*basicfont.Face7x13.Width, lineHeight*basicfont.Face7x13.Height 97 | if sz.WidthPx == 0 && sz.HeightPx == 0 { 98 | return 99 | } 100 | if imgW > sz.WidthPx { 101 | imgW = sz.WidthPx 102 | } 103 | 104 | if l.sz != sz { 105 | l.sz = sz 106 | if l.m != nil { 107 | l.m.Release() 108 | } 109 | l.m = l.images.NewImage(imgW, imgH) 110 | } 111 | // Clear the drawing image. 112 | for i := 0; i < len(l.m.RGBA.Pix); i++ { 113 | l.m.RGBA.Pix[i] = 0 114 | } 115 | 116 | l.drawer = &font.Drawer{ 117 | Dst: l.m.RGBA, 118 | Src: image.NewUniform(color.RGBA{0, 100, 125, 255}), 119 | Face: basicfont.Face7x13, 120 | Dot: fixed.P(5, 10), 121 | } 122 | l.drawer.DrawString(s) 123 | l.m.Upload() 124 | l.m.Draw( 125 | sz, 126 | geom.Point{X: 0, Y: 50}, 127 | geom.Point{X: geom.Pt(imgW), Y: 50}, 128 | geom.Point{X: 0, Y: geom.Pt(imgH)}, 129 | l.m.RGBA.Bounds(), 130 | ) 131 | } 132 | 133 | func (l *Label) Release() { 134 | if l.m != nil { 135 | l.m.Release() 136 | l.m = nil 137 | l.images = nil 138 | } 139 | } 140 | 141 | // GclipApp is the application instance. 142 | type GclipApp struct { 143 | app app.App 144 | 145 | ctx gl.Context 146 | siz size.Event 147 | 148 | images *glutil.Images 149 | l *Label 150 | 151 | counter int 152 | } 153 | 154 | // WatchClipboard watches the system clipboard every seconds. 155 | func (g *GclipApp) WatchClipboard() { 156 | go func() { 157 | tk := time.NewTicker(time.Second) 158 | for range tk.C { 159 | // Write something to the clipboard 160 | w := fmt.Sprintf("(gclip: %d)", g.counter) 161 | clipboard.Write(clipboard.FmtText, []byte(w)) 162 | g.counter++ 163 | log.Println(w) 164 | 165 | // Read it back and render it, if possible. 166 | data := clipboard.Read(clipboard.FmtText) 167 | if len(data) == 0 { 168 | continue 169 | } 170 | 171 | // Set the current clipboard data as label content and render on the screen. 172 | r := fmt.Sprintf("clipboard: %s", string(data)) 173 | g.l.SetLabel(r) 174 | g.app.Send(paint.Event{}) 175 | } 176 | }() 177 | } 178 | 179 | func (g *GclipApp) OnStart(e lifecycle.Event) { 180 | g.ctx, _ = e.DrawContext.(gl.Context) 181 | g.images = glutil.NewImages(g.ctx) 182 | g.l = NewLabel(g.images) 183 | g.app.Send(paint.Event{}) 184 | } 185 | 186 | func (g *GclipApp) OnStop() { 187 | g.l.Release() 188 | g.images.Release() 189 | g.ctx = nil 190 | } 191 | 192 | func (g *GclipApp) OnSize(size size.Event) { 193 | g.siz = size 194 | } 195 | 196 | func (g *GclipApp) OnDraw() { 197 | if g.ctx == nil { 198 | return 199 | } 200 | defer g.app.Send(paint.Event{}) 201 | defer g.app.Publish() 202 | g.ctx.ClearColor(0, 0, 0, 1) 203 | g.ctx.Clear(gl.COLOR_BUFFER_BIT) 204 | g.l.Draw(g.siz) 205 | } 206 | 207 | func init() { 208 | err := clipboard.Init() 209 | if err != nil { 210 | panic(err) 211 | } 212 | } 213 | 214 | func main() { 215 | app.Main(func(a app.App) { 216 | gclip := GclipApp{app: a} 217 | gclip.app.Send(size.Event{WidthPx: 800, HeightPx: 500}) 218 | gclip.WatchClipboard() 219 | for e := range gclip.app.Events() { 220 | switch e := gclip.app.Filter(e).(type) { 221 | case lifecycle.Event: 222 | switch e.Crosses(lifecycle.StageVisible) { 223 | case lifecycle.CrossOn: 224 | gclip.OnStart(e) 225 | case lifecycle.CrossOff: 226 | gclip.OnStop() 227 | os.Exit(0) 228 | } 229 | case size.Event: 230 | gclip.OnSize(e) 231 | case paint.Event: 232 | gclip.OnDraw() 233 | } 234 | } 235 | }) 236 | } 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | Cross platform (macOS/Linux/Windows/Android/iOS) clipboard package in Go 4 | 5 | ```go 6 | import "golang.design/x/clipboard" 7 | ``` 8 | 9 | ## Features 10 | 11 | - Cross platform supports: **macOS, Linux (X11), Windows, iOS, and Android** 12 | - Copy/paste UTF-8 text 13 | - Copy/paste PNG encoded images (Desktop-only) 14 | - Command `gclip` as a demo application 15 | - Mobile app `gclip-gui` as a demo application 16 | 17 | ## API Usage 18 | 19 | Package clipboard provides cross platform clipboard access and supports 20 | macOS/Linux/Windows/Android/iOS platform. Before interacting with the 21 | clipboard, one must call Init to assert if it is possible to use this 22 | package: 23 | 24 | ```go 25 | // Init returns an error if the package is not ready for use. 26 | err := clipboard.Init() 27 | if err != nil { 28 | panic(err) 29 | } 30 | ``` 31 | 32 | The most common operations are `Read` and `Write`. To use them: 33 | 34 | ```go 35 | // write/read text format data of the clipboard, and 36 | // the byte buffer regarding the text are UTF8 encoded. 37 | clipboard.Write(clipboard.FmtText, []byte("text data")) 38 | clipboard.Read(clipboard.FmtText) 39 | 40 | // write/read image format data of the clipboard, and 41 | // the byte buffer regarding the image are PNG encoded. 42 | clipboard.Write(clipboard.FmtImage, []byte("image data")) 43 | clipboard.Read(clipboard.FmtImage) 44 | ``` 45 | 46 | Note that read/write regarding image format assumes that the bytes are 47 | PNG encoded since it serves the alpha blending purpose that might be 48 | used in other graphical software. 49 | 50 | In addition, `clipboard.Write` returns a channel that can receive an 51 | empty struct as a signal, which indicates the corresponding write call 52 | to the clipboard is outdated, meaning the clipboard has been overwritten 53 | by others and the previously written data is lost. For instance: 54 | 55 | ```go 56 | changed := clipboard.Write(clipboard.FmtText, []byte("text data")) 57 | 58 | select { 59 | case <-changed: 60 | println(`"text data" is no longer available from clipboard.`) 61 | } 62 | ``` 63 | 64 | You can ignore the returning channel if you don't need this type of 65 | notification. Furthermore, when you need more than just knowing whether 66 | clipboard data is changed, use the watcher API: 67 | 68 | ```go 69 | ch := clipboard.Watch(context.TODO(), clipboard.FmtText) 70 | for data := range ch { 71 | // print out clipboard data whenever it is changed 72 | println(string(data)) 73 | } 74 | ``` 75 | 76 | ## Demos 77 | 78 | - A command line tool `gclip` for command line clipboard accesses, see document [here](./cmd/gclip/README.md). 79 | - A GUI application `gclip-gui` for functionality verifications on mobile systems, see a document [here](./cmd/gclip-gui/README.md). 80 | 81 | 82 | ## Command Usage 83 | 84 | `gclip` command offers the ability to interact with the system clipboard 85 | from the shell. To install: 86 | 87 | ```bash 88 | $ go install golang.design/x/clipboard/cmd/gclip@latest 89 | ``` 90 | 91 | ```bash 92 | $ gclip 93 | gclip is a command that provides clipboard interaction. 94 | 95 | usage: gclip [-copy|-paste] [-f ] 96 | 97 | options: 98 | -copy 99 | copy data to clipboard 100 | -f string 101 | source or destination to a given file path 102 | -paste 103 | paste data from clipboard 104 | 105 | examples: 106 | gclip -paste paste from clipboard and prints the content 107 | gclip -paste -f x.txt paste from clipboard and save as text to x.txt 108 | gclip -paste -f x.png paste from clipboard and save as image to x.png 109 | 110 | cat x.txt | gclip -copy copy content from x.txt to clipboard 111 | gclip -copy -f x.txt copy content from x.txt to clipboard 112 | gclip -copy -f x.png copy x.png as image data to clipboard 113 | ``` 114 | 115 | If `-copy` is used, the command will exit when the data is no longer 116 | available from the clipboard. You can always send the command to the 117 | background using a shell `&` operator, for example: 118 | 119 | ```bash 120 | $ cat x.txt | gclip -copy & 121 | ``` 122 | 123 | ## Platform Specific Details 124 | 125 | This package spent efforts to provide cross platform abstraction regarding 126 | accessing system clipboards, but here are a few details you might need to know. 127 | 128 | ### Dependency 129 | 130 | - macOS: require Cgo, no dependency 131 | - Linux: require X11 dev package. For instance, install `libx11-dev` or `xorg-dev` or `libX11-devel` to access X window system. 132 | Wayland sessions are currently unsupported; running under Wayland 133 | typically requires an XWayland bridge and `DISPLAY` to be set. 134 | - Windows: no Cgo, no dependency 135 | - iOS/Android: collaborate with [`gomobile`](https://golang.org/x/mobile) 136 | 137 | ### Screenshot 138 | 139 | In general, when you need test your implementation regarding images, 140 | There are system level shortcuts to put screenshot image into your system clipboard: 141 | 142 | - On macOS, use `Ctrl+Shift+Cmd+4` 143 | - On Linux/Ubuntu, use `Ctrl+Shift+PrintScreen` 144 | - On Windows, use `Shift+Win+s` 145 | 146 | As described in the API documentation, the package supports read/write 147 | UTF8 encoded plain text or PNG encoded image data. Thus, 148 | the other types of data are not supported yet, i.e. undefined behavior. 149 | 150 | ## Who is using this package? 151 | 152 | The main purpose of building this package is to support the 153 | [midgard](https://changkun.de/s/midgard) project, which offers 154 | clipboard-based features like universal clipboard service that syncs 155 | clipboard content across multiple systems, allocating public accessible 156 | for clipboard content, etc. 157 | 158 | To know more projects, check our [wiki](https://github.com/golang-design/clipboard/wiki) page. 159 | 160 | ## License 161 | 162 | MIT | © 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de). -------------------------------------------------------------------------------- /clipboard_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | package clipboard_test 8 | 9 | import ( 10 | "bytes" 11 | "context" 12 | "errors" 13 | "image/color" 14 | "image/png" 15 | "os" 16 | "reflect" 17 | "runtime" 18 | "testing" 19 | "time" 20 | 21 | "golang.design/x/clipboard" 22 | ) 23 | 24 | func init() { 25 | clipboard.Debug = true 26 | } 27 | 28 | func TestClipboardInit(t *testing.T) { 29 | t.Run("no-cgo", func(t *testing.T) { 30 | if val, ok := os.LookupEnv("CGO_ENABLED"); !ok || val != "0" { 31 | t.Skip("CGO_ENABLED is set to 1") 32 | } 33 | if runtime.GOOS == "windows" { 34 | t.Skip("Windows does not need to check for cgo") 35 | } 36 | 37 | if err := clipboard.Init(); !errors.Is(err, clipboard.ErrCgoDisabled) { 38 | t.Fatalf("expect ErrCgoDisabled, got: %v", err) 39 | } 40 | }) 41 | t.Run("with-cgo", func(t *testing.T) { 42 | if val, ok := os.LookupEnv("CGO_ENABLED"); ok && val == "0" { 43 | t.Skip("CGO_ENABLED is set to 0") 44 | } 45 | if runtime.GOOS != "linux" { 46 | t.Skip("Only Linux may return error at the moment.") 47 | } 48 | 49 | if err := clipboard.Init(); err != nil && !errors.Is(err, clipboard.ErrUnavailable) { 50 | t.Fatalf("expect ErrUnavailable, but got: %v", err) 51 | } 52 | }) 53 | } 54 | 55 | func TestClipboard(t *testing.T) { 56 | if runtime.GOOS != "windows" { 57 | if val, ok := os.LookupEnv("CGO_ENABLED"); ok && val == "0" { 58 | t.Skip("CGO_ENABLED is set to 0") 59 | } 60 | } 61 | 62 | t.Run("image", func(t *testing.T) { 63 | data, err := os.ReadFile("tests/testdata/clipboard.png") 64 | if err != nil { 65 | t.Fatalf("failed to read gold file: %v", err) 66 | } 67 | clipboard.Write(clipboard.FmtImage, data) 68 | 69 | b := clipboard.Read(clipboard.FmtText) 70 | if b != nil { 71 | t.Fatalf("read clipboard that stores image data as text should fail, but got len: %d", len(b)) 72 | } 73 | 74 | b = clipboard.Read(clipboard.FmtImage) 75 | if b == nil { 76 | t.Fatalf("read clipboard that stores image data as image should success, but got: nil") 77 | } 78 | 79 | img1, err := png.Decode(bytes.NewReader(data)) 80 | if err != nil { 81 | t.Fatalf("write image is not png encoded: %v", err) 82 | } 83 | img2, err := png.Decode(bytes.NewReader(b)) 84 | if err != nil { 85 | t.Fatalf("read image is not png encoded: %v", err) 86 | } 87 | 88 | w := img2.Bounds().Dx() 89 | h := img2.Bounds().Dy() 90 | 91 | incorrect := 0 92 | for i := 0; i < w; i++ { 93 | for j := 0; j < h; j++ { 94 | wr, wg, wb, wa := img1.At(i, j).RGBA() 95 | gr, gg, gb, ga := img2.At(i, j).RGBA() 96 | want := color.RGBA{ 97 | R: uint8(wr), 98 | G: uint8(wg), 99 | B: uint8(wb), 100 | A: uint8(wa), 101 | } 102 | got := color.RGBA{ 103 | R: uint8(gr), 104 | G: uint8(gg), 105 | B: uint8(gb), 106 | A: uint8(ga), 107 | } 108 | 109 | if !reflect.DeepEqual(want, got) { 110 | t.Logf("read data from clipbaord is inconsistent with previous written data, pix: (%d,%d), got: %+v, want: %+v", i, j, got, want) 111 | incorrect++ 112 | } 113 | } 114 | } 115 | 116 | if incorrect > 0 { 117 | t.Fatalf("read data from clipboard contains too much inconsistent pixels to the previous written data, number of incorrect pixels: %v", incorrect) 118 | } 119 | }) 120 | 121 | t.Run("text", func(t *testing.T) { 122 | data := []byte("golang.design/x/clipboard") 123 | clipboard.Write(clipboard.FmtText, data) 124 | 125 | b := clipboard.Read(clipboard.FmtImage) 126 | if b != nil { 127 | t.Fatalf("read clipboard that stores text data as image should fail, but got len: %d", len(b)) 128 | } 129 | b = clipboard.Read(clipboard.FmtText) 130 | if b == nil { 131 | t.Fatal("read clipboard taht stores text data as text should success, but got: nil") 132 | } 133 | 134 | if !reflect.DeepEqual(data, b) { 135 | t.Fatalf("read data from clipbaord is inconsistent with previous written data, got: %d, want: %d", len(b), len(data)) 136 | } 137 | }) 138 | } 139 | 140 | func TestClipboardMultipleWrites(t *testing.T) { 141 | if runtime.GOOS != "windows" { 142 | if val, ok := os.LookupEnv("CGO_ENABLED"); ok && val == "0" { 143 | t.Skip("CGO_ENABLED is set to 0") 144 | } 145 | } 146 | 147 | data, err := os.ReadFile("tests/testdata/clipboard.png") 148 | if err != nil { 149 | t.Fatalf("failed to read gold file: %v", err) 150 | } 151 | chg := clipboard.Write(clipboard.FmtImage, data) 152 | 153 | data = []byte("golang.design/x/clipboard") 154 | clipboard.Write(clipboard.FmtText, data) 155 | 156 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) 157 | defer cancel() 158 | 159 | select { 160 | case <-ctx.Done(): 161 | t.Fatalf("failed to receive clipboard change notification") 162 | case _, ok := <-chg: 163 | if !ok { 164 | t.Fatalf("change channel is closed before receiving the changed clipboard data") 165 | } 166 | } 167 | _, ok := <-chg 168 | if ok { 169 | t.Fatalf("changed channel should be closed after receiving the notification") 170 | } 171 | 172 | b := clipboard.Read(clipboard.FmtImage) 173 | if b != nil { 174 | t.Fatalf("read clipboard that should store text data as image should fail, but got: %d", len(b)) 175 | } 176 | 177 | b = clipboard.Read(clipboard.FmtText) 178 | if b == nil { 179 | t.Fatalf("read clipboard that should store text data as text should success, got: nil") 180 | } 181 | 182 | if !reflect.DeepEqual(data, b) { 183 | t.Fatalf("read data from clipbaord is inconsistent with previous write, want %s, got: %s", string(data), string(b)) 184 | } 185 | } 186 | 187 | func TestClipboardConcurrentRead(t *testing.T) { 188 | if runtime.GOOS != "windows" { 189 | if val, ok := os.LookupEnv("CGO_ENABLED"); ok && val == "0" { 190 | t.Skip("CGO_ENABLED is set to 0") 191 | } 192 | } 193 | 194 | // This test check that concurrent read/write to the clipboard does 195 | // not cause crashes on some specific platform, such as macOS. 196 | done := make(chan bool, 2) 197 | go func() { 198 | defer func() { 199 | done <- true 200 | }() 201 | clipboard.Read(clipboard.FmtText) 202 | }() 203 | go func() { 204 | defer func() { 205 | done <- true 206 | }() 207 | clipboard.Read(clipboard.FmtImage) 208 | }() 209 | <-done 210 | <-done 211 | } 212 | 213 | func TestClipboardWriteEmpty(t *testing.T) { 214 | if runtime.GOOS != "windows" { 215 | if val, ok := os.LookupEnv("CGO_ENABLED"); ok && val == "0" { 216 | t.Skip("CGO_ENABLED is set to 0") 217 | } 218 | } 219 | 220 | chg1 := clipboard.Write(clipboard.FmtText, nil) 221 | if got := clipboard.Read(clipboard.FmtText); got != nil { 222 | t.Fatalf("write nil to clipboard should read nil, got: %v", string(got)) 223 | } 224 | clipboard.Write(clipboard.FmtText, []byte("")) 225 | <-chg1 226 | 227 | if got := clipboard.Read(clipboard.FmtText); string(got) != "" { 228 | t.Fatalf("write empty string to clipboard should read empty string, got: `%v`", string(got)) 229 | } 230 | } 231 | 232 | func TestClipboardWatch(t *testing.T) { 233 | if runtime.GOOS != "windows" { 234 | if val, ok := os.LookupEnv("CGO_ENABLED"); ok && val == "0" { 235 | t.Skip("CGO_ENABLED is set to 0") 236 | } 237 | } 238 | 239 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) 240 | defer cancel() 241 | 242 | // clear clipboard 243 | clipboard.Write(clipboard.FmtText, []byte("")) 244 | lastRead := clipboard.Read(clipboard.FmtText) 245 | 246 | changed := clipboard.Watch(ctx, clipboard.FmtText) 247 | 248 | want := []byte("golang.design/x/clipboard") 249 | go func(ctx context.Context) { 250 | t := time.NewTicker(time.Millisecond * 500) 251 | for { 252 | select { 253 | case <-ctx.Done(): 254 | return 255 | case <-t.C: 256 | clipboard.Write(clipboard.FmtText, want) 257 | } 258 | } 259 | }(ctx) 260 | for { 261 | select { 262 | case <-ctx.Done(): 263 | if string(lastRead) == "" { 264 | t.Fatalf("clipboard watch never receives a notification") 265 | } 266 | t.Log(string(lastRead)) 267 | return 268 | case data, ok := <-changed: 269 | if !ok { 270 | if string(lastRead) == "" { 271 | t.Fatalf("clipboard watch never receives a notification") 272 | } 273 | return 274 | } 275 | if !bytes.Equal(data, want) { 276 | t.Fatalf("received data from watch mismatch, want: %v, got %v", string(want), string(data)) 277 | } 278 | lastRead = data 279 | } 280 | } 281 | } 282 | 283 | func BenchmarkClipboard(b *testing.B) { 284 | b.Run("text", func(b *testing.B) { 285 | data := []byte("golang.design/x/clipboard") 286 | 287 | b.ReportAllocs() 288 | b.ResetTimer() 289 | for i := 0; i < b.N; i++ { 290 | clipboard.Write(clipboard.FmtText, data) 291 | _ = clipboard.Read(clipboard.FmtText) 292 | } 293 | }) 294 | } 295 | 296 | func TestClipboardNoCgo(t *testing.T) { 297 | if val, ok := os.LookupEnv("CGO_ENABLED"); !ok || val != "0" { 298 | t.Skip("CGO_ENABLED is set to 1") 299 | } 300 | if runtime.GOOS == "windows" { 301 | t.Skip("Windows should always be tested") 302 | } 303 | 304 | t.Run("Read", func(t *testing.T) { 305 | defer func() { 306 | if r := recover(); r != nil { 307 | return 308 | } 309 | t.Fatalf("expect to fail when CGO_ENABLED=0") 310 | }() 311 | 312 | clipboard.Read(clipboard.FmtText) 313 | }) 314 | 315 | t.Run("Write", func(t *testing.T) { 316 | defer func() { 317 | if r := recover(); r != nil { 318 | return 319 | } 320 | t.Fatalf("expect to fail when CGO_ENABLED=0") 321 | }() 322 | 323 | clipboard.Write(clipboard.FmtText, []byte("dummy")) 324 | }) 325 | 326 | t.Run("Watch", func(t *testing.T) { 327 | defer func() { 328 | if r := recover(); r != nil { 329 | return 330 | } 331 | t.Fatalf("expect to fail when CGO_ENABLED=0") 332 | }() 333 | 334 | clipboard.Watch(context.TODO(), clipboard.FmtText) 335 | }) 336 | } 337 | -------------------------------------------------------------------------------- /clipboard_linux.c: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | //go:build linux && !android 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | // syncStatus is a function from the Go side. 18 | extern void syncStatus(uintptr_t handle, int status); 19 | 20 | void *libX11; 21 | 22 | Display* (*P_XOpenDisplay)(int); 23 | void (*P_XCloseDisplay)(Display*); 24 | Window (*P_XDefaultRootWindow)(Display*); 25 | Window (*P_XCreateSimpleWindow)(Display*, Window, int, int, int, int, int, int, int); 26 | Atom (*P_XInternAtom)(Display*, char*, int); 27 | void (*P_XSetSelectionOwner)(Display*, Atom, Window, unsigned long); 28 | Window (*P_XGetSelectionOwner)(Display*, Atom); 29 | void (*P_XNextEvent)(Display*, XEvent*); 30 | int (*P_XChangeProperty)(Display*, Window, Atom, Atom, int, int, unsigned char*, int); 31 | void (*P_XSendEvent)(Display*, Window, int, long , XEvent*); 32 | int (*P_XGetWindowProperty) (Display*, Window, Atom, long, long, Bool, Atom, Atom*, int*, unsigned long *, unsigned long *, unsigned char **); 33 | void (*P_XFree) (void*); 34 | void (*P_XDeleteProperty) (Display*, Window, Atom); 35 | void (*P_XConvertSelection)(Display*, Atom, Atom, Atom, Window, Time); 36 | 37 | int initX11() { 38 | if (libX11) { 39 | return 1; 40 | } 41 | libX11 = dlopen("libX11.so", RTLD_LAZY); 42 | if (!libX11) { 43 | return 0; 44 | } 45 | P_XOpenDisplay = (Display* (*)(int)) dlsym(libX11, "XOpenDisplay"); 46 | P_XCloseDisplay = (void (*)(Display*)) dlsym(libX11, "XCloseDisplay"); 47 | P_XDefaultRootWindow = (Window (*)(Display*)) dlsym(libX11, "XDefaultRootWindow"); 48 | P_XCreateSimpleWindow = (Window (*)(Display*, Window, int, int, int, int, int, int, int)) dlsym(libX11, "XCreateSimpleWindow"); 49 | P_XInternAtom = (Atom (*)(Display*, char*, int)) dlsym(libX11, "XInternAtom"); 50 | P_XSetSelectionOwner = (void (*)(Display*, Atom, Window, unsigned long)) dlsym(libX11, "XSetSelectionOwner"); 51 | P_XGetSelectionOwner = (Window (*)(Display*, Atom)) dlsym(libX11, "XGetSelectionOwner"); 52 | P_XNextEvent = (void (*)(Display*, XEvent*)) dlsym(libX11, "XNextEvent"); 53 | P_XChangeProperty = (int (*)(Display*, Window, Atom, Atom, int, int, unsigned char*, int)) dlsym(libX11, "XChangeProperty"); 54 | P_XSendEvent = (void (*)(Display*, Window, int, long , XEvent*)) dlsym(libX11, "XSendEvent"); 55 | P_XGetWindowProperty = (int (*)(Display*, Window, Atom, long, long, Bool, Atom, Atom*, int*, unsigned long *, unsigned long *, unsigned char **)) dlsym(libX11, "XGetWindowProperty"); 56 | P_XFree = (void (*)(void*)) dlsym(libX11, "XFree"); 57 | P_XDeleteProperty = (void (*)(Display*, Window, Atom)) dlsym(libX11, "XDeleteProperty"); 58 | P_XConvertSelection = (void (*)(Display*, Atom, Atom, Atom, Window, Time)) dlsym(libX11, "XConvertSelection"); 59 | return 1; 60 | } 61 | 62 | int clipboard_test() { 63 | if (!initX11()) { 64 | return -1; 65 | } 66 | 67 | Display* d = NULL; 68 | for (int i = 0; i < 42; i++) { 69 | d = (*P_XOpenDisplay)(0); 70 | if (d == NULL) { 71 | continue; 72 | } 73 | break; 74 | } 75 | if (d == NULL) { 76 | return -1; 77 | } 78 | (*P_XCloseDisplay)(d); 79 | return 0; 80 | } 81 | 82 | // clipboard_write writes the given buf of size n as type typ. 83 | // if start is provided, the value of start will be changed to 1 to indicate 84 | // if the write is availiable for reading. 85 | int clipboard_write(char *typ, unsigned char *buf, size_t n, uintptr_t handle) { 86 | if (!initX11()) { 87 | return -1; 88 | } 89 | 90 | Display* d = NULL; 91 | for (int i = 0; i < 42; i++) { 92 | d = (*P_XOpenDisplay)(0); 93 | if (d == NULL) { 94 | continue; 95 | } 96 | break; 97 | } 98 | if (d == NULL) { 99 | syncStatus(handle, -1); 100 | return -1; 101 | } 102 | Window w = (*P_XCreateSimpleWindow)(d, (*P_XDefaultRootWindow)(d), 0, 0, 1, 1, 0, 0, 0); 103 | 104 | // Use False because these may not available for the first time. 105 | Atom sel = (*P_XInternAtom)(d, "CLIPBOARD", 0); 106 | Atom atomString = (*P_XInternAtom)(d, "UTF8_STRING", 0); 107 | Atom atomImage = (*P_XInternAtom)(d, "image/png", 0); 108 | Atom targetsAtom = (*P_XInternAtom)(d, "TARGETS", 0); 109 | 110 | // Use True to makesure the requested type is a valid type. 111 | Atom target = (*P_XInternAtom)(d, typ, 1); 112 | if (target == None) { 113 | (*P_XCloseDisplay)(d); 114 | syncStatus(handle, -2); 115 | return -2; 116 | } 117 | 118 | (*P_XSetSelectionOwner)(d, sel, w, CurrentTime); 119 | if ((*P_XGetSelectionOwner)(d, sel) != w) { 120 | (*P_XCloseDisplay)(d); 121 | syncStatus(handle, -3); 122 | return -3; 123 | } 124 | 125 | XEvent event; 126 | XSelectionRequestEvent* xsr; 127 | int notified = 0; 128 | for (;;) { 129 | if (notified == 0) { 130 | syncStatus(handle, 1); // notify Go side 131 | notified = 1; 132 | } 133 | 134 | (*P_XNextEvent)(d, &event); 135 | switch (event.type) { 136 | case SelectionClear: 137 | // For debugging: 138 | // printf("x11write: lost ownership of clipboard selection.\n"); 139 | // fflush(stdout); 140 | (*P_XCloseDisplay)(d); 141 | return 0; 142 | case SelectionNotify: 143 | // For debugging: 144 | // printf("x11write: notify.\n"); 145 | // fflush(stdout); 146 | break; 147 | case SelectionRequest: 148 | if (event.xselectionrequest.selection != sel) { 149 | break; 150 | } 151 | 152 | XSelectionRequestEvent * xsr = &event.xselectionrequest; 153 | XSelectionEvent ev = {0}; 154 | int R = 0; 155 | 156 | ev.type = SelectionNotify; 157 | ev.display = xsr->display; 158 | ev.requestor = xsr->requestor; 159 | ev.selection = xsr->selection; 160 | ev.time = xsr->time; 161 | ev.target = xsr->target; 162 | ev.property = xsr->property; 163 | 164 | if (ev.target == atomString && ev.target == target) { 165 | R = (*P_XChangeProperty)(ev.display, ev.requestor, ev.property, 166 | atomString, 8, PropModeReplace, buf, n); 167 | } else if (ev.target == atomImage && ev.target == target) { 168 | R = (*P_XChangeProperty)(ev.display, ev.requestor, ev.property, 169 | atomImage, 8, PropModeReplace, buf, n); 170 | } else if (ev.target == targetsAtom) { 171 | // Reply atoms for supported targets, other clients should 172 | // request the clipboard again and obtain the data if their 173 | // implementation is correct. 174 | Atom targets[] = { atomString, atomImage }; 175 | R = (*P_XChangeProperty)(ev.display, ev.requestor, ev.property, 176 | XA_ATOM, 32, PropModeReplace, 177 | (unsigned char *)&targets, sizeof(targets)/sizeof(Atom)); 178 | } else { 179 | ev.property = None; 180 | } 181 | 182 | if ((R & 2) == 0) (*P_XSendEvent)(d, ev.requestor, 0, 0, (XEvent *)&ev); 183 | break; 184 | } 185 | } 186 | } 187 | 188 | // read_data reads the property of a selection if the target atom matches 189 | // the actual atom. 190 | unsigned long read_data(XSelectionEvent *sev, Atom sel, Atom prop, Atom target, char **buf) { 191 | if (!initX11()) { 192 | return -1; 193 | } 194 | 195 | unsigned char *data; 196 | Atom actual; 197 | int format; 198 | unsigned long n = 0; 199 | unsigned long size = 0; 200 | if (sev->property == None || sev->selection != sel || sev->property != prop) { 201 | return 0; 202 | } 203 | 204 | int ret = (*P_XGetWindowProperty)(sev->display, sev->requestor, sev->property, 205 | 0L, (~0L), 0, AnyPropertyType, &actual, &format, &size, &n, &data); 206 | if (ret != Success) { 207 | return 0; 208 | } 209 | 210 | if (actual == target && buf != NULL) { 211 | *buf = (char *)malloc(size * sizeof(char)); 212 | memcpy(*buf, data, size*sizeof(char)); 213 | } 214 | (*P_XFree)(data); 215 | (*P_XDeleteProperty)(sev->display, sev->requestor, sev->property); 216 | return size * sizeof(char); 217 | } 218 | 219 | // clipboard_read reads the clipboard selection in given format typ. 220 | // the read bytes are written into buf and returns the size of the buffer. 221 | // 222 | // The caller of this function should responsible for the free of the buf. 223 | unsigned long clipboard_read(char* typ, char **buf) { 224 | if (!initX11()) { 225 | return -1; 226 | } 227 | 228 | Display* d = NULL; 229 | for (int i = 0; i < 42; i++) { 230 | d = (*P_XOpenDisplay)(0); 231 | if (d == NULL) { 232 | continue; 233 | } 234 | break; 235 | } 236 | if (d == NULL) { 237 | return -1; 238 | } 239 | 240 | Window w = (*P_XCreateSimpleWindow)(d, (*P_XDefaultRootWindow)(d), 0, 0, 1, 1, 0, 0, 0); 241 | 242 | // Use False because these may not available for the first time. 243 | Atom sel = (*P_XInternAtom)(d, "CLIPBOARD", False); 244 | Atom prop = (*P_XInternAtom)(d, "GOLANG_DESIGN_DATA", False); 245 | 246 | // Use True to makesure the requested type is a valid type. 247 | Atom target = (*P_XInternAtom)(d, typ, True); 248 | if (target == None) { 249 | (*P_XCloseDisplay)(d); 250 | return -2; 251 | } 252 | 253 | (*P_XConvertSelection)(d, sel, target, prop, w, CurrentTime); 254 | XEvent event; 255 | for (;;) { 256 | (*P_XNextEvent)(d, &event); 257 | if (event.type != SelectionNotify) continue; 258 | break; 259 | } 260 | unsigned long n = read_data((XSelectionEvent *)&event.xselection, sel, prop, target, buf); 261 | (*P_XCloseDisplay)(d); 262 | return n; 263 | } 264 | -------------------------------------------------------------------------------- /clipboard_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | // 5 | // Written by Changkun Ou 6 | 7 | //go:build windows 8 | 9 | package clipboard 10 | 11 | // Interacting with Clipboard on Windows: 12 | // https://docs.microsoft.com/zh-cn/windows/win32/dataxchg/using-the-clipboard 13 | 14 | import ( 15 | "bytes" 16 | "context" 17 | "encoding/binary" 18 | "errors" 19 | "fmt" 20 | "image" 21 | "image/color" 22 | "image/png" 23 | "reflect" 24 | "runtime" 25 | "syscall" 26 | "time" 27 | "unicode/utf16" 28 | "unsafe" 29 | 30 | "golang.org/x/image/bmp" 31 | ) 32 | 33 | func initialize() error { return nil } 34 | 35 | // readText reads the clipboard and returns the text data if presents. 36 | // The caller is responsible for opening/closing the clipboard before 37 | // calling this function. 38 | func readText() (buf []byte, err error) { 39 | hMem, _, err := getClipboardData.Call(cFmtUnicodeText) 40 | if hMem == 0 { 41 | return nil, err 42 | } 43 | p, _, err := gLock.Call(hMem) 44 | if p == 0 { 45 | return nil, err 46 | } 47 | defer gUnlock.Call(hMem) 48 | 49 | // Find NUL terminator 50 | n := 0 51 | for ptr := unsafe.Pointer(p); *(*uint16)(ptr) != 0; n++ { 52 | ptr = unsafe.Pointer(uintptr(ptr) + 53 | unsafe.Sizeof(*((*uint16)(unsafe.Pointer(p))))) 54 | } 55 | 56 | var s []uint16 57 | h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) 58 | h.Data = p 59 | h.Len = n 60 | h.Cap = n 61 | return []byte(string(utf16.Decode(s))), nil 62 | } 63 | 64 | // writeText writes given data to the clipboard. It is the caller's 65 | // responsibility for opening/closing the clipboard before calling 66 | // this function. 67 | func writeText(buf []byte) error { 68 | r, _, err := emptyClipboard.Call() 69 | if r == 0 { 70 | return fmt.Errorf("failed to clear clipboard: %w", err) 71 | } 72 | 73 | // empty text, we are done here. 74 | if len(buf) == 0 { 75 | return nil 76 | } 77 | 78 | s, err := syscall.UTF16FromString(string(buf)) 79 | if err != nil { 80 | return fmt.Errorf("failed to convert given string: %w", err) 81 | } 82 | 83 | hMem, _, err := gAlloc.Call(gmemMoveable, uintptr(len(s)*int(unsafe.Sizeof(s[0])))) 84 | if hMem == 0 { 85 | return fmt.Errorf("failed to alloc global memory: %w", err) 86 | } 87 | 88 | p, _, err := gLock.Call(hMem) 89 | if p == 0 { 90 | return fmt.Errorf("failed to lock global memory: %w", err) 91 | } 92 | defer gUnlock.Call(hMem) 93 | 94 | // no return value 95 | memMove.Call(p, uintptr(unsafe.Pointer(&s[0])), 96 | uintptr(len(s)*int(unsafe.Sizeof(s[0])))) 97 | 98 | v, _, err := setClipboardData.Call(cFmtUnicodeText, hMem) 99 | if v == 0 { 100 | gFree.Call(hMem) 101 | return fmt.Errorf("failed to set text to clipboard: %w", err) 102 | } 103 | 104 | return nil 105 | } 106 | 107 | // readImage reads the clipboard and returns PNG encoded image data 108 | // if presents. The caller is responsible for opening/closing the 109 | // clipboard before calling this function. 110 | func readImage() ([]byte, error) { 111 | hMem, _, err := getClipboardData.Call(cFmtDIBV5) 112 | if hMem == 0 { 113 | // second chance to try FmtDIB 114 | return readImageDib() 115 | } 116 | p, _, err := gLock.Call(hMem) 117 | if p == 0 { 118 | return nil, err 119 | } 120 | defer gUnlock.Call(hMem) 121 | 122 | // inspect header information 123 | info := (*bitmapV5Header)(unsafe.Pointer(p)) 124 | 125 | // maybe deal with other formats? 126 | if info.BitCount != 32 { 127 | return nil, errUnsupported 128 | } 129 | 130 | var data []byte 131 | sh := (*reflect.SliceHeader)(unsafe.Pointer(&data)) 132 | sh.Data = uintptr(p) 133 | sh.Cap = int(info.Size + 4*uint32(info.Width)*uint32(info.Height)) 134 | sh.Len = int(info.Size + 4*uint32(info.Width)*uint32(info.Height)) 135 | img := image.NewRGBA(image.Rect(0, 0, int(info.Width), int(info.Height))) 136 | offset := int(info.Size) 137 | stride := int(info.Width) 138 | for y := 0; y < int(info.Height); y++ { 139 | for x := 0; x < int(info.Width); x++ { 140 | idx := offset + 4*(y*stride+x) 141 | xhat := (x + int(info.Width)) % int(info.Width) 142 | yhat := int(info.Height) - 1 - y 143 | r := data[idx+2] 144 | g := data[idx+1] 145 | b := data[idx+0] 146 | a := data[idx+3] 147 | img.SetRGBA(xhat, yhat, color.RGBA{r, g, b, a}) 148 | } 149 | } 150 | // always use PNG encoding. 151 | var buf bytes.Buffer 152 | png.Encode(&buf, img) 153 | return buf.Bytes(), nil 154 | } 155 | 156 | func readImageDib() ([]byte, error) { 157 | const ( 158 | fileHeaderLen = 14 159 | infoHeaderLen = 40 160 | cFmtDIB = 8 161 | ) 162 | 163 | hClipDat, _, err := getClipboardData.Call(cFmtDIB) 164 | if err != nil { 165 | return nil, errors.New("not dib format data: " + err.Error()) 166 | } 167 | pMemBlk, _, err := gLock.Call(hClipDat) 168 | if pMemBlk == 0 { 169 | return nil, errors.New("failed to call global lock: " + err.Error()) 170 | } 171 | defer gUnlock.Call(hClipDat) 172 | 173 | bmpHeader := (*bitmapHeader)(unsafe.Pointer(pMemBlk)) 174 | dataSize := bmpHeader.SizeImage + fileHeaderLen + infoHeaderLen 175 | 176 | if bmpHeader.SizeImage == 0 && bmpHeader.Compression == 0 { 177 | iSizeImage := bmpHeader.Height * ((bmpHeader.Width*uint32(bmpHeader.BitCount)/8 + 3) &^ 3) 178 | dataSize += iSizeImage 179 | } 180 | buf := new(bytes.Buffer) 181 | binary.Write(buf, binary.LittleEndian, uint16('B')|(uint16('M')<<8)) 182 | binary.Write(buf, binary.LittleEndian, uint32(dataSize)) 183 | binary.Write(buf, binary.LittleEndian, uint32(0)) 184 | const sizeof_colorbar = 0 185 | binary.Write(buf, binary.LittleEndian, uint32(fileHeaderLen+infoHeaderLen+sizeof_colorbar)) 186 | j := 0 187 | for i := fileHeaderLen; i < int(dataSize); i++ { 188 | binary.Write(buf, binary.BigEndian, *(*byte)(unsafe.Pointer(pMemBlk + uintptr(j)))) 189 | j++ 190 | } 191 | return bmpToPng(buf) 192 | } 193 | 194 | func bmpToPng(bmpBuf *bytes.Buffer) (buf []byte, err error) { 195 | var f bytes.Buffer 196 | original_image, err := bmp.Decode(bmpBuf) 197 | if err != nil { 198 | return nil, err 199 | } 200 | err = png.Encode(&f, original_image) 201 | if err != nil { 202 | return nil, err 203 | } 204 | return f.Bytes(), nil 205 | } 206 | 207 | func writeImage(buf []byte) error { 208 | r, _, err := emptyClipboard.Call() 209 | if r == 0 { 210 | return fmt.Errorf("failed to clear clipboard: %w", err) 211 | } 212 | 213 | // empty text, we are done here. 214 | if len(buf) == 0 { 215 | return nil 216 | } 217 | 218 | img, err := png.Decode(bytes.NewReader(buf)) 219 | if err != nil { 220 | return fmt.Errorf("input bytes is not PNG encoded: %w", err) 221 | } 222 | 223 | offset := unsafe.Sizeof(bitmapV5Header{}) 224 | width := img.Bounds().Dx() 225 | height := img.Bounds().Dy() 226 | imageSize := 4 * width * height 227 | 228 | data := make([]byte, int(offset)+imageSize) 229 | for y := 0; y < height; y++ { 230 | for x := 0; x < width; x++ { 231 | idx := int(offset) + 4*(y*width+x) 232 | r, g, b, a := img.At(x, height-1-y).RGBA() 233 | data[idx+2] = uint8(r) 234 | data[idx+1] = uint8(g) 235 | data[idx+0] = uint8(b) 236 | data[idx+3] = uint8(a) 237 | } 238 | } 239 | 240 | info := bitmapV5Header{} 241 | info.Size = uint32(offset) 242 | info.Width = int32(width) 243 | info.Height = int32(height) 244 | info.Planes = 1 245 | info.Compression = 0 // BI_RGB 246 | info.SizeImage = uint32(4 * info.Width * info.Height) 247 | info.RedMask = 0xff0000 // default mask 248 | info.GreenMask = 0xff00 249 | info.BlueMask = 0xff 250 | info.AlphaMask = 0xff000000 251 | info.BitCount = 32 // we only deal with 32 bpp at the moment. 252 | // Use calibrated RGB values as Go's image/png assumes linear color space. 253 | // Other options: 254 | // - LCS_CALIBRATED_RGB = 0x00000000 255 | // - LCS_sRGB = 0x73524742 256 | // - LCS_WINDOWS_COLOR_SPACE = 0x57696E20 257 | // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/eb4bbd50-b3ce-4917-895c-be31f214797f 258 | info.CSType = 0x73524742 259 | // Use GL_IMAGES for GamutMappingIntent 260 | // Other options: 261 | // - LCS_GM_ABS_COLORIMETRIC = 0x00000008 262 | // - LCS_GM_BUSINESS = 0x00000001 263 | // - LCS_GM_GRAPHICS = 0x00000002 264 | // - LCS_GM_IMAGES = 0x00000004 265 | // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/9fec0834-607d-427d-abd5-ab240fb0db38 266 | info.Intent = 4 // LCS_GM_IMAGES 267 | 268 | infob := make([]byte, int(unsafe.Sizeof(info))) 269 | for i, v := range *(*[unsafe.Sizeof(info)]byte)(unsafe.Pointer(&info)) { 270 | infob[i] = v 271 | } 272 | copy(data[:], infob[:]) 273 | 274 | hMem, _, err := gAlloc.Call(gmemMoveable, 275 | uintptr(len(data)*int(unsafe.Sizeof(data[0])))) 276 | if hMem == 0 { 277 | return fmt.Errorf("failed to alloc global memory: %w", err) 278 | } 279 | 280 | p, _, err := gLock.Call(hMem) 281 | if p == 0 { 282 | return fmt.Errorf("failed to lock global memory: %w", err) 283 | } 284 | defer gUnlock.Call(hMem) 285 | 286 | memMove.Call(p, uintptr(unsafe.Pointer(&data[0])), 287 | uintptr(len(data)*int(unsafe.Sizeof(data[0])))) 288 | 289 | v, _, err := setClipboardData.Call(cFmtDIBV5, hMem) 290 | if v == 0 { 291 | gFree.Call(hMem) 292 | return fmt.Errorf("failed to set text to clipboard: %w", err) 293 | } 294 | 295 | return nil 296 | } 297 | 298 | func read(t Format) (buf []byte, err error) { 299 | // On Windows, OpenClipboard and CloseClipboard must be executed on 300 | // the same thread. Thus, lock the OS thread for further execution. 301 | runtime.LockOSThread() 302 | defer runtime.UnlockOSThread() 303 | 304 | var format uintptr 305 | switch t { 306 | case FmtImage: 307 | format = cFmtDIBV5 308 | case FmtText: 309 | fallthrough 310 | default: 311 | format = cFmtUnicodeText 312 | } 313 | 314 | // check if clipboard is avaliable for the requested format 315 | r, _, err := isClipboardFormatAvailable.Call(format) 316 | if r == 0 { 317 | return nil, errUnavailable 318 | } 319 | 320 | // try again until open clipboard successed 321 | for { 322 | r, _, _ = openClipboard.Call() 323 | if r == 0 { 324 | continue 325 | } 326 | break 327 | } 328 | defer closeClipboard.Call() 329 | 330 | switch format { 331 | case cFmtDIBV5: 332 | return readImage() 333 | case cFmtUnicodeText: 334 | fallthrough 335 | default: 336 | return readText() 337 | } 338 | } 339 | 340 | // write writes the given data to clipboard and 341 | // returns true if success or false if failed. 342 | func write(t Format, buf []byte) (<-chan struct{}, error) { 343 | errch := make(chan error) 344 | changed := make(chan struct{}, 1) 345 | go func() { 346 | // make sure GetClipboardSequenceNumber happens with 347 | // OpenClipboard on the same thread. 348 | runtime.LockOSThread() 349 | defer runtime.UnlockOSThread() 350 | for { 351 | r, _, _ := openClipboard.Call(0) 352 | if r == 0 { 353 | continue 354 | } 355 | break 356 | } 357 | 358 | // var param uintptr 359 | switch t { 360 | case FmtImage: 361 | err := writeImage(buf) 362 | if err != nil { 363 | errch <- err 364 | closeClipboard.Call() 365 | return 366 | } 367 | case FmtText: 368 | fallthrough 369 | default: 370 | // param = cFmtUnicodeText 371 | err := writeText(buf) 372 | if err != nil { 373 | errch <- err 374 | closeClipboard.Call() 375 | return 376 | } 377 | } 378 | // Close the clipboard otherwise other applications cannot 379 | // paste the data. 380 | closeClipboard.Call() 381 | 382 | cnt, _, _ := getClipboardSequenceNumber.Call() 383 | errch <- nil 384 | for { 385 | time.Sleep(time.Second) 386 | cur, _, _ := getClipboardSequenceNumber.Call() 387 | if cur != cnt { 388 | changed <- struct{}{} 389 | close(changed) 390 | return 391 | } 392 | } 393 | }() 394 | err := <-errch 395 | if err != nil { 396 | return nil, err 397 | } 398 | return changed, nil 399 | } 400 | 401 | func watch(ctx context.Context, t Format) <-chan []byte { 402 | recv := make(chan []byte, 1) 403 | ready := make(chan struct{}) 404 | go func() { 405 | // not sure if we are too slow or the user too fast :) 406 | ti := time.NewTicker(time.Second) 407 | cnt, _, _ := getClipboardSequenceNumber.Call() 408 | ready <- struct{}{} 409 | for { 410 | select { 411 | case <-ctx.Done(): 412 | close(recv) 413 | return 414 | case <-ti.C: 415 | cur, _, _ := getClipboardSequenceNumber.Call() 416 | if cnt != cur { 417 | b := Read(t) 418 | if b == nil { 419 | continue 420 | } 421 | recv <- b 422 | cnt = cur 423 | } 424 | } 425 | } 426 | }() 427 | <-ready 428 | return recv 429 | } 430 | 431 | const ( 432 | cFmtBitmap = 2 // Win+PrintScreen 433 | cFmtUnicodeText = 13 434 | cFmtDIBV5 = 17 435 | // Screenshot taken from special shortcut is in different format (why??), see: 436 | // https://jpsoft.com/forums/threads/detecting-clipboard-format.5225/ 437 | cFmtDataObject = 49161 // Shift+Win+s, returned from enumClipboardFormats 438 | gmemMoveable = 0x0002 439 | ) 440 | 441 | // BITMAPV5Header structure, see: 442 | // https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header 443 | type bitmapV5Header struct { 444 | Size uint32 445 | Width int32 446 | Height int32 447 | Planes uint16 448 | BitCount uint16 449 | Compression uint32 450 | SizeImage uint32 451 | XPelsPerMeter int32 452 | YPelsPerMeter int32 453 | ClrUsed uint32 454 | ClrImportant uint32 455 | RedMask uint32 456 | GreenMask uint32 457 | BlueMask uint32 458 | AlphaMask uint32 459 | CSType uint32 460 | Endpoints struct { 461 | CiexyzRed, CiexyzGreen, CiexyzBlue struct { 462 | CiexyzX, CiexyzY, CiexyzZ int32 // FXPT2DOT30 463 | } 464 | } 465 | GammaRed uint32 466 | GammaGreen uint32 467 | GammaBlue uint32 468 | Intent uint32 469 | ProfileData uint32 470 | ProfileSize uint32 471 | Reserved uint32 472 | } 473 | 474 | type bitmapHeader struct { 475 | Size uint32 476 | Width uint32 477 | Height uint32 478 | PLanes uint16 479 | BitCount uint16 480 | Compression uint32 481 | SizeImage uint32 482 | XPelsPerMeter uint32 483 | YPelsPerMeter uint32 484 | ClrUsed uint32 485 | ClrImportant uint32 486 | } 487 | 488 | // Calling a Windows DLL, see: 489 | // https://github.com/golang/go/wiki/WindowsDLLs 490 | var ( 491 | user32 = syscall.MustLoadDLL("user32") 492 | // Opens the clipboard for examination and prevents other 493 | // applications from modifying the clipboard content. 494 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-openclipboard 495 | openClipboard = user32.MustFindProc("OpenClipboard") 496 | // Closes the clipboard. 497 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-closeclipboard 498 | closeClipboard = user32.MustFindProc("CloseClipboard") 499 | // Empties the clipboard and frees handles to data in the clipboard. 500 | // The function then assigns ownership of the clipboard to the 501 | // window that currently has the clipboard open. 502 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-emptyclipboard 503 | emptyClipboard = user32.MustFindProc("EmptyClipboard") 504 | // Retrieves data from the clipboard in a specified format. 505 | // The clipboard must have been opened previously. 506 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboarddata 507 | getClipboardData = user32.MustFindProc("GetClipboardData") 508 | // Places data on the clipboard in a specified clipboard format. 509 | // The window must be the current clipboard owner, and the 510 | // application must have called the OpenClipboard function. (When 511 | // responding to the WM_RENDERFORMAT message, the clipboard owner 512 | // must not call OpenClipboard before calling SetClipboardData.) 513 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata 514 | setClipboardData = user32.MustFindProc("SetClipboardData") 515 | // Determines whether the clipboard contains data in the specified format. 516 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-isclipboardformatavailable 517 | isClipboardFormatAvailable = user32.MustFindProc("IsClipboardFormatAvailable") 518 | // Clipboard data formats are stored in an ordered list. To perform 519 | // an enumeration of clipboard data formats, you make a series of 520 | // calls to the EnumClipboardFormats function. For each call, the 521 | // format parameter specifies an available clipboard format, and the 522 | // function returns the next available clipboard format. 523 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-isclipboardformatavailable 524 | enumClipboardFormats = user32.MustFindProc("EnumClipboardFormats") 525 | // Retrieves the clipboard sequence number for the current window station. 526 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboardsequencenumber 527 | getClipboardSequenceNumber = user32.MustFindProc("GetClipboardSequenceNumber") 528 | // Registers a new clipboard format. This format can then be used as 529 | // a valid clipboard format. 530 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformata 531 | registerClipboardFormatA = user32.MustFindProc("RegisterClipboardFormatA") 532 | 533 | kernel32 = syscall.NewLazyDLL("kernel32") 534 | 535 | // Locks a global memory object and returns a pointer to the first 536 | // byte of the object's memory block. 537 | // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globallock 538 | gLock = kernel32.NewProc("GlobalLock") 539 | // Decrements the lock count associated with a memory object that was 540 | // allocated with GMEM_MOVEABLE. This function has no effect on memory 541 | // objects allocated with GMEM_FIXED. 542 | // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalunlock 543 | gUnlock = kernel32.NewProc("GlobalUnlock") 544 | // Allocates the specified number of bytes from the heap. 545 | // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalalloc 546 | gAlloc = kernel32.NewProc("GlobalAlloc") 547 | // Frees the specified global memory object and invalidates its handle. 548 | // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalfree 549 | gFree = kernel32.NewProc("GlobalFree") 550 | memMove = kernel32.NewProc("RtlMoveMemory") 551 | ) 552 | --------------------------------------------------------------------------------