""" Windows input control module for simulating keyboard inputs to GSPro. """ import logging import time from typing import Optional, List try: import pydirectinput import win32gui import win32con import win32process import psutil except ImportError as e: raise ImportError(f"Required Windows dependencies not installed: {e}") logger = logging.getLogger(__name__) # Configure pydirectinput pydirectinput.PAUSE = 0.01 # Reduce default pause between actions def is_gspro_running(window_title: str = "GSPro") -> bool: """ Check if GSPro is running by looking for its window. Args: window_title: The window title to search for Returns: True if GSPro window is found, False otherwise """ def enum_window_callback(hwnd, windows): if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd): window_text = win32gui.GetWindowText(hwnd) if window_title.lower() in window_text.lower(): windows.append(hwnd) return True windows = [] win32gui.EnumWindows(enum_window_callback, windows) return len(windows) > 0 def find_gspro_window(window_title: str = "GSPro") -> Optional[int]: """ Find the GSPro window handle. Args: window_title: The window title to search for Returns: Window handle if found, None otherwise """ def enum_window_callback(hwnd, result): window_text = win32gui.GetWindowText(hwnd) if window_title.lower() in window_text.lower(): result.append(hwnd) return True result = [] win32gui.EnumWindows(enum_window_callback, result) if result: return result[0] return None def focus_window(window_title: str = "GSPro") -> bool: """ Focus the GSPro window to ensure it receives keyboard input. Args: window_title: The window title to focus Returns: True if window was focused successfully, False otherwise """ try: hwnd = find_gspro_window(window_title) if hwnd: # Restore window if minimized if win32gui.IsIconic(hwnd): win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) # Set foreground window win32gui.SetForegroundWindow(hwnd) # Small delay to ensure window is focused time.sleep(0.1) logger.debug(f"Focused window: {window_title}") return True else: logger.warning(f"Window not found: {window_title}") return False except Exception as e: logger.error(f"Failed to focus window: {e}") return False def press_key(key: str, interval: float = 0.0) -> None: """ Simulate a single key press. Args: key: The key to press (e.g., 'a', 'space', 'f1', 'up') interval: Time to wait after pressing the key """ try: # Normalize key name for pydirectinput key_normalized = key.lower().strip() # Handle special key mappings key_mappings = { "ctrl": "ctrl", "control": "ctrl", "alt": "alt", "shift": "shift", "tab": "tab", "space": "space", "enter": "enter", "return": "enter", "escape": "esc", "esc": "esc", "backspace": "backspace", "delete": "delete", "del": "delete", "insert": "insert", "ins": "insert", "home": "home", "end": "end", "pageup": "pageup", "pagedown": "pagedown", "up": "up", "down": "down", "left": "left", "right": "right", "plus": "+", "minus": "-", "apostrophe": "'", "quote": "'", } # Map key if needed key_to_press = key_mappings.get(key_normalized, key_normalized) # Press the key pydirectinput.press(key_to_press) if interval > 0: time.sleep(interval) logger.debug(f"Pressed key: {key}") except Exception as e: logger.error(f"Failed to press key '{key}': {e}") raise def press_keys(keys: str, interval: float = 0.0) -> None: """ Simulate a key combination or sequence. Args: keys: Key combination string (e.g., 'ctrl+m', 'shift+tab') interval: Time to wait after pressing the keys """ try: # Check if it's a key combination if "+" in keys: # Split into modifiers and key parts = keys.lower().split("+") modifiers = [] main_key = parts[-1] # Identify modifiers for part in parts[:-1]: if part in ["ctrl", "control"]: modifiers.append("ctrl") elif part in ["alt"]: modifiers.append("alt") elif part in ["shift"]: modifiers.append("shift") elif part in ["win", "windows", "cmd", "command"]: modifiers.append("win") # Press combination using hotkey if modifiers: hotkey_parts = modifiers + [main_key] pydirectinput.hotkey(*hotkey_parts) else: press_key(main_key) else: # Single key press press_key(keys) if interval > 0: time.sleep(interval) logger.debug(f"Pressed keys: {keys}") except Exception as e: logger.error(f"Failed to press keys '{keys}': {e}") raise def key_down(key: str) -> None: """ Hold a key down. Args: key: The key to hold down """ try: key_normalized = key.lower().strip() pydirectinput.keyDown(key_normalized) logger.debug(f"Key down: {key}") except Exception as e: logger.error(f"Failed to hold key down '{key}': {e}") raise def key_up(key: str) -> None: """ Release a held key. Args: key: The key to release """ try: key_normalized = key.lower().strip() pydirectinput.keyUp(key_normalized) logger.debug(f"Key up: {key}") except Exception as e: logger.error(f"Failed to release key '{key}': {e}") raise def type_text(text: str, interval: float = 0.0) -> None: """ Type a string of text. Args: text: The text to type interval: Time between each character """ try: pydirectinput.typewrite(text, interval=interval) logger.debug(f"Typed text: {text[:20]}...") except Exception as e: logger.error(f"Failed to type text: {e}") raise def mouse_click(x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> None: """ Simulate a mouse click. Args: x: X coordinate (None for current position) y: Y coordinate (None for current position) button: Mouse button ('left', 'right', 'middle') """ try: if x is not None and y is not None: pydirectinput.click(x, y, button=button) logger.debug(f"Mouse click at ({x}, {y}) with {button} button") else: pydirectinput.click(button=button) logger.debug(f"Mouse click with {button} button") except Exception as e: logger.error(f"Failed to perform mouse click: {e}") raise def mouse_move(x: int, y: int, duration: float = 0.0) -> None: """ Move the mouse cursor. Args: x: Target X coordinate y: Target Y coordinate duration: Time to take for the movement """ try: if duration > 0: pydirectinput.moveTo(x, y, duration=duration) else: pydirectinput.moveTo(x, y) logger.debug(f"Mouse moved to ({x}, {y})") except Exception as e: logger.error(f"Failed to move mouse: {e}") raise def get_gspro_process_info() -> Optional[dict]: """ Get information about the GSPro process if it's running. Returns: Dictionary with process info or None if not found """ try: for proc in psutil.process_iter(["pid", "name", "cpu_percent", "memory_info"]): if "gspro" in proc.info["name"].lower(): return { "pid": proc.info["pid"], "name": proc.info["name"], "cpu_percent": proc.info["cpu_percent"], "memory_mb": proc.info["memory_info"].rss / 1024 / 1024 if proc.info["memory_info"] else 0, } except Exception as e: logger.error(f"Failed to get GSPro process info: {e}") return None # Test function for development def test_input_control(): """Test function to verify input control is working.""" print("Testing input control...") # Check if GSPro is running if is_gspro_running(): print("✓ GSPro is running") # Try to focus the window if focus_window(): print("✓ GSPro window focused") else: print("✗ Could not focus GSPro window") else: print("✗ GSPro is not running") print("Please start GSPro and try again") return # Get process info info = get_gspro_process_info() if info: print( f"✓ GSPro process found: PID={info['pid']}, CPU={info['cpu_percent']:.1f}%, Memory={info['memory_mb']:.1f}MB" ) print("\nInput control test complete!") if __name__ == "__main__": # Run test when module is executed directly test_input_control()