Sources/OrgCore/Agenda/Reminders.swift
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}