Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions sportscli/cli.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import sys

import typer
from rich.columns import Columns
from rich.panel import Panel
from rich.text import Text

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(
Expand Down Expand Up @@ -86,7 +91,7 @@ def config_show():


# ---------------------------------------------------------------------------
# Welcome screen
# Welcome screen (non-TTY / fallback)
# ---------------------------------------------------------------------------

def _show_welcome() -> None:
Expand Down Expand Up @@ -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__":
Expand Down
15 changes: 15 additions & 0 deletions sportscli/core/display.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from contextlib import contextmanager

from rich.console import Console
from rich.prompt import IntPrompt

console = Console()

Expand All @@ -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,
)
34 changes: 32 additions & 2 deletions sportscli/sports/chess/app.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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()
33 changes: 31 additions & 2 deletions sportscli/sports/cricket/app.py
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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()
34 changes: 32 additions & 2 deletions sportscli/sports/football/app.py
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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)
Expand Down Expand Up @@ -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()
Loading