feat: add bounded agenda view
Add a read-only 42-day occurrence agenda with keyboard navigation, six-week paging, exact return-to-grid focus, search and calendar overlays, and bounded recurrence expansion. Keep agenda state outside persistence, document month-only mutations, and cover empty windows, filtering, overlays, navigation, rendering, and terminal restoration.
This commit is contained in:
@@ -169,6 +169,111 @@ void TuiApp::toggle_selected_calendar() {
|
||||
state_.focused_event_id.reset();
|
||||
state_.show_event_details = false;
|
||||
}
|
||||
if (state_.show_agenda) refresh_agenda();
|
||||
}
|
||||
|
||||
void TuiApp::begin_agenda() {
|
||||
state_.show_agenda = true;
|
||||
state_.agenda_start_day = state_.selected_day;
|
||||
state_.agenda_index = 0;
|
||||
state_.agenda_items.clear();
|
||||
state_.show_event_details = false;
|
||||
state_.show_help = false;
|
||||
state_.confirm_delete = false;
|
||||
state_.search_prompt = false;
|
||||
state_.show_search_results = false;
|
||||
state_.show_calendar_picker = false;
|
||||
refresh_agenda();
|
||||
}
|
||||
|
||||
void TuiApp::close_agenda() {
|
||||
state_.show_agenda = false;
|
||||
}
|
||||
|
||||
void TuiApp::refresh_agenda() {
|
||||
if (!state_.agenda_start_day.ok()) state_.agenda_start_day = state_.selected_day;
|
||||
std::optional<std::string> selected_identity;
|
||||
if (state_.agenda_index < state_.agenda_items.size()) {
|
||||
selected_identity = state_.agenda_items[state_.agenda_index].focus_id;
|
||||
}
|
||||
|
||||
state_.agenda_items.clear();
|
||||
const auto event_span = std::span<const Event>{events_};
|
||||
const auto calendar_span = std::span<const Calendar>{calendars_};
|
||||
const auto first_day = sys_days{state_.agenda_start_day};
|
||||
constexpr auto window = days{42};
|
||||
const auto end_day = first_day + window;
|
||||
for (const auto& occurrence : occurrences_overlapping(
|
||||
event_span, local_day_start(year_month_day{first_day}),
|
||||
local_day_start(year_month_day{end_day}))) {
|
||||
if (occurrence.source == nullptr ||
|
||||
!event_is_visible(*occurrence.source, calendar_span)) {
|
||||
continue;
|
||||
}
|
||||
const auto occurrence_day = local_date(occurrence.start);
|
||||
const auto display_day = std::max(occurrence_day, state_.agenda_start_day);
|
||||
const bool continuation = display_day != occurrence_day;
|
||||
state_.agenda_items.push_back(AgendaItem{
|
||||
.focus_id = occurrence_focus_id(event_span, occurrence),
|
||||
.title = occurrence.source->title,
|
||||
.day = display_day,
|
||||
.time_of_day = occurrence.source->all_day || continuation
|
||||
? std::nullopt
|
||||
: std::optional{local_time_of_day(occurrence.start)},
|
||||
.all_day = occurrence.source->all_day || continuation,
|
||||
.location = occurrence.source->location,
|
||||
.calendar_id = occurrence.source->calendar_id,
|
||||
.recurring = occurrence.source->recurrence.has_value(),
|
||||
});
|
||||
}
|
||||
std::stable_sort(state_.agenda_items.begin(), state_.agenda_items.end(),
|
||||
[](const AgendaItem& left, const AgendaItem& right) {
|
||||
const auto left_day = sys_days{left.day};
|
||||
const auto right_day = sys_days{right.day};
|
||||
if (left_day != right_day) return left_day < right_day;
|
||||
if (left.all_day != right.all_day) return left.all_day;
|
||||
if (left.time_of_day != right.time_of_day) {
|
||||
return left.time_of_day.value_or(minutes{-1}) <
|
||||
right.time_of_day.value_or(minutes{-1});
|
||||
}
|
||||
if (left.title != right.title) return left.title < right.title;
|
||||
return left.focus_id < right.focus_id;
|
||||
});
|
||||
|
||||
if (state_.agenda_items.empty()) {
|
||||
state_.agenda_index = 0;
|
||||
return;
|
||||
}
|
||||
if (selected_identity) {
|
||||
const auto selected = std::find_if(
|
||||
state_.agenda_items.begin(), state_.agenda_items.end(),
|
||||
[&](const AgendaItem& item) { return item.focus_id == *selected_identity; });
|
||||
if (selected != state_.agenda_items.end()) {
|
||||
state_.agenda_index = static_cast<std::size_t>(selected - state_.agenda_items.begin());
|
||||
return;
|
||||
}
|
||||
}
|
||||
state_.agenda_index = std::min(state_.agenda_index, state_.agenda_items.size() - 1);
|
||||
}
|
||||
|
||||
void TuiApp::shift_agenda(const int direction) {
|
||||
if (direction == 0) return;
|
||||
constexpr auto window = days{42};
|
||||
state_.agenda_start_day = year_month_day{
|
||||
sys_days{state_.agenda_start_day} + window * direction};
|
||||
state_.agenda_items.clear();
|
||||
state_.agenda_index = 0;
|
||||
refresh_agenda();
|
||||
}
|
||||
|
||||
void TuiApp::choose_agenda_item() {
|
||||
if (state_.agenda_index >= state_.agenda_items.size()) return;
|
||||
const auto& item = state_.agenda_items[state_.agenda_index];
|
||||
state_.selected_day = item.day;
|
||||
state_.visible_month = item.day.year() / item.day.month();
|
||||
state_.focused_event_id = item.focus_id;
|
||||
state_.show_event_details = false;
|
||||
close_agenda();
|
||||
}
|
||||
|
||||
void TuiApp::set_notification(std::string message, const bool error) {
|
||||
@@ -198,6 +303,7 @@ void TuiApp::restore_history_snapshot(HistorySnapshot snapshot) {
|
||||
state_.search_prompt = false;
|
||||
state_.show_search_results = false;
|
||||
state_.show_calendar_picker = false;
|
||||
state_.show_agenda = false;
|
||||
synchronize_calendars();
|
||||
}
|
||||
|
||||
@@ -272,6 +378,7 @@ void TuiApp::begin_add() {
|
||||
state_.show_help = false;
|
||||
state_.confirm_delete = false;
|
||||
state_.show_calendar_picker = false;
|
||||
state_.show_agenda = false;
|
||||
}
|
||||
|
||||
void TuiApp::begin_edit() {
|
||||
@@ -292,6 +399,7 @@ void TuiApp::begin_edit() {
|
||||
state_.show_help = false;
|
||||
state_.confirm_delete = false;
|
||||
state_.show_calendar_picker = false;
|
||||
state_.show_agenda = false;
|
||||
}
|
||||
|
||||
void TuiApp::begin_delete() {
|
||||
@@ -304,6 +412,7 @@ void TuiApp::begin_delete() {
|
||||
state_.show_event_details = false;
|
||||
state_.show_help = false;
|
||||
state_.show_calendar_picker = false;
|
||||
state_.show_agenda = false;
|
||||
}
|
||||
|
||||
void TuiApp::submit_editor() {
|
||||
@@ -491,6 +600,7 @@ void TuiApp::choose_search_result() {
|
||||
state_.visible_month = result.day.year() / result.day.month();
|
||||
state_.focused_event_id = result.focus_id;
|
||||
state_.show_event_details = false;
|
||||
state_.show_agenda = false;
|
||||
close_search();
|
||||
}
|
||||
|
||||
@@ -611,6 +721,7 @@ void TuiApp::dispatch(const Action action) {
|
||||
case Action::delete_event:
|
||||
case Action::undo:
|
||||
case Action::redo:
|
||||
case Action::agenda:
|
||||
case Action::calendars:
|
||||
return;
|
||||
}
|
||||
@@ -652,6 +763,59 @@ void TuiApp::dispatch(const Action action) {
|
||||
case Action::undo:
|
||||
case Action::redo:
|
||||
case Action::search:
|
||||
case Action::agenda:
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (state_.show_agenda) {
|
||||
const auto count = state_.agenda_items.size();
|
||||
switch (action) {
|
||||
case Action::up:
|
||||
case Action::left:
|
||||
case Action::previous_event:
|
||||
if (count > 0 && state_.agenda_index > 0) --state_.agenda_index;
|
||||
return;
|
||||
case Action::down:
|
||||
case Action::right:
|
||||
case Action::next_event:
|
||||
if (count > 0 && state_.agenda_index + 1 < count) ++state_.agenda_index;
|
||||
return;
|
||||
case Action::previous_month:
|
||||
shift_agenda(-1);
|
||||
return;
|
||||
case Action::next_month:
|
||||
shift_agenda(1);
|
||||
return;
|
||||
case Action::today:
|
||||
state_.today = current_day();
|
||||
state_.agenda_start_day = state_.today;
|
||||
state_.agenda_items.clear();
|
||||
state_.agenda_index = 0;
|
||||
refresh_agenda();
|
||||
return;
|
||||
case Action::select:
|
||||
choose_agenda_item();
|
||||
return;
|
||||
case Action::back:
|
||||
case Action::agenda:
|
||||
close_agenda();
|
||||
return;
|
||||
case Action::search:
|
||||
begin_search();
|
||||
return;
|
||||
case Action::calendars:
|
||||
begin_calendar_picker();
|
||||
return;
|
||||
case Action::quit:
|
||||
running_ = false;
|
||||
return;
|
||||
case Action::none:
|
||||
case Action::toggle_help:
|
||||
case Action::add_event:
|
||||
case Action::edit_event:
|
||||
case Action::delete_event:
|
||||
case Action::undo:
|
||||
case Action::redo:
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -723,6 +887,9 @@ void TuiApp::dispatch(const Action action) {
|
||||
case Action::search:
|
||||
begin_search();
|
||||
return;
|
||||
case Action::agenda:
|
||||
begin_agenda();
|
||||
return;
|
||||
case Action::calendars:
|
||||
begin_calendar_picker();
|
||||
return;
|
||||
@@ -816,6 +983,9 @@ void TuiApp::dispatch(const Action action) {
|
||||
case Action::search:
|
||||
begin_search();
|
||||
return;
|
||||
case Action::agenda:
|
||||
begin_agenda();
|
||||
return;
|
||||
case Action::calendars:
|
||||
begin_calendar_picker();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user