diff --git a/clipboard.go b/clipboard.go index d7907d3..35f6067 100644 --- a/clipboard.go +++ b/clipboard.go @@ -10,11 +10,28 @@ func ReadAll() (string, error) { return readAll() } +func ReadAllWithFormat(cf uintptr) (string, error) { + return readAllWithFormat(cf) +} + // WriteAll write string to clipboard func WriteAll(text string) error { return writeAll(text) } +// WriteAllWithFormat write string to clipboard with format +func WriteAllWithFormat(text string, cf uintptr) error { + return writeAllWithFormat(text, cf) +} + +func ClearClipboard() error { + return clearClipboard() +} + +func GetClipboardFormat(format string) (uintptr, error) { + return getClipboardFormat(format) +} + // Unsupported might be set true during clipboard init, to help callers decide // whether or not to offer clipboard options. var Unsupported bool diff --git a/clipboard_darwin.go b/clipboard_darwin.go index 6f33078..1845af9 100644 --- a/clipboard_darwin.go +++ b/clipboard_darwin.go @@ -32,6 +32,10 @@ func readAll() (string, error) { return string(out), nil } +func readAllWithFormat(_ uintptr) (string, error) { + return readAll() +} + func writeAll(text string) error { copyCmd := getCopyCommand() in, err := copyCmd.StdinPipe() @@ -50,3 +54,15 @@ func writeAll(text string) error { } return copyCmd.Wait() } + +func writeAllWithFormat(text string, _ uintptr) error { + return writeAll(text) +} + +func clearClipboard() error { + return nil +} + +func getClipboardFormat(_ string) (uintptr, error) { + return 0, nil +} diff --git a/clipboard_test.go b/clipboard_test.go index 075e657..fc4dce4 100644 --- a/clipboard_test.go +++ b/clipboard_test.go @@ -2,16 +2,14 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package clipboard_test +package clipboard import ( "testing" - - . "github.com/atotto/clipboard" ) func TestCopyAndPaste(t *testing.T) { - expected := "日本語" + expected := "日本語\n" err := WriteAll(expected) if err != nil { diff --git a/clipboard_unix.go b/clipboard_unix.go index 0acd5fa..2b74e27 100644 --- a/clipboard_unix.go +++ b/clipboard_unix.go @@ -75,6 +75,10 @@ func readAll() (string, error) { return string(out), nil } +func readAllWithFormat(_ uintptr) (string, error) { + return readAll() +} + func writeAll(text string) error { if Unsupported { return missingCommands @@ -96,3 +100,15 @@ func writeAll(text string) error { } return copyCmd.Wait() } + +func writeAllWithFormat(text string, _ uintptr) error { + return writeAll(text) +} + +func clearClipboard() error { + return nil +} + +func getClipboardFormat(_ string) (uintptr, error) { + return 0, nil +} diff --git a/clipboard_windows.go b/clipboard_windows.go index 4b4aedb..c0314c2 100644 --- a/clipboard_windows.go +++ b/clipboard_windows.go @@ -14,23 +14,26 @@ import ( const ( cfUnicodetext = 13 + cfText = 1 gmemMoveable = 0x0002 ) var ( - user32 = syscall.MustLoadDLL("user32") - openClipboard = user32.MustFindProc("OpenClipboard") - closeClipboard = user32.MustFindProc("CloseClipboard") - emptyClipboard = user32.MustFindProc("EmptyClipboard") - getClipboardData = user32.MustFindProc("GetClipboardData") - setClipboardData = user32.MustFindProc("SetClipboardData") + user32 = syscall.MustLoadDLL("user32") + openClipboard = user32.MustFindProc("OpenClipboard") + closeClipboard = user32.MustFindProc("CloseClipboard") + emptyClipboard = user32.MustFindProc("EmptyClipboard") + getClipboardData = user32.MustFindProc("GetClipboardData") + setClipboardData = user32.MustFindProc("SetClipboardData") + registerClipboardFormat = user32.MustFindProc("RegisterClipboardFormatW") kernel32 = syscall.NewLazyDLL("kernel32") globalAlloc = kernel32.NewProc("GlobalAlloc") globalFree = kernel32.NewProc("GlobalFree") + globalSize = kernel32.NewProc("GlobalSize") globalLock = kernel32.NewProc("GlobalLock") globalUnlock = kernel32.NewProc("GlobalUnlock") - lstrcpy = kernel32.NewProc("lstrcpyW") + memcpy = kernel32.NewProc("RtlCopyMemory") ) // waitOpenClipboard opens the clipboard, waiting for up to a second to do so. @@ -50,14 +53,27 @@ func waitOpenClipboard() error { } func readAll() (string, error) { + return readAllWithFormat(cfText) +} + +func readAllWithFormat(cf uintptr) (string, error) { err := waitOpenClipboard() if err != nil { return "", err } defer closeClipboard.Call() - h, _, err := getClipboardData.Call(cfUnicodetext) + h, _, err := getClipboardData.Call(cf) if h == 0 { + if err != syscall.Errno(0) { + return "", err + } + + return "", nil + } + + size, _, err := globalSize.Call(h) + if size == 0 { return "", err } @@ -66,7 +82,7 @@ func readAll() (string, error) { return "", err } - text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:]) + text := string((*[1 << 20]byte)(unsafe.Pointer(l))[:(size - 1)]) r, _, err := globalUnlock.Call(h) if r == 0 { @@ -77,25 +93,25 @@ func readAll() (string, error) { } func writeAll(text string) error { + return writeAllWithFormat(text, cfText) +} + +func writeAllWithFormat(text string, cf uintptr) error { err := waitOpenClipboard() if err != nil { return err } defer closeClipboard.Call() - r, _, err := emptyClipboard.Call(0) - if r == 0 { - return err - } - - data := syscall.StringToUTF16(text) + data := syscall.StringByteSlice(text) // "If the hMem parameter identifies a memory object, the object must have // been allocated using the function with the GMEM_MOVEABLE flag." - h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0])))) + h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data))) if h == 0 { return err } + defer func() { if h != 0 { globalFree.Call(h) @@ -107,7 +123,7 @@ func writeAll(text string) error { return err } - r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0]))) + r, _, err := memcpy.Call(l, uintptr(unsafe.Pointer(&data[0])), uintptr(len(data))) if r == 0 { return err } @@ -119,10 +135,40 @@ func writeAll(text string) error { } } - r, _, err = setClipboardData.Call(cfUnicodetext, h) + r, _, err = setClipboardData.Call(cf, h) if r == 0 { return err } + h = 0 // suppress deferred cleanup return nil } + +func getClipboardFormat(text string) (uintptr, error) { + ptr, err := syscall.UTF16PtrFromString(text) + if err != nil { + return 0, err + } + + cf, _, err := registerClipboardFormat.Call(uintptr(unsafe.Pointer(ptr))) + if cf == 0 { + return 0, err + } + + return cf, nil +} + +func clearClipboard() error { + err := waitOpenClipboard() + if err != nil { + return err + } + defer closeClipboard.Call() + + r, _, err := emptyClipboard.Call() + if r == 0 { + return err + } + + return nil +} diff --git a/cmd/goclip/.gitignore b/cmd/goclip/.gitignore new file mode 100644 index 0000000..87b5ffc --- /dev/null +++ b/cmd/goclip/.gitignore @@ -0,0 +1,3 @@ +bin +goclip +goclip.exe diff --git a/cmd/goclip/goclip.go b/cmd/goclip/goclip.go new file mode 100644 index 0000000..7f3a4a6 --- /dev/null +++ b/cmd/goclip/goclip.go @@ -0,0 +1,140 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "github.com/kiennq/clipboard" + "io/ioutil" + "os" + "strconv" +) + +const ( + HtmlMarkerBlock = "Version:0.9\n" + + "StartHTML:%010d\n" + + "EndHTML:%010d\n" + + "StartFragment:%010d\n" + + "EndFragment:%010d\n" + + HtmlHeader = "\n\n" + HtmlFooter = "\n\n" +) + +var ( + cfText uintptr = 1 +) + +func getHtmlBlock(htmlText string) string { + testPrefix := fmt.Sprintf(HtmlMarkerBlock, 0, 0, 0, 0) + markerBlockLen := len(testPrefix) // Should be always 100, can be optimized + headerLen := len(HtmlHeader) + footerLen := len(HtmlFooter) + prefix := fmt.Sprintf(HtmlMarkerBlock, + markerBlockLen, + markerBlockLen+headerLen+len(htmlText)+footerLen, + markerBlockLen+headerLen, + markerBlockLen+headerLen+len(htmlText)) + return prefix + HtmlHeader + htmlText + HtmlFooter +} + +func getHtmlClipboard() uintptr { + cf, err := clipboard.GetClipboardFormat("HTML Format") + if err != nil { + panic(err) + } + + return cf +} + +func getCfFromString(format string) uintptr { + cf := cfText + if format != "" { + atoiCf, err := strconv.Atoi(format) + cf = uintptr(atoiCf) + if err != nil { + // not integer, register new clipboard format + if cf, err = clipboard.GetClipboardFormat(format); err != nil { + panic(err) + } + } + } + + return cf +} + +func doCopy(dontClear bool) { + if !dontClear { + if err := clipboard.ClearClipboard(); err != nil { + panic(err) + } + } + + type Message struct { + Cf, Data string + } + + data, err := ioutil.ReadAll(os.Stdin) + if err != nil { + panic(err) + } + + var m []Message + if err := json.Unmarshal(data, &m); err != nil { + m = []Message{{"", string(data)}} + } + + for _, d := range m { + cf := getCfFromString(d.Cf) + if d.Data != "" { + switch d.Cf { + case "HTML Format": + d.Data = getHtmlBlock(d.Data) + } + + if err := clipboard.WriteAllWithFormat(d.Data, cf); err != nil { + panic(err) + } + } + } +} + +func doPaste(format string) { + cf := getCfFromString(format) + out, err := clipboard.ReadAllWithFormat(cf) + if err != nil { + panic(err) + } + + fmt.Println(out) +} + +func usage() { + fmt.Println("usage: goclip []") + fmt.Println("Available commands are:") + fmt.Println(" copy Copy to clipboard through stdin. The json format is [{cf:, data:},]") + fmt.Println(" paste Read from clipboard") +} + +func main() { + copyCmd := flag.NewFlagSet("copy", flag.ExitOnError) + dontClear := copyCmd.Bool("no-clear", false, "don't clear previous clipboard") + + pasteCmd := flag.NewFlagSet("paste", flag.ExitOnError) + format := pasteCmd.String("format", "", "paste from clipboard format. Can be string or integer.") + + flag.Usage = usage + flag.Parse() + + switch flag.Arg(0) { + case "copy": + copyCmd.Parse(os.Args[2:]) + doCopy(*dontClear) + case "paste": + pasteCmd.Parse(os.Args[2:]) + doPaste(*format) + default: + usage() + os.Exit(2) + } +} diff --git a/example_test.go b/example_test.go index 0f970a0..54a1ae0 100644 --- a/example_test.go +++ b/example_test.go @@ -1,14 +1,12 @@ -package clipboard_test +package clipboard import ( "fmt" - - "github.com/atotto/clipboard" ) func Example() { - clipboard.WriteAll("日本語") - text, _ := clipboard.ReadAll() + WriteAll("日本語") + text, _ := ReadAll() fmt.Println(text) // Output: