diff --git a/lib/input/input.go b/lib/input/input.go index 0eed8fe..f31ef38 100644 --- a/lib/input/input.go +++ b/lib/input/input.go @@ -2,6 +2,7 @@ package input import ( "errors" + "runtime" "strings" "syscall" "unicode/utf16" @@ -12,25 +13,37 @@ const ( inputMouse = 0 inputKeyboard = 1 - mouseLeftDown = 0x0002 - mouseLeftUp = 0x0004 - mouseRightDown = 0x0008 - mouseRightUp = 0x0010 - - keyeventfKeyup = 0x0002 - keyeventfUnicode = 0x0004 + mouseMove = 0x0001 + mouseLeftDown = 0x0002 + mouseLeftUp = 0x0004 + mouseRightDown = 0x0008 + mouseRightUp = 0x0010 + mouseAbsolute = 0x8000 + mouseVirtualDesk = 0x4000 + desktopAllAccess = 0x01FF + keyeventfKeyup = 0x0002 + keyeventfUnicode = 0x0004 + smXVirtualScreen = 76 + smYVirtualScreen = 77 + smCXVirtualScreen = 78 + smCYVirtualScreen = 79 ) var ( ErrBadButton = errors.New("button must be left or right") ErrEmptyText = errors.New("text must not be empty") - user32 = syscall.NewLazyDLL("user32.dll") - procSetCursorPos = user32.NewProc("SetCursorPos") - procSendInput = user32.NewProc("SendInput") + user32 = syscall.NewLazyDLL("user32.dll") + procSetCursorPos = user32.NewProc("SetCursorPos") + procSendInput = user32.NewProc("SendInput") procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware") + procOpenInputDesktop = user32.NewProc("OpenInputDesktop") + procSetThreadDesktop = user32.NewProc("SetThreadDesktop") + procCloseDesktop = user32.NewProc("CloseDesktop") + procGetSystemMetrics = user32.NewProc("GetSystemMetrics") ) +// ponytail: INPUT must be 40 bytes on amd64; wrong size yields SendInput failures. type mouseInput struct { Type uint32 _ uint32 @@ -39,6 +52,7 @@ type mouseInput struct { MouseData uint32 Flags uint32 Time uint32 + _ uint32 ExtraInfo uintptr } @@ -49,8 +63,8 @@ type keybdInput struct { Scan uint16 Flags uint32 Time uint32 + _ uint32 ExtraInfo uintptr - _ [8]byte } func EnableDPIAwareness() { @@ -58,25 +72,43 @@ func EnableDPIAwareness() { } func Click(x, y int, button string) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if err := attachInputDesktop(); err != nil { + return err + } + down, up, err := mouseFlags(button) if err != nil { return err } - ok, _, callErr := procSetCursorPos.Call(uintptr(x), uintptr(y)) - if ok == 0 { - return callErr - } + + absX, absY := absoluteCoords(x, y) inputs := []mouseInput{ + {Type: inputMouse, Dx: absX, Dy: absY, Flags: mouseMove | mouseAbsolute | mouseVirtualDesk}, {Type: inputMouse, Flags: down}, {Type: inputMouse, Flags: up}, } - return sendMouse(inputs) + if err := sendMouse(inputs); err != nil { + return err + } + + // ponytail: best-effort cursor sync for the local user; ignore denial after SendInput click. + _, _, _ = procSetCursorPos.Call(uintptr(x), uintptr(y)) + return nil } func TypeText(text string) error { if text == "" { return ErrEmptyText } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if err := attachInputDesktop(); err != nil { + return err + } + units := utf16.Encode([]rune(text)) inputs := make([]keybdInput, 0, len(units)*2) for _, unit := range units { @@ -88,6 +120,35 @@ func TypeText(text string) error { return sendKeys(inputs) } +func attachInputDesktop() error { + handle, _, err := procOpenInputDesktop.Call(0, 0, desktopAllAccess) + if handle == 0 { + return err + } + defer procCloseDesktop.Call(handle) + ok, _, err := procSetThreadDesktop.Call(handle) + if ok == 0 { + return err + } + return nil +} + +func absoluteCoords(x, y int) (int32, int32) { + left, _, _ := procGetSystemMetrics.Call(smXVirtualScreen) + top, _, _ := procGetSystemMetrics.Call(smYVirtualScreen) + width, _, _ := procGetSystemMetrics.Call(smCXVirtualScreen) + height, _, _ := procGetSystemMetrics.Call(smCYVirtualScreen) + if width <= 1 { + width = 1 + } + if height <= 1 { + height = 1 + } + absX := int32((int64(x-int(left)) * 65535) / int64(width-1)) + absY := int32((int64(y-int(top)) * 65535) / int64(height-1)) + return absX, absY +} + func mouseFlags(button string) (down, up uint32, err error) { switch strings.ToLower(strings.TrimSpace(button)) { case "", "left":