From a82f001918bcdc4471d52a1dc0f22f3090cb5db7 Mon Sep 17 00:00:00 2001 From: Bendt Date: Fri, 19 Dec 2025 10:40:33 -0500 Subject: [PATCH] Add mini-calendar sidebar to Calendar TUI - Add MonthCalendar widget as a collapsible sidebar (toggle with 's') - Sidebar syncs with main week grid (week highlight, selected date) - Click dates in sidebar to navigate week grid to that date - Click month navigation arrows to change displayed month - Add goto_date() method to WeekGrid for date navigation --- src/calendar/app.py | 62 ++++++++++++++++++++++++++- src/calendar/widgets/MonthCalendar.py | 38 ++++++++++++++++ src/calendar/widgets/WeekGrid.py | 17 ++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) diff --git a/src/calendar/app.py b/src/calendar/app.py index c896580..efa99b8 100644 --- a/src/calendar/app.py +++ b/src/calendar/app.py @@ -18,6 +18,7 @@ from textual.reactive import reactive from src.calendar.backend import CalendarBackend, Event from src.calendar.widgets.WeekGrid import WeekGrid +from src.calendar.widgets.MonthCalendar import MonthCalendar from src.calendar.widgets.AddEventForm import EventFormData from src.utils.shared_config import get_theme_name @@ -51,6 +52,26 @@ class CalendarApp(App): Screen { layout: vertical; } + + #main-content { + layout: horizontal; + height: 1fr; + } + + #sidebar { + width: 26; + border-right: solid $surface-darken-1; + background: $surface; + padding: 1 0; + } + + #sidebar.hidden { + display: none; + } + + #sidebar-calendar { + height: auto; + } #week-grid { height: 1fr; @@ -98,6 +119,7 @@ class CalendarApp(App): Binding("L", "next_week", "Next Week", show=True), Binding("g", "goto_today", "Today", show=True), Binding("w", "toggle_weekends", "Weekends", show=True), + Binding("s", "toggle_sidebar", "Sidebar", show=True), Binding("r", "refresh", "Refresh", show=True), Binding("enter", "view_event", "View", show=True), Binding("a", "add_event", "Add", show=True), @@ -106,6 +128,7 @@ class CalendarApp(App): # Reactive attributes include_weekends: reactive[bool] = reactive(True) + show_sidebar: reactive[bool] = reactive(True) # Instance attributes backend: Optional[CalendarBackend] @@ -124,7 +147,10 @@ class CalendarApp(App): def compose(self) -> ComposeResult: """Create the app layout.""" yield Header() - yield WeekGrid(id="week-grid") + with Horizontal(id="main-content"): + with Vertical(id="sidebar"): + yield MonthCalendar(id="sidebar-calendar") + yield WeekGrid(id="week-grid") yield Static(id="event-detail", classes="hidden") yield CalendarStatusBar(id="status-bar") yield Footer() @@ -136,10 +162,23 @@ class CalendarApp(App): # Load events for current week self.load_events() + # Sync sidebar calendar with current week + self._sync_sidebar_calendar() + # Update status bar and title self._update_status() self._update_title() + def _sync_sidebar_calendar(self) -> None: + """Sync the sidebar calendar with the main week grid.""" + try: + grid = self.query_one("#week-grid", WeekGrid) + calendar = self.query_one("#sidebar-calendar", MonthCalendar) + calendar.update_week(grid.week_start) + calendar.update_selected(grid.get_cursor_date()) + except Exception: + pass # Sidebar might not exist yet + def load_events(self) -> None: """Load events from backend for the current week.""" if not self.backend: @@ -255,11 +294,22 @@ class CalendarApp(App): def on_week_grid_week_changed(self, message: WeekGrid.WeekChanged) -> None: """Handle week change - reload events.""" self.load_events() + self._sync_sidebar_calendar() def on_week_grid_event_selected(self, message: WeekGrid.EventSelected) -> None: """Handle event selection.""" self._update_event_detail(message.event) + # Handle MonthCalendar messages + def on_month_calendar_date_selected( + self, message: MonthCalendar.DateSelected + ) -> None: + """Handle date selection from sidebar calendar.""" + grid = self.query_one("#week-grid", WeekGrid) + grid.goto_date(message.date) + self.load_events() + self._sync_sidebar_calendar() + # Navigation actions (forwarded to grid) def action_cursor_down(self) -> None: """Move cursor down.""" @@ -311,6 +361,15 @@ class CalendarApp(App): mode = "7 days" if self.include_weekends else "5 days (weekdays)" self.notify(f"Showing {mode}") + def action_toggle_sidebar(self) -> None: + """Toggle sidebar visibility.""" + self.show_sidebar = not self.show_sidebar + sidebar = self.query_one("#sidebar", Vertical) + if self.show_sidebar: + sidebar.remove_class("hidden") + else: + sidebar.add_class("hidden") + def action_refresh(self) -> None: """Refresh events from backend.""" self.load_events() @@ -383,6 +442,7 @@ Keybindings: H/L - Previous/Next week g - Go to today w - Toggle weekends (5/7 days) + s - Toggle sidebar Enter - View event details a - Add new event r - Refresh diff --git a/src/calendar/widgets/MonthCalendar.py b/src/calendar/widgets/MonthCalendar.py index 0edce4f..82f42de 100644 --- a/src/calendar/widgets/MonthCalendar.py +++ b/src/calendar/widgets/MonthCalendar.py @@ -240,3 +240,41 @@ class MonthCalendar(Widget): self.display_month = date(year, month, 1) self.post_message(self.MonthChanged(self.display_month)) self.refresh() + + def on_click(self, event) -> None: + """Handle mouse clicks on the calendar.""" + # Row 0 is the month header (< Month Year >) + # Row 1 is day names (Mo Tu We ...) + # Row 2+ are the week rows + y = event.y + x = event.x + + if y == 0: + # Month header - check for navigation arrows + if x < 2: + self.prev_month() + elif x >= self.size.width - 2: + self.next_month() + return + + if y == 1: + # Day names row - ignore + return + + # Week row - calculate which date was clicked + week_idx = y - 2 + weeks = self._weeks + if week_idx < 0 or week_idx >= len(weeks): + return + + week = weeks[week_idx] + # Each day takes 3 characters ("DD "), so find which day column + day_col = x // 3 + if day_col < 0 or day_col >= 7: + return + + clicked_date = week[day_col] + if clicked_date is not None: + self.selected_date = clicked_date + self.post_message(self.DateSelected(clicked_date)) + self.refresh() diff --git a/src/calendar/widgets/WeekGrid.py b/src/calendar/widgets/WeekGrid.py index 8d64512..51422fc 100644 --- a/src/calendar/widgets/WeekGrid.py +++ b/src/calendar/widgets/WeekGrid.py @@ -761,3 +761,20 @@ class WeekGrid(Vertical): event = self.get_event_at_cursor() if event: self.post_message(self.EventSelected(event)) + + def goto_date(self, target_date: date) -> None: + """Navigate to a specific date. + + Sets the week to contain the target date and places cursor on that day. + """ + # Get the week start for the target date + week_start_date = get_week_start_for_date(target_date) + + if self.week_start != week_start_date: + self.week_start = week_start_date + + # Set cursor column to the target date + col = get_day_column_for_date(target_date, self.week_start) + if not self.include_weekends and col >= 5: + col = 4 # Last weekday if weekend + self.cursor_col = col