Initial commit: GSPro Remote MVP - Phase 1 complete
This commit is contained in:
commit
74ca4b38eb
50 changed files with 12818 additions and 0 deletions
350
backend/app/core/input_ctrl.py
Normal file
350
backend/app/core/input_ctrl.py
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
"""
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue