krz/orgstar

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

Sources/OrgCore/Agenda/PrefixFormat.swift

15f6b0709d88971fb62ed432c3e5b8032643670a
orgstar/Sources/OrgCore/Agenda/PrefixFormat.swift history · blame · raw

83 lines · 4018 bytes

 1import Foundation
 2
 3/// `org-agenda-prefix-format` for one view, compiled as `org-compile-prefix-format` does:
 4/// `%c` category, `%t` time, `%l` level, `%s` extra, `%e` effort, `%b` breadcrumbs, `%T` last
 5/// tag (Org's regexp lists only lower-case keys but matches `T` under `case-fold-search`),
 6/// `%i` icon (none in Orgstar), each with an optional `?` (empty when the value is), a `%-12`-style
 7/// width with an optional `.N` limit on categories, and a punctuation character added after a
 8/// non-empty value. `%(…)` forms run Lisp; they are empty here.
 9public struct PrefixFormat: Sendable, Equatable {
10    enum Piece: Sendable, Equatable {
11        case literal(String)
12        case field(key: Character, optional: Bool, width: String, punctuation: String)
13    }
14
15    let pieces: [Piece]
16    /// `org-prefix-has-time`: times move out of the heading into the prefix.
17    let hasTime: Bool
18    /// `.N` in the category width: categories this long or longer are cut to N - 1.
19    let categoryMaxLength: Int?
20
21    public init(_ format: String) {
22        let regex = try! NSRegularExpression(pattern: "%(\\?)?([-+]?[0-9.]*)([ .;,:!?=|/<>]?)([cltseibT]|\\(.+?\\))")
23        let ns = format as NSString
24        var pieces: [Piece] = []
25        var hasTime = false
26        var maxLength: Int?
27        var at = 0
28        for m in regex.matches(in: format, range: NSRange(location: 0, length: ns.length)) {
29            if m.range.location > at { pieces.append(.literal(ns.substring(with: NSRange(location: at, length: m.range.location - at)))) }
30            let key = ns.substring(with: m.range(at: 4))
31            let width = ns.substring(with: m.range(at: 2))
32            if key.hasPrefix("(") {
33                pieces.append(.field(key: "(", optional: false, width: width, punctuation: ""))
34            } else {
35                let k = key.first!
36                if k == "t" { hasTime = true }
37                if k == "c", let dot = width.firstIndex(of: ".") { maxLength = Int(width[width.index(after: dot)...].prefix { $0.isNumber }) }
38                pieces.append(.field(key: k, optional: m.range(at: 1).location != NSNotFound, width: width, punctuation: ns.substring(with: m.range(at: 3))))
39            }
40            at = NSMaxRange(m.range)
41        }
42        if at < ns.length { pieces.append(.literal(ns.substring(from: at))) }
43        self.pieces = pieces
44        self.hasTime = hasTime
45        self.categoryMaxLength = maxLength
46    }
47
48    /// The prefix for these values, by field key.
49    func render(_ values: [Character: String]) -> String {
50        var out = ""
51        for piece in pieces {
52            switch piece {
53            case .literal(let s): out += s
54            case .field(let key, let optional, let width, let punctuation):
55                var value = values[key] ?? ""
56                if key == "c", let max = categoryMaxLength, value.count >= max { value = String(value.prefix(max - 1)) }
57                if value.isEmpty {
58                    out += optional ? "" : Self.format("", width)
59                } else {
60                    out += Self.format(value + punctuation, width)
61                }
62            }
63        }
64        return out
65    }
66
67    /// Emacs `format` with `%WIDTHs` or `%-WIDTH.PRECISIONs`.
68    static func format(_ s: String, _ spec: String) -> String {
69        var spec = Substring(spec)
70        let left = spec.first == "-"
71        if left || spec.first == "+" { spec = spec.dropFirst() }
72        let parts = spec.split(separator: ".", omittingEmptySubsequences: false)
73        let width = Int(parts.first ?? "") ?? 0
74        var value = s
75        if parts.count > 1, let precision = Int(parts[1]) { value = String(value.prefix(precision)) }
76        let padding = String(repeating: " ", count: max(0, width - displayWidth(value)))
77        return left ? value + padding : padding + value
78    }
79
80    public static let defaults: [String: String] = [
81        "agenda": " %i %-12:c%?-12t% s", "todo": " %i %-12:c", "tags": " %i %-12:c", "search": " %i %-12:c",
82    ]
83}