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 {
476476 add(name, .integer(n))
477477 case "org-insert-heading-respect-content", "org-list-allow-alphabetical", "org-hide-emphasis-markers", "org-pretty-entities",
478478 "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":
480481 add(name, .bool(truthy))
482 case "org-agenda-dim-blocked-tasks":
483 add(name, .string(value.symbol == "invisible" ? "invisible" : truthy ? "t" : "nil"))
481484 case "org-hide-drawer-startup", "org-hide-block-startup":
482485 add(name == "org-hide-drawer-startup" ? "org-cycle-hide-drawer-startup" : "org-cycle-hide-block-startup", .bool(truthy))
483486 case "org-M-RET-may-split-line":
Sources/OrgApp/OrgPreferences.swift +6 −1
@@ -31,6 +31,8 @@ public enum OrgPreferences {
3131 spellCheck: bool("spellCheck", defaults)
3232 )
3333 options.electricPair = bool("electricPair", defaults)
34 options.enforceTodoDependencies = bool("enforceTodoDependencies", defaults)
35 options.enforceTodoCheckboxDependencies = bool("enforceTodoCheckboxDependencies", defaults)
3436 return options
3537 }
3638
@@ -56,13 +58,16 @@ public enum OrgPreferences {
5658 public static func agendaSkipDeadlineIfDone(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSkipDeadlineIfDone", defaults) }
5759 public static func agendaSearchAlwaysBoolean(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSearchAlwaysBoolean", defaults) }
5860 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.
6062 public static func agendaOptions(_ options: AgendaOptions, _ defaults: UserDefaults = .standard) -> AgendaOptions {
6163 var options = options
6264 options.skipScheduledIfDone = agendaSkipScheduledIfDone(defaults)
6365 options.skipDeadlineIfDone = agendaSkipDeadlineIfDone(defaults)
6466 options.searchAlwaysBoolean = agendaSearchAlwaysBoolean(defaults)
6567 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
6671 return options
6772 }
6873 /// `calendar-events`: Calendar's events show in the agenda.
Sources/OrgApp/UserConfig.swift +7
@@ -115,6 +115,10 @@ public enum UserConfig {
115115 fallback: .string("nil"), comment: "nil, time or note: log changing or removing a DEADLINE", legacy: ""),
116116 Setting(section: "", key: "org-log-into-drawer", defaultsKey: "logIntoDrawer", kind: .string, fallback: .string(""),
117117 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: ""),
118122 Setting(section: "", key: "org-startup-indented", defaultsKey: "startupIndented", kind: .bool, fallback: .bool(true),
119123 comment: "indent bodies under their headings (org-indent-mode); #+STARTUP: (no)indent overrides", legacy: ""),
120124 Setting(section: "", key: "org-hide-leading-stars", defaultsKey: "hideLeadingStars", kind: .bool, fallback: .bool(false),
@@ -151,6 +155,9 @@ public enum UserConfig {
151155 comment: "search strings are always +word -word {regexp} snippets, never a phrase", legacy: ""),
152156 Setting(section: "", key: "org-agenda-search-view-force-full-words", defaultsKey: "agendaSearchForceFullWords", kind: .bool, fallback: .bool(false),
153157 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: ""),
154161 Setting(section: "", key: "appt-message-warning-time", defaultsKey: "reminderLead", kind: .integer, fallback: .integer(12),
155162 comment: "minutes of warning before timed entries; APPT_WARNTIME overrides", legacy: "agenda.reminder-lead"),
156163 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 {
5252 /// Deadlines and scheduled items: the day (as `Days` numbers it) of the timestamp the item
5353 /// is for, the repeater's occurrence when the item shows on it.
5454 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
5559}
5660
5761public struct AgendaDay: Sendable, Equatable {
@@ -84,6 +88,16 @@ public struct AgendaOptions: Sendable, Equatable {
8488 public var searchAlwaysBoolean = false
8589 /// `org-agenda-search-view-force-full-words`: search snippets match whole words only.
8690 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 }
87101
88102 public init() {}
89103
@@ -114,7 +128,7 @@ public enum Agenda {
114128 if (isToday || days == 1), items.contains(where: { $0.timeOfDay != nil }) {
115129 items = grid(day: day, isToday: isToday, now: now, options: options) + items
116130 }
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)))
118132 }
119133 }
120134
@@ -387,13 +401,30 @@ public enum Agenda {
387401 ])
388402 txt = highlightTodo(txt, keywords: source.keywords)
389403 let priority = Self.priority(prefix + txt, source.priorities)
390 return AgendaItem(
404 var item = AgendaItem(
391405 kind: kind, path: source.path, headingOffset: attached ? heading.start : nil, headingLine: attached ? heading.line : nil, markerOffset: marker, category: category,
392406 timeOfDay: timed.timeOfDay, time: timed.time, extra: extra, text: txt, tags: tags.map(\.name),
393407 todo: heading.todo, isDone: heading.isDone, urgency: urgency(priority),
394408 warntime: heading.properties["APPT_WARNTIME"].flatMap { Int($0.trimmingCharacters(in: .whitespaces)) }, habit: habit,
395409 line: prefix + txt + (tagString.isEmpty ? "" : " " + tagString), prefix: prefix, effort: heading.properties["EFFORT"]
396410 )
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 }
397428 }
398429
399430 /// `%b`: the outline path above the heading, each title followed by `->`.
Sources/OrgCore/Agenda/AgendaSearch.swift +2 −2
@@ -20,7 +20,7 @@ extension Agenda {
2020 ))
2121 }
2222 }
23 return sortByUrgency(items)
23 return sortByUrgency(markBlocked(items, options: options))
2424 }
2525
2626 /// `org-tags-view`: entries matching a tags/property/TODO match string. `todoOnly` keeps
@@ -41,7 +41,7 @@ extension Agenda {
4141 ))
4242 }
4343 }
44 return sortByUrgency(items)
44 return sortByUrgency(markBlocked(items, options: options))
4545 }
4646
4747 /// `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 {
4141 let isHabit: Bool
4242 /// `org-habit-parse-todo`, nil where it signals an error.
4343 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
4448 }
4549
4650 struct Stamp: Sendable {
@@ -170,6 +174,9 @@ public struct AgendaSource: Sendable {
170174 // Archived and commented trees are skipped whole, as `org-agenda-skip` does.
171175 var skipped: [Bool] = []
172176 var headings: [Heading] = []
177 var options = EditingOptions.org
178 options.listAllowAlphabetical = settings.listAllowAlphabetical
179 let buffer = EmacsBuffer(text, point: 0, settings: settings, options: options)
173180 for (index, info) in model.headings.enumerated() {
174181 let own = info.localTags.contains("ARCHIVE") || Self.isCommented(info.title)
175182 skipped.append(fileArchived || own || (info.parent.map { skipped[$0] } ?? false))
@@ -227,13 +234,21 @@ public struct AgendaSource: Sendable {
227234 let entry = ns.substring(with: NSRange(location: info.headingRange.lowerBound, length: end - info.headingRange.lowerBound))
228235 habit = Habit.parse(entry: entry, scheduled: scheduled.0, doneKeywords: settings.todoSequences.flatMap { $0.done.map(\.name) })
229236 }
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 }
230244 headings.append(Heading(
231245 start: info.headingRange.lowerBound, level: info.level, skipped: skipped[index],
232246 title: info.title.trimmingCharacters(in: .whitespaces), priority: info.priority, localTags: info.localTags,
233247 properties: properties, planning: planningValues,
234248 firstActive: ownStamps.first { $0.hasPrefix("<") }, firstInactive: ownStamps.first { $0.hasPrefix("[") },
235249 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
237252 ))
238253 }
239254 self.headings = headings
Sources/OrgCore/Commands/Command.swift +6 −1
@@ -22,6 +22,11 @@ public struct EditingOptions: Sendable, Equatable, Codable {
2222 public var logRedeadline: LogHow?
2323 /// `org-use-fast-todo-selection` `auto`: with keys in the keywords, C-c C-t asks for one.
2424 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
2530 /// `org-use-speed-commands`: single keys at the start of a heading line run commands.
2631 public var useSpeedCommands: Bool
2732 /// `org-startup-with-inline-images`.
@@ -277,7 +282,7 @@ public enum Commands {
277282 ShiftTimestamp(up: true), ShiftTimestamp(up: false), ShiftTimestampDay(later: true), ShiftTimestampDay(later: false),
278283 OpenLinkAtPoint(), CreateID(), ToggleComment(), ToggleHeading(), ToggleItem(), MarkSubtree(),
279284 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(),
281286 ]
282287
283288 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 {
3333 if settings.isDone(current) {
3434 buffer.setTodoKeyword(sequence.active.first?.name)
3535 } else if let done = sequence.done.first?.name {
36 try buffer.checkTodoBlocking(from: current, to: done)
3637 buffer.setTodoKeyword(done)
3738 }
3839 try buffer.afterTodoChange(previous: current, answers: context.answers)
Sources/OrgCore/Commands/HeadingCommands.swift +4 −1
@@ -210,7 +210,8 @@ public struct TodoCycle: OrgCommand {
210210 /// `org-todo`, then the parent's TODO statistics, and the repeat of a repeating entry
211211 /// that became done.
212212 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
214215 if context.options.fastTodoSelection, entryHeading(at: context.caret, in: context.tree) != nil,
215216 let fast = FastSelection.todo(context.tree.settings) {
216217 guard let answer = context.answers["todo"] else {
@@ -219,6 +220,8 @@ public struct TodoCycle: OrgCommand {
219220 guard let key = answer.first, let keyword = fast.keyword(for: key, current: previous) else { return .failed("Quit") }
220221 return SetTodo(keyword: keyword).run(in: context)
221222 }
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) }
222225 return followUp(context, cycle(in: context)) { buffer in
223226 try buffer.saveExcursion {
224227 buffer.updateParentTodoStatistics()
Sources/OrgCore/Commands/Properties.swift +1
@@ -36,6 +36,7 @@ public struct SetTodo: OrgCommand {
3636 return commitBuffer(context) { buffer in
3737 try buffer.saveExcursion {
3838 let previous = buffer.todoKeyword
39 try buffer.checkTodoBlocking(from: previous, to: keyword)
3940 buffer.setTodoKeyword(keyword)
4041 try buffer.afterTodoChange(previous: previous, answers: context.answers)
4142 }
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 {
761761 keys = "C-c C-x A"
762762 command = "org.archive.to-sibling"
763763 [[bind]]
764 keys = "C-c C-x o"
765 command = "org.property.toggle-ordered"
766 [[bind]]
764767 keys = "C-c C-x C-v"
765768 command = "org.toggle-inline-images"
766769 [[bind]]
Sources/OrgPresentation/AgendaRows.swift +4
@@ -73,6 +73,8 @@ public struct AgendaRow: Sendable, Equatable, Identifiable {
7373 public let tags: [String]
7474 public let todo: String?
7575 public let isDone: Bool
76 /// Blocked from becoming done (`org-agenda-dim-blocked-tasks`): shown dimmed.
77 public let isBlocked: Bool
7678 public let priority: String?
7779 /// The statistics cookie: `1/3`, `50%`.
7880 public let progress: String?
@@ -146,6 +148,7 @@ public struct AgendaRow: Sendable, Equatable, Identifiable {
146148 self.tags = item.tags
147149 self.todo = item.todo
148150 self.isDone = item.isDone
151 self.isBlocked = item.isBlocked
149152 self.priority = item.priorityLetter
150153 self.progress = item.progress
151154 self.detail = item.kind == .clock || item.kind == .state ? Self.logDetail(item.extra) : nil
@@ -185,6 +188,7 @@ public struct AgendaRow: Sendable, Equatable, Identifiable {
185188 self.tags = []
186189 self.todo = nil
187190 self.isDone = false
191 self.isBlocked = false
188192 self.priority = nil
189193 self.progress = nil
190194 self.detail = nil
Sources/Orgstar/AgendaView.swift +2
@@ -1096,6 +1096,8 @@ struct AgendaRowView: View {
10961096 .padding(.horizontal, 16)
10971097 .padding(.vertical, 6)
10981098 .frame(minHeight: 44)
1099 // `org-agenda-dim-blocked-tasks`.
1100 .opacity(row.isBlocked ? 0.5 : 1)
10991101 .background {
11001102 RoundedRectangle(cornerRadius: 6)
11011103 .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 {
4949 @AppStorage("electricPair") private var electricPair = true
5050 @AppStorage("hideDrawerStartup") private var hideDrawerStartup = true
5151 @AppStorage("hideBlockStartup") private var hideBlockStartup = false
52 @AppStorage("enforceTodoDependencies") private var enforceTodoDependencies = false
53 @AppStorage("enforceTodoCheckboxDependencies") private var enforceTodoCheckboxDependencies = false
5254
5355 private var options: EditingOptions {
5456 Preferences.options(
@@ -57,7 +59,8 @@ struct ContentView: View {
5759 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, logReschedule: logReschedule, logRedeadline: logRedeadline,
5860 useSpeedCommands: useSpeedCommands, startupWithInlineImages: startupWithInlineImages,
5961 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
6164 )
6265 }
6366 /// A block being edited apart (`org-edit-special`).
Sources/Orgstar/OrgstarApp.swift +16 −1
@@ -517,10 +517,13 @@ struct SettingsView: View {
517517 @AppStorage("prettyEntities") private var prettyEntities = true
518518 @AppStorage("startupTruncated") private var startupTruncated = false
519519 @AppStorage("todoKeywords") private var todoKeywords = Preferences.defaultTodoKeywords
520 @AppStorage("enforceTodoDependencies") private var enforceTodoDependencies = false
521 @AppStorage("enforceTodoCheckboxDependencies") private var enforceTodoCheckboxDependencies = false
520522 @AppStorage("agendaSpan") private var agendaSpan = 10
521523 @AppStorage("agendaStartOffset") private var agendaStartOffset = -3
522524 @AppStorage("agendaSubfolders") private var agendaSubfolders = false
523525 @AppStorage("agendaShowAllDates") private var agendaShowAllDates = false
526 @AppStorage("agendaDimBlockedTasks") private var agendaDimBlockedTasks = "t"
524527 @AppStorage("calendarEvents") private var calendarEvents = false
525528 @AppStorage("calendarEventCalendars") private var calendarEventCalendars = ""
526529 @State private var calendars: [(id: String, title: String, color: RGBA?)] = []
@@ -634,9 +637,16 @@ struct SettingsView: View {
634637 Text("For files without #+TODO, in #+TODO syntax, one sequence per line. Takes effect when Orgstar restarts.")
635638 .settingsFooter()
636639 }
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 }
637647 }
638648 .formStyle(.grouped)
639 .frame(height: 520)
649 .frame(height: 640)
640650 }
641651
642652 private var appearance: some View {
@@ -681,6 +691,11 @@ struct SettingsView: View {
681691 Stepper(agendaStartOffset == 0 ? "Agenda starts today" : "Agenda starts \(-agendaStartOffset) days before today", value: $agendaStartOffset, in: -14...0)
682692 Toggle("Include files in subfolders", isOn: $agendaSubfolders)
683693 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 }
684699 }
685700 Section {
686701 Toggle("Show events from Calendar", isOn: $calendarEvents)
Sources/Orgstar/Preferences.swift +4 −1
@@ -18,7 +18,8 @@ enum Preferences {
1818 tagsColumn: Int, headingAfterSubtree: Bool, splitLine: Bool, alphabeticalLists: Bool, fillColumn: Int,
1919 hiddenMarkers: Bool, prettyEntities: Bool, logDone: String, logIntoDrawer: String,
2020 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
2223 ) -> EditingOptions {
2324 var options = EditingOptions(
2425 tagsColumn: tagsColumn, insertHeadingRespectContent: headingAfterSubtree,
@@ -32,6 +33,8 @@ enum Preferences {
3233 startupTruncated: startupTruncated, spellCheck: spellCheck
3334 )
3435 options.electricPair = electricPair
36 options.enforceTodoDependencies = enforceTodoDependencies
37 options.enforceTodoCheckboxDependencies = enforceTodoCheckboxDependencies
3538 return options
3639 }
3740}
Sources/OrgstarMobile/AgendaScreen.swift +2 −1
@@ -620,7 +620,8 @@ struct AgendaRowView: View {
620620 }
621621 }
622622 .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)
624625 }
625626
626627 /// 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
828828| 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=). |
829829| 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=). |
830830| 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=). |
831832| 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=). |
832833| 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=). |
833834| 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, ⌘
9595
9696** Dependencies
9797
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]].
99122
100123* Logging state changes
101124
@@ -313,5 +336,7 @@ The =COOKIE_DATA= property changes what the parent's cookie counts:
313336| =org-todo-keywords= | Settings ▸ Editing, =config.toml= | See "The default keywords" above |
314337| =org-log-done= | =config.toml= | =nil= |
315338| =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= |
316341| =org-tags-column= | Settings ▸ Editing, =config.toml= | =-77= |
317342| =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
153153
154154The 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.
155155
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
156158* Calendar events
157159
158160The 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
7575| Emacs hides emphasis markers (org-hide-emphasis-markers) | On or off | On | =org-hide-emphasis-markers= |
7676| Emacs shows entities as characters (org-pretty-entities) | On or off | On | =org-pretty-entities= |
7777| 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= |
7880
7981The 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.
8082
@@ -84,6 +86,8 @@ The long-lines switch applies to files as they open; View ▸ Truncate or Wrap L
8486
8587Default 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.
8688
89The two dependency switches are described under Dependencies in [[file:05-todos-and-tags.org][TODOs and tags]].
90
8791** Appearance
8892
8993| Control | Range | Default | Key |
@@ -105,6 +109,7 @@ See Themes and Fonts below.
105109| Agenda starts N days before today | 0 to 14 days before | 3 days | =org-agenda-start-day= |
106110| Include files in subfolders | On or off | Off | =agenda-include-subfolders= |
107111| 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= |
108113| Show events from Calendar | On or off | Off | =calendar-events= |
109114| A checkbox for each calendar | On or off | All on | =calendar-event-calendars= |
110115| 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
179184| =org-log-reschedule= | string | ="nil"= | =nil=, =time= or =note=: log changing or removing a =SCHEDULED= date. Mirrors =org-log-reschedule=. |
180185| =org-log-redeadline= | string | ="nil"= | =nil=, =time= or =note=: log changing or removing a =DEADLINE=. Mirrors =org-log-redeadline=. |
181186| =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=. |
182189| =org-startup-indented= | boolean | =true= | Indent bodies under their headings, as =org-indent-mode=. =#+STARTUP: indent= / =noindent= override it. Mirrors =org-startup-indented=. |
183190| =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=. |
184191| =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
197204| =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=. |
198205| =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=. |
199206| =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=. |
200208| =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=. |
201209| =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=. |
202210| =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:
329329- =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=
330330- =[theme]= (=font=, =font-size=, =line-spacing=, =heading-size-step=, =theme-file= and colours), =[theme.light]=, =[theme.dark]=, =[theme.todo]=, =[theme.category]=
331331- =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=
332333- =org-use-speed-commands=, with a hardware keyboard
333334- =electric-pair-mode=, =spell-check=
334335- =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:
247247- Most =#+STARTUP= options for footnotes, entities, LaTeX previews, numbering and odd levels are accepted and ignored; see [[file:13-configuration.org][Configuration]].
248248- =display-line-numbers-type= has no relative or visual mode.
249249- 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.