feat: add occurrence-aware appointment search

This commit is contained in:
2026-07-18 08:31:09 +01:00
parent c19098004e
commit 2774087d14
14 changed files with 485 additions and 11 deletions

View File

@@ -389,7 +389,7 @@ std::string detail_frame(std::span<const CalendarItem> items, const ScreenState&
add(" No notes for this appointment.", "2;3");
}
const auto footer = " ↑↓/Tab browse e edit d delete u undo Ctrl-R redo Esc back " +
const auto footer = " ↑↓/Tab browse / search e edit d delete u undo Ctrl-R redo Esc back " +
std::to_string(ordinal) + "/" + std::to_string(total) + " ";
const int body_rows = height - 2;
std::ostringstream output;
@@ -513,6 +513,68 @@ std::string source_focus_id(const std::span<const Event> events, const std::size
return unique ? uid : "@nocal:" + std::to_string(index);
}
std::string search_frame(const ScreenState& state) {
const int width = std::max(1, state.width);
const int height = std::max(1, state.height);
std::vector<std::string> lines;
lines.reserve(static_cast<std::size_t>(height));
lines.push_back(styled(centred("SEARCH APPOINTMENTS", width), "1", state.ansi));
if (state.search_prompt) {
if (height > 1) lines.push_back(fit_text("/ " + printable_text(state.search_query) + "_",
width, false));
if (height > 2) lines.push_back(styled(fit_text(
"Search title, description, location, or calendar ID", width), "2", state.ansi));
if (height > 3) lines.push_back(styled(fit_text(
"Enter search Backspace edit Esc cancel", width), "2", state.ansi));
} else {
const auto count = state.search_results.size();
const auto summary = count == 0
? "No matches for “" + printable_text(state.search_query) + ""
: std::to_string(count) + (count == 1 ? " match for “" : " matches for “") +
printable_text(state.search_query) + "";
if (height > 1) lines.push_back(styled(fit_text(summary, width), count == 0 ? "1" : "1;36",
state.ansi));
const auto row_capacity = static_cast<std::size_t>(std::max(0, height - 4));
std::size_t first = 0;
if (row_capacity > 0 && state.search_result_index >= row_capacity) {
first = state.search_result_index - row_capacity + 1;
}
const auto last = std::min(count, first + row_capacity);
for (std::size_t index = first; index < last; ++index) {
const auto& result = state.search_results[index];
std::string when = iso_date(result.day) + " ";
if (result.all_day || !result.time_of_day) when += "all day";
else when += clock_time(*result.time_of_day);
auto title = result.title.empty() ? std::string{"(untitled)"}
: printable_text(result.title);
std::string label = (index == state.search_result_index ? " " : " ") + when +
" " + (result.recurring ? "" : "") + title;
if (!result.location.empty()) label += "" + printable_text(result.location);
lines.push_back(styled(fit_text(label, width),
index == state.search_result_index ? "7;1" : "", state.ansi));
}
while (static_cast<int>(lines.size()) < height - 2) {
lines.emplace_back(static_cast<std::size_t>(width), ' ');
}
if (height > 2) lines.push_back(styled(fit_text(
"Five years either side of the selected date", width), "2", state.ansi));
if (height > 1) lines.push_back(styled(fit_text(
"↑/↓ choose Enter jump / new search Esc close", width), "2", state.ansi));
}
while (static_cast<int>(lines.size()) < height) {
lines.emplace_back(static_cast<std::size_t>(width), ' ');
}
if (static_cast<int>(lines.size()) > height) lines.resize(static_cast<std::size_t>(height));
std::ostringstream output;
for (int line = 0; line < height; ++line) {
if (line != 0) output << '\n';
output << lines[static_cast<std::size_t>(line)];
}
return output.str();
}
std::string compact_frame(std::span<const CalendarItem> items, const ScreenState& state) {
const int width = std::max(1, state.width);
const int height = std::max(1, state.height);
@@ -570,9 +632,9 @@ std::string compact_frame(std::span<const CalendarItem> items, const ScreenState
if (!state.notification.empty()) {
controls = printable_text(state.notification, true);
} else if (focused == nullptr) {
controls = "a add u undo Ctrl-R redo ? help n/p month t today q quit";
controls = "/ search a add u undo Ctrl-R redo ? help n/p month t today q quit";
} else {
controls = "Tab appointment Enter open e edit d delete u undo Ctrl-R redo";
controls = "Tab appointment Enter open / search e edit d delete u undo Ctrl-R redo";
}
if (static_cast<int>(lines.size()) < height) {
const auto style = state.notification.empty() ? "2"
@@ -621,11 +683,13 @@ Action decode_key(const std::string_view sequence) noexcept {
if (sequence == "d") return Action::delete_event;
if (sequence == "u") return Action::undo;
if (sequence == "\x12") return Action::redo;
if (sequence == "/") return Action::search;
if (sequence == "q" || sequence == "Q" || sequence == "\x03") return Action::quit;
return Action::none;
}
std::string render_month(std::span<const CalendarItem> items, const ScreenState& state) {
if (state.search_prompt || state.show_search_results) return search_frame(state);
const auto focused = focused_item(items, state);
if (state.confirm_delete && focused != nullptr) {
return delete_confirmation_frame(state, *focused);
@@ -744,19 +808,19 @@ std::string render_month(std::span<const CalendarItem> items, const ScreenState&
? by_day.at(sys_days{state.selected_day}).size() : 0U;
std::string status;
if (state.show_help) {
status = " arrows/hjkl day Tab/⇧Tab appointment Enter read a add e edit d delete u undo Ctrl-R redo n/p month t today ? close q quit";
status = " arrows/hjkl day Tab/⇧Tab appointment Enter read / search a add e edit d delete u undo Ctrl-R redo n/p month t today ? close q quit";
} else if (!state.notification.empty()) {
status = " " + printable_text(state.notification, true);
} else if (focused != nullptr) {
const auto focus_title = focused->title.empty() ? std::string{"(untitled)"}
: printable_text(focused->title);
status = " " + iso_date(state.selected_day) + " " +
focus_title + " Tab next Enter read e edit d delete u undo Ctrl-R redo";
focus_title + " Tab next Enter read / search e edit d delete u undo Ctrl-R redo";
} else {
status = " " + iso_date(state.selected_day) + " " + std::to_string(selected_count) +
(selected_count == 1 ? " appointment" : " appointments") +
(selected_count > 0 ? " Tab focus" : "") +
" a add u undo Ctrl-R redo ? help q quit";
" / search a add u undo Ctrl-R redo ? help q quit";
}
const auto status_style = !state.notification.empty()
? (state.notification_is_error ? "1;31" : "1;32")