gsproremote/backend/app/api/actions.py

233 lines
7.5 KiB
Python
Raw Permalink Normal View History

"""
Actions API for sending keyboard inputs to GSPro.
"""
import asyncio
import logging
from typing import Optional
from fastapi import APIRouter, HTTPException, BackgroundTasks
from pydantic import BaseModel, Field
from ..core.input_ctrl import press_key, press_keys, key_down, key_up, focus_window, is_gspro_running
from ..core.config import get_config
logger = logging.getLogger(__name__)
router = APIRouter()
class KeyPressRequest(BaseModel):
"""Request model for single key press."""
key: str = Field(..., description="Key or key combination to press (e.g., 'a', 'ctrl+m')")
delay: Optional[float] = Field(0.0, description="Delay in seconds before pressing the key")
class KeyHoldRequest(BaseModel):
"""Request model for key hold operations."""
key: str = Field(..., description="Key to hold or release")
duration: Optional[float] = Field(None, description="Duration to hold the key (seconds)")
class KeySequenceRequest(BaseModel):
"""Request model for key sequence."""
keys: list[str] = Field(..., description="List of keys to press in sequence")
interval: float = Field(0.1, description="Interval between key presses (seconds)")
class ActionResponse(BaseModel):
"""Response model for action endpoints."""
success: bool
message: str
key: Optional[str] = None
@router.post("/key", response_model=ActionResponse)
async def send_key(request: KeyPressRequest):
"""
Send a single key press or key combination to GSPro.
Supports:
- Single keys: 'a', 'space', 'enter'
- Combinations: 'ctrl+m', 'shift+tab'
- Function keys: 'f1', 'f11'
- Arrow keys: 'up', 'down', 'left', 'right'
"""
config = get_config()
# Check if GSPro is running
if not is_gspro_running():
raise HTTPException(status_code=409, detail="GSPro is not running or window not found")
# Focus GSPro window if auto-focus is enabled
if config.gspro.auto_focus:
if not focus_window(config.gspro.window_title):
logger.warning(f"Could not focus window: {config.gspro.window_title}")
try:
# Apply delay if specified
if request.delay > 0:
await asyncio.sleep(request.delay)
# Send the key press
if "+" in request.key:
# Handle key combination
press_keys(request.key)
else:
# Handle single key
press_key(request.key)
logger.info(f"Sent key press: {request.key}")
return ActionResponse(success=True, message=f"Key '{request.key}' pressed successfully", key=request.key)
except Exception as e:
logger.error(f"Failed to send key press: {e}")
raise HTTPException(status_code=500, detail=f"Failed to send key press: {str(e)}")
@router.post("/keydown", response_model=ActionResponse)
async def send_key_down(request: KeyHoldRequest, background_tasks: BackgroundTasks):
"""
Press and hold a key down.
If duration is specified, the key will be automatically released after that time.
Otherwise, you must call /keyup to release it.
"""
config = get_config()
if not is_gspro_running():
raise HTTPException(status_code=409, detail="GSPro is not running or window not found")
if config.gspro.auto_focus:
focus_window(config.gspro.window_title)
try:
key_down(request.key)
logger.info(f"Key down: {request.key}")
# If duration is specified, schedule key release
if request.duration:
async def release_key():
await asyncio.sleep(request.duration)
key_up(request.key)
logger.info(f"Key released after {request.duration}s: {request.key}")
background_tasks.add_task(release_key)
return ActionResponse(
success=True, message=f"Key '{request.key}' held for {request.duration}s", key=request.key
)
else:
return ActionResponse(
success=True, message=f"Key '{request.key}' pressed down (call /keyup to release)", key=request.key
)
except Exception as e:
logger.error(f"Failed to hold key down: {e}")
raise HTTPException(status_code=500, detail=f"Failed to hold key down: {str(e)}")
@router.post("/keyup", response_model=ActionResponse)
async def send_key_up(request: KeyHoldRequest):
"""
Release a held key.
"""
config = get_config()
if not is_gspro_running():
raise HTTPException(status_code=409, detail="GSPro is not running or window not found")
if config.gspro.auto_focus:
focus_window(config.gspro.window_title)
try:
key_up(request.key)
logger.info(f"Key up: {request.key}")
return ActionResponse(success=True, message=f"Key '{request.key}' released", key=request.key)
except Exception as e:
logger.error(f"Failed to release key: {e}")
raise HTTPException(status_code=500, detail=f"Failed to release key: {str(e)}")
@router.post("/sequence", response_model=ActionResponse)
async def send_key_sequence(request: KeySequenceRequest):
"""
Send a sequence of key presses with specified interval between them.
"""
config = get_config()
if not is_gspro_running():
raise HTTPException(status_code=409, detail="GSPro is not running or window not found")
if config.gspro.auto_focus:
focus_window(config.gspro.window_title)
try:
for i, key in enumerate(request.keys):
if "+" in key:
press_keys(key)
else:
press_key(key)
# Add interval between keys (except after last one)
if i < len(request.keys) - 1:
await asyncio.sleep(request.interval)
logger.info(f"Sent key sequence: {request.keys}")
return ActionResponse(
success=True, message=f"Sent {len(request.keys)} key presses", key=", ".join(request.keys)
)
except Exception as e:
logger.error(f"Failed to send key sequence: {e}")
raise HTTPException(status_code=500, detail=f"Failed to send key sequence: {str(e)}")
@router.get("/shortcuts")
async def get_shortcuts():
"""
Get a list of all available GSPro keyboard shortcuts.
"""
shortcuts = {
"aim": {"up": "up", "down": "down", "left": "left", "right": "right", "reset": "a"},
"club": {"up": "u", "down": "k"},
"shot": {"mulligan": "ctrl+m", "options": "'", "putt_toggle": "u"},
"tee": {"left": "c", "right": "v"},
"view": {
"map_toggle": "s",
"map_zoom_in": "q",
"map_zoom_out": "w",
"scorecard": "t",
"range_finder": "r",
"heat_map": "y",
"pin_indicator": "p",
"flyover": "o",
"free_look": "f5",
"aim_point": "f3",
"green_grid": "g",
"ui_toggle": "h",
},
"camera": {"go_to_ball": "5", "fly_to_ball": "6", "shot_camera": "j"},
"practice": {"go_to_ball": "8", "previous_hole": "9", "next_hole": "0"},
"system": {
"fullscreen": "f11",
"fps_display": "f",
"console_short": "f8",
"console_tall": "f9",
"tracer_clear": "f1",
},
"settings": {
"sound_on": "+",
"sound_off": "-",
"lighting": "l",
"3d_grass": "z",
"switch_hand": "n",
"shadow_increase": ">",
"shadow_decrease": "<",
},
}
return {"shortcuts": shortcuts, "total": sum(len(category) for category in shortcuts.values())}