gsproremote/backend/app/core/input_ctrl.py

351 lines
9.5 KiB
Python
Raw Normal View History

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