mirror of
https://github.com/Misterio77/Foundry.git
synced 2026-08-24 02:14:13 -05:00
feat(projects/khora): add week agenda view
Assisted-by: pi (gpt-5.6-sol)
This commit is contained in:
@@ -12,12 +12,12 @@ Khora is an early, read-only prototype. It currently provides:
|
||||
- a native GTK 4/libadwaita interface;
|
||||
- calendar discovery from the existing khal configuration;
|
||||
- per-calendar visibility controls;
|
||||
- a day agenda with recurring and all-day events; and
|
||||
- day and week agendas with recurring and all-day events; and
|
||||
- explicit refreshes of khal's local index.
|
||||
|
||||
Editing, week and month views, event details, and filesystem monitoring come
|
||||
next. The khal dependency is isolated in `khora.khal_adapter` so its internal
|
||||
API can change without leaking through the application.
|
||||
Editing, a month view, event details, and filesystem monitoring come next. The
|
||||
khal dependency is isolated in `khora.khal_adapter` so its internal API can
|
||||
change without leaking through the application.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -29,13 +29,27 @@ class KhalRepository:
|
||||
)
|
||||
|
||||
def events_on(self, day: dt.date, visible: set[str] | None = None) -> tuple[Event, ...]:
|
||||
return self.events_for_days((day,), visible)[day]
|
||||
|
||||
def events_for_days(
|
||||
self,
|
||||
days: tuple[dt.date, ...],
|
||||
visible: set[str] | None = None,
|
||||
) -> dict[dt.date, tuple[Event, ...]]:
|
||||
self._collection.update_db()
|
||||
events = (
|
||||
self._to_event(event)
|
||||
for event in self._collection.get_events_on(day)
|
||||
if visible is None or event.calendar in visible
|
||||
)
|
||||
return tuple(sorted(events, key=self._sort_key))
|
||||
return {
|
||||
day: tuple(
|
||||
sorted(
|
||||
(
|
||||
self._to_event(event)
|
||||
for event in self._collection.get_events_on(day)
|
||||
if visible is None or event.calendar in visible
|
||||
),
|
||||
key=self._sort_key,
|
||||
)
|
||||
)
|
||||
for day in days
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _to_event(event) -> Event:
|
||||
|
||||
@@ -4,6 +4,11 @@ import datetime as dt
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
def week_dates(day: dt.date) -> tuple[dt.date, ...]:
|
||||
monday = day - dt.timedelta(days=day.weekday())
|
||||
return tuple(monday + dt.timedelta(days=offset) for offset in range(7))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Calendar:
|
||||
name: str
|
||||
|
||||
@@ -10,7 +10,7 @@ from gi.repository import Adw, Gio, GLib, Gtk
|
||||
|
||||
from .colors import display_color
|
||||
from .khal_adapter import KhalRepository
|
||||
from .model import Event
|
||||
from .model import Event, week_dates
|
||||
|
||||
|
||||
class KhoraWindow(Adw.ApplicationWindow):
|
||||
@@ -24,6 +24,7 @@ class KhoraWindow(Adw.ApplicationWindow):
|
||||
self.set_default_size(960, 680)
|
||||
self._repository = repository
|
||||
self._visible_calendars: set[str] = set()
|
||||
self._view_mode = "day"
|
||||
|
||||
self._toolbar = Adw.ToolbarView()
|
||||
self.set_content(self._toolbar)
|
||||
@@ -35,8 +36,7 @@ class KhoraWindow(Adw.ApplicationWindow):
|
||||
|
||||
assert repository is not None
|
||||
self._visible_calendars = {calendar.name for calendar in repository.calendars}
|
||||
self._event_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
|
||||
self._event_list.add_css_class("boxed-list")
|
||||
self._agenda_content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=24)
|
||||
self._empty = Adw.StatusPage(
|
||||
icon_name="x-office-calendar-symbolic",
|
||||
title="No events",
|
||||
@@ -57,6 +57,15 @@ class KhoraWindow(Adw.ApplicationWindow):
|
||||
|
||||
today = Gtk.Button(label="Today", action_name="win.today")
|
||||
header.pack_start(today)
|
||||
|
||||
view_switcher = Gtk.Box(css_classes=["linked"])
|
||||
day = Gtk.ToggleButton(label="Day", active=True)
|
||||
week = Gtk.ToggleButton(label="Week", group=day)
|
||||
day.connect("toggled", self._on_view_toggled, "day")
|
||||
week.connect("toggled", self._on_view_toggled, "week")
|
||||
view_switcher.append(day)
|
||||
view_switcher.append(week)
|
||||
header.pack_start(view_switcher)
|
||||
header.pack_end(Gtk.Button(icon_name="view-refresh-symbolic", action_name="win.refresh"))
|
||||
|
||||
self._install_action("today", self._on_today)
|
||||
@@ -94,7 +103,7 @@ class KhoraWindow(Adw.ApplicationWindow):
|
||||
def _build_agenda(self) -> Gtk.Widget:
|
||||
overlay = Gtk.Overlay()
|
||||
scroller = Gtk.ScrolledWindow(
|
||||
child=self._event_list,
|
||||
child=self._agenda_content,
|
||||
hscrollbar_policy=Gtk.PolicyType.NEVER,
|
||||
margin_top=24,
|
||||
margin_bottom=24,
|
||||
@@ -117,21 +126,46 @@ class KhoraWindow(Adw.ApplicationWindow):
|
||||
return dt.date(selected.get_year(), selected.get_month(), selected.get_day_of_month())
|
||||
|
||||
def _refresh(self) -> None:
|
||||
if self._repository is None or not hasattr(self, "_event_list"):
|
||||
if self._repository is None or not hasattr(self, "_agenda_content"):
|
||||
return
|
||||
|
||||
days = (
|
||||
week_dates(self._selected_day())
|
||||
if self._view_mode == "week"
|
||||
else (self._selected_day(),)
|
||||
)
|
||||
try:
|
||||
events = self._repository.events_on(self._selected_day(), self._visible_calendars)
|
||||
events_by_day = self._repository.events_for_days(days, self._visible_calendars)
|
||||
except Exception as error: # khal exposes several backend-specific errors
|
||||
self._show_toast(str(error))
|
||||
return
|
||||
|
||||
while row := self._event_list.get_row_at_index(0):
|
||||
self._event_list.remove(row)
|
||||
for event in events:
|
||||
self._event_list.append(self._event_row(event))
|
||||
self._empty.set_visible(not events)
|
||||
self._event_list.set_visible(bool(events))
|
||||
while child := self._agenda_content.get_first_child():
|
||||
self._agenda_content.remove(child)
|
||||
|
||||
has_events = any(events_by_day.values())
|
||||
if self._view_mode == "week":
|
||||
for day in days:
|
||||
self._agenda_content.append(self._day_group(day, events_by_day[day]))
|
||||
elif has_events:
|
||||
event_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
|
||||
event_list.add_css_class("boxed-list")
|
||||
for event in events_by_day[days[0]]:
|
||||
event_list.append(self._event_row(event))
|
||||
self._agenda_content.append(event_list)
|
||||
|
||||
self._empty.set_visible(not has_events and self._view_mode == "day")
|
||||
self._agenda_content.set_visible(has_events or self._view_mode == "week")
|
||||
|
||||
@staticmethod
|
||||
def _day_group(day: dt.date, events: tuple[Event, ...]) -> Adw.PreferencesGroup:
|
||||
group = Adw.PreferencesGroup(title=f"{day:%A, %B} {day.day}")
|
||||
if events:
|
||||
for event in events:
|
||||
group.add(KhoraWindow._event_row(event))
|
||||
else:
|
||||
group.set_description("No events")
|
||||
return group
|
||||
|
||||
@staticmethod
|
||||
def _event_row(event: Event) -> Adw.ActionRow:
|
||||
@@ -159,6 +193,11 @@ class KhoraWindow(Adw.ApplicationWindow):
|
||||
self._calendar.select_day(GLib.DateTime.new_now_local())
|
||||
self._refresh()
|
||||
|
||||
def _on_view_toggled(self, button: Gtk.ToggleButton, mode: str) -> None:
|
||||
if button.get_active():
|
||||
self._view_mode = mode
|
||||
self._refresh()
|
||||
|
||||
def _install_action(self, name: str, callback) -> None:
|
||||
action = Gio.SimpleAction.new(name, None)
|
||||
action.connect("activate", callback)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import datetime as dt
|
||||
|
||||
from khora.model import Event
|
||||
from khora.model import Event, week_dates
|
||||
|
||||
|
||||
def test_timed_event_label_uses_24_hour_time() -> None:
|
||||
@@ -14,6 +14,18 @@ def test_timed_event_label_uses_24_hour_time() -> None:
|
||||
assert event.time_label == "13:30–15:00"
|
||||
|
||||
|
||||
def test_week_dates_runs_from_monday_through_sunday() -> None:
|
||||
assert week_dates(dt.date(2026, 8, 15)) == (
|
||||
dt.date(2026, 8, 10),
|
||||
dt.date(2026, 8, 11),
|
||||
dt.date(2026, 8, 12),
|
||||
dt.date(2026, 8, 13),
|
||||
dt.date(2026, 8, 14),
|
||||
dt.date(2026, 8, 15),
|
||||
dt.date(2026, 8, 16),
|
||||
)
|
||||
|
||||
|
||||
def test_all_day_event_label() -> None:
|
||||
event = Event(
|
||||
summary="Escape the calendar mines",
|
||||
|
||||
Reference in New Issue
Block a user