Commit 191ddb506f
Verified · cmc
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 | ||
| 57 | public struct AgendaDay: Sendable, Equatable { | 61 | public 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 @@ | |||
| 1 | import 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 | |||
| 8 | extension 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`. | ||
| 138 | public 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 @@ | |||
| 1 | import Foundation | ||
| 2 | import Testing | ||
| 3 | @testable import OrgCore | ||
| 4 | |||
| 5 | struct 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 | ||
| 98 | Orgstar 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. | 98 | With =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 | ||
| 101 | TODO state change from TODO to DONE blocked (by "TODO Pack the kitchen") | ||
| 102 | #+END_EXAMPLE | ||
| 103 | |||
| 104 | The =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 | |||
| 115 | Here 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 | |||
| 117 | With =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 | |||
| 119 | The 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 | |||
| 121 | Both 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 | ||
| 154 | The 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. | 154 | The 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 | ||
| 156 | With =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 | ||
| 158 | The 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: | 160 | The 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 | ||
| 79 | The 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. | 81 | The 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 | ||
| 85 | Default 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. | 87 | Default 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 | ||
| 89 | The 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. | ||