diff --git a/README.md b/README.md index 6b1a84b..e3eae3f 100644 --- a/README.md +++ b/README.md @@ -50,88 +50,6 @@ cd faceoff uv run faceoff ``` -## Usage - -### Schedule Screen (Main Screen) - -| Key | Action | -|-----|--------| -| `Arrow keys` | Navigate between game cards | -| `h` | Previous day | -| `l` | Next day | -| `t` | Jump to today | -| `s` | View standings | -| `p` | View player stats | -| `m` | Browse teams | -| `r` | Refresh | -| `Enter` | Select game (for live/completed games) | -| `q` | Quit | - -### Game Screen - -| Key | Action | -|-----|--------| -| `b` or `Escape` | Back to schedule | -| `r` | Refresh | -| `Tab` | Switch between tabs (Play-by-Play, Box Score, Summary) | -| `q` | Quit | - -### Pre-Game Screen - -| Key | Action | -|-----|--------| -| `b` or `Escape` | Back to schedule | -| `r` | Refresh | -| `q` | Quit | - -### Standings Screen - -| Key | Action | -|-----|--------| -| `Tab` | Switch between views (Wild Card, Division, Conference, League) | -| `Up/Down` or `j/k` | Scroll standings | -| `b` or `Escape` | Back to schedule | -| `r` | Refresh | -| `q` | Quit | - -### Stats Screen - -| Key | Action | -|-----|--------| -| `Tab` | Switch between Skaters and Goalies | -| `b` or `Escape` | Back to schedule | -| `r` | Refresh | -| `q` | Quit | - -### Teams Screen - -| Key | Action | -|-----|--------| -| `Arrow keys` | Navigate between team cards | -| `Enter` | Select team to view details | -| `b` or `Escape` | Back to schedule | -| `r` | Refresh | -| `q` | Quit | - -### Team Detail Screen - -| Key | Action | -|-----|--------| -| `Tab` | Switch between Roster and Schedule | -| `Enter` | Select player to view profile | -| `b` or `Escape` | Back to teams | -| `r` | Refresh | -| `q` | Quit | - -### Player Screen - -| Key | Action | -|-----|--------| -| `Tab` | Switch between Info, Stats, and Game Log | -| `b` or `Escape` | Back | -| `r` | Refresh | -| `q` | Quit | - ## Screenshot ![Faceoff Screenshot](screenshot.png) diff --git a/docs/index.md b/docs/index.md index 5febe05..2c3e5d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,88 +40,6 @@ pip install faceoff faceoff ``` -## Navigation - -### Schedule Screen (Main Screen) - -| Key | Action | -|-----|--------| -| `Arrow keys` | Navigate between game cards | -| `h` | Previous day | -| `l` | Next day | -| `t` | Jump to today | -| `s` | View standings | -| `p` | View player stats | -| `m` | Browse teams | -| `r` | Refresh | -| `Enter` | Select game (for live/completed games) | -| `q` | Quit | - -### Game Screen - -| Key | Action | -|-----|--------| -| `b` or `Escape` | Back to schedule | -| `r` | Refresh | -| `Tab` | Switch between tabs (Play-by-Play, Box Score, Summary) | -| `q` | Quit | - -### Pre-Game Screen - -| Key | Action | -|-----|--------| -| `b` or `Escape` | Back to schedule | -| `r` | Refresh | -| `q` | Quit | - -### Standings Screen - -| Key | Action | -|-----|--------| -| `Tab` | Switch between views (Wild Card, Division, Conference, League) | -| `Up/Down` or `j/k` | Scroll standings | -| `b` or `Escape` | Back to schedule | -| `r` | Refresh | -| `q` | Quit | - -### Stats Screen - -| Key | Action | -|-----|--------| -| `Tab` | Switch between Skaters and Goalies | -| `b` or `Escape` | Back to schedule | -| `r` | Refresh | -| `q` | Quit | - -### Teams Screen - -| Key | Action | -|-----|--------| -| `Arrow keys` | Navigate between team cards | -| `Enter` | Select team to view details | -| `b` or `Escape` | Back to schedule | -| `r` | Refresh | -| `q` | Quit | - -### Team Detail Screen - -| Key | Action | -|-----|--------| -| `Tab` | Switch between Roster and Schedule | -| `Enter` | Select player to view profile | -| `b` or `Escape` | Back to teams | -| `r` | Refresh | -| `q` | Quit | - -### Player Screen - -| Key | Action | -|-----|--------| -| `Tab` | Switch between Info, Stats, and Game Log | -| `b` or `Escape` | Back | -| `r` | Refresh | -| `q` | Quit | - ## Acknowledgments This project was inspired by [Playball](https://github.com/paaatrick/playball), a similar terminal application for following MLB baseball games. diff --git a/src/faceoff/screens/game.py b/src/faceoff/screens/game.py index b6729f2..f7b0370 100644 --- a/src/faceoff/screens/game.py +++ b/src/faceoff/screens/game.py @@ -246,6 +246,8 @@ class GameScreen(Screen): } """ + REFRESH_INTERVAL: ClassVar[int] = 30 # Seconds between auto-refreshes + def __init__(self, client: NHLClient, game_id: int, game_data: dict, **kwargs) -> None: super().__init__(**kwargs) self.client = client @@ -255,6 +257,8 @@ def __init__(self, client: NHLClient, game_id: int, game_data: dict, **kwargs) - self.play_by_play: list = [] self.scoring_summary: list = [] # From landing page self._refresh_timer: Timer | None = None + self._countdown_timer: Timer | None = None + self._countdown: int = self.REFRESH_INTERVAL self._last_width: int = 0 def compose(self) -> ComposeResult: @@ -267,15 +271,20 @@ def on_mount(self) -> None: """Load game data when screen is mounted.""" self.load_game_data() - # Set up auto-refresh for pre-game and live games (every 30 seconds) + # Set up auto-refresh for pre-game and live games game_state = self.game_data.get("gameState", "FUT") if game_state in ("PRE", "LIVE", "CRIT"): - self._refresh_timer = self.set_interval(30, callback=self._auto_refresh) + self._countdown = self.REFRESH_INTERVAL + self._refresh_timer = self.set_interval(30, callback=self._auto_refresh) # type: ignore[arg-type] + self._countdown_timer = self.set_interval(1, callback=self._update_countdown) + self._update_subtitle() def on_unmount(self) -> None: """Clean up when screen is unmounted.""" if self._refresh_timer: self._refresh_timer.stop() + if self._countdown_timer: + self._countdown_timer.stop() def on_resize(self, event) -> None: """Handle resize to adjust layout.""" @@ -792,8 +801,21 @@ def _get_assists(self, details: dict) -> list[str]: assists.append(name) return assists + def _update_countdown(self) -> None: + """Update the countdown timer every second.""" + self._countdown -= 1 + if self._countdown < 0: + self._countdown = self.REFRESH_INTERVAL + self._update_subtitle() + + def _update_subtitle(self) -> None: + """Update the screen subtitle with countdown.""" + self.sub_title = f"Refreshing in {self._countdown}s" + def _auto_refresh(self) -> None: """Auto-refresh game data.""" + self._countdown = self.REFRESH_INTERVAL + self._update_subtitle() self.client.clear_cache() self.load_game_data() @@ -803,6 +825,8 @@ def action_back(self) -> None: def action_refresh(self) -> None: """Manually refresh game data.""" + self._countdown = self.REFRESH_INTERVAL + self._update_subtitle() self.client.clear_cache() self.load_game_data() self.notify("Refreshed") diff --git a/src/faceoff/screens/schedule.py b/src/faceoff/screens/schedule.py index 18a6695..ef70e48 100644 --- a/src/faceoff/screens/schedule.py +++ b/src/faceoff/screens/schedule.py @@ -1,6 +1,6 @@ """Schedule screen for browsing games.""" -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from typing import ClassVar from zoneinfo import ZoneInfo @@ -21,7 +21,7 @@ NHL_TIMEZONE = ZoneInfo("America/New_York") -def get_nhl_today() -> datetime: +def get_nhl_today() -> date: """Get the current date in NHL timezone (Eastern Time).""" return datetime.now(NHL_TIMEZONE).date() @@ -97,12 +97,16 @@ class ScheduleScreen(Screen): } """ + REFRESH_INTERVAL: ClassVar[int] = 30 # Seconds between auto-refreshes + def __init__(self, client: NHLClient, **kwargs) -> None: super().__init__(**kwargs) self.client = client self.current_date = get_nhl_today() self.games: list = [] self._refresh_timer: Timer | None = None + self._countdown_timer: Timer | None = None + self._countdown: int = self.REFRESH_INTERVAL self._last_width: int = 0 def compose(self) -> ComposeResult: @@ -116,13 +120,18 @@ def compose(self) -> ComposeResult: def on_mount(self) -> None: """Load games when the screen is mounted.""" self.load_games() - # Set up auto-refresh every 30 seconds - self._refresh_timer = self.set_interval(30, callback=self._auto_refresh) + # Set up auto-refresh every 30 seconds (only for today's games) + self._countdown = self.REFRESH_INTERVAL + self._refresh_timer = self.set_interval(30, callback=self._auto_refresh) # type: ignore[arg-type] + self._countdown_timer = self.set_interval(1, callback=self._update_countdown) + self._update_subtitle() def on_unmount(self) -> None: """Clean up when screen is unmounted.""" if self._refresh_timer: self._refresh_timer.stop() + if self._countdown_timer: + self._countdown_timer.stop() def _format_date(self) -> str: """Format the current date for display.""" @@ -215,10 +224,26 @@ def on_resize(self, event) -> None: self._last_width = new_width self._update_games_display() + def _update_countdown(self) -> None: + """Update the countdown timer every second.""" + self._countdown -= 1 + if self._countdown < 0: + self._countdown = self.REFRESH_INTERVAL + self._update_subtitle() + + def _update_subtitle(self) -> None: + """Update the screen subtitle with countdown (only for today).""" + if self.current_date == get_nhl_today(): + self.sub_title = f"Refreshing in {self._countdown}s" + else: + self.sub_title = "" + def _auto_refresh(self) -> None: """Auto-refresh games (for live updates).""" + self._countdown = self.REFRESH_INTERVAL # Only refresh if viewing today's games if self.current_date == get_nhl_today(): + self._update_subtitle() self.client.clear_cache() self.load_games() @@ -248,20 +273,26 @@ def on_game_card_selected(self, event: GameCard.Selected) -> None: def action_prev_day(self) -> None: """Go to previous day.""" self.current_date -= timedelta(days=1) + self._update_subtitle() self.load_games() def action_next_day(self) -> None: """Go to next day.""" self.current_date += timedelta(days=1) + self._update_subtitle() self.load_games() def action_today(self) -> None: """Go to today.""" self.current_date = get_nhl_today() + self._countdown = self.REFRESH_INTERVAL + self._update_subtitle() self.load_games() def action_refresh(self) -> None: """Manually refresh games.""" + self._countdown = self.REFRESH_INTERVAL + self._update_subtitle() self.client.clear_cache() self.load_games() self.notify("Refreshed") diff --git a/src/faceoff/screens/standings.py b/src/faceoff/screens/standings.py index 052dfd7..5342086 100644 --- a/src/faceoff/screens/standings.py +++ b/src/faceoff/screens/standings.py @@ -22,8 +22,10 @@ class StandingsScreen(Screen): Binding("down,j", "scroll_down", "Scroll Down", show=False), ] - # Minimum width to display conferences side-by-side (two columns of ~40 chars each) - SIDE_BY_SIDE_MIN_WIDTH = 90 + # Minimum width to display conferences side-by-side + # Each conference needs ~70 chars (rank:4 + name:24 + gp:6 + w:6 + l:6 + otl:6 + pts:6 + pct:8 + padding) + # Two conferences side-by-side need ~150 chars total + SIDE_BY_SIDE_MIN_WIDTH = 150 DEFAULT_CSS = """ StandingsScreen { diff --git a/src/faceoff/widgets/game_card.py b/src/faceoff/widgets/game_card.py index 8a46050..b243b77 100644 --- a/src/faceoff/widgets/game_card.py +++ b/src/faceoff/widgets/game_card.py @@ -61,10 +61,18 @@ class GameCard(Widget): border: solid $success; } + GameCard.-live:focus { + border: double $accent; + } + GameCard.-final { border: solid $surface; } + GameCard.-final:focus { + border: double $accent; + } + GameCard .team-row { width: 100%; height: 1; diff --git a/uv.lock b/uv.lock index 61db96b..5726571 100644 --- a/uv.lock +++ b/uv.lock @@ -204,7 +204,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -213,7 +213,7 @@ wheels = [ [[package]] name = "faceoff" -version = "0.0.1" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "httpx" }, @@ -232,6 +232,11 @@ dev = [ { name = "tox-uv" }, { name = "ty" }, ] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, +] [package.metadata] requires-dist = [ @@ -251,6 +256,11 @@ dev = [ { name = "tox-uv", specifier = ">=1.29.0" }, { name = "ty", specifier = ">=0.0.8" }, ] +docs = [ + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-material", specifier = ">=9.7.1" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.0" }, +] [[package]] name = "filelock"