krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgCore/Agenda/Reminders.swift

3a3dedba062790e1580c66d38606eda4b02cb501
orgstar/Sources/OrgCore/Agenda/Reminders.swift history · blame · raw

58 lines · 2963 bytes

 1import Foundation
 2
 3/// A notification for an agenda entry with a time of day.
 4public struct Reminder: Sendable, Equatable {
 5    /// Stable while the entry, its day and time stay the same.
 6    public let id: String
 7    public let fireDate: Date
 8    public let eventDate: Date
 9    public let title: String
10    public let body: String
11    public let path: String
12    public let offset: Int
13
14    public init(id: String, fireDate: Date, eventDate: Date, title: String, body: String, path: String, offset: Int) {
15        self.id = id
16        self.fireDate = fireDate
17        self.eventDate = eventDate
18        self.title = title
19        self.body = body
20        self.path = path
21        self.offset = offset
22    }
23}
24
25/// What `org-agenda-to-appt` hands appt: timed entries, each with a warning before it.
26public enum Reminders {
27    /// Reminders for the next `days` days from `now`, earliest first, at most `limit`. An
28    /// entry whose warning time has passed but which hasn't started fires now.
29    public static func upcoming(
30        _ sources: [AgendaSource], now: Date, calendar: Calendar, days: Int = 7, lead: Int = 12, limit: Int = 64
31    ) -> [Reminder] {
32        let today = Days.absolute(now, calendar: calendar)
33        let clock = calendar.dateComponents([.hour, .minute], from: now)
34        let list = Agenda.list(sources, start: today, days: days, today: today, now: (clock.hour ?? 0, clock.minute ?? 0))
35        var reminders: [Reminder] = []
36        for day in list {
37            let date = Days.date(day.day)
38            guard let midnight = calendar.date(from: DateComponents(year: date.year, month: date.month, day: date.day)) else { continue }
39            for item in day.items {
40                guard let time = item.timeOfDay, let path = item.path, !item.isDone,
41                      [.deadline, .scheduled, .timestamp, .block].contains(item.kind),
42                      let event = calendar.date(byAdding: .minute, value: (time / 100) * 60 + time % 100, to: midnight),
43                      event > now else { continue }
44                let fire = max(event.addingTimeInterval(-Double((item.warntime ?? lead) * 60)), now.addingTimeInterval(1))
45                let clockText = String(format: "%d:%02d", time / 100, time % 100)
46                let title = item.text.trimmingCharacters(in: .whitespaces)
47                let leader = item.extra.trimmingCharacters(in: .whitespaces)
48                reminders.append(Reminder(
49                    id: "orgstar.\(path)|\(item.headingLine ?? title)|\(day.day)|\(time)|\(item.kind.rawValue)",
50                    fireDate: fire, eventDate: event, title: title,
51                    body: ([clockText] + (leader.isEmpty ? [] : [leader]) + [item.category]).joined(separator: " · "),
52                    path: path, offset: item.markerOffset ?? item.headingOffset ?? 0
53                ))
54            }
55        }
56        return Array(reminders.sorted { $0.fireDate < $1.fireDate }.prefix(limit))
57    }
58}