2026-08-18 19:12:25 +03:00
|
|
|
package input
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
2026-08-28 13:20:04 +03:00
|
|
|
"fmt"
|
2026-08-28 12:47:14 +03:00
|
|
|
"runtime"
|
2026-08-18 19:12:25 +03:00
|
|
|
"strings"
|
|
|
|
|
"syscall"
|
2026-08-28 12:52:26 +03:00
|
|
|
"time"
|
2026-08-18 19:12:25 +03:00
|
|
|
"unicode/utf16"
|
|
|
|
|
"unsafe"
|
2026-08-28 12:52:26 +03:00
|
|
|
|
|
|
|
|
"tea.chunkbyte.com/kato/go-worm/lib/config"
|
2026-08-28 13:20:04 +03:00
|
|
|
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
|
2026-08-18 19:12:25 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
2026-08-28 13:20:04 +03:00
|
|
|
inputMouse = 0
|
2026-08-18 19:12:25 +03:00
|
|
|
inputKeyboard = 1
|
|
|
|
|
|
2026-08-28 13:20:04 +03:00
|
|
|
mouseMove = 0x0001
|
|
|
|
|
mouseLeftDown = 0x0002
|
|
|
|
|
mouseLeftUp = 0x0004
|
|
|
|
|
mouseRightDown = 0x0008
|
|
|
|
|
mouseRightUp = 0x0010
|
|
|
|
|
mouseAbsolute = 0x8000
|
|
|
|
|
mouseVirtualDesk = 0x4000
|
|
|
|
|
desktopAllAccess = 0x01FF
|
|
|
|
|
|
|
|
|
|
keyeventfExtended = 0x0001
|
|
|
|
|
keyeventfKeyup = 0x0002
|
|
|
|
|
keyeventfUnicode = 0x0004
|
|
|
|
|
|
|
|
|
|
smXVirtualScreen = 76
|
|
|
|
|
smYVirtualScreen = 77
|
|
|
|
|
smCXVirtualScreen = 78
|
|
|
|
|
smCYVirtualScreen = 79
|
2026-08-18 19:12:25 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
ErrBadButton = errors.New("button must be left or right")
|
|
|
|
|
ErrEmptyText = errors.New("text must not be empty")
|
|
|
|
|
|
2026-08-28 13:20:04 +03:00
|
|
|
user32 = syscall.NewLazyDLL("user32.dll")
|
|
|
|
|
procSetCursorPos = user32.NewProc("SetCursorPos")
|
|
|
|
|
procSendInput = user32.NewProc("SendInput")
|
2026-08-18 19:12:25 +03:00
|
|
|
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
2026-08-28 13:20:04 +03:00
|
|
|
procOpenInputDesktop = user32.NewProc("OpenInputDesktop")
|
|
|
|
|
procSetThreadDesktop = user32.NewProc("SetThreadDesktop")
|
|
|
|
|
procCloseDesktop = user32.NewProc("CloseDesktop")
|
|
|
|
|
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
|
|
|
|
|
|
|
|
|
inputStructSize = int(unsafe.Sizeof(winInput{}))
|
2026-08-18 19:12:25 +03:00
|
|
|
)
|
|
|
|
|
|
2026-08-28 13:20:04 +03:00
|
|
|
func init() {
|
|
|
|
|
if inputStructSize != 40 {
|
|
|
|
|
panic(fmt.Sprintf("winInput must be 40 bytes on this platform, got %d", inputStructSize))
|
|
|
|
|
}
|
|
|
|
|
if unsafe.Sizeof(winMouseInput{}) != 40 {
|
|
|
|
|
panic(fmt.Sprintf("winMouseInput must be 40 bytes on this platform, got %d", unsafe.Sizeof(winMouseInput{})))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// winInput matches the 40-byte INPUT struct on amd64 (8-byte header + 32-byte union).
|
|
|
|
|
type keyboardData struct {
|
|
|
|
|
Vk uint16
|
|
|
|
|
Scan uint16
|
|
|
|
|
Flags uint32
|
|
|
|
|
Time uint32
|
2026-08-18 19:12:25 +03:00
|
|
|
_ uint32
|
2026-08-28 13:20:04 +03:00
|
|
|
ExtraInfo uintptr
|
|
|
|
|
_ [8]byte
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type mouseData struct {
|
2026-08-18 19:12:25 +03:00
|
|
|
Dx int32
|
|
|
|
|
Dy int32
|
|
|
|
|
MouseData uint32
|
|
|
|
|
Flags uint32
|
|
|
|
|
Time uint32
|
2026-08-28 12:47:14 +03:00
|
|
|
_ uint32
|
2026-08-18 19:12:25 +03:00
|
|
|
ExtraInfo uintptr
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-28 13:20:04 +03:00
|
|
|
type winInput struct {
|
|
|
|
|
Type uint32
|
|
|
|
|
_ uint32
|
|
|
|
|
Ki keyboardData
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type winMouseInput struct {
|
|
|
|
|
Type uint32
|
|
|
|
|
_ uint32
|
|
|
|
|
Mi mouseData
|
2026-08-18 19:12:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func EnableDPIAwareness() {
|
|
|
|
|
_, _, _ = procSetProcessDPIAware.Call()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Click(x, y int, button string) error {
|
2026-08-28 12:47:14 +03:00
|
|
|
runtime.LockOSThread()
|
|
|
|
|
defer runtime.UnlockOSThread()
|
|
|
|
|
if err := attachInputDesktop(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 19:12:25 +03:00
|
|
|
down, up, err := mouseFlags(button)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-08-28 12:47:14 +03:00
|
|
|
|
|
|
|
|
absX, absY := absoluteCoords(x, y)
|
2026-08-28 13:20:04 +03:00
|
|
|
inputs := []winMouseInput{
|
|
|
|
|
{Type: inputMouse, Mi: mouseData{Dx: absX, Dy: absY, Flags: mouseMove | mouseAbsolute | mouseVirtualDesk}},
|
|
|
|
|
{Type: inputMouse, Mi: mouseData{Flags: down}},
|
|
|
|
|
{Type: inputMouse, Mi: mouseData{Flags: up}},
|
2026-08-18 19:12:25 +03:00
|
|
|
}
|
2026-08-28 13:20:04 +03:00
|
|
|
if err := sendMouseInputs(inputs); err != nil {
|
2026-08-28 12:47:14 +03:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, _, _ = procSetCursorPos.Call(uintptr(x), uintptr(y))
|
|
|
|
|
return nil
|
2026-08-18 19:12:25 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-28 12:52:26 +03:00
|
|
|
func TypeText(text string, delayMs int) error {
|
2026-08-18 19:12:25 +03:00
|
|
|
if text == "" {
|
|
|
|
|
return ErrEmptyText
|
|
|
|
|
}
|
2026-08-28 12:52:26 +03:00
|
|
|
delayMs = resolveKeyDelay(delayMs)
|
2026-08-28 12:47:14 +03:00
|
|
|
|
|
|
|
|
runtime.LockOSThread()
|
|
|
|
|
defer runtime.UnlockOSThread()
|
|
|
|
|
if err := attachInputDesktop(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-28 12:52:26 +03:00
|
|
|
runes := []rune(text)
|
|
|
|
|
for i, r := range runes {
|
|
|
|
|
if err := sendUnicodeRune(r); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if i < len(runes)-1 && delayMs > 0 {
|
|
|
|
|
time.Sleep(time.Duration(delayMs) * time.Millisecond)
|
|
|
|
|
}
|
2026-08-18 19:12:25 +03:00
|
|
|
}
|
2026-08-28 12:52:26 +03:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func resolveKeyDelay(ms int) int {
|
|
|
|
|
if ms < 0 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
if ms > config.MaxKeyDelayMs {
|
|
|
|
|
return config.MaxKeyDelayMs
|
|
|
|
|
}
|
|
|
|
|
return ms
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ResolveKeyDelay(ms int) int {
|
|
|
|
|
return resolveKeyDelay(ms)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sendUnicodeRune(r rune) error {
|
|
|
|
|
for _, unit := range utf16.Encode([]rune{r}) {
|
2026-08-28 13:20:04 +03:00
|
|
|
inputs := []winInput{
|
|
|
|
|
{Type: inputKeyboard, Ki: keyboardData{Scan: unit, Flags: keyeventfUnicode}},
|
|
|
|
|
{Type: inputKeyboard, Ki: keyboardData{Scan: unit, Flags: keyeventfUnicode | keyeventfKeyup}},
|
2026-08-28 12:52:26 +03:00
|
|
|
}
|
2026-08-28 13:20:04 +03:00
|
|
|
if err := sendKeyboardInputs(inputs); err != nil {
|
2026-08-28 12:52:26 +03:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2026-08-18 19:12:25 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-28 12:47:14 +03:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 19:12:25 +03:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-28 13:20:04 +03:00
|
|
|
func sendMouseInputs(inputs []winMouseInput) error {
|
|
|
|
|
return sendInput(len(inputs), unsafe.Pointer(&inputs[0]), unsafe.Sizeof(inputs[0]), "mouse")
|
2026-08-18 19:12:25 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-28 13:20:04 +03:00
|
|
|
func sendKeyboardInputs(inputs []winInput) error {
|
|
|
|
|
return sendInput(len(inputs), unsafe.Pointer(&inputs[0]), unsafe.Sizeof(inputs[0]), "keyboard")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sendInput(count int, ptr unsafe.Pointer, elemSize uintptr, kind string) error {
|
|
|
|
|
if count == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if int(elemSize) != inputStructSize {
|
|
|
|
|
return fmt.Errorf("input struct size mismatch: %s has %d bytes, want %d", kind, elemSize, inputStructSize)
|
|
|
|
|
}
|
|
|
|
|
n, _, err := procSendInput.Call(uintptr(count), uintptr(ptr), elemSize)
|
|
|
|
|
if n != 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil && err.Error() != "The operation completed successfully." {
|
|
|
|
|
helpers.Log.Printf("SendInput %s count=%d cbSize=%d: %v", kind, count, elemSize, err)
|
2026-08-18 19:12:25 +03:00
|
|
|
return err
|
|
|
|
|
}
|
2026-08-28 13:20:04 +03:00
|
|
|
helpers.Log.Printf("SendInput %s count=%d cbSize=%d failed (n=0)", kind, count, elemSize)
|
|
|
|
|
return fmt.Errorf("SendInput failed")
|
2026-08-18 19:12:25 +03:00
|
|
|
}
|