From 4cbe592e0a0fae13f0a89918a66fd983dd84464f Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 15 Apr 2026 22:28:45 -0500 Subject: feat: add Textual TUI with auto-refresh, leaders, playoff picture, and box score MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces an interactive terminal UI via the `--tui` flag backed by the Textual framework. Scores and standings auto-refresh on a configurable interval (default 60s, minimum 10s) using asyncio with nba_api calls offloaded to a thread pool to keep the event loop unblocked. New tabs: - Scores / Standings (existing views, now live-updating) - Leaders: top-25 by stat category, cycle with < / > keys - Playoff Picture: conference seeding with clinch/elimination status - Box Score: per-player live stats, load any game with keys 1–9 Refactored scores.py and standings.py to separate rendering from I/O (get_scoreboard_table, get_east/west_standings_table), keeping the existing static CLI path fully intact. Added -sc / -st shortcut flags. Adds textual>=0.89.1,<7.0 dependency, build-system declaration for uv, and package-data entry for the bundled .tcss stylesheet. --- nba/tui/widgets.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 nba/tui/widgets.py (limited to 'nba/tui/widgets.py') diff --git a/nba/tui/widgets.py b/nba/tui/widgets.py new file mode 100644 index 0000000..f5e8c1d --- /dev/null +++ b/nba/tui/widgets.py @@ -0,0 +1,50 @@ +""" +Custom Textual widgets for the NBA scores TUI. +""" + +from textual.reactive import reactive +from textual.widgets import Static + + +class ScoresWidget(Static): + """Displays the NBA scoreboard as a scrollable ANSI-formatted table.""" + + +class StandingsWidget(Static): + """Displays the NBA standings as a scrollable ANSI-formatted table.""" + + +class CountdownBar(Static): + """ + Docked status bar that counts down to the next auto-refresh. + + Maintains its own 1-second ticker and re-renders via a reactive attribute. + """ + + seconds: reactive[int] = reactive(60) + + def __init__(self, interval: int, **kwargs) -> None: + super().__init__(**kwargs) + self._interval = interval + self.seconds = interval + + def on_mount(self) -> None: + self.set_interval(1, self._tick) + + def _tick(self) -> None: + if self.seconds > 0: + self.seconds -= 1 + + def watch_seconds(self, value: int) -> None: + if value <= 0: + self.update("Refreshing...") + else: + self.update( + f"Next refresh in {value}s | " + "\\[1-9] Box Score | \\[] Leaders Cat | \\[r] Refresh | \\[q] Quit" + ) + + def reset(self, interval: int) -> None: + """Reset the countdown to the given interval.""" + self._interval = interval + self.seconds = interval -- cgit v1.2.3