feat(projects/khora): add forward agenda stream

Load agenda events in two-week chunks from the selected date, continuing near the scroll boundary with an explicit fallback to load more.

Assisted-by: pi (gpt-5.6-sol)
This commit is contained in:
Gabriel Fontes
2026-08-15 16:17:38 -03:00
parent 2957f91fb8
commit f4a1942885
3 changed files with 89 additions and 21 deletions
+4 -4
View File
@@ -13,12 +13,12 @@ Khora is an early, read-only prototype. It currently provides:
- calendar discovery from the existing khal configuration;
- per-calendar visibility controls;
- day and week time grids with recurring and all-day events;
- a read-only agenda for the selected day; and
- a forward-scrolling, read-only agenda; and
- explicit refreshes of khal's local index.
A month grid, a forward-scrolling agenda, editing, 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.
A month grid and event editing come next. The khal dependency is isolated in
`khora.khal_adapter` so its internal API can change without leaking through the
application.
## Development
+2 -2
View File
@@ -11,8 +11,8 @@ The order reflects engineering dependencies rather than product priority.
width, collapsed accounts, and visible calendars across restarts.
4. **Filesystem monitoring — done.** Refresh when local vdir files change,
without requiring the toolbar refresh button.
5. **Forward agenda** — replace the selected-day list with a lazily loaded,
continuously scrolling agenda starting at the selected date.
5. **Forward agenda — done.** A lazily loaded, continuously scrolling agenda
starts at the selected date.
6. **Overlapping event layout** — place simultaneous timed events in adjacent
lanes instead of drawing them on top of each other.
7. **Mini-calendar indicators** — show calendar-colored event dots on dates in
+83 -15
View File
@@ -19,6 +19,8 @@ DEFAULT_SLOT_HEIGHT = 24
MIN_SLOT_HEIGHT = 12
MAX_SLOT_HEIGHT = 64
ZOOM_STEP = 4
AGENDA_CHUNK_DAYS = 14
MAX_EMPTY_AGENDA_CHUNKS = 6
class KhoraWindow(Adw.ApplicationWindow):
@@ -70,6 +72,10 @@ class KhoraWindow(Adw.ApplicationWindow):
self._zoom_scroll_accumulator = 0.0
self._displayed_days: tuple[dt.date, ...] = ()
self._displayed_events: dict[dt.date, tuple[Event, ...]] = {}
self._agenda_next_day = dt.date.today()
self._agenda_loading = False
self._agenda_empty_chunks = 0
self._agenda_load_more: Gtk.Button | None = None
self._time_indicator: Gtk.Widget | None = None
self._rendered_today = dt.date.today()
self._empty = Adw.StatusPage(
@@ -223,6 +229,10 @@ class KhoraWindow(Adw.ApplicationWindow):
)
zoom_controller.connect("scroll", self._on_time_grid_scroll)
self._calendar_scroller.add_controller(zoom_controller)
self._calendar_scroller.get_vadjustment().connect(
"value-changed",
self._on_calendar_scroll,
)
overlay.set_child(self._calendar_scroller)
overlay.add_overlay(self._empty)
return overlay
@@ -279,6 +289,9 @@ class KhoraWindow(Adw.ApplicationWindow):
)
)
return
if self._view_mode == "agenda":
self._start_agenda()
return
days = self._visible_days()
try:
@@ -289,10 +302,7 @@ class KhoraWindow(Adw.ApplicationWindow):
self._displayed_days = days
self._displayed_events = events_by_day
if self._view_mode == "agenda":
self._render_agenda(days[0], events_by_day[days[0]])
else:
self._view_content.append(self._time_grid(days, events_by_day))
self._view_content.append(self._time_grid(days, events_by_day))
def _visible_days(self) -> tuple[dt.date, ...]:
if self._view_mode == "week":
@@ -304,21 +314,79 @@ class KhoraWindow(Adw.ApplicationWindow):
while child := self._view_content.get_first_child():
self._view_content.remove(child)
def _render_agenda(self, day: dt.date, events: tuple[Event, ...]) -> None:
if not events:
self._empty.set_visible(True)
def _start_agenda(self) -> None:
self._agenda_next_day = self._selected_day()
self._agenda_empty_chunks = 0
self._agenda_load_more = None
self._load_more_agenda()
def _load_more_agenda(self, *_args) -> None:
if self._agenda_loading or self._repository is None or self._view_mode != "agenda":
return
self._agenda_loading = True
if (
self._agenda_load_more is not None
and self._agenda_load_more.get_parent() is self._view_content
):
self._view_content.remove(self._agenda_load_more)
days = tuple(
self._agenda_next_day + dt.timedelta(days=offset)
for offset in range(AGENDA_CHUNK_DAYS)
)
try:
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))
self._agenda_loading = False
return
group = Adw.PreferencesGroup(
title=f"{day:%A, %B} {day.day}",
margin_top=24,
found_events = False
for day in days:
events = events_by_day[day]
if not events:
continue
found_events = True
group = Adw.PreferencesGroup(
title=f"{day:%A, %B} {day.day}",
margin_top=24,
margin_start=24,
margin_end=24,
)
for event in events:
group.add(self._event_row(event))
self._view_content.append(group)
self._agenda_next_day = days[-1] + dt.timedelta(days=1)
self._agenda_load_more = Gtk.Button(
label="Load more",
halign=Gtk.Align.CENTER,
margin_top=12,
margin_bottom=24,
margin_start=24,
margin_end=24,
)
for event in events:
group.add(self._event_row(event))
self._view_content.append(group)
self._agenda_load_more.connect("clicked", self._load_more_agenda)
self._view_content.append(self._agenda_load_more)
self._agenda_loading = False
if found_events:
self._agenda_empty_chunks = 0
else:
self._agenda_empty_chunks += 1
if self._agenda_empty_chunks < MAX_EMPTY_AGENDA_CHUNKS:
GLib.idle_add(self._load_next_empty_agenda_chunk)
def _load_next_empty_agenda_chunk(self) -> bool:
self._load_more_agenda()
return GLib.SOURCE_REMOVE
def _on_calendar_scroll(self, adjustment: Gtk.Adjustment) -> None:
if self._view_mode != "agenda" or self._agenda_loading:
return
if (
adjustment.get_value() + adjustment.get_page_size()
>= adjustment.get_upper() - 240
):
self._load_more_agenda()
def _time_grid(
self,