diff --git a/sportscli/cli.py b/sportscli/cli.py index 2308191..91f0d52 100644 --- a/sportscli/cli.py +++ b/sportscli/cli.py @@ -1,3 +1,5 @@ +import sys + import typer from rich.columns import Columns from rich.panel import Panel @@ -5,9 +7,12 @@ import sportscli.config as config from sportscli import __version__ -from sportscli.core.display import console, print_error, print_success, print_warning +from sportscli.core.display import console, print_error, print_success, print_warning, select_from_menu +from sportscli.sports.chess.app import _show_menu as _chess_menu from sportscli.sports.chess.app import app as chess_app +from sportscli.sports.cricket.app import _show_menu as _cricket_menu from sportscli.sports.cricket.app import app as cricket_app +from sportscli.sports.football.app import _show_menu as _football_menu from sportscli.sports.football.app import app as football_app app = typer.Typer( @@ -86,7 +91,7 @@ def config_show(): # --------------------------------------------------------------------------- -# Welcome screen +# Welcome screen (non-TTY / fallback) # --------------------------------------------------------------------------- def _show_welcome() -> None: @@ -119,10 +124,42 @@ def _show_welcome() -> None: console.print() +# --------------------------------------------------------------------------- +# Interactive root menu (TTY) +# --------------------------------------------------------------------------- + +def _show_header() -> None: + console.print() + console.print(Panel( + Text("⚽ 🏏 ♟ sportscli", style="bold white"), + border_style="bold white", + subtitle=f"v{__version__}", + padding=(0, 2), + )) + + +def _show_root_menu() -> None: + _show_header() + choice = select_from_menu("Select a sport", [ + ("chess", "Tournaments, live games, broadcasts, player profiles"), + ("cricket", "Live scores, scorecards, schedule"), + ("football", "Live scores, standings, fixtures"), + ]) + if choice == 1: + _chess_menu() + elif choice == 2: + _cricket_menu() + elif choice == 3: + _football_menu() + + @app.callback(invoke_without_command=True) def main(ctx: typer.Context): if ctx.invoked_subcommand is None: - _show_welcome() + if sys.stdin.isatty(): + _show_root_menu() + else: + _show_welcome() if __name__ == "__main__": diff --git a/sportscli/core/display.py b/sportscli/core/display.py index 0b92958..b378804 100644 --- a/sportscli/core/display.py +++ b/sportscli/core/display.py @@ -1,6 +1,7 @@ from contextlib import contextmanager from rich.console import Console +from rich.prompt import IntPrompt console = Console() @@ -21,3 +22,17 @@ def print_success(msg: str) -> None: def status_spinner(msg: str): with console.status(f"[bold cyan]{msg}[/bold cyan]"): yield + + +def select_from_menu(title: str, options: list[tuple[str, str]]) -> int: + """Display a numbered menu and return the 1-based selection.""" + console.print() + console.print(f"[bold cyan]{title}[/bold cyan]") + for i, (name, desc) in enumerate(options, 1): + console.print(f" [bold cyan][{i}][/bold cyan] [bold]{name:<14}[/bold] [dim]{desc}[/dim]") + console.print() + return IntPrompt.ask( + "Choose", + choices=[str(i) for i in range(1, len(options) + 1)], + show_choices=False, + ) diff --git a/sportscli/sports/chess/app.py b/sportscli/sports/chess/app.py index 9d3b3e2..a68d633 100644 --- a/sportscli/sports/chess/app.py +++ b/sportscli/sports/chess/app.py @@ -1,11 +1,17 @@ +import sys + import typer +from rich.prompt import Prompt -from sportscli.core.display import print_error, status_spinner +from sportscli.core.display import print_error, select_from_menu, status_spinner from sportscli.core.exceptions import ApiError, NetworkError from sportscli.sports.chess import display from sportscli.sports.chess.client import LichessClient -app = typer.Typer(help="Chess data from Lichess (no API key required).") +app = typer.Typer( + help="Chess data from Lichess (no API key required).", + no_args_is_help=False, +) @app.command() @@ -68,3 +74,27 @@ def player(username: str = typer.Argument(..., help="Lichess username")): except ApiError as e: print_error(f"API error: {e}") raise typer.Exit(1) + + +def _show_menu() -> None: + choice = select_from_menu("Chess — Select a command", [ + ("tournaments", "Current and upcoming tournaments"), + ("live", "Live games on Lichess TV"), + ("broadcasts", "Major chess broadcasts"), + ("player", "Player profile and recent games"), + ]) + if choice == 1: + tournaments() + elif choice == 2: + live() + elif choice == 3: + broadcasts() + elif choice == 4: + username = Prompt.ask("Lichess username") + player(username) + + +@app.callback(invoke_without_command=True) +def _main(ctx: typer.Context) -> None: + if ctx.invoked_subcommand is None and sys.stdin.isatty(): + _show_menu() diff --git a/sportscli/sports/cricket/app.py b/sportscli/sports/cricket/app.py index 338aef2..3cd771d 100644 --- a/sportscli/sports/cricket/app.py +++ b/sportscli/sports/cricket/app.py @@ -1,12 +1,18 @@ +import sys + import typer +from rich.prompt import Prompt import sportscli.config as config -from sportscli.core.display import print_error, status_spinner +from sportscli.core.display import console, print_error, select_from_menu, status_spinner from sportscli.core.exceptions import ApiError, AuthError, NetworkError from sportscli.sports.cricket import display from sportscli.sports.cricket.client import CricketDataClient -app = typer.Typer(help="Cricket live scores and schedules (cricketdata.org API key required).") +app = typer.Typer( + help="Cricket live scores and schedules (cricketdata.org API key required).", + no_args_is_help=False, +) _SPORT = "cricket" @@ -76,3 +82,26 @@ def schedule(): except ApiError as e: print_error(f"API error: {e}") raise typer.Exit(1) + + +def _show_menu() -> None: + choice = select_from_menu("Cricket — Select a command", [ + ("live", "Currently live matches"), + ("scorecard", "Detailed scorecard for a match"), + ("schedule", "Upcoming matches"), + ]) + if choice == 1: + live() + elif choice == 2: + console.print("\n[dim]Fetching live matches so you can pick a match ID...[/dim]") + live() + match_id = Prompt.ask("\nEnter match ID from the list above") + scorecard(match_id) + elif choice == 3: + schedule() + + +@app.callback(invoke_without_command=True) +def _main(ctx: typer.Context) -> None: + if ctx.invoked_subcommand is None and sys.stdin.isatty(): + _show_menu() diff --git a/sportscli/sports/football/app.py b/sportscli/sports/football/app.py index dc80c5a..34b2011 100644 --- a/sportscli/sports/football/app.py +++ b/sportscli/sports/football/app.py @@ -1,12 +1,18 @@ +import sys + import typer +from rich.prompt import Prompt import sportscli.config as config -from sportscli.core.display import print_error, status_spinner +from sportscli.core.display import print_error, select_from_menu, status_spinner from sportscli.core.exceptions import ApiError, AuthError, NetworkError from sportscli.sports.football import display from sportscli.sports.football.client import LEAGUE_IDS, FootballDataClient -app = typer.Typer(help="Football scores, standings, and fixtures (football-data.org API key required).") +app = typer.Typer( + help="Football scores, standings, and fixtures (football-data.org API key required).", + no_args_is_help=False, +) _SPORT = "football" @@ -15,6 +21,8 @@ "pd (La Liga), fl1 (Ligue 1), ucl (Champions League)" ) +_LEAGUE_PROMPT = "League code (pl/bl1/sa/pd/fl1/ucl)" + def _get_client() -> FootballDataClient: key = config.get_api_key(_SPORT) @@ -86,3 +94,25 @@ def fixtures( except ApiError as e: print_error(f"API error: {e}") raise typer.Exit(1) + + +def _show_menu() -> None: + choice = select_from_menu("Football — Select a command", [ + ("live", "All currently live matches"), + ("standings", "League table"), + ("fixtures", "Upcoming fixtures"), + ]) + if choice == 1: + live() + elif choice == 2: + league = Prompt.ask(_LEAGUE_PROMPT) + standings(league) + elif choice == 3: + league = Prompt.ask(_LEAGUE_PROMPT) + fixtures(league) + + +@app.callback(invoke_without_command=True) +def _main(ctx: typer.Context) -> None: + if ctx.invoked_subcommand is None and sys.stdin.isatty(): + _show_menu()