package input import ( "errors" "runtime" "strings" "syscall" "unicode/utf16" "unsafe" ) const ( inputMouse = 0 inputKeyboard = 1 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") 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 Dx int32 Dy int32 MouseData uint32 Flags uint32 Time uint32 _ uint32 ExtraInfo uintptr } type keybdInput struct { Type uint32 _ uint32 Vk uint16 Scan uint16 Flags uint32 Time uint32 _ uint32 ExtraInfo uintptr } func EnableDPIAwareness() { _, _, _ = procSetProcessDPIAware.Call() } 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 } 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}, } 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 { inputs = append(inputs, keybdInput{Type: inputKeyboard, Scan: unit, Flags: keyeventfUnicode}, keybdInput{Type: inputKeyboard, Scan: unit, Flags: keyeventfUnicode | keyeventfKeyup}, ) } 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": return mouseLeftDown, mouseLeftUp, nil case "right": return mouseRightDown, mouseRightUp, nil default: return 0, 0, ErrBadButton } } func sendMouse(inputs []mouseInput) error { n, _, err := procSendInput.Call(uintptr(len(inputs)), uintptr(unsafe.Pointer(&inputs[0])), uintptr(unsafe.Sizeof(inputs[0]))) if n == 0 { return err } return nil } func sendKeys(inputs []keybdInput) error { n, _, err := procSendInput.Call(uintptr(len(inputs)), uintptr(unsafe.Pointer(&inputs[0])), uintptr(unsafe.Sizeof(inputs[0]))) if n == 0 { return err } return nil }