233 lines
7.5 KiB
Python
233 lines
7.5 KiB
Python
|
|
"""
|
||
|
|
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())}
|