krz/orgstar

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

Commit eba8a09f88

eba8a09f88f681c8e9a6bb7aeab5fbdc303c4e16

parent: 2fa201330f

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-09 06:40 UTC

Add the agenda text search view (org-search-view)

Agenda.textSearch: phrase and boolean (+word, -word, {regexp}, "quoted")
searches over entries, with the *, ! and : prefixes, todo-only, and
org-agenda-search-view-always-boolean / -force-full-words settings.
search and todo-search views in views.toml; Search… and TODO Search… in
the Mac view menu, Search… in the iOS Views menu. Oracle tests against
Org 9.8.7.

Layout: unified · split

Sources/OrgApp/AgendaModel.swift +3 −1
@@ -11,7 +11,7 @@ import OrgPresentation
11@Observable 11@Observable
12public final class AgendaModel { 12public final class AgendaModel {
13 public private(set) var days: [AgendaDay] = [] 13 public private(set) var days: [AgendaDay] = []
14 /// The entries of a TODO list or match view. 14 /// The entries of a TODO list, match or search view.
15 public private(set) var list: [AgendaItem] = [] 15 public private(set) var list: [AgendaItem] = []
16 public var mode: AgendaMode = .agenda(span: nil, startOffset: nil) 16 public var mode: AgendaMode = .agenda(span: nil, startOffset: nil)
17 /// The first day shown, as `Days` numbers it; nil follows today. 17 /// The first day shown, as `Days` numbers it; nil follows today.
@@ -84,6 +84,8 @@ public final class AgendaModel {
84 list = Agenda.todoList(all, keywords: keywords, options: options) 84 list = Agenda.todoList(all, keywords: keywords, options: options)
85 case .match(let match, let todoOnly): 85 case .match(let match, let todoOnly):
86 list = Agenda.tagsMatch(all, match: match, todoOnly: todoOnly, now: now, calendar: calendar, options: options) 86 list = Agenda.tagsMatch(all, match: match, todoOnly: todoOnly, now: now, calendar: calendar, options: options)
87 case .search(let string, let todoOnly):
88 list = Agenda.textSearch(all, string: string, todoOnly: todoOnly, options: options)
87 } 89 }
88 return (days, list, events) 90 return (days, list, events)
89 }.value 91 }.value
Sources/OrgApp/AgendaViews.swift +10 −2
@@ -9,6 +9,8 @@ public enum AgendaMode: Hashable, Sendable {
9 case todo(keywords: String?) 9 case todo(keywords: String?)
10 /// `org-tags-view`. 10 /// `org-tags-view`.
11 case match(String, todoOnly: Bool) 11 case match(String, todoOnly: Bool)
12 /// `org-search-view`.
13 case search(String, todoOnly: Bool)
12} 14}
13 15
14/// A named view: the built-in ones and those in `views.toml`. 16/// A named view: the built-in ones and those in `views.toml`.
@@ -28,12 +30,12 @@ public struct AgendaViewDefinition: Hashable, Sendable, Identifiable {
28/// ```toml 30/// ```toml
29/// [[view]] 31/// [[view]]
30/// name = "Work" 32/// name = "Work"
31/// type = "match" # agenda, todo, match or todo-match 33/// type = "match" # agenda, todo, match, todo-match, search or todo-search
32/// match = "+work-boss" 34/// match = "+work-boss"
33/// ``` 35/// ```
34/// 36///
35/// `agenda` views take `span` and `start` (days from today), `todo` views `keywords` 37/// `agenda` views take `span` and `start` (days from today), `todo` views `keywords`
36/// (`WAIT|HOLD`). 38/// (`WAIT|HOLD`); a `search` view's `match` is its search string.
37@MainActor 39@MainActor
38public enum AgendaViewLoader { 40public enum AgendaViewLoader {
39 public static var userFile: URL { UserConfig.file("views.toml") } 41 public static var userFile: URL { UserConfig.file("views.toml") }
@@ -76,6 +78,12 @@ public enum AgendaViewLoader {
76 continue 78 continue
77 } 79 }
78 mode = .match(match, todoOnly: table.values["type"]?.string == "todo-match") 80 mode = .match(match, todoOnly: table.values["type"]?.string == "todo-match")
81 case "search", "todo-search":
82 guard let match else {
83 problems.append("views.toml line \(table.line): \(name) needs a match")
84 continue
85 }
86 mode = .search(match, todoOnly: table.values["type"]?.string == "todo-search")
79 case let other: 87 case let other:
80 problems.append("views.toml line \(table.line): unknown type \(other)") 88 problems.append("views.toml line \(table.line): unknown type \(other)")
81 continue 89 continue
Sources/OrgApp/OrgPreferences.swift +5 −1
@@ -54,11 +54,15 @@ public enum OrgPreferences {
54 public static func agendaShowAllDates(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaShowAllDates", defaults) } 54 public static func agendaShowAllDates(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaShowAllDates", defaults) }
55 public static func agendaSkipScheduledIfDone(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSkipScheduledIfDone", defaults) } 55 public static func agendaSkipScheduledIfDone(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSkipScheduledIfDone", defaults) }
56 public static func agendaSkipDeadlineIfDone(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSkipDeadlineIfDone", defaults) } 56 public static func agendaSkipDeadlineIfDone(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSkipDeadlineIfDone", defaults) }
57 /// `options` with the skip-if-done settings. 57 public static func agendaSearchAlwaysBoolean(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSearchAlwaysBoolean", defaults) }
58 public static func agendaSearchForceFullWords(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSearchForceFullWords", defaults) }
59 /// `options` with the skip-if-done and search settings.
58 public static func agendaOptions(_ options: AgendaOptions, _ defaults: UserDefaults = .standard) -> AgendaOptions { 60 public static func agendaOptions(_ options: AgendaOptions, _ defaults: UserDefaults = .standard) -> AgendaOptions {
59 var options = options 61 var options = options
60 options.skipScheduledIfDone = agendaSkipScheduledIfDone(defaults) 62 options.skipScheduledIfDone = agendaSkipScheduledIfDone(defaults)
61 options.skipDeadlineIfDone = agendaSkipDeadlineIfDone(defaults) 63 options.skipDeadlineIfDone = agendaSkipDeadlineIfDone(defaults)
64 options.searchAlwaysBoolean = agendaSearchAlwaysBoolean(defaults)
65 options.searchForceFullWords = agendaSearchForceFullWords(defaults)
62 return options 66 return options
63 } 67 }
64 /// `calendar-events`: Calendar's events show in the agenda. 68 /// `calendar-events`: Calendar's events show in the agenda.
Sources/OrgApp/UserConfig.swift +4
@@ -147,6 +147,10 @@ public enum UserConfig {
147 comment: "leave done entries out of the agenda even on the day they are scheduled", legacy: ""), 147 comment: "leave done entries out of the agenda even on the day they are scheduled", legacy: ""),
148 Setting(section: "", key: "org-agenda-skip-deadline-if-done", defaultsKey: "agendaSkipDeadlineIfDone", kind: .bool, fallback: .bool(false), 148 Setting(section: "", key: "org-agenda-skip-deadline-if-done", defaultsKey: "agendaSkipDeadlineIfDone", kind: .bool, fallback: .bool(false),
149 comment: "leave done entries out of the agenda even on the day they are due", legacy: ""), 149 comment: "leave done entries out of the agenda even on the day they are due", legacy: ""),
150 Setting(section: "", key: "org-agenda-search-view-always-boolean", defaultsKey: "agendaSearchAlwaysBoolean", kind: .bool, fallback: .bool(false),
151 comment: "search strings are always +word -word {regexp} snippets, never a phrase", legacy: ""),
152 Setting(section: "", key: "org-agenda-search-view-force-full-words", defaultsKey: "agendaSearchForceFullWords", kind: .bool, fallback: .bool(false),
153 comment: "search snippets match whole words only", legacy: ""),
150 Setting(section: "", key: "appt-message-warning-time", defaultsKey: "reminderLead", kind: .integer, fallback: .integer(12), 154 Setting(section: "", key: "appt-message-warning-time", defaultsKey: "reminderLead", kind: .integer, fallback: .integer(12),
151 comment: "minutes of warning before timed entries; APPT_WARNTIME overrides", legacy: "agenda.reminder-lead"), 155 comment: "minutes of warning before timed entries; APPT_WARNTIME overrides", legacy: "agenda.reminder-lead"),
152 Setting(section: "", key: "org-clock-idle-time", defaultsKey: "clockIdleTime", kind: .integer, fallback: .integer(0), 156 Setting(section: "", key: "org-clock-idle-time", defaultsKey: "clockIdleTime", kind: .integer, fallback: .integer(0),
Sources/OrgCore/Agenda/Agenda.swift +5
@@ -13,6 +13,7 @@ public struct AgendaItem: Sendable, Equatable {
13 case currentTime 13 case currentTime
14 case todo 14 case todo
15 case tagsMatch = "tagsmatch" 15 case tagsMatch = "tagsmatch"
16 case search
16 /// Log mode: a closed entry, a clock line, a state change. 17 /// Log mode: a closed entry, a clock line, a state change.
17 case closed, clock, state 18 case closed, clock, state
18 } 19 }
@@ -79,6 +80,10 @@ public struct AgendaOptions: Sendable, Equatable {
79 public var skipScheduledIfDone = false 80 public var skipScheduledIfDone = false
80 /// `org-agenda-skip-deadline-if-done`: done entries' deadlines are left out even on the day due. 81 /// `org-agenda-skip-deadline-if-done`: done entries' deadlines are left out even on the day due.
81 public var skipDeadlineIfDone = false 82 public var skipDeadlineIfDone = false
83 /// `org-agenda-search-view-always-boolean`: search strings are always `+word -word` snippets.
84 public var searchAlwaysBoolean = false
85 /// `org-agenda-search-view-force-full-words`: search snippets match whole words only.
86 public var searchForceFullWords = false
82 87
83 public init() {} 88 public init() {}
84 89
Sources/OrgCore/Agenda/AgendaSearch.swift +185
@@ -44,6 +44,185 @@ extension Agenda {
44 return sortByUrgency(items) 44 return sortByUrgency(items)
45 } 45 }
46 46
47 /// `org-search-view`: entries, heading and body, containing `string` as a phrase, or with
48 /// a leading `+`, `-` or `{` (or `searchAlwaysBoolean`) matching each of its snippets. A
49 /// leading `*` searches headlines only, `!` keeps unfinished TODO entries as `todoOnly`
50 /// does, and `:` matches whole words. Text before the first heading is not searched.
51 public static func textSearch(_ sources: [AgendaSource], string: String, todoOnly: Bool = false, options: AgendaOptions = AgendaOptions()) -> [AgendaItem] {
52 let query = SearchQuery(string, todoOnly: todoOnly, options: options)
53 // Org searches for the longest positive snippet, then checks the entry it is in.
54 var positive = query.positive
55 let first = positive.isEmpty ? nil : positive.removeFirst()
56 let patterns = positive.compactMap(\.regex)
57 let negative = query.negative.compactMap(\.regex)
58 guard let search = SearchQuery.compile(first.map { (query.headlinesOnly ? "^\\*+ .*?" : "") + $0.emacs } ?? "^\\*+ "),
59 patterns.count == positive.count, negative.count == query.negative.count else { return [] }
60 let headingLine = try! NSRegularExpression(pattern: "^\\*+ ", options: .anchorsMatchLines)
61 var items: [AgendaItem] = []
62 for source in sources {
63 let text = source.text
64 let ns = text as NSString
65 let length = ns.length
66 let starts = headingLine.matches(in: text, range: NSRange(location: 0, length: length)).map(\.range.location)
67 guard let firstHeading = starts.first else { continue }
68 let byStart = Dictionary(source.headings.map { ($0.start, $0) }, uniquingKeysWith: { a, _ in a })
69 var required = patterns
70 if query.todoOnly {
71 guard !source.notDoneKeywords.isEmpty else { continue }
72 let keywords = source.notDoneKeywords.map(SearchQuery.quote).joined(separator: "\\|")
73 required.insert(SearchQuery.compile("^\\*+[ \\t]+\\(" + keywords + "\\)")!, at: 0)
74 }
75 func lineStart(_ at: Int) -> Int {
76 let newline = ns.range(of: "\n", options: .backwards, range: NSRange(location: 0, length: at))
77 return newline.location == NSNotFound ? 0 : newline.location + 1
78 }
79 var at = max(0, firstHeading - 1)
80 while at <= length, let m = search.firstMatch(in: text, options: [.withoutAnchoringBounds, .withTransparentBounds], range: NSRange(location: at, length: length - at)) {
81 let matchEnd = NSMaxRange(m.range)
82 // `org-back-to-heading`, then `outline-next-heading`.
83 guard let index = starts.lastIndex(where: { $0 <= lineStart(matchEnd) }) else { break }
84 let beg = starts[index]
85 let end = index + 1 < starts.count ? starts[index + 1] : length
86 let next = max(end - 1, at + 1)
87 guard let entry = byStart[beg] else {
88 at = next
89 continue
90 }
91 if entry.skipped {
92 // `org-agenda-skip` leaves point at the end of the subtree, before blank lines.
93 var stop = starts[(index + 1)...].first { byStart[$0].map { $0.level <= entry.level } ?? false } ?? length
94 if stop > 0, [10, 13].contains(ns.character(at: stop - 1)) {
95 stop -= 1
96 while stop > beg, [9, 10, 13, 32].contains(ns.character(at: stop - 1)) { stop -= 1 }
97 }
98 at = max(stop, at + 1)
99 continue
100 }
101 let lineEnd = ns.range(of: "\n", range: NSRange(location: beg, length: length - beg)).location
102 let stop = query.headlinesOnly && lineEnd != NSNotFound ? lineEnd : end
103 let entryText = ns.substring(with: NSRange(location: beg, length: stop - beg))
104 func found(_ regex: NSRegularExpression) -> Bool {
105 regex.firstMatch(in: entryText, range: NSRange(location: 0, length: (entryText as NSString).length)) != nil
106 }
107 if !negative.contains(where: found), required.allSatisfy(found) {
108 items.append(format(
109 source, entry, kind: .search, marker: beg, extra: "", dotime: .headline, removing: nil,
110 trailing: "", prefix: options.prefix("search"), urgency: { _ in 1000 }
111 ))
112 at = max(next, matchEnd)
113 } else {
114 at = next
115 }
116 }
117 }
118 return items
119 }
120
121 /// An `org-search-view` string read into regexps, in Emacs syntax and compiled.
122 struct SearchQuery {
123 struct Pattern {
124 let emacs: String
125 let regex: NSRegularExpression?
126 }
127
128 let headlinesOnly: Bool
129 let todoOnly: Bool
130 let boolean: Bool
131 /// Longest first, as Org sorts them.
132 let positive: [Pattern]
133 let negative: [Pattern]
134
135 init(_ string: String, todoOnly: Bool, options: AgendaOptions) {
136 var words = Substring(string)
137 headlinesOnly = words.first == "*"
138 if headlinesOnly { words.removeFirst() }
139 var todo = todoOnly
140 if words.first == "!" {
141 todo = true
142 words.removeFirst()
143 }
144 self.todoOnly = todo
145 var fullWords = options.searchForceFullWords
146 if words.first == ":" {
147 fullWords = true
148 words.removeFirst()
149 }
150 boolean = options.searchAlwaysBoolean || ["-", "+", "{"].contains(words.first)
151
152 // `split-string`, then words ending in `\` joined to the next, and `{…}` kept whole.
153 var pending = words.split(whereSeparator: { " \u{0C}\t\n\r\u{0B}".contains($0) }).map(String.init)
154 var joined: [String] = []
155 while !pending.isEmpty {
156 var w = pending.removeFirst()
157 while w.hasSuffix("\\"), !pending.isEmpty { w = String(w.dropLast()) + " " + pending.removeFirst() }
158 joined.append(w)
159 }
160 pending = joined
161 joined = []
162 while !pending.isEmpty {
163 var w = pending.removeFirst()
164 if w.range(of: "^[-+]?\\{", options: .regularExpression) != nil, !w.hasSuffix("}") {
165 while let next = pending.first, !next.hasSuffix("}") { w += " " + pending.removeFirst() }
166 w += " " + (pending.isEmpty ? "" : pending.removeFirst())
167 }
168 joined.append(w)
169 }
170 if boolean {
171 pending = joined
172 joined = []
173 while !pending.isEmpty {
174 var w = pending.removeFirst()
175 let chars = Array(w)
176 if chars[0] == "\"" || (chars.count > 1 && "+-".contains(chars[0]) && chars[1] == "\"") {
177 while !pending.isEmpty, !w.hasSuffix("\"") { w += " " + pending.removeFirst() }
178 }
179 w = w.replacingOccurrences(of: "^([-+]?)\"", with: "$1", options: .regularExpression)
180 if w.hasSuffix("\"") { w.removeLast() }
181 joined.append(w)
182 }
183 }
184
185 var positive: [String] = []
186 var negative: [String] = []
187 if boolean {
188 for word in joined {
189 var w = Substring(word)
190 let negated = w.first == "-"
191 if negated || w.first == "+" { w = w.dropFirst() }
192 let re: String
193 if w.count >= 2, w.first == "{", w.last == "}" {
194 re = String(w.dropFirst().dropLast())
195 } else {
196 re = fullWords ? "\\<" + Self.quote(w.lowercased()) + "\\>" : Self.quote(w.lowercased())
197 }
198 if negated { negative.insert(re, at: 0) } else { positive.insert(re, at: 0) }
199 }
200 } else {
201 positive = [joined.map(Self.quote).joined(separator: "\\s-+")]
202 }
203 // `sort` is stable; the snippets were pushed, so the last comes first among equals.
204 self.positive = positive.enumerated().sorted { a, b in
205 a.element.count != b.element.count ? a.element.count > b.element.count : a.offset < b.offset
206 }.map { Pattern(emacs: $0.element, regex: Self.compile($0.element)) }
207 self.negative = negative.map { Pattern(emacs: $0, regex: Self.compile($0)) }
208 }
209
210 /// `regexp-quote`.
211 static func quote(_ s: String) -> String {
212 var out = ""
213 for c in s {
214 if "[*.\\?+^$".contains(c) { out.append("\\") }
215 out.append(c)
216 }
217 return out
218 }
219
220 /// Case-insensitive, as Org binds `case-fold-search`, with `^` and `$` at each line.
221 static func compile(_ emacs: String) -> NSRegularExpression? {
222 try? NSRegularExpression(pattern: EmacsRegex.translate(emacs), options: [.caseInsensitive, .anchorsMatchLines])
223 }
224 }
225
47 /// `(urgency-down category-keep)`, stably. 226 /// `(urgency-down category-keep)`, stably.
48 static func sortByUrgency(_ items: [AgendaItem]) -> [AgendaItem] { 227 static func sortByUrgency(_ items: [AgendaItem]) -> [AgendaItem] {
49 items.enumerated().sorted { a, b in 228 items.enumerated().sorted { a, b in
@@ -337,6 +516,12 @@ enum EmacsRegex {
337 case "|": out += "|" 516 case "|": out += "|"
338 case "{": out += "{" 517 case "{": out += "{"
339 case "}": out += "}" 518 case "}": out += "}"
519 case "s" where i + 2 < chars.count && chars[i + 2] == "-",
520 "S" where i + 2 < chars.count && chars[i + 2] == "-":
521 // Whitespace syntax in Org buffers.
522 out += n == "s" ? "[\\t\\n\\f\\r \\u00a0]" : "[^\\t\\n\\f\\r \\u00a0]"
523 i += 3
524 continue
340 case "<", ">": out += "\\b" 525 case "<", ">": out += "\\b"
341 case "`": out += "^" 526 case "`": out += "^"
342 case "'": out += "$" 527 case "'": out += "$"
Sources/OrgCore/Agenda/AgendaSource.swift +6
@@ -94,6 +94,10 @@ public struct AgendaSource: Sendable {
94 let progress: [Progress] 94 let progress: [Progress]
95 /// The file's TODO keywords, active and done. 95 /// The file's TODO keywords, active and done.
96 let keywords: [String] 96 let keywords: [String]
97 /// The active ones (`org-not-done-keywords`).
98 let notDoneKeywords: [String]
99 /// The file's text, for the search view.
100 let text: String
97 let priorities: Priorities 101 let priorities: Priorities
98 102
99 public init(path: String, text: String, defaults: OrgSettings = .default) { 103 public init(path: String, text: String, defaults: OrgSettings = .default) {
@@ -104,6 +108,8 @@ public struct AgendaSource: Sendable {
104 let ns = text as NSString 108 let ns = text as NSString
105 let settings = tree.settings 109 let settings = tree.settings
106 keywords = settings.todoSequences.flatMap { $0.active.map(\.name) + $0.done.map(\.name) } 110 keywords = settings.todoSequences.flatMap { $0.active.map(\.name) + $0.done.map(\.name) }
111 notDoneKeywords = settings.todoSequences.flatMap { $0.active.map(\.name) }
112 self.text = text
107 priorities = settings.priorities 113 priorities = settings.priorities
108 114
109 var stampsByHeading: [Int: [String]] = [:] 115 var stampsByHeading: [Int: [String]] = [:]
Sources/OrgPresentation/AgendaRows.swift +1 −1
@@ -134,7 +134,7 @@ public struct AgendaRow: Sendable, Equatable, Identifiable {
134 case .closed: (icon, untimed) = (.timestamp, "Closed") 134 case .closed: (icon, untimed) = (.timestamp, "Closed")
135 case .clock: (icon, untimed) = (.timestamp, "Clocked") 135 case .clock: (icon, untimed) = (.timestamp, "Clocked")
136 case .state: (icon, untimed) = (.timestamp, "State") 136 case .state: (icon, untimed) = (.timestamp, "State")
137 case .todo, .tagsMatch, .timeGrid, .currentTime: break 137 case .todo, .tagsMatch, .search, .timeGrid, .currentTime: break
138 } 138 }
139 if item.isDone { status = .done } 139 if item.isDone { status = .done }
140 self.id = [item.path ?? "", "\(item.headingOffset ?? -1)", "\(item.markerOffset ?? -1)", item.kind.rawValue, item.text].joined(separator: "\u{1f}") 140 self.id = [item.path ?? "", "\(item.headingOffset ?? -1)", "\(item.markerOffset ?? -1)", item.kind.rawValue, item.text].joined(separator: "\u{1f}")
Sources/Orgstar/AgendaView.swift +12 −3
@@ -16,7 +16,7 @@ enum CalendarAccess {
16 static let events = CalendarEvents() 16 static let events = CalendarEvents()
17} 17}
18 18
19/// The agenda window: the day/week agenda, the TODO list, tag matches and saved views. 19/// The agenda window: the day/week agenda, the TODO list, tag matches, text search and saved views.
20/// Return or a double click jumps to the entry in the main window. Keys as in Emacs's agenda: 20/// Return or a double click jumps to the entry in the main window. Keys as in Emacs's agenda:
21/// n/p (or the arrows) move between lines, f/b/. move the span, j picks a date, v d/v w/v m (or 21/// n/p (or the arrows) move between lines, f/b/. move the span, j picks a date, v d/v w/v m (or
22/// d/w) show a day, week or month, g refreshes, l shows the log (closed and clocked), / filters 22/// d/w) show a day, week or month, g refreshes, l shows the log (closed and clocked), / filters
@@ -80,6 +80,8 @@ struct AgendaView: View {
80 80
81 static let match = "Match…" 81 static let match = "Match…"
82 static let todoMatch = "TODO Match…" 82 static let todoMatch = "TODO Match…"
83 static let search = "Search…"
84 static let todoSearch = "TODO Search…"
83 85
84 init(workspace: WorkspaceModel, session: DocumentSession, commands: AppCommands, scheduler: ReminderScheduler?, clock: ClockModel) { 86 init(workspace: WorkspaceModel, session: DocumentSession, commands: AppCommands, scheduler: ReminderScheduler?, clock: ClockModel) {
85 self.workspace = workspace 87 self.workspace = workspace
@@ -143,6 +145,7 @@ struct AgendaView: View {
143 } 145 }
144 146
145 private var isMatch: Bool { viewName == Self.match || viewName == Self.todoMatch } 147 private var isMatch: Bool { viewName == Self.match || viewName == Self.todoMatch }
148 private var isSearch: Bool { viewName == Self.search || viewName == Self.todoSearch }
146 149
147 @Environment(\.orgTheme) private var theme 150 @Environment(\.orgTheme) private var theme
148 151
@@ -466,12 +469,14 @@ struct AgendaView: View {
466 Divider() 469 Divider()
467 Button(Self.match) { viewName = Self.match } 470 Button(Self.match) { viewName = Self.match }
468 Button(Self.todoMatch) { viewName = Self.todoMatch } 471 Button(Self.todoMatch) { viewName = Self.todoMatch }
472 Button(Self.search) { viewName = Self.search }
473 Button(Self.todoSearch) { viewName = Self.todoSearch }
469 } label: { 474 } label: {
470 Label(viewName, systemImage: "calendar").labelStyle(.titleAndIcon) 475 Label(viewName, systemImage: "calendar").labelStyle(.titleAndIcon)
471 } 476 }
472 .help("What the agenda shows") 477 .help("What the agenda shows")
473 if isMatch { 478 if isMatch || isSearch {
474 TextField("+work-boss|LEVEL>2/!TODO", text: $matchText) 479 TextField(isSearch ? "Phrase or +word -word {regexp}" : "+work-boss|LEVEL>2/!TODO", text: $matchText)
475 .frame(minWidth: 220) 480 .frame(minWidth: 220)
476 .onSubmit { applyView() } 481 .onSubmit { applyView() }
477 } 482 }
@@ -633,6 +638,7 @@ struct AgendaView: View {
633 switch agenda.mode { 638 switch agenda.mode {
634 case .todo(let keywords): "TODO items: \(keywords ?? "all")" 639 case .todo(let keywords): "TODO items: \(keywords ?? "all")"
635 case .match(let match, let todoOnly): (todoOnly ? "TODO entries" : "Entries") + " matching \(match)" 640 case .match(let match, let todoOnly): (todoOnly ? "TODO entries" : "Entries") + " matching \(match)"
641 case .search(let string, let todoOnly): (todoOnly ? "TODO entries" : "Entries") + " containing \(string)"
636 case .agenda: "" 642 case .agenda: ""
637 } 643 }
638 } 644 }
@@ -667,6 +673,9 @@ struct AgendaView: View {
667 let match = matchText.trimmingCharacters(in: .whitespaces) 673 let match = matchText.trimmingCharacters(in: .whitespaces)
668 guard !match.isEmpty else { return } 674 guard !match.isEmpty else { return }
669 agenda.mode = .match(match, todoOnly: viewName == Self.todoMatch) 675 agenda.mode = .match(match, todoOnly: viewName == Self.todoMatch)
676 case Self.search, Self.todoSearch:
677 guard matchText.contains(where: { !$0.isWhitespace }) else { return }
678 agenda.mode = .search(matchText, todoOnly: viewName == Self.todoSearch)
670 default: 679 default:
671 if let view = views.first(where: { $0.name == viewName }) { agenda.mode = view.mode } 680 if let view = views.first(where: { $0.name == viewName }) { agenda.mode = view.mode }
672 } 681 }
Sources/OrgstarMobile/AgendaScreen.swift +21 −11
@@ -6,8 +6,8 @@ import OrgPresentation
6import SwiftUI 6import SwiftUI
7 7
8/// The agenda over the workspace's agenda files: the day/week view, the TODO list, tag and 8/// The agenda over the workspace's agenda files: the day/week view, the TODO list, tag and
9/// property matches and the views in `views.toml`, with Org's filters and the agenda's 9/// property matches, text search and the views in `views.toml`, with Org's filters and the
10/// actions on each entry. 10/// agenda's actions on each entry.
11struct AgendaScreen: View { 11struct AgendaScreen: View {
12 let workspace: WorkspaceModel 12 let workspace: WorkspaceModel
13 let session: DocumentSession 13 let session: DocumentSession
@@ -39,10 +39,10 @@ struct AgendaScreen: View {
39 @State private var done = 0 39 @State private var done = 0
40 @State private var moved = 0 40 @State private var moved = 0
41 41
42 /// A match typed for the tag and property view. 42 /// A match typed for the tag and property view, or a search string.
43 private enum Asking: Identifiable { 43 private enum Asking: Identifiable {
44 case match 44 case match, search
45 var id: Int { 0 } 45 var id: Self { self }
46 } 46 }
47 47
48 enum SpanRange: String, CaseIterable, Identifiable { 48 enum SpanRange: String, CaseIterable, Identifiable {
@@ -119,6 +119,7 @@ struct AgendaScreen: View {
119 Button(definition.name) { show(definition) } 119 Button(definition.name) { show(definition) }
120 } 120 }
121 Button("Tags and Properties…") { asking = .match } 121 Button("Tags and Properties…") { asking = .match }
122 Button("Search…") { asking = .search }
122 } label: { 123 } label: {
123 Label("Views", systemImage: "list.bullet.rectangle") 124 Label("Views", systemImage: "list.bullet.rectangle")
124 } 125 }
@@ -139,12 +140,21 @@ struct AgendaScreen: View {
139 if let picked, let item = row.item { reschedule(row, item, to: Days.absolute(picked, calendar: .current)) } 140 if let picked, let item = row.item { reschedule(row, item, to: Days.absolute(picked, calendar: .current)) }
140 } 141 }
141 } 142 }
142 .sheet(item: $asking) { _ in 143 .sheet(item: $asking) { kind in
143 PromptSheet(prompt: Prompt(key: "match", message: "Match: tags and properties, as +work-home or TODO=\"WAIT\"")) { answer in 144 if kind == .search {
144 asking = nil 145 PromptSheet(prompt: Prompt(key: "search", message: "Search: a phrase, or +word -word {regexp}; * for headlines, ! for TODO entries")) { answer in
145 guard let answer, !answer.isEmpty else { return } 146 asking = nil
146 agenda.mode = .match(answer, todoOnly: false) 147 guard let answer, answer.contains(where: { !$0.isWhitespace }) else { return }
147 viewName = answer 148 agenda.mode = .search(answer, todoOnly: false)
149 viewName = answer
150 }
151 } else {
152 PromptSheet(prompt: Prompt(key: "match", message: "Match: tags and properties, as +work-home or TODO=\"WAIT\"")) { answer in
153 asking = nil
154 guard let answer, !answer.isEmpty else { return }
155 agenda.mode = .match(answer, todoOnly: false)
156 viewName = answer
157 }
148 } 158 }
149 } 159 }
150 .sheet(item: $prompt) { request in 160 .sheet(item: $prompt) { request in
Tests/OrgCoreTests/AgendaSearchTests.swift added +115
@@ -0,0 +1,115 @@
1import Foundation
2import Testing
3@testable import OrgCore
4
5/// `org-search-view` against Emacs.
6struct AgendaSearchTests {
7 static let first = """
8 #+TODO: TODO NEXT | DONE
9 Rent before the first heading.
10 * TODO [#A] Pay rent :home:
11 SCHEDULED: <2026-10-01 Thu>
12 Ask the landlord about the boiler.
13 * Meeting 10:00 with Bob
14 Notes about the end of
15 the line, and Bob's plan.
16 * DONE Paid the rent
17 * NEXT Report :work:
18 ** Section with alpha and beta
19 Alpha only here, and a boilerplate.
20 ** Section with alpha
21 Gamma.
22 * todo list of chores
23 Water the plants, alpha.
24 * Archived :ARCHIVE:
25 The rent is here too.
26 ** TODO Under archived rent
27 * COMMENT Commented rent
28 * Two words
29 The phrase two words
30 spans here; foo_bar and foo-bar.
31 * The last
32 word ends this entry with last
33 * word starts the next heading
34 * Ünïcode tïtle :tag:
35 Straße and STRASSE.
36 """
37
38 static let second = """
39 #+CATEGORY: other
40 * WAIT Second file rent
41 * Plans
42 alpha, beta, gamma in the body.
43 ** HOLD Nested beta
44 * Headline alpha
45 """
46
47 static let strings = [
48 // Phrases.
49 "rent", "RENT", "pay rent", "end of the line", "end of\nthe", "two words", "last word",
50 // A match ending in the next heading's line selects that entry.
51 "last * word","Bob's", "boiler",
52 "straße", "foo_bar", "10:00", "nothing at all", "alpha, beta",
53 // Boolean.
54 "+alpha", "+alpha -beta", "+alpha +gamma", "-alpha", "+beta -nested", "{alph?a}", "+{^Gam}", "-{b[eo]}",
55 "+{\\<rent\\>}", "+\"two words\"", "+\"two words\"", "+\"the line\"", "{boiler\\b}", "+alpha +\"in the body\"",
56 "+{foo\\s-bar}", "+boiler -plate", "+{a b}", "+{alpha} -{x y}",
57 // Headlines only.
58 "*rent", "*alpha", "*+alpha -beta", "*+section", "*-alpha", "*pay rent", "*with alpha and",
59 // TODO only and whole words.
60 "!rent", "!+alpha", "*!section", ":+boiler", ":+alpha", "+:boiler", ":boiler", "*!:+rent",
61 ]
62
63 static func lisp(_ s: String) -> String {
64 "\"" + s.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"").replacingOccurrences(of: "\n", with: "\\n") + "\""
65 }
66
67 static let files = [("a.org", first), ("b.org", second)]
68 static let today = Days.absolute(year: 2026, month: 10, day: 5)
69
70 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
71 func searchMatchesEmacs() throws {
72 for string in Self.strings {
73 for todoOnly in [false, true] {
74 try AgendaOracle.compareList(files: Self.files, form: "(org-search-view \(todoOnly ? "t" : "nil") \(Self.lisp(string)))", today: Self.today) {
75 Agenda.textSearch($0, string: string, todoOnly: todoOnly)
76 }
77 }
78 }
79 }
80
81 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
82 func searchOptionsMatchEmacs() throws {
83 for string in ["boiler", "+boiler", "alpha beta", "+alpha -beta", "{^Gam}", "two words", "*rent", "rent -paid", "+\"two words\""] {
84 for (boolean, full) in [(true, false), (false, true), (true, true)] {
85 var options = AgendaOptions()
86 options.searchAlwaysBoolean = boolean
87 options.searchForceFullWords = full
88 let form = "(let ((org-agenda-search-view-always-boolean \(boolean ? "t" : "nil")) (org-agenda-search-view-force-full-words \(full ? "t" : "nil"))) (org-search-view nil \(Self.lisp(string))))"
89 try AgendaOracle.compareList(files: Self.files, form: form, today: Self.today) {
90 Agenda.textSearch($0, string: string, options: options)
91 }
92 }
93 }
94 }
95
96 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
97 func searchPrefixMatchesEmacs() throws {
98 let formats = ["search": "%c %?-5:l %b", "agenda": " %i %-12:c%?-12t% s", "todo": " %i %-12:c", "tags": " %i %-12:c"]
99 var options = AgendaOptions()
100 options.prefixFormat = formats
101 let form = "(let ((org-agenda-prefix-format \(AgendaOracle.prefixLisp(formats)))) (org-search-view nil \"alpha\"))"
102 try AgendaOracle.compareList(files: Self.files, form: form, today: Self.today) {
103 Agenda.textSearch($0, string: "alpha", options: options)
104 }
105 }
106
107 @Test func parsesSnippets() {
108 let query = Agenda.SearchQuery("*!:+alpha -\"two words\" {a b} back\\ slash", todoOnly: false, options: AgendaOptions())
109 #expect(query.headlinesOnly && query.todoOnly && query.boolean)
110 #expect(query.positive.map(\.emacs) == ["\\<back slash\\>","\\<alpha\\>", "a b"])
111 #expect(query.negative.map(\.emacs) == ["\\<two words\\>"])
112 let phrase = Agenda.SearchQuery("pay rent.", todoOnly: false, options: AgendaOptions())
113 #expect(!phrase.boolean && phrase.positive.map(\.emacs) == ["pay\\s-+rent\\."])
114 }
115}
docs/manual/guide/03-keys.org +2
@@ -689,6 +689,8 @@ The Agenda window has its own keys, modelled on =org-agenda-mode=, which =keymap
689| =C-c C-w= | Refile. | 689| =C-c C-w= | Refile. |
690| =C-c $=, =C-c C-x C-s= | Archive. | 690| =C-c $=, =C-c C-x C-s= | Archive. |
691 691
692=s= saves, as in =org-agenda-mode=. The text search that =s= starts from Org's agenda dispatcher is Search… in the view menu, and =S= is TODO Search….
693
692See [[file:07-agenda.org][Agenda]] for what these do. In the Capture window, typing a template's key picks it, =⌘↩= files the entry and =Esc= cancels; see [[file:08-capture.org][Capture]]. 694See [[file:07-agenda.org][Agenda]] for what these do. In the Capture window, typing a template's key picks it, =⌘↩= files the entry and =Esc= cancels; see [[file:08-capture.org][Capture]].
693 695
694* iOS and iPadOS 696* iOS and iPadOS
docs/manual/guide/07-agenda.org +35 −6
@@ -339,11 +339,35 @@ After the last =/= (one not followed by a quote), the match restricts TODO keywo
339 339
340* Text search 340* Text search
341 341
342The agenda has no =org-search-view= (=s=). To search the text of your files, use Search Notes (=⇧⌘F=). 342Choose Search… from the view menu to list entries containing some text, as =org-search-view= (=s=) does, or TODO Search… to list only entries with an open TODO keyword (=S=, or =C-u s=). Type the search string in the toolbar field and press Return. An entry is its heading and the text up to the next heading; text before the first heading of a file is not searched. Entries show in file order, with the rows of the TODO list.
343
344By default the string is a phrase: =pay rent= finds those words in that order, case-insensitively, and each space matches any run of spaces, tabs and line breaks, so a phrase can span lines. A string that starts with =+=, =-= or ={= is a list of snippets separated by spaces instead, all of which must hold:
345
346| Snippet | Entries that |
347|------------------+-----------------------------------------------------------------|
348| =+word=, =word= | contain the text |
349| =-word= | don't contain it |
350| ={regexp}= | match the Emacs regular expression, without case |
351| =-{regexp}= | don't match it |
352| ="two words"= | contain the words with one space between them |
353
354Phrases and snippets match parts of words: =+rent= finds =parent=. These may come first in the string, in this order:
355
356| Prefix | Effect |
357|--------+---------------------------------------------------------------------|
358| =*= | Search headlines only, not the text below them |
359| =!= | Only entries with an open TODO keyword, as TODO Search… |
360| =:= | Snippets match whole words (regexps are unchanged) |
361
362=*!:+rent -paid= lists open TODO headlines with the word =rent= and without =paid=.
363
364Two settings in =config.toml= change the defaults: =org-agenda-search-view-always-boolean= reads every string as snippets, and =org-agenda-search-view-force-full-words= makes snippets match whole words (see [[file:13-configuration.org][Configuration]]).
365
366The search covers the agenda files only, not =org-agenda-text-search-extra-files= or archive files. Entries in archived or commented trees are left out, as in the other views. To search every file in the folders, use Search Notes (=⇧⌘F=).
343 367
344* Views 368* Views
345 369
346The view menu lists the built-in views, Agenda and TODO List, then the views in =views.toml=, then Match… and TODO Match…. Saved views mirror =org-agenda-custom-commands=, with fewer options. 370The view menu lists the built-in views, Agenda and TODO List, then the views in =views.toml=, then Match…, TODO Match…, Search… and TODO Search…. Saved views mirror =org-agenda-custom-commands=, with fewer options.
347 371
348=views.toml= lives in the configuration folder beside =config.toml= (=~/.config/orgstar/= by default; see [[file:13-configuration.org][Configuration]]). Each view is a =[[view]]= table: 372=views.toml= lives in the configuration folder beside =config.toml= (=~/.config/orgstar/= by default; see [[file:13-configuration.org][Configuration]]). Each view is a =[[view]]= table:
349 373
@@ -368,20 +392,25 @@ match = "+work-boss"
368name = "Next at work" 392name = "Next at work"
369type = "todo-match" 393type = "todo-match"
370match = "+work/NEXT" 394match = "+work/NEXT"
395
396[[view]]
397name = "Invoices"
398type = "search"
399match = "+invoice -paid"
371#+END_SRC 400#+END_SRC
372 401
373| Key | Applies to | Meaning | 402| Key | Applies to | Meaning |
374|------------+--------------------+----------------------------------------------------------------------------| 403|------------+--------------------+----------------------------------------------------------------------------|
375| =name= | all, required | The name in the view menu | 404| =name= | all, required | The name in the view menu |
376| =type= | all | =agenda= (the default), =todo=, =match= or =todo-match= | 405| =type= | all | =agenda= (the default), =todo=, =match=, =todo-match=, =search= or =todo-search= |
377| =span= | =agenda= | Days shown; the setting when omitted | 406| =span= | =agenda= | Days shown; the setting when omitted |
378| =start= | =agenda= | First day as an integer number of days from today (=-3=, =0=); the setting when omitted | 407| =start= | =agenda= | First day as an integer number of days from today (=-3=, =0=); the setting when omitted |
379| =keywords= | =todo= | Keywords separated by the vertical bar; all open TODOs when omitted | 408| =keywords= | =todo= | Keywords separated by the vertical bar; all open TODOs when omitted |
380| =match= | =match=, =todo-match=, required | A match string as in [[*Tag and property matches][Tag and property matches]]; =todo-match= keeps only open TODO entries | 409| =match= | =match=, =todo-match=, =search=, =todo-search=, required | A match string as in [[*Tag and property matches][Tag and property matches]], or for =search= a search string as in [[*Text search][Text search]]; the =todo-= types keep only open TODO entries |
381 410
382Note that =start= is a plain integer here, while =org-agenda-start-day= in =config.toml= is a string such as ="-3d"=. 411Note that =start= is a plain integer here, while =org-agenda-start-day= in =config.toml= is a string such as ="-3d"=.
383 412
384Not supported: block agendas (several views in one), per-view settings such as =org-agenda-skip-function= or a different prefix format, =stuck= projects, and search views. The views load when the window opens and again on =g= or =r=. A problem in the file, such as a missing =name= or an unknown =type=, shows in the status line at the bottom of the window, and the remaining views still load. 413Not supported: block agendas (several views in one), per-view settings such as =org-agenda-skip-function= or a different prefix format, and =stuck= projects. The views load when the window opens and again on =g= or =r=. A problem in the file, such as a missing =name= or an unknown =type=, shows in the status line at the bottom of the window, and the remaining views still load.
385 414
386* Filters 415* Filters
387 416
@@ -529,7 +558,7 @@ The board is not available on iOS.
529 558
530The Agenda tab shows the same views from the same files, using the settings in the synced =config.toml= and the views in =views.toml= (see [[file:14-ios.org][iOS]]). Differences from the Mac: 559The Agenda tab shows the same views from the same files, using the settings in the synced =config.toml= and the views in =views.toml= (see [[file:14-ios.org][iOS]]). Differences from the Mac:
531 560
532- The Views menu, at the top left, lists the built-in views, your saved views, and Tags and Properties…, which asks for a match string. There is no TODO-only match from the menu; use =/!= in the match or a =todo-match= view. 561- The Views menu, at the top left, lists the built-in views, your saved views, Tags and Properties…, which asks for a match string, and Search…, which asks for a search string. There is no TODO-only match or search from the menu; use =/!= in the match, =!= at the start of the search string, or a =todo-match= or =todo-search= view.
533- A bar floats at the bottom of the screen with Earlier, Filter, Today, Go to Date, the span and Later. The span menu switches between Day, Week, Month and the configured span (=10 Days= by default). Weeks start on Monday, as on the Mac. In the TODO list and match views the bar has Filter and Agenda, which returns to the day view. Messages show above the bar; tap one to dismiss it. 562- A bar floats at the bottom of the screen with Earlier, Filter, Today, Go to Date, the span and Later. The span menu switches between Day, Week, Month and the configured span (=10 Days= by default). Weeks start on Monday, as on the Mac. In the TODO list and match views the bar has Filter and Agenda, which returns to the day view. Messages show above the bar; tap one to dismiss it.
534- The title names the days shown: =Thu 8 Oct=, =Week of 5 Oct=, =October 2026=, or =5 Oct – 14 Oct=. 563- The title names the days shown: =Thu 8 Oct=, =Week of 5 Oct=, =October 2026=, or =5 Oct – 14 Oct=.
535- Day headers read =Today · Thu 8 Oct=, =Tomorrow · Fri 9 Oct= or =Mon 12 Oct=, with a + that captures onto that day. Days before today are grey. 564- Day headers read =Today · Thu 8 Oct=, =Tomorrow · Fri 9 Oct= or =Mon 12 Oct=, with a + that captures onto that day. Days before today are grey.
docs/manual/guide/13-configuration.org +4
@@ -152,6 +152,8 @@ These settings have no control in the Settings window. Some are in the View menu
152| =ignored-folders= | | 152| =ignored-folders= | |
153| =org-agenda-skip-scheduled-if-done= | | 153| =org-agenda-skip-scheduled-if-done= | |
154| =org-agenda-skip-deadline-if-done= | | 154| =org-agenda-skip-deadline-if-done= | |
155| =org-agenda-search-view-always-boolean= | |
156| =org-agenda-search-view-force-full-words= | |
155| =[org-agenda-prefix-format]= | | 157| =[org-agenda-prefix-format]= | |
156| =theme-file= | | 158| =theme-file= | |
157 159
@@ -193,6 +195,8 @@ A key you remove from the file goes back to its default. A key that isn't in the
193| =org-agenda-show-all-dates= | boolean | =false= | Show days without entries in the agenda; today always shows. Emacs's default is =t=. Mirrors =org-agenda-show-all-dates=. | 195| =org-agenda-show-all-dates= | boolean | =false= | Show days without entries in the agenda; today always shows. Emacs's default is =t=. Mirrors =org-agenda-show-all-dates=. |
194| =org-agenda-skip-scheduled-if-done= | boolean | =false= | Leave a done entry's scheduled date out of the agenda, even on its own day. Mirrors =org-agenda-skip-scheduled-if-done=. | 196| =org-agenda-skip-scheduled-if-done= | boolean | =false= | Leave a done entry's scheduled date out of the agenda, even on its own day. Mirrors =org-agenda-skip-scheduled-if-done=. |
195| =org-agenda-skip-deadline-if-done= | boolean | =false= | Leave a done entry's deadline out of the agenda, even on its own day. Mirrors =org-agenda-skip-deadline-if-done=. | 197| =org-agenda-skip-deadline-if-done= | boolean | =false= | Leave a done entry's deadline out of the agenda, even on its own day. Mirrors =org-agenda-skip-deadline-if-done=. |
198| =org-agenda-search-view-always-boolean= | boolean | =false= | The agenda's text search reads every string as =+word -word {regexp}= snippets, never as a phrase. Mirrors =org-agenda-search-view-always-boolean=. |
199| =org-agenda-search-view-force-full-words= | boolean | =false= | Text search snippets match whole words only, as a leading =:= does. Mirrors =org-agenda-search-view-force-full-words=. |
196| =appt-message-warning-time= | integer | =12= | Minutes of warning before timed entries. An entry's =APPT_WARNTIME= property overrides it. Mirrors =appt-message-warning-time=. | 200| =appt-message-warning-time= | integer | =12= | Minutes of warning before timed entries. An entry's =APPT_WARNTIME= property overrides it. Mirrors =appt-message-warning-time=. |
197| =org-clock-idle-time= | integer | =0= | Minutes without keyboard or mouse input, with a clock running, before Orgstar asks what to do with the idle time. =0= never asks (Emacs's =nil=). Mac only. Mirrors =org-clock-idle-time=. | 201| =org-clock-idle-time= | integer | =0= | Minutes without keyboard or mouse input, with a clock running, before Orgstar asks what to do with the idle time. =0= never asks (Emacs's =nil=). Mac only. Mirrors =org-clock-idle-time=. |
198| =org-clock-history-length= | integer | =5= | How many recently clocked entries Orgstar remembers. Mirrors =org-clock-history-length=. | 202| =org-clock-history-length= | integer | =5= | How many recently clocked entries Orgstar remembers. Mirrors =org-clock-history-length=. |
docs/manual/guide/14-ios.org +2 −2
@@ -154,7 +154,7 @@ Tap a result to open it in the reader. A heading result opens at that heading.
154 154
155The Agenda tab shows the agenda over all your folders, with the span and start day from the settings (10 days from 3 days ago by default). Each day lists its entries in rows: the time or a status such as =Due today= or =3d late= on the left, the category and the keyword above the title. Days without entries are hidden, except today. Today's overdue entries are in a group of their own, and a line marks the current time between timed entries. Pull down to refresh. 155The Agenda tab shows the agenda over all your folders, with the span and start day from the settings (10 days from 3 days ago by default). Each day lists its entries in rows: the time or a status such as =Due today= or =3d late= on the left, the category and the keyword above the title. Days without entries are hidden, except today. Today's overdue entries are in a group of their own, and a line marks the current time between timed entries. Pull down to refresh.
156 156
157- Views (top left) :: the built-in views and those in =views.toml=, and Tags and Properties… (a match such as =+work-home= or ~TODO="WAIT"~). 157- Views (top left) :: the built-in views and those in =views.toml=, Tags and Properties… (a match such as =+work-home= or ~TODO="WAIT"~), and Search… (a phrase, or snippets such as =+invoice -paid=).
158- The bar at the bottom :: Earlier, Filter, Today, Go to Date, the span (Day, Week, Month or the configured span) and Later. 158- The bar at the bottom :: Earlier, Filter, Today, Go to Date, the span (Day, Week, Month or the configured span) and Later.
159- Filter :: keep or leave out tags and categories of the entries shown, or type a filter as Org's =/= takes it: =+keep= and =-drop= tags or categories, =<0:30= for effort, =/regexp/=. The filter in use shows above the bar. 159- Filter :: keep or leave out tags and categories of the entries shown, or type a filter as Org's =/= takes it: =+keep= and =-drop= tags or categories, =<0:30= for effort, =/regexp/=. The filter in use shows above the bar.
160- + on a day's header :: captures an entry scheduled on that day (see [[file:08-capture.org][Capture]]). 160- + on a day's header :: captures an entry scheduled on that day (see [[file:08-capture.org][Capture]]).
@@ -331,7 +331,7 @@ These settings from =config.toml= apply on iOS:
331- =org-log-done=, =org-log-reschedule=, =org-log-redeadline=, =org-log-into-drawer= 331- =org-log-done=, =org-log-reschedule=, =org-log-redeadline=, =org-log-into-drawer=
332- =org-use-speed-commands=, with a hardware keyboard 332- =org-use-speed-commands=, with a hardware keyboard
333- =electric-pair-mode=, =spell-check= 333- =electric-pair-mode=, =spell-check=
334- =org-agenda-span=, =org-agenda-start-day=, =agenda-include-subfolders=, =org-agenda-show-all-dates=, =org-agenda-skip-scheduled-if-done=, =org-agenda-skip-deadline-if-done= 334- =org-agenda-span=, =org-agenda-start-day=, =agenda-include-subfolders=, =org-agenda-show-all-dates=, =org-agenda-skip-scheduled-if-done=, =org-agenda-skip-deadline-if-done=, =org-agenda-search-view-always-boolean=, =org-agenda-search-view-force-full-words=
335- =calendar-events=, =calendar-event-calendars= 335- =calendar-events=, =calendar-event-calendars=
336- =reminders=, =appt-message-warning-time= 336- =reminders=, =appt-message-warning-time=
337- =org-clock-history-length= 337- =org-clock-history-length=
docs/manual/guide/15-alongside-emacs.org +1
@@ -246,3 +246,4 @@ Other:
246- =#+SETUPFILE= URLs aren't fetched. 246- =#+SETUPFILE= URLs aren't fetched.
247- Most =#+STARTUP= options for footnotes, entities, LaTeX previews, numbering and odd levels are accepted and ignored; see [[file:13-configuration.org][Configuration]]. 247- Most =#+STARTUP= options for footnotes, entities, LaTeX previews, numbering and odd levels are accepted and ignored; see [[file:13-configuration.org][Configuration]].
248- =display-line-numbers-type= has no relative or visual mode. 248- =display-line-numbers-type= has no relative or visual mode.
249- The agenda's text search (see [[file:07-agenda.org][The agenda]]) covers the agenda files only: =org-agenda-text-search-extra-files= and =org-agenda-search-view-max-outline-level= aren't supported. It runs from the view menu rather than the dispatcher's =s= and =S=, and the search view's =[=, =]=, ={= and =}= keys, which add words and regexps to the search, aren't supported.