krz/orgstar

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

Commit 191ddb506f

191ddb506f3a91b93c755ba6e8123a4e8ac4f90d

parent: 1d6a2cda53

Verified · cmc

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

Enforce TODO dependencies and dim blocked agenda entries

org-enforce-todo-dependencies (children, ORDERED siblings, blocked
ancestors), org-enforce-todo-checkbox-dependencies, NOBLOCKING, and
org-toggle-ordered-property on C-c C-x o. Every change to a done
keyword is refused with Org's message. org-agenda-dim-blocked-tasks
dims or hides blocked entries on Mac and iOS. Settings in config.toml
and the Mac Settings window; oracle tests against Org 9.8.7.

Layout: unified · split

Sources/OrgApp/EmacsImport.swift +4 −1
@@ -476,8 +476,11 @@ public struct EmacsImport: Sendable {
476 add(name, .integer(n)) 476 add(name, .integer(n))
477 case "org-insert-heading-respect-content", "org-list-allow-alphabetical", "org-hide-emphasis-markers", "org-pretty-entities", 477 case "org-insert-heading-respect-content", "org-list-allow-alphabetical", "org-hide-emphasis-markers", "org-pretty-entities",
478 "org-cycle-hide-drawer-startup", "org-cycle-hide-block-startup", "org-use-speed-commands", "org-startup-with-inline-images", 478 "org-cycle-hide-drawer-startup", "org-cycle-hide-block-startup", "org-use-speed-commands", "org-startup-with-inline-images",
479 "org-startup-indented", "org-hide-leading-stars", "org-startup-align-all-tables", "org-startup-truncated": 479 "org-startup-indented", "org-hide-leading-stars", "org-startup-align-all-tables", "org-startup-truncated",
480 "org-enforce-todo-dependencies", "org-enforce-todo-checkbox-dependencies":
480 add(name, .bool(truthy)) 481 add(name, .bool(truthy))
482 case "org-agenda-dim-blocked-tasks":
483 add(name, .string(value.symbol == "invisible" ? "invisible" : truthy ? "t" : "nil"))
481 case "org-hide-drawer-startup", "org-hide-block-startup": 484 case "org-hide-drawer-startup", "org-hide-block-startup":
482 add(name == "org-hide-drawer-startup" ? "org-cycle-hide-drawer-startup" : "org-cycle-hide-block-startup", .bool(truthy)) 485 add(name == "org-hide-drawer-startup" ? "org-cycle-hide-drawer-startup" : "org-cycle-hide-block-startup", .bool(truthy))
483 case "org-M-RET-may-split-line": 486 case "org-M-RET-may-split-line":
Sources/OrgApp/OrgPreferences.swift +6 −1
@@ -31,6 +31,8 @@ public enum OrgPreferences {
31 spellCheck: bool("spellCheck", defaults) 31 spellCheck: bool("spellCheck", defaults)
32 ) 32 )
33 options.electricPair = bool("electricPair", defaults) 33 options.electricPair = bool("electricPair", defaults)
34 options.enforceTodoDependencies = bool("enforceTodoDependencies", defaults)
35 options.enforceTodoCheckboxDependencies = bool("enforceTodoCheckboxDependencies", defaults)
34 return options 36 return options
35 } 37 }
36 38
@@ -56,13 +58,16 @@ public enum OrgPreferences {
56 public static func agendaSkipDeadlineIfDone(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSkipDeadlineIfDone", defaults) } 58 public static func agendaSkipDeadlineIfDone(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSkipDeadlineIfDone", defaults) }
57 public static func agendaSearchAlwaysBoolean(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSearchAlwaysBoolean", defaults) } 59 public static func agendaSearchAlwaysBoolean(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSearchAlwaysBoolean", defaults) }
58 public static func agendaSearchForceFullWords(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSearchForceFullWords", defaults) } 60 public static func agendaSearchForceFullWords(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSearchForceFullWords", defaults) }
59 /// `options` with the skip-if-done and search settings. 61 /// `options` with the skip-if-done, search and blocked-entry settings.
60 public static func agendaOptions(_ options: AgendaOptions, _ defaults: UserDefaults = .standard) -> AgendaOptions { 62 public static func agendaOptions(_ options: AgendaOptions, _ defaults: UserDefaults = .standard) -> AgendaOptions {
61 var options = options 63 var options = options
62 options.skipScheduledIfDone = agendaSkipScheduledIfDone(defaults) 64 options.skipScheduledIfDone = agendaSkipScheduledIfDone(defaults)
63 options.skipDeadlineIfDone = agendaSkipDeadlineIfDone(defaults) 65 options.skipDeadlineIfDone = agendaSkipDeadlineIfDone(defaults)
64 options.searchAlwaysBoolean = agendaSearchAlwaysBoolean(defaults) 66 options.searchAlwaysBoolean = agendaSearchAlwaysBoolean(defaults)
65 options.searchForceFullWords = agendaSearchForceFullWords(defaults) 67 options.searchForceFullWords = agendaSearchForceFullWords(defaults)
68 options.enforceTodoDependencies = bool("enforceTodoDependencies", defaults)
69 options.enforceTodoCheckboxDependencies = bool("enforceTodoCheckboxDependencies", defaults)
70 options.dimBlockedTasks = AgendaOptions.DimBlocked(rawValue: string("agendaDimBlockedTasks", defaults)) ?? .dim
66 return options 71 return options
67 } 72 }
68 /// `calendar-events`: Calendar's events show in the agenda. 73 /// `calendar-events`: Calendar's events show in the agenda.
Sources/OrgApp/UserConfig.swift +7
@@ -115,6 +115,10 @@ public enum UserConfig {
115 fallback: .string("nil"), comment: "nil, time or note: log changing or removing a DEADLINE", legacy: ""), 115 fallback: .string("nil"), comment: "nil, time or note: log changing or removing a DEADLINE", legacy: ""),
116 Setting(section: "", key: "org-log-into-drawer", defaultsKey: "logIntoDrawer", kind: .string, fallback: .string(""), 116 Setting(section: "", key: "org-log-into-drawer", defaultsKey: "logIntoDrawer", kind: .string, fallback: .string(""),
117 comment: "the drawer state notes go in (\"LOGBOOK\" is Emacs's t); \"\" for none", legacy: ""), 117 comment: "the drawer state notes go in (\"LOGBOOK\" is Emacs's t); \"\" for none", legacy: ""),
118 Setting(section: "", key: "org-enforce-todo-dependencies", defaultsKey: "enforceTodoDependencies", kind: .bool, fallback: .bool(false),
119 comment: "an entry can't be done before its TODO children, or under an ORDERED parent before its earlier siblings", legacy: ""),
120 Setting(section: "", key: "org-enforce-todo-checkbox-dependencies", defaultsKey: "enforceTodoCheckboxDependencies", kind: .bool, fallback: .bool(false),
121 comment: "an entry can't be done while it has unchecked boxes", legacy: ""),
118 Setting(section: "", key: "org-startup-indented", defaultsKey: "startupIndented", kind: .bool, fallback: .bool(true), 122 Setting(section: "", key: "org-startup-indented", defaultsKey: "startupIndented", kind: .bool, fallback: .bool(true),
119 comment: "indent bodies under their headings (org-indent-mode); #+STARTUP: (no)indent overrides", legacy: ""), 123 comment: "indent bodies under their headings (org-indent-mode); #+STARTUP: (no)indent overrides", legacy: ""),
120 Setting(section: "", key: "org-hide-leading-stars", defaultsKey: "hideLeadingStars", kind: .bool, fallback: .bool(false), 124 Setting(section: "", key: "org-hide-leading-stars", defaultsKey: "hideLeadingStars", kind: .bool, fallback: .bool(false),
@@ -151,6 +155,9 @@ public enum UserConfig {
151 comment: "search strings are always +word -word {regexp} snippets, never a phrase", legacy: ""), 155 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), 156 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: ""), 157 comment: "search snippets match whole words only", legacy: ""),
158 Setting(section: "", key: "org-agenda-dim-blocked-tasks", defaultsKey: "agendaDimBlockedTasks",
159 kind: .choice([("t", "t"), ("nil", "nil"), ("invisible", "invisible")]), fallback: .string("t"),
160 comment: "t dims entries the enforce settings block, invisible hides them (unless only checkboxes block them), nil shows them as usual", legacy: ""),
154 Setting(section: "", key: "appt-message-warning-time", defaultsKey: "reminderLead", kind: .integer, fallback: .integer(12), 161 Setting(section: "", key: "appt-message-warning-time", defaultsKey: "reminderLead", kind: .integer, fallback: .integer(12),
155 comment: "minutes of warning before timed entries; APPT_WARNTIME overrides", legacy: "agenda.reminder-lead"), 162 comment: "minutes of warning before timed entries; APPT_WARNTIME overrides", legacy: "agenda.reminder-lead"),
156 Setting(section: "", key: "org-clock-idle-time", defaultsKey: "clockIdleTime", kind: .integer, fallback: .integer(0), 163 Setting(section: "", key: "org-clock-idle-time", defaultsKey: "clockIdleTime", kind: .integer, fallback: .integer(0),
Sources/OrgCore/Agenda/Agenda.swift +33 −2
@@ -52,6 +52,10 @@ public struct AgendaItem: Sendable, Equatable {
52 /// Deadlines and scheduled items: the day (as `Days` numbers it) of the timestamp the item 52 /// Deadlines and scheduled items: the day (as `Days` numbers it) of the timestamp the item
53 /// is for, the repeater's occurrence when the item shows on it. 53 /// is for, the repeater's occurrence when the item shows on it.
54 public var planningDay: Int? 54 public var planningDay: Int?
55 /// Blocked from becoming done, for `org-agenda-dim-blocked-tasks`.
56 public var isBlocked = false
57 var blockedByCheckboxes = false
58 var blockedByHierarchy = false
55} 59}
56 60
57public struct AgendaDay: Sendable, Equatable { 61public struct AgendaDay: Sendable, Equatable {
@@ -84,6 +88,16 @@ public struct AgendaOptions: Sendable, Equatable {
84 public var searchAlwaysBoolean = false 88 public var searchAlwaysBoolean = false
85 /// `org-agenda-search-view-force-full-words`: search snippets match whole words only. 89 /// `org-agenda-search-view-force-full-words`: search snippets match whole words only.
86 public var searchForceFullWords = false 90 public var searchForceFullWords = false
91 /// `org-enforce-todo-dependencies` and `org-enforce-todo-checkbox-dependencies`, which
92 /// decide the entries `dimBlockedTasks` applies to.
93 public var enforceTodoDependencies = false
94 public var enforceTodoCheckboxDependencies = false
95 /// `org-agenda-dim-blocked-tasks`.
96 public var dimBlockedTasks = DimBlocked.dim
97
98 public enum DimBlocked: String, Sendable {
99 case off = "nil", dim = "t", invisible
100 }
87 101
88 public init() {} 102 public init() {}
89 103
@@ -114,7 +128,7 @@ public enum Agenda {
114 if (isToday || days == 1), items.contains(where: { $0.timeOfDay != nil }) { 128 if (isToday || days == 1), items.contains(where: { $0.timeOfDay != nil }) {
115 items = grid(day: day, isToday: isToday, now: now, options: options) + items 129 items = grid(day: day, isToday: isToday, now: now, options: options) + items
116 } 130 }
117 return AgendaDay(day: day, isToday: isToday, header: header(day), items: sort(items)) 131 return AgendaDay(day: day, isToday: isToday, header: header(day), items: sort(markBlocked(items, options: options)))
118 } 132 }
119 } 133 }
120 134
@@ -387,13 +401,30 @@ public enum Agenda {
387 ]) 401 ])
388 txt = highlightTodo(txt, keywords: source.keywords) 402 txt = highlightTodo(txt, keywords: source.keywords)
389 let priority = Self.priority(prefix + txt, source.priorities) 403 let priority = Self.priority(prefix + txt, source.priorities)
390 return AgendaItem( 404 var item = AgendaItem(
391 kind: kind, path: source.path, headingOffset: attached ? heading.start : nil, headingLine: attached ? heading.line : nil, markerOffset: marker, category: category, 405 kind: kind, path: source.path, headingOffset: attached ? heading.start : nil, headingLine: attached ? heading.line : nil, markerOffset: marker, category: category,
392 timeOfDay: timed.timeOfDay, time: timed.time, extra: extra, text: txt, tags: tags.map(\.name), 406 timeOfDay: timed.timeOfDay, time: timed.time, extra: extra, text: txt, tags: tags.map(\.name),
393 todo: heading.todo, isDone: heading.isDone, urgency: urgency(priority), 407 todo: heading.todo, isDone: heading.isDone, urgency: urgency(priority),
394 warntime: heading.properties["APPT_WARNTIME"].flatMap { Int($0.trimmingCharacters(in: .whitespaces)) }, habit: habit, 408 warntime: heading.properties["APPT_WARNTIME"].flatMap { Int($0.trimmingCharacters(in: .whitespaces)) }, habit: habit,
395 line: prefix + txt + (tagString.isEmpty ? "" : " " + tagString), prefix: prefix, effort: heading.properties["EFFORT"] 409 line: prefix + txt + (tagString.isEmpty ? "" : " " + tagString), prefix: prefix, effort: heading.properties["EFFORT"]
396 ) 410 )
411 item.blockedByCheckboxes = heading.blockedByCheckboxes
412 item.blockedByHierarchy = heading.blockedByHierarchy
413 return item
414 }
415
416 /// `org-agenda--mark-blocked-entry`: blocked entries are marked, or with `invisible` left
417 /// out, except those blocked by checkboxes, which only dim.
418 static func markBlocked(_ items: [AgendaItem], options: AgendaOptions) -> [AgendaItem] {
419 guard options.dimBlockedTasks != .off else { return items }
420 return items.compactMap { item in
421 let checkboxes = options.enforceTodoCheckboxDependencies && item.blockedByCheckboxes
422 guard checkboxes || (options.enforceTodoDependencies && item.blockedByHierarchy) else { return item }
423 if options.dimBlockedTasks == .invisible, !checkboxes { return nil }
424 var marked = item
425 marked.isBlocked = true
426 return marked
427 }
397 } 428 }
398 429
399 /// `%b`: the outline path above the heading, each title followed by `->`. 430 /// `%b`: the outline path above the heading, each title followed by `->`.
Sources/OrgCore/Agenda/AgendaSearch.swift +2 −2
@@ -20,7 +20,7 @@ extension Agenda {
20 )) 20 ))
21 } 21 }
22 } 22 }
23 return sortByUrgency(items) 23 return sortByUrgency(markBlocked(items, options: options))
24 } 24 }
25 25
26 /// `org-tags-view`: entries matching a tags/property/TODO match string. `todoOnly` keeps 26 /// `org-tags-view`: entries matching a tags/property/TODO match string. `todoOnly` keeps
@@ -41,7 +41,7 @@ extension Agenda {
41 )) 41 ))
42 } 42 }
43 } 43 }
44 return sortByUrgency(items) 44 return sortByUrgency(markBlocked(items, options: options))
45 } 45 }
46 46
47 /// `org-search-view`: entries, heading and body, containing `string` as a phrase, or with 47 /// `org-search-view`: entries, heading and body, containing `string` as a phrase, or with
Sources/OrgCore/Agenda/AgendaSource.swift +16 −1
@@ -41,6 +41,10 @@ public struct AgendaSource: Sendable {
41 let isHabit: Bool 41 let isHabit: Bool
42 /// `org-habit-parse-todo`, nil where it signals an error. 42 /// `org-habit-parse-todo`, nil where it signals an error.
43 let habit: Habit? 43 let habit: Habit?
44 /// `org-entry-blocked-p` with only `org-enforce-todo-checkbox-dependencies`, and with
45 /// only `org-enforce-todo-dependencies`.
46 let blockedByCheckboxes: Bool
47 let blockedByHierarchy: Bool
44 } 48 }
45 49
46 struct Stamp: Sendable { 50 struct Stamp: Sendable {
@@ -170,6 +174,9 @@ public struct AgendaSource: Sendable {
170 // Archived and commented trees are skipped whole, as `org-agenda-skip` does. 174 // Archived and commented trees are skipped whole, as `org-agenda-skip` does.
171 var skipped: [Bool] = [] 175 var skipped: [Bool] = []
172 var headings: [Heading] = [] 176 var headings: [Heading] = []
177 var options = EditingOptions.org
178 options.listAllowAlphabetical = settings.listAllowAlphabetical
179 let buffer = EmacsBuffer(text, point: 0, settings: settings, options: options)
173 for (index, info) in model.headings.enumerated() { 180 for (index, info) in model.headings.enumerated() {
174 let own = info.localTags.contains("ARCHIVE") || Self.isCommented(info.title) 181 let own = info.localTags.contains("ARCHIVE") || Self.isCommented(info.title)
175 skipped.append(fileArchived || own || (info.parent.map { skipped[$0] } ?? false)) 182 skipped.append(fileArchived || own || (info.parent.map { skipped[$0] } ?? false))
@@ -227,13 +234,21 @@ public struct AgendaSource: Sendable {
227 let entry = ns.substring(with: NSRange(location: info.headingRange.lowerBound, length: end - info.headingRange.lowerBound)) 234 let entry = ns.substring(with: NSRange(location: info.headingRange.lowerBound, length: end - info.headingRange.lowerBound))
228 habit = Habit.parse(entry: entry, scheduled: scheduled.0, doneKeywords: settings.todoSequences.flatMap { $0.done.map(\.name) }) 235 habit = Habit.parse(entry: entry, scheduled: scheduled.0, doneKeywords: settings.todoSequences.flatMap { $0.done.map(\.name) })
229 } 236 }
237 var blockedByCheckboxes = false
238 var blockedByHierarchy = false
239 if info.todo != nil, !info.isDone, buffer.localProperty("NOBLOCKING", heading: info.headingRange.lowerBound).map({ $0 == "nil" }) ?? true {
240 buffer.point = info.headingRange.lowerBound
241 blockedByCheckboxes = buffer.hasOpenCheckbox()
242 blockedByHierarchy = buffer.hierarchyBlocker() != nil
243 }
230 headings.append(Heading( 244 headings.append(Heading(
231 start: info.headingRange.lowerBound, level: info.level, skipped: skipped[index], 245 start: info.headingRange.lowerBound, level: info.level, skipped: skipped[index],
232 title: info.title.trimmingCharacters(in: .whitespaces), priority: info.priority, localTags: info.localTags, 246 title: info.title.trimmingCharacters(in: .whitespaces), priority: info.priority, localTags: info.localTags,
233 properties: properties, planning: planningValues, 247 properties: properties, planning: planningValues,
234 firstActive: ownStamps.first { $0.hasPrefix("<") }, firstInactive: ownStamps.first { $0.hasPrefix("[") }, 248 firstActive: ownStamps.first { $0.hasPrefix("<") }, firstInactive: ownStamps.first { $0.hasPrefix("[") },
235 line: line.trimmingCharacters(in: .newlines), head: head, todo: info.todo, isDone: info.isDone, 249 line: line.trimmingCharacters(in: .newlines), head: head, todo: info.todo, isDone: info.isDone,
236 tags: tags, category: category, scheduled: scheduled, deadline: deadline, isHabit: isHabit, habit: habit 250 tags: tags, category: category, scheduled: scheduled, deadline: deadline, isHabit: isHabit, habit: habit,
251 blockedByCheckboxes: blockedByCheckboxes, blockedByHierarchy: blockedByHierarchy
237 )) 252 ))
238 } 253 }
239 self.headings = headings 254 self.headings = headings
Sources/OrgCore/Commands/Command.swift +6 −1
@@ -22,6 +22,11 @@ public struct EditingOptions: Sendable, Equatable, Codable {
22 public var logRedeadline: LogHow? 22 public var logRedeadline: LogHow?
23 /// `org-use-fast-todo-selection` `auto`: with keys in the keywords, C-c C-t asks for one. 23 /// `org-use-fast-todo-selection` `auto`: with keys in the keywords, C-c C-t asks for one.
24 public var fastTodoSelection: Bool 24 public var fastTodoSelection: Bool
25 /// `org-enforce-todo-dependencies`: an entry can't become done before its TODO
26 /// descendants, or under an `ORDERED` parent before its earlier TODO siblings.
27 public var enforceTodoDependencies = false
28 /// `org-enforce-todo-checkbox-dependencies`: an entry can't become done with unchecked boxes.
29 public var enforceTodoCheckboxDependencies = false
25 /// `org-use-speed-commands`: single keys at the start of a heading line run commands. 30 /// `org-use-speed-commands`: single keys at the start of a heading line run commands.
26 public var useSpeedCommands: Bool 31 public var useSpeedCommands: Bool
27 /// `org-startup-with-inline-images`. 32 /// `org-startup-with-inline-images`.
@@ -277,7 +282,7 @@ public enum Commands {
277 ShiftTimestamp(up: true), ShiftTimestamp(up: false), ShiftTimestampDay(later: true), ShiftTimestampDay(later: false), 282 ShiftTimestamp(up: true), ShiftTimestamp(up: false), ShiftTimestampDay(later: true), ShiftTimestampDay(later: false),
278 OpenLinkAtPoint(), CreateID(), ToggleComment(), ToggleHeading(), ToggleItem(), MarkSubtree(), 283 OpenLinkAtPoint(), CreateID(), ToggleComment(), ToggleHeading(), ToggleItem(), MarkSubtree(),
279 CopySubtree(cut: false), CopySubtree(cut: true), CloneSubtree(), SortEntries(), InsertStructureTemplate(), EditSpecial(), TableEditField(), TangleCommand(.all), TangleCommand(.block), TangleCommand(.target), 284 CopySubtree(cut: false), CopySubtree(cut: true), CloneSubtree(), SortEntries(), InsertStructureTemplate(), EditSpecial(), TableEditField(), TangleCommand(.all), TangleCommand(.block), TangleCommand(.target),
280 UpdateDynamicBlock(), ClockReportTable(), SetEffort(), CtrlCCtrlC(), EvaluateTimeRange(), FootnoteAction(), FootnoteAction(special: true), TableEvalFormula(named: false), TableEvalFormula(named: true), SortTableLines(), TransposeTable(), GotoHeading(), SparseTreeCommand(), SetPriority(), SetPriority(value: "A"), SetPriority(value: "B"), SetPriority(value: "C"), SetPriority(value: " "), ReadAndSetProperty(), DeleteProperty(), DeleteProperty(globally: true), PropertyAction(), NextAllowedValue(previous: false), NextAllowedValue(previous: true), InsertColumnView(), ShowColumnView(), ShowColumnView(global: true), ToggleArchiveTag(), ArchiveToSibling(), 285 UpdateDynamicBlock(), ClockReportTable(), SetEffort(), CtrlCCtrlC(), EvaluateTimeRange(), FootnoteAction(), FootnoteAction(special: true), TableEvalFormula(named: false), TableEvalFormula(named: true), SortTableLines(), TransposeTable(), GotoHeading(), SparseTreeCommand(), SetPriority(), SetPriority(value: "A"), SetPriority(value: "B"), SetPriority(value: "C"), SetPriority(value: " "), ReadAndSetProperty(), DeleteProperty(), DeleteProperty(globally: true), PropertyAction(), NextAllowedValue(previous: false), NextAllowedValue(previous: true), InsertColumnView(), ShowColumnView(), ShowColumnView(global: true), ToggleArchiveTag(), ArchiveToSibling(), ToggleOrderedProperty(),
281 ] 286 ]
282 287
283 public static var everything: [any OrgCommand] { all + TableCommand.Action.allCases.map(TableCommand.init) } 288 public static var everything: [any OrgCommand] { all + TableCommand.Action.allCases.map(TableCommand.init) }
Sources/OrgCore/Commands/Dwim.swift +1
@@ -33,6 +33,7 @@ public struct OrgDwim: OrgCommand {
33 if settings.isDone(current) { 33 if settings.isDone(current) {
34 buffer.setTodoKeyword(sequence.active.first?.name) 34 buffer.setTodoKeyword(sequence.active.first?.name)
35 } else if let done = sequence.done.first?.name { 35 } else if let done = sequence.done.first?.name {
36 try buffer.checkTodoBlocking(from: current, to: done)
36 buffer.setTodoKeyword(done) 37 buffer.setTodoKeyword(done)
37 } 38 }
38 try buffer.afterTodoChange(previous: current, answers: context.answers) 39 try buffer.afterTodoChange(previous: current, answers: context.answers)
Sources/OrgCore/Commands/HeadingCommands.swift +4 −1
@@ -210,7 +210,8 @@ public struct TodoCycle: OrgCommand {
210 /// `org-todo`, then the parent's TODO statistics, and the repeat of a repeating entry 210 /// `org-todo`, then the parent's TODO statistics, and the repeat of a repeating entry
211 /// that became done. 211 /// that became done.
212 public func run(in context: EditContext) -> CommandStep { 212 public func run(in context: EditContext) -> CommandStep {
213 let previous = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings).todoKeyword 213 let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings, options: context.options)
214 let previous = buffer.todoKeyword
214 if context.options.fastTodoSelection, entryHeading(at: context.caret, in: context.tree) != nil, 215 if context.options.fastTodoSelection, entryHeading(at: context.caret, in: context.tree) != nil,
215 let fast = FastSelection.todo(context.tree.settings) { 216 let fast = FastSelection.todo(context.tree.settings) {
216 guard let answer = context.answers["todo"] else { 217 guard let answer = context.answers["todo"] else {
@@ -219,6 +220,8 @@ public struct TodoCycle: OrgCommand {
219 guard let key = answer.first, let keyword = fast.keyword(for: key, current: previous) else { return .failed("Quit") } 220 guard let key = answer.first, let keyword = fast.keyword(for: key, current: previous) else { return .failed("Quit") }
220 return SetTodo(keyword: keyword).run(in: context) 221 return SetTodo(keyword: keyword).run(in: context)
221 } 222 }
223 let next = Self.next(after: previous, in: context.tree.settings, repeated: context.lastCommand == id)
224 if let message = buffer.todoBlockedMessage(from: previous, to: next) { return .failed(message) }
222 return followUp(context, cycle(in: context)) { buffer in 225 return followUp(context, cycle(in: context)) { buffer in
223 try buffer.saveExcursion { 226 try buffer.saveExcursion {
224 buffer.updateParentTodoStatistics() 227 buffer.updateParentTodoStatistics()
Sources/OrgCore/Commands/Properties.swift +1
@@ -36,6 +36,7 @@ public struct SetTodo: OrgCommand {
36 return commitBuffer(context) { buffer in 36 return commitBuffer(context) { buffer in
37 try buffer.saveExcursion { 37 try buffer.saveExcursion {
38 let previous = buffer.todoKeyword 38 let previous = buffer.todoKeyword
39 try buffer.checkTodoBlocking(from: previous, to: keyword)
39 buffer.setTodoKeyword(keyword) 40 buffer.setTodoKeyword(keyword)
40 try buffer.afterTodoChange(previous: previous, answers: context.answers) 41 try buffer.afterTodoChange(previous: previous, answers: context.answers)
41 } 42 }
Sources/OrgCore/Commands/TodoDependencies.swift added +157
@@ -0,0 +1,157 @@
1import Foundation
2
3// TODO dependencies (Org 9.8.7): `org-block-todo-from-children-or-siblings-or-parent` with
4// `org-enforce-todo-dependencies`, `org-block-todo-from-checkboxes` with
5// `org-enforce-todo-checkbox-dependencies`, `org-entry-blocked-p` and
6// `org-toggle-ordered-property` with `org-track-ordered-property-with-tag` nil.
7
8extension EmacsBuffer {
9 /// `org-not-done-heading-regexp`.
10 var notDoneHeadingPattern: String? {
11 let names = settings.todoSequences.flatMap { $0.active.map(\.name) }
12 guard !names.isEmpty else { return nil }
13 return "^(\\*+)(?: +(" + names.map { NSRegularExpression.escapedPattern(for: $0) }.joined(separator: "|") + "))(?: +(.*?))?[ \\t]*$"
14 }
15
16 /// `org-get-heading` for the heading at point.
17 func orgGetHeading() -> String {
18 saveExcursion {
19 _ = try? backToHeading()
20 guard let match = lookingAt(complexHeadingPattern) else { return "" }
21 let parts = [match[2], match[3]].map { $0.map { substring($0.lowerBound, $0.upperBound) } }
22 + [match[4].map { substring($0.lowerBound, $0.upperBound) } ?? "", match[5].map { substring($0.lowerBound, $0.upperBound) }]
23 return parts.compactMap { $0 }.joined(separator: " ")
24 }
25 }
26
27 /// Whether the heading at `heading` has a not-done keyword.
28 private func isNotDone(at heading: Int) -> Bool {
29 saveExcursion {
30 point = heading
31 return todoKeyword.map { keywordsInOrder.contains($0) && !settings.isDone($0) } ?? false
32 }
33 }
34
35 /// `ORDERED` on the entry at `heading`, as `org-not-nil` of `org-entry-get`.
36 private func isOrdered(_ heading: Int) -> Bool {
37 localProperty("ORDERED", heading: heading).map { $0 != "nil" } ?? false
38 }
39
40 /// `org-block-todo-from-children-or-siblings-or-parent` for the entry at point becoming
41 /// done: the heading that blocks it, or nil.
42 func hierarchyBlocker() -> String? {
43 saveExcursion {
44 guard let start = try? backToHeading() else { return nil }
45 let level = headingLevel(atLine: start)!
46 outlineNextHeading()
47 while !eobp, outlineLevel > level {
48 if isNotDone(at: point) { return orgGetHeading() }
49 outlineNextHeading()
50 }
51 guard let notDone = notDoneHeadingPattern else { return nil }
52 point = start
53 var pos = start
54 guard upHeadingSafe() != nil else { return nil }
55 var parent = point
56 if isOrdered(parent) {
57 forwardLine(1)
58 if let match = searchForward(notDone, bound: pos, caseInsensitive: false) { return substring(match[0]!.lowerBound, match[0]!.upperBound) }
59 }
60 // An ancestor blocked by its own ordered siblings blocks its subtree.
61 while true {
62 point = parent
63 guard lookingAt(notDone) != nil else { return nil }
64 pos = point
65 guard upHeadingSafe() != nil else { return nil }
66 parent = point
67 if isOrdered(parent) {
68 forwardLine(1)
69 if searchForward(notDone, bound: pos, caseInsensitive: false) != nil { return orgGetHeading() }
70 }
71 }
72 }
73 }
74
75 /// `org-block-todo-from-checkboxes`: an unchecked or partly checked box in the entry's
76 /// own text, outside blocks lists can't be in.
77 func hasOpenCheckbox() -> Bool {
78 saveExcursion {
79 guard let start = try? backToHeading() else { return false }
80 outlineNextHeading()
81 let end = point
82 point = start
83 let pattern = itemBeginningPattern + "(?:\\[@(?:start:)?([0-9]+|[A-Za-z])\\][ \\t]*)?\\[[- ]\\]"
84 while searchForward(pattern, bound: end, caseInsensitive: false) != nil {
85 if !inBlock(Self.forbiddenBlocks) { return true }
86 }
87 return false
88 }
89 }
90
91 /// `org-in-block-p`: point is in a block of one of the types, between the headings around it.
92 func inBlock(_ names: [String]) -> Bool {
93 saveExcursion {
94 let pos = point
95 let up = saveExcursion { () -> Int in outlinePreviousHeading(); return point }
96 let down = saveExcursion { () -> Int in outlineNextHeading(); return point }
97 for name in names {
98 let begin = "^[ \\t]*#\\+begin_" + NSRegularExpression.escapedPattern(for: name)
99 let end = "^[ \\t]*#\\+end_" + NSRegularExpression.escapedPattern(for: name)
100 point = pos
101 guard let open = searchBackward(begin, bound: up) else { continue }
102 let beg = open[0]!.lowerBound
103 point = open[0]!.upperBound
104 guard let close = searchForward(end, bound: down), close[0]!.upperBound > pos else { continue }
105 point = close[0]!.lowerBound
106 if searchBackward(begin, bound: beg + 1) == nil { return true }
107 }
108 return false
109 }
110 }
111
112 /// `org-blocker-hook` as `org-todo` runs it for a change from `from` to `to`, with the
113 /// checkbox hook first, as `org-mode` adds them: the message refusing the change, or nil.
114 func todoBlockedMessage(from: String?, to: String?) -> String? {
115 let checkboxes = options.enforceTodoCheckboxDependencies
116 let dependencies = options.enforceTodoDependencies
117 guard checkboxes || dependencies, let to, settings.isDone(to), !(from.map(settings.isDone) ?? false) else { return nil }
118 let heading = saveExcursion { try? backToHeading() }
119 guard let heading, localProperty("NOBLOCKING", heading: heading).map({ $0 == "nil" }) ?? true else { return nil }
120 let reason: String
121 if checkboxes, hasOpenCheckbox() {
122 reason = "contained checkboxes"
123 } else if dependencies, let blocker = hierarchyBlocker() {
124 reason = "\"\(blocker)\""
125 } else {
126 return nil
127 }
128 return "TODO state change from \(from ?? "nil") to \(to) blocked (by \(reason))"
129 }
130
131 /// Throws the refusal of a change from `from` to `to`, if it is blocked.
132 func checkTodoBlocking(from: String?, to: String?) throws {
133 if let message = todoBlockedMessage(from: from, to: to) { throw UserError(message: message) }
134 }
135}
136
137/// C-c C-x o: `org-toggle-ordered-property`.
138public struct ToggleOrderedProperty: OrgCommand {
139 public init() {}
140 public var id: String { "org.property.toggle-ordered" }
141 public var title: String { "Toggle ORDERED Property" }
142 public func applies(in context: EditContext) -> Bool { entryHeading(at: context.caret, in: context.tree) != nil }
143 public func run(in context: EditContext) -> CommandStep {
144 commitBuffer(context) { buffer in
145 try buffer.saveExcursion {
146 let heading = try buffer.backToHeading()
147 if buffer.localProperty("ORDERED", heading: heading).map({ $0 != "nil" }) ?? false {
148 buffer.entryDelete("ORDERED")
149 buffer.message = "Subtasks can be completed in arbitrary order"
150 } else {
151 buffer.entryPut("ORDERED", "t")
152 buffer.message = "Subtasks must be completed in sequence"
153 }
154 }
155 }
156 }
157}
Sources/OrgCore/Keymap/Presets.swift +3
@@ -761,6 +761,9 @@ public enum KeymapPreset: String, Sendable, CaseIterable {
761 keys = "C-c C-x A" 761 keys = "C-c C-x A"
762 command = "org.archive.to-sibling" 762 command = "org.archive.to-sibling"
763 [[bind]] 763 [[bind]]
764 keys = "C-c C-x o"
765 command = "org.property.toggle-ordered"
766 [[bind]]
764 keys = "C-c C-x C-v" 767 keys = "C-c C-x C-v"
765 command = "org.toggle-inline-images" 768 command = "org.toggle-inline-images"
766 [[bind]] 769 [[bind]]
Sources/OrgPresentation/AgendaRows.swift +4
@@ -73,6 +73,8 @@ public struct AgendaRow: Sendable, Equatable, Identifiable {
73 public let tags: [String] 73 public let tags: [String]
74 public let todo: String? 74 public let todo: String?
75 public let isDone: Bool 75 public let isDone: Bool
76 /// Blocked from becoming done (`org-agenda-dim-blocked-tasks`): shown dimmed.
77 public let isBlocked: Bool
76 public let priority: String? 78 public let priority: String?
77 /// The statistics cookie: `1/3`, `50%`. 79 /// The statistics cookie: `1/3`, `50%`.
78 public let progress: String? 80 public let progress: String?
@@ -146,6 +148,7 @@ public struct AgendaRow: Sendable, Equatable, Identifiable {
146 self.tags = item.tags 148 self.tags = item.tags
147 self.todo = item.todo 149 self.todo = item.todo
148 self.isDone = item.isDone 150 self.isDone = item.isDone
151 self.isBlocked = item.isBlocked
149 self.priority = item.priorityLetter 152 self.priority = item.priorityLetter
150 self.progress = item.progress 153 self.progress = item.progress
151 self.detail = item.kind == .clock || item.kind == .state ? Self.logDetail(item.extra) : nil 154 self.detail = item.kind == .clock || item.kind == .state ? Self.logDetail(item.extra) : nil
@@ -185,6 +188,7 @@ public struct AgendaRow: Sendable, Equatable, Identifiable {
185 self.tags = [] 188 self.tags = []
186 self.todo = nil 189 self.todo = nil
187 self.isDone = false 190 self.isDone = false
191 self.isBlocked = false
188 self.priority = nil 192 self.priority = nil
189 self.progress = nil 193 self.progress = nil
190 self.detail = nil 194 self.detail = nil
Sources/Orgstar/AgendaView.swift +2
@@ -1096,6 +1096,8 @@ struct AgendaRowView: View {
1096 .padding(.horizontal, 16) 1096 .padding(.horizontal, 16)
1097 .padding(.vertical, 6) 1097 .padding(.vertical, 6)
1098 .frame(minHeight: 44) 1098 .frame(minHeight: 44)
1099 // `org-agenda-dim-blocked-tasks`.
1100 .opacity(row.isBlocked ? 0.5 : 1)
1099 .background { 1101 .background {
1100 RoundedRectangle(cornerRadius: 6) 1102 RoundedRectangle(cornerRadius: 6)
1101 .fill(selected ? theme.color("selection") ?? Color.accentColor.opacity(0.2) : hovered ? Color.primary.opacity(0.05) : .clear) 1103 .fill(selected ? theme.color("selection") ?? Color.accentColor.opacity(0.2) : hovered ? Color.primary.opacity(0.05) : .clear)
Sources/Orgstar/ContentView.swift +4 −1
@@ -49,6 +49,8 @@ struct ContentView: View {
49 @AppStorage("electricPair") private var electricPair = true 49 @AppStorage("electricPair") private var electricPair = true
50 @AppStorage("hideDrawerStartup") private var hideDrawerStartup = true 50 @AppStorage("hideDrawerStartup") private var hideDrawerStartup = true
51 @AppStorage("hideBlockStartup") private var hideBlockStartup = false 51 @AppStorage("hideBlockStartup") private var hideBlockStartup = false
52 @AppStorage("enforceTodoDependencies") private var enforceTodoDependencies = false
53 @AppStorage("enforceTodoCheckboxDependencies") private var enforceTodoCheckboxDependencies = false
52 54
53 private var options: EditingOptions { 55 private var options: EditingOptions {
54 Preferences.options( 56 Preferences.options(
@@ -57,7 +59,8 @@ struct ContentView: View {
57 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, logReschedule: logReschedule, logRedeadline: logRedeadline, 59 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, logReschedule: logReschedule, logRedeadline: logRedeadline,
58 useSpeedCommands: useSpeedCommands, startupWithInlineImages: startupWithInlineImages, 60 useSpeedCommands: useSpeedCommands, startupWithInlineImages: startupWithInlineImages,
59 startupIndented: startupIndented, hideLeadingStars: hideLeadingStars, startupAlignAllTables: startupAlignAllTables, 61 startupIndented: startupIndented, hideLeadingStars: hideLeadingStars, startupAlignAllTables: startupAlignAllTables,
60 startupTruncated: startupTruncated, spellCheck: spellCheck, electricPair: electricPair 62 startupTruncated: startupTruncated, spellCheck: spellCheck, electricPair: electricPair,
63 enforceTodoDependencies: enforceTodoDependencies, enforceTodoCheckboxDependencies: enforceTodoCheckboxDependencies
61 ) 64 )
62 } 65 }
63 /// A block being edited apart (`org-edit-special`). 66 /// A block being edited apart (`org-edit-special`).
Sources/Orgstar/OrgstarApp.swift +16 −1
@@ -517,10 +517,13 @@ struct SettingsView: View {
517 @AppStorage("prettyEntities") private var prettyEntities = true 517 @AppStorage("prettyEntities") private var prettyEntities = true
518 @AppStorage("startupTruncated") private var startupTruncated = false 518 @AppStorage("startupTruncated") private var startupTruncated = false
519 @AppStorage("todoKeywords") private var todoKeywords = Preferences.defaultTodoKeywords 519 @AppStorage("todoKeywords") private var todoKeywords = Preferences.defaultTodoKeywords
520 @AppStorage("enforceTodoDependencies") private var enforceTodoDependencies = false
521 @AppStorage("enforceTodoCheckboxDependencies") private var enforceTodoCheckboxDependencies = false
520 @AppStorage("agendaSpan") private var agendaSpan = 10 522 @AppStorage("agendaSpan") private var agendaSpan = 10
521 @AppStorage("agendaStartOffset") private var agendaStartOffset = -3 523 @AppStorage("agendaStartOffset") private var agendaStartOffset = -3
522 @AppStorage("agendaSubfolders") private var agendaSubfolders = false 524 @AppStorage("agendaSubfolders") private var agendaSubfolders = false
523 @AppStorage("agendaShowAllDates") private var agendaShowAllDates = false 525 @AppStorage("agendaShowAllDates") private var agendaShowAllDates = false
526 @AppStorage("agendaDimBlockedTasks") private var agendaDimBlockedTasks = "t"
524 @AppStorage("calendarEvents") private var calendarEvents = false 527 @AppStorage("calendarEvents") private var calendarEvents = false
525 @AppStorage("calendarEventCalendars") private var calendarEventCalendars = "" 528 @AppStorage("calendarEventCalendars") private var calendarEventCalendars = ""
526 @State private var calendars: [(id: String, title: String, color: RGBA?)] = [] 529 @State private var calendars: [(id: String, title: String, color: RGBA?)] = []
@@ -634,9 +637,16 @@ struct SettingsView: View {
634 Text("For files without #+TODO, in #+TODO syntax, one sequence per line. Takes effect when Orgstar restarts.") 637 Text("For files without #+TODO, in #+TODO syntax, one sequence per line. Takes effect when Orgstar restarts.")
635 .settingsFooter() 638 .settingsFooter()
636 } 639 }
640 Section {
641 Toggle("Entries can't be done before their TODO children (org-enforce-todo-dependencies)", isOn: $enforceTodoDependencies)
642 Toggle("Entries can't be done with unchecked boxes (org-enforce-todo-checkbox-dependencies)", isOn: $enforceTodoCheckboxDependencies)
643 } footer: {
644 Text("Under a parent with the ORDERED property (Toggle ORDERED Property, C-c C-x o), TODO children are also done in order.")
645 .settingsFooter()
646 }
637 } 647 }
638 .formStyle(.grouped) 648 .formStyle(.grouped)
639 .frame(height: 520) 649 .frame(height: 640)
640 } 650 }
641 651
642 private var appearance: some View { 652 private var appearance: some View {
@@ -681,6 +691,11 @@ struct SettingsView: View {
681 Stepper(agendaStartOffset == 0 ? "Agenda starts today" : "Agenda starts \(-agendaStartOffset) days before today", value: $agendaStartOffset, in: -14...0) 691 Stepper(agendaStartOffset == 0 ? "Agenda starts today" : "Agenda starts \(-agendaStartOffset) days before today", value: $agendaStartOffset, in: -14...0)
682 Toggle("Include files in subfolders", isOn: $agendaSubfolders) 692 Toggle("Include files in subfolders", isOn: $agendaSubfolders)
683 Toggle("Show days without entries", isOn: $agendaShowAllDates) 693 Toggle("Show days without entries", isOn: $agendaShowAllDates)
694 Picker("Blocked entries", selection: $agendaDimBlockedTasks) {
695 Text("Dimmed").tag("t")
696 Text("Hidden").tag("invisible")
697 Text("Shown as usual").tag("nil")
698 }
684 } 699 }
685 Section { 700 Section {
686 Toggle("Show events from Calendar", isOn: $calendarEvents) 701 Toggle("Show events from Calendar", isOn: $calendarEvents)
Sources/Orgstar/Preferences.swift +4 −1
@@ -18,7 +18,8 @@ enum Preferences {
18 tagsColumn: Int, headingAfterSubtree: Bool, splitLine: Bool, alphabeticalLists: Bool, fillColumn: Int, 18 tagsColumn: Int, headingAfterSubtree: Bool, splitLine: Bool, alphabeticalLists: Bool, fillColumn: Int,
19 hiddenMarkers: Bool, prettyEntities: Bool, logDone: String, logIntoDrawer: String, 19 hiddenMarkers: Bool, prettyEntities: Bool, logDone: String, logIntoDrawer: String,
20 hideDrawerStartup: Bool, hideBlockStartup: Bool, logReschedule: String, logRedeadline: String, useSpeedCommands: Bool, startupWithInlineImages: Bool, 20 hideDrawerStartup: Bool, hideBlockStartup: Bool, logReschedule: String, logRedeadline: String, useSpeedCommands: Bool, startupWithInlineImages: Bool,
21 startupIndented: Bool, hideLeadingStars: Bool, startupAlignAllTables: Bool, startupTruncated: Bool, spellCheck: Bool, electricPair: Bool 21 startupIndented: Bool, hideLeadingStars: Bool, startupAlignAllTables: Bool, startupTruncated: Bool, spellCheck: Bool, electricPair: Bool,
22 enforceTodoDependencies: Bool, enforceTodoCheckboxDependencies: Bool
22 ) -> EditingOptions { 23 ) -> EditingOptions {
23 var options = EditingOptions( 24 var options = EditingOptions(
24 tagsColumn: tagsColumn, insertHeadingRespectContent: headingAfterSubtree, 25 tagsColumn: tagsColumn, insertHeadingRespectContent: headingAfterSubtree,
@@ -32,6 +33,8 @@ enum Preferences {
32 startupTruncated: startupTruncated, spellCheck: spellCheck 33 startupTruncated: startupTruncated, spellCheck: spellCheck
33 ) 34 )
34 options.electricPair = electricPair 35 options.electricPair = electricPair
36 options.enforceTodoDependencies = enforceTodoDependencies
37 options.enforceTodoCheckboxDependencies = enforceTodoCheckboxDependencies
35 return options 38 return options
36 } 39 }
37} 40}
Sources/OrgstarMobile/AgendaScreen.swift +2 −1
@@ -620,7 +620,8 @@ struct AgendaRowView: View {
620 } 620 }
621 } 621 }
622 .padding(.vertical, 2) 622 .padding(.vertical, 2)
623 .opacity(row.isDone ? 0.75 : 1) 623 // Blocked entries dim, as `org-agenda-dim-blocked-tasks`.
624 .opacity(row.isBlocked ? 0.5 : row.isDone ? 0.75 : 1)
624 } 625 }
625 626
626 /// The TODO keyword and the priority, on the category's line. 627 /// The TODO keyword and the priority, on the category's line.
Tests/OrgCoreTests/TodoDependencyTests.swift added +202
@@ -0,0 +1,202 @@
1import Foundation
2import Testing
3@testable import OrgCore
4
5struct TodoDependencyTests {
6 static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil
7
8 /// Point is at the .
9 static let texts = [
10 "* TODO A¶\n** TODO B\n",
11 "* TODO A¶\n** DONE B\n",
12 "* TODO A¶\n** B\n",
13 "* TODO A¶\n** B\n*** TODO C\n",
14 "* TODO A¶\n** DONE B\n*** TODO C\n",
15 "* A¶\n** TODO B\n",
16 "* TODO A\nbody¶\n** TODO [#A] B :x:y:\n",
17 "* TODO A¶\n** TODO\n",
18 "* TODO A¶\n** TODO B :ARCHIVE:\n",
19 "* TODO A¶\n:PROPERTIES:\n:NOBLOCKING: t\n:END:\n** TODO B\n",
20 "* TODO A¶\n:PROPERTIES:\n:NOBLOCKING: nil\n:END:\n** TODO B\n",
21 "* TODO A¶\nSCHEDULED: <2026-10-05 Mon +1d>\n** TODO B\n",
22 "* P\n:PROPERTIES:\n:ORDERED: t\n:END:\n** TODO A :x:\n** TODO B¶\n",
23 "* P\n:PROPERTIES:\n:ORDERED: t\n:END:\n** DONE A\n** TODO B¶\n",
24 "* P\n:PROPERTIES:\n:ORDERED: t\n:END:\n** TODO A¶\n** TODO B\n",
25 "* P\n:PROPERTIES:\n:ORDERED: t\n:END:\n** A\n** TODO B¶\n",
26 "* P\n:PROPERTIES:\n:ORDERED: nil\n:END:\n** TODO A\n** TODO B¶\n",
27 "* P\n:PROPERTIES:\n:ORDERED:\n:END:\n** TODO A\n** TODO B¶\n",
28 "* P\n:PROPERTIES:\n:ordered: t\n:END:\n** TODO A\n** TODO B¶\n",
29 "* P\n:PROPERTIES:\n:ORDERED: t\n:END:\n** DONE A\n*** TODO A1\n** TODO B¶\n",
30 "* G\n:PROPERTIES:\n:ORDERED: t\n:END:\n** P\n*** TODO A\n*** TODO B¶\n",
31 "* G\n:PROPERTIES:\n:ORDERED: t\n:END:\n** TODO A\n** TODO P\n*** TODO B¶\n",
32 "* G\n:PROPERTIES:\n:ORDERED: t\n:END:\n** TODO A\n** P\n*** TODO B¶\n",
33 "* G\n:PROPERTIES:\n:ORDERED: t\n:END:\n** TODO A\n** TODO P\n*** TODO Q\n**** TODO B¶\n",
34 "* G\n:PROPERTIES:\n:ORDERED: t\n:END:\n** DONE A\n** TODO P\n*** TODO B¶\n",
35 "#+PROPERTY: ORDERED t\n* P\n** TODO A\n** TODO B¶\n",
36 "* TODO A\n* TODO B¶\n",
37 "* TODO A¶\n- [ ] x\n",
38 "* TODO A¶\n- [X] x\n",
39 "* TODO A¶\n- [-] x\n - [X] y\n - [ ] z\n",
40 "* TODO A¶\n1. [@3] [ ] x\n",
41 "* TODO A¶\n + [ ] x\n",
42 "* TODO A¶\n * [ ] x\n",
43 "* TODO A¶\n- [ ]\n",
44 "* TODO A¶\nSome [ ] text\n",
45 "* TODO A¶\n** B\n- [ ] x\n",
46 "* TODO A¶\n#+begin_src text\n- [ ] x\n#+end_src\n",
47 "* TODO A¶\n#+begin_example\n- [ ] x\n#+end_example\n",
48 "* TODO A¶\n#+begin_quote\n- [ ] x\n#+end_quote\n",
49 "* TODO A¶\n:NOTES:\n- [ ] x\n:END:\n",
50 "* TODO A¶\n- [ ] x\n** TODO B\n",
51 "* TODO A¶\n- [X] x\n** TODO B\n",
52 "#+TODO: TODO NEXT | DONE CANCELED\n* NEXT A¶\n** TODO B\n",
53 "#+TODO: TODO NEXT | DONE CANCELED\n* DONE A¶\n** TODO B\n",
54 "#+TODO: TODO NEXT | DONE CANCELED\n* TODO A¶\n** NEXT B\n",
55 ]
56
57 static let settings: [(dependencies: Bool, checkboxes: Bool)] = [(true, false), (false, true), (true, true), (false, false)]
58
59 static func place(_ marked: String) -> (text: String, caret: Int) {
60 let caret = (marked as NSString).range(of: "").location
61 return (marked.replacingOccurrences(of: "", with: ""), caret)
62 }
63
64 static func run(_ command: any OrgCommand, _ text: String, caret: Int, options: EditingOptions) -> (text: String, message: String?) {
65 var utc = Calendar(identifier: .gregorian)
66 utc.timeZone = TimeZone(identifier: "UTC")!
67 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret],
68 now: Date(timeIntervalSince1970: 1_789_000_000), calendar: utc, options: options)
69 switch command.run(in: context) {
70 case .commit(let result):
71 let new = result.edits.sorted { $0.range.lowerBound > $1.range.lowerBound }.reduce(text) { $1.apply(to: $0) }
72 let message = result.effects.compactMap { if case .message(let m) = $0 { m } else { nil } }.last
73 return (new, message)
74 case .failed(let message): return (text, message)
75 default: return (text, "other")
76 }
77 }
78
79 /// Runs `form` with the settings, failing with the last message it shows that `keep` keeps.
80 static func form(_ form: String, dependencies: Bool, checkboxes: Bool, keep: String = "blocked") -> String {
81 frozenClock(1_789_000_000, """
82 (let (msgs)
83 (setq org-blocker-hook nil org-enforce-todo-dependencies \(dependencies ? "t" : "nil")
84 org-enforce-todo-checkbox-dependencies \(checkboxes ? "t" : "nil") org-use-fast-todo-selection nil)
85 (org-mode)
86 (cl-letf (((symbol-function 'message) (lambda (f &rest a) (when f (push (apply #'format-message f a) msgs)) (car msgs))))
87 \(form)
88 (run-hooks 'post-command-hook))
89 (let ((m (seq-find (lambda (s) (string-match-p \(lispString(keep)) s)) msgs))) (when m (error "%s" m))))
90 """)
91 }
92
93 @Test(.enabled(if: oracle))
94 func blocksLikeOrgTodo() throws {
95 let changes: [(String, any OrgCommand, String)] = [
96 ("DONE", SetTodo(keyword: "DONE"), "(org-todo \"DONE\")"),
97 ("CANCELED", SetTodo(keyword: "CANCELED"), "(org-todo \"CANCELED\")"),
98 ("TODO", SetTodo(keyword: "TODO"), "(org-todo \"TODO\")"),
99 ("none", SetTodo(keyword: nil), "(org-todo 'none)"),
100 ("cycle", TodoCycle(), "(org-todo)"),
101 ("right", TodoShift(right: true), "(org-todo 'right)"),
102 ("left", TodoShift(right: false), "(org-todo 'left)"),
103 ]
104 var cases: [EmacsOracle.Case] = []
105 var ours: [(text: String, message: String?)] = []
106 var labels: [String] = []
107 for marked in Self.texts {
108 let (text, caret) = Self.place(marked)
109 let sequences = text.hasPrefix("#+TODO:")
110 for (dependencies, checkboxes) in Self.settings {
111 var options = EditingOptions.org
112 options.enforceTodoDependencies = dependencies
113 options.enforceTodoCheckboxDependencies = checkboxes
114 for (name, command, form) in changes where sequences || name != "CANCELED" {
115 cases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text),
116 form: Self.form(form, dependencies: dependencies, checkboxes: checkboxes)))
117 ours.append(Self.run(command, text, caret: caret, options: options))
118 labels.append("\(form) dependencies \(dependencies) checkboxes \(checkboxes)")
119 }
120 }
121 }
122 for (i, theirs) in try EmacsOracle.run(cases).enumerated() {
123 #expect(ours[i].text == theirs.text && (ours[i].message ?? "") == theirs.error,
124 "\(labels[i]) on \(cases[i].text.debugDescription)\nours: \(ours[i].text.debugDescription) \(ours[i].message ?? "")\nemacs: \(theirs.text.debugDescription) \(theirs.error)")
125 }
126 }
127
128 @Test(.enabled(if: oracle))
129 func togglesOrderedLikeOrg() throws {
130 let texts = [
131 "* A¶\n",
132 "* A¶\n:PROPERTIES:\n:ORDERED: t\n:END:\n",
133 "* A\n:PROPERTIES:\n:X: 1\n:ORDERED: t\n:END:\nbody¶\n",
134 "* A¶\n:PROPERTIES:\n:ORDERED: nil\n:END:\n",
135 "* A¶\n:PROPERTIES:\n:ORDERED:\n:END:\n",
136 "* A¶\nSCHEDULED: <2026-10-05 Mon>\nbody\n",
137 "* A\n** TODO B¶ :x:\n** C\n",
138 "* A¶",
139 "text¶\n* A\n",
140 ]
141 var cases: [EmacsOracle.Case] = []
142 var ours: [(text: String, message: String?)] = []
143 for marked in texts {
144 let (text, caret) = Self.place(marked)
145 cases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text),
146 form: Self.form("(org-toggle-ordered-property)", dependencies: false, checkboxes: false, keep: "Subtasks")))
147 ours.append(Self.run(ToggleOrderedProperty(), text, caret: caret, options: .org))
148 }
149 for (i, theirs) in try EmacsOracle.run(cases).enumerated() {
150 // Org's message also names the position it reached and the buffer.
151 let before = "Before first headline"
152 let message = theirs.error.hasPrefix(before) && ours[i].message?.hasPrefix(before) == true ? theirs.error : ours[i].message
153 #expect(ours[i].text == theirs.text && (message ?? "") == theirs.error,
154 "on \(cases[i].text.debugDescription)\nours: \(ours[i].text.debugDescription) \(ours[i].message ?? "")\nemacs: \(theirs.text.debugDescription) \(theirs.error)")
155 }
156 }
157
158 /// `org-entry-blocked-p` on every heading, which the agenda dims.
159 @Test(.enabled(if: oracle))
160 func agendaBlockedLikeOrg() throws {
161 var cases: [EmacsOracle.Case] = []
162 var ours: [String] = []
163 for marked in Self.texts {
164 let text = Self.place(marked).text
165 let source = AgendaSource(path: "/tmp/oracle.org", text: text)
166 for dependencies in [true, false] {
167 let form = "(push (mapconcat #'identity (org-map-entries (lambda () (if (org-entry-blocked-p) \"b\" \"-\"))) \"\") msgs)"
168 cases.append(EmacsOracle.Case(text: text, point: 1, form: Self.form(form, dependencies: dependencies, checkboxes: !dependencies, keep: "")))
169 ours.append(source.headings.map { (dependencies ? $0.blockedByHierarchy : $0.blockedByCheckboxes) ? "b" : "-" }.joined())
170 }
171 }
172 for (i, theirs) in try EmacsOracle.run(cases).enumerated() {
173 #expect(ours[i] == theirs.error, "\(i % 2 == 0 ? "dependencies" : "checkboxes") on \(cases[i].text.debugDescription)")
174 }
175 }
176
177 @Test func agendaDimsOrHidesBlockedEntries() {
178 let text = "* TODO A\n** TODO B\n* TODO C\n- [ ] x\n* TODO D\n"
179 let source = AgendaSource(path: "/tmp/a.org", text: text)
180 func blocked(_ dependencies: Bool, _ checkboxes: Bool, _ dim: AgendaOptions.DimBlocked) -> [String] {
181 var options = AgendaOptions()
182 options.enforceTodoDependencies = dependencies
183 options.enforceTodoCheckboxDependencies = checkboxes
184 options.dimBlockedTasks = dim
185 return Agenda.todoList([source], options: options).map { ($0.isBlocked ? "~" : "") + $0.text }
186 }
187 #expect(blocked(false, false, .dim) == ["TODO A", "TODO B", "TODO C", "TODO D"])
188 #expect(blocked(true, true, .dim) == ["~TODO A", "TODO B", "~TODO C", "TODO D"])
189 #expect(blocked(true, false, .dim) == ["~TODO A", "TODO B", "TODO C", "TODO D"])
190 #expect(blocked(true, true, .off) == ["TODO A", "TODO B", "TODO C", "TODO D"])
191 // Entries blocked by checkboxes are dimmed, never hidden.
192 #expect(blocked(true, true, .invisible) == ["TODO B", "~TODO C", "TODO D"])
193 }
194
195 @Test func dwimIsBlocked() {
196 let text = "* TODO A\n** TODO B\n"
197 var options = EditingOptions.org
198 options.enforceTodoDependencies = true
199 #expect(Self.run(OrgDwim(), text, caret: 3, options: options) == (text, "TODO state change from TODO to DONE blocked (by \"TODO B\")"))
200 #expect(Self.run(OrgDwim(), text, caret: 3, options: .org).text == "* DONE A\n** TODO B\n")
201 }
202}
docs/manual/guide/03-keys.org +1
@@ -828,6 +828,7 @@ When a key appears in several rows, it runs the command whose condition holds at
828| Cycle TODO State (=org.todo.cycle=) | =C-c C-t= | =⌃⌘T= | =SPC m t= (N) | Cycles the TODO keyword; with fast-selection keys in =#+TODO=, asks for one (=org-todo=). | 828| Cycle TODO State (=org.todo.cycle=) | =C-c C-t= | =⌃⌘T= | =SPC m t= (N) | Cycles the TODO keyword; with fast-selection keys in =#+TODO=, asks for one (=org-todo=). |
829| Next TODO Keyword (=org.todo.next=) | =S-<right>= | =⌃⇧⌘→= | =C-S-l= (N, I) | On a heading, sets the next keyword across all sequences (=org-shiftright=). | 829| Next TODO Keyword (=org.todo.next=) | =S-<right>= | =⌃⇧⌘→= | =C-S-l= (N, I) | On a heading, sets the next keyword across all sequences (=org-shiftright=). |
830| Previous TODO Keyword (=org.todo.previous=) | =S-<left>= | =⌃⇧⌘←= | =C-S-h= (N, I) | On a heading, sets the previous keyword (=org-shiftleft=). | 830| Previous TODO Keyword (=org.todo.previous=) | =S-<left>= | =⌃⇧⌘←= | =C-S-h= (N, I) | On a heading, sets the previous keyword (=org-shiftleft=). |
831| Toggle ORDERED Property (=org.property.toggle-ordered=) | =C-c C-x o= | — | — | Sets or removes =:ORDERED: t=, which makes the entry's TODO children be done in order (=org-toggle-ordered-property=). |
831| Raise Priority (=org.priority.up=) | =S-<up>= | =⌃⌘↑= | =C-S-k= (N, I), =SPC m p u= (N) | On a heading, raises the priority (=org-priority-up=). | 832| Raise Priority (=org.priority.up=) | =S-<up>= | =⌃⌘↑= | =C-S-k= (N, I), =SPC m p u= (N) | On a heading, raises the priority (=org-priority-up=). |
832| Lower Priority (=org.priority.down=) | =S-<down>= | =⌃⌘↓= | =C-S-j= (N, I), =SPC m p d= (N) | On a heading, lowers the priority (=org-priority-down=). | 833| Lower Priority (=org.priority.down=) | =S-<down>= | =⌃⌘↓= | =C-S-j= (N, I), =SPC m p d= (N) | On a heading, lowers the priority (=org-priority-down=). |
833| Set Priority… (=org.priority.set=) | =C-c ,= | =⌃⌘,= | =SPC m p p= (N) | Asks for a priority; =SPC= removes it (=org-priority=). | 834| Set Priority… (=org.priority.set=) | =C-c ,= | =⌃⌘,= | =SPC m p p= (N) | Asks for a priority; =SPC= removes it (=org-priority=). |
docs/manual/guide/05-todos-and-tags.org +26 −1
@@ -95,7 +95,30 @@ Insert TODO Heading (=org-insert-todo-heading=, =M-S-RET= in Emacs and Doom, ⌘
95 95
96** Dependencies 96** Dependencies
97 97
98Orgstar does not enforce TODO dependencies. =org-enforce-todo-dependencies= and the =ORDERED= and =NOBLOCKING= properties have no effect: a parent can be marked done while its children are open. The property names are offered in completion only so that files written for Emacs keep working there. 98With =org-enforce-todo-dependencies= on, an entry can't move to a done keyword while one of its descendants has an active keyword. Descendants without a keyword don't count. Orgstar refuses the change with Org's message:
99
100#+BEGIN_EXAMPLE
101TODO state change from TODO to DONE blocked (by "TODO Pack the kitchen")
102#+END_EXAMPLE
103
104The =ORDERED= property adds an order. Under a parent with =:ORDERED: t=, an entry can't be done while any heading between the parent and the entry has an active keyword: an earlier sibling, or an open child of one. The order also reaches down. When an entry with an active keyword is held back this way, its descendants can't be done either, as long as every heading between them has an active keyword too. =ORDERED= counts only on the direct parent, not inherited from higher up or from =#+PROPERTY=. Any value but =nil= turns it on, including an empty one.
105
106#+BEGIN_SRC org
107,* Move house
108:PROPERTIES:
109:ORDERED: t
110:END:
111,** TODO Hire a van
112,** TODO Pack the kitchen
113#+END_SRC
114
115Here Pack the kitchen can't be done until Hire a van is. Toggle ORDERED Property (=org-toggle-ordered-property=, =C-c C-x o= in Emacs and Doom) adds =:ORDERED: t= to the entry's property drawer, or removes the property when it is set.
116
117With =org-enforce-todo-checkbox-dependencies= on, an entry can't be done while its own text has an unchecked (=[ ]=) or partly checked (=[-]=) checkbox. Checkboxes in child entries and in src, example, export and verse blocks don't count. The message ends in =(by contained checkboxes)=; when both settings would refuse the change, it names the checkboxes.
118
119The check applies to every change to a done keyword: =C-c C-t=, fast selection, =S-<right>= and =S-<left>=, Doom's =RET= and the agenda's TODO actions on both platforms. A blocked repeating entry doesn't repeat. Changes from a done keyword, and changes to an active keyword or to none, are never refused. An entry with a =NOBLOCKING= property (any value but =nil=) is never blocked.
120
121Both settings are off by default, as in Org. They are under Settings ▸ Editing and in =config.toml=. The agenda dims blocked entries; see [[file:07-agenda.org][The agenda]].
99 122
100* Logging state changes 123* Logging state changes
101 124
@@ -313,5 +336,7 @@ The =COOKIE_DATA= property changes what the parent's cookie counts:
313| =org-todo-keywords= | Settings ▸ Editing, =config.toml= | See "The default keywords" above | 336| =org-todo-keywords= | Settings ▸ Editing, =config.toml= | See "The default keywords" above |
314| =org-log-done= | =config.toml= | =nil= | 337| =org-log-done= | =config.toml= | =nil= |
315| =org-log-into-drawer= | =config.toml= | =""= (no drawer) | 338| =org-log-into-drawer= | =config.toml= | =""= (no drawer) |
339| =org-enforce-todo-dependencies= | Settings ▸ Editing, =config.toml= | =false= |
340| =org-enforce-todo-checkbox-dependencies= | Settings ▸ Editing, =config.toml= | =false= |
316| =org-tags-column= | Settings ▸ Editing, =config.toml= | =-77= | 341| =org-tags-column= | Settings ▸ Editing, =config.toml= | =-77= |
317| =org-use-speed-commands= | =config.toml= | =false= | 342| =org-use-speed-commands= | =config.toml= | =false= |
docs/manual/guide/07-agenda.org +2
@@ -153,6 +153,8 @@ Ages and distances are written compactly: days up to 13 (=3d=), then whole weeks
153 153
154The keyword's color comes from =[theme.todo]=, else from its class: =todo-next= for keywords of work under way such as =NEXT= and =STRT=, =todo-waiting= for =WAIT=, =HOLD= and similar, =todo-cancelled= for done keywords such as =KILL= and =CANCELLED=, and otherwise =todo= or =done=. Priorities =A=, =B= and =C= use =priority-a=, =priority-b= and =priority-c=; others use =priority=. A category's dot takes its color from =[theme.category]=, else a hue worked out from the category's name, the same on every device. See [[file:13-configuration.org][Configuration]] for the keyword lists and the theme keys. 154The keyword's color comes from =[theme.todo]=, else from its class: =todo-next= for keywords of work under way such as =NEXT= and =STRT=, =todo-waiting= for =WAIT=, =HOLD= and similar, =todo-cancelled= for done keywords such as =KILL= and =CANCELLED=, and otherwise =todo= or =done=. Priorities =A=, =B= and =C= use =priority-a=, =priority-b= and =priority-c=; others use =priority=. A category's dot takes its color from =[theme.category]=, else a hue worked out from the category's name, the same on every device. See [[file:13-configuration.org][Configuration]] for the keyword lists and the theme keys.
155 155
156With =org-enforce-todo-dependencies= or =org-enforce-todo-checkbox-dependencies= on, an entry that can't be marked done yet is dimmed in every view (=org-agenda-dim-blocked-tasks= =t=). Settings ▸ Agenda ▸ Blocked entries can hide such entries instead (=invisible=), though one held back only by its own checkboxes stays dimmed, or show them as usual (=nil=). Marking a blocked entry done from the agenda is refused with the same message as in the editor; see [[file:05-todos-and-tags.org][TODOs and tags]].
157
156* Calendar events 158* Calendar events
157 159
158The agenda can show events from the system Calendar app (EventKit) among your entries. They are read-only: Orgstar never changes your calendars. It is off by default. To turn it on: 160The agenda can show events from the system Calendar app (EventKit) among your entries. They are read-only: Orgstar never changes your calendars. It is off by default. To turn it on:
docs/manual/guide/13-configuration.org +8
@@ -75,6 +75,8 @@ Automatic saving writes a file one second after you stop typing. The keymap pres
75| Emacs hides emphasis markers (org-hide-emphasis-markers) | On or off | On | =org-hide-emphasis-markers= | 75| Emacs hides emphasis markers (org-hide-emphasis-markers) | On or off | On | =org-hide-emphasis-markers= |
76| Emacs shows entities as characters (org-pretty-entities) | On or off | On | =org-pretty-entities= | 76| Emacs shows entities as characters (org-pretty-entities) | On or off | On | =org-pretty-entities= |
77| Default TODO keywords | Text in =#+TODO= syntax, one sequence per line | See below | =org-todo-keywords= | 77| Default TODO keywords | Text in =#+TODO= syntax, one sequence per line | See below | =org-todo-keywords= |
78| Entries can't be done before their TODO children (org-enforce-todo-dependencies) | On or off | Off | =org-enforce-todo-dependencies= |
79| Entries can't be done with unchecked boxes (org-enforce-todo-checkbox-dependencies) | On or off | Off | =org-enforce-todo-checkbox-dependencies= |
78 80
79The two "Emacs hides" and "Emacs shows" switches describe your Emacs, not Orgstar's display. Tag alignment, table alignment and =M-q= measure text the way your Emacs displays it, so a file edited in both keeps the same layout. Set them to match your Emacs configuration. They also control whether Orgstar hides markers and shows entities when markup is hidden. 81The two "Emacs hides" and "Emacs shows" switches describe your Emacs, not Orgstar's display. Tag alignment, table alignment and =M-q= measure text the way your Emacs displays it, so a file edited in both keeps the same layout. Set them to match your Emacs configuration. They also control whether Orgstar hides markers and shows entities when markup is hidden.
80 82
@@ -84,6 +86,8 @@ The long-lines switch applies to files as they open; View ▸ Truncate or Wrap L
84 86
85Default TODO keywords apply to files without a =#+TODO= line. Orgstar reads them at launch, so a change takes effect after you quit and reopen Orgstar. 87Default TODO keywords apply to files without a =#+TODO= line. Orgstar reads them at launch, so a change takes effect after you quit and reopen Orgstar.
86 88
89The two dependency switches are described under Dependencies in [[file:05-todos-and-tags.org][TODOs and tags]].
90
87** Appearance 91** Appearance
88 92
89| Control | Range | Default | Key | 93| Control | Range | Default | Key |
@@ -105,6 +109,7 @@ See Themes and Fonts below.
105| Agenda starts N days before today | 0 to 14 days before | 3 days | =org-agenda-start-day= | 109| Agenda starts N days before today | 0 to 14 days before | 3 days | =org-agenda-start-day= |
106| Include files in subfolders | On or off | Off | =agenda-include-subfolders= | 110| Include files in subfolders | On or off | Off | =agenda-include-subfolders= |
107| Show days without entries | On or off | Off | =org-agenda-show-all-dates= | 111| Show days without entries | On or off | Off | =org-agenda-show-all-dates= |
112| Blocked entries | Dimmed; Hidden; Shown as usual | Dimmed | =org-agenda-dim-blocked-tasks= |
108| Show events from Calendar | On or off | Off | =calendar-events= | 113| Show events from Calendar | On or off | Off | =calendar-events= |
109| A checkbox for each calendar | On or off | All on | =calendar-event-calendars= | 114| A checkbox for each calendar | On or off | All on | =calendar-event-calendars= |
110| Notify before timed agenda entries | On or off | On | =reminders= | 115| Notify before timed agenda entries | On or off | On | =reminders= |
@@ -179,6 +184,8 @@ A key you remove from the file goes back to its default. A key that isn't in the
179| =org-log-reschedule= | string | ="nil"= | =nil=, =time= or =note=: log changing or removing a =SCHEDULED= date. Mirrors =org-log-reschedule=. | 184| =org-log-reschedule= | string | ="nil"= | =nil=, =time= or =note=: log changing or removing a =SCHEDULED= date. Mirrors =org-log-reschedule=. |
180| =org-log-redeadline= | string | ="nil"= | =nil=, =time= or =note=: log changing or removing a =DEADLINE=. Mirrors =org-log-redeadline=. | 185| =org-log-redeadline= | string | ="nil"= | =nil=, =time= or =note=: log changing or removing a =DEADLINE=. Mirrors =org-log-redeadline=. |
181| =org-log-into-drawer= | string | =""= | The drawer state notes go in. ="LOGBOOK"= is Emacs's =t=; =""= puts them under the heading. Mirrors =org-log-into-drawer=. | 186| =org-log-into-drawer= | string | =""= | The drawer state notes go in. ="LOGBOOK"= is Emacs's =t=; =""= puts them under the heading. Mirrors =org-log-into-drawer=. |
187| =org-enforce-todo-dependencies= | boolean | =false= | An entry can't become done while a descendant has an active TODO keyword, or under an =ORDERED= parent while an earlier sibling has one. Mirrors =org-enforce-todo-dependencies=. |
188| =org-enforce-todo-checkbox-dependencies= | boolean | =false= | An entry can't become done while its text has an unchecked checkbox. Mirrors =org-enforce-todo-checkbox-dependencies=. |
182| =org-startup-indented= | boolean | =true= | Indent bodies under their headings, as =org-indent-mode=. =#+STARTUP: indent= / =noindent= override it. Mirrors =org-startup-indented=. | 189| =org-startup-indented= | boolean | =true= | Indent bodies under their headings, as =org-indent-mode=. =#+STARTUP: indent= / =noindent= override it. Mirrors =org-startup-indented=. |
183| =org-hide-leading-stars= | boolean | =false= | Show only a heading's last star, without indentation. =#+STARTUP: hidestars= / =showstars= override it. Mirrors =org-hide-leading-stars=. | 190| =org-hide-leading-stars= | boolean | =false= | Show only a heading's last star, without indentation. =#+STARTUP: hidestars= / =showstars= override it. Mirrors =org-hide-leading-stars=. |
184| =org-startup-align-all-tables= | boolean | =false= | Align every table when a file opens. =#+STARTUP: align= / =noalign= override it. Mirrors =org-startup-align-all-tables=. | 191| =org-startup-align-all-tables= | boolean | =false= | Align every table when a file opens. =#+STARTUP: align= / =noalign= override it. Mirrors =org-startup-align-all-tables=. |
@@ -197,6 +204,7 @@ A key you remove from the file goes back to its default. A key that isn't in the
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=. | 204| =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=. | 205| =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=. | 206| =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=. |
207| =org-agenda-dim-blocked-tasks= | string | ="t"= | =t= dims entries the two enforce settings block, =invisible= hides them (entries blocked only by checkboxes are still dimmed), =nil= shows them as usual. Mirrors =org-agenda-dim-blocked-tasks=. |
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=. | 208| =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=. |
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=. | 209| =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=. |
202| =org-clock-history-length= | integer | =5= | How many recently clocked entries Orgstar remembers. Mirrors =org-clock-history-length=. | 210| =org-clock-history-length= | integer | =5= | How many recently clocked entries Orgstar remembers. Mirrors =org-clock-history-length=. |
docs/manual/guide/14-ios.org +1
@@ -329,6 +329,7 @@ These settings from =config.toml= apply on iOS:
329- =show-markup=, =org-startup-indented=, =org-hide-leading-stars=, =org-startup-with-inline-images=, =org-startup-align-all-tables=, =org-startup-truncated=, =org-cycle-hide-drawer-startup=, =org-cycle-hide-block-startup= 329- =show-markup=, =org-startup-indented=, =org-hide-leading-stars=, =org-startup-with-inline-images=, =org-startup-align-all-tables=, =org-startup-truncated=, =org-cycle-hide-drawer-startup=, =org-cycle-hide-block-startup=
330- =[theme]= (=font=, =font-size=, =line-spacing=, =heading-size-step=, =theme-file= and colours), =[theme.light]=, =[theme.dark]=, =[theme.todo]=, =[theme.category]= 330- =[theme]= (=font=, =font-size=, =line-spacing=, =heading-size-step=, =theme-file= and colours), =[theme.light]=, =[theme.dark]=, =[theme.todo]=, =[theme.category]=
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-enforce-todo-dependencies=, =org-enforce-todo-checkbox-dependencies=, =org-agenda-dim-blocked-tasks=
332- =org-use-speed-commands=, with a hardware keyboard 333- =org-use-speed-commands=, with a hardware keyboard
333- =electric-pair-mode=, =spell-check= 334- =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=, =org-agenda-search-view-always-boolean=, =org-agenda-search-view-force-full-words= 335- =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=
docs/manual/guide/15-alongside-emacs.org +2
@@ -247,3 +247,5 @@ Other:
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. 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.
250- =org-track-ordered-property-with-tag= isn't supported; Toggle ORDERED Property sets only the property.
251- When TODO dependencies refuse a change on a =COMMENT= heading, the heading keeps =COMMENT=. Org removes it before the check and doesn't put it back.