package controllers import com.sun.jna.Native import com.sun.jna.Pointer import com.sun.jna.win32.StdCallLibrary import params.MouseWiggleParams import java.awt.MouseInfo import java.awt.Point import kotlin.random.Random /** * Interface for controllers that interact with the desktop. * * This defines methods for getting desktop state like the mouse pointer * location. * * Classes that implement this can serve as desktop automation controllers. */ interface DesktopController { /** * Gets the current pointer/mouse location on the desktop. * * This returns a [Point] representing the x, y coordinates of the mouse pointer on the screen. * * For example: * * ``` * val mouseLocation = getPointerLocation() * * println(mouseLocation) // Might print "Point[x=1920,y=1080]" * ``` * * @return The current [Point] location of the mouse pointer on the screen. */ fun getPointerLocation(): Point { return MouseInfo.getPointerInfo().location } /** * Gets a point near the given [point], with some random wiggle. * * This generates a new point that is randomly offset from the given [point] * by an amount controlled by the given [MouseWiggleParams]. * * Usage example: * * ``` * val base = Point(10, 20) * val params = WiggleParams(xWiggle = 5, yWiggle = 5) * val randomPoint = getAlmostPoint(base, params) * * // randomPoint might be (8, 22) * ``` * * Alternatively, params may be omitted to use the default WiggleParams values * ``` * val base = Point(10, 20) * val randomPoint = getAlmostPoint(base) * * // randomPoint might be (8, 22) * ``` * * @param point The base point to start from * @param params The wiggle parameters that control the random offset amount * @return A new [Point] near the given point with some random wiggle applied */ fun getAlmostPoint(point: Point, params: MouseWiggleParams = MouseWiggleParams()): Point { val xDel = Random.nextInt(0, params.xWiggle) val yDel = Random.nextInt(0, params.yWiggle) val xDir = if (Random.nextDouble() > 0.5) { 1 } else { -1 } val yDir = if (Random.nextDouble() > 0.5) { 1 } else { -1 } return Point(point.x + (xDel * xDir), point.y + (yDel * yDir)) } } interface OSProxy{ fun getActiveWindowName(): String } /** * Windows implementation of [DesktopController]. * * This class provides methods to interact with the desktop on Windows * by calling Win32 APIs. * * It implements the [DesktopController] interface to provide desktop * functionality like getting the mouse pointer location on Windows. */ class WindowsDesktopController : DesktopController, OSProxy { companion object{ /** * Converts a native byte buffer to a String. * * This takes a byte array [byteBuffer] containing text from a native Win32 call, * converts it to a String using JNA, and trims whitespace characters. * * Usage example: * * ``` * val buffer = ByteArray(256) * GetWindowTextA(hwnd, buffer, buffer.size) // Win32 call * * val windowTitle = nativeByteBufferToString(buffer) * println(windowTitle) // Print title string * ``` * * @param byteBuffer Byte array containing text from a native call * @return The native text as a String */ private fun nativeByteBufferToString(byteBuffer: ByteArray): String{ // I guess this prunes anything that isn't on the ascii table? val wText = Native.toString(byteBuffer).trim { it <= ' ' } return wText } } /** * Interface for calling Windows User32 API functions. * * This defines an interface extending StdCallLibrary to call native * Windows User32 library functions like EnumWindows, GetWindowTextA etc. * * Classes can implement this interface to make direct calls to the * User32 DLL on Windows. */ internal interface User32 : StdCallLibrary { /** * Interface for a Windows callback function to enumerate windows. * * This extends the StdCallLibrary.StdCallCallback to define a callback * method that will be invoked by the Windows API EnumWindows function. * * The callback method accepts a window handle (HWND) and a user-defined * pointer, and returns a Boolean indicating whether to continue enumeration. * * Usage example: * ``` * val callback = object : WNDENUMPROC { * override fun callback(hWnd: Pointer?, arg: Pointer?): Boolean { * // Check if hWnd matches target window * if (matchesTarget(hWnd)) { * // Found target window, stop enumeration * return false * } * // Keep enumerating * return true * } * } * ``` * * @param hWnd Window handle (HWND) for the current enumerated window. * @param arg User-defined data pointer passed to EnumWindows. * @return True to continue enumerating windows, false to stop. */ interface WNDENUMPROC : StdCallLibrary.StdCallCallback { fun callback(hWnd: Pointer?, arg: Pointer?): Boolean } /** * Enumerates windows on the system. * * This calls the Windows API EnumWindows function to enumerate all top-level windows. * * For each window, it calls the provided [WNDENUMPROC] callback function, * passing the window handle [hWnd] and user-defined [userData] pointer. * * Enumeration can be stopped by returning false from the callback. * * Usage example: * * ``` * val windows = mutableListOf() * * val callback = object : WNDENUMPROC { * override fun callback(hWnd: Pointer?, arg: Pointer?): Boolean { * windows.add(hWnd) // Add hWnd to list * return true // Continue enumerating * } * } * * EnumWindows(callback, null) // Get all top-level windows * * println(windows) // Print list of HWNDs * ``` * * @param lpEnumFunc The [WNDENUMPROC] callback to call for each window. * @param userData Optional user-defined data to pass to the callback. * @return True if successful, false otherwise. */ fun EnumWindows(lpEnumFunc: WNDENUMPROC?, userData: Pointer?): Boolean /** * Gets the title text of the specified window. * * This calls the Win32 API GetWindowTextA function to get the title * text for the window handle [hWnd]. * * The window text is copied into the [lpString] buffer up to [nMaxCount] characters. * * Usage example: * * ``` * val buffer = ByteArray(256) * val hwnd = getWindowHandle() // get some HWND * * GetWindowTextA(hwnd, buffer, buffer.size) * * val windowTitle = String(buffer) * println(windowTitle) * ``` * * @param hWnd The window handle (HWND). * @param lpString The buffer to receive the window text. * @param nMaxCount The maximum number of characters to copy to the buffer. * @return The length of the window text (excluding null-terminator). */ fun GetWindowTextA(hWnd: Pointer?, lpString: ByteArray?, nMaxCount: Int): Int fun GetForegroundWindow(): Pointer? companion object { val INSTANCE = Native.load("user32", User32::class.java) as User32 } } /** * Gets the title of the active/foreground window. * * This calls Win32 APIs to get the handle of the foreground window, * then gets its title text. * * Usage example: * * ``` * val activeWindowName = getActiveWindowName() * * println(activeWindowName) // Prints foreground window title * ``` * * @return The title text of the current foreground window. */ override fun getActiveWindowName(): String { val user32 = User32.INSTANCE val textBuffer = ByteArray(512) user32.GetWindowTextA(user32.GetForegroundWindow(), textBuffer, 512) return nativeByteBufferToString(textBuffer) } }