350 lines
9.5 KiB
Python
350 lines
9.5 KiB
Python
"""
|
|
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()
|