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