feat: add recurrence and time zone support

This commit is contained in:
2026-07-17 21:52:02 +01:00
parent 22c6399056
commit c19098004e
18 changed files with 2321 additions and 132 deletions

View File

@@ -1,13 +1,31 @@
#pragma once
#include <chrono>
#include <optional>
#include <string>
#include <vector>
namespace nocal {
using Clock = std::chrono::system_clock;
using TimePoint = Clock::time_point;
enum class TimeBasis { floating, utc, zoned };
enum class RecurrenceFrequency { daily, weekly, monthly, yearly };
struct RecurrenceRule {
RecurrenceFrequency frequency{RecurrenceFrequency::daily};
unsigned interval{1};
std::optional<unsigned> count;
std::optional<TimePoint> until;
std::vector<std::chrono::weekday> by_weekday;
std::vector<int> by_month_day;
std::vector<unsigned> by_month;
bool operator==(const RecurrenceRule&) const = default;
};
struct Event {
std::string uid;
std::string title;
@@ -18,6 +36,10 @@ struct Event {
std::string description;
std::string calendar_id;
std::string color;
TimeBasis time_basis{TimeBasis::utc};
std::string time_zone;
std::optional<RecurrenceRule> recurrence;
std::vector<TimePoint> recurrence_exceptions;
};
struct Calendar {

View File

@@ -3,6 +3,7 @@
#include "nocal/domain/date.hpp"
#include "nocal/domain/event.hpp"
#include <cstddef>
#include <functional>
#include <span>
#include <vector>
@@ -12,6 +13,30 @@ namespace nocal {
using EventRef = std::reference_wrapper<const Event>;
using EventRefs = std::vector<EventRef>;
struct EventOccurrence {
const Event* source{};
TimePoint start{};
TimePoint end{};
std::size_t ordinal{};
};
using EventOccurrences = std::vector<EventOccurrence>;
[[nodiscard]] bool occurrence_less(const EventOccurrence& lhs,
const EventOccurrence& rhs) noexcept;
void sort_occurrences(EventOccurrences& occurrences);
// Recurrence query intervals are half-open. DTSTART is ordinal zero and COUNT
// includes it. EXDATE can remove any exact generated start, including DTSTART.
// Invalid rules produce no additional occurrences but retain DTSTART unless it
// is explicitly excepted.
[[nodiscard]] EventOccurrences occurrences_overlapping(
std::span<const Event> events, TimePoint begin, TimePoint end);
[[nodiscard]] EventOccurrences occurrences_on_day(std::span<const Event> events,
Date date);
[[nodiscard]] EventOccurrences occurrences_in_month(std::span<const Event> events,
YearMonth month);
[[nodiscard]] bool event_less(const Event& lhs, const Event& rhs) noexcept;
void sort_events(EventRefs& events);

View File

@@ -7,9 +7,17 @@
#include <string_view>
#include "nocal/domain/event.hpp"
#include "nocal/domain/event_query.hpp"
namespace nocal::tui {
enum class ItemSegment {
single,
start,
continuation,
end,
};
// A deliberately small view-model keeps the renderer independent of storage and
// makes it straightforward to unit test. TuiApp adapts domain Events to this.
struct CalendarItem {
@@ -26,6 +34,10 @@ struct CalendarItem {
std::string detail_title;
bool detail_all_day{false};
std::optional<std::chrono::minutes> detail_start_time_of_day;
ItemSegment segment{ItemSegment::single};
bool recurring{false};
std::string recurrence_summary;
std::string source_time_zone;
};
struct ScreenState {
@@ -50,11 +62,17 @@ struct ScreenState {
[[nodiscard]] std::string render_month(std::span<const CalendarItem> items,
const ScreenState& state);
// Convenience adapter for the domain model. Event timestamps are projected to
// the process's local civil time until explicit calendar time zones are added.
// Convenience adapter for the domain model. Occurrence instants are projected
// into the user's process-local zone; the reader retains an explicit source TZID.
[[nodiscard]] std::string render_month(std::span<const Event> events,
const ScreenState& state);
// Stable focus identity shared by the renderer and controller. Non-recurring
// events retain their historical UID/synthetic identity; recurring instances
// additionally include their concrete start instant.
[[nodiscard]] std::string occurrence_focus_id(std::span<const Event> events,
const EventOccurrence& occurrence);
enum class Action {
none,
left,

View File

@@ -72,6 +72,7 @@ private:
std::vector<HistoryEntry> redo_history_;
bool running_{true};
[[nodiscard]] std::optional<EventOccurrence> focused_occurrence() const;
[[nodiscard]] std::optional<std::size_t> focused_event_index() const;
[[nodiscard]] HistorySnapshot capture_history_snapshot() const;
void restore_history_snapshot(HistorySnapshot snapshot);