mlb-cli-py 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. mlb_cli_py-0.1.0/LICENSE +7 -0
  2. mlb_cli_py-0.1.0/PKG-INFO +160 -0
  3. mlb_cli_py-0.1.0/README.md +131 -0
  4. mlb_cli_py-0.1.0/app/__init__.py +0 -0
  5. mlb_cli_py-0.1.0/app/config.py +26 -0
  6. mlb_cli_py-0.1.0/app/exceptions.py +6 -0
  7. mlb_cli_py-0.1.0/app/logger.py +29 -0
  8. mlb_cli_py-0.1.0/app/mlb_cli.py +332 -0
  9. mlb_cli_py-0.1.0/app/models/__init__.py +0 -0
  10. mlb_cli_py-0.1.0/app/models/base_data_source.py +28 -0
  11. mlb_cli_py-0.1.0/app/models/cache_service.py +58 -0
  12. mlb_cli_py-0.1.0/app/models/data_service.py +190 -0
  13. mlb_cli_py-0.1.0/app/models/statsapi_source.py +71 -0
  14. mlb_cli_py-0.1.0/app/screens/__init__.py +9 -0
  15. mlb_cli_py-0.1.0/app/screens/calendar_screen.py +58 -0
  16. mlb_cli_py-0.1.0/app/screens/error_screen.py +42 -0
  17. mlb_cli_py-0.1.0/app/screens/schedule_screen.py +33 -0
  18. mlb_cli_py-0.1.0/app/screens/standings_screen.py +39 -0
  19. mlb_cli_py-0.1.0/app/state.py +74 -0
  20. mlb_cli_py-0.1.0/app/widgets/__init__.py +22 -0
  21. mlb_cli_py-0.1.0/app/widgets/animations.py +66 -0
  22. mlb_cli_py-0.1.0/app/widgets/calendar_widget.py +78 -0
  23. mlb_cli_py-0.1.0/app/widgets/game_widget.py +139 -0
  24. mlb_cli_py-0.1.0/app/widgets/navigation_widget.py +73 -0
  25. mlb_cli_py-0.1.0/app/widgets/separator.py +11 -0
  26. mlb_cli_py-0.1.0/app/widgets/standing_widget.py +66 -0
  27. mlb_cli_py-0.1.0/mlb_cli_py.egg-info/PKG-INFO +160 -0
  28. mlb_cli_py-0.1.0/mlb_cli_py.egg-info/SOURCES.txt +37 -0
  29. mlb_cli_py-0.1.0/mlb_cli_py.egg-info/dependency_links.txt +1 -0
  30. mlb_cli_py-0.1.0/mlb_cli_py.egg-info/entry_points.txt +2 -0
  31. mlb_cli_py-0.1.0/mlb_cli_py.egg-info/requires.txt +8 -0
  32. mlb_cli_py-0.1.0/mlb_cli_py.egg-info/top_level.txt +1 -0
  33. mlb_cli_py-0.1.0/pyproject.toml +40 -0
  34. mlb_cli_py-0.1.0/setup.cfg +4 -0
  35. mlb_cli_py-0.1.0/tests/test_app_lifecycle.py +95 -0
  36. mlb_cli_py-0.1.0/tests/test_application_state.py +109 -0
  37. mlb_cli_py-0.1.0/tests/test_calendar_interaction.py +293 -0
  38. mlb_cli_py-0.1.0/tests/test_error_handling.py +72 -0
  39. mlb_cli_py-0.1.0/tests/test_navigation_logic.py +143 -0
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Conor Cleary
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: mlb-cli-py
3
+ Version: 0.1.0
4
+ Summary: A modern TUI for MLB scores, standings, and schedules
5
+ Author: Conor Cleary
6
+ License: Copyright 2026 Conor Cleary
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13
+
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Environment :: Console
18
+ Classifier: Topic :: Terminals
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: pytermgui==7.7.4
22
+ Requires-Dist: MLB-StatsAPI==1.9.0
23
+ Requires-Dist: redis==5.0.4
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest==8.2.0; extra == "dev"
26
+ Requires-Dist: pytest-cov==5.0.0; extra == "dev"
27
+ Requires-Dist: fakeredis==2.23.2; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # MLB Box Score TUI
31
+
32
+ A Python-based Terminal User Interface (TUI) for Major League Baseball (MLB) scores, schedules, and standings. Built with `pytermgui` and the `MLB-StatsAPI`, featuring a modular architecture and 100% test coverage.
33
+
34
+ ## Features
35
+
36
+ - **Full Season Schedule:** Navigate the entire 2026 MLB season through a dedicated calendar view or daily snapshots.
37
+ - **Dynamic Standings:** Real-time standings for all 6 MLB divisions (AL and NL), including comprehensive **Wild Card** rankings.
38
+ - **Interactive Calendar:** A multi-month calendar view for quick date selection, featuring intuitive **WASD** keyboard navigation and automatic focus management.
39
+ - **Smart Caching:** Built-in caching service to minimize API calls and ensure a responsive user experience.
40
+ - **Smooth Transitions:** Animated screen transitions for a modern, fluid feel.
41
+ - **Robust Architecture:** Surgical, class-based organization of widgets and screens for high maintainability.
42
+
43
+ ## Installation
44
+
45
+ ### From PyPI (Recommended)
46
+
47
+ Install the application directly using `pip`:
48
+
49
+ ```bash
50
+ pip install mlb-cli-py
51
+ ```
52
+
53
+ ### Local Setup (Development)
54
+
55
+ 1. **Clone the repository:**
56
+ ```bash
57
+ git clone https://github.com/conorpcleary/mlb-cli-py.git
58
+ cd mlb-cli-py
59
+ ```
60
+
61
+ 2. **Create and activate a virtual environment:**
62
+ ```bash
63
+ python3 -m venv venv
64
+ source venv/bin/activate
65
+ ```
66
+
67
+ 3. **Install in editable mode:**
68
+ ```bash
69
+ pip install -e ".[dev]"
70
+ ```
71
+
72
+ ## Usage
73
+
74
+ After installation, run the application from anywhere in your terminal:
75
+
76
+ ```bash
77
+ mlb-cli
78
+ ```
79
+
80
+ ## Controls
81
+
82
+ The application is designed for rapid keyboard-driven navigation.
83
+
84
+ ### Global Controls
85
+
86
+ | Key | Action |
87
+ | --- | --- |
88
+ | `[` | Previous Day / Previous Calendar Page (with wrapping) |
89
+ | `]` | Next Day / Next Calendar Page (with wrapping) |
90
+ | `t` | Jump to Today's Schedule |
91
+ | `c` | Switch to Calendar View |
92
+ | `x` | Toggle Standings View |
93
+ | `ESC` | Exit the application |
94
+ | `Tab` | Cycle focus between UI components |
95
+
96
+ ### Calendar Navigation
97
+
98
+ When in the Calendar view, you can use specialized keys for precise date selection:
99
+
100
+ | Key | Action |
101
+ | --- | --- |
102
+ | `W` | Move focus up one week |
103
+ | `A` | Move focus left one day |
104
+ | `S` | Move focus down one week |
105
+ | `D` | Move focus right one day |
106
+ | `Enter` | Select the focused date and view its schedule |
107
+
108
+ ## Project Structure
109
+
110
+ The project follows a strict modular design optimized for distribution:
111
+
112
+ ```text
113
+ mlb-cli-py/
114
+ ├── pyproject.toml # Modern build system configuration and metadata
115
+ ├── app/
116
+ │ ├── mlb_cli.py # Main application controller and TUI entry point
117
+ │ ├── models/ # Data services (API fetching, caching, date utilities)
118
+ │ ├── widgets/ # Modular TUI components (Game, Standing, Calendar widgets)
119
+ │ └── screens/ # Class-based screen definitions (Schedule, Standings, Calendar)
120
+ ├── tests/ # 100% covered test suite (Unit and integration tests)
121
+ └── README.md # You are here!
122
+ ```
123
+
124
+ ## Development & Testing
125
+
126
+ The project maintains a perfect **10.00/10 Pylint score** and **100% test coverage**.
127
+
128
+ ### Running Tests
129
+
130
+ To run the full test suite using `pytest`:
131
+
132
+ ```bash
133
+ PYTHONPATH=. pytest
134
+ ```
135
+
136
+ ### Linting
137
+
138
+ To verify code quality:
139
+
140
+ ```bash
141
+ pylint app tests
142
+ ```
143
+
144
+ ## Release Workflow
145
+
146
+ This project uses an automated release pipeline to publish updates to PyPI:
147
+
148
+ 1. **Version Bump:** Update the version in `pyproject.toml`.
149
+ 2. **GitHub Release:** Create and publish a new Release on GitHub with a version tag (e.g., `v0.1.0`).
150
+ 3. **Automated Publish:** A GitHub Action is triggered by the release, which:
151
+ - Builds the source distribution and wheel.
152
+ - Publishes the package to PyPI using **Trusted Publishing (OIDC)**.
153
+
154
+ ## Acknowledgements
155
+
156
+ This project would not be possible without the [MLB-StatsAPI](https://github.com/toddrob99/MLB-StatsAPI) and [PyTermGUI](https://github.com/bczsalba/pytermgui) projects.
157
+
158
+ ## License
159
+
160
+ MIT License - see [LICENSE](LICENSE) for details.
@@ -0,0 +1,131 @@
1
+ # MLB Box Score TUI
2
+
3
+ A Python-based Terminal User Interface (TUI) for Major League Baseball (MLB) scores, schedules, and standings. Built with `pytermgui` and the `MLB-StatsAPI`, featuring a modular architecture and 100% test coverage.
4
+
5
+ ## Features
6
+
7
+ - **Full Season Schedule:** Navigate the entire 2026 MLB season through a dedicated calendar view or daily snapshots.
8
+ - **Dynamic Standings:** Real-time standings for all 6 MLB divisions (AL and NL), including comprehensive **Wild Card** rankings.
9
+ - **Interactive Calendar:** A multi-month calendar view for quick date selection, featuring intuitive **WASD** keyboard navigation and automatic focus management.
10
+ - **Smart Caching:** Built-in caching service to minimize API calls and ensure a responsive user experience.
11
+ - **Smooth Transitions:** Animated screen transitions for a modern, fluid feel.
12
+ - **Robust Architecture:** Surgical, class-based organization of widgets and screens for high maintainability.
13
+
14
+ ## Installation
15
+
16
+ ### From PyPI (Recommended)
17
+
18
+ Install the application directly using `pip`:
19
+
20
+ ```bash
21
+ pip install mlb-cli-py
22
+ ```
23
+
24
+ ### Local Setup (Development)
25
+
26
+ 1. **Clone the repository:**
27
+ ```bash
28
+ git clone https://github.com/conorpcleary/mlb-cli-py.git
29
+ cd mlb-cli-py
30
+ ```
31
+
32
+ 2. **Create and activate a virtual environment:**
33
+ ```bash
34
+ python3 -m venv venv
35
+ source venv/bin/activate
36
+ ```
37
+
38
+ 3. **Install in editable mode:**
39
+ ```bash
40
+ pip install -e ".[dev]"
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ After installation, run the application from anywhere in your terminal:
46
+
47
+ ```bash
48
+ mlb-cli
49
+ ```
50
+
51
+ ## Controls
52
+
53
+ The application is designed for rapid keyboard-driven navigation.
54
+
55
+ ### Global Controls
56
+
57
+ | Key | Action |
58
+ | --- | --- |
59
+ | `[` | Previous Day / Previous Calendar Page (with wrapping) |
60
+ | `]` | Next Day / Next Calendar Page (with wrapping) |
61
+ | `t` | Jump to Today's Schedule |
62
+ | `c` | Switch to Calendar View |
63
+ | `x` | Toggle Standings View |
64
+ | `ESC` | Exit the application |
65
+ | `Tab` | Cycle focus between UI components |
66
+
67
+ ### Calendar Navigation
68
+
69
+ When in the Calendar view, you can use specialized keys for precise date selection:
70
+
71
+ | Key | Action |
72
+ | --- | --- |
73
+ | `W` | Move focus up one week |
74
+ | `A` | Move focus left one day |
75
+ | `S` | Move focus down one week |
76
+ | `D` | Move focus right one day |
77
+ | `Enter` | Select the focused date and view its schedule |
78
+
79
+ ## Project Structure
80
+
81
+ The project follows a strict modular design optimized for distribution:
82
+
83
+ ```text
84
+ mlb-cli-py/
85
+ ├── pyproject.toml # Modern build system configuration and metadata
86
+ ├── app/
87
+ │ ├── mlb_cli.py # Main application controller and TUI entry point
88
+ │ ├── models/ # Data services (API fetching, caching, date utilities)
89
+ │ ├── widgets/ # Modular TUI components (Game, Standing, Calendar widgets)
90
+ │ └── screens/ # Class-based screen definitions (Schedule, Standings, Calendar)
91
+ ├── tests/ # 100% covered test suite (Unit and integration tests)
92
+ └── README.md # You are here!
93
+ ```
94
+
95
+ ## Development & Testing
96
+
97
+ The project maintains a perfect **10.00/10 Pylint score** and **100% test coverage**.
98
+
99
+ ### Running Tests
100
+
101
+ To run the full test suite using `pytest`:
102
+
103
+ ```bash
104
+ PYTHONPATH=. pytest
105
+ ```
106
+
107
+ ### Linting
108
+
109
+ To verify code quality:
110
+
111
+ ```bash
112
+ pylint app tests
113
+ ```
114
+
115
+ ## Release Workflow
116
+
117
+ This project uses an automated release pipeline to publish updates to PyPI:
118
+
119
+ 1. **Version Bump:** Update the version in `pyproject.toml`.
120
+ 2. **GitHub Release:** Create and publish a new Release on GitHub with a version tag (e.g., `v0.1.0`).
121
+ 3. **Automated Publish:** A GitHub Action is triggered by the release, which:
122
+ - Builds the source distribution and wheel.
123
+ - Publishes the package to PyPI using **Trusted Publishing (OIDC)**.
124
+
125
+ ## Acknowledgements
126
+
127
+ This project would not be possible without the [MLB-StatsAPI](https://github.com/toddrob99/MLB-StatsAPI) and [PyTermGUI](https://github.com/bczsalba/pytermgui) projects.
128
+
129
+ ## License
130
+
131
+ MIT License - see [LICENSE](LICENSE) for details.
File without changes
@@ -0,0 +1,26 @@
1
+ """
2
+ Configuration module for the MLB CLI application.
3
+ Centralizes all constants and environment-specific settings.
4
+ """
5
+
6
+ # TUI Settings
7
+ STATIC_WIDTH = 80
8
+ INITIAL_HEIGHT = 36
9
+
10
+ # Redis Settings
11
+ REDIS_HOST = 'localhost'
12
+ REDIS_PORT = 6379
13
+ REDIS_DB = 0
14
+
15
+ # Data Service Settings
16
+ LIVE_DATA_TTL = 300 # 5 minutes
17
+
18
+ # Division name mapping
19
+ DIVISION_NAMES = {
20
+ 200: "AL West",
21
+ 201: "AL East",
22
+ 202: "AL Central",
23
+ 203: "NL West",
24
+ 204: "NL East",
25
+ 205: "NL Central"
26
+ }
@@ -0,0 +1,6 @@
1
+ """
2
+ Custom exceptions for the MLB CLI application.
3
+ """
4
+
5
+ class APIError(Exception):
6
+ """Raised when an error occurs while fetching data from the MLB API."""
@@ -0,0 +1,29 @@
1
+ """
2
+ Logging configuration for the MLB CLI application.
3
+ Provides a standard logger that writes to a local log file.
4
+ """
5
+ import os
6
+ import logging
7
+ from logging.handlers import RotatingFileHandler
8
+
9
+ # Ensure log directory exists
10
+ LOG_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
11
+ LOG_FILE = os.path.join(LOG_DIR, "mlb_cli.log")
12
+
13
+ def get_logger(name):
14
+ """
15
+ Configures and returns a logger instance.
16
+ """
17
+ logger = logging.getLogger(name)
18
+ if not logger.handlers:
19
+ logger.setLevel(logging.DEBUG)
20
+
21
+ # Rotating file handler (5MB per file, keeps 2 backups)
22
+ handler = RotatingFileHandler(LOG_FILE, maxBytes=5*1024*1024, backupCount=2)
23
+ formatter = logging.Formatter(
24
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
25
+ )
26
+ handler.setFormatter(formatter)
27
+ logger.addHandler(handler)
28
+
29
+ return logger
@@ -0,0 +1,332 @@
1
+ """
2
+ Main entry point for the MLB CLI application.
3
+ This module initializes the Terminal UI and manages the primary window and global keybindings.
4
+ """
5
+ from datetime import datetime, timedelta
6
+ import pytermgui as ptg
7
+ from .models.data_service import (
8
+ fetch_teams,
9
+ format_date
10
+ )
11
+ from .screens import (
12
+ ScheduleScreen,
13
+ StandingsScreen,
14
+ CalendarScreen,
15
+ ErrorScreen
16
+ )
17
+ from .widgets import CalendarWidget, slide_transition
18
+ from .config import STATIC_WIDTH, INITIAL_HEIGHT
19
+ from .state import ApplicationState
20
+ from .exceptions import APIError
21
+ from .logger import get_logger
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ class MLBApp:
27
+ """
28
+ TUI Manager class for the MLB CLI application.
29
+ Handles window management, screen transitions, and global keybindings.
30
+ """
31
+ # pylint: disable=too-many-instance-attributes
32
+
33
+ def __init__(self):
34
+ """Initializes the application UI and logic state."""
35
+ fetch_teams()
36
+ self.manager = ptg.WindowManager()
37
+ self.state = ApplicationState()
38
+ self.static_width = STATIC_WIDTH
39
+ self.static_height = 0 # Will be set in run()
40
+ self.main_window = None
41
+ self.is_initialized = False
42
+
43
+ def set_window_data(self, widgets, title, page_name, on_finish=None):
44
+ """Sets content for the main window, using animation if already initialized."""
45
+ if self.state.active_page == page_name:
46
+ if on_finish:
47
+ on_finish()
48
+ return
49
+
50
+ # Calculate required height based on content
51
+ max_height = self.manager.terminal.height - 2
52
+ temp_window = ptg.Window(*widgets, width=self.static_width)
53
+ temp_window.set_title(title)
54
+ target_height = min(max_height, temp_window.height)
55
+
56
+ self.state.set_active_page(page_name)
57
+ if not self.is_initialized:
58
+ self.is_initialized = True
59
+ self.main_window.set_widgets(widgets)
60
+ self.main_window.set_title(title)
61
+ self.main_window.width = self.static_width
62
+ self.main_window.height = target_height
63
+ self.main_window.styles.border = "green"
64
+ self.main_window.styles.corner = "green"
65
+ self.main_window.center()
66
+ if on_finish:
67
+ on_finish()
68
+ return
69
+
70
+ slide_transition(
71
+ self.main_window,
72
+ self.manager,
73
+ widgets,
74
+ title,
75
+ on_finish=on_finish,
76
+ new_height=target_height
77
+ )
78
+
79
+ @staticmethod
80
+ def handle_errors(func):
81
+ """Decorator to handle errors during screen transitions."""
82
+ def wrapper(self, *args, **kwargs):
83
+ try:
84
+ return func(self, *args, **kwargs)
85
+ except (APIError, Exception) as e: # pylint: disable=broad-exception-caught
86
+ logger.error("Error in %s: %s", func.__name__, e, exc_info=True)
87
+ # Provide a user-friendly message for generic exceptions
88
+ msg = str(e) if isinstance(e, APIError) else "An unexpected error occurred"
89
+ widgets, title = ErrorScreen.get_widgets(msg)
90
+ self.set_window_data(widgets, title, "error")
91
+ return False
92
+ return wrapper
93
+
94
+ @handle_errors
95
+ def update_to_schedule(self, *_args, **_kwargs):
96
+ """Transitions the main window to show the schedule for current_date."""
97
+ date_str = format_date(self.state.current_date)
98
+ widgets, title = ScheduleScreen.get_widgets(date_str)
99
+ self.set_window_data(widgets, title, f"schedule:{date_str}")
100
+ return True
101
+
102
+ def go_to_previous_day(self, *_args, **_kwargs):
103
+ """Decrements the current date or page."""
104
+ if self.state.on_calendar_screen:
105
+ return self.go_to_previous_page()
106
+
107
+ if self.state.decrement_date():
108
+ return self.update_to_schedule()
109
+ return True
110
+
111
+ def go_to_next_day(self, *_args, **_kwargs):
112
+ """Increments the current date or page."""
113
+ if self.state.on_calendar_screen:
114
+ return self.go_to_next_page()
115
+
116
+ if self.state.increment_date():
117
+ return self.update_to_schedule()
118
+ return True
119
+
120
+ @handle_errors
121
+ def toggle_standings(self, *_args, **_kwargs):
122
+ """Toggles between standings and schedule/calendar."""
123
+ if self.state.on_standings_screen:
124
+ return self.update_to_schedule()
125
+
126
+ widgets, title = StandingsScreen.get_widgets()
127
+ self.set_window_data(widgets, title, "standings")
128
+ return True
129
+
130
+ @handle_errors
131
+ def update_to_calendar(self, *_args, sync_page=True, focus_target=None, **_kwargs):
132
+ """Transitions to the calendar view."""
133
+ if self.state.on_standings_screen:
134
+ self.state.reset_to_today()
135
+
136
+ if sync_page:
137
+ # Ensure calendar_page matches current_date
138
+ self.state.determine_initial_calendar_page()
139
+
140
+ pages = [
141
+ [3, 4, 5],
142
+ [6, 7, 8],
143
+ [9, 10]
144
+ ]
145
+ months = pages[self.state.calendar_page]
146
+ widgets, title = CalendarScreen.get_widgets(
147
+ 2026,
148
+ months,
149
+ self.on_calendar_date_selected,
150
+ selected_date=self.state.current_date
151
+ )
152
+ self.set_window_data(
153
+ widgets,
154
+ title,
155
+ f"calendar:{self.state.calendar_page}",
156
+ on_finish=lambda: self._focus_current_date_in_calendar(target=focus_target)
157
+ )
158
+ return True
159
+
160
+ def _focus_current_date_in_calendar(self, target=None):
161
+ # pylint: disable=too-many-branches
162
+ """
163
+ Helper to find and focus a date in the calendar view.
164
+ If target is None, focuses current_date.
165
+ If target is 'first', focuses first date of first month.
166
+ If target is 'last', focuses last date of last month.
167
+ """
168
+ calendar_widgets = []
169
+ for widget in self.main_window:
170
+ if not isinstance(widget, ptg.Container):
171
+ continue
172
+ for sub in widget:
173
+ if isinstance(sub, CalendarWidget):
174
+ calendar_widgets.append(sub)
175
+
176
+ if not calendar_widgets:
177
+ return False
178
+
179
+ target_btn = None
180
+ if target == "first":
181
+ sub = calendar_widgets[0]
182
+ first_day = min(sub.day_to_button.keys())
183
+ target_btn = sub.day_to_button[first_day]
184
+ elif target == "last":
185
+ sub = calendar_widgets[-1]
186
+ last_day = max(sub.day_to_button.keys())
187
+ target_btn = sub.day_to_button[last_day]
188
+ else:
189
+ # Default: focus current_date
190
+ for sub in calendar_widgets:
191
+ if sub.month == self.state.current_date.month:
192
+ day = self.state.current_date.day
193
+ if day in sub.day_to_button:
194
+ target_btn = sub.day_to_button[day]
195
+ break
196
+
197
+ if target_btn:
198
+ for i, (selectable, _) in enumerate(self.main_window.selectables):
199
+ if selectable is target_btn:
200
+ self.main_window.select(i)
201
+ self.manager.focused = target_btn
202
+ return True
203
+ return False
204
+
205
+ def on_calendar_date_selected(self, year, month, day):
206
+ """Callback for when a date is selected in the calendar."""
207
+ self.state.current_date = datetime(year, month, day)
208
+ return self.update_to_schedule()
209
+
210
+ def _navigate_calendar(self, direction):
211
+ # pylint: disable=too-many-branches
212
+ """
213
+ Global WASD navigation for the calendar.
214
+ Moves focus between buttons based on date logic.
215
+ """
216
+ if not self.state.on_calendar_screen:
217
+ return False
218
+
219
+ focused = self.manager.focused
220
+ if focused is None:
221
+ return False
222
+
223
+ # Find which CalendarWidget the focused button belongs to
224
+ target_widget = None
225
+ for widget in self.main_window:
226
+ if not isinstance(widget, ptg.Container):
227
+ continue
228
+ for sub in widget:
229
+ if isinstance(sub, CalendarWidget) and focused in sub.button_to_day:
230
+ target_widget = sub
231
+ break
232
+ if target_widget:
233
+ break
234
+
235
+ if not target_widget:
236
+ return False
237
+
238
+ day = target_widget.button_to_day[focused]
239
+ current_date = datetime(target_widget.year, target_widget.month, day)
240
+
241
+ delta = {
242
+ "w": timedelta(days=-7),
243
+ "a": timedelta(days=-1),
244
+ "s": timedelta(days=7),
245
+ "d": timedelta(days=1),
246
+ }.get(direction)
247
+
248
+ if not delta:
249
+ return False
250
+
251
+ target_date = current_date + delta
252
+
253
+ # Look for the target date in any CalendarWidget in the main window
254
+ for widget in self.main_window:
255
+ if not isinstance(widget, ptg.Container):
256
+ continue
257
+ for sub in widget:
258
+ if not (isinstance(sub, CalendarWidget) and
259
+ sub.year == target_date.year and
260
+ sub.month == target_date.month):
261
+ continue
262
+
263
+ if target_date.day in sub.day_to_button:
264
+ target_btn = sub.day_to_button[target_date.day]
265
+ # Find and select the button index in the window
266
+ for i, (selectable, _) in enumerate(self.main_window.selectables):
267
+ if selectable is target_btn:
268
+ self.main_window.select(i)
269
+ self.manager.focused = target_btn
270
+ return True
271
+ return False
272
+
273
+ def go_to_previous_page(self, *_args, **_kwargs):
274
+ """Moves calendar view to the previous page with wrapping."""
275
+ self.state.prev_calendar_page()
276
+ return self.update_to_calendar(sync_page=False, focus_target="last")
277
+
278
+ def go_to_next_page(self, *_args, **_kwargs):
279
+ """Moves calendar view to the next page with wrapping."""
280
+ self.state.next_calendar_page()
281
+ return self.update_to_calendar(sync_page=False, focus_target="first")
282
+
283
+ def go_to_today(self, *_args, **_kwargs):
284
+ """Resets the current date to today and updates the view."""
285
+ self.state.reset_to_today()
286
+ return self.update_to_schedule()
287
+
288
+ def exit_app(self, *_args, **_kwargs):
289
+ """Stops the WindowManager and exits the application."""
290
+ self.manager.stop()
291
+ return True
292
+
293
+ def run(self):
294
+ """Starts the application main loop."""
295
+ with self.manager:
296
+ self.static_height = min(INITIAL_HEIGHT, self.manager.terminal.height - 2)
297
+
298
+ self.main_window = ptg.Window(
299
+ width=self.static_width,
300
+ height=self.static_height,
301
+ is_static=True,
302
+ is_noresize=True)
303
+ self.manager.add(self.main_window)
304
+
305
+ # Initial content: Calendar
306
+ self.update_to_calendar()
307
+
308
+ # Global bindings
309
+ self.manager.bind("[", self.go_to_previous_day)
310
+ self.manager.bind("]", self.go_to_next_day)
311
+ self.manager.bind("t", self.go_to_today)
312
+ self.manager.bind("c", self.update_to_calendar)
313
+ self.manager.bind("x", self.toggle_standings)
314
+ self.manager.bind(ptg.keys.ESC, self.exit_app)
315
+
316
+ # WASD for Calendar Navigation
317
+ self.manager.bind("w", lambda *_: self._navigate_calendar("w"))
318
+ self.manager.bind("a", lambda *_: self._navigate_calendar("a"))
319
+ self.manager.bind("s", lambda *_: self._navigate_calendar("s"))
320
+ self.manager.bind("d", lambda *_: self._navigate_calendar("d"))
321
+
322
+ self.manager.run()
323
+
324
+
325
+ def main():
326
+ """Entry point for the application."""
327
+ app = MLBApp()
328
+ app.run()
329
+
330
+
331
+ if __name__ == "__main__": # pragma: no cover
332
+ main()