├── .travis.yml ├── LICENSE ├── README.md ├── clipboard.go ├── clipboard_darwin.go ├── clipboard_plan9.go ├── clipboard_test.go ├── clipboard_unix.go ├── clipboard_windows.go ├── cmd ├── gocopy │ └── gocopy.go └── gopaste │ └── gopaste.go ├── example_test.go └── go.mod /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | os: 4 | - linux 5 | - osx 6 | - windows 7 | 8 | go: 9 | - go1.13.x 10 | - go1.x 11 | 12 | services: 13 | - xvfb 14 | 15 | before_install: 16 | - export DISPLAY=:99.0 17 | 18 | script: 19 | - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get install xsel; fi 20 | - go test -v . 21 | - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get install xclip; fi 22 | - go test -v . 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Ato Araki. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of @atotto. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/atotto/clipboard.svg?branch=master)](https://travis-ci.com/atotto/clipboard) 2 | 3 | [![GoDoc](https://godoc.org/github.com/atotto/clipboard?status.svg)](http://godoc.org/github.com/atotto/clipboard) 4 | 5 | # Clipboard for Go 6 | 7 | Provide copying and pasting to the Clipboard for Go. 8 | 9 | Build: 10 | 11 | $ go get github.com/atotto/clipboard 12 | 13 | Platforms: 14 | 15 | * OSX 16 | * Windows 7 (probably work on other Windows) 17 | * Linux, Unix (requires 'xclip' or 'xsel' command to be installed) 18 | 19 | 20 | Document: 21 | 22 | * http://godoc.org/github.com/atotto/clipboard 23 | 24 | Notes: 25 | 26 | * Text string only 27 | * UTF-8 text encoding only (no conversion) 28 | 29 | TODO: 30 | 31 | * Clipboard watcher(?) 32 | 33 | ## Commands: 34 | 35 | paste shell command: 36 | 37 | $ go get github.com/atotto/clipboard/cmd/gopaste 38 | $ # example: 39 | $ gopaste > document.txt 40 | 41 | copy shell command: 42 | 43 | $ go get github.com/atotto/clipboard/cmd/gocopy 44 | $ # example: 45 | $ cat document.txt | gocopy 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /clipboard.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 @atotto. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package clipboard read/write on clipboard 6 | package clipboard 7 | 8 | // ReadAll read string from clipboard 9 | func ReadAll() (string, error) { 10 | return readAll() 11 | } 12 | 13 | // WriteAll write string to clipboard 14 | func WriteAll(text string) error { 15 | return writeAll(text) 16 | } 17 | 18 | // Unsupported might be set true during clipboard init, to help callers decide 19 | // whether or not to offer clipboard options. 20 | var Unsupported bool 21 | -------------------------------------------------------------------------------- /clipboard_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 @atotto. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin 6 | 7 | package clipboard 8 | 9 | import ( 10 | "os/exec" 11 | ) 12 | 13 | var ( 14 | pasteCmdArgs = "pbpaste" 15 | copyCmdArgs = "pbcopy" 16 | ) 17 | 18 | func getPasteCommand() *exec.Cmd { 19 | return exec.Command(pasteCmdArgs) 20 | } 21 | 22 | func getCopyCommand() *exec.Cmd { 23 | return exec.Command(copyCmdArgs) 24 | } 25 | 26 | func readAll() (string, error) { 27 | pasteCmd := getPasteCommand() 28 | out, err := pasteCmd.Output() 29 | if err != nil { 30 | return "", err 31 | } 32 | return string(out), nil 33 | } 34 | 35 | func writeAll(text string) error { 36 | copyCmd := getCopyCommand() 37 | in, err := copyCmd.StdinPipe() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if err := copyCmd.Start(); err != nil { 43 | return err 44 | } 45 | if _, err := in.Write([]byte(text)); err != nil { 46 | return err 47 | } 48 | if err := in.Close(); err != nil { 49 | return err 50 | } 51 | return copyCmd.Wait() 52 | } 53 | -------------------------------------------------------------------------------- /clipboard_plan9.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 @atotto. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build plan9 6 | 7 | package clipboard 8 | 9 | import ( 10 | "os" 11 | "io/ioutil" 12 | ) 13 | 14 | func readAll() (string, error) { 15 | f, err := os.Open("/dev/snarf") 16 | if err != nil { 17 | return "", err 18 | } 19 | defer f.Close() 20 | 21 | str, err := ioutil.ReadAll(f) 22 | if err != nil { 23 | return "", err 24 | } 25 | 26 | return string(str), nil 27 | } 28 | 29 | func writeAll(text string) error { 30 | f, err := os.OpenFile("/dev/snarf", os.O_WRONLY, 0666) 31 | if err != nil { 32 | return err 33 | } 34 | defer f.Close() 35 | 36 | _, err = f.Write([]byte(text)) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /clipboard_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 @atotto. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package clipboard_test 6 | 7 | import ( 8 | "testing" 9 | 10 | . "github.com/atotto/clipboard" 11 | ) 12 | 13 | func TestCopyAndPaste(t *testing.T) { 14 | expected := "日本語" 15 | 16 | err := WriteAll(expected) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | actual, err := ReadAll() 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if actual != expected { 27 | t.Errorf("want %s, got %s", expected, actual) 28 | } 29 | } 30 | 31 | func TestMultiCopyAndPaste(t *testing.T) { 32 | expected1 := "French: éèêëàùœç" 33 | expected2 := "Weird UTF-8: 💩☃" 34 | 35 | err := WriteAll(expected1) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | actual1, err := ReadAll() 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | if actual1 != expected1 { 45 | t.Errorf("want %s, got %s", expected1, actual1) 46 | } 47 | 48 | err = WriteAll(expected2) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | actual2, err := ReadAll() 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | if actual2 != expected2 { 58 | t.Errorf("want %s, got %s", expected2, actual2) 59 | } 60 | } 61 | 62 | func BenchmarkReadAll(b *testing.B) { 63 | for i := 0; i < b.N; i++ { 64 | ReadAll() 65 | } 66 | } 67 | 68 | func BenchmarkWriteAll(b *testing.B) { 69 | text := "いろはにほへと" 70 | for i := 0; i < b.N; i++ { 71 | WriteAll(text) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /clipboard_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 @atotto. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build freebsd linux netbsd openbsd solaris dragonfly 6 | 7 | package clipboard 8 | 9 | import ( 10 | "errors" 11 | "os" 12 | "os/exec" 13 | ) 14 | 15 | const ( 16 | xsel = "xsel" 17 | xclip = "xclip" 18 | powershellExe = "powershell.exe" 19 | clipExe = "clip.exe" 20 | wlcopy = "wl-copy" 21 | wlpaste = "wl-paste" 22 | termuxClipboardGet = "termux-clipboard-get" 23 | termuxClipboardSet = "termux-clipboard-set" 24 | ) 25 | 26 | var ( 27 | Primary bool 28 | trimDos bool 29 | 30 | pasteCmdArgs []string 31 | copyCmdArgs []string 32 | 33 | xselPasteArgs = []string{xsel, "--output", "--clipboard"} 34 | xselCopyArgs = []string{xsel, "--input", "--clipboard"} 35 | 36 | xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"} 37 | xclipCopyArgs = []string{xclip, "-in", "-selection", "clipboard"} 38 | 39 | powershellExePasteArgs = []string{powershellExe, "Get-Clipboard"} 40 | clipExeCopyArgs = []string{clipExe} 41 | 42 | wlpasteArgs = []string{wlpaste, "--no-newline"} 43 | wlcopyArgs = []string{wlcopy} 44 | 45 | termuxPasteArgs = []string{termuxClipboardGet} 46 | termuxCopyArgs = []string{termuxClipboardSet} 47 | 48 | missingCommands = errors.New("No clipboard utilities available. Please install xsel, xclip, wl-clipboard or Termux:API add-on for termux-clipboard-get/set.") 49 | ) 50 | 51 | func init() { 52 | if os.Getenv("WAYLAND_DISPLAY") != "" { 53 | pasteCmdArgs = wlpasteArgs 54 | copyCmdArgs = wlcopyArgs 55 | 56 | if _, err := exec.LookPath(wlcopy); err == nil { 57 | if _, err := exec.LookPath(wlpaste); err == nil { 58 | return 59 | } 60 | } 61 | } 62 | 63 | pasteCmdArgs = xclipPasteArgs 64 | copyCmdArgs = xclipCopyArgs 65 | 66 | if _, err := exec.LookPath(xclip); err == nil { 67 | return 68 | } 69 | 70 | pasteCmdArgs = xselPasteArgs 71 | copyCmdArgs = xselCopyArgs 72 | 73 | if _, err := exec.LookPath(xsel); err == nil { 74 | return 75 | } 76 | 77 | pasteCmdArgs = termuxPasteArgs 78 | copyCmdArgs = termuxCopyArgs 79 | 80 | if _, err := exec.LookPath(termuxClipboardSet); err == nil { 81 | if _, err := exec.LookPath(termuxClipboardGet); err == nil { 82 | return 83 | } 84 | } 85 | 86 | pasteCmdArgs = powershellExePasteArgs 87 | copyCmdArgs = clipExeCopyArgs 88 | trimDos = true 89 | 90 | if _, err := exec.LookPath(clipExe); err == nil { 91 | if _, err := exec.LookPath(powershellExe); err == nil { 92 | return 93 | } 94 | } 95 | 96 | Unsupported = true 97 | } 98 | 99 | func getPasteCommand() *exec.Cmd { 100 | if Primary { 101 | pasteCmdArgs = pasteCmdArgs[:1] 102 | } 103 | return exec.Command(pasteCmdArgs[0], pasteCmdArgs[1:]...) 104 | } 105 | 106 | func getCopyCommand() *exec.Cmd { 107 | if Primary { 108 | copyCmdArgs = copyCmdArgs[:1] 109 | } 110 | return exec.Command(copyCmdArgs[0], copyCmdArgs[1:]...) 111 | } 112 | 113 | func readAll() (string, error) { 114 | if Unsupported { 115 | return "", missingCommands 116 | } 117 | pasteCmd := getPasteCommand() 118 | out, err := pasteCmd.Output() 119 | if err != nil { 120 | return "", err 121 | } 122 | result := string(out) 123 | if trimDos && len(result) > 1 { 124 | result = result[:len(result)-2] 125 | } 126 | return result, nil 127 | } 128 | 129 | func writeAll(text string) error { 130 | if Unsupported { 131 | return missingCommands 132 | } 133 | copyCmd := getCopyCommand() 134 | in, err := copyCmd.StdinPipe() 135 | if err != nil { 136 | return err 137 | } 138 | 139 | if err := copyCmd.Start(); err != nil { 140 | return err 141 | } 142 | if _, err := in.Write([]byte(text)); err != nil { 143 | return err 144 | } 145 | if err := in.Close(); err != nil { 146 | return err 147 | } 148 | return copyCmd.Wait() 149 | } 150 | -------------------------------------------------------------------------------- /clipboard_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 @atotto. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build windows 6 | 7 | package clipboard 8 | 9 | import ( 10 | "runtime" 11 | "syscall" 12 | "time" 13 | "unsafe" 14 | ) 15 | 16 | const ( 17 | cfUnicodetext = 13 18 | gmemMoveable = 0x0002 19 | ) 20 | 21 | var ( 22 | user32 = syscall.MustLoadDLL("user32") 23 | isClipboardFormatAvailable = user32.MustFindProc("IsClipboardFormatAvailable") 24 | openClipboard = user32.MustFindProc("OpenClipboard") 25 | closeClipboard = user32.MustFindProc("CloseClipboard") 26 | emptyClipboard = user32.MustFindProc("EmptyClipboard") 27 | getClipboardData = user32.MustFindProc("GetClipboardData") 28 | setClipboardData = user32.MustFindProc("SetClipboardData") 29 | 30 | kernel32 = syscall.NewLazyDLL("kernel32") 31 | globalAlloc = kernel32.NewProc("GlobalAlloc") 32 | globalFree = kernel32.NewProc("GlobalFree") 33 | globalLock = kernel32.NewProc("GlobalLock") 34 | globalUnlock = kernel32.NewProc("GlobalUnlock") 35 | lstrcpy = kernel32.NewProc("lstrcpyW") 36 | ) 37 | 38 | // waitOpenClipboard opens the clipboard, waiting for up to a second to do so. 39 | func waitOpenClipboard() error { 40 | started := time.Now() 41 | limit := started.Add(time.Second) 42 | var r uintptr 43 | var err error 44 | for time.Now().Before(limit) { 45 | r, _, err = openClipboard.Call(0) 46 | if r != 0 { 47 | return nil 48 | } 49 | time.Sleep(time.Millisecond) 50 | } 51 | return err 52 | } 53 | 54 | func readAll() (string, error) { 55 | // LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution). 56 | // Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock. 57 | runtime.LockOSThread() 58 | defer runtime.UnlockOSThread() 59 | if formatAvailable, _, err := isClipboardFormatAvailable.Call(cfUnicodetext); formatAvailable == 0 { 60 | return "", err 61 | } 62 | err := waitOpenClipboard() 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | h, _, err := getClipboardData.Call(cfUnicodetext) 68 | if h == 0 { 69 | _, _, _ = closeClipboard.Call() 70 | return "", err 71 | } 72 | 73 | l, _, err := globalLock.Call(h) 74 | if l == 0 { 75 | _, _, _ = closeClipboard.Call() 76 | return "", err 77 | } 78 | 79 | text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:]) 80 | 81 | r, _, err := globalUnlock.Call(h) 82 | if r == 0 { 83 | _, _, _ = closeClipboard.Call() 84 | return "", err 85 | } 86 | 87 | closed, _, err := closeClipboard.Call() 88 | if closed == 0 { 89 | return "", err 90 | } 91 | return text, nil 92 | } 93 | 94 | func writeAll(text string) error { 95 | // LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution). 96 | // Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock. 97 | runtime.LockOSThread() 98 | defer runtime.UnlockOSThread() 99 | 100 | err := waitOpenClipboard() 101 | if err != nil { 102 | return err 103 | } 104 | 105 | r, _, err := emptyClipboard.Call(0) 106 | if r == 0 { 107 | _, _, _ = closeClipboard.Call() 108 | return err 109 | } 110 | 111 | data := syscall.StringToUTF16(text) 112 | 113 | // "If the hMem parameter identifies a memory object, the object must have 114 | // been allocated using the function with the GMEM_MOVEABLE flag." 115 | h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0])))) 116 | if h == 0 { 117 | _, _, _ = closeClipboard.Call() 118 | return err 119 | } 120 | defer func() { 121 | if h != 0 { 122 | globalFree.Call(h) 123 | } 124 | }() 125 | 126 | l, _, err := globalLock.Call(h) 127 | if l == 0 { 128 | _, _, _ = closeClipboard.Call() 129 | return err 130 | } 131 | 132 | r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0]))) 133 | if r == 0 { 134 | _, _, _ = closeClipboard.Call() 135 | return err 136 | } 137 | 138 | r, _, err = globalUnlock.Call(h) 139 | if r == 0 { 140 | if err.(syscall.Errno) != 0 { 141 | _, _, _ = closeClipboard.Call() 142 | return err 143 | } 144 | } 145 | 146 | r, _, err = setClipboardData.Call(cfUnicodetext, h) 147 | if r == 0 { 148 | _, _, _ = closeClipboard.Call() 149 | return err 150 | } 151 | h = 0 // suppress deferred cleanup 152 | closed, _, err := closeClipboard.Call() 153 | if closed == 0 { 154 | return err 155 | } 156 | return nil 157 | } 158 | -------------------------------------------------------------------------------- /cmd/gocopy/gocopy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io/ioutil" 6 | "os" 7 | "time" 8 | 9 | "github.com/atotto/clipboard" 10 | ) 11 | 12 | func main() { 13 | timeout := flag.Duration("t", 0, "Erase clipboard after timeout. Durations are specified like \"20s\" or \"2h45m\". 0 (default) means never erase.") 14 | flag.Parse() 15 | 16 | out, err := ioutil.ReadAll(os.Stdin) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | if err := clipboard.WriteAll(string(out)); err != nil { 22 | panic(err) 23 | } 24 | 25 | if timeout != nil && *timeout > 0 { 26 | <-time.After(*timeout) 27 | text, err := clipboard.ReadAll() 28 | if err != nil { 29 | os.Exit(1) 30 | } 31 | if text == string(out) { 32 | err = clipboard.WriteAll("") 33 | } 34 | } 35 | if err != nil { 36 | os.Exit(1) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cmd/gopaste/gopaste.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/atotto/clipboard" 7 | ) 8 | 9 | func main() { 10 | text, err := clipboard.ReadAll() 11 | if err != nil { 12 | panic(err) 13 | } 14 | 15 | fmt.Print(text) 16 | } 17 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package clipboard_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/atotto/clipboard" 7 | ) 8 | 9 | func Example() { 10 | clipboard.WriteAll("日本語") 11 | text, _ := clipboard.ReadAll() 12 | fmt.Println(text) 13 | 14 | // Output: 15 | // 日本語 16 | } 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/atotto/clipboard 2 | --------------------------------------------------------------------------------