krz/orgstar

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

Sources/OrgApp/AgendaViews.swift

3a3dedba062790e1580c66d38606eda4b02cb501
orgstar/Sources/OrgApp/AgendaViews.swift history · blame · raw

87 lines · 3289 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 { UserConfig.file("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}