Sources/OrgCore/Agenda/Board.swift
58 lines · 2500 bytes
1import Foundation
2
3/// An entry as the table and kanban views show it.
4public struct BoardEntry: Sendable, Equatable, Identifiable {
5 public let path: String
6 public let headingOffset: Int
7 /// The heading line, to check the offset still holds before editing.
8 public let headingLine: String
9 public let title: String
10 public let todo: String?
11 public let isDone: Bool
12 public let priority: String?
13 public let tags: [String]
14 public let category: String
15 /// Planning timestamps without brackets.
16 public let scheduled: String?
17 public let deadline: String?
18 /// Upper-cased keys of the entry's own drawer.
19 public let properties: [String: String]
20
21 public var id: String { "\(path)#\(headingOffset)" }
22}
23
24public enum Board {
25 /// Entries of `sources` that match `match` (a tags/property/TODO match, as for `m` in the
26 /// agenda), or every entry with a TODO keyword when `match` is empty.
27 public static func entries(_ sources: [AgendaSource], match: String, now: Date = Date(), calendar: Calendar = .current) -> [BoardEntry] {
28 let trimmed = match.trimmingCharacters(in: .whitespaces)
29 let matcher = trimmed.isEmpty ? nil : TagsMatcher(trimmed)
30 var result: [BoardEntry] = []
31 for source in sources {
32 for heading in source.headings where !heading.skipped {
33 if let matcher {
34 guard matcher.matches(heading, in: source, now: now, calendar: calendar) else { continue }
35 } else if heading.todo == nil {
36 continue
37 }
38 result.append(BoardEntry(
39 path: source.path, headingOffset: heading.start, headingLine: heading.line, title: heading.title,
40 todo: heading.todo, isDone: heading.isDone, priority: heading.priority, tags: heading.tags.map(\.name),
41 category: heading.category, scheduled: heading.scheduled?.raw, deadline: heading.deadline?.raw,
42 properties: heading.properties
43 ))
44 }
45 }
46 return result
47 }
48
49 /// The keywords for kanban columns, in sequence order: each file's keywords, first come
50 /// first.
51 public static func keywords(_ sources: [AgendaSource]) -> [String] {
52 var seen: [String] = []
53 for source in sources {
54 for keyword in source.keywords where !seen.contains(keyword) { seen.append(keyword) }
55 }
56 return seen
57 }
58}