Sources/OrgApp/AgendaViews.swift
87 lines · 3328 bytes
1import Foundation
2import OrgCore
3
4/// What the agenda window shows.
5public enum AgendaMode: Hashable, Sendable {
6 /// `org-agenda-list`; nil span and start use the settings.
7 case agenda(span: Int?, startOffset: Int?)
8 /// `org-todo-list`, optionally for `KWD1|KWD2`.
9 case todo(keywords: String?)
10 /// `org-tags-view`.
11 case match(String, todoOnly: Bool)
12}
13
14/// A named view: the built-in ones and those in `views.toml`.
15public struct AgendaViewDefinition: Hashable, Sendable, Identifiable {
16 public let name: String
17 public let mode: AgendaMode
18 public var id: String { name }
19
20 public init(name: String, mode: AgendaMode) {
21 self.name = name
22 self.mode = mode
23 }
24}
25
26/// Reads `views.toml`:
27///
28/// ```toml
29/// [[view]]
30/// name = "Work"
31/// type = "match" # agenda, todo, match or todo-match
32/// match = "+work-boss"
33/// ```
34///
35/// `agenda` views take `span` and `start` (days from today), `todo` views `keywords`
36/// (`WAIT|HOLD`).
37@MainActor
38public enum AgendaViewLoader {
39 public static var userFile: URL { WorkspaceModel.defaultDirectory.appendingPathComponent("views.toml") }
40
41 public static let builtIn = [
42 AgendaViewDefinition(name: "Agenda", mode: .agenda(span: nil, startOffset: nil)),
43 AgendaViewDefinition(name: "TODO List", mode: .todo(keywords: nil)),
44 ]
45
46 /// The built-in views followed by the user's, and the problems found in the file.
47 public static func load(userFile: URL = userFile) -> (views: [AgendaViewDefinition], problems: [String]) {
48 guard let text = try? String(contentsOf: userFile, encoding: .utf8) else { return (builtIn, []) }
49 let tables: [TOML.Table]
50 do {
51 tables = try TOML.parse(text)
52 } catch {
53 return (builtIn, ["views.toml: \(error)"])
54 }
55 var views = builtIn
56 var problems: [String] = []
57 for table in tables where !(table.name.isEmpty && table.values.isEmpty) {
58 guard table.name == "view", table.isArrayElement else {
59 problems.append("views.toml line \(table.line): expected [[view]]")
60 continue
61 }
62 guard let name = table.values["name"]?.string else {
63 problems.append("views.toml line \(table.line): a view needs a name")
64 continue
65 }
66 let match = table.values["match"]?.string
67 let mode: AgendaMode
68 switch table.values["type"]?.string ?? "agenda" {
69 case "agenda":
70 mode = .agenda(span: table.values["span"]?.integer, startOffset: table.values["start"]?.integer)
71 case "todo":
72 mode = .todo(keywords: table.values["keywords"]?.string)
73 case "match", "todo-match":
74 guard let match else {
75 problems.append("views.toml line \(table.line): \(name) needs a match")
76 continue
77 }
78 mode = .match(match, todoOnly: table.values["type"]?.string == "todo-match")
79 case let other:
80 problems.append("views.toml line \(table.line): unknown type \(other)")
81 continue
82 }
83 views.append(AgendaViewDefinition(name: name, mode: mode))
84 }
85 return (views, problems)
86 }
87}