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:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user