From d7274f34e0f3005fd68f5c213c8fbaeca71eb140 Mon Sep 17 00:00:00 2001 From: Daniel Legt Date: Fri, 28 Aug 2026 12:47:14 +0300 Subject: [PATCH] Enhance input handling by adding support for absolute mouse coordinates and desktop attachment. Implement runtime locking for thread safety during input operations. Update mouse input flags and add new functions for desktop management. --- lib/input/input.go | 93 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 16 deletions(-) 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":