gsproremote/backend/app/core/input_ctrl.py

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()