//go:build windows
|
|
package input
|
|
import (
|
"fmt"
|
"syscall"
|
"time"
|
"unsafe"
|
)
|
|
var (
|
user32 = syscall.NewLazyDLL("user32.dll")
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
procOpenClipboard = user32.NewProc("OpenClipboard")
|
procCloseClipboard = user32.NewProc("CloseClipboard")
|
procEmptyClipboard = user32.NewProc("EmptyClipboard")
|
procSetClipboardData = user32.NewProc("SetClipboardData")
|
procGetClipboardData = user32.NewProc("GetClipboardData")
|
procIsClipboardFormatAvailable = user32.NewProc("IsClipboardFormatAvailable")
|
procSendInput = user32.NewProc("SendInput")
|
procGlobalAlloc = kernel32.NewProc("GlobalAlloc")
|
procGlobalLock = kernel32.NewProc("GlobalLock")
|
procGlobalUnlock = kernel32.NewProc("GlobalUnlock")
|
procGlobalSize = kernel32.NewProc("GlobalSize")
|
)
|
|
const (
|
cfUnicodeText = 13
|
gmemMoveable = 0x0002
|
inputKeyboard = 1
|
keyeventfKeyUp = 0x0002
|
keyeventfUnicode = 0x0004
|
vkControl = 0x11
|
vkV = 0x56
|
)
|
|
type keyboardInput struct {
|
wVk uint16
|
wScan uint16
|
dwFlags uint32
|
time uint32
|
dwExtraInfo uintptr
|
}
|
|
type input struct {
|
inputType uint32
|
ki keyboardInput
|
padding [8]byte // Alignment padding
|
}
|
|
type windowsPaster struct{}
|
|
func newPlatformPaster() Paster {
|
return &windowsPaster{}
|
}
|
|
func (p *windowsPaster) Paste(text string, keepClipboard bool) error {
|
// Save original clipboard content
|
var oldClip *string
|
if !keepClipboard {
|
oldClip = getClipboardText()
|
}
|
|
if err := setClipboard(text); err != nil {
|
return err
|
}
|
|
// Simulate Ctrl+V
|
inputs := []input{
|
makeKeyInput(vkControl, 0, 0),
|
makeKeyInput(vkV, 0, 0),
|
makeKeyInput(vkV, 0, keyeventfKeyUp),
|
makeKeyInput(vkControl, 0, keyeventfKeyUp),
|
}
|
|
ret, _, err := procSendInput.Call(
|
uintptr(len(inputs)),
|
uintptr(unsafe.Pointer(&inputs[0])),
|
uintptr(unsafe.Sizeof(inputs[0])),
|
)
|
if ret == 0 {
|
return fmt.Errorf("SendInput failed: %v", err)
|
}
|
|
// Restore original clipboard after a short delay (let Ctrl+V complete)
|
if oldClip != nil {
|
go func() {
|
time.Sleep(300 * time.Millisecond)
|
setClipboard(*oldClip)
|
}()
|
}
|
|
return nil
|
}
|
|
func (p *windowsPaster) TypeText(text string) error {
|
runes := []rune(text)
|
inputs := make([]input, len(runes)*2)
|
|
for i, r := range runes {
|
// Key down
|
inputs[i*2] = input{
|
inputType: inputKeyboard,
|
ki: keyboardInput{
|
wVk: 0,
|
wScan: uint16(r),
|
dwFlags: keyeventfUnicode,
|
},
|
}
|
// Key up
|
inputs[i*2+1] = input{
|
inputType: inputKeyboard,
|
ki: keyboardInput{
|
wVk: 0,
|
wScan: uint16(r),
|
dwFlags: keyeventfUnicode | keyeventfKeyUp,
|
},
|
}
|
}
|
|
ret, _, err := procSendInput.Call(
|
uintptr(len(inputs)),
|
uintptr(unsafe.Pointer(&inputs[0])),
|
uintptr(unsafe.Sizeof(inputs[0])),
|
)
|
if ret == 0 {
|
return fmt.Errorf("SendInput (unicode) failed: %v", err)
|
}
|
return nil
|
}
|
|
// getClipboardText reads the current clipboard text, returns nil if empty or non-text.
|
func getClipboardText() *string {
|
ret, _, _ := procIsClipboardFormatAvailable.Call(cfUnicodeText)
|
if ret == 0 {
|
return nil
|
}
|
|
for i := 0; i < 5; i++ {
|
ret, _, _ := procOpenClipboard.Call(0)
|
if ret != 0 {
|
hMem, _, _ := procGetClipboardData.Call(cfUnicodeText)
|
if hMem == 0 {
|
procCloseClipboard.Call()
|
return nil
|
}
|
|
size, _, _ := procGlobalSize.Call(hMem)
|
if size == 0 {
|
procCloseClipboard.Call()
|
return nil
|
}
|
|
ptr, _, _ := procGlobalLock.Call(hMem)
|
if ptr == 0 {
|
procCloseClipboard.Call()
|
return nil
|
}
|
|
// Read UTF-16 string
|
text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(ptr))[:size/2])
|
procGlobalUnlock.Call(hMem)
|
procCloseClipboard.Call()
|
return &text
|
}
|
time.Sleep(30 * time.Millisecond)
|
}
|
return nil
|
}
|
|
func setClipboard(text string) error {
|
// Retry opening clipboard up to 10 times
|
for i := 0; i < 10; i++ {
|
ret, _, _ := procOpenClipboard.Call(0)
|
if ret != 0 {
|
defer procCloseClipboard.Call()
|
|
procEmptyClipboard.Call()
|
|
utf16 := syscall.StringToUTF16(text)
|
size := len(utf16) * 2
|
|
hMem, _, _ := procGlobalAlloc.Call(gmemMoveable, uintptr(size))
|
if hMem == 0 {
|
return fmt.Errorf("GlobalAlloc failed")
|
}
|
|
ptr, _, _ := procGlobalLock.Call(hMem)
|
if ptr == 0 {
|
return fmt.Errorf("GlobalLock failed")
|
}
|
|
// Copy UTF-16 data
|
src := unsafe.Pointer(&utf16[0])
|
dst := unsafe.Pointer(ptr)
|
copy((*[1 << 30]byte)(dst)[:size], (*[1 << 30]byte)(src)[:size])
|
|
procGlobalUnlock.Call(hMem)
|
procSetClipboardData.Call(cfUnicodeText, hMem)
|
|
return nil
|
}
|
time.Sleep(50 * time.Millisecond)
|
}
|
return fmt.Errorf("failed to open clipboard after 10 attempts")
|
}
|
|
func makeKeyInput(vk uint16, scan uint16, flags uint32) input {
|
return input{
|
inputType: inputKeyboard,
|
ki: keyboardInput{
|
wVk: vk,
|
wScan: scan,
|
dwFlags: flags,
|
},
|
}
|
}
|