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.backend import CalendarBackend, Event
from src.calendar.widgets.WeekGrid import WeekGrid from src.calendar.widgets.WeekGrid import WeekGrid
from src.calendar.widgets.MonthCalendar import MonthCalendar
from src.calendar.widgets.AddEventForm import EventFormData from src.calendar.widgets.AddEventForm import EventFormData
from src.utils.shared_config import get_theme_name from src.utils.shared_config import get_theme_name
@@ -52,6 +53,26 @@ class CalendarApp(App):
layout: vertical; 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 { #week-grid {
height: 1fr; height: 1fr;
} }
@@ -98,6 +119,7 @@ class CalendarApp(App):
Binding("L", "next_week", "Next Week", show=True), Binding("L", "next_week", "Next Week", show=True),
Binding("g", "goto_today", "Today", show=True), Binding("g", "goto_today", "Today", show=True),
Binding("w", "toggle_weekends", "Weekends", show=True), Binding("w", "toggle_weekends", "Weekends", show=True),
Binding("s", "toggle_sidebar", "Sidebar", show=True),
Binding("r", "refresh", "Refresh", show=True), Binding("r", "refresh", "Refresh", show=True),
Binding("enter", "view_event", "View", show=True), Binding("enter", "view_event", "View", show=True),
Binding("a", "add_event", "Add", show=True), Binding("a", "add_event", "Add", show=True),
@@ -106,6 +128,7 @@ class CalendarApp(App):
# Reactive attributes # Reactive attributes
include_weekends: reactive[bool] = reactive(True) include_weekends: reactive[bool] = reactive(True)
show_sidebar: reactive[bool] = reactive(True)
# Instance attributes # Instance attributes
backend: Optional[CalendarBackend] backend: Optional[CalendarBackend]
@@ -124,7 +147,10 @@ class CalendarApp(App):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
"""Create the app layout.""" """Create the app layout."""
yield Header() 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 Static(id="event-detail", classes="hidden")
yield CalendarStatusBar(id="status-bar") yield CalendarStatusBar(id="status-bar")
yield Footer() yield Footer()
@@ -136,10 +162,23 @@ class CalendarApp(App):
# Load events for current week # Load events for current week
self.load_events() self.load_events()
# Sync sidebar calendar with current week
self._sync_sidebar_calendar()
# Update status bar and title # Update status bar and title
self._update_status() self._update_status()
self._update_title() 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: def load_events(self) -> None:
"""Load events from backend for the current week.""" """Load events from backend for the current week."""
if not self.backend: if not self.backend:
@@ -255,11 +294,22 @@ class CalendarApp(App):
def on_week_grid_week_changed(self, message: WeekGrid.WeekChanged) -> None: def on_week_grid_week_changed(self, message: WeekGrid.WeekChanged) -> None:
"""Handle week change - reload events.""" """Handle week change - reload events."""
self.load_events() self.load_events()
self._sync_sidebar_calendar()
def on_week_grid_event_selected(self, message: WeekGrid.EventSelected) -> None: def on_week_grid_event_selected(self, message: WeekGrid.EventSelected) -> None:
"""Handle event selection.""" """Handle event selection."""
self._update_event_detail(message.event) 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) # Navigation actions (forwarded to grid)
def action_cursor_down(self) -> None: def action_cursor_down(self) -> None:
"""Move cursor down.""" """Move cursor down."""
@@ -311,6 +361,15 @@ class CalendarApp(App):
mode = "7 days" if self.include_weekends else "5 days (weekdays)" mode = "7 days" if self.include_weekends else "5 days (weekdays)"
self.notify(f"Showing {mode}") 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: def action_refresh(self) -> None:
"""Refresh events from backend.""" """Refresh events from backend."""
self.load_events() self.load_events()
@@ -383,6 +442,7 @@ Keybindings:
H/L - Previous/Next week H/L - Previous/Next week
g - Go to today g - Go to today
w - Toggle weekends (5/7 days) w - Toggle weekends (5/7 days)
s - Toggle sidebar
Enter - View event details Enter - View event details
a - Add new event a - Add new event
r - Refresh r - Refresh

View File

@@ -240,3 +240,41 @@ class MonthCalendar(Widget):
self.display_month = date(year, month, 1) self.display_month = date(year, month, 1)
self.post_message(self.MonthChanged(self.display_month)) self.post_message(self.MonthChanged(self.display_month))
self.refresh() 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() event = self.get_event_at_cursor()
if event: if event:
self.post_message(self.EventSelected(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