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
82 changes: 0 additions & 82 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
82 changes: 0 additions & 82 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 26 additions & 2 deletions src/faceoff/screens/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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."""
Expand Down Expand Up @@ -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()

Expand All @@ -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")
Expand Down
39 changes: 35 additions & 4 deletions src/faceoff/screens/schedule.py
Original file line number Diff line number Diff line change
@@ -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

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

Expand Down Expand Up @@ -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:
Expand All @@ -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."""
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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")
Expand Down
6 changes: 4 additions & 2 deletions src/faceoff/screens/standings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions src/faceoff/widgets/game_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading