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
This commit is contained in:
Bendt
2025-12-19 10:40:33 -05:00
parent 48d2455b9c
commit a82f001918
3 changed files with 116 additions and 1 deletions

View File

@@ -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

View File

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

View File

@@ -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