From 4b5eefb65d79199f54aa12d20c98255723eb274f Mon Sep 17 00:00:00 2001 From: ashprice Date: Wed, 17 Jun 2026 21:27:26 +0100 Subject: [PATCH 01/49] Use the new dependency map for getDependencyTasks()/getBlockedTasks() Signed-off-by: ashprice --- src/Task.cpp | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/Task.cpp b/src/Task.cpp index 6608db850..aa1e78a22 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -1057,41 +1057,52 @@ std::vector Task::getDependencyIDs() const { //////////////////////////////////////////////////////////////////////////////// std::vector Task::getDependencyUUIDs() const { std::vector uuids; - for (auto& attr : all()) { - if (!isDepAttr(attr)) continue; - auto dep = attr2Dep(attr); - uuids.push_back(dep); + for (auto& pair : data) { + if (!isDepAttr(pair.first)) continue; + uuids.push_back(attr2Dep(pair.first)); } return uuids; } //////////////////////////////////////////////////////////////////////////////// +// Uses the cached dependency map instead of scanning the vector. std::vector Task::getDependencyTasks() const { - auto uuids = getDependencyUUIDs(); + const auto& uuid = get_ref("uuid"); - // NOTE: this may seem inefficient, but note that `TDB2::get` performs a - // linear search on each invocation, so scanning *once* is quite a bit more - // efficient. std::vector blocking; - if (uuids.size() > 0) - for (auto& it : Context::getContext().tdb2.pending_tasks()) - if (it.getStatus() != Task::completed && it.getStatus() != Task::deleted && - std::find(uuids.begin(), uuids.end(), it.get("uuid")) != uuids.end()) - blocking.push_back(it); + + auto& graph = Context::getContext().tdb2.dependency_graph(); + auto found = graph.dependencies.find(uuid); + + if (found == graph.dependencies.end()) return blocking; + + blocking.reserve(found->second.size()); + + const auto& tasks = Context::getContext().tdb2.pending_tasks(); + for (auto idx : found->second) + if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) + blocking.push_back(tasks[idx]); return blocking; } //////////////////////////////////////////////////////////////////////////////// std::vector Task::getBlockedTasks() const { - auto uuid = get("uuid"); + const auto& uuid = get_ref("uuid"); std::vector blocked; - for (auto& it : Context::getContext().tdb2.pending_tasks()) - if (it.getStatus() != Task::completed && it.getStatus() != Task::deleted && - it.hasDependency(uuid)) - blocked.push_back(it); + auto& graph = Context::getContext().tdb2.dependency_graph(); + auto found = graph.dependents.find(uuid); + + if (found == graph.dependents.end()) return blocked; + + blocked.reserve(found->second.size()); + + const auto& tasks = Context::getContext().tdb2.pending_tasks(); + for (auto idx : found->second) + if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) + blocked.push_back(tasks[idx]); return blocked; } From a9a92a95099ea30607fca688b419071373c7b373 Mon Sep 17 00:00:00 2001 From: ashprice Date: Wed, 17 Jun 2026 22:07:25 +0100 Subject: [PATCH 02/49] Use the pending tasks cache in the circular dependency check. Signed-off-by: ashprice Comment. Signed-off-by: ashprice --- src/dependency.cpp | 60 ++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/src/dependency.cpp b/src/dependency.cpp index 380c471cb..affc4bb5a 100644 --- a/src/dependency.cpp +++ b/src/dependency.cpp @@ -42,39 +42,35 @@ bool dependencyIsCircular(const Task& task) { // A new task has no UUID assigned yet, and therefore cannot be part of any // dependency chain. - if (task.has("uuid")) { - auto task_uuid = task.get("uuid"); - - std::stack s; - s.push(task); - - std::unordered_set visited; - visited.insert(task_uuid); - - while (!s.empty()) { - Task& current = s.top(); - auto deps_current = current.getDependencyUUIDs(); - - // This is a basic depth first search that always terminates given the - // fact that we do not visit any task twice - for (const auto& dep : deps_current) { - if (Context::getContext().tdb2.get(dep, current)) { - auto current_uuid = current.get("uuid"); - - if (task_uuid == current_uuid) { - // Cycle found, initial task reached for the second time! - return true; - } - - if (visited.find(current_uuid) == visited.end()) { - // Push the task to the stack, if it has not been processed yet - s.push(current); - visited.insert(current_uuid); - } - } - } + if (!task.has("uuid")) return false; + + const auto& task_uuid = task.get_ref("uuid"); + auto& tdb2 = Context::getContext().tdb2; + + std::unordered_set visited{task_uuid}; + + // Vector of dependency UUIDs. The initial set is taken from the task object, + // because there may have been a dependency added which is not yet in the cache. + // Subsequent searches use the _pending_tasks cache. + // This will always terminate as we do not return any UUID twice. + std::vector to_visit; + for (const auto& dep : task.getDependencyUUIDs()) { + if (dep == task_uuid) return true; + if (visited.insert(dep).second) to_visit.push_back(dep); + } + + while (!to_visit.empty()) { + std::string dep_uuid = std::move(to_visit.back()); + to_visit.pop_back(); + + auto* dep_task = tdb2.find_pending(dep_uuid); + if (!dep_task) continue; + + const auto& deps = dep_task->getDependencyUUIDs(); - s.pop(); + for (const auto& dep : deps) { + if (dep == task_uuid) return true; + if (visited.insert(dep).second) to_visit.push_back(dep); } } From 9c70907543a86dda5918f1640b97e700b64c78b2 Mon Sep 17 00:00:00 2001 From: ashprice Date: Thu, 18 Jun 2026 00:54:24 +0100 Subject: [PATCH 03/49] Use is_blocked for rendering report columns instead of copying whole objects. Signed-off-by: ashprice Derp. Signed-off-by: ashprice --- src/columns/ColDepends.cpp | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/columns/ColDepends.cpp b/src/columns/ColDepends.cpp index 446737d6d..73d190373 100644 --- a/src/columns/ColDepends.cpp +++ b/src/columns/ColDepends.cpp @@ -65,18 +65,22 @@ void ColumnDepends::setStyle(const std::string& value) { // Set the minimum and maximum widths for the value. void ColumnDepends::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; - auto deptasks = task.getDependencyTasks(); - if (deptasks.size() > 0) { - if (_style == "indicator") { + if (_style == "indicator") { + // We only need to know if the task has a dependency. We don't have to + // look at the whole list. The flags are set during cache construction. + if (task.is_blocked) { minimum = maximum = utf8_width(Context::getContext().config.get("dependency.indicator")); } + return; + } - else if (_style == "count") { - minimum = maximum = 2 + format((int)deptasks.size()).length(); - } + auto deptasks = task.getDependencyTasks(); - else if (_style == "default" || _style == "list") { + if (deptasks.size() > 0) { + if (_style == "count") { + minimum = maximum = 2 + format((int)deptasks.size()).length(); + } else if (_style == "default" || _style == "list") { minimum = maximum = 0; std::vector blocking_ids; @@ -97,19 +101,21 @@ void ColumnDepends::measure(Task& task, unsigned int& minimum, unsigned int& max //////////////////////////////////////////////////////////////////////////////// void ColumnDepends::render(std::vector& lines, Task& task, int width, Color& color) { - auto deptasks = task.getDependencyTasks(); - - if (deptasks.size() > 0) { - if (_style == "indicator") { + // We only need to know if the task has a dependency. We don't have to + // look at the whole list. The flags are set during cache construction. + if (_style == "indicator") { + if (task.is_blocked) renderStringRight(lines, width, color, Context::getContext().config.get("dependency.indicator")); - } + return; + } - else if (_style == "count") { - renderStringRight(lines, width, color, '[' + format(static_cast(deptasks.size())) + ']'); - } + auto deptasks = task.getDependencyTasks(); - else if (_style == "default" || _style == "list") { + if (deptasks.size() > 0) { + if (_style == "count") { + renderStringRight(lines, width, color, '[' + format(static_cast(deptasks.size())) + ']'); + } else if (_style == "default" || _style == "list") { std::vector blocking_ids; blocking_ids.reserve(deptasks.size()); for (const auto& t : deptasks) blocking_ids.push_back(t.id); From 6f340302d2a268f3426f91965a98d4b414196ac4 Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 19 Jun 2026 12:57:46 +0100 Subject: [PATCH 04/49] Use getDependencyUUIDs() to avoid copying full objects from the cache in colDepends. Signed-off-by: ashprice --- src/columns/ColDepends.cpp | 43 ++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/columns/ColDepends.cpp b/src/columns/ColDepends.cpp index 73d190373..83e7b4a06 100644 --- a/src/columns/ColDepends.cpp +++ b/src/columns/ColDepends.cpp @@ -75,24 +75,31 @@ void ColumnDepends::measure(Task& task, unsigned int& minimum, unsigned int& max return; } - auto deptasks = task.getDependencyTasks(); + // We also don't need to call getDependencyTasks() anymore, + // which would copy the full objects from the cache just to read the ID + // field. Instead, we can use getDependencyUUIDs() and tdb2.id(). - if (deptasks.size() > 0) { + auto dep_uuids = task.getDependencyUUIDs(); + + std::vector blocking_ids; + blocking_ids.reserve(dep_uuids.size()); + for (const auto& uuid : dep_uuids) { + int id = Context::getContext().tdb2.id(uuid); + if (id > 0) blocking_ids.push_back(id); + } + + if (blocking_ids.size() > 0) { if (_style == "count") { - minimum = maximum = 2 + format((int)deptasks.size()).length(); + minimum = maximum = 2 + format((int)blocking_ids.size()).length(); } else if (_style == "default" || _style == "list") { minimum = maximum = 0; - std::vector blocking_ids; - blocking_ids.reserve(deptasks.size()); - for (auto& i : deptasks) blocking_ids.push_back(i.id); - auto all = join(" ", blocking_ids); maximum = all.length(); unsigned int length; - for (auto& i : deptasks) { - length = format(i.id).length(); + for (auto& id : blocking_ids) { + length = format(id).length(); if (length > minimum) minimum = length; } } @@ -110,16 +117,20 @@ void ColumnDepends::render(std::vector& lines, Task& task, int widt return; } - auto deptasks = task.getDependencyTasks(); + // We use the same approach to look up UUIDs as for measure(). + auto dep_uuids = task.getDependencyUUIDs(); - if (deptasks.size() > 0) { + std::vector blocking_ids; + blocking_ids.reserve(dep_uuids.size()); + for (const auto& uuid : dep_uuids) { + int id = Context::getContext().tdb2.id(uuid); + if (id > 0) blocking_ids.push_back(id); + } + + if (blocking_ids.size() > 0) { if (_style == "count") { - renderStringRight(lines, width, color, '[' + format(static_cast(deptasks.size())) + ']'); + renderStringRight(lines, width, color, '[' + format(static_cast(blocking_ids.size())) + ']'); } else if (_style == "default" || _style == "list") { - std::vector blocking_ids; - blocking_ids.reserve(deptasks.size()); - for (const auto& t : deptasks) blocking_ids.push_back(t.id); - auto combined = join(" ", blocking_ids); std::vector all; From 14d7f703fe3e18d96db11c79c46d8971b9e113c3 Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 19 Jun 2026 13:02:14 +0100 Subject: [PATCH 05/49] Use get_ref in ColDepends::modify() Signed-off-by: ashprice --- src/columns/ColDepends.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/columns/ColDepends.cpp b/src/columns/ColDepends.cpp index 83e7b4a06..466285014 100644 --- a/src/columns/ColDepends.cpp +++ b/src/columns/ColDepends.cpp @@ -129,7 +129,8 @@ void ColumnDepends::render(std::vector& lines, Task& task, int widt if (blocking_ids.size() > 0) { if (_style == "count") { - renderStringRight(lines, width, color, '[' + format(static_cast(blocking_ids.size())) + ']'); + renderStringRight(lines, width, color, + '[' + format(static_cast(blocking_ids.size())) + ']'); } else if (_style == "default" || _style == "list") { auto combined = join(" ", blocking_ids); @@ -169,9 +170,9 @@ void ColumnDepends::modify(Task& task, const std::string& value) { Task loaded_task; if (Context::getContext().tdb2.get(dep, loaded_task)) if (removal) - task.removeDependency(loaded_task.get("uuid")); + task.removeDependency(loaded_task.get_ref("uuid")); else - task.addDependency(loaded_task.get("uuid")); + task.addDependency(loaded_task.get_ref("uuid")); else throw format("Dependency could not be set - task with UUID '{1}' does not exist.", dep); } From cbe954065b2159b7fb0dbf1435f8d3cf1a3b2f8d Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 19 Jun 2026 14:09:36 +0100 Subject: [PATCH 06/49] Feat: Pre-parse urgency coefficients for reports. Signed-off-by: ashprice --- src/Context.cpp | 4 ++ src/Task.cpp | 158 +++++++++++++++++++++++++++++------------------- src/Task.h | 23 ++++++- 3 files changed, 121 insertions(+), 64 deletions(-) diff --git a/src/Context.cpp b/src/Context.cpp index 7f5a482b3..155674b8d 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -1101,6 +1101,10 @@ void Context::staticInitialization() { for (auto& var : config.all()) if (var.substr(0, 13) == "urgency.user." || var.substr(0, 12) == "urgency.uda.") Task::coefficients[var] = config.getReal(var); + + // Pre-parse the coefficient keys, so urgency_c() doesn't have to re-parse for + // each task. + Task::setUrgencyCoefficients(); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/Task.cpp b/src/Task.cpp index aa1e78a22..d3a5ee6f2 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -74,6 +74,8 @@ bool Task::regex = false; std::map Task::attributes; std::map Task::coefficients; +std::vector Task::userCoefficients; +bool Task::urgencyInherit = false; float Task::urgencyProjectCoefficient = 0.0; float Task::urgencyActiveCoefficient = 0.0; float Task::urgencyScheduledCoefficient = 0.0; @@ -191,9 +193,9 @@ const std::string Task::identifier(bool shortened /* = false */) const { if (id != 0) return format(id); else if (shortened) - return get("uuid").substr(0, 8); + return get_ref("uuid").substr(0, 8); else - return get("uuid"); + return get_ref("uuid"); } //////////////////////////////////////////////////////////////////////////////// @@ -721,14 +723,13 @@ void Task::parseJSON(const json::object* root_obj) { void Task::parseTC(rust::Box task) { auto items = task->items(); data.clear(); - for (auto& item : items) { - data[static_cast(item.prop)] = static_cast(item.value); - } // count annotations annotation_count = 0; - for (auto i : data) { - if (isAnnotationAttr(i.first)) { + for (auto& item : items) { + const auto& key = static_cast(item.prop); + data[key] = static_cast(item.value); + if (isAnnotationAttr(key)) { ++annotation_count; } } @@ -1703,6 +1704,46 @@ int Task::determineVersion(const std::string& line) { return 0; } +///////////////////////////////////////////////////////////////////////////////// +// Pre-parse the user's UDA/urgency coefficient keys once, instead of reparsing +// for every task in the report. The keys are static. +void Task::setUrgencyCoefficients() { + urgencyInherit = Context::getContext().config.getBoolean("urgency.inherit"); + + userCoefficients.clear(); + userCoefficients.reserve(coefficients.size()); + + for (const auto& var : coefficients) { + const std::string& key = var.first; + float coeff = var.second; + if (fabs(coeff) <= epsilon) continue; + + auto end = key.find(".coefficient"); + if (end == std::string::npos) continue; + + if (!key.compare(0, 13, "urgency.user.", 13)) { + if (!key.compare(13, 8, "project.", 8)) + userCoefficients.push_back( + {UrgencyCoefficient::project, key.substr(21, end - 21), "", coeff}); + else if (!key.compare(13, 4, "tag.", 4)) + userCoefficients.push_back({UrgencyCoefficient::tag, key.substr(17, end - 17), "", coeff}); + else if (!key.compare(13, 8, "keyword.", 8)) + userCoefficients.push_back( + {UrgencyCoefficient::keyword, key.substr(21, end - 21), "", coeff}); + } else if (!key.compare(0, 12, "urgency.uda.", 12)) { + // covers both urgency.uda..coefficient and + // urgency.uda...coefficient + std::string uda = key.substr(12, end - 12); + auto dot = uda.find('.'); + if (dot == std::string::npos) + userCoefficients.push_back({UrgencyCoefficient::uda, std::move(uda), "", coeff}); + else + userCoefficients.push_back( + {UrgencyCoefficient::udaValue, uda.substr(0, dot), uda.substr(dot + 1), coeff}); + } + } +} + //////////////////////////////////////////////////////////////////////////////// // Urgency is defined as a polynomial, the value of which is calculated in this // function, according to: @@ -1751,58 +1792,45 @@ float Task::urgency_c() const { value += fabsf(Task::urgencyAgeCoefficient) > epsilon ? (urgency_age() * Task::urgencyAgeCoefficient) : 0.0; - - const std::string taskProjectName = get("project"); - // Tag- and project-specific coefficients. - for (auto& var : Task::coefficients) { - if (fabs(var.second) > epsilon) { - if (!var.first.compare(0, 13, "urgency.user.", 13)) { - // urgency.user.project..coefficient - auto end = std::string::npos; - if (var.first.substr(13, 8) == "project." && - (end = var.first.find(".coefficient")) != std::string::npos) { - std::string project = var.first.substr(21, end - 21); - - if (taskProjectName == project || taskProjectName.find(project + '.') == 0) { - value += var.second; - } - } - - // urgency.user.tag..coefficient - if (var.first.substr(13, 4) == "tag." && - (end = var.first.find(".coefficient")) != std::string::npos) { - std::string tag = var.first.substr(17, end - 17); - - if (hasTag(tag)) value += var.second; - } - - // urgency.user.keyword..coefficient - if (var.first.substr(13, 8) == "keyword." && - (end = var.first.find(".coefficient")) != std::string::npos) { - std::string keyword = var.first.substr(21, end - 21); - - if (get("description").find(keyword) != std::string::npos) value += var.second; - } - } else if (var.first.substr(0, 12) == "urgency.uda.") { - // urgency.uda..coefficient - // urgency.uda...coefficient - auto end = var.first.find(".coefficient"); - if (end != std::string::npos) { - const std::string uda = var.first.substr(12, end - 12); - auto dot = uda.find('.'); - if (dot == std::string::npos) { - // urgency.uda..coefficient - if (has(uda)) value += var.second; - } else { - // urgency.uda...coefficient - if (get(uda.substr(0, dot)) == uda.substr(dot + 1)) value += var.second; - } - } + // Tag-, project-, keyword-, and UDA specific coefficients. + // We pre-parse with Task::userCoefficients so we can make + // per-task attribute lookups. We cache project and description + // across multiple coefficients of the same kind to avoid + // repeated lookups. + // The reason we do it with description, despite descriptions + // varying a large amount, is for the keyword coefficient. + // We don't do it with tag/UDAs because each refer to a unique one. + const std::string* projectName = nullptr; + const std::string* description = nullptr; + + for (const auto& uc : Task::userCoefficients) { + switch (uc.kind) { + case UrgencyCoefficient::project: { + if (projectName == nullptr) projectName = &get_ref("project"); + // Match exact project or subproject prefix. + if (*projectName == uc.name || (projectName->size() > uc.name.size() && + projectName->compare(0, uc.name.size(), uc.name) == 0 && + (*projectName)[uc.name.size()] == '.')) + value += uc.coefficient; + break; } + case UrgencyCoefficient::tag: + if (hasTag(uc.name)) value += uc.coefficient; + break; + case UrgencyCoefficient::keyword: + if (description == nullptr) description = &get_ref("description"); + if (description->find(uc.name) != std::string::npos) value += uc.coefficient; + break; + case UrgencyCoefficient::uda: + if (has(uc.name)) value += uc.coefficient; + break; + case UrgencyCoefficient::udaValue: + if (get_ref(uc.name) == uc.value) value += uc.coefficient; + break; } } - if (is_blocking && Context::getContext().config.getBoolean("urgency.inherit")) { + if (is_blocking && Task::urgencyInherit) { float prev = value; value = std::max(value, urgency_inherit()); @@ -1816,7 +1844,7 @@ float Task::urgency_c() const { } //////////////////////////////////////////////////////////////////////////////// -float Task::urgency() { +float Task::urgency() const { if (recalc_urgency) { urgency_value = urgency_c(); @@ -1828,14 +1856,22 @@ float Task::urgency() { } //////////////////////////////////////////////////////////////////////////////// +// Inherit urgency from the highest-urgency task dependent on this one. Uses +// the dependency graph via TDB2::dependency_graph() instead of scanning. float Task::urgency_inherit() const { float v = -FLT_MAX; #ifdef PRODUCT_TASKWARRIOR - // Calling getBlockedTasks is rather expensive. - // It is called recursively for each dependency in the chain here. - for (auto& task : getBlockedTasks()) { - // Find highest urgency in all blocked tasks. - v = std::max(v, task.urgency()); + auto& graph = Context::getContext().tdb2.dependency_graph(); + const auto& uuid = get_ref("uuid"); + auto found = graph.dependents.find(uuid); + + if (found != graph.dependents.end()) { + const auto& tasks = Context::getContext().tdb2.pending_tasks(); + for (auto idx : found->second) { + if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) { + v = std::max(v, tasks[idx].urgency()); + } + } } #endif diff --git a/src/Task.h b/src/Task.h index 0b577baff..9f9394c18 100644 --- a/src/Task.h +++ b/src/Task.h @@ -39,6 +39,15 @@ class Task { public: + // A user/UDA coefficient where we pre-parse its key, so urgency_c() + // doesn't re-parse each time. + struct UrgencyCoefficient { + enum Kind { project, tag, keyword, uda, udaValue } kind; + std::string name; // The name of the project/tag/keyword/UDA. + std::string value; // only for udaValue + float coefficient; + }; + static std::string defaultProject; static std::string defaultDue; static std::string defaultScheduled; @@ -46,6 +55,10 @@ class Task { static bool regex; static std::map attributes; // name -> type static std::map coefficients; + // Parsed version of coefficients, built by setUrgencyCoefficients(). + static std::vector userCoefficients; + // Cached value of rc.urgency.inherit. + static bool urgencyInherit; static std::map> customOrder; static float urgencyProjectCoefficient; static float urgencyActiveCoefficient; @@ -78,8 +91,8 @@ class Task { // Public data. int id{0}; - float urgency_value{0.0}; - bool recalc_urgency{true}; + mutable float urgency_value{0.0}; + mutable bool recalc_urgency{true}; bool is_blocked{false}; bool is_blocking{false}; int annotation_count{0}; @@ -88,6 +101,10 @@ class Task { static status textToStatus(const std::string&); static std::string statusToText(status); + // Parse coefficients into userCoefficients. This is called + // after that map has been initialized. + static void setUrgencyCoefficients(); + void setAsNow(const std::string&); bool has(const std::string&) const; std::vector all() const; @@ -172,7 +189,7 @@ class Task { void validate(bool applyDefault = true); float urgency_c() const; - float urgency(); + float urgency() const; #ifdef PRODUCT_TASKWARRIOR enum modType { modReplace, modPrepend, modAppend, modAnnotate }; From 359b99311e45182bf26fd820a9001f3e42ac63dc Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 17 Jul 2026 16:27:36 +0100 Subject: [PATCH 07/49] Guard urgency() against infinite recursion. Signed-off-by: ashprice --- src/Task.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Task.cpp b/src/Task.cpp index d3a5ee6f2..6de95bc5b 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -1846,10 +1846,10 @@ float Task::urgency_c() const { //////////////////////////////////////////////////////////////////////////////// float Task::urgency() const { if (recalc_urgency) { - urgency_value = urgency_c(); - - // Return the sum of all terms. + // We set the guard first to avoid infinite recursion. It will be 0.0 on + // first call and then the computed value will be reused. recalc_urgency = false; + urgency_value = urgency_c(); } return urgency_value; From fff564164b64ff0a3400c06e286b6ac780823a66 Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 17 Jul 2026 16:41:40 +0100 Subject: [PATCH 08/49] Add subset_indices() to filter pending tasks without copying. Signed-off-by: ashprice --- src/Filter.cpp | 38 ++++++++++++++++++++++++++++++++++++++ src/Filter.h | 1 + 2 files changed, 39 insertions(+) diff --git a/src/Filter.cpp b/src/Filter.cpp index 2e6d20a00..77d64741c 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -145,6 +145,44 @@ void Filter::subset(std::vector& output) { Context::getContext().time_filter_us += timer.total_us(); } +///////////////////////////////////////////////////////////////////////////////// +// Like subset(), but leverages the pending tasks cache. +void Filter::subset_indices(const std::vector& pending, std::vector& indices) { + Timer timer; + Context::getContext().cli2.prepareFilter(); + + std::vector> precompiled; + for (auto& a : Context::getContext().cli2._args) + if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); + + if (precompiled.empty()) { + safety(); + indices.clear(); + indices.reserve(pending.size()); + for (int i = 0; i < (int)pending.size(); ++i) indices.push_back(i); + _startCount = (int)pending.size(); + } else { + _startCount = (int)pending.size(); + Eval eval; + eval.addSource(domSource); + eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3); + eval.compileExpression(precompiled); + indices.clear(); + for (int i = 0; i < (int)pending.size(); ++i) { + auto currentTask = Context::getContext().withCurrentTask(&pending[i]); + Variant var; + eval.evaluateCompiledExpression(var); + if (var.get_bool()) indices.push_back(i); + } + eval.debug(false); + } + + _endCount = (int)indices.size(); + Context::getContext().debug( + format("Filtered {1} tasks -> {2} tasks [pending cache]", _startCount, _endCount)); + Context::getContext().time_filter_us += timer.total_us(); +} + //////////////////////////////////////////////////////////////////////////////// bool Filter::hasFilter() const { for (const auto& a : Context::getContext().cli2._args) diff --git a/src/Filter.h b/src/Filter.h index bb7449318..c79cc20fd 100644 --- a/src/Filter.h +++ b/src/Filter.h @@ -38,6 +38,7 @@ class Filter { void subset(const std::vector&, std::vector&); void subset(std::vector&); + void subset_indices(const std::vector& pending, std::vector& indices); bool hasFilter() const; bool pendingOnly() const; void safety() const; From 475535126e301bb7d12b838a101ef178d072b9c3 Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 17 Jul 2026 16:46:44 +0100 Subject: [PATCH 09/49] Add +READY to pending-only filter checks. Signed-off-by: ashprice --- src/Filter.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Filter.cpp b/src/Filter.cpp index 77d64741c..c5c9e266f 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -216,6 +216,7 @@ bool Filter::pendingOnly() const { int countNot = 0; bool pendingTag = false; bool activeTag = false; + bool readyTag = false; for (const auto& a : Context::getContext().cli2._args) { if (a.hasTag("FILTER")) { @@ -235,13 +236,14 @@ bool Filter::pendingOnly() const { for (const auto& word : Context::getContext().cli2._original_args) { if (word.attribute("raw") == "+PENDING") pendingTag = true; if (word.attribute("raw") == "+ACTIVE") activeTag = true; + if (word.attribute("raw") == "+READY") readyTag = true; } if (countUUID) return false; if (countOr || countXor || countNot) return false; - if (pendingTag || activeTag) return true; + if (pendingTag || activeTag || readyTag) return true; if (countStatus) { if (!countPending && !countWaiting && !countRecurring) return false; From 8b3ad3e7eef21e1f81d9c24af8f6042cd150524a Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 17 Jul 2026 17:16:54 +0100 Subject: [PATCH 10/49] Use const auto& for pending/completed tasks in subset() Signed-off-by: ashprice --- src/Filter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Filter.cpp b/src/Filter.cpp index c5c9e266f..76da00fa5 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -91,7 +91,7 @@ void Filter::subset(std::vector& output) { if (precompiled.size()) { Timer timer_pending; - auto pending = Context::getContext().tdb2.pending_tasks(); + const auto& pending = Context::getContext().tdb2.pending_tasks(); Context::getContext().time_filter_us -= timer_pending.total_us(); _startCount = (int)pending.size(); @@ -116,7 +116,7 @@ void Filter::subset(std::vector& output) { shortcut = pendingOnly(); if (!shortcut) { Timer timer_completed; - auto completed = Context::getContext().tdb2.completed_tasks(); + const auto& completed = Context::getContext().tdb2.completed_tasks(); Context::getContext().time_filter_us -= timer_completed.total_us(); _startCount += (int)completed.size(); From 9233d94686bf7e1c06e1294d568ad21def824064 Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 17 Jul 2026 17:57:19 +0100 Subject: [PATCH 11/49] Use const in column measure/render/colorize functions; use get_ref for uuid lookup with random report. Signed-off-by: ashprice --- src/ViewTask.cpp | 2 +- src/ViewTask.h | 2 +- src/columns/ColDepends.cpp | 5 +++-- src/columns/ColDepends.h | 4 ++-- src/columns/ColDescription.cpp | 4 ++-- src/columns/ColDescription.h | 4 ++-- src/columns/ColID.cpp | 4 ++-- src/columns/ColID.h | 4 ++-- src/columns/ColIMask.cpp | 5 +++-- src/columns/ColIMask.h | 4 ++-- src/columns/ColLast.cpp | 5 +++-- src/columns/ColLast.h | 4 ++-- src/columns/ColMask.cpp | 5 +++-- src/columns/ColMask.h | 4 ++-- src/columns/ColParent.cpp | 5 +++-- src/columns/ColParent.h | 4 ++-- src/columns/ColProject.cpp | 5 +++-- src/columns/ColProject.h | 4 ++-- src/columns/ColRType.cpp | 5 +++-- src/columns/ColRType.h | 4 ++-- src/columns/ColRecur.cpp | 5 +++-- src/columns/ColRecur.h | 4 ++-- src/columns/ColStart.cpp | 5 +++-- src/columns/ColStart.h | 4 ++-- src/columns/ColStatus.cpp | 5 +++-- src/columns/ColStatus.h | 4 ++-- src/columns/ColTags.cpp | 5 +++-- src/columns/ColTags.h | 4 ++-- src/columns/ColTemplate.cpp | 5 +++-- src/columns/ColTemplate.h | 4 ++-- src/columns/ColTypeDate.cpp | 5 +++-- src/columns/ColTypeDate.h | 4 ++-- src/columns/ColUDA.cpp | 18 +++++++++------- src/columns/ColUDA.h | 16 +++++++------- src/columns/ColUUID.cpp | 5 +++-- src/columns/ColUUID.h | 4 ++-- src/columns/ColUrgency.cpp | 5 +++-- src/columns/ColUrgency.h | 4 ++-- src/columns/Column.h | 4 ++-- src/rules.cpp | 38 +++++++++++++++++----------------- src/rules.h | 2 +- src/sort.cpp | 8 +++---- src/sort.h | 2 +- 43 files changed, 130 insertions(+), 113 deletions(-) diff --git a/src/ViewTask.cpp b/src/ViewTask.cpp index 5c04ec8f9..28263dbac 100644 --- a/src/ViewTask.cpp +++ b/src/ViewTask.cpp @@ -105,7 +105,7 @@ ViewTask::~ViewTask() { // the larger fields. If the widest field is W0, and the second widest // field is W1, then a solution may be achievable by reducing W0 --> W1. // -std::string ViewTask::render(std::vector& data, std::vector& sequence) { +std::string ViewTask::render(const std::vector& data, std::vector& sequence) { Timer timer; bool const obfuscate = Context::getContext().config.getBoolean("obfuscate"); diff --git a/src/ViewTask.h b/src/ViewTask.h index b606ec992..479edd6a4 100644 --- a/src/ViewTask.h +++ b/src/ViewTask.h @@ -66,7 +66,7 @@ class ViewTask { int rows() { return _rows; } // View rendering. - std::string render(std::vector&, std::vector&); + std::string render(const std::vector&, std::vector&); private: std::vector _columns; diff --git a/src/columns/ColDepends.cpp b/src/columns/ColDepends.cpp index 466285014..0606713ec 100644 --- a/src/columns/ColDepends.cpp +++ b/src/columns/ColDepends.cpp @@ -63,7 +63,7 @@ void ColumnDepends::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnDepends::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnDepends::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (_style == "indicator") { @@ -107,7 +107,8 @@ void ColumnDepends::measure(Task& task, unsigned int& minimum, unsigned int& max } //////////////////////////////////////////////////////////////////////////////// -void ColumnDepends::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnDepends::render(std::vector& lines, const Task& task, int width, + Color& color) { // We only need to know if the task has a dependency. We don't have to // look at the whole list. The flags are set during cache construction. if (_style == "indicator") { diff --git a/src/columns/ColDepends.h b/src/columns/ColDepends.h index ef79b8f06..80c3540e6 100644 --- a/src/columns/ColDepends.h +++ b/src/columns/ColDepends.h @@ -34,8 +34,8 @@ class ColumnDepends : public ColumnTypeString { ColumnDepends(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); void modify(Task&, const std::string&); private: diff --git a/src/columns/ColDescription.cpp b/src/columns/ColDescription.cpp index 460465cff..168a13ebb 100644 --- a/src/columns/ColDescription.cpp +++ b/src/columns/ColDescription.cpp @@ -71,7 +71,7 @@ ColumnDescription::ColumnDescription() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnDescription::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnDescription::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { std::string description = task.get(_name); // The text @@ -130,7 +130,7 @@ void ColumnDescription::measure(Task& task, unsigned int& minimum, unsigned int& } //////////////////////////////////////////////////////////////////////////////// -void ColumnDescription::render(std::vector& lines, Task& task, int width, +void ColumnDescription::render(std::vector& lines, const Task& task, int width, Color& color) { std::string description = task.get(_name); diff --git a/src/columns/ColDescription.h b/src/columns/ColDescription.h index 0178316a5..fdac89005 100644 --- a/src/columns/ColDescription.h +++ b/src/columns/ColDescription.h @@ -32,8 +32,8 @@ class ColumnDescription : public ColumnTypeString { public: ColumnDescription(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: bool _hyphenate; diff --git a/src/columns/ColID.cpp b/src/columns/ColID.cpp index 69f572434..105958281 100644 --- a/src/columns/ColID.cpp +++ b/src/columns/ColID.cpp @@ -43,7 +43,7 @@ ColumnID::ColumnID() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnID::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnID::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { int length; if (task.id < 10) @@ -63,7 +63,7 @@ void ColumnID::measure(Task& task, unsigned int& minimum, unsigned int& maximum) } //////////////////////////////////////////////////////////////////////////////// -void ColumnID::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnID::render(std::vector& lines, const Task& task, int width, Color& color) { // Completed and deleted tasks have no ID. if (task.id) renderInteger(lines, width, color, task.id); diff --git a/src/columns/ColID.h b/src/columns/ColID.h index 8279995a3..4021a0b49 100644 --- a/src/columns/ColID.h +++ b/src/columns/ColID.h @@ -32,8 +32,8 @@ class ColumnID : public ColumnTypeNumeric { public: ColumnID(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColIMask.cpp b/src/columns/ColIMask.cpp index 067e8b85c..f21d51e67 100644 --- a/src/columns/ColIMask.cpp +++ b/src/columns/ColIMask.cpp @@ -42,13 +42,14 @@ ColumnIMask::ColumnIMask() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnIMask::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnIMask::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) minimum = maximum = task.get(_name).length(); } //////////////////////////////////////////////////////////////////////////////// -void ColumnIMask::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnIMask::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) renderStringRight(lines, width, color, task.get(_name)); } diff --git a/src/columns/ColIMask.h b/src/columns/ColIMask.h index 7fdc4b07f..c9abbedb6 100644 --- a/src/columns/ColIMask.h +++ b/src/columns/ColIMask.h @@ -32,8 +32,8 @@ class ColumnIMask : public ColumnTypeNumeric { public: ColumnIMask(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColLast.cpp b/src/columns/ColLast.cpp index 341f4dcee..c34646510 100644 --- a/src/columns/ColLast.cpp +++ b/src/columns/ColLast.cpp @@ -42,13 +42,14 @@ ColumnLast::ColumnLast() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnLast::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnLast::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) minimum = maximum = task.get(_name).length(); } //////////////////////////////////////////////////////////////////////////////// -void ColumnLast::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnLast::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) renderStringRight(lines, width, color, task.get(_name)); } diff --git a/src/columns/ColLast.h b/src/columns/ColLast.h index 8f24c291f..a56afcc14 100644 --- a/src/columns/ColLast.h +++ b/src/columns/ColLast.h @@ -32,8 +32,8 @@ class ColumnLast : public ColumnTypeNumeric { public: ColumnLast(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColMask.cpp b/src/columns/ColMask.cpp index ca5a52f92..aa14b9e66 100644 --- a/src/columns/ColMask.cpp +++ b/src/columns/ColMask.cpp @@ -42,13 +42,14 @@ ColumnMask::ColumnMask() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnMask::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnMask::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) minimum = maximum = task.get(_name).length(); } //////////////////////////////////////////////////////////////////////////////// -void ColumnMask::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnMask::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) renderStringLeft(lines, width, color, task.get(_name)); } diff --git a/src/columns/ColMask.h b/src/columns/ColMask.h index b7f28a9f4..1a4e9c4d2 100644 --- a/src/columns/ColMask.h +++ b/src/columns/ColMask.h @@ -32,8 +32,8 @@ class ColumnMask : public ColumnTypeString { public: ColumnMask(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColParent.cpp b/src/columns/ColParent.cpp index e49315904..72b31ac98 100644 --- a/src/columns/ColParent.cpp +++ b/src/columns/ColParent.cpp @@ -42,7 +42,7 @@ ColumnParent::ColumnParent() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnParent::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnParent::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default" || _style == "long") @@ -53,7 +53,8 @@ void ColumnParent::measure(Task& task, unsigned int& minimum, unsigned int& maxi } //////////////////////////////////////////////////////////////////////////////// -void ColumnParent::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnParent::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { // f30cb9c3-3fc0-483f-bfb2-3bf134f00694 default // f30cb9c3 short diff --git a/src/columns/ColParent.h b/src/columns/ColParent.h index 9332a729d..7fe814c6d 100644 --- a/src/columns/ColParent.h +++ b/src/columns/ColParent.h @@ -32,8 +32,8 @@ class ColumnParent : public ColumnTypeString { public: ColumnParent(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColProject.cpp b/src/columns/ColProject.cpp index 0454bb5bb..d200c554e 100644 --- a/src/columns/ColProject.cpp +++ b/src/columns/ColProject.cpp @@ -50,7 +50,7 @@ ColumnProject::ColumnProject() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnProject::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnProject::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { std::string project = task.get(_name); @@ -68,7 +68,8 @@ void ColumnProject::measure(Task& task, unsigned int& minimum, unsigned int& max } //////////////////////////////////////////////////////////////////////////////// -void ColumnProject::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnProject::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { std::string project = task.get(_name); if (_style == "parent") { diff --git a/src/columns/ColProject.h b/src/columns/ColProject.h index ae470f3b9..3097d4245 100644 --- a/src/columns/ColProject.h +++ b/src/columns/ColProject.h @@ -32,8 +32,8 @@ class ColumnProject : public ColumnTypeString { public: ColumnProject(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); void modify(Task&, const std::string&); private: diff --git a/src/columns/ColRType.cpp b/src/columns/ColRType.cpp index 600e2201c..52f3da3ef 100644 --- a/src/columns/ColRType.cpp +++ b/src/columns/ColRType.cpp @@ -56,7 +56,7 @@ void ColumnRType::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnRType::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnRType::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") @@ -67,7 +67,8 @@ void ColumnRType::measure(Task& task, unsigned int& minimum, unsigned int& maxim } //////////////////////////////////////////////////////////////////////////////// -void ColumnRType::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnRType::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "default") renderStringRight(lines, width, color, task.get(_name)); diff --git a/src/columns/ColRType.h b/src/columns/ColRType.h index dc7b86ffe..1b2482716 100644 --- a/src/columns/ColRType.h +++ b/src/columns/ColRType.h @@ -33,8 +33,8 @@ class ColumnRType : public ColumnTypeString { public: ColumnRType(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); bool validate(const std::string&) const; private: diff --git a/src/columns/ColRecur.cpp b/src/columns/ColRecur.cpp index dc6f0fefc..57ae5ebea 100644 --- a/src/columns/ColRecur.cpp +++ b/src/columns/ColRecur.cpp @@ -60,7 +60,7 @@ void ColumnRecur::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnRecur::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnRecur::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default" || _style == "duration") { @@ -72,7 +72,8 @@ void ColumnRecur::measure(Task& task, unsigned int& minimum, unsigned int& maxim } //////////////////////////////////////////////////////////////////////////////// -void ColumnRecur::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnRecur::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "default" || _style == "duration") renderStringRight(lines, width, color, Duration(task.get(_name)).formatISO()); diff --git a/src/columns/ColRecur.h b/src/columns/ColRecur.h index 3de674aa9..c2fb104c3 100644 --- a/src/columns/ColRecur.h +++ b/src/columns/ColRecur.h @@ -35,8 +35,8 @@ class ColumnRecur : public ColumnTypeString { public: ColumnRecur(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); void modify(Task&, const std::string&); private: diff --git a/src/columns/ColStart.cpp b/src/columns/ColStart.cpp index 4a4dcc1a4..703b085ae 100644 --- a/src/columns/ColStart.cpp +++ b/src/columns/ColStart.cpp @@ -51,7 +51,7 @@ void ColumnStart::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnStart::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnStart::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "active") @@ -64,7 +64,8 @@ void ColumnStart::measure(Task& task, unsigned int& minimum, unsigned int& maxim } //////////////////////////////////////////////////////////////////////////////// -void ColumnStart::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnStart::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "active") { if (!task.has("end")) diff --git a/src/columns/ColStart.h b/src/columns/ColStart.h index 2196ccabd..040c86aa7 100644 --- a/src/columns/ColStart.h +++ b/src/columns/ColStart.h @@ -33,8 +33,8 @@ class ColumnStart : public ColumnTypeDate { public: ColumnStart(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); }; #endif diff --git a/src/columns/ColStatus.cpp b/src/columns/ColStatus.cpp index 1fed6d73f..43fb1e6f3 100644 --- a/src/columns/ColStatus.cpp +++ b/src/columns/ColStatus.cpp @@ -51,7 +51,7 @@ void ColumnStatus::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnStatus::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnStatus::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { Task::status status = task.getStatus(); if (_style == "default" || _style == "long") { @@ -70,7 +70,8 @@ void ColumnStatus::measure(Task& task, unsigned int& minimum, unsigned int& maxi } //////////////////////////////////////////////////////////////////////////////// -void ColumnStatus::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnStatus::render(std::vector& lines, const Task& task, int width, + Color& color) { Task::status status = task.getStatus(); std::string value; diff --git a/src/columns/ColStatus.h b/src/columns/ColStatus.h index 9c6f2d9a4..1c2026ef4 100644 --- a/src/columns/ColStatus.h +++ b/src/columns/ColStatus.h @@ -33,8 +33,8 @@ class ColumnStatus : public ColumnTypeString { public: ColumnStatus(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColTags.cpp b/src/columns/ColTags.cpp index ceaf86187..d5a801688 100644 --- a/src/columns/ColTags.cpp +++ b/src/columns/ColTags.cpp @@ -64,7 +64,7 @@ void ColumnTags::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnTags::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnTags::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.getTagCount() > 0) { if (_style == "indicator") { @@ -86,7 +86,8 @@ void ColumnTags::measure(Task& task, unsigned int& minimum, unsigned int& maximu } //////////////////////////////////////////////////////////////////////////////// -void ColumnTags::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnTags::render(std::vector& lines, const Task& task, int width, + Color& color) { auto all = task.getTags(); if (all.size() > 0) { if (_style == "default" || _style == "list") { diff --git a/src/columns/ColTags.h b/src/columns/ColTags.h index 86d58e335..709b7a554 100644 --- a/src/columns/ColTags.h +++ b/src/columns/ColTags.h @@ -33,8 +33,8 @@ class ColumnTags : public ColumnTypeString { public: ColumnTags(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); void modify(Task&, const std::string&); private: diff --git a/src/columns/ColTemplate.cpp b/src/columns/ColTemplate.cpp index 02f536b1c..a3713b664 100644 --- a/src/columns/ColTemplate.cpp +++ b/src/columns/ColTemplate.cpp @@ -42,7 +42,7 @@ ColumnTemplate::ColumnTemplate() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnTemplate::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnTemplate::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default" || _style == "long") @@ -53,7 +53,8 @@ void ColumnTemplate::measure(Task& task, unsigned int& minimum, unsigned int& ma } //////////////////////////////////////////////////////////////////////////////// -void ColumnTemplate::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnTemplate::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { // f30cb9c3-3fc0-483f-bfb2-3bf134f00694 default // f30cb9c3 short diff --git a/src/columns/ColTemplate.h b/src/columns/ColTemplate.h index 991f42a37..88d7880e1 100644 --- a/src/columns/ColTemplate.h +++ b/src/columns/ColTemplate.h @@ -32,8 +32,8 @@ class ColumnTemplate : public ColumnTypeString { public: ColumnTemplate(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColTypeDate.cpp b/src/columns/ColTypeDate.cpp index cb459cd3b..bf74358bb 100644 --- a/src/columns/ColTypeDate.cpp +++ b/src/columns/ColTypeDate.cpp @@ -58,7 +58,7 @@ ColumnTypeDate::ColumnTypeDate() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnTypeDate::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnTypeDate::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { Datetime date(task.get_date(_name)); @@ -102,7 +102,8 @@ void ColumnTypeDate::measure(Task& task, unsigned int& minimum, unsigned int& ma } //////////////////////////////////////////////////////////////////////////////// -void ColumnTypeDate::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnTypeDate::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { Datetime date(task.get_date(_name)); diff --git a/src/columns/ColTypeDate.h b/src/columns/ColTypeDate.h index dc4bc9aea..29fd9617a 100644 --- a/src/columns/ColTypeDate.h +++ b/src/columns/ColTypeDate.h @@ -37,8 +37,8 @@ class ColumnTypeDate : public Column { public: ColumnTypeDate(); - virtual void measure(Task&, unsigned int&, unsigned int&); - virtual void render(std::vector&, Task&, int, Color&); + virtual void measure(const Task&, unsigned int&, unsigned int&); + virtual void render(std::vector&, const Task&, int, Color&); virtual bool validate(const std::string&) const; virtual void modify(Task&, const std::string&); }; diff --git a/src/columns/ColUDA.cpp b/src/columns/ColUDA.cpp index a6452c631..2921d7c7f 100644 --- a/src/columns/ColUDA.cpp +++ b/src/columns/ColUDA.cpp @@ -63,7 +63,7 @@ bool ColumnUDAString::validate(const std::string& value) const { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. // -void ColumnUDAString::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUDAString::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { @@ -83,7 +83,8 @@ void ColumnUDAString::measure(Task& task, unsigned int& minimum, unsigned int& m } //////////////////////////////////////////////////////////////////////////////// -void ColumnUDAString::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnUDAString::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "default") { std::string value = task.get(_name); @@ -126,7 +127,7 @@ bool ColumnUDANumeric::validate(const std::string& value) const { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. // -void ColumnUDANumeric::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUDANumeric::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { @@ -142,7 +143,7 @@ void ColumnUDANumeric::measure(Task& task, unsigned int& minimum, unsigned int& } //////////////////////////////////////////////////////////////////////////////// -void ColumnUDANumeric::render(std::vector& lines, Task& task, int width, +void ColumnUDANumeric::render(std::vector& lines, const Task& task, int width, Color& color) { if (task.has(_name)) { if (_style == "default") { @@ -183,7 +184,7 @@ bool ColumnUDADate::validate(const std::string& value) const { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. // -void ColumnUDADate::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUDADate::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { @@ -210,7 +211,8 @@ void ColumnUDADate::measure(Task& task, unsigned int& minimum, unsigned int& max } //////////////////////////////////////////////////////////////////////////////// -void ColumnUDADate::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnUDADate::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "default") { auto value = task.get(_name); @@ -262,7 +264,7 @@ bool ColumnUDADuration::validate(const std::string& value) const { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. // -void ColumnUDADuration::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUDADuration::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { @@ -281,7 +283,7 @@ void ColumnUDADuration::measure(Task& task, unsigned int& minimum, unsigned int& } //////////////////////////////////////////////////////////////////////////////// -void ColumnUDADuration::render(std::vector& lines, Task& task, int width, +void ColumnUDADuration::render(std::vector& lines, const Task& task, int width, Color& color) { if (task.has(_name)) { if (_style == "default") { diff --git a/src/columns/ColUDA.h b/src/columns/ColUDA.h index 04d57a8d7..662ed7e66 100644 --- a/src/columns/ColUDA.h +++ b/src/columns/ColUDA.h @@ -38,8 +38,8 @@ class ColumnUDAString : public ColumnTypeString { public: ColumnUDAString(); bool validate(const std::string&) const; - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); public: std::vector _values; @@ -53,8 +53,8 @@ class ColumnUDANumeric : public ColumnTypeNumeric { public: ColumnUDANumeric(); bool validate(const std::string&) const; - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); public: std::vector _values; @@ -65,8 +65,8 @@ class ColumnUDADate : public ColumnTypeDate { public: ColumnUDADate(); bool validate(const std::string&) const; - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); public: std::vector _values; @@ -77,8 +77,8 @@ class ColumnUDADuration : public ColumnTypeDuration { public: ColumnUDADuration(); bool validate(const std::string&) const; - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); public: std::vector _values; diff --git a/src/columns/ColUUID.cpp b/src/columns/ColUUID.cpp index 3b81a4419..2a66b566a 100644 --- a/src/columns/ColUUID.cpp +++ b/src/columns/ColUUID.cpp @@ -42,7 +42,7 @@ ColumnUUID::ColumnUUID() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnUUID::measure(Task&, unsigned int& minimum, unsigned int& maximum) { +void ColumnUUID::measure(const Task&, unsigned int& minimum, unsigned int& maximum) { // Mandatory attribute, no need to check the value. if (_style == "default" || _style == "long") @@ -52,7 +52,8 @@ void ColumnUUID::measure(Task&, unsigned int& minimum, unsigned int& maximum) { } //////////////////////////////////////////////////////////////////////////////// -void ColumnUUID::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnUUID::render(std::vector& lines, const Task& task, int width, + Color& color) { // No need to check the presence of UUID - all tasks have one. // f30cb9c3-3fc0-483f-bfb2-3bf134f00694 default diff --git a/src/columns/ColUUID.h b/src/columns/ColUUID.h index 7a8232a41..b45808c95 100644 --- a/src/columns/ColUUID.h +++ b/src/columns/ColUUID.h @@ -32,8 +32,8 @@ class ColumnUUID : public ColumnTypeString { public: ColumnUUID(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColUrgency.cpp b/src/columns/ColUrgency.cpp index d202a7d91..87fcb058f 100644 --- a/src/columns/ColUrgency.cpp +++ b/src/columns/ColUrgency.cpp @@ -42,7 +42,7 @@ ColumnUrgency::ColumnUrgency() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnUrgency::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUrgency::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { if (_style == "default" || _style == "real") minimum = maximum = format(task.urgency(), 4, 3).length(); @@ -51,7 +51,8 @@ void ColumnUrgency::measure(Task& task, unsigned int& minimum, unsigned int& max } //////////////////////////////////////////////////////////////////////////////// -void ColumnUrgency::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnUrgency::render(std::vector& lines, const Task& task, int width, + Color& color) { if (_style == "default" || _style == "real") renderDouble(lines, width, color, task.urgency()); diff --git a/src/columns/ColUrgency.h b/src/columns/ColUrgency.h index c4c31ab0d..e826f4daf 100644 --- a/src/columns/ColUrgency.h +++ b/src/columns/ColUrgency.h @@ -32,8 +32,8 @@ class ColumnUrgency : public ColumnTypeNumeric { public: ColumnUrgency(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/Column.h b/src/columns/Column.h index 20dad26fb..a6cb728f9 100644 --- a/src/columns/Column.h +++ b/src/columns/Column.h @@ -58,10 +58,10 @@ class Column { virtual void setReport(const std::string& value) { _report = value; } virtual void measure(const std::string&, unsigned int&, unsigned int&) {}; - virtual void measure(Task&, unsigned int&, unsigned int&) {}; + virtual void measure(const Task&, unsigned int&, unsigned int&) {}; virtual void renderHeader(std::vector&, int, Color&); virtual void render(std::vector&, const std::string&, int, Color&) {}; - virtual void render(std::vector&, Task&, int, Color&) {}; + virtual void render(std::vector&, const Task&, int, Color&) {}; virtual bool validate(const std::string&) const { return false; }; virtual void modify(Task&, const std::string&) {}; diff --git a/src/rules.cpp b/src/rules.cpp index 21c46307d..4883a3276 100644 --- a/src/rules.cpp +++ b/src/rules.cpp @@ -85,46 +85,46 @@ static void applyColor(const Color& base, Color& c, bool merge) { } //////////////////////////////////////////////////////////////////////////////// -static void colorizeBlocked(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeBlocked(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_blocked) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeBlocking(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeBlocking(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_blocking) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeTagged(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeTagged(const Task& task, const Color& base, Color& c, bool merge) { if (task.getTagCount()) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeActive(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeActive(const Task& task, const Color& base, Color& c, bool merge) { // TODO: Not consistent with the implementation of the +ACTIVE virtual tag if (task.has("start") && !task.has("end")) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeScheduled(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeScheduled(const Task& task, const Color& base, Color& c, bool merge) { // TODO: Not consistent with the implementation of the +SCHEDULED virtual tag if (task.has("scheduled") && Datetime(task.get_date("scheduled")) <= now) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeUntil(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeUntil(const Task& task, const Color& base, Color& c, bool merge) { if (task.has("until")) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeTag(Task& task, const std::string& rule, const Color& base, Color& c, +static void colorizeTag(const Task& task, const std::string& rule, const Color& base, Color& c, bool merge) { if (task.hasTag(rule.substr(10))) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeProject(Task& task, const std::string& rule, const Color& base, Color& c, +static void colorizeProject(const Task& task, const std::string& rule, const Color& base, Color& c, bool merge) { // Observe the case sensitivity setting. bool sensitive = Context::getContext().config.getBoolean("search.case.sensitive"); @@ -139,17 +139,17 @@ static void colorizeProject(Task& task, const std::string& rule, const Color& ba } //////////////////////////////////////////////////////////////////////////////// -static void colorizeProjectNone(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeProjectNone(const Task& task, const Color& base, Color& c, bool merge) { if (!task.has("project")) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeTagNone(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeTagNone(const Task& task, const Color& base, Color& c, bool merge) { if (task.getTagCount() == 0) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeKeyword(Task& task, const std::string& rule, const Color& base, Color& c, +static void colorizeKeyword(const Task& task, const std::string& rule, const Color& base, Color& c, bool merge) { // Observe the case sensitivity setting. auto sensitive = Context::getContext().config.getBoolean("search.case.sensitive"); @@ -172,7 +172,7 @@ static void colorizeKeyword(Task& task, const std::string& rule, const Color& ba } //////////////////////////////////////////////////////////////////////////////// -static void colorizeUDA(Task& task, const std::string& rule, const Color& base, Color& c, +static void colorizeUDA(const Task& task, const std::string& rule, const Color& base, Color& c, bool merge) { // Is the rule color.uda.name.value or color.uda.name? auto pos = rule.find('.', 10); @@ -186,37 +186,37 @@ static void colorizeUDA(Task& task, const std::string& rule, const Color& base, } //////////////////////////////////////////////////////////////////////////////// -static void colorizeDue(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeDue(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_due()) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeDueToday(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeDueToday(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_duetoday()) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeOverdue(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeOverdue(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_overdue()) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeRecurring(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeRecurring(const Task& task, const Color& base, Color& c, bool merge) { if (task.has("recur")) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeCompleted(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeCompleted(const Task& task, const Color& base, Color& c, bool merge) { if (task.getStatus() == Task::completed) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeDeleted(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeDeleted(const Task& task, const Color& base, Color& c, bool merge) { if (task.getStatus() == Task::deleted) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -void autoColorize(Task& task, Color& c) { +void autoColorize(const Task& task, Color& c) { // The special tag 'nocolor' overrides all auto and specific colorization. if (!Context::getContext().color() || task.hasTag("nocolor")) { c = Color(); diff --git a/src/rules.h b/src/rules.h index d0d8c7f85..cbe73b56f 100644 --- a/src/rules.h +++ b/src/rules.h @@ -36,7 +36,7 @@ #include void initializeColorRules(); -void autoColorize(Task& task, Color& c); +void autoColorize(const Task& task, Color& c); std::string colorizeHeader(const std::string& input); std::string colorizeFootnote(const std::string& input); std::string colorizeError(const std::string& input); diff --git a/src/sort.cpp b/src/sort.cpp index f87be111a..76ce79630 100644 --- a/src/sort.cpp +++ b/src/sort.cpp @@ -43,13 +43,13 @@ #include #include -static std::vector* global_data = nullptr; +static const std::vector* global_data = nullptr; static std::vector global_keys; static unsigned int sort_random_seed = 0; static bool sort_compare(int, int); //////////////////////////////////////////////////////////////////////////////// -void sort_tasks(std::vector& data, std::vector& order, const std::string& keys) { +void sort_tasks(const std::vector& data, std::vector& order, const std::string& keys) { Timer timer; global_data = &data; @@ -130,8 +130,8 @@ static bool sort_compare(int left, int right) { if (field == "random") { // For "random" sort, we produce a stable number for each task based on a hash of its // UUID plus the random seed. - std::string left_uuid = (*global_data)[left].get("uuid"); - std::string right_uuid = (*global_data)[right].get("uuid"); + const auto& left_uuid = (*global_data)[left].get_ref("uuid"); + const auto& right_uuid = (*global_data)[right].get_ref("uuid"); std::string left_scrambled = std::to_string(std::hash{}(left_uuid + std::to_string(sort_random_seed))); diff --git a/src/sort.h b/src/sort.h index 6972f9c2e..4d842d3e0 100644 --- a/src/sort.h +++ b/src/sort.h @@ -38,7 +38,7 @@ #include #include -void sort_tasks(std::vector& data, std::vector& order, const std::string& keys); +void sort_tasks(const std::vector& data, std::vector& order, const std::string& keys); void sort_projects(std::list>& sorted, std::map& allProjects); From 5de5706e9d6e0c8721b6ae0205d2d55318041a50 Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 17 Jul 2026 18:17:09 +0100 Subject: [PATCH 12/49] Switched to get_ref() inside many functions used for reports. Signed-off-by: ashprice --- src/ViewTask.cpp | 2 +- src/columns/ColIMask.cpp | 4 ++-- src/columns/ColLast.cpp | 4 ++-- src/columns/ColMask.cpp | 4 ++-- src/columns/ColParent.cpp | 4 ++-- src/columns/ColRType.cpp | 6 +++--- src/columns/ColRecur.cpp | 4 ++-- src/columns/ColTemplate.cpp | 4 ++-- src/columns/ColUDA.cpp | 16 ++++++++-------- src/columns/ColUUID.cpp | 4 ++-- src/commands/CmdExport.cpp | 2 +- src/dependency.cpp | 6 +++--- src/feedback.cpp | 15 ++++++++------- src/nag.cpp | 9 +++++---- src/rules.cpp | 6 +++--- 15 files changed, 46 insertions(+), 44 deletions(-) diff --git a/src/ViewTask.cpp b/src/ViewTask.cpp index 28263dbac..bfece1591 100644 --- a/src/ViewTask.cpp +++ b/src/ViewTask.cpp @@ -331,7 +331,7 @@ std::string ViewTask::render(const std::vector& data, std::vector& se // changes. if (s > 0 && _breaks.size() > 0) { for (const auto& b : _breaks) { - if (data[sequence[s - 1]].get(b) != data[sequence[s]].get(b)) { + if (data[sequence[s - 1]].get_ref(b) != data[sequence[s]].get(b)) { out += "\n"; ++_lines; diff --git a/src/columns/ColIMask.cpp b/src/columns/ColIMask.cpp index f21d51e67..e59031d7c 100644 --- a/src/columns/ColIMask.cpp +++ b/src/columns/ColIMask.cpp @@ -44,13 +44,13 @@ ColumnIMask::ColumnIMask() { // Set the minimum and maximum widths for the value. void ColumnIMask::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; - if (task.has(_name)) minimum = maximum = task.get(_name).length(); + if (task.has(_name)) minimum = maximum = task.get_ref(_name).length(); } //////////////////////////////////////////////////////////////////////////////// void ColumnIMask::render(std::vector& lines, const Task& task, int width, Color& color) { - if (task.has(_name)) renderStringRight(lines, width, color, task.get(_name)); + if (task.has(_name)) renderStringRight(lines, width, color, task.get_ref(_name)); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColLast.cpp b/src/columns/ColLast.cpp index c34646510..ec8313808 100644 --- a/src/columns/ColLast.cpp +++ b/src/columns/ColLast.cpp @@ -44,13 +44,13 @@ ColumnLast::ColumnLast() { // Set the minimum and maximum widths for the value. void ColumnLast::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; - if (task.has(_name)) minimum = maximum = task.get(_name).length(); + if (task.has(_name)) minimum = maximum = task.get_ref(_name).length(); } //////////////////////////////////////////////////////////////////////////////// void ColumnLast::render(std::vector& lines, const Task& task, int width, Color& color) { - if (task.has(_name)) renderStringRight(lines, width, color, task.get(_name)); + if (task.has(_name)) renderStringRight(lines, width, color, task.get_ref(_name)); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColMask.cpp b/src/columns/ColMask.cpp index aa14b9e66..cd34df30b 100644 --- a/src/columns/ColMask.cpp +++ b/src/columns/ColMask.cpp @@ -44,13 +44,13 @@ ColumnMask::ColumnMask() { // Set the minimum and maximum widths for the value. void ColumnMask::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; - if (task.has(_name)) minimum = maximum = task.get(_name).length(); + if (task.has(_name)) minimum = maximum = task.get_ref(_name).length(); } //////////////////////////////////////////////////////////////////////////////// void ColumnMask::render(std::vector& lines, const Task& task, int width, Color& color) { - if (task.has(_name)) renderStringLeft(lines, width, color, task.get(_name)); + if (task.has(_name)) renderStringLeft(lines, width, color, task.get_ref(_name)); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColParent.cpp b/src/columns/ColParent.cpp index 72b31ac98..398048fd8 100644 --- a/src/columns/ColParent.cpp +++ b/src/columns/ColParent.cpp @@ -59,10 +59,10 @@ void ColumnParent::render(std::vector& lines, const Task& task, int // f30cb9c3-3fc0-483f-bfb2-3bf134f00694 default // f30cb9c3 short if (_style == "default" || _style == "long") - renderStringLeft(lines, width, color, task.get(_name)); + renderStringLeft(lines, width, color, task.get_ref(_name)); else if (_style == "short") - renderStringLeft(lines, width, color, task.get(_name).substr(0, 8)); + renderStringLeft(lines, width, color, task.get_ref(_name).substr(0, 8)); } } diff --git a/src/columns/ColRType.cpp b/src/columns/ColRType.cpp index 52f3da3ef..3c2651cae 100644 --- a/src/columns/ColRType.cpp +++ b/src/columns/ColRType.cpp @@ -60,7 +60,7 @@ void ColumnRType::measure(const Task& task, unsigned int& minimum, unsigned int& minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") - minimum = maximum = task.get(_name).length(); + minimum = maximum = task.get_ref(_name).length(); else if (_style == "indicator") minimum = maximum = 1; } @@ -71,11 +71,11 @@ void ColumnRType::render(std::vector& lines, const Task& task, int Color& color) { if (task.has(_name)) { if (_style == "default") - renderStringRight(lines, width, color, task.get(_name)); + renderStringRight(lines, width, color, task.get_ref(_name)); else if (_style == "indicator") { std::string value{" "}; - value[0] = toupper(task.get(_name)[0]); + value[0] = toupper(task.get_ref(_name)[0]); renderStringRight(lines, width, color, value); } } diff --git a/src/columns/ColRecur.cpp b/src/columns/ColRecur.cpp index 57ae5ebea..fd7687ff8 100644 --- a/src/columns/ColRecur.cpp +++ b/src/columns/ColRecur.cpp @@ -64,7 +64,7 @@ void ColumnRecur::measure(const Task& task, unsigned int& minimum, unsigned int& minimum = maximum = 0; if (task.has(_name)) { if (_style == "default" || _style == "duration") { - minimum = maximum = Duration(task.get(_name)).formatISO().length(); + minimum = maximum = Duration(task.get_ref(_name)).formatISO().length(); } else if (_style == "indicator") { minimum = maximum = utf8_width(Context::getContext().config.get("recurrence.indicator")); } @@ -76,7 +76,7 @@ void ColumnRecur::render(std::vector& lines, const Task& task, int Color& color) { if (task.has(_name)) { if (_style == "default" || _style == "duration") - renderStringRight(lines, width, color, Duration(task.get(_name)).formatISO()); + renderStringRight(lines, width, color, Duration(task.get_ref(_name)).formatISO()); else if (_style == "indicator") renderStringRight(lines, width, color, diff --git a/src/columns/ColTemplate.cpp b/src/columns/ColTemplate.cpp index a3713b664..379110120 100644 --- a/src/columns/ColTemplate.cpp +++ b/src/columns/ColTemplate.cpp @@ -59,10 +59,10 @@ void ColumnTemplate::render(std::vector& lines, const Task& task, i // f30cb9c3-3fc0-483f-bfb2-3bf134f00694 default // f30cb9c3 short if (_style == "default" || _style == "long") - renderStringLeft(lines, width, color, task.get(_name)); + renderStringLeft(lines, width, color, task.get_ref(_name)); else if (_style == "short") - renderStringLeft(lines, width, color, task.get(_name).substr(0, 8)); + renderStringLeft(lines, width, color, task.get_ref(_name).substr(0, 8)); } } diff --git a/src/columns/ColUDA.cpp b/src/columns/ColUDA.cpp index 2921d7c7f..94a985df8 100644 --- a/src/columns/ColUDA.cpp +++ b/src/columns/ColUDA.cpp @@ -67,7 +67,7 @@ void ColumnUDAString::measure(const Task& task, unsigned int& minimum, unsigned minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - std::string value = task.get(_name); + std::string value = task.get_ref(_name); if (value != "") { auto stripped = Color::strip(value); maximum = longestLine(stripped); @@ -87,7 +87,7 @@ void ColumnUDAString::render(std::vector& lines, const Task& task, Color& color) { if (task.has(_name)) { if (_style == "default") { - std::string value = task.get(_name); + std::string value = task.get_ref(_name); std::vector raw; wrapText(raw, value, width, _hyphenate); @@ -131,7 +131,7 @@ void ColumnUDANumeric::measure(const Task& task, unsigned int& minimum, unsigned minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + auto value = task.get_ref(_name); if (value != "") minimum = maximum = value.length(); } else if (_style == "indicator") { auto indicator = Context::getContext().config.get("uda." + _name + ".indicator"); @@ -147,7 +147,7 @@ void ColumnUDANumeric::render(std::vector& lines, const Task& task, Color& color) { if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + auto value = task.get_ref(_name); renderStringRight(lines, width, color, value); } else if (_style == "indicator") { auto indicator = Context::getContext().config.get("uda." + _name + ".indicator"); @@ -188,7 +188,7 @@ void ColumnUDADate::measure(const Task& task, unsigned int& minimum, unsigned in minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + auto value = task.get_ref(_name); if (value != "") { // Determine the output date format, which uses a hierarchy of definitions. // rc.report..dateformat @@ -215,7 +215,7 @@ void ColumnUDADate::render(std::vector& lines, const Task& task, in Color& color) { if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + auto value = task.get_ref(_name); // Determine the output date format, which uses a hierarchy of definitions. // rc.report..dateformat @@ -268,7 +268,7 @@ void ColumnUDADuration::measure(const Task& task, unsigned int& minimum, unsigne minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + auto value = task.get_ref(_name); if (value != "") minimum = maximum = Duration(value).formatISO().length(); } else if (_style == "indicator") { if (task.has(_name)) { @@ -287,7 +287,7 @@ void ColumnUDADuration::render(std::vector& lines, const Task& task Color& color) { if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + auto value = task.get_ref(_name); renderStringRight(lines, width, color, Duration(value).formatISO()); } else if (_style == "indicator") { auto indicator = Context::getContext().config.get("uda." + _name + ".indicator"); diff --git a/src/columns/ColUUID.cpp b/src/columns/ColUUID.cpp index 2a66b566a..df1353dad 100644 --- a/src/columns/ColUUID.cpp +++ b/src/columns/ColUUID.cpp @@ -59,10 +59,10 @@ void ColumnUUID::render(std::vector& lines, const Task& task, int w // f30cb9c3-3fc0-483f-bfb2-3bf134f00694 default // f30cb9c3 short if (_style == "default" || _style == "long") - renderStringLeft(lines, width, color, task.get(_name)); + renderStringLeft(lines, width, color, task.get_ref(_name)); else if (_style == "short") - renderStringLeft(lines, width, color, task.get(_name).substr(0, 8)); + renderStringLeft(lines, width, color, task.get_ref(_name).substr(0, 8)); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/commands/CmdExport.cpp b/src/commands/CmdExport.cpp index 2924645dd..7e382a6b3 100644 --- a/src/commands/CmdExport.cpp +++ b/src/commands/CmdExport.cpp @@ -97,7 +97,7 @@ int CmdExport::execute(std::string& output) { sortOrder.clear(); for (auto& i : Context::getContext().cli2._uuid_list) for (unsigned int t = 0; t < filtered.size(); ++t) - if (filtered[t].get("uuid") == i) sequence.push_back(t); + if (filtered[t].get_ref("uuid") == i) sequence.push_back(t); } else { // sort_tasks requires the order array initially be identity for (unsigned int i = 0; i < filtered.size(); ++i) sequence.push_back(i); diff --git a/src/dependency.cpp b/src/dependency.cpp index affc4bb5a..274603be2 100644 --- a/src/dependency.cpp +++ b/src/dependency.cpp @@ -118,7 +118,7 @@ void dependencyChainOnComplete(Task& task) { std::cout << format(STRING_DEPEND_BLOCKED, task.identifier()) << '\n'; for (const auto& b : blocking) - std::cout << " " << b.id << ' ' << b.get("description") << '\n'; + std::cout << " " << b.id << ' ' << b.get_ref("description") << '\n'; } // If there are both blocking and blocked tasks, the chain is broken. @@ -127,7 +127,7 @@ void dependencyChainOnComplete(Task& task) { std::cout << "and is blocking:\n"; for (const auto& b : blocked) - std::cout << " " << b.id << ' ' << b.get("description") << '\n'; + std::cout << " " << b.id << ' ' << b.get_ref("description") << '\n'; } if (!Context::getContext().config.getBoolean("dependency.confirmation") || @@ -160,7 +160,7 @@ void dependencyChainOnStart(Task& task) { std::cout << format(STRING_DEPEND_BLOCKED, task.identifier()) << '\n'; for (const auto& b : blocking) - std::cout << " " << b.id << ' ' << b.get("description") << '\n'; + std::cout << " " << b.id << ' ' << b.get_ref("description") << '\n'; } } } diff --git a/src/feedback.cpp b/src/feedback.cpp index efd2302b4..83634f39d 100644 --- a/src/feedback.cpp +++ b/src/feedback.cpp @@ -90,7 +90,7 @@ void feedback_affected(const std::string& effect, int quantity) { // {2} Description void feedback_affected(const std::string& effect, const Task& task) { if (Context::getContext().verbose("affected")) { - std::cout << format(effect, task.identifier(true), task.get("description")) << "\n"; + std::cout << format(effect, task.identifier(true), task.get_ref("description")) << "\n"; } } @@ -152,10 +152,11 @@ void feedback_unblocked(const Task& task) { auto blocking = i.getDependencyTasks(); if (blocking.size() == 0) { if (i.id) - std::cout << format("Unblocked {1} '{2}'.", i.id, i.get("description")) << "\n"; + std::cout << format("Unblocked {1} '{2}'.", i.id, i.get_ref("description")) << "\n"; else { std::string uuid = i.get("uuid"); - std::cout << format("Unblocked {1} '{2}'.", i.get("uuid"), i.get("description")) << "\n"; + std::cout << format("Unblocked {1} '{2}'.", i.get("uuid"), i.get_ref("description")) + << "\n"; } } } @@ -182,7 +183,7 @@ void feedback_backlog() { /////////////////////////////////////////////////////////////////////////////// std::string onProjectChange(Task& task, bool scope /* = true */) { std::stringstream msg; - std::string project = task.get("project"); + std::string project = task.get_ref("project"); if (project != "") { if (scope) msg << format("The project '{1}' has changed.", project) << " "; @@ -220,7 +221,7 @@ std::string onProjectChange(Task& task, bool scope /* = true */) { /////////////////////////////////////////////////////////////////////////////// std::string onProjectChange(Task& task1, Task& task2) { - if (task1.get("project") == task2.get("project")) return onProjectChange(task1, false); + if (task1.get_ref("project") == task2.get("project")) return onProjectChange(task1, false); std::string messages1 = onProjectChange(task1); std::string messages2 = onProjectChange(task2); @@ -236,7 +237,7 @@ std::string onExpiration(Task& task) { if (Context::getContext().verbose("affected")) msg << format("Task {1} '{2}' expired and was deleted.", task.identifier(true), - task.get("description")); + task.get_ref("description")); return msg.str(); } @@ -245,7 +246,7 @@ std::string onExpiration(Task& task) { static void countTasks(const std::vector& all, const std::string& project, int& count_pending, int& count_done) { for (auto& it : all) { - if (it.get("project") == project) { + if (it.get_ref("project") == project) { switch (it.getStatus()) { case Task::pending: case Task::waiting: diff --git a/src/nag.cpp b/src/nag.cpp index 356287f45..5a65a3145 100644 --- a/src/nag.cpp +++ b/src/nag.cpp @@ -36,12 +36,13 @@ void nag(std::vector& tasks) { auto msg = Context::getContext().config.get("nag"); if (msg == "") return; - auto pending = Context::getContext().tdb2.pending_tasks(); - for (auto& t1 : tasks) { + const auto& pending = Context::getContext().tdb2.pending_tasks(); + for (const auto& t1 : tasks) { if (t1.hasTag("nonag")) continue; - for (auto& t2 : pending) { - if (t1.get("uuid") != t2.get("uuid") && t2.hasTag("READY") && t1.urgency() < t2.urgency()) { + for (const auto& t2 : pending) { + if (t1.get_ref("uuid") != t2.get_ref("uuid") && t2.hasTag("READY") && + t1.urgency() < t2.urgency()) { Context::getContext().footnote(msg); return; } diff --git a/src/rules.cpp b/src/rules.cpp index 4883a3276..9fabdc19e 100644 --- a/src/rules.cpp +++ b/src/rules.cpp @@ -129,7 +129,7 @@ static void colorizeProject(const Task& task, const std::string& rule, const Col // Observe the case sensitivity setting. bool sensitive = Context::getContext().config.getBoolean("search.case.sensitive"); - auto project = task.get("project"); + const auto& project = task.get_ref("project"); auto rule_trunc = rule.substr(14); // Match project names leftmost. @@ -156,7 +156,7 @@ static void colorizeKeyword(const Task& task, const std::string& rule, const Col // The easiest thing to check is the description, because it is just one // attribute. - if (find(task.get("description"), rule.substr(14), sensitive) != std::string::npos) + if (find(task.get_ref("description"), rule.substr(14), sensitive) != std::string::npos) applyColor(base, c, merge); // Failing the description check, look at all annotations, returning on the @@ -181,7 +181,7 @@ static void colorizeUDA(const Task& task, const std::string& rule, const Color& } else { auto uda = rule.substr(10, pos - 10); auto val = rule.substr(pos + 1); - if ((val == "none" && !task.has(uda)) || task.get(uda) == val) applyColor(base, c, merge); + if ((val == "none" && !task.has(uda)) || task.get_ref(uda) == val) applyColor(base, c, merge); } } From 19543ca9c6443bab34829088a648387c614f7640 Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 15 Aug 2026 12:14:30 +0100 Subject: [PATCH 13/49] Small fixes and comments Signed-off-by: ashprice --- src/Task.cpp | 4 +++- src/Task.h | 2 +- src/ViewTask.cpp | 2 +- src/columns/ColDepends.cpp | 4 ++-- src/columns/ColUDA.cpp | 16 ++++++++-------- src/feedback.cpp | 9 ++++----- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/Task.cpp b/src/Task.cpp index 6de95bc5b..9aa09c345 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -1089,10 +1089,12 @@ std::vector Task::getDependencyTasks() const { } //////////////////////////////////////////////////////////////////////////////// +// Returns the pending tasks that depend on a given task. std::vector Task::getBlockedTasks() const { const auto& uuid = get_ref("uuid"); std::vector blocked; + auto& graph = Context::getContext().tdb2.dependency_graph(); auto found = graph.dependents.find(uuid); @@ -1847,7 +1849,7 @@ float Task::urgency_c() const { float Task::urgency() const { if (recalc_urgency) { // We set the guard first to avoid infinite recursion. It will be 0.0 on - // first call and then the computed value will be reused. + // first call and then the computed value will be reused from the cache.. recalc_urgency = false; urgency_value = urgency_c(); } diff --git a/src/Task.h b/src/Task.h index 9f9394c18..cacf12fcd 100644 --- a/src/Task.h +++ b/src/Task.h @@ -39,7 +39,7 @@ class Task { public: - // A user/UDA coefficient where we pre-parse its key, so urgency_c() + // A user/UDA coefficient with a pre-parsed key, so urgency_c() // doesn't re-parse each time. struct UrgencyCoefficient { enum Kind { project, tag, keyword, uda, udaValue } kind; diff --git a/src/ViewTask.cpp b/src/ViewTask.cpp index bfece1591..8fa40bd3d 100644 --- a/src/ViewTask.cpp +++ b/src/ViewTask.cpp @@ -331,7 +331,7 @@ std::string ViewTask::render(const std::vector& data, std::vector& se // changes. if (s > 0 && _breaks.size() > 0) { for (const auto& b : _breaks) { - if (data[sequence[s - 1]].get_ref(b) != data[sequence[s]].get(b)) { + if (data[sequence[s - 1]].get_ref(b) != data[sequence[s]].get_ref(b)) { out += "\n"; ++_lines; diff --git a/src/columns/ColDepends.cpp b/src/columns/ColDepends.cpp index 0606713ec..962a18a91 100644 --- a/src/columns/ColDepends.cpp +++ b/src/columns/ColDepends.cpp @@ -75,7 +75,7 @@ void ColumnDepends::measure(const Task& task, unsigned int& minimum, unsigned in return; } - // We also don't need to call getDependencyTasks() anymore, + // We also don't need to call getDependencyTasks(), // which would copy the full objects from the cache just to read the ID // field. Instead, we can use getDependencyUUIDs() and tdb2.id(). @@ -98,7 +98,7 @@ void ColumnDepends::measure(const Task& task, unsigned int& minimum, unsigned in maximum = all.length(); unsigned int length; - for (auto& id : blocking_ids) { + for (auto id : blocking_ids) { length = format(id).length(); if (length > minimum) minimum = length; } diff --git a/src/columns/ColUDA.cpp b/src/columns/ColUDA.cpp index 94a985df8..c9c3f50b6 100644 --- a/src/columns/ColUDA.cpp +++ b/src/columns/ColUDA.cpp @@ -67,7 +67,7 @@ void ColumnUDAString::measure(const Task& task, unsigned int& minimum, unsigned minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - std::string value = task.get_ref(_name); + const auto& value = task.get_ref(_name); if (value != "") { auto stripped = Color::strip(value); maximum = longestLine(stripped); @@ -87,7 +87,7 @@ void ColumnUDAString::render(std::vector& lines, const Task& task, Color& color) { if (task.has(_name)) { if (_style == "default") { - std::string value = task.get_ref(_name); + const auto& value = task.get_ref(_name); std::vector raw; wrapText(raw, value, width, _hyphenate); @@ -131,7 +131,7 @@ void ColumnUDANumeric::measure(const Task& task, unsigned int& minimum, unsigned minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - auto value = task.get_ref(_name); + const auto& value = task.get_ref(_name); if (value != "") minimum = maximum = value.length(); } else if (_style == "indicator") { auto indicator = Context::getContext().config.get("uda." + _name + ".indicator"); @@ -147,7 +147,7 @@ void ColumnUDANumeric::render(std::vector& lines, const Task& task, Color& color) { if (task.has(_name)) { if (_style == "default") { - auto value = task.get_ref(_name); + const auto& value = task.get_ref(_name); renderStringRight(lines, width, color, value); } else if (_style == "indicator") { auto indicator = Context::getContext().config.get("uda." + _name + ".indicator"); @@ -188,7 +188,7 @@ void ColumnUDADate::measure(const Task& task, unsigned int& minimum, unsigned in minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - auto value = task.get_ref(_name); + const auto& value = task.get_ref(_name); if (value != "") { // Determine the output date format, which uses a hierarchy of definitions. // rc.report..dateformat @@ -215,7 +215,7 @@ void ColumnUDADate::render(std::vector& lines, const Task& task, in Color& color) { if (task.has(_name)) { if (_style == "default") { - auto value = task.get_ref(_name); + const auto& value = task.get_ref(_name); // Determine the output date format, which uses a hierarchy of definitions. // rc.report..dateformat @@ -268,7 +268,7 @@ void ColumnUDADuration::measure(const Task& task, unsigned int& minimum, unsigne minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - auto value = task.get_ref(_name); + const auto& value = task.get_ref(_name); if (value != "") minimum = maximum = Duration(value).formatISO().length(); } else if (_style == "indicator") { if (task.has(_name)) { @@ -287,7 +287,7 @@ void ColumnUDADuration::render(std::vector& lines, const Task& task Color& color) { if (task.has(_name)) { if (_style == "default") { - auto value = task.get_ref(_name); + const auto& value = task.get_ref(_name); renderStringRight(lines, width, color, Duration(value).formatISO()); } else if (_style == "indicator") { auto indicator = Context::getContext().config.get("uda." + _name + ".indicator"); diff --git a/src/feedback.cpp b/src/feedback.cpp index 83634f39d..b33176131 100644 --- a/src/feedback.cpp +++ b/src/feedback.cpp @@ -154,9 +154,8 @@ void feedback_unblocked(const Task& task) { if (i.id) std::cout << format("Unblocked {1} '{2}'.", i.id, i.get_ref("description")) << "\n"; else { - std::string uuid = i.get("uuid"); - std::cout << format("Unblocked {1} '{2}'.", i.get("uuid"), i.get_ref("description")) - << "\n"; + const auto& uuid = i.get_ref("uuid"); + std::cout << format("Unblocked {1} '{2}'.", uuid, i.get_ref("description")) << "\n"; } } } @@ -183,7 +182,7 @@ void feedback_backlog() { /////////////////////////////////////////////////////////////////////////////// std::string onProjectChange(Task& task, bool scope /* = true */) { std::stringstream msg; - std::string project = task.get_ref("project"); + const auto& project = task.get_ref("project"); if (project != "") { if (scope) msg << format("The project '{1}' has changed.", project) << " "; @@ -221,7 +220,7 @@ std::string onProjectChange(Task& task, bool scope /* = true */) { /////////////////////////////////////////////////////////////////////////////// std::string onProjectChange(Task& task1, Task& task2) { - if (task1.get_ref("project") == task2.get("project")) return onProjectChange(task1, false); + if (task1.get_ref("project") == task2.get_ref("project")) return onProjectChange(task1, false); std::string messages1 = onProjectChange(task1); std::string messages2 = onProjectChange(task2); From 6a14fd728c68df74d01a6deaa9f9cf7763acff50 Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 15 Aug 2026 14:13:19 +0100 Subject: [PATCH 14/49] Cleaned up implementation of previous commits. Signed-off-by: ashprice --- src/Filter.cpp | 104 +++++++++++++------------------------ src/Filter.h | 3 +- src/commands/CmdCustom.cpp | 55 +++++++++++++------- 3 files changed, 73 insertions(+), 89 deletions(-) diff --git a/src/Filter.cpp b/src/Filter.cpp index 76da00fa5..d78059e35 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -44,31 +44,7 @@ void Filter::subset(const std::vector& input, std::vector& output) { Context::getContext().cli2.prepareFilter(); - std::vector> precompiled; - for (auto& a : Context::getContext().cli2._args) - if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); - - if (precompiled.size()) { - Eval eval; - eval.addSource(domSource); - - // Debug output from Eval during compilation is useful. During evaluation - // it is mostly noise. - eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3 ? true : false); - eval.compileExpression(precompiled); - - for (auto& task : input) { - // Set up context for any DOM references. - auto currentTask = Context::getContext().withCurrentTask(&task); - - Variant var; - eval.evaluateCompiledExpression(var); - if (var.get_bool()) output.push_back(task); - } - - eval.debug(false); - } else - output = input; + filter_to_tasks(input, output); _endCount = (int)output.size(); Context::getContext().debug( @@ -95,23 +71,9 @@ void Filter::subset(std::vector& output) { Context::getContext().time_filter_us -= timer_pending.total_us(); _startCount = (int)pending.size(); - Eval eval; - eval.addSource(domSource); - - // Debug output from Eval during compilation is useful. During evaluation - // it is mostly noise. - eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3 ? true : false); - eval.compileExpression(precompiled); - output.clear(); - for (auto& task : pending) { - // Set up context for any DOM references. - auto currentTask = Context::getContext().withCurrentTask(&task); - Variant var; - eval.evaluateCompiledExpression(var); - if (var.get_bool()) output.push_back(task); - } + filter_to_tasks(pending, output); shortcut = pendingOnly(); if (!shortcut) { @@ -120,17 +82,8 @@ void Filter::subset(std::vector& output) { Context::getContext().time_filter_us -= timer_completed.total_us(); _startCount += (int)completed.size(); - for (auto& task : completed) { - // Set up context for any DOM references. - auto currentTask = Context::getContext().withCurrentTask(&task); - - Variant var; - eval.evaluateCompiledExpression(var); - if (var.get_bool()) output.push_back(task); - } + filter_to_tasks(completed, output); } - - eval.debug(false); } else { safety(); @@ -146,28 +99,30 @@ void Filter::subset(std::vector& output) { } ///////////////////////////////////////////////////////////////////////////////// -// Like subset(), but leverages the pending tasks cache. -void Filter::subset_indices(const std::vector& pending, std::vector& indices) { - Timer timer; - Context::getContext().cli2.prepareFilter(); +bool Filter::hasFilter() const { + for (const auto& a : Context::getContext().cli2._args) + if (a.hasTag("FILTER")) return true; + return false; +} + +///////////////////////////////////////////////////////////////////////////////// +// Evaluates a pre-parsed filter against a set of tasks and stores their indices +// from the vector. The filter is parsed with prepareFilter(). This function does +// not call safety(), it is assumed that callers do this themselves. +void Filter::filter_to_indices(const std::vector& pending, std::vector& indices) const { std::vector> precompiled; for (auto& a : Context::getContext().cli2._args) if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); if (precompiled.empty()) { - safety(); - indices.clear(); indices.reserve(pending.size()); for (int i = 0; i < (int)pending.size(); ++i) indices.push_back(i); - _startCount = (int)pending.size(); } else { - _startCount = (int)pending.size(); Eval eval; eval.addSource(domSource); eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3); eval.compileExpression(precompiled); - indices.clear(); for (int i = 0; i < (int)pending.size(); ++i) { auto currentTask = Context::getContext().withCurrentTask(&pending[i]); Variant var; @@ -177,18 +132,32 @@ void Filter::subset_indices(const std::vector& pending, std::vector& eval.debug(false); } - _endCount = (int)indices.size(); Context::getContext().debug( - format("Filtered {1} tasks -> {2} tasks [pending cache]", _startCount, _endCount)); - Context::getContext().time_filter_us += timer.total_us(); + format("Filtered {1} tasks --> {2} tasks [pending only]", pending.size(), indices.size())); } //////////////////////////////////////////////////////////////////////////////// -bool Filter::hasFilter() const { - for (const auto& a : Context::getContext().cli2._args) - if (a.hasTag("FILTER")) return true; +// Like filter_to_indices, but copies matched tasks into the output. +void Filter::filter_to_tasks(const std::vector& input, std::vector& output) const { + std::vector> precompiled; + for (auto& a : Context::getContext().cli2._args) + if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); - return false; + if (precompiled.empty()) { + output = input; + } else { + Eval eval; + eval.addSource(domSource); + eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3); + eval.compileExpression(precompiled); + for (auto& task : input) { + auto currentTask = Context::getContext().withCurrentTask(&task); + Variant var; + eval.evaluateCompiledExpression(var); + if (var.get_bool()) output.push_back(task); + } + eval.debug(false); + } } //////////////////////////////////////////////////////////////////////////////// @@ -196,9 +165,6 @@ bool Filter::hasFilter() const { // status values 'pending', 'waiting' or 'recurring', then the filter is // guaranteed to only need data from pending.data. bool Filter::pendingOnly() const { - // When GC is off, there are no shortcuts. - if (!Context::getContext().config.getBoolean("gc")) return false; - // To skip loading completed.data, there should be: // - 'status' in filter // - no 'completed' diff --git a/src/Filter.h b/src/Filter.h index c79cc20fd..60f672dda 100644 --- a/src/Filter.h +++ b/src/Filter.h @@ -38,9 +38,10 @@ class Filter { void subset(const std::vector&, std::vector&); void subset(std::vector&); - void subset_indices(const std::vector& pending, std::vector& indices); bool hasFilter() const; bool pendingOnly() const; + void filter_to_indices(const std::vector&, std::vector&) const; + void filter_to_tasks(const std::vector&, std::vector&) const; void safety() const; void disableSafety(); diff --git a/src/commands/CmdCustom.cpp b/src/commands/CmdCustom.cpp index 8bba6c902..d14a84e8e 100644 --- a/src/commands/CmdCustom.cpp +++ b/src/commands/CmdCustom.cpp @@ -101,27 +101,44 @@ int CmdCustom::execute(std::string& output) { // Add the report filter to any existing filter. if (reportFilter != "") Context::getContext().cli2.addFilter(reportFilter); - // Apply filter. + // Apply filter. When the filter is pending only, we use indices with the pending + // cache. Otherwise we fall back to copying all tasks. Filter filter; + const std::vector* data = nullptr; + std::vector sequence; std::vector filtered; - filter.subset(filtered); - std::vector sequence; + // call prepareFilter() here so that filter_to_indices and filter_to_tasks + // don't have to call it. + Context::getContext().cli2.prepareFilter(); + + bool use_pending_indices = filter.pendingOnly(); + + if (use_pending_indices) { + const auto& pending = Context::getContext().tdb2.pending_tasks(); + filter.filter_to_indices(pending, sequence); + data = &pending; + } else { + auto all = Context::getContext().tdb2.all_tasks(); + filter.filter_to_tasks(all, filtered); + sequence.clear(); + for (unsigned int i = 0; i < filtered.size(); ++i) sequence.push_back(i); + data = &filtered; + } + if (sortOrder.size() && sortOrder[0] == "none") { - // Assemble a sequence vector that represents the tasks listed in - // Context::getContext ().cli2._uuid_ranges, in the order in which they appear. This - // equates to no sorting, just a specified order. + // If there is no sort order, we preserve the order they were specified + // and rebuild the sequence using indices. sortOrder.clear(); + sequence.clear(); for (auto& i : Context::getContext().cli2._uuid_list) - for (unsigned int t = 0; t < filtered.size(); ++t) - if (filtered[t].get("uuid") == i) sequence.push_back(t); + for (unsigned int t = 0; t < data->size(); ++t) + if ((*data)[t].get_ref("uuid") == i) sequence.push_back(t); } else { - // There is a sortOrder, so sorting will take place, which means the initial - // order of sequence is ascending. - for (unsigned int i = 0; i < filtered.size(); ++i) sequence.push_back(i); - - // Sort the tasks. - if (sortOrder.size()) sort_tasks(filtered, sequence, reportSort); + if (sortOrder.size()) { + // If there is a sortOrder, sorting will reorder the indices. + sort_tasks(*data, sequence, reportSort); + } } // Configure the view. @@ -197,19 +214,19 @@ int CmdCustom::execute(std::string& output) { // Render. std::stringstream out; - if (filtered.size()) { + if (sequence.size()) { view.truncateRows(maxrows); view.truncateLines(maxlines); - out << optionalBlankLine() << view.render(filtered, sequence) << optionalBlankLine(); + out << optionalBlankLine() << view.render(*data, sequence) << optionalBlankLine(); // Print the number of rendered tasks if (Context::getContext().verbose("affected")) { - out << (filtered.size() == 1 ? "1 task" : format("{1} tasks", filtered.size())); + out << (sequence.size() == 1 ? "1 task" : format("{1} tasks", sequence.size())); - if (maxrows && maxrows < (int)filtered.size()) out << ", " << format("{1} shown", maxrows); + if (maxrows && maxrows < (int)sequence.size()) out << ", " << format("{1} shown", maxrows); - if (maxlines && maxlines < (int)filtered.size()) + if (maxlines && maxlines < (int)sequence.size()) out << ", " << format("truncated to {1} lines", maxlines - table_header); out << '\n'; From c8046832fe8d0abf895098eae8937c9731a8f034 Mon Sep 17 00:00:00 2001 From: ashprice Date: Tue, 25 Aug 2026 20:30:02 +0100 Subject: [PATCH 15/49] Comments. Signed-off-by: ashprice --- src/Filter.cpp | 6 +++--- src/Filter.h | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Filter.cpp b/src/Filter.cpp index d78059e35..0ee397f53 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -62,7 +62,7 @@ void Filter::subset(std::vector& output) { for (auto& a : Context::getContext().cli2._args) if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); - // Shortcut indicates that only pending.data needs to be loaded. + // Shortcut indicates that only tasks in the working set are loaded. bool shortcut = false; if (precompiled.size()) { @@ -108,8 +108,8 @@ bool Filter::hasFilter() const { ///////////////////////////////////////////////////////////////////////////////// // Evaluates a pre-parsed filter against a set of tasks and stores their indices -// from the vector. The filter is parsed with prepareFilter(). This function does -// not call safety(), it is assumed that callers do this themselves. +// from the vector. The filter is parsed with prepareFilter(), but this +// function does not call that or safety() itself - callers are expected to do so. void Filter::filter_to_indices(const std::vector& pending, std::vector& indices) const { std::vector> precompiled; for (auto& a : Context::getContext().cli2._args) diff --git a/src/Filter.h b/src/Filter.h index 60f672dda..dfd80a3e9 100644 --- a/src/Filter.h +++ b/src/Filter.h @@ -40,8 +40,13 @@ class Filter { void subset(std::vector&); bool hasFilter() const; bool pendingOnly() const; + + // These evaluate already-prepared filters (ie. caller calls + // cli2.prepareFilter() and safety()). filter_to_indices() stores matching + // indices, filter_to_tasks() copies task objects. void filter_to_indices(const std::vector&, std::vector&) const; void filter_to_tasks(const std::vector&, std::vector&) const; + void safety() const; void disableSafety(); From 36841b53b00200aa741214fc2509f09e678644f9 Mon Sep 17 00:00:00 2001 From: ashprice Date: Tue, 25 Aug 2026 20:51:09 +0100 Subject: [PATCH 16/49] Precompute duration values and UDA columns for sort Signed-off-by: ashprice --- src/sort.cpp | 87 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/src/sort.cpp b/src/sort.cpp index 76ce79630..226007f43 100644 --- a/src/sort.cpp +++ b/src/sort.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include static const std::vector* global_data = nullptr; @@ -48,6 +49,14 @@ static std::vector global_keys; static unsigned int sort_random_seed = 0; static bool sort_compare(int, int); +// Pre-computed values to avoid repeated parsing. Duration fields are parsed +// once into time_t and stored by field name (recur and duration UDAs). +static std::unordered_map> global_durations; + +// UDA column details to avoid repeated lookups. +static std::unordered_map global_uda_columns; +static std::unordered_map global_uda_types; + //////////////////////////////////////////////////////////////////////////////// void sort_tasks(const std::vector& data, std::vector& order, const std::string& keys) { Timer timer; @@ -56,6 +65,41 @@ void sort_tasks(const std::vector& data, std::vector& order, const st // Split the key defs. global_keys = split(keys, ','); + // Pre-computing of sorting values. + global_durations.clear(); + global_uda_columns.clear(); + global_uda_types.clear(); + for (auto& k : global_keys) { + std::string field; + bool ascending, breakIndicator; + Context::getContext().decomposeSortField(k, field, ascending, breakIndicator); + + if (field == "recur") { + auto& cache = global_durations[field]; + cache.resize(data.size(), 0); + for (size_t i = 0; i < data.size(); ++i) { + auto s = data[i].get_ref("recur"); + if (!s.empty()) cache[i] = Duration(s).toTime_t(); + } + continue; + } + + auto col_it = Context::getContext().columns.find(field); + if (col_it != Context::getContext().columns.end()) { + Column* col = col_it->second; + global_uda_columns[field] = col; + global_uda_types[field] = col->type(); + if (col->type() == "duration") { + auto& cache = global_durations[field]; + cache.resize(data.size(), 0); + for (size_t i = 0; i < data.size(); ++i) { + auto s = data[i].get_ref(field); + if (!s.empty()) cache[i] = Duration(s).toTime_t(); + } + } + } + } + // Generate a random seend for sorting by "random". if (sort_random_seed == 0) { // For testing purposes, allow the seed to be specified in an undocumented configuration @@ -117,7 +161,6 @@ static bool sort_compare(int left, int right) { std::string field; bool ascending; bool breakIndicator; - Column* column; int left_number; int right_number; float left_real; @@ -215,19 +258,23 @@ static bool sort_compare(int left, int right) { // Duration. else if (field == "recur") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); - - if (left_string == right_string) continue; - - Duration left_duration(left_string); - Duration right_duration(right_string); - return ascending ? (left_duration < right_duration) : (left_duration > right_duration); + auto it = global_durations.find(field); + if (it != global_durations.end()) { + auto left_dur = it->second[left]; + auto right_dur = it->second[right]; + if (left_dur == right_dur) continue; + return ascending ? (left_dur < right_dur) : (left_dur > right_dur); + } } // UDAs. - else if ((column = Context::getContext().columns[field]) != nullptr) { - std::string type = column->type(); + else { + auto cuda = global_uda_columns.find(field); + if (cuda == global_uda_columns.end()) + throw format("The '{1}' column is not a valid sort field.", field); + + const std::string& type = global_uda_types[field]; + if (type == "numeric") { auto left_real = strtof(((*global_data)[left].get_ref(field)).c_str(), nullptr); auto right_real = strtof(((*global_data)[right].get_ref(field)).c_str(), nullptr); @@ -271,17 +318,15 @@ static bool sort_compare(int left, int right) { return ascending ? (left_string < right_string) : (left_string > right_string); } else if (type == "duration") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); - - if (left_string == right_string) continue; - - Duration left_duration(left_string); - Duration right_duration(right_string); - return ascending ? (left_duration < right_duration) : (left_duration > right_duration); + auto it = global_durations.find(field); + if (it != global_durations.end()) { + auto left_dur = it->second[left]; + auto right_dur = it->second[right]; + if (left_dur == right_dur) continue; + return ascending ? (left_dur < right_dur) : (left_dur > right_dur); + } } - } else - throw format("The '{1}' column is not a valid sort field.", field); + } } return false; From eb6cc8156d31789556def8991d8675aeaf54704c Mon Sep 17 00:00:00 2001 From: ashprice Date: Tue, 25 Aug 2026 21:18:02 +0100 Subject: [PATCH 17/49] Extend get_ref() approach to ColDescription and ColProject Signed-off-by: ashprice --- src/columns/ColDescription.cpp | 4 ++- src/columns/ColProject.cpp | 52 +++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/columns/ColDescription.cpp b/src/columns/ColDescription.cpp index 168a13ebb..757d4250a 100644 --- a/src/columns/ColDescription.cpp +++ b/src/columns/ColDescription.cpp @@ -72,7 +72,7 @@ ColumnDescription::ColumnDescription() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. void ColumnDescription::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { - std::string description = task.get(_name); + const auto& description = task.get_ref(_name); // The text // @@ -132,6 +132,8 @@ void ColumnDescription::measure(const Task& task, unsigned int& minimum, unsigne //////////////////////////////////////////////////////////////////////////////// void ColumnDescription::render(std::vector& lines, const Task& task, int width, Color& color) { + // Unlike measure, we use get() here because this render function appends + // annotation counts or strings. std::string description = task.get(_name); // This is a description diff --git a/src/columns/ColProject.cpp b/src/columns/ColProject.cpp index d200c554e..c8de1807d 100644 --- a/src/columns/ColProject.cpp +++ b/src/columns/ColProject.cpp @@ -53,17 +53,23 @@ ColumnProject::ColumnProject() { void ColumnProject::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { - std::string project = task.get(_name); + if (_style == "full") { + const auto& project = task.get_ref(_name); + minimum = longestWord(project); + maximum = utf8_width(project); + } else { + std::string project = task.get(_name); - if (_style == "parent") { - auto period = project.find('.'); - if (period != std::string::npos) project = project.substr(0, period); - } else if (_style == "indented") { - project = indentProject(project, " ", '.'); - } + if (_style == "parent") { + auto period = project.find('.'); + if (period != std::string::npos) project = project.substr(0, period); + } else if (_style == "indented") { + project = indentProject(project, " ", '.'); + } - minimum = longestWord(project); - maximum = utf8_width(project); + minimum = longestWord(project); + maximum = utf8_width(project); + } } } @@ -71,18 +77,24 @@ void ColumnProject::measure(const Task& task, unsigned int& minimum, unsigned in void ColumnProject::render(std::vector& lines, const Task& task, int width, Color& color) { if (task.has(_name)) { - std::string project = task.get(_name); - if (_style == "parent") { - auto period = project.find('.'); - if (period != std::string::npos) project = project.substr(0, period); - } else if (_style == "indented") { - project = indentProject(project, " ", '.'); - } + if (_style == "full") { + const auto& project = task.get_ref(_name); + std::vector raw; + wrapText(raw, project, width, _hyphenate); + for (const auto& i : raw) renderStringLeft(lines, width, color, i); + } else { + std::string project = task.get(_name); + if (_style == "parent") { + auto period = project.find('.'); + if (period != std::string::npos) project = project.substr(0, period); + } else if (_style == "indented") { + project = indentProject(project, " ", '.'); + } - std::vector raw; - wrapText(raw, project, width, _hyphenate); - - for (const auto& i : raw) renderStringLeft(lines, width, color, i); + std::vector raw; + wrapText(raw, project, width, _hyphenate); + for (const auto& i : raw) renderStringLeft(lines, width, color, i); + } } } From f963136b86645b875fb65d60430e2decc31a082d Mon Sep 17 00:00:00 2001 From: ashprice Date: Tue, 25 Aug 2026 21:24:41 +0100 Subject: [PATCH 18/49] Precompute sorted dependency field to avoid repeated getDependencyUUID() calls Signed-off-by: ashprice --- src/sort.cpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/sort.cpp b/src/sort.cpp index 226007f43..45d3492fa 100644 --- a/src/sort.cpp +++ b/src/sort.cpp @@ -53,6 +53,9 @@ static bool sort_compare(int, int); // once into time_t and stored by field name (recur and duration UDAs). static std::unordered_map> global_durations; +// Pre-computed dependency UUIDs to avoid repeated sort calls. +static std::vector> global_sorted_dep_uuids; + // UDA column details to avoid repeated lookups. static std::unordered_map global_uda_columns; static std::unordered_map global_uda_types; @@ -69,11 +72,21 @@ void sort_tasks(const std::vector& data, std::vector& order, const st global_durations.clear(); global_uda_columns.clear(); global_uda_types.clear(); + global_sorted_dep_uuids.clear(); for (auto& k : global_keys) { std::string field; bool ascending, breakIndicator; Context::getContext().decomposeSortField(k, field, ascending, breakIndicator); + if (field == "depends") { + global_sorted_dep_uuids.resize(data.size()); + for (size_t i = 0; i < data.size(); ++i) { + auto deps = data[i].getDependencyUUIDs(); + std::sort(deps.begin(), deps.end()); + global_sorted_dep_uuids[i] = std::move(deps); + } + } + if (field == "recur") { auto& cache = global_durations[field]; cache.resize(data.size(), 0); @@ -234,12 +247,8 @@ static bool sort_compare(int left, int right) { // Depends string. else if (field == "depends") { - // Raw data is an un-sorted list of UUIDs. We just need a stable - // sort, so we sort them lexically. - auto left_deps = (*global_data)[left].getDependencyUUIDs(); - std::sort(left_deps.begin(), left_deps.end()); - auto right_deps = (*global_data)[right].getDependencyUUIDs(); - std::sort(right_deps.begin(), right_deps.end()); + const auto& left_deps = global_sorted_dep_uuids[left]; + const auto& right_deps = global_sorted_dep_uuids[right]; if (left_deps == right_deps) continue; From 498dca6f565f5e0061e148bad99e05aae7d0d3a4 Mon Sep 17 00:00:00 2001 From: ashprice Date: Mon, 24 Aug 2026 06:09:26 +0100 Subject: [PATCH 19/49] Use the pending tasks cache in the circular dependency check. Signed-off-by: ashprice --- src/TDB2.cpp | 1 + test/dependencies.test.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/TDB2.cpp b/src/TDB2.cpp index d2278f845..befe6521c 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -380,6 +380,7 @@ const std::unordered_map& TDB2::pending_index() { // Finds the UUID in the index. Returns nullptr if the task is not in the pending // set. Task* TDB2::find_pending(const std::string& uuid) { + pending_tasks(); auto& idx = pending_index(); auto it = idx.find(uuid); if (it != idx.end()) return &(*_pending_tasks)[it->second]; diff --git a/test/dependencies.test.py b/test/dependencies.test.py index b3f5dec39..951129ec3 100755 --- a/test/dependencies.test.py +++ b/test/dependencies.test.py @@ -96,6 +96,38 @@ def test_dag(self): code, out, err = self.t("1 modify dep:2") self.assertNotIn("Circular dependency detected and disallowed.", err) + def test_completed_not_circular(self): + """Checks that completed tasks do not contribute to circularity check""" + self.t("2 modify dep:1") + completed_uuid = self.t.export_one(2)["uuid"] + self.t("2 done") + self.t("next") + + self.t("1 modify dep:%s" % completed_uuid) + self.assertEqual([completed_uuid], self.t.export_one(1)["depends"]) + + def test_circular_with_visited_sibling(self): + """Checks that the branching logic in the check still catches after + visited siblings""" + self.t("add three") + self.t("add four") + a, b, x, root = sorted(task["uuid"] for task in self.t.export()) + self.t("%s modify dep:%s" % (a, root)) + self.t("%s modify dep:%s,%s" % (x, a, b)) + + code, out, err = self.t.runError("%s modify dep:%s,%s" % (root, b, x)) + self.assertIn("Circular dependency detected and disallowed.", err) + + def test_dependency_modify_with_cold_pending_cache(self): + """Checks the pending cache is initialized on dependency modifications""" + missing_uuid = "90000000-0000-0000-0000-000000000000" + self.t( + "rc.allow.empty.filter=1 rc.bulk=0 modify depends:%s" % missing_uuid, + input="yes\n", + ) + for task in self.t.export(): + self.assertEqual([missing_uuid], task["depends"]) + def test_blocked_blocking(self): """Check blocked/blocking status of two tasks""" self.t("2 modify dep:1") From caf7d2bfa3362f1d83dfcb9c51e5c7e17ca707e2 Mon Sep 17 00:00:00 2001 From: ashprice Date: Thu, 27 Aug 2026 16:28:22 +0100 Subject: [PATCH 20/49] avoid cache loads during bulk operations, update cache flags individually, add tests Signed-off-by: ashprice --- src/TDB2.cpp | 92 +++++++++++++++++++++++++++++++++++--- src/TDB2.h | 1 + src/Task.cpp | 9 ++-- src/Task.h | 3 +- src/commands/CmdDelete.cpp | 2 +- src/commands/CmdDone.cpp | 2 +- src/commands/CmdImport.cpp | 5 ++- src/dependency.cpp | 2 + test/completed.test.py | 10 +++++ test/tdb2_test.cpp | 53 +++++++++++++++++++++- 10 files changed, 163 insertions(+), 16 deletions(-) diff --git a/src/TDB2.cpp b/src/TDB2.cpp index befe6521c..390fb405d 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -42,9 +43,15 @@ #include #include +#include "taskchampion-cpp/lib.h" + bool TDB2::debug_mode = false; // This functions main job is to set Task::is_blocked / Task::is_blocking flags. -static void dependency_scan(std::vector&, const std::unordered_map&); +static void dependency_scan(std::vector&, const std::unordered_map&, + std::unordered_map* = nullptr); +static void dependency_update(std::vector&, const std::unordered_map&, + std::unordered_map&, size_t, + const std::vector&, const std::vector&); // Build maps for dependency queries. static DependencyGraph build_dependency_graph(const std::vector&, @@ -124,10 +131,19 @@ void TDB2::modify(Task& task) { // invoke the hook and allow it to modify the task before updating Task original; - bool found_original = get(uuid, original); + bool found_original = false; + tc::Uuid tcuuid = tc::uuid_from_string(uuid); + if (!_pending_tasks && task.id > 0) { + auto maybe_original = replica()->get_task_data(tcuuid); + if (maybe_original.is_some()) { + original = Task{maybe_original.take(), task.id}; + found_original = true; + } else { + found_original = get(uuid, original); + } + } Context::getContext().hooks.onModify(original, task); - tc::Uuid tcuuid = tc::uuid_from_string(uuid); auto maybe_tctask = replica()->get_task_data(tcuuid); if (maybe_tctask.is_none()) { throw std::string("task no longer exists"); @@ -191,7 +207,9 @@ void TDB2::modify(Task& task) { auto new_deps = task.getDependencyUUIDs(); if (old_deps != new_deps) { deps_changed = true; - dependency_scan(*_pending_tasks, pending_index()); + auto& index = pending_index(); + dependency_update(*_pending_tasks, index, *_pending_dependency_counts, index.at(uuid), + old_deps, new_deps); } } } @@ -317,7 +335,8 @@ const std::vector& TDB2::pending_tasks() { for (size_t i = 0, n = result.size(); i < n; ++i) _pending_index->emplace(result[i].get_ref("uuid"), i); - dependency_scan(result, *_pending_index); + _pending_dependency_counts.emplace(); + dependency_scan(result, *_pending_index, &*_pending_dependency_counts); Context::getContext().time_load_us += timer.total_us(); _pending_tasks = std::move(result); @@ -367,6 +386,7 @@ const DependencyGraph& TDB2::dependency_graph() { ///////////////////////////////////////////////////////////////////////////////// // This builds and returns the UUID map if it is missing. const std::unordered_map& TDB2::pending_index() { + pending_tasks(); if (!_pending_index) { _pending_index.emplace(); _pending_index->reserve(_pending_tasks->size()); @@ -380,7 +400,6 @@ const std::unordered_map& TDB2::pending_index() { // Finds the UUID in the index. Returns nullptr if the task is not in the pending // set. Task* TDB2::find_pending(const std::string& uuid) { - pending_tasks(); auto& idx = pending_index(); auto it = idx.find(uuid); if (it != idx.end()) return &(*_pending_tasks)[it->second]; @@ -394,6 +413,7 @@ void TDB2::invalidate_cached_info() { _working_set = std::nullopt; _dependency_graph = std::nullopt; _pending_index = std::nullopt; + _pending_dependency_counts = std::nullopt; } //////////////////////////////////////////////////////////////////////////////// @@ -428,6 +448,18 @@ bool TDB2::get(const std::string& uuid, Task& task) { return true; } + if (uuid.length() == 36) { + Lexer lexer(uuid); + Lexer::Type type; + std::string token; + if (lexer.isUUID(token, type, true) && token.length() == uuid.length()) { + auto maybe_tctask = replica()->get_task_data(tc::uuid_from_string(uuid)); + if (maybe_tctask.is_none()) return false; + task = Task{maybe_tctask.take()}; + return true; + } + } + // try a partial match if (uuid.length() < 36) { for (const auto& pending_task : *_pending_tasks) { @@ -519,7 +551,9 @@ int TDB2::num_reverts_possible() { return (int)replica()->num_undo_points(); } //////////////////////////////////////////////////////////////////////////////// // Set Task::is_blocked / Task::is_blocking flags using the pre-built UUID map static void dependency_scan(std::vector& tasks, - const std::unordered_map& uuid_index) { + const std::unordered_map& uuid_index, + std::unordered_map* dependency_counts) { + if (dependency_counts) dependency_counts->clear(); // Reset all flags first. This is for safety reasons - if we don't do this // dependency_scan() only sets them to true, so it can stay true (within the cache) // even after we have changed a task's dependencies after a modify when @@ -542,11 +576,55 @@ static void dependency_scan(std::vector& tasks, rstatus != Task::deleted) { tasks[i].is_blocked = true; tasks[j].is_blocking = true; + if (dependency_counts) ++(*dependency_counts)[dep]; } } } } +///////////////////////////////////////////////////////////////////////////////// +static void dependency_update(std::vector& tasks, + const std::unordered_map& uuid_index, + std::unordered_map& dependency_counts, + size_t task_index, const std::vector& old_deps, + const std::vector& new_deps) { + std::unordered_set old_set(old_deps.begin(), old_deps.end()); + std::unordered_set new_set(new_deps.begin(), new_deps.end()); + auto& task = tasks[task_index]; + + task.is_blocked = false; + auto task_status = task.getStatus(); + bool task_is_pending = task_status != Task::completed && task_status != Task::deleted; + + for (const auto& dep : old_set) { + if (new_set.find(dep) != new_set.end()) continue; + auto target = uuid_index.find(dep); + if (target == uuid_index.end()) continue; + + auto count = dependency_counts.find(dep); + if (count != dependency_counts.end()) { + if (--count->second == 0) dependency_counts.erase(count); + } + tasks[target->second].is_blocking = dependency_counts.find(dep) != dependency_counts.end(); + } + + for (const auto& dep : new_set) { + auto target = uuid_index.find(dep); + if (target == uuid_index.end()) continue; + + auto target_status = tasks[target->second].getStatus(); + bool target_is_pending = target_status != Task::completed && target_status != Task::deleted; + if (!task_is_pending || !target_is_pending) continue; + + task.is_blocked = true; + if (old_set.find(dep) == old_set.end()) ++dependency_counts[dep]; + tasks[target->second].is_blocking = true; + } + + const auto& uuid = task.get_ref("uuid"); + task.is_blocking = dependency_counts.find(uuid) != dependency_counts.end(); +} + ///////////////////////////////////////////////////////////////////////////////// // Build the full dependency map from the task vector. static DependencyGraph build_dependency_graph( diff --git a/src/TDB2.h b/src/TDB2.h index a3d671c59..4c38552ec 100644 --- a/src/TDB2.h +++ b/src/TDB2.h @@ -111,6 +111,7 @@ class TDB2 { // Lazily cache UUIDs within the pending set. // Avoids scans of the vectors with get/modify.. std::optional> _pending_index; + std::optional> _pending_dependency_counts; void invalidate_cached_info(); // Return the full pending UUID map. diff --git a/src/Task.cpp b/src/Task.cpp index 9aa09c345..f3525336e 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -141,7 +141,9 @@ Task::Task(const json::object* obj) { } //////////////////////////////////////////////////////////////////////////////// -Task::Task(rust::Box obj) { +Task::Task(rust::Box obj) : Task(std::move(obj), -1) {} + +Task::Task(rust::Box obj, int known_id) { id = 0; urgency_value = 0.0; recalc_urgency = true; @@ -149,7 +151,7 @@ Task::Task(rust::Box obj) { is_blocking = false; annotation_count = 0; - parseTC(std::move(obj)); + parseTC(std::move(obj), known_id); } //////////////////////////////////////////////////////////////////////////////// @@ -720,7 +722,7 @@ void Task::parseJSON(const json::object* root_obj) { //////////////////////////////////////////////////////////////////////////////// // Note that all fields undergo encode/decode. -void Task::parseTC(rust::Box task) { +void Task::parseTC(rust::Box task, int known_id) { auto items = task->items(); data.clear(); @@ -736,6 +738,7 @@ void Task::parseTC(rust::Box task) { data["uuid"] = static_cast(task->get_uuid().to_string()); id = Context::getContext().tdb2.id(data["uuid"]); + id = known_id >= 0 ? known_id : Context::getContext().tdb2.id(data["uuid"]); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/Task.h b/src/Task.h index cacf12fcd..e1859245a 100644 --- a/src/Task.h +++ b/src/Task.h @@ -79,6 +79,7 @@ class Task { Task(const std::string&); Task(const json::object*); Task(rust::Box); + Task(rust::Box, int); void parse(const std::string&); std::string composeJSON(bool decorate = false); @@ -202,7 +203,7 @@ class Task { int determineVersion(const std::string&); void parseJSON(const std::string&); void parseJSON(const json::object*); - void parseTC(rust::Box); + void parseTC(rust::Box, int); void parseLegacy(const std::string&); void validate_before(const std::string&, const std::string&); const std::string encode(const std::string&) const; diff --git a/src/commands/CmdDelete.cpp b/src/commands/CmdDelete.cpp index 35b5c7ecf..b548ea983 100644 --- a/src/commands/CmdDelete.cpp +++ b/src/commands/CmdDelete.cpp @@ -96,7 +96,7 @@ int CmdDelete::execute(std::string&) { ++count; Context::getContext().tdb2.modify(task); feedback_affected("Deleting task {1} '{2}'.", task); - feedback_unblocked(task); + if (task.is_blocking) feedback_unblocked(task); dependencyChainOnComplete(task); if (Context::getContext().verbose("project")) projectChanges[task.get("project")] = onProjectChange(task); diff --git a/src/commands/CmdDone.cpp b/src/commands/CmdDone.cpp index deb10a0dc..e6ad19ee9 100644 --- a/src/commands/CmdDone.cpp +++ b/src/commands/CmdDone.cpp @@ -101,7 +101,7 @@ int CmdDone::execute(std::string&) { Context::getContext().tdb2.modify(task); ++count; feedback_affected("Completed task {1} '{2}'.", task); - feedback_unblocked(task); + if (task.is_blocking) feedback_unblocked(task); dependencyChainOnComplete(task); if (Context::getContext().verbose("project")) projectChanges[task.get("project")] = onProjectChange(task); diff --git a/src/commands/CmdImport.cpp b/src/commands/CmdImport.cpp index b3ccc0c2a..7260c4218 100644 --- a/src/commands/CmdImport.cpp +++ b/src/commands/CmdImport.cpp @@ -189,7 +189,8 @@ void CmdImport::importSingleTask(json::object* obj) { Task before; auto uuid = task.get("uuid"); uuid_occurrences[uuid]++; - if (Context::getContext().tdb2.get(uuid, before)) { + auto& tdb2 = Context::getContext().tdb2; + if (tdb2.has(uuid) && tdb2.get(uuid, before)) { // We need to neglect updates from attributes with dynamic defaults // unless they have been explicitly specified on import. // @@ -218,7 +219,7 @@ void CmdImport::importSingleTask(json::object* obj) { std::cout << " skip "; } } else { - Context::getContext().tdb2.add(task); + tdb2.add(task); std::cout << " add "; } diff --git a/src/dependency.cpp b/src/dependency.cpp index 274603be2..a6933aeb0 100644 --- a/src/dependency.cpp +++ b/src/dependency.cpp @@ -107,6 +107,8 @@ bool dependencyIsCircular(const Task& task) { // 4 dep:3,5 // void dependencyChainOnComplete(Task& task) { + if (task.getDependencyUUIDs().empty() && !task.is_blocking) return; + auto blocking = task.getDependencyTasks(); // If the task is anything but the tail end of a dependency chain. diff --git a/test/completed.test.py b/test/completed.test.py index 91f612808..abfbe109f 100755 --- a/test/completed.test.py +++ b/test/completed.test.py @@ -54,6 +54,16 @@ def test_completed_report(self): self.assertNotIn("two", out) self.assertNotIn("three", out) + def test_complete_with_empty_filter(self): + """Verify that you can complete all tasks with an empty filter, + this should be successful with cold pending set cache""" + self.t("add one") + self.t("add two") + + code, out, err = self.t("rc.allow.empty.filter=1 rc.bulk=0 done", input="yes\n") + self.assertIn("Completed 2 tasks", out) + self.assertEqual([], self.t.export("status:pending")) + if __name__ == "__main__": from simpletap import TAPTestRunner diff --git a/test/tdb2_test.cpp b/test/tdb2_test.cpp index f1baf2444..f1b475e4b 100644 --- a/test/tdb2_test.cpp +++ b/test/tdb2_test.cpp @@ -44,7 +44,7 @@ void cleardb() { //////////////////////////////////////////////////////////////////////////////// int TEST_NAME(int, char**) { - UnitTest t(12); + UnitTest t(27); Context context; Context::setContext(&context); @@ -108,6 +108,57 @@ int TEST_NAME(int, char**) { cleardb(); context.tdb2.open_replica(".", /*create_if_missing=*/true, /*read_write=*/true); + Task blocker(R"([description:"blocking"])"); + Task first(R"([description:"first"])"); + Task second(R"([description:"second"])"); + + first.addDependency(blocker.get_ref("uuid")); + context.tdb2.modify(first); + second.addDependency(blocker.get_ref("uuid")); + context.tdb2.modify(second); + + t.ok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 dependency is blocking"); + t.ok(context.tdb2.find_pending(first.get_ref("uuid"))->is_blocked, + "TDB2 first dependent is blocked"); + t.ok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 second dependent is blocked"); + t.is((int)context.tdb2.dependency_graph().dependencies.size(), 2, + "TDB2 dependency graph contains two dependents"); + t.is((int)context.tdb2.dependency_graph().dependents.at(blocker.get_ref("uuid")).size(), 2, + "TDB2 dependency map contains both edges"); + + first.removeDependency(blocker.get_ref("uuid")); + context.tdb2.modify(first); + + t.ok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 dependency remains blocking"); + t.notok(context.tdb2.find_pending(first.get_ref("uuid"))->is_blocked, + "TDB2 first dependent is unblocked"); + t.ok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 second dependent remains blocked"); + t.is((int)context.tdb2.dependency_graph().dependencies.size(), 1, + "TDB2 dependency map correctly removed the first dependent"); + t.is((int)context.tdb2.dependency_graph().dependents.at(blocker.get_ref("uuid")).size(), 1, + "TDB2 dependency map contains the second edge"); + + second.removeDependency(blocker.get_ref("uuid")); + context.tdb2.modify(second); + + t.notok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 dependency no longer blocks tasks"); + t.notok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 second dependent is unblocked"); + t.is((int)context.tdb2.pending_tasks().size(), 3, + "TDB2 dependency changes keep correct pending number"); + t.is((int)context.tdb2.dependency_graph().dependencies.size(), 0, + "TDB2 dependency changes invalidated the old graph - dependencies"); + t.is((int)context.tdb2.dependency_graph().dependents.size(), 0, + "TDB2 dependency changes remove dependents"); + + // Reset for reuse. + cleardb(); + context.tdb2.open_replica(".", /*create_if_missing=*/true, /*read_write=*/true); // TODO complete a task // TODO gc } From 8d3c6ff7e7d09bdfb5d0f13ec03870ff125a916d Mon Sep 17 00:00:00 2001 From: ashprice Date: Thu, 27 Aug 2026 16:37:01 +0100 Subject: [PATCH 21/49] fixed regression: dependency id sorting Signed-off-by: ashprice --- src/Task.cpp | 6 ++++++ src/columns/ColDepends.cpp | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/Task.cpp b/src/Task.cpp index f3525336e..75b5bd2e0 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -1088,6 +1088,9 @@ std::vector Task::getDependencyTasks() const { if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) blocking.push_back(tasks[idx]); + std::sort(blocking.begin(), blocking.end(), + [](const Task& left, const Task& right) { return left.id < right.id; }); + return blocking; } @@ -1110,6 +1113,9 @@ std::vector Task::getBlockedTasks() const { if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) blocked.push_back(tasks[idx]); + std::sort(blocked.begin(), blocked.end(), + [](const Task& left, const Task& right) { return left.id < right.id; }); + return blocked; } #endif diff --git a/src/columns/ColDepends.cpp b/src/columns/ColDepends.cpp index 962a18a91..b87a301db 100644 --- a/src/columns/ColDepends.cpp +++ b/src/columns/ColDepends.cpp @@ -34,6 +34,7 @@ #include #include +#include #include #define STRING_COLUMN_LABEL_DEP "Depends" @@ -88,6 +89,8 @@ void ColumnDepends::measure(const Task& task, unsigned int& minimum, unsigned in if (id > 0) blocking_ids.push_back(id); } + std::sort(blocking_ids.begin(), blocking_ids.end()); + if (blocking_ids.size() > 0) { if (_style == "count") { minimum = maximum = 2 + format((int)blocking_ids.size()).length(); @@ -128,6 +131,8 @@ void ColumnDepends::render(std::vector& lines, const Task& task, in if (id > 0) blocking_ids.push_back(id); } + std::sort(blocking_ids.begin(), blocking_ids.end()); + if (blocking_ids.size() > 0) { if (_style == "count") { renderStringRight(lines, width, color, From b1257c1cb213633a52cda38cd503cce8d96eeeff Mon Sep 17 00:00:00 2001 From: ashprice Date: Thu, 27 Aug 2026 17:04:12 +0100 Subject: [PATCH 22/49] Fixed hooks regression + upstream bug Signed-off-by: ashprice --- src/Hooks.cpp | 6 ++++++ src/TDB2.cpp | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 1d1e440a4..33589b8c6 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -279,6 +279,9 @@ void Hooks::onModify(Task& before, Task& after) const { if (!_enabled) return; Timer timer; + const auto id = after.id; + const auto is_blocked = after.is_blocked; + const auto is_blocking = after.is_blocking; std::vector matchingScripts = scripts("on-modify"); if (matchingScripts.size()) { @@ -314,6 +317,9 @@ void Hooks::onModify(Task& before, Task& after) const { } after = Task(input[1]); + after.id = id; + after.is_blocked = is_blocked; + after.is_blocking = is_blocking; } Context::getContext().time_hooks_us += timer.total_us(); diff --git a/src/TDB2.cpp b/src/TDB2.cpp index 390fb405d..d5ae21a5d 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -138,9 +138,9 @@ void TDB2::modify(Task& task) { if (maybe_original.is_some()) { original = Task{maybe_original.take(), task.id}; found_original = true; - } else { - found_original = get(uuid, original); } + } else { + found_original = get(uuid, original); } Context::getContext().hooks.onModify(original, task); @@ -210,6 +210,8 @@ void TDB2::modify(Task& task) { auto& index = pending_index(); dependency_update(*_pending_tasks, index, *_pending_dependency_counts, index.at(uuid), old_deps, new_deps); + task.is_blocked = pt->is_blocked; + task.is_blocking = pt->is_blocking; } } } From 3cc792bf1f277d6fbb9383b0104ca0c5eee1cd9a Mon Sep 17 00:00:00 2001 From: ashprice Date: Thu, 27 Aug 2026 17:48:36 +0100 Subject: [PATCH 23/49] Waiting/random report and sorting micro-optimisations Signed-off-by: ashprice --- src/Filter.cpp | 4 ++- src/sort.cpp | 87 ++++++++++++++++++++++++++++---------------------- 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/src/Filter.cpp b/src/Filter.cpp index 0ee397f53..c3a350f78 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -183,6 +183,7 @@ bool Filter::pendingOnly() const { bool pendingTag = false; bool activeTag = false; bool readyTag = false; + bool waitingTag = false; for (const auto& a : Context::getContext().cli2._args) { if (a.hasTag("FILTER")) { @@ -203,13 +204,14 @@ bool Filter::pendingOnly() const { if (word.attribute("raw") == "+PENDING") pendingTag = true; if (word.attribute("raw") == "+ACTIVE") activeTag = true; if (word.attribute("raw") == "+READY") readyTag = true; + if (word.attribute("raw") == "+WAITING") waitingTag = true; } if (countUUID) return false; if (countOr || countXor || countNot) return false; - if (pendingTag || activeTag || readyTag) return true; + if (pendingTag || activeTag || readyTag || waitingTag) return true; if (countStatus) { if (!countPending && !countWaiting && !countRecurring) return false; diff --git a/src/sort.cpp b/src/sort.cpp index 45d3492fa..18f675b42 100644 --- a/src/sort.cpp +++ b/src/sort.cpp @@ -45,10 +45,16 @@ #include static const std::vector* global_data = nullptr; -static std::vector global_keys; static unsigned int sort_random_seed = 0; static bool sort_compare(int, int); +struct SortKey { + std::string field; + bool ascending; +}; + +static std::vector global_sort_keys; + // Pre-computed values to avoid repeated parsing. Duration fields are parsed // once into time_t and stored by field name (recur and duration UDAs). static std::unordered_map> global_durations; @@ -59,6 +65,7 @@ static std::vector> global_sorted_dep_uuids; // UDA column details to avoid repeated lookups. static std::unordered_map global_uda_columns; static std::unordered_map global_uda_types; +static std::vector global_random_keys; //////////////////////////////////////////////////////////////////////////////// void sort_tasks(const std::vector& data, std::vector& order, const std::string& keys) { @@ -66,17 +73,43 @@ void sort_tasks(const std::vector& data, std::vector& order, const st global_data = &data; // Split the key defs. - global_keys = split(keys, ','); + auto key_defs = split(keys, ','); // Pre-computing of sorting values. global_durations.clear(); global_uda_columns.clear(); global_uda_types.clear(); global_sorted_dep_uuids.clear(); - for (auto& k : global_keys) { + global_random_keys.clear(); + global_sort_keys.clear(); + global_sort_keys.reserve(key_defs.size()); + for (auto& k : key_defs) { std::string field; bool ascending, breakIndicator; Context::getContext().decomposeSortField(k, field, ascending, breakIndicator); + global_sort_keys.push_back({field, ascending}); + + // Generate a random seend for sorting by "random". + if (field == "random") { + if (sort_random_seed == 0) { + // For testing purposes, allow the seed to be specified in an undocumented configuration + // setting. + std::string seed_str = Context::getContext().config.get("debug.random.seed"); + if (seed_str.empty()) { + std::random_device rd; + sort_random_seed = rd(); + } else { + sort_random_seed = std::stoul(seed_str); + } + } + + auto seed = std::to_string(sort_random_seed); + global_random_keys.resize(data.size()); + for (size_t i = 0; i < data.size(); ++i) { + global_random_keys[i] = + std::to_string(std::hash{}(data[i].get_ref("uuid") + seed)); + } + } if (field == "depends") { global_sorted_dep_uuids.resize(data.size()); @@ -113,19 +146,6 @@ void sort_tasks(const std::vector& data, std::vector& order, const st } } - // Generate a random seend for sorting by "random". - if (sort_random_seed == 0) { - // For testing purposes, allow the seed to be specified in an undocumented configuration - // setting. - std::string seed_str = Context::getContext().config.get("debug.random.seed"); - if (seed_str.empty()) { - std::random_device rd; - sort_random_seed = rd(); - } else { - sort_random_seed = std::stoul(seed_str); - } - } - // Only sort if necessary. if (order.size()) std::stable_sort(order.begin(), order.end(), sort_compare); @@ -171,28 +191,19 @@ void sort_projects(std::list>& sorted, // // Essentially a static implementation of a dynamic operator<. static bool sort_compare(int left, int right) { - std::string field; - bool ascending; - bool breakIndicator; int left_number; int right_number; float left_real; float right_real; - for (auto& k : global_keys) { - Context::getContext().decomposeSortField(k, field, ascending, breakIndicator); + for (const auto& key : global_sort_keys) { + const auto& field = key.field; + bool ascending = key.ascending; // Random. if (field == "random") { - // For "random" sort, we produce a stable number for each task based on a hash of its - // UUID plus the random seed. - const auto& left_uuid = (*global_data)[left].get_ref("uuid"); - const auto& right_uuid = (*global_data)[right].get_ref("uuid"); - - std::string left_scrambled = - std::to_string(std::hash{}(left_uuid + std::to_string(sort_random_seed))); - std::string right_scrambled = - std::to_string(std::hash{}(right_uuid + std::to_string(sort_random_seed))); + const auto& left_scrambled = global_random_keys[left]; + const auto& right_scrambled = global_random_keys[right]; if (left_scrambled == right_scrambled) continue; @@ -222,8 +233,8 @@ static bool sort_compare(int left, int right) { // String. else if (field == "description" || field == "project" || field == "status" || field == "tags" || field == "uuid" || field == "parent" || field == "imask" || field == "mask") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); + const auto& left_string = (*global_data)[left].get_ref(field); + const auto& right_string = (*global_data)[right].get_ref(field); if (left_string == right_string) continue; @@ -233,8 +244,8 @@ static bool sort_compare(int left, int right) { // Due Date. else if (field == "due" || field == "end" || field == "entry" || field == "start" || field == "until" || field == "wait" || field == "modified" || field == "scheduled") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); + const auto& left_string = (*global_data)[left].get_ref(field); + const auto& right_string = (*global_data)[right].get_ref(field); if (left_string != "" && right_string == "") return true; @@ -292,8 +303,8 @@ static bool sort_compare(int left, int right) { return ascending ? (left_real < right_real) : (left_real > right_real); } else if (type == "string") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); + const auto& left_string = (*global_data)[left].get_ref(field); + const auto& right_string = (*global_data)[right].get_ref(field); if (left_string == right_string) continue; @@ -316,8 +327,8 @@ static bool sort_compare(int left, int right) { } else if (type == "date") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); + const auto& left_string = (*global_data)[left].get_ref(field); + const auto& right_string = (*global_data)[right].get_ref(field); if (left_string != "" && right_string == "") return true; From e8a0de4bb2a91e1550a70a14e7057af06ad4777e Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 28 Aug 2026 00:51:30 +0100 Subject: [PATCH 24/49] Tighten pendingOnly filter guard Signed-off-by: ashprice --- src/Filter.cpp | 78 ++++++++++++++++++---------------------------- test/debug.test.py | 13 ++++++++ 2 files changed, 43 insertions(+), 48 deletions(-) diff --git a/src/Filter.cpp b/src/Filter.cpp index c3a350f78..b739749a8 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -164,62 +164,44 @@ void Filter::filter_to_tasks(const std::vector& input, std::vector& // If the filter contains no 'or', 'xor' or 'not' operators, and only includes // status values 'pending', 'waiting' or 'recurring', then the filter is // guaranteed to only need data from pending.data. -bool Filter::pendingOnly() const { - // To skip loading completed.data, there should be: - // - 'status' in filter - // - no 'completed' - // - no 'deleted' - // - no 'xor' - // - no 'or' - int countStatus = 0; - int countPending = 0; - int countWaiting = 0; - int countRecurring = 0; - int countId = (int)Context::getContext().cli2._id_ranges.size(); - int countUUID = (int)Context::getContext().cli2._uuid_list.size(); - int countOr = 0; - int countXor = 0; - int countNot = 0; - bool pendingTag = false; - bool activeTag = false; - bool readyTag = false; - bool waitingTag = false; - - for (const auto& a : Context::getContext().cli2._args) { - if (a.hasTag("FILTER")) { - std::string raw = a.attribute("raw"); - std::string canonical = a.attribute("canonical"); - - if (a._lextype == Lexer::Type::op && raw == "or") ++countOr; - if (a._lextype == Lexer::Type::op && raw == "xor") ++countXor; - if (a._lextype == Lexer::Type::op && raw == "not") ++countNot; - if (a._lextype == Lexer::Type::dom && canonical == "status") ++countStatus; - if (raw == "pending") ++countPending; - if (raw == "waiting") ++countWaiting; - if (raw == "recurring") ++countRecurring; - } - } - for (const auto& word : Context::getContext().cli2._original_args) { - if (word.attribute("raw") == "+PENDING") pendingTag = true; - if (word.attribute("raw") == "+ACTIVE") activeTag = true; - if (word.attribute("raw") == "+READY") readyTag = true; - if (word.attribute("raw") == "+WAITING") waitingTag = true; - } +bool Filter::pendingOnly() const { + if (!Context::getContext().config.getBoolean("gc")) return false; - if (countUUID) return false; + const auto& cli = Context::getContext().cli2; + if (!cli._uuid_list.empty()) return false; - if (countOr || countXor || countNot) return false; + std::vector filter_args; + for (const auto& arg : cli._args) { + if (!arg.hasTag("FILTER")) continue; - if (pendingTag || activeTag || readyTag || waitingTag) return true; + const auto& raw = arg.attribute("raw"); + if (arg._lextype == Lexer::Type::op && + (raw == "or" || raw == "xor" || raw == "!" || raw == "not")) + return false; - if (countStatus) { - if (!countPending && !countWaiting && !countRecurring) return false; + filter_args.push_back(&arg); + } - return true; + for (size_t i = 0; i + 2 < filter_args.size(); ++i) { + const auto& left = *filter_args[i]; + const auto& op = *filter_args[i + 1]; + const auto& right = *filter_args[i + 2]; + const auto& value = right.attribute("raw"); + + if (left._lextype == Lexer::Type::dom && left.attribute("canonical") == "status" && + op._lextype == Lexer::Type::op && + (op.attribute("raw") == "=" || op.attribute("raw") == "==") && + (value == "pending" || value == "waiting" || value == "recurring")) + return true; + + if (left._lextype == Lexer::Type::dom && left.attribute("raw") == "tags" && + op._lextype == Lexer::Type::op && op.attribute("raw") == "_hastag_" && + (value == "PENDING" || value == "ACTIVE" || value == "READY" || value == "WAITING")) + return true; } - if (countId) return true; + if (!cli._id_ranges.empty()) return true; return false; } diff --git a/test/debug.test.py b/test/debug.test.py index 1838a51d9..71bd5eb69 100755 --- a/test/debug.test.py +++ b/test/debug.test.py @@ -85,6 +85,19 @@ def test_debug_hooks_output(self): self.assertIn("Filtered 2 tasks --> 2 tasks [pending only]", err) self.assertIn("Perf task", err) + def test_status_filter_shortcut_ignores_other_filters(self): + """Checks that we aren't accidentally matching other filters + with the same keywords""" + self.t("1 modify description:pending") + self.t("1 done") + + for filter_ in ( + "status:completed description:pending", + "status.not:pending description:pending", + ): + code, out, err = self.t(f"{filter_} all") + self.assertIn("pending", out) + if __name__ == "__main__": from simpletap import TAPTestRunner From 909ebcfb525382a76b961ff148dbfea562ab50fa Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 28 Aug 2026 11:43:33 +0100 Subject: [PATCH 25/49] Improve dependency flag handling; add tests Signed-off-by: ashprice --- src/TDB2.cpp | 31 ++++++++++++++++------------ test/tdb2_test.cpp | 51 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/src/TDB2.cpp b/src/TDB2.cpp index 91b6de791..68c4be49e 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -52,6 +52,7 @@ static void dependency_scan(std::vector&, const std::unordered_map&, const std::unordered_map&, std::unordered_map&, size_t, const std::vector&, const std::vector&); +static bool participates_in_dependency_graph(const Task&); // Build maps for dependency queries. static DependencyGraph build_dependency_graph(const std::vector&, @@ -208,10 +209,11 @@ void TDB2::modify(Task& task) { replica()->commit_operations(std::move(ops)); - // If the task entered or left the pending set, we must invalidate the cache. - bool was_pending = found_original && (original.getStatus() == Task::pending); - bool now_pending = task.getStatus() == Task::pending; - if (was_pending != now_pending || !found_original) { + // If the task entered or left the working set/dependency graph, we must + // invalidate the cache. + bool was_active = found_original && participates_in_dependency_graph(original); + bool now_active = participates_in_dependency_graph(task); + if (was_active != now_active || !found_original) { invalidate_cached_info(); return; } @@ -588,15 +590,13 @@ static void dependency_scan(std::vector& tasks, task.is_blocked = false; } for (size_t i = 0; i < tasks.size(); ++i) { - auto lstatus = tasks[i].getStatus(); + if (!participates_in_dependency_graph(tasks[i])) continue; for (const auto& dep : tasks[i].getDependencyUUIDs()) { auto it = uuid_index.find(dep); if (it == uuid_index.end()) continue; size_t j = it->second; - auto rstatus = tasks[j].getStatus(); - if (lstatus != Task::completed && lstatus != Task::deleted && rstatus != Task::completed && - rstatus != Task::deleted) { + if (participates_in_dependency_graph(tasks[j])) { tasks[i].is_blocked = true; tasks[j].is_blocking = true; if (dependency_counts) ++(*dependency_counts)[dep]; @@ -616,8 +616,7 @@ static void dependency_update(std::vector& tasks, auto& task = tasks[task_index]; task.is_blocked = false; - auto task_status = task.getStatus(); - bool task_is_pending = task_status != Task::completed && task_status != Task::deleted; + bool task_is_active = participates_in_dependency_graph(task); for (const auto& dep : old_set) { if (new_set.find(dep) != new_set.end()) continue; @@ -635,9 +634,8 @@ static void dependency_update(std::vector& tasks, auto target = uuid_index.find(dep); if (target == uuid_index.end()) continue; - auto target_status = tasks[target->second].getStatus(); - bool target_is_pending = target_status != Task::completed && target_status != Task::deleted; - if (!task_is_pending || !target_is_pending) continue; + bool target_is_active = participates_in_dependency_graph(tasks[target->second]); + if (!task_is_active || !target_is_active) continue; task.is_blocked = true; if (old_set.find(dep) == old_set.end()) ++dependency_counts[dep]; @@ -648,6 +646,13 @@ static void dependency_update(std::vector& tasks, task.is_blocking = dependency_counts.find(uuid) != dependency_counts.end(); } +////////////////////////////////////////////////////////////////////////////////// +// Defined here for reuse. +static bool participates_in_dependency_graph(const Task& task) { + auto status = task.getStatus(); + return status != Task::completed && status != Task::deleted; +} + ///////////////////////////////////////////////////////////////////////////////// // Build the full dependency map from the task vector. static DependencyGraph build_dependency_graph( diff --git a/test/tdb2_test.cpp b/test/tdb2_test.cpp index f1b475e4b..65de11cb2 100644 --- a/test/tdb2_test.cpp +++ b/test/tdb2_test.cpp @@ -44,7 +44,7 @@ void cleardb() { //////////////////////////////////////////////////////////////////////////////// int TEST_NAME(int, char**) { - UnitTest t(27); + UnitTest t(35); Context context; Context::setContext(&context); @@ -84,7 +84,7 @@ int TEST_NAME(int, char**) { t.is((int)pending.size(), 1, "TDB2 after add, 1 pending task"); t.is((int)completed.size(), 0, "TDB2 after add, 0 completed tasks"); t.is((int)num_reverts_possible, 1, "TDB2 after add, 1 revert possible"); - t.is((int)num_local_changes, 6, "TDB2 after add, 6 local changes"); + t.is((int)num_local_changes, 7, "TDB2 after add, 7 local changes"); task.set("description", "This is a test"); context.tdb2.modify(task); @@ -98,11 +98,11 @@ int TEST_NAME(int, char**) { t.is((int)completed.size(), 0, "TDB2 after set, 0 completed tasks"); t.is((int)num_reverts_possible, 1, "TDB2 after set, 1 revert possible"); - // At this point, there may be 7 or 8 local changes, depending on whether + // At this point, there may be 8 or 9 local changes, depending on whether // the `modified` property changed between the `add` and `modify` // invocation. That only happens if the clock ticks over to the next second // between those invocations. - t.ok(num_local_changes == 7 || num_local_changes == 8, "TDB2 after set, 7 or 8 local changes"); + t.ok(num_local_changes == 8 || num_local_changes == 9, "TDB2 after set, 7 or 8 local changes"); // Reset for reuse. cleardb(); @@ -158,7 +158,50 @@ int TEST_NAME(int, char**) { // Reset for reuse. cleardb(); + context.tdb2.open_replica(".", /*create_if_missing=*/true, /*read_write=*/true); + + first.addDependency(blocker.get_ref("uuid")); + context.tdb2.modify(first); + second.addDependency(blocker.get_ref("uuid")); + + Task waiting = *context.tdb2.find_pending(first.get_ref("uuid")); + waiting.set("wait", "20380118T000000Z"); + context.tdb2.modify(waiting); + + Task recurring = *context.tdb2.find_pending(second.get_ref("uuid")); + recurring.setStatus(Task::recurring); + context.tdb2.modify(recurring); + + t.ok(context.tdb2.find_pending(first.get_ref("uuid"))->is_blocked, + "TDB2 waiting dependent is blocked"); + t.ok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 recurring dependent is blocked"); + t.ok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 active dependents keep the dependency as blocking"); + + waiting.setStatus(Task::completed); + context.tdb2.modify(waiting); + + t.notok(context.tdb2.find_pending(first.get_ref("uuid"))->is_blocked, + "TDB2 completed waiting dependent is unblocked"); + t.ok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 recurring dependent stays blocked"); + t.ok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 active dependents keep the dependency as blocking - recurring tasks"); + + recurring.setStatus(Task::deleted); + context.tdb2.modify(recurring); + + t.notok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 deleted recurring dependent gets unblocked"); + t.notok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 inactive dependents cause dependency to clear blocking"); + + // Reset for reuse. + cleardb(); + context.tdb2.open_replica(".", /*create_if_missing=*/true, /*read_write=*/true); + // TODO complete a task // TODO gc } From 5d840938f61a96f16baf2ba640758a9861137f1d Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 28 Aug 2026 13:07:16 +0100 Subject: [PATCH 26/49] Fixed regression: dep id listing with gc off; test added Signed-off-by: ashprice --- src/Task.cpp | 17 ++++++++++++----- src/columns/ColDepends.cpp | 27 ++------------------------- test/dependencies.test.py | 13 +++++++++++++ 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/Task.cpp b/src/Task.cpp index 75b5bd2e0..3d3bbd961 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -1049,11 +1049,18 @@ bool Task::hasDependency(const std::string& uuid) const { //////////////////////////////////////////////////////////////////////////////// std::vector Task::getDependencyIDs() const { std::vector ids; - for (auto& attr : all()) { - if (!isDepAttr(attr)) continue; - auto dep = attr2Dep(attr); - ids.push_back(Context::getContext().tdb2.id(dep)); - } + + const auto& graph = Context::getContext().tdb2.dependency_graph(); + auto found = graph.dependencies.find(get_ref("uuid")); + if (found == graph.dependencies.end()) return ids; + + const auto& tasks = Context::getContext().tdb2.pending_tasks(); + ids.reserve(found->second.size()); + for (auto idx : found->second) + if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) + ids.push_back(tasks[idx].id); + + std::sort(ids.begin(), ids.end()); return ids; } diff --git a/src/columns/ColDepends.cpp b/src/columns/ColDepends.cpp index b87a301db..dcfaf29cf 100644 --- a/src/columns/ColDepends.cpp +++ b/src/columns/ColDepends.cpp @@ -76,20 +76,7 @@ void ColumnDepends::measure(const Task& task, unsigned int& minimum, unsigned in return; } - // We also don't need to call getDependencyTasks(), - // which would copy the full objects from the cache just to read the ID - // field. Instead, we can use getDependencyUUIDs() and tdb2.id(). - - auto dep_uuids = task.getDependencyUUIDs(); - - std::vector blocking_ids; - blocking_ids.reserve(dep_uuids.size()); - for (const auto& uuid : dep_uuids) { - int id = Context::getContext().tdb2.id(uuid); - if (id > 0) blocking_ids.push_back(id); - } - - std::sort(blocking_ids.begin(), blocking_ids.end()); + auto blocking_ids = task.getDependencyIDs(); if (blocking_ids.size() > 0) { if (_style == "count") { @@ -121,17 +108,7 @@ void ColumnDepends::render(std::vector& lines, const Task& task, in return; } - // We use the same approach to look up UUIDs as for measure(). - auto dep_uuids = task.getDependencyUUIDs(); - - std::vector blocking_ids; - blocking_ids.reserve(dep_uuids.size()); - for (const auto& uuid : dep_uuids) { - int id = Context::getContext().tdb2.id(uuid); - if (id > 0) blocking_ids.push_back(id); - } - - std::sort(blocking_ids.begin(), blocking_ids.end()); + auto blocking_ids = task.getDependencyIDs(); if (blocking_ids.size() > 0) { if (_style == "count") { diff --git a/test/dependencies.test.py b/test/dependencies.test.py index 951129ec3..d1d98da8d 100755 --- a/test/dependencies.test.py +++ b/test/dependencies.test.py @@ -87,6 +87,19 @@ def test_circular_5(self): code, out, err = self.t.runError("1 modify dep:5") self.assertIn("Circular dependency detected and disallowed.", err) + def test_completed_dependency_is_not_rendered(self): + """Completed dependencies should not be listed even if working set is stale""" + self.t("2 modify dep:1") + self.t("1 done rc.gc=off") + + self.assertIn("depends", self.t.export_one("rc.gc=off 2")) + code, out, err = self.t( + "2 list rc.gc=off rc.report.list.columns=id,depends " + "rc.report.list.labels=ID,Depends" + ) + + self.assertIn(["2"], [line.split() for line in out.splitlines()]) + def test_dag(self): """Check acyclic graph support""" self.t("add three") From 5edd5b737dfefeb4cce99e3e1c420849197f46db Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 28 Aug 2026 18:19:13 +0100 Subject: [PATCH 27/49] Skip clean working set rebuilds and invalidate caches after replica mutation Signed-off-by: ashprice --- src/TDB2.cpp | 28 ++++++++++- src/TDB2.h | 2 + src/commands/CmdSync.cpp | 2 + src/commands/CmdUndo.cpp | 2 + test/CMakeLists.txt | 1 + test/sync.test.py | 100 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 133 insertions(+), 2 deletions(-) create mode 100755 test/sync.test.py diff --git a/src/TDB2.cpp b/src/TDB2.cpp index 68c4be49e..f7c89db6c 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -295,15 +295,39 @@ void TDB2::gc() { Timer timer; // Allowed as an override, but not recommended. - if (Context::getContext().config.getBoolean("gc")) { + if (Context::getContext().config.getBoolean("gc") && !working_set_is_clean()) { replica()->rebuild_working_set(true); + invalidate_cached_info(); } Context::getContext().time_gc_us += timer.total_us(); } +bool TDB2::working_set_is_clean() { + const auto& ws = working_set(); + const auto& tasks = pending_tasks(); + const auto largest = ws->largest_index(); + + if (tasks.size() != largest) return false; + + for (size_t i = 1; i <= largest; ++i) { + if (ws->by_index(i).is_nil()) return false; + } + + return std::all_of(tasks.begin(), tasks.end(), [](const Task& task) { + const auto& status = task.get_ref("status"); + return status == "pending" || status == "recurring" || status == "iterative"; + }); +} + +//////////////////////////////////////////////////////////////////////////////// +void TDB2::expire_tasks() { + replica()->expire_tasks(); + invalidate_cached_info(); +} + //////////////////////////////////////////////////////////////////////////////// -void TDB2::expire_tasks() { replica()->expire_tasks(); } +void TDB2::invalidate_cache() { invalidate_cached_info(); } //////////////////////////////////////////////////////////////////////////////// // Latest ID is that of the last pending task. diff --git a/src/TDB2.h b/src/TDB2.h index 4c38552ec..8c8c55551 100644 --- a/src/TDB2.h +++ b/src/TDB2.h @@ -62,6 +62,7 @@ class TDB2 { void get_changes(std::vector&); void gc(); void expire_tasks(); + void invalidate_cache(); int latest_id(); // Generalized task accessors. @@ -121,6 +122,7 @@ class TDB2 { std::map changes; const rust::Box& working_set(); + bool working_set_is_clean(); void maybe_add_undo_point(rust::Vec&); }; diff --git a/src/commands/CmdSync.cpp b/src/commands/CmdSync.cpp index 6e966286a..7dafecd05 100644 --- a/src/commands/CmdSync.cpp +++ b/src/commands/CmdSync.cpp @@ -190,6 +190,8 @@ int CmdSync::execute(std::string& output) { if (context.config.getBoolean("purge.on-sync")) { context.tdb2.expire_tasks(); + } else { + context.tdb2.invalidate_cache(); } if (verbose) { diff --git a/src/commands/CmdUndo.cpp b/src/commands/CmdUndo.cpp index 83156410e..8cac605b9 100644 --- a/src/commands/CmdUndo.cpp +++ b/src/commands/CmdUndo.cpp @@ -61,6 +61,8 @@ int CmdUndo::execute(std::string&) { // need not be done here. if (!replica->commit_reversed_operations(std::move(undo_ops))) { std::cout << "Could not undo: other operations have occurred."; + } else { + Context::getContext().tdb2.invalidate_cache(); } } return 0; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a38f727cb..930ba20a7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -175,6 +175,7 @@ set (pythonTests substitute.test.py sugar.test.py summary.test.py + sync.test.py tag.test.py taskrc.test.py timesheet.test.py diff --git a/test/sync.test.py b/test/sync.test.py new file mode 100755 index 000000000..7295af145 --- /dev/null +++ b/test/sync.test.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +import os +import sys +import unittest + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from basetest import Task, TestCase + + +class TestWorkingSetSync(TestCase): + def setUp(self): + self.first = Task() + self.second = Task() + self.server = os.path.join(self.first.datadir, "server") + os.makedirs(self.server) + self.first.config("sync.local.server_dir", self.server) + self.second.config("sync.local.server_dir", self.server) + + def test_remote_completion_and_additions_are_reported(self): + self.first("add one") + self.first("sync") + self.second("sync") + + self.second("1 done") + self.second("add two") + two_uuid = self.second("_get 2.uuid")[1].strip() + self.second("sync") + self.first("sync") + + code, out, err = self.first("list") + self.assertNotIn("one", out) + self.assertRegex(out, r"1\s+-\s+two") + self.assertEqual(two_uuid, self.first("_get 1.uuid")[1].strip()) + + self.first("1 modify two-updated") + self.first("sync") + self.second("sync") + + code, out, err = self.second("list") + self.assertRegex(out, r"2\s+\S+\s+two-updated") + self.assertEqual(two_uuid, self.second("_get 2.uuid")[1].strip()) + + def test_recurrence_converges(self): + self.first.config("recurrence.limit", "1") + self.second.config("recurrence.limit", "1") + self.first("add due:tomorrow recur:daily recurring") + self.first("list") + expected = self.first.export_one("status:pending")["uuid"] + + self.first("sync") + self.second("sync") + self.second("list") + + self.assertEqual(expected, self.second.export_one("status:pending")["uuid"]) + + self.second("sync") + self.first("sync") + + self.assertEqual(expected, self.first.export_one("status:pending")["uuid"]) + + def test_synced_dependency_state_updates(self): + self.second("add one") + self.second("add two") + self.second("add three") + self.second("add four") + first_uuid = self.second.export_one(1)["uuid"] + second_uuid = self.second.export_one(2)["uuid"] + third_uuid = self.second.export_one(3)["uuid"] + fourth_uuid = self.second.export_one(4)["uuid"] + self.second("3-4 modify depends:1 rc.bulk=0") + + self.second("sync") + self.first("sync") + + third_id = self.first(f"_get {third_uuid}.id")[1].strip() + fourth_id = self.first(f"_get {fourth_uuid}.id")[1].strip() + self.first( + f"{third_id},{fourth_id} modify depends:-{first_uuid} depends:{second_uuid} rc.bulk=0" + ) + self.assertEqual("\n", self.first(f"_get {first_uuid}.tags.BLOCKING")[1]) + self.assertEqual([second_uuid], self.first.export_one(third_uuid)["depends"]) + self.assertEqual([second_uuid], self.first.export_one(fourth_uuid)["depends"]) + + self.first("sync") + self.second("sync") + + self.assertEqual("\n", self.second(f"_get {first_uuid}.tags.BLOCKING")[1]) + self.assertEqual( + "BLOCKING\n", self.second(f"_get {second_uuid}.tags.BLOCKING")[1] + ) + self.assertEqual([second_uuid], self.second.export_one(third_uuid)["depends"]) + self.assertEqual([second_uuid], self.second.export_one(fourth_uuid)["depends"]) + + +if __name__ == "__main__": + from simpletap import TAPTestRunner + + unittest.main(testRunner=TAPTestRunner()) From d551358dd3fa615acaaf268464a8b7fca84f26a8 Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 28 Aug 2026 22:52:33 +0100 Subject: [PATCH 28/49] Simplify the rendering of project column Signed-off-by: ashprice --- src/columns/ColProject.cpp | 52 +++++++++++++++----------------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/src/columns/ColProject.cpp b/src/columns/ColProject.cpp index c8de1807d..fcd9833f8 100644 --- a/src/columns/ColProject.cpp +++ b/src/columns/ColProject.cpp @@ -53,23 +53,17 @@ ColumnProject::ColumnProject() { void ColumnProject::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { - if (_style == "full") { - const auto& project = task.get_ref(_name); - minimum = longestWord(project); - maximum = utf8_width(project); - } else { - std::string project = task.get(_name); + std::string project = task.get(_name); - if (_style == "parent") { - auto period = project.find('.'); - if (period != std::string::npos) project = project.substr(0, period); - } else if (_style == "indented") { - project = indentProject(project, " ", '.'); - } - - minimum = longestWord(project); - maximum = utf8_width(project); + if (_style == "parent") { + auto period = project.find("."); + if (period != std::string::npos) project = project.substr(0, period); + } else if (_style == "indented") { + project = indentProject(project, " ", '.'); } + + minimum = longestWord(project); + maximum = utf8_width(project); } } @@ -77,24 +71,18 @@ void ColumnProject::measure(const Task& task, unsigned int& minimum, unsigned in void ColumnProject::render(std::vector& lines, const Task& task, int width, Color& color) { if (task.has(_name)) { - if (_style == "full") { - const auto& project = task.get_ref(_name); - std::vector raw; - wrapText(raw, project, width, _hyphenate); - for (const auto& i : raw) renderStringLeft(lines, width, color, i); - } else { - std::string project = task.get(_name); - if (_style == "parent") { - auto period = project.find('.'); - if (period != std::string::npos) project = project.substr(0, period); - } else if (_style == "indented") { - project = indentProject(project, " ", '.'); - } - - std::vector raw; - wrapText(raw, project, width, _hyphenate); - for (const auto& i : raw) renderStringLeft(lines, width, color, i); + std::string project = task.get(_name); + if (_style == "parent") { + auto period = project.find('.'); + if (period != std::string::npos) project = project.substr(0, period); + } else if (_style == "indented") { + project = indentProject(project, " ", '.'); } + + std::vector raw; + wrapText(raw, project, width, _hyphenate); + + for (const auto& i : raw) renderStringLeft(lines, width, color, i); } } From 3c3935bb66cb4094adad6b070f02f16f3d835dab Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 28 Aug 2026 22:54:18 +0100 Subject: [PATCH 29/49] Sync test doesn't really test what we want it to, removed Signed-off-by: ashprice --- test/CMakeLists.txt | 1 - test/sync.test.py | 100 -------------------------------------------- 2 files changed, 101 deletions(-) delete mode 100755 test/sync.test.py diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 930ba20a7..a38f727cb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -175,7 +175,6 @@ set (pythonTests substitute.test.py sugar.test.py summary.test.py - sync.test.py tag.test.py taskrc.test.py timesheet.test.py diff --git a/test/sync.test.py b/test/sync.test.py deleted file mode 100755 index 7295af145..000000000 --- a/test/sync.test.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 - -import os -import sys -import unittest - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from basetest import Task, TestCase - - -class TestWorkingSetSync(TestCase): - def setUp(self): - self.first = Task() - self.second = Task() - self.server = os.path.join(self.first.datadir, "server") - os.makedirs(self.server) - self.first.config("sync.local.server_dir", self.server) - self.second.config("sync.local.server_dir", self.server) - - def test_remote_completion_and_additions_are_reported(self): - self.first("add one") - self.first("sync") - self.second("sync") - - self.second("1 done") - self.second("add two") - two_uuid = self.second("_get 2.uuid")[1].strip() - self.second("sync") - self.first("sync") - - code, out, err = self.first("list") - self.assertNotIn("one", out) - self.assertRegex(out, r"1\s+-\s+two") - self.assertEqual(two_uuid, self.first("_get 1.uuid")[1].strip()) - - self.first("1 modify two-updated") - self.first("sync") - self.second("sync") - - code, out, err = self.second("list") - self.assertRegex(out, r"2\s+\S+\s+two-updated") - self.assertEqual(two_uuid, self.second("_get 2.uuid")[1].strip()) - - def test_recurrence_converges(self): - self.first.config("recurrence.limit", "1") - self.second.config("recurrence.limit", "1") - self.first("add due:tomorrow recur:daily recurring") - self.first("list") - expected = self.first.export_one("status:pending")["uuid"] - - self.first("sync") - self.second("sync") - self.second("list") - - self.assertEqual(expected, self.second.export_one("status:pending")["uuid"]) - - self.second("sync") - self.first("sync") - - self.assertEqual(expected, self.first.export_one("status:pending")["uuid"]) - - def test_synced_dependency_state_updates(self): - self.second("add one") - self.second("add two") - self.second("add three") - self.second("add four") - first_uuid = self.second.export_one(1)["uuid"] - second_uuid = self.second.export_one(2)["uuid"] - third_uuid = self.second.export_one(3)["uuid"] - fourth_uuid = self.second.export_one(4)["uuid"] - self.second("3-4 modify depends:1 rc.bulk=0") - - self.second("sync") - self.first("sync") - - third_id = self.first(f"_get {third_uuid}.id")[1].strip() - fourth_id = self.first(f"_get {fourth_uuid}.id")[1].strip() - self.first( - f"{third_id},{fourth_id} modify depends:-{first_uuid} depends:{second_uuid} rc.bulk=0" - ) - self.assertEqual("\n", self.first(f"_get {first_uuid}.tags.BLOCKING")[1]) - self.assertEqual([second_uuid], self.first.export_one(third_uuid)["depends"]) - self.assertEqual([second_uuid], self.first.export_one(fourth_uuid)["depends"]) - - self.first("sync") - self.second("sync") - - self.assertEqual("\n", self.second(f"_get {first_uuid}.tags.BLOCKING")[1]) - self.assertEqual( - "BLOCKING\n", self.second(f"_get {second_uuid}.tags.BLOCKING")[1] - ) - self.assertEqual([second_uuid], self.second.export_one(third_uuid)["depends"]) - self.assertEqual([second_uuid], self.second.export_one(fourth_uuid)["depends"]) - - -if __name__ == "__main__": - from simpletap import TAPTestRunner - - unittest.main(testRunner=TAPTestRunner()) From 2ad4db71d2fba9e9a9fd86c71730cd85c54f9a60 Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 28 Aug 2026 23:06:18 +0100 Subject: [PATCH 30/49] Caching-related simplifications Signed-off-by: ashprice --- src/TDB2.cpp | 6 ++---- src/Task.cpp | 5 +++-- test/tdb2_test.cpp | 3 ++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/TDB2.cpp b/src/TDB2.cpp index f7c89db6c..5a3e06fe9 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -72,6 +72,8 @@ static tc::Status statusFromString(const std::string& s) { //////////////////////////////////////////////////////////////////////////////// void TDB2::open_replica(const std::string& location, bool create_if_missing, bool read_write) { _replica = tc::new_replica_on_disk(location, create_if_missing, read_write); + invalidate_cached_info(); + changes.clear(); } //////////////////////////////////////////////////////////////////////////////// @@ -472,8 +474,6 @@ bool TDB2::get(int id, Task& task) { const auto tcuuid = ws->by_index(id); if (!tcuuid.is_nil()) { std::string uuid = static_cast(tcuuid.to_string()); - // Load index of pending tasks. - pending_tasks(); // Lookup the UUID in the index instead of scanning the vector. auto* pt = find_pending(uuid); if (pt) { @@ -488,8 +488,6 @@ bool TDB2::get(int id, Task& task) { //////////////////////////////////////////////////////////////////////////////// // Locate task by UUID, including by partial ID, wherever it is. bool TDB2::get(const std::string& uuid, Task& task) { - pending_tasks(); - // Try to match exact UUID within the index. auto* pt = find_pending(uuid); if (pt) { diff --git a/src/Task.cpp b/src/Task.cpp index 3d3bbd961..bd0a8f142 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -729,7 +729,7 @@ void Task::parseTC(rust::Box task, int known_id) { // count annotations annotation_count = 0; for (auto& item : items) { - const auto& key = static_cast(item.prop); + auto key = static_cast(item.prop); data[key] = static_cast(item.value); if (isAnnotationAttr(key)) { ++annotation_count; @@ -1865,7 +1865,8 @@ float Task::urgency_c() const { float Task::urgency() const { if (recalc_urgency) { // We set the guard first to avoid infinite recursion. It will be 0.0 on - // first call and then the computed value will be reused from the cache.. + // first call and then the computed value will be reused from the cache. + urgency_value = 0.0; recalc_urgency = false; urgency_value = urgency_c(); } diff --git a/test/tdb2_test.cpp b/test/tdb2_test.cpp index 65de11cb2..ff640c0de 100644 --- a/test/tdb2_test.cpp +++ b/test/tdb2_test.cpp @@ -44,7 +44,7 @@ void cleardb() { //////////////////////////////////////////////////////////////////////////////// int TEST_NAME(int, char**) { - UnitTest t(35); + UnitTest t(36); Context context; Context::setContext(&context); @@ -107,6 +107,7 @@ int TEST_NAME(int, char**) { // Reset for reuse. cleardb(); context.tdb2.open_replica(".", /*create_if_missing=*/true, /*read_write=*/true); + t.is((int)context.tdb2.pending_tasks().size(), 0, "Fresh invocation uses fresh cache"); Task blocker(R"([description:"blocking"])"); Task first(R"([description:"first"])"); From 3ccf8aa79bf5cdd41692b111cdfef48f6e8a5c54 Mon Sep 17 00:00:00 2001 From: ashprice Date: Fri, 28 Aug 2026 23:15:54 +0100 Subject: [PATCH 31/49] Simplify sort comparison Signed-off-by: ashprice --- src/sort.cpp | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src/sort.cpp b/src/sort.cpp index 18f675b42..5cfe5c16b 100644 --- a/src/sort.cpp +++ b/src/sort.cpp @@ -62,8 +62,7 @@ static std::unordered_map> global_durations; // Pre-computed dependency UUIDs to avoid repeated sort calls. static std::vector> global_sorted_dep_uuids; -// UDA column details to avoid repeated lookups. -static std::unordered_map global_uda_columns; +// UDA types to avoid repeated lookups. static std::unordered_map global_uda_types; static std::vector global_random_keys; @@ -77,7 +76,6 @@ void sort_tasks(const std::vector& data, std::vector& order, const st // Pre-computing of sorting values. global_durations.clear(); - global_uda_columns.clear(); global_uda_types.clear(); global_sorted_dep_uuids.clear(); global_random_keys.clear(); @@ -132,10 +130,9 @@ void sort_tasks(const std::vector& data, std::vector& order, const st auto col_it = Context::getContext().columns.find(field); if (col_it != Context::getContext().columns.end()) { - Column* col = col_it->second; - global_uda_columns[field] = col; - global_uda_types[field] = col->type(); - if (col->type() == "duration") { + auto type = col_it->second->type(); + global_uda_types[field] = type; + if (type == "duration") { auto& cache = global_durations[field]; cache.resize(data.size(), 0); for (size_t i = 0; i < data.size(); ++i) { @@ -191,11 +188,6 @@ void sort_projects(std::list>& sorted, // // Essentially a static implementation of a dynamic operator<. static bool sort_compare(int left, int right) { - int left_number; - int right_number; - float left_real; - float right_real; - for (const auto& key : global_sort_keys) { const auto& field = key.field; bool ascending = key.ascending; @@ -212,8 +204,8 @@ static bool sort_compare(int left, int right) { // Urgency. else if (field == "urgency") { - left_real = (*global_data)[left].urgency(); - right_real = (*global_data)[right].urgency(); + auto left_real = (*global_data)[left].urgency(); + auto right_real = (*global_data)[right].urgency(); if (left_real == right_real) continue; @@ -222,8 +214,8 @@ static bool sort_compare(int left, int right) { // Number. else if (field == "id") { - left_number = (*global_data)[left].id; - right_number = (*global_data)[right].id; + auto left_number = (*global_data)[left].id; + auto right_number = (*global_data)[right].id; if (left_number == right_number) continue; @@ -263,13 +255,13 @@ static bool sort_compare(int left, int right) { if (left_deps == right_deps) continue; - if (left_deps.size() == 0 && right_deps.size() > 0) return ascending; + if (left_deps.empty() && right_deps.empty()) return ascending; - if (left_deps.size() > 0 && right_deps.size() == 0) return !ascending; + if (!left_deps.empty() && right_deps.empty()) return !ascending; // Sort on the first dependency. - left_number = Context::getContext().tdb2.id(left_deps[0]); - right_number = Context::getContext().tdb2.id(right_deps[0]); + auto left_number = Context::getContext().tdb2.id(left_deps[0]); + auto right_number = Context::getContext().tdb2.id(right_deps[0]); if (left_number == right_number) continue; @@ -289,11 +281,11 @@ static bool sort_compare(int left, int right) { // UDAs. else { - auto cuda = global_uda_columns.find(field); - if (cuda == global_uda_columns.end()) + auto type_it = global_uda_types.find(field); + if (type_it == global_uda_types.end()) throw format("The '{1}' column is not a valid sort field.", field); - const std::string& type = global_uda_types[field]; + const auto& type = type_it->second; if (type == "numeric") { auto left_real = strtof(((*global_data)[left].get_ref(field)).c_str(), nullptr); From 60989978c07ef085eca248a383a5aa080fc4b339 Mon Sep 17 00:00:00 2001 From: ashprice Date: Mon, 31 Aug 2026 15:38:44 +0100 Subject: [PATCH 32/49] Batch project feedback for bulk mods Signed-off-by: ashprice --- src/commands/CmdDelete.cpp | 8 ++++---- src/commands/CmdDone.cpp | 8 ++++---- src/commands/CmdStart.cpp | 6 +++--- src/commands/CmdStop.cpp | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/commands/CmdDelete.cpp b/src/commands/CmdDelete.cpp index b548ea983..8faebc080 100644 --- a/src/commands/CmdDelete.cpp +++ b/src/commands/CmdDelete.cpp @@ -74,7 +74,7 @@ int CmdDelete::execute(std::string&) { } // Accumulated project change notifications. - std::map projectChanges; + std::map projectChanges; if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); @@ -99,7 +99,7 @@ int CmdDelete::execute(std::string&) { if (task.is_blocking) feedback_unblocked(task); dependencyChainOnComplete(task); if (Context::getContext().verbose("project")) - projectChanges[task.get("project")] = onProjectChange(task); + projectChanges.insert_or_assign(task.get("project"), task); // Delete siblings. if (task.has("parent")) { @@ -163,8 +163,8 @@ int CmdDelete::execute(std::string&) { } // Now list the project changes. - for (const auto& change : projectChanges) - if (change.first != "") Context::getContext().footnote(change.second); + for (auto& change : projectChanges) + if (change.first != "") Context::getContext().footnote(onProjectChange(change.second)); feedback_affected(count == 1 ? "Deleted {1} task." : "Deleted {1} tasks.", count); diff --git a/src/commands/CmdDone.cpp b/src/commands/CmdDone.cpp index e6ad19ee9..08a36d5b4 100644 --- a/src/commands/CmdDone.cpp +++ b/src/commands/CmdDone.cpp @@ -70,7 +70,7 @@ int CmdDone::execute(std::string&) { } // Accumulated project change notifications. - std::map projectChanges; + std::map projectChanges; if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); @@ -104,7 +104,7 @@ int CmdDone::execute(std::string&) { if (task.is_blocking) feedback_unblocked(task); dependencyChainOnComplete(task); if (Context::getContext().verbose("project")) - projectChanges[task.get("project")] = onProjectChange(task); + projectChanges.insert_or_assign(task.get("project"), task); // Save unmodified task for potential nagging later modified.push_back(before); @@ -124,8 +124,8 @@ int CmdDone::execute(std::string&) { nag(modified); // Now list the project changes. - for (const auto& change : projectChanges) - if (change.first != "") Context::getContext().footnote(change.second); + for (auto& change : projectChanges) + if (change.first != "") Context::getContext().footnote(onProjectChange(change.second)); feedback_affected(count == 1 ? "Completed {1} task." : "Completed {1} tasks.", count); return rc; diff --git a/src/commands/CmdStart.cpp b/src/commands/CmdStart.cpp index 057b61fc5..7cf0fbbb6 100644 --- a/src/commands/CmdStart.cpp +++ b/src/commands/CmdStart.cpp @@ -70,7 +70,7 @@ int CmdStart::execute(std::string&) { } // Accumulated project change notifications. - std::map projectChanges; + std::map projectChanges; if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); @@ -103,7 +103,7 @@ int CmdStart::execute(std::string&) { feedback_affected("Starting task {1} '{2}'.", task); dependencyChainOnStart(task); if (Context::getContext().verbose("project")) - projectChanges[task.get("project")] = onProjectChange(task, false); + projectChanges.insert_or_assign(task.get("project"), task); // Save unmodified task for potential nagging later modified.push_back(before); @@ -123,7 +123,7 @@ int CmdStart::execute(std::string&) { // Now list the project changes. for (auto& change : projectChanges) - if (change.first != "") Context::getContext().footnote(change.second); + if (change.first != "") Context::getContext().footnote(onProjectChange(change.second, false)); feedback_affected(count == 1 ? "Started {1} task." : "Started {1} tasks.", count); return rc; diff --git a/src/commands/CmdStop.cpp b/src/commands/CmdStop.cpp index 9924c5a58..5df670371 100644 --- a/src/commands/CmdStop.cpp +++ b/src/commands/CmdStop.cpp @@ -68,7 +68,7 @@ int CmdStop::execute(std::string&) { } // Accumulated project change notifications. - std::map projectChanges; + std::map projectChanges; if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); @@ -94,7 +94,7 @@ int CmdStop::execute(std::string&) { feedback_affected("Stopping task {1} '{2}'.", task); dependencyChainOnStart(task); if (Context::getContext().verbose("project")) - projectChanges[task.get("project")] = onProjectChange(task, false); + projectChanges.insert_or_assign(task.get("project"), task); } else { std::cout << "Task not stopped.\n"; rc = 1; @@ -110,7 +110,7 @@ int CmdStop::execute(std::string&) { // Now list the project changes. for (auto& change : projectChanges) - if (change.first != "") Context::getContext().footnote(change.second); + if (change.first != "") Context::getContext().footnote(onProjectChange(change.second, false)); feedback_affected(count == 1 ? "Stopped {1} task." : "Stopped {1} tasks.", count); return rc; From 55b82a0450339dda470c8d39cd299637fc247dfa Mon Sep 17 00:00:00 2001 From: ashprice Date: Mon, 31 Aug 2026 16:14:53 +0100 Subject: [PATCH 33/49] Better handling of bulk updates to recurrence masks + tests Signed-off-by: ashprice --- src/Hooks.cpp | 3 ++ src/Hooks.h | 1 + src/commands/CmdDelete.cpp | 19 +++++++++-- src/commands/CmdDone.cpp | 7 +++- src/recur.cpp | 63 +++++++++++++++++++++++------------- src/recur.h | 6 +++- test/hooks.on-modify.test.py | 12 +++++++ test/recurrence.test.py | 17 ++++++++++ 8 files changed, 101 insertions(+), 27 deletions(-) diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 33589b8c6..b88b84e1b 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -328,6 +328,9 @@ void Hooks::onModify(Task& before, Task& after) const { //////////////////////////////////////////////////////////////////////////////// std::vector Hooks::list() const { return _scripts; } +//////////////////////////////////////////////////////////////////////////////// +bool Hooks::hasOnModify() const { return _enabled && !scripts("on-modify").empty(); } + //////////////////////////////////////////////////////////////////////////////// std::vector Hooks::scripts(const std::string& event) const { std::vector matching; diff --git a/src/Hooks.h b/src/Hooks.h index 448c59744..894599900 100644 --- a/src/Hooks.h +++ b/src/Hooks.h @@ -41,6 +41,7 @@ class Hooks { void onExit() const; void onAdd(Task&) const; void onModify(Task&, Task&) const; + bool hasOnModify() const; std::vector list() const; private: diff --git a/src/commands/CmdDelete.cpp b/src/commands/CmdDelete.cpp index 8faebc080..055db2eac 100644 --- a/src/commands/CmdDelete.cpp +++ b/src/commands/CmdDelete.cpp @@ -36,6 +36,7 @@ #include #include +#include #include #define STRING_CMD_DELETE_TASK_R "Deleting recurring task {1} '{2}'." @@ -76,6 +77,16 @@ int CmdDelete::execute(std::string&) { // Accumulated project change notifications. std::map projectChanges; + RecurrenceMaskUpdates recurrenceMaskUpdates; + const auto recurrenceConfirmation = Context::getContext().config.get("recurrence.confirmation"); + const auto hasRecurringParent = + std::any_of(filtered.begin(), filtered.end(), + [](const Task& task) { return task.getStatus() == Task::recurring; }); + RecurrenceMaskUpdates* recurrenceMaskUpdatesPtr = nullptr; + if (!Context::getContext().hooks.hasOnModify() && recurrenceConfirmation != "prompt" && + !Context::getContext().config.getBoolean("recurrence.confirmation") && !hasRecurringParent) + recurrenceMaskUpdatesPtr = &recurrenceMaskUpdates; + if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); } @@ -92,7 +103,7 @@ int CmdDelete::execute(std::string&) { if (!task.has("end")) task.setAsNow("end"); if (permission(question, filtered.size())) { - updateRecurrenceMask(task); + updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); ++count; Context::getContext().tdb2.modify(task); feedback_affected("Deleting task {1} '{2}'.", task); @@ -112,7 +123,7 @@ int CmdDelete::execute(std::string&) { sibling.setStatus(Task::deleted); if (!sibling.has("end")) sibling.setAsNow("end"); - updateRecurrenceMask(sibling); + updateRecurrenceMask(sibling, recurrenceMaskUpdatesPtr); Context::getContext().tdb2.modify(sibling); feedback_affected(STRING_CMD_DELETE_TASK_R, sibling); feedback_unblocked(sibling); @@ -141,7 +152,7 @@ int CmdDelete::execute(std::string&) { child.setStatus(Task::deleted); if (!child.has("end")) child.setAsNow("end"); - updateRecurrenceMask(child); + updateRecurrenceMask(child, recurrenceMaskUpdatesPtr); Context::getContext().tdb2.modify(child); feedback_affected(STRING_CMD_DELETE_TASK_R, child); feedback_unblocked(child); @@ -162,6 +173,8 @@ int CmdDelete::execute(std::string&) { } } + commitRecurrenceMaskUpdates(recurrenceMaskUpdates); + // Now list the project changes. for (auto& change : projectChanges) if (change.first != "") Context::getContext().footnote(onProjectChange(change.second)); diff --git a/src/commands/CmdDone.cpp b/src/commands/CmdDone.cpp index 08a36d5b4..4416e4dd9 100644 --- a/src/commands/CmdDone.cpp +++ b/src/commands/CmdDone.cpp @@ -72,6 +72,10 @@ int CmdDone::execute(std::string&) { // Accumulated project change notifications. std::map projectChanges; + RecurrenceMaskUpdates recurrenceMaskUpdates; + auto* recurrenceMaskUpdatesPtr = + Context::getContext().hooks.hasOnModify() ? nullptr : &recurrenceMaskUpdates; + if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); } @@ -97,7 +101,7 @@ int CmdDone::execute(std::string&) { } if (permission(before.diff(task) + question, filtered.size())) { - updateRecurrenceMask(task); + updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); Context::getContext().tdb2.modify(task); ++count; feedback_affected("Completed task {1} '{2}'.", task); @@ -121,6 +125,7 @@ int CmdDone::execute(std::string&) { } } + commitRecurrenceMaskUpdates(recurrenceMaskUpdates); nag(modified); // Now list the project changes. diff --git a/src/recur.cpp b/src/recur.cpp index dc3d957d3..2360a9d47 100644 --- a/src/recur.cpp +++ b/src/recur.cpp @@ -365,35 +365,54 @@ std::optional getNextRecurrence(Datetime& current, std::string& period //////////////////////////////////////////////////////////////////////////////// // When the status of a recurring child task changes, the parent task must // update it's mask. -void updateRecurrenceMask(Task& task) { +static void updateRecurrenceMaskValue(Task& task, Task& parent) { + unsigned int index = strtol(task.get("imask").c_str(), nullptr, 10); + auto mask = parent.get("mask"); + if (mask.length() > index) { + mask[index] = (task.getStatus() == Task::pending) ? '-' + : (task.getStatus() == Task::completed) ? '+' + : (task.getStatus() == Task::deleted) ? 'X' + : (task.getStatus() == Task::waiting) ? 'W' + : '?'; + } else { + mask.clear(); + for (unsigned int i = 0; i < index; ++i) mask += "?"; + + mask += (task.getStatus() == Task::pending) ? '-' + : (task.getStatus() == Task::completed) ? '+' + : (task.getStatus() == Task::deleted) ? 'X' + : (task.getStatus() == Task::waiting) ? 'W' + : '?'; + } + parent.set("mask", mask); +} + +void updateRecurrenceMask(Task& task, RecurrenceMaskUpdates* updates) { auto uuid = task.get("parent"); - Task parent; - - if (uuid != "" && Context::getContext().tdb2.get(uuid, parent)) { - unsigned int index = strtol(task.get("imask").c_str(), nullptr, 10); - auto mask = parent.get("mask"); - if (mask.length() > index) { - mask[index] = (task.getStatus() == Task::pending) ? '-' - : (task.getStatus() == Task::completed) ? '+' - : (task.getStatus() == Task::deleted) ? 'X' - : (task.getStatus() == Task::waiting) ? 'W' - : '?'; - } else { - std::string mask; - for (unsigned int i = 0; i < index; ++i) mask += "?"; - - mask += (task.getStatus() == Task::pending) ? '-' - : (task.getStatus() == Task::completed) ? '+' - : (task.getStatus() == Task::deleted) ? 'X' - : (task.getStatus() == Task::waiting) ? 'W' - : '?'; + if (uuid == "") return; + + if (updates) { + auto existing = updates->find(uuid); + if (existing == updates->end()) { + Task parent; + if (!Context::getContext().tdb2.get(uuid, parent)) return; + existing = updates->emplace(uuid, std::move(parent)).first; } - parent.set("mask", mask); + updateRecurrenceMaskValue(task, existing->second); + } else { + Task parent; + if (!Context::getContext().tdb2.get(uuid, parent)) return; + updateRecurrenceMaskValue(task, parent); Context::getContext().tdb2.modify(parent); } } +void commitRecurrenceMaskUpdates(RecurrenceMaskUpdates& updates) { + for (auto& update : updates) Context::getContext().tdb2.modify(update.second); + updates.clear(); +} + //////////////////////////////////////////////////////////////////////////////// // Delete expired tasks. void handleUntil() { diff --git a/src/recur.h b/src/recur.h index db0b6306d..7ae82f6a4 100644 --- a/src/recur.h +++ b/src/recur.h @@ -43,13 +43,17 @@ #include #include +#include #include +using RecurrenceMaskUpdates = std::map; + std::optional checked_add_datetime(Datetime& base, time_t delta); void handleRecurrence(); bool generateDueDates(Task& parent, std::vector& allDue); std::optional getNextRecurrence(Datetime& current, std::string& period); -void updateRecurrenceMask(Task& task); +void updateRecurrenceMask(Task& task, RecurrenceMaskUpdates* updates = nullptr); +void commitRecurrenceMaskUpdates(RecurrenceMaskUpdates& updates); void handleUntil(); #endif diff --git a/test/hooks.on-modify.test.py b/test/hooks.on-modify.test.py index 9d9a615f2..798047869 100755 --- a/test/hooks.on-modify.test.py +++ b/test/hooks.on-modify.test.py @@ -204,6 +204,18 @@ def test_onmodify_escaped_backslash(self): code, out, err = self.t(f"_get 1.description") self.assertEqual(out.strip(), r"tab\ttab foo") + def test_onmodify_recurring_bulk_completion(self): + hookname = "on-modify-accept" + self.t("add one due:tomorrow recur:daily") + self.t("list rc.recurrence.limit:3 rc.verbose:nothing") + self.t.hooks.add_default(hookname, log=True) + + self.t("2-4 done rc.bulk=0 rc.confirmation=off") + + hook = self.t.hooks[hookname] + hook.assertTriggeredCount(6) + self.assertEqual("+++", self.t.export_one("status:recurring")["mask"]) + if __name__ == "__main__": from simpletap import TAPTestRunner diff --git a/test/recurrence.test.py b/test/recurrence.test.py index 155eb9d1f..deaa80eba 100755 --- a/test/recurrence.test.py +++ b/test/recurrence.test.py @@ -649,6 +649,23 @@ def test_annual_creep(self): self.assertIn("17 20150101", out) +class TestBulkRecurrenceMask(TestCase): + def setUp(self): + self.t = Task() + self.t("add one due:tomorrow recur:daily") + self.t("list rc.recurrence.limit:3 rc.verbose:nothing") + + def test_done(self): + self.t("2-4 done rc.bulk=0 rc.confirmation=off") + self.assertEqual("+++", self.t.export_one("status:recurring")["mask"]) + + def test_delete(self): + self.t( + "2-4 delete rc.bulk=0 rc.confirmation=off rc.recurrence.confirmation=off" + ) + self.assertEqual("XXX", self.t.export_one("status:recurring")["mask"]) + + # TODO Wait a recurring task # TODO Downgrade a recurring task to a regular task # TODO Duplicate a recurring child task From 2e5324a1dc50f8520a9399ffae53f702ab233c4e Mon Sep 17 00:00:00 2001 From: ashprice Date: Mon, 31 Aug 2026 18:41:57 +0100 Subject: [PATCH 34/49] Small fixes to sort Signed-off-by: ashprice --- src/sort.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sort.cpp b/src/sort.cpp index 5cfe5c16b..c3801b3b6 100644 --- a/src/sort.cpp +++ b/src/sort.cpp @@ -107,6 +107,7 @@ void sort_tasks(const std::vector& data, std::vector& order, const st global_random_keys[i] = std::to_string(std::hash{}(data[i].get_ref("uuid") + seed)); } + continue; } if (field == "depends") { @@ -116,6 +117,7 @@ void sort_tasks(const std::vector& data, std::vector& order, const st std::sort(deps.begin(), deps.end()); global_sorted_dep_uuids[i] = std::move(deps); } + continue; } if (field == "recur") { @@ -255,7 +257,7 @@ static bool sort_compare(int left, int right) { if (left_deps == right_deps) continue; - if (left_deps.empty() && right_deps.empty()) return ascending; + if (left_deps.empty() && !right_deps.empty()) return ascending; if (!left_deps.empty() && right_deps.empty()) return !ascending; From e3fe653e5017a816cce7ddb413039795f19df09d Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 10:25:36 +0100 Subject: [PATCH 35/49] Fix filter debug diag timers Signed-off-by: ashprice --- src/Filter.cpp | 3 --- src/commands/CmdCustom.cpp | 15 +++++++++++++++ test/debug.test.py | 9 +++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Filter.cpp b/src/Filter.cpp index b739749a8..ec159688a 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -131,9 +131,6 @@ void Filter::filter_to_indices(const std::vector& pending, std::vector {2} tasks [pending only]", pending.size(), indices.size())); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/commands/CmdCustom.cpp b/src/commands/CmdCustom.cpp index d14a84e8e..12a96d471 100644 --- a/src/commands/CmdCustom.cpp +++ b/src/commands/CmdCustom.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -107,6 +108,9 @@ int CmdCustom::execute(std::string& output) { const std::vector* data = nullptr; std::vector sequence; std::vector filtered; + Timer filterTimer; + double loadTime = 0; + size_t sourceCount = 0; // call prepareFilter() here so that filter_to_indices and filter_to_tasks // don't have to call it. @@ -115,17 +119,28 @@ int CmdCustom::execute(std::string& output) { bool use_pending_indices = filter.pendingOnly(); if (use_pending_indices) { + Timer loadTimer; const auto& pending = Context::getContext().tdb2.pending_tasks(); + loadTime = loadTimer.total_us(); + sourceCount = pending.size(); filter.filter_to_indices(pending, sequence); data = &pending; } else { + Timer loadTimer; auto all = Context::getContext().tdb2.all_tasks(); + loadTime = loadTimer.total_us(); + sourceCount = all.size(); filter.filter_to_tasks(all, filtered); sequence.clear(); for (unsigned int i = 0; i < filtered.size(); ++i) sequence.push_back(i); data = &filtered; } + Context::getContext().debug(format("Filtered {1} tasks --> {2} tasks [{3}]", sourceCount, + sequence.size(), + use_pending_indices ? "pending only" : "all tasks")); + Context::getContext().time_filter_us += filterTimer.total_us() - loadTime; + if (sortOrder.size() && sortOrder[0] == "none") { // If there is no sort order, we preserve the order they were specified // and rebuild the sequence using indices. diff --git a/test/debug.test.py b/test/debug.test.py index 71bd5eb69..d4bf5a534 100755 --- a/test/debug.test.py +++ b/test/debug.test.py @@ -85,6 +85,15 @@ def test_debug_hooks_output(self): self.assertIn("Filtered 2 tasks --> 2 tasks [pending only]", err) self.assertIn("Perf task", err) + def test_all_report_debug_filter_output(self): + """Verify reports that call all tasks instead of pending + retain the filter debug summary""" + self.t("1 done") + + code, out, err = self.t("status:completed all rc.debug=1") + + self.assertIn("Filtered 2 tasks --> 1 tasks [all tasks]", err) + def test_status_filter_shortcut_ignores_other_filters(self): """Checks that we aren't accidentally matching other filters with the same keywords""" From a3651513abb0fe30638bcb8cf57805958061e795 Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 10:51:10 +0100 Subject: [PATCH 36/49] Preserve references to dep ids in completed task reports Signed-off-by: ashprice --- src/Task.cpp | 17 ++++++++--------- test/dependencies.test.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Task.cpp b/src/Task.cpp index bd0a8f142..a5e869df8 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -1050,15 +1050,14 @@ bool Task::hasDependency(const std::string& uuid) const { std::vector Task::getDependencyIDs() const { std::vector ids; - const auto& graph = Context::getContext().tdb2.dependency_graph(); - auto found = graph.dependencies.find(get_ref("uuid")); - if (found == graph.dependencies.end()) return ids; - - const auto& tasks = Context::getContext().tdb2.pending_tasks(); - ids.reserve(found->second.size()); - for (auto idx : found->second) - if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) - ids.push_back(tasks[idx].id); + auto dependencies = getDependencyUUIDs(); + ids.reserve(dependencies.size()); + for (const auto& dependency : dependencies) { + auto* task = Context::getContext().tdb2.find_pending(dependency); + if (!task) continue; + auto status = task->getStatus(); + if (status != Task::completed && status != Task::deleted) ids.push_back(task->id); + } std::sort(ids.begin(), ids.end()); diff --git a/test/dependencies.test.py b/test/dependencies.test.py index d1d98da8d..b5767d4aa 100755 --- a/test/dependencies.test.py +++ b/test/dependencies.test.py @@ -100,6 +100,20 @@ def test_completed_dependency_is_not_rendered(self): self.assertIn(["2"], [line.split() for line in out.splitlines()]) + def test_completed_dependent_renders_active_dep(self): + """Completed tasks retain references to IDs for active dependencies""" + self.t("2 modify dep:1") + self.t("2 done rc.confirmation=off") + + code, out, err = self.t( + "status:completed all rc.verbose=nothing " + "rc.report.all.columns=description,depends " + "rc.report.all.labels=Description,Depends " + "rc.report.all.sort=description+" + ) + + self.assertIn(["two", "1"], [line.split() for line in out.splitlines()]) + def test_dag(self): """Check acyclic graph support""" self.t("add three") From a15fa68be88c404e0393f83e9d7536512dfcc78f Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 10:57:48 +0100 Subject: [PATCH 37/49] Fix on-exit handling for hooks Signed-off-by: ashprice --- src/TDB2.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/TDB2.cpp b/src/TDB2.cpp index 5a3e06fe9..fa4b80172 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -88,7 +88,6 @@ void TDB2::add(Task& task) { maybe_add_undo_point(ops); auto uuid = task.get_ref("uuid"); - changes[uuid] = task; tc::Uuid tcuuid = tc::uuid_from_string(uuid); // run hooks for this new task @@ -122,6 +121,7 @@ void TDB2::add(Task& task) { if (id > 0) { task.id = id; } + changes[uuid] = task; } //////////////////////////////////////////////////////////////////////////////// @@ -149,8 +149,6 @@ void TDB2::modify(Task& task) { rust::Vec ops; maybe_add_undo_point(ops); - changes[uuid] = task; - // invoke the hook and allow it to modify the task before updating Task original; bool found_original = false; @@ -210,6 +208,7 @@ void TDB2::modify(Task& task) { } replica()->commit_operations(std::move(ops)); + changes[uuid] = task; // If the task entered or left the working set/dependency graph, we must // invalidate the cache. From 7631d3d2c54a6986fb230b824fef693964ad1a55 Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 11:22:21 +0100 Subject: [PATCH 38/49] Cache metadata correctness fixes Signed-off-by: ashprice --- src/Hooks.cpp | 11 ++++------- src/TDB2.cpp | 5 +++++ src/Task.cpp | 14 ++++++++++++++ src/Task.h | 5 +++++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/Hooks.cpp b/src/Hooks.cpp index b88b84e1b..ad73ab18b 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -30,6 +30,7 @@ #include #include +#include // If is included, put it after , because it includes // , and therefore would ignore the _WITH_GETLINE. #ifdef FREEBSD @@ -279,9 +280,6 @@ void Hooks::onModify(Task& before, Task& after) const { if (!_enabled) return; Timer timer; - const auto id = after.id; - const auto is_blocked = after.is_blocked; - const auto is_blocking = after.is_blocking; std::vector matchingScripts = scripts("on-modify"); if (matchingScripts.size()) { @@ -316,10 +314,9 @@ void Hooks::onModify(Task& before, Task& after) const { } } - after = Task(input[1]); - after.id = id; - after.is_blocked = is_blocked; - after.is_blocking = is_blocking; + Task updated(input[1]); + updated.copyTransientState(after); + after = std::move(updated); } Context::getContext().time_hooks_us += timer.total_us(); diff --git a/src/TDB2.cpp b/src/TDB2.cpp index fa4b80172..f21481fc8 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -219,6 +219,8 @@ void TDB2::modify(Task& task) { return; } + Task::invalidateUrgencyCaches(); + // If the task stayed in the set, we can edit the vector in-place. // This speeds up modifications a lot relative to reloading and parsing from rust. bool deps_changed = false; @@ -226,6 +228,7 @@ void TDB2::modify(Task& task) { auto* pt = find_pending(uuid); if (pt) { auto old_deps = pt->getDependencyUUIDs(); + task.copyTransientState(*pt); *pt = task; auto new_deps = task.getDependencyUUIDs(); if (old_deps != new_deps) { @@ -458,6 +461,7 @@ Task* TDB2::find_pending(const std::string& uuid) { //////////////////////////////////////////////////////////////////////////////// void TDB2::invalidate_cached_info() { + Task::invalidateUrgencyCaches(); _pending_tasks = std::nullopt; _completed_tasks = std::nullopt; _working_set = std::nullopt; @@ -643,6 +647,7 @@ static void dependency_update(std::vector& tasks, if (new_set.find(dep) != new_set.end()) continue; auto target = uuid_index.find(dep); if (target == uuid_index.end()) continue; + if (!task_is_active || !participates_in_dependency_graph(tasks[target->second])) continue; auto count = dependency_counts.find(dep); if (count != dependency_counts.end()) { diff --git a/src/Task.cpp b/src/Task.cpp index a5e869df8..7a75145f0 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -76,6 +76,7 @@ std::map Task::attributes; std::map Task::coefficients; std::vector Task::userCoefficients; bool Task::urgencyInherit = false; +uint64_t Task::urgencyGeneration = 1; float Task::urgencyProjectCoefficient = 0.0; float Task::urgencyActiveCoefficient = 0.0; float Task::urgencyScheduledCoefficient = 0.0; @@ -116,6 +117,13 @@ bool Task::operator==(const Task& other) { //////////////////////////////////////////////////////////////////////////////// bool Task::operator!=(const Task& other) { return !(*this == other); } +//////////////////////////////////////////////////////////////////////////////// +void Task::copyTransientState(const Task& other) { + id = other.id; + is_blocked = other.is_blocked; + is_blocking = other.is_blocking; +} + //////////////////////////////////////////////////////////////////////////////// Task::Task(const std::string& input) { id = 0; @@ -1759,8 +1767,13 @@ void Task::setUrgencyCoefficients() { {UrgencyCoefficient::udaValue, uda.substr(0, dot), uda.substr(dot + 1), coeff}); } } + + invalidateUrgencyCaches(); } +//////////////////////////////////////////////////////////////////////////////// +void Task::invalidateUrgencyCaches() { ++urgencyGeneration; } + //////////////////////////////////////////////////////////////////////////////// // Urgency is defined as a polynomial, the value of which is calculated in this // function, according to: @@ -1867,6 +1880,7 @@ float Task::urgency() const { // first call and then the computed value will be reused from the cache. urgency_value = 0.0; recalc_urgency = false; + urgency_generation = urgencyGeneration; urgency_value = urgency_c(); } diff --git a/src/Task.h b/src/Task.h index e1859245a..545c21094 100644 --- a/src/Task.h +++ b/src/Task.h @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -59,6 +60,7 @@ class Task { static std::vector userCoefficients; // Cached value of rc.urgency.inherit. static bool urgencyInherit; + static uint64_t urgencyGeneration; static std::map> customOrder; static float urgencyProjectCoefficient; static float urgencyActiveCoefficient; @@ -76,6 +78,7 @@ class Task { Task() = default; bool operator==(const Task&); bool operator!=(const Task&); + void copyTransientState(const Task&); Task(const std::string&); Task(const json::object*); Task(rust::Box); @@ -94,6 +97,7 @@ class Task { int id{0}; mutable float urgency_value{0.0}; mutable bool recalc_urgency{true}; + mutable uint64_t urgency_generation{0}; bool is_blocked{false}; bool is_blocking{false}; int annotation_count{0}; @@ -105,6 +109,7 @@ class Task { // Parse coefficients into userCoefficients. This is called // after that map has been initialized. static void setUrgencyCoefficients(); + static void invalidateUrgencyCaches(); void setAsNow(const std::string&); bool has(const std::string&) const; From 7e81187dfc1c8a44d3288ea1e958aa104f6655e5 Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 11:35:38 +0100 Subject: [PATCH 39/49] Recurrence mask correctness changes Signed-off-by: ashprice --- src/recur.cpp | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/recur.cpp b/src/recur.cpp index 2360a9d47..2261cd13f 100644 --- a/src/recur.cpp +++ b/src/recur.cpp @@ -42,6 +42,7 @@ #include #include +#include #include #include @@ -366,24 +367,20 @@ std::optional getNextRecurrence(Datetime& current, std::string& period // When the status of a recurring child task changes, the parent task must // update it's mask. static void updateRecurrenceMaskValue(Task& task, Task& parent) { - unsigned int index = strtol(task.get("imask").c_str(), nullptr, 10); + const auto& imask = task.get_ref("imask"); + unsigned int index = 0; + auto parsed = std::from_chars(imask.data(), imask.data() + imask.size(), index); + if (parsed.ec != std::errc() || parsed.ptr != imask.data() + imask.size() || + index == std::numeric_limits::max()) + return; auto mask = parent.get("mask"); - if (mask.length() > index) { - mask[index] = (task.getStatus() == Task::pending) ? '-' - : (task.getStatus() == Task::completed) ? '+' - : (task.getStatus() == Task::deleted) ? 'X' - : (task.getStatus() == Task::waiting) ? 'W' - : '?'; - } else { - mask.clear(); - for (unsigned int i = 0; i < index; ++i) mask += "?"; - - mask += (task.getStatus() == Task::pending) ? '-' - : (task.getStatus() == Task::completed) ? '+' - : (task.getStatus() == Task::deleted) ? 'X' - : (task.getStatus() == Task::waiting) ? 'W' - : '?'; - } + auto value = (task.getStatus() == Task::pending) ? '-' + : (task.getStatus() == Task::completed) ? '+' + : (task.getStatus() == Task::deleted) ? 'X' + : (task.getStatus() == Task::waiting) ? 'W' + : '?'; + if (mask.length() <= index) mask.resize(index + 1, '?'); + mask[index] = value; parent.set("mask", mask); } From 28a76a2416b801577317cf1b5b7889abf7de042c Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 12:21:52 +0100 Subject: [PATCH 40/49] Fixes: urgency cache invalidation, reversed burndown ranges, ID lookup conditional Signed-off-by: ashprice --- src/Task.cpp | 3 +-- src/commands/CmdBurndown.cpp | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Task.cpp b/src/Task.cpp index 7a75145f0..3281f80d4 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -745,7 +745,6 @@ void Task::parseTC(rust::Box task, int known_id) { } data["uuid"] = static_cast(task->get_uuid().to_string()); - id = Context::getContext().tdb2.id(data["uuid"]); id = known_id >= 0 ? known_id : Context::getContext().tdb2.id(data["uuid"]); } @@ -1875,7 +1874,7 @@ float Task::urgency_c() const { //////////////////////////////////////////////////////////////////////////////// float Task::urgency() const { - if (recalc_urgency) { + if (recalc_urgency || urgency_generation != urgencyGeneration) { // We set the guard first to avoid infinite recursion. It will be 0.0 on // first call and then the computed value will be reused from the cache. urgency_value = 0.0; diff --git a/src/commands/CmdBurndown.cpp b/src/commands/CmdBurndown.cpp index 7ec55f867..9ea83e09a 100644 --- a/src/commands/CmdBurndown.cpp +++ b/src/commands/CmdBurndown.cpp @@ -244,6 +244,7 @@ void Chart::accumulateTasks(const std::vector& tasks, auto last_it = _peak_day_index.find(last_day); if (first_it == _peak_day_index.end() || last_it == _peak_day_index.end()) return; size_t fi = first_it->second, li = last_it->second; + if (fi > li) return; _peak_diff[fi]++; if (li + 1 < _peak_diff.size()) _peak_diff[li + 1]--; }; @@ -266,6 +267,7 @@ void Chart::accumulateTasks(const std::vector& tasks, // ranges is the taskEpochRange returned by findTaskEpochRange(). const auto& r = ranges[i]; const auto& task = tasks[i]; + if (r.first_epoch > r.last_epoch) continue; // Peak and _current_count are made regardless of the task status. peak_add(r.peak_entry, r.peak_end); @@ -872,6 +874,7 @@ static int runBurndown(char period, std::string& output) { for (const auto& task : tasks) { auto r = findTaskEpochRange(task, now_epoch, now_day_epoch, period, cumulative); result.ranges.push_back(r); + if (r.first_epoch > r.last_epoch) continue; if (r.peak_entry < result.earliest_day) result.earliest_day = r.peak_entry; if (r.peak_end > result.latest_day) result.latest_day = r.peak_end; } From 433857b5b6bb975df3251e550c1caa069223f9fc Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 13:06:21 +0100 Subject: [PATCH 41/49] Preserve parent masks if the command partially fails. Signed-off-by: ashprice --- src/commands/CmdDelete.cpp | 149 +++++++++++++++++++------------------ src/commands/CmdDone.cpp | 82 ++++++++++---------- 2 files changed, 120 insertions(+), 111 deletions(-) diff --git a/src/commands/CmdDelete.cpp b/src/commands/CmdDelete.cpp index 055db2eac..7b41339a3 100644 --- a/src/commands/CmdDelete.cpp +++ b/src/commands/CmdDelete.cpp @@ -90,87 +90,90 @@ int CmdDelete::execute(std::string&) { if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); } - for (auto& task : filtered) { - Task before(task); - - if (task.getStatus() != Task::deleted) { - // Delete the specified task. - std::string question; - question = format("Delete task {1} '{2}'?", task.identifier(true), task.get("description")); - - task.modify(Task::modAnnotate); - task.setStatus(Task::deleted); - if (!task.has("end")) task.setAsNow("end"); - - if (permission(question, filtered.size())) { - updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); - ++count; - Context::getContext().tdb2.modify(task); - feedback_affected("Deleting task {1} '{2}'.", task); - if (task.is_blocking) feedback_unblocked(task); - dependencyChainOnComplete(task); - if (Context::getContext().verbose("project")) - projectChanges.insert_or_assign(task.get("project"), task); - - // Delete siblings. - if (task.has("parent")) { - if ((Context::getContext().config.get("recurrence.confirmation") == "prompt" && - confirm(STRING_CMD_DELETE_CONFIRM_R)) || - Context::getContext().config.getBoolean("recurrence.confirmation")) { - std::vector siblings = Context::getContext().tdb2.siblings(task); - for (auto& sibling : siblings) { - sibling.modify(Task::modAnnotate); - sibling.setStatus(Task::deleted); - if (!sibling.has("end")) sibling.setAsNow("end"); - - updateRecurrenceMask(sibling, recurrenceMaskUpdatesPtr); - Context::getContext().tdb2.modify(sibling); - feedback_affected(STRING_CMD_DELETE_TASK_R, sibling); - feedback_unblocked(sibling); - ++count; - } - - // Delete the parent - Task parent; - Context::getContext().tdb2.get(task.get("parent"), parent); - parent.setStatus(Task::deleted); - if (!parent.has("end")) parent.setAsNow("end"); - - Context::getContext().tdb2.modify(parent); - } - } - // Task potentially has child tasks - optionally delete them. - else { - std::vector children = Context::getContext().tdb2.children(task); - if (children.size() && - ((Context::getContext().config.get("recurrence.confirmation") == "prompt" && - confirm(STRING_CMD_DELETE_CONFIRM_R)) || - Context::getContext().config.getBoolean("recurrence.confirmation"))) { - for (auto& child : children) { - child.modify(Task::modAnnotate); - child.setStatus(Task::deleted); - if (!child.has("end")) child.setAsNow("end"); - - updateRecurrenceMask(child, recurrenceMaskUpdatesPtr); - Context::getContext().tdb2.modify(child); - feedback_affected(STRING_CMD_DELETE_TASK_R, child); - feedback_unblocked(child); - ++count; + try { + for (auto& task : filtered) { + Task before(task); + + if (task.getStatus() != Task::deleted) { + std::string question; + question = format("Delete task {1} '{2}'?", task.identifier(true), task.get("description")); + + task.modify(Task::modAnnotate); + task.setStatus(Task::deleted); + if (!task.has("end")) task.setAsNow("end"); + + if (permission(question, filtered.size())) { + if (!recurrenceMaskUpdatesPtr) updateRecurrenceMask(task); + ++count; + Context::getContext().tdb2.modify(task); + if (recurrenceMaskUpdatesPtr) updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); + feedback_affected("Deleting task {1} '{2}'.", task); + if (task.is_blocking) feedback_unblocked(task); + dependencyChainOnComplete(task); + if (Context::getContext().verbose("project")) + projectChanges.insert_or_assign(task.get("project"), task); + + // Delete the siblings + if (task.has("parent")) { + if ((Context::getContext().config.get("recurrence.confirmation") == "prompt" && + confirm(STRING_CMD_DELETE_CONFIRM_R)) || + Context::getContext().config.getBoolean("recurrence.confirmation")) { + std::vector siblings = Context::getContext().tdb2.siblings(task); + for (auto& sibling : siblings) { + sibling.modify(Task::modAnnotate); + sibling.setStatus(Task::deleted); + if (!sibling.has("end")) sibling.setAsNow("end"); + + updateRecurrenceMask(sibling, recurrenceMaskUpdatesPtr); + Context::getContext().tdb2.modify(sibling); + feedback_affected(STRING_CMD_DELETE_TASK_R, sibling); + feedback_unblocked(sibling); + ++count; + } + + // Delete the parent + Task parent; + Context::getContext().tdb2.get(task.get("parent"), parent); + parent.setStatus(Task::deleted); + if (!parent.has("end")) parent.setAsNow("end"); + + Context::getContext().tdb2.modify(parent); + } + } else { + std::vector children = Context::getContext().tdb2.children(task); + if (children.size() && + ((Context::getContext().config.get("recurrence.confirmation") == "prompt" && + confirm(STRING_CMD_DELETE_CONFIRM_R)) || + Context::getContext().config.getBoolean("recurrence.confirmation"))) { + for (auto& child : children) { + child.modify(Task::modAnnotate); + child.setStatus(Task::deleted); + if (!child.has("end")) child.setAsNow("end"); + + updateRecurrenceMask(child, recurrenceMaskUpdatesPtr); + Context::getContext().tdb2.modify(child); + feedback_affected(STRING_CMD_DELETE_TASK_R, child); + feedback_unblocked(child); + ++count; + } } } + } else { + std::cout << "Task not deleted.\n"; + rc = 1; + if (_permission_quit) break; } } else { - std::cout << "Task not deleted.\n"; + std::cout << format("Task {1} '{2}' is not deleteable.", task.identifier(true), + task.get("description")) + << '\n'; rc = 1; - if (_permission_quit) break; } - } else { - std::cout << format("Task {1} '{2}' is not deletable.", task.identifier(true), - task.get("description")) - << '\n'; - rc = 1; } + } catch (...) { + commitRecurrenceMaskUpdates(recurrenceMaskUpdates); + throw; } commitRecurrenceMaskUpdates(recurrenceMaskUpdates); diff --git a/src/commands/CmdDone.cpp b/src/commands/CmdDone.cpp index 4416e4dd9..6f83f85c3 100644 --- a/src/commands/CmdDone.cpp +++ b/src/commands/CmdDone.cpp @@ -81,48 +81,54 @@ int CmdDone::execute(std::string&) { } std::vector modified; - for (auto& task : filtered) { - Task before(task); - - if (task.getStatus() == Task::pending || task.getStatus() == Task::waiting) { - // Complete the specified task. - std::string question = - format("Complete task {1} '{2}'?", task.identifier(true), task.get("description")); - - task.modify(Task::modAnnotate); - task.setStatus(Task::completed); - if (!task.has("end")) task.setAsNow("end"); - - // Stop the task, if started. - if (task.has("start")) { - task.remove("start"); - if (Context::getContext().config.getBoolean("journal.time")) - task.addAnnotation(Context::getContext().config.get("journal.time.stop.annotation")); - } - - if (permission(before.diff(task) + question, filtered.size())) { - updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); - Context::getContext().tdb2.modify(task); - ++count; - feedback_affected("Completed task {1} '{2}'.", task); - if (task.is_blocking) feedback_unblocked(task); - dependencyChainOnComplete(task); - if (Context::getContext().verbose("project")) - projectChanges.insert_or_assign(task.get("project"), task); - - // Save unmodified task for potential nagging later - modified.push_back(before); + try { + for (auto& task : filtered) { + Task before(task); + + if (task.getStatus() == Task::pending || task.getStatus() == Task::waiting) { + // Complete the task + std::string question = + format("Complete task {1} '{2}'?", task.identifier(true), task.get("description")); + + task.modify(Task::modAnnotate); + task.setStatus(Task::completed); + if (!task.has("end")) task.setAsNow("end"); + + // Stop the task if it has a start time. + if (task.has("start")) { + task.remove("start"); + if (Context::getContext().config.getBoolean("journal.time")) + task.addAnnotation(Context::getContext().config.get("journal.time.stop.annotation")); + } + + if (permission(before.diff(task) + question, filtered.size())) { + if (!recurrenceMaskUpdatesPtr) updateRecurrenceMask(task); + Context::getContext().tdb2.modify(task); + if (recurrenceMaskUpdatesPtr) updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); + ++count; + feedback_affected("Completed task {1} '{2}'.", task); + if (task.is_blocking) feedback_unblocked(task); + dependencyChainOnComplete(task); + if (Context::getContext().verbose("project")) + projectChanges.insert_or_assign(task.get("project"), task); + + // Save unmodified task for nag. + modified.push_back(before); + } else { + std::cout << "Task not completed.\n"; + rc = 1; + if (_permission_quit) break; + } } else { - std::cout << "Task not completed.\n"; + std::cout << format("Task {1} '{2}' is neither pending nor waiting.", task.identifier(true), + task.get("description")) + << '\n'; rc = 1; - if (_permission_quit) break; } - } else { - std::cout << format("Task {1} '{2}' is neither pending nor waiting.", task.identifier(true), - task.get("description")) - << '\n'; - rc = 1; } + } catch (...) { + commitRecurrenceMaskUpdates(recurrenceMaskUpdates); + throw; } commitRecurrenceMaskUpdates(recurrenceMaskUpdates); From d15b3ebbb9f55fc6dfb87db6d06297b617b3327b Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 13:17:18 +0100 Subject: [PATCH 42/49] Switched getDependencyTasks() to cached UUID lookup, preserves completed tasks' dep references after working set rebuild. Signed-off-by: ashprice --- src/Task.cpp | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/Task.cpp b/src/Task.cpp index 3281f80d4..682b0472a 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -1083,23 +1083,17 @@ std::vector Task::getDependencyUUIDs() const { } //////////////////////////////////////////////////////////////////////////////// -// Uses the cached dependency map instead of scanning the vector. +// Return the pending task this task depends on using cached UUID lookup. std::vector Task::getDependencyTasks() const { - const auto& uuid = get_ref("uuid"); - std::vector blocking; - auto& graph = Context::getContext().tdb2.dependency_graph(); - auto found = graph.dependencies.find(uuid); - - if (found == graph.dependencies.end()) return blocking; - - blocking.reserve(found->second.size()); - - const auto& tasks = Context::getContext().tdb2.pending_tasks(); - for (auto idx : found->second) - if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) - blocking.push_back(tasks[idx]); + auto dependencies = getDependencyUUIDs(); + blocking.reserve(dependencies.size()); + for (const auto& dependency : dependencies) { + auto* task = Context::getContext().tdb2.find_pending(dependency); + if (task && task->getStatus() != Task::completed && task->getStatus() != Task::deleted) + blocking.push_back(*task); + } std::sort(blocking.begin(), blocking.end(), [](const Task& left, const Task& right) { return left.id < right.id; }); From d3698b6d1b55b4169cba29b8db9e45ae7f83a16a Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 13:41:17 +0100 Subject: [PATCH 43/49] Further tightened pendingOnly filter and fixed upstream bug. Signed-off-by: ashprice --- src/Filter.cpp | 51 +++++++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/Filter.cpp b/src/Filter.cpp index ec159688a..4dc96f239 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -90,6 +90,7 @@ void Filter::subset(std::vector& output) { Timer pending_completed; output = Context::getContext().tdb2.all_tasks(); Context::getContext().time_filter_us -= pending_completed.total_us(); + _startCount = (int)output.size(); } _endCount = (int)output.size(); @@ -158,9 +159,9 @@ void Filter::filter_to_tasks(const std::vector& input, std::vector& } //////////////////////////////////////////////////////////////////////////////// -// If the filter contains no 'or', 'xor' or 'not' operators, and only includes -// status values 'pending', 'waiting' or 'recurring', then the filter is -// guaranteed to only need data from pending.data. +// We can use a shortcut for simple comparisons. More general expressions +// cannot do not permit us to do this, because of things like +// (status::pending) = false. bool Filter::pendingOnly() const { if (!Context::getContext().config.getBoolean("gc")) return false; @@ -172,35 +173,47 @@ bool Filter::pendingOnly() const { for (const auto& arg : cli._args) { if (!arg.hasTag("FILTER")) continue; - const auto& raw = arg.attribute("raw"); - if (arg._lextype == Lexer::Type::op && - (raw == "or" || raw == "xor" || raw == "!" || raw == "not")) - return false; - filter_args.push_back(&arg); } - for (size_t i = 0; i + 2 < filter_args.size(); ++i) { + bool pending_constraint = false; + for (size_t i = 0; i < filter_args.size();) { const auto& left = *filter_args[i]; + const auto& raw = left.attribute("raw"); + if (left._lextype == Lexer::Type::op && (raw == "(" || raw == ")" || raw == "and")) { + ++i; + continue; + } + + if (i + 2 >= filter_args.size()) return false; + const auto& op = *filter_args[i + 1]; + const auto& right = *filter_args[i + 2]; + const auto& operation = op.attribute("raw"); + if (left._lextype != Lexer::Type::dom || op._lextype != Lexer::Type::op || + right._lextype == Lexer::Type::op || + (operation != "=" && operation != "==" && operation != "!=" && operation != "!==" && + operation != "<" && operation != "<=" && operation != ">" && operation != ">=" && + operation != "~" && operation != "!~" && operation != "_hastag_" && + operation != "_notag_")) + return false; + i += 3; + + if (right._lextype != Lexer::Type::string) continue; + const auto& value = right.attribute("raw"); - if (left._lextype == Lexer::Type::dom && left.attribute("canonical") == "status" && - op._lextype == Lexer::Type::op && - (op.attribute("raw") == "=" || op.attribute("raw") == "==") && + if (left.attribute("canonical") == "status" && (operation == "=" || operation == "==") && (value == "pending" || value == "waiting" || value == "recurring")) - return true; + pending_constraint = true; - if (left._lextype == Lexer::Type::dom && left.attribute("raw") == "tags" && - op._lextype == Lexer::Type::op && op.attribute("raw") == "_hastag_" && + if (raw == "tags" && operation == "_hastag_" && (value == "PENDING" || value == "ACTIVE" || value == "READY" || value == "WAITING")) - return true; + pending_constraint = true; } - if (!cli._id_ranges.empty()) return true; - - return false; + return pending_constraint || !cli._id_ranges.empty(); } //////////////////////////////////////////////////////////////////////////////// From bcf95de20e18f44158b312d06a6c7c99d735f888 Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 13:55:20 +0100 Subject: [PATCH 44/49] Updated debug timers and moved commit_operations to a helper to that end. Small comment change for DependencyGraph(). Signed-off-by: ashprice --- src/Context.cpp | 7 +++---- src/TDB2.cpp | 18 ++++++++++++++---- src/TDB2.h | 4 ++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Context.cpp b/src/Context.cpp index 1347b7f0d..efd422569 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -738,12 +738,11 @@ int Context::run() { #endif << ' ' << Datetime().toISO() - << " init:" << time_init_us << " load:" << time_load_us - << " gc:" << (time_gc_us > 0 ? time_gc_us - time_load_us : time_gc_us) + << " init:" << time_init_us << " load:" << time_load_us << " gc:" << time_gc_us << " filter:" << time_filter_us << " commit:" << time_commit_us << " sort:" << time_sort_us << " render:" << time_render_us << " hooks:" << time_hooks_us << " other:" - << time_total_us - time_init_us - time_gc_us - time_filter_us - time_commit_us - - time_sort_us - time_render_us - time_hooks_us + << time_total_us - time_init_us - time_load_us - time_gc_us - time_filter_us - + time_commit_us - time_sort_us - time_render_us - time_hooks_us << " total:" << time_total_us << '\n'; debug(s.str()); } diff --git a/src/TDB2.cpp b/src/TDB2.cpp index f21481fc8..bc0a887bf 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -112,7 +112,7 @@ void TDB2::add(Task& task) { if (!deferred_status.empty()) { tctask->set_status(statusFromString(deferred_status), ops); } - replica()->commit_operations(std::move(ops)); + commit_operations(std::move(ops)); invalidate_cached_info(); @@ -207,7 +207,7 @@ void TDB2::modify(Task& task) { tctask->set_status(statusFromString(deferred_status.value()), ops); } - replica()->commit_operations(std::move(ops)); + commit_operations(std::move(ops)); changes[uuid] = task; // If the task entered or left the working set/dependency graph, we must @@ -257,7 +257,7 @@ void TDB2::purge(Task& task) { if (maybe_tctask.is_some()) { auto tctask = maybe_tctask.take(); tctask->delete_task(ops); - replica()->commit_operations(std::move(ops)); + commit_operations(std::move(ops)); } invalidate_cached_info(); @@ -269,6 +269,12 @@ rust::Box& TDB2::replica() { assert(_replica); return _replica.value(); } +//////////////////////////////////////////////////////////////////////////////// +void TDB2::commit_operations(rust::Vec&& ops) { + Timer timer; + replica()->commit_operations(std::move(ops)); + Context::getContext().time_commit_us += timer.total_us(); +} //////////////////////////////////////////////////////////////////////////////// const rust::Box& TDB2::working_set() { @@ -297,6 +303,7 @@ void TDB2::get_changes(std::vector& changes) { //////////////////////////////////////////////////////////////////////////////// void TDB2::gc() { Timer timer; + auto load_before = Context::getContext().time_load_us; // Allowed as an override, but not recommended. if (Context::getContext().config.getBoolean("gc") && !working_set_is_clean()) { @@ -304,7 +311,8 @@ void TDB2::gc() { invalidate_cached_info(); } - Context::getContext().time_gc_us += timer.total_us(); + Context::getContext().time_gc_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); } bool TDB2::working_set_is_clean() { @@ -403,6 +411,7 @@ const std::vector& TDB2::pending_tasks() { // which involve completed tasks. const std::vector& TDB2::completed_tasks() { if (!_completed_tasks) { + Timer timer; auto all_tctasks = replica()->all_task_data(); auto& ws = working_set(); @@ -418,6 +427,7 @@ const std::vector& TDB2::completed_tasks() { } } _completed_tasks = std::move(result); + Context::getContext().time_load_us += timer.total_us(); } return *_completed_tasks; } diff --git a/src/TDB2.h b/src/TDB2.h index 8c8c55551..8058c4488 100644 --- a/src/TDB2.h +++ b/src/TDB2.h @@ -74,8 +74,7 @@ class TDB2 { const std::vector& pending_tasks(); const std::vector& completed_tasks(); // dependency_graph is built on first use from pending_tasks() and reused. - // functions that use it are Task::getDependencyTasks(), getBlockedTasks() - // and urgency_inherit(). + // It is used by Task::getBlockedTasks() and urgency_inherit(). const DependencyGraph& dependency_graph(); bool get(int, Task&); @@ -122,6 +121,7 @@ class TDB2 { std::map changes; const rust::Box& working_set(); + void commit_operations(rust::Vec&&); bool working_set_is_clean(); void maybe_add_undo_point(rust::Vec&); }; From da5f4fb2a242be0b89bf43ad2adf4692b37095ae Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 14:36:59 +0100 Subject: [PATCH 45/49] Keep depends indicators for inactive source tasks. Signed-off-by: ashprice --- src/columns/ColDepends.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/columns/ColDepends.cpp b/src/columns/ColDepends.cpp index dcfaf29cf..f75a32add 100644 --- a/src/columns/ColDepends.cpp +++ b/src/columns/ColDepends.cpp @@ -39,6 +39,12 @@ #define STRING_COLUMN_LABEL_DEP "Depends" +static bool hasActiveDependency(const Task& task) { + if (task.is_blocked) return true; + const auto& status = task.get_ref("status"); + return (status == "completed" || status == "deleted") && !task.getDependencyIDs().empty(); +} + //////////////////////////////////////////////////////////////////////////////// ColumnDepends::ColumnDepends() { _name = "depends"; @@ -68,11 +74,8 @@ void ColumnDepends::measure(const Task& task, unsigned int& minimum, unsigned in minimum = maximum = 0; if (_style == "indicator") { - // We only need to know if the task has a dependency. We don't have to - // look at the whole list. The flags are set during cache construction. - if (task.is_blocked) { + if (hasActiveDependency(task)) minimum = maximum = utf8_width(Context::getContext().config.get("dependency.indicator")); - } return; } @@ -102,7 +105,7 @@ void ColumnDepends::render(std::vector& lines, const Task& task, in // We only need to know if the task has a dependency. We don't have to // look at the whole list. The flags are set during cache construction. if (_style == "indicator") { - if (task.is_blocked) + if (hasActiveDependency(task)) renderStringRight(lines, width, color, Context::getContext().config.get("dependency.indicator")); return; From edaeb08e9bc2bc273ab458f5e31abe7c74b3dd87 Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 14:49:46 +0100 Subject: [PATCH 46/49] Exclude nested loading from debug diag timers Signed-off-by: ashprice --- src/Filter.cpp | 14 ++++++-------- src/ViewTask.cpp | 13 +++++++++---- src/commands/CmdCustom.cpp | 9 +++------ src/sort.cpp | 4 +++- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Filter.cpp b/src/Filter.cpp index 4dc96f239..a47865b9b 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -40,6 +40,7 @@ // Take an input set of tasks and filter into a subset. void Filter::subset(const std::vector& input, std::vector& output) { Timer timer; + const auto load_before = Context::getContext().time_load_us; _startCount = (int)input.size(); Context::getContext().cli2.prepareFilter(); @@ -49,13 +50,15 @@ void Filter::subset(const std::vector& input, std::vector& output) { _endCount = (int)output.size(); Context::getContext().debug( format("Filtered {1} tasks --> {2} tasks [list subset]", _startCount, _endCount)); - Context::getContext().time_filter_us += timer.total_us(); + Context::getContext().time_filter_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); } //////////////////////////////////////////////////////////////////////////////// // Take the set of all tasks and filter into a subset. void Filter::subset(std::vector& output) { Timer timer; + const auto load_before = Context::getContext().time_load_us; Context::getContext().cli2.prepareFilter(); std::vector> precompiled; @@ -66,9 +69,7 @@ void Filter::subset(std::vector& output) { bool shortcut = false; if (precompiled.size()) { - Timer timer_pending; const auto& pending = Context::getContext().tdb2.pending_tasks(); - Context::getContext().time_filter_us -= timer_pending.total_us(); _startCount = (int)pending.size(); output.clear(); @@ -77,9 +78,7 @@ void Filter::subset(std::vector& output) { shortcut = pendingOnly(); if (!shortcut) { - Timer timer_completed; const auto& completed = Context::getContext().tdb2.completed_tasks(); - Context::getContext().time_filter_us -= timer_completed.total_us(); _startCount += (int)completed.size(); filter_to_tasks(completed, output); @@ -87,16 +86,15 @@ void Filter::subset(std::vector& output) { } else { safety(); - Timer pending_completed; output = Context::getContext().tdb2.all_tasks(); - Context::getContext().time_filter_us -= pending_completed.total_us(); _startCount = (int)output.size(); } _endCount = (int)output.size(); Context::getContext().debug(format("Filtered {1} tasks --> {2} tasks [{3}]", _startCount, _endCount, (shortcut ? "pending only" : "all tasks"))); - Context::getContext().time_filter_us += timer.total_us(); + Context::getContext().time_filter_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); } ///////////////////////////////////////////////////////////////////////////////// diff --git a/src/ViewTask.cpp b/src/ViewTask.cpp index 8fa40bd3d..db0846f50 100644 --- a/src/ViewTask.cpp +++ b/src/ViewTask.cpp @@ -107,6 +107,7 @@ ViewTask::~ViewTask() { // std::string ViewTask::render(const std::vector& data, std::vector& sequence) { Timer timer; + const auto load_before = Context::getContext().time_load_us; bool const obfuscate = Context::getContext().config.getBoolean("obfuscate"); bool const print_empty_columns = Context::getContext().config.getBoolean("print.empty.columns"); @@ -293,7 +294,8 @@ std::string ViewTask::render(const std::vector& data, std::vector& se // Stop if the line limit is exceeded. if (++_lines >= _truncate_lines && _truncate_lines != 0) { - Context::getContext().time_render_us += timer.total_us(); + Context::getContext().time_render_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); return out; } } @@ -366,7 +368,8 @@ std::string ViewTask::render(const std::vector& data, std::vector& se // Stop if the line limit is exceeded. if (++_lines >= _truncate_lines && _truncate_lines != 0) { - Context::getContext().time_render_us += timer.total_us(); + Context::getContext().time_render_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); return out; } } @@ -375,12 +378,14 @@ std::string ViewTask::render(const std::vector& data, std::vector& se // Stop if the row limit is exceeded. if (++_rows >= _truncate_rows && _truncate_rows != 0) { - Context::getContext().time_render_us += timer.total_us(); + Context::getContext().time_render_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); return out; } } - Context::getContext().time_render_us += timer.total_us(); + Context::getContext().time_render_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); return out; } diff --git a/src/commands/CmdCustom.cpp b/src/commands/CmdCustom.cpp index 12a96d471..056f33f2c 100644 --- a/src/commands/CmdCustom.cpp +++ b/src/commands/CmdCustom.cpp @@ -109,7 +109,7 @@ int CmdCustom::execute(std::string& output) { std::vector sequence; std::vector filtered; Timer filterTimer; - double loadTime = 0; + const auto load_before = Context::getContext().time_load_us; size_t sourceCount = 0; // call prepareFilter() here so that filter_to_indices and filter_to_tasks @@ -119,16 +119,12 @@ int CmdCustom::execute(std::string& output) { bool use_pending_indices = filter.pendingOnly(); if (use_pending_indices) { - Timer loadTimer; const auto& pending = Context::getContext().tdb2.pending_tasks(); - loadTime = loadTimer.total_us(); sourceCount = pending.size(); filter.filter_to_indices(pending, sequence); data = &pending; } else { - Timer loadTimer; auto all = Context::getContext().tdb2.all_tasks(); - loadTime = loadTimer.total_us(); sourceCount = all.size(); filter.filter_to_tasks(all, filtered); sequence.clear(); @@ -139,7 +135,8 @@ int CmdCustom::execute(std::string& output) { Context::getContext().debug(format("Filtered {1} tasks --> {2} tasks [{3}]", sourceCount, sequence.size(), use_pending_indices ? "pending only" : "all tasks")); - Context::getContext().time_filter_us += filterTimer.total_us() - loadTime; + Context::getContext().time_filter_us += + filterTimer.total_us() - (Context::getContext().time_load_us - load_before); if (sortOrder.size() && sortOrder[0] == "none") { // If there is no sort order, we preserve the order they were specified diff --git a/src/sort.cpp b/src/sort.cpp index c3801b3b6..e7175309b 100644 --- a/src/sort.cpp +++ b/src/sort.cpp @@ -69,6 +69,7 @@ static std::vector global_random_keys; //////////////////////////////////////////////////////////////////////////////// void sort_tasks(const std::vector& data, std::vector& order, const std::string& keys) { Timer timer; + const auto load_before = Context::getContext().time_load_us; global_data = &data; // Split the key defs. @@ -148,7 +149,8 @@ void sort_tasks(const std::vector& data, std::vector& order, const st // Only sort if necessary. if (order.size()) std::stable_sort(order.begin(), order.end(), sort_compare); - Context::getContext().time_sort_us += timer.total_us(); + Context::getContext().time_sort_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); } void sort_projects(std::list>& sorted, From 37ef112a26b4cdae7419f3ab06ee24982df9916f Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 15:12:21 +0100 Subject: [PATCH 47/49] pendingOnly filter rewrite (again) Signed-off-by: ashprice --- src/Filter.cpp | 85 +++++++++++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/src/Filter.cpp b/src/Filter.cpp index a47865b9b..ae333e48a 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -157,9 +157,7 @@ void Filter::filter_to_tasks(const std::vector& input, std::vector& } //////////////////////////////////////////////////////////////////////////////// -// We can use a shortcut for simple comparisons. More general expressions -// cannot do not permit us to do this, because of things like -// (status::pending) = false. +// Recognizes pending only constraints that allow us to use the shortcut. bool Filter::pendingOnly() const { if (!Context::getContext().config.getBoolean("gc")) return false; @@ -174,44 +172,61 @@ bool Filter::pendingOnly() const { filter_args.push_back(&arg); } - bool pending_constraint = false; - for (size_t i = 0; i < filter_args.size();) { - const auto& left = *filter_args[i]; - const auto& raw = left.attribute("raw"); - if (left._lextype == Lexer::Type::op && (raw == "(" || raw == ")" || raw == "and")) { - ++i; - continue; + const auto requires_pending = [&](const auto& self, size_t begin, size_t end) -> bool { + if (begin == end) return false; + int depth = 0; + size_t first_close = end; + std::vector conjunctions; + for (size_t i = begin; i < end; ++i) { + const auto& arg = *filter_args[i]; + if (arg._lextype != Lexer::Type::op) continue; + const auto& op = arg.attribute("raw"); + if (op == "(") { + ++depth; + } else if (op == ")") { + if (--depth < 0) return false; + if (depth == 0 && first_close == end) first_close = i; + } else if (depth == 0) { + if (op == "or" || op == "xor") return false; + if (op == "and") conjunctions.push_back(i); + } } - if (i + 2 >= filter_args.size()) return false; - - const auto& op = *filter_args[i + 1]; + if (depth != 0) return false; + if (filter_args[begin]->_lextype == Lexer::Type::op && + filter_args[begin]->attribute("raw") == "(" && first_close == end - 1) + return self(self, begin + 1, end - 1); + + if (!conjunctions.empty()) { + bool required = false; + for (auto boundary : conjunctions) { + required |= self(self, begin, boundary); + begin = boundary + 1; + } + return self(self, begin, end) || required; + } - const auto& right = *filter_args[i + 2]; + if (end - begin != 3) return false; + const auto& left = *filter_args[begin]; + const auto& op = *filter_args[begin + 1]; + const auto& right = *filter_args[begin + 2]; + if (left._lextype != Lexer::Type::dom || op._lextype != Lexer::Type::op) return false; const auto& operation = op.attribute("raw"); - if (left._lextype != Lexer::Type::dom || op._lextype != Lexer::Type::op || - right._lextype == Lexer::Type::op || - (operation != "=" && operation != "==" && operation != "!=" && operation != "!==" && - operation != "<" && operation != "<=" && operation != ">" && operation != ">=" && - operation != "~" && operation != "!~" && operation != "_hastag_" && - operation != "_notag_")) - return false; - i += 3; - - if (right._lextype != Lexer::Type::string) continue; - const auto& value = right.attribute("raw"); - if (left.attribute("canonical") == "status" && (operation == "=" || operation == "==") && - (value == "pending" || value == "waiting" || value == "recurring")) - pending_constraint = true; - - if (raw == "tags" && operation == "_hastag_" && - (value == "PENDING" || value == "ACTIVE" || value == "READY" || value == "WAITING")) - pending_constraint = true; - } - - return pending_constraint || !cli._id_ranges.empty(); + if (left.attribute("raw") == "id" && right._lextype == Lexer::Type::number && + (operation == "=" || operation == "==" || operation == ">=") && + value.find_first_not_of("0123456789") == std::string::npos && + value.find_first_not_of('0') != std::string::npos) + return true; + if (right._lextype != Lexer::Type::string) return false; + return (left.attribute("canonical") == "status" && (operation == "=" || operation == "==") && + (value == "pending" || value == "waiting" || value == "recurring")) || + (left.attribute("raw") == "tags" && operation == "_hastag_" && + (value == "PENDING" || value == "ACTIVE" || value == "READY" || value == "WAITING")); + }; + + return requires_pending(requires_pending, 0, filter_args.size()); } //////////////////////////////////////////////////////////////////////////////// From 2b39a3176cfc29eef7de930da451dfaedee78998 Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 12 Sep 2026 15:23:42 +0100 Subject: [PATCH 48/49] regression fix: recurrance and on-modify hooks returned incorrect masks Signed-off-by: ashprice --- src/commands/CmdDelete.cpp | 7 +++---- src/commands/CmdDone.cpp | 3 +-- src/recur.cpp | 7 +++++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/commands/CmdDelete.cpp b/src/commands/CmdDelete.cpp index 7b41339a3..45893d9b8 100644 --- a/src/commands/CmdDelete.cpp +++ b/src/commands/CmdDelete.cpp @@ -104,10 +104,9 @@ int CmdDelete::execute(std::string&) { if (!task.has("end")) task.setAsNow("end"); if (permission(question, filtered.size())) { - if (!recurrenceMaskUpdatesPtr) updateRecurrenceMask(task); ++count; Context::getContext().tdb2.modify(task); - if (recurrenceMaskUpdatesPtr) updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); + updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); feedback_affected("Deleting task {1} '{2}'.", task); if (task.is_blocking) feedback_unblocked(task); dependencyChainOnComplete(task); @@ -125,8 +124,8 @@ int CmdDelete::execute(std::string&) { sibling.setStatus(Task::deleted); if (!sibling.has("end")) sibling.setAsNow("end"); - updateRecurrenceMask(sibling, recurrenceMaskUpdatesPtr); Context::getContext().tdb2.modify(sibling); + updateRecurrenceMask(sibling, recurrenceMaskUpdatesPtr); feedback_affected(STRING_CMD_DELETE_TASK_R, sibling); feedback_unblocked(sibling); ++count; @@ -151,8 +150,8 @@ int CmdDelete::execute(std::string&) { child.setStatus(Task::deleted); if (!child.has("end")) child.setAsNow("end"); - updateRecurrenceMask(child, recurrenceMaskUpdatesPtr); Context::getContext().tdb2.modify(child); + updateRecurrenceMask(child, recurrenceMaskUpdatesPtr); feedback_affected(STRING_CMD_DELETE_TASK_R, child); feedback_unblocked(child); ++count; diff --git a/src/commands/CmdDone.cpp b/src/commands/CmdDone.cpp index 6f83f85c3..ed424018a 100644 --- a/src/commands/CmdDone.cpp +++ b/src/commands/CmdDone.cpp @@ -102,9 +102,8 @@ int CmdDone::execute(std::string&) { } if (permission(before.diff(task) + question, filtered.size())) { - if (!recurrenceMaskUpdatesPtr) updateRecurrenceMask(task); Context::getContext().tdb2.modify(task); - if (recurrenceMaskUpdatesPtr) updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); + updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); ++count; feedback_affected("Completed task {1} '{2}'.", task); if (task.is_blocking) feedback_unblocked(task); diff --git a/src/recur.cpp b/src/recur.cpp index 2261cd13f..a0f04adb7 100644 --- a/src/recur.cpp +++ b/src/recur.cpp @@ -42,6 +42,7 @@ #include #include +#include #include #include #include @@ -370,8 +371,10 @@ static void updateRecurrenceMaskValue(Task& task, Task& parent) { const auto& imask = task.get_ref("imask"); unsigned int index = 0; auto parsed = std::from_chars(imask.data(), imask.data() + imask.size(), index); - if (parsed.ec != std::errc() || parsed.ptr != imask.data() + imask.size() || - index == std::numeric_limits::max()) + if (parsed.ec != std::errc() || index == std::numeric_limits::max()) return; + const auto end = imask.data() + imask.size(); + if (parsed.ptr != end && (*parsed.ptr != '.' || parsed.ptr + 1 == end || + !std::all_of(parsed.ptr + 1, end, [](char c) { return c == '0'; }))) return; auto mask = parent.get("mask"); auto value = (task.getStatus() == Task::pending) ? '-' From 8bbbcea7f29d6562318a29d38e90dc77df8de7f3 Mon Sep 17 00:00:00 2001 From: ashprice Date: Sat, 19 Sep 2026 13:48:48 +0100 Subject: [PATCH 49/49] Commit parent and child recurrence masks together. Signed-off-by: ashprice --- src/TDB2.cpp | 39 +++++++++++++++++++++++++++++++------- src/TDB2.h | 7 ++++++- src/commands/CmdDelete.cpp | 17 +++++++++++------ src/commands/CmdDone.cpp | 4 ++-- src/recur.cpp | 17 ++++++++++++++++- src/recur.h | 1 + 6 files changed, 68 insertions(+), 17 deletions(-) diff --git a/src/TDB2.cpp b/src/TDB2.cpp index bc0a887bf..b32a1581a 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -139,31 +139,30 @@ void TDB2::add(Task& task) { // this method. In this case, this method throws an error that will make sense // to the user. This is especially unlikely since tasks are only deleted when // they have been unmodified for a long time. -void TDB2::modify(Task& task) { +Task TDB2::prepare_modify(Task& task) { // All locally modified tasks are timestamped, implicitly overwriting any // changes the user or hooks tried to apply to the "modified" attribute. task.setAsNow("modified"); task.validate(false); auto uuid = task.get_ref("uuid"); - rust::Vec ops; - maybe_add_undo_point(ops); - // invoke the hook and allow it to modify the task before updating Task original; - bool found_original = false; tc::Uuid tcuuid = tc::uuid_from_string(uuid); if (!_pending_tasks && task.id > 0) { auto maybe_original = replica()->get_task_data(tcuuid); if (maybe_original.is_some()) { original = Task{maybe_original.take(), task.id}; - found_original = true; } } else { - found_original = get(uuid, original); + get(uuid, original); } Context::getContext().hooks.onModify(original, task); + return original; +} +void TDB2::append_modify(const Task& task, const Task& original, rust::Vec& ops) { + auto tcuuid = tc::uuid_from_string(task.get_ref("uuid")); auto maybe_tctask = replica()->get_task(tcuuid); if (maybe_tctask.is_none()) { throw std::string("task no longer exists"); @@ -206,12 +205,38 @@ void TDB2::modify(Task& task) { if (deferred_status) { tctask->set_status(statusFromString(deferred_status.value()), ops); } +} + +void TDB2::modify(Task& task, const std::function(const Task&)>& related) { + auto original = prepare_modify(task); + std::optional related_task; + std::optional related_task_original; + if (related) { + related_task = related(task); + if (related_task) { + if (related_task->get_ref("uuid") == task.get_ref("uuid")) + throw std::string("A related-task modification must refer to a different task."); + related_task_original = prepare_modify(*related_task); + } + } + + rust::Vec ops; + maybe_add_undo_point(ops); + append_modify(task, original, ops); + if (related_task) append_modify(*related_task, *related_task_original, ops); commit_operations(std::move(ops)); + const auto& uuid = task.get_ref("uuid"); changes[uuid] = task; + if (related_task) { + changes[related_task->get_ref("uuid")] = *related_task; + invalidate_cached_info(); + return; + } // If the task entered or left the working set/dependency graph, we must // invalidate the cache. + bool found_original = original.has("uuid"); bool was_active = found_original && participates_in_dependency_graph(original); bool now_active = participates_in_dependency_graph(task); if (was_active != now_active || !found_original) { diff --git a/src/TDB2.h b/src/TDB2.h index 8058c4488..ddc88d514 100644 --- a/src/TDB2.h +++ b/src/TDB2.h @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -57,7 +58,9 @@ class TDB2 { void open_replica(const std::string&, bool create_if_missing, bool read_write); void add(Task&); - void modify(Task&); + // The optional related task change is taken after the first task's hooks have ran. + // Both tasks' hooks then finish before either modification is committed. + void modify(Task&, const std::function(const Task&)>& related = {}); void purge(Task&); void get_changes(std::vector&); void gc(); @@ -121,6 +124,8 @@ class TDB2 { std::map changes; const rust::Box& working_set(); + Task prepare_modify(Task&); + void append_modify(const Task&, const Task&, rust::Vec&); void commit_operations(rust::Vec&&); bool working_set_is_clean(); void maybe_add_undo_point(rust::Vec&); diff --git a/src/commands/CmdDelete.cpp b/src/commands/CmdDelete.cpp index 45893d9b8..d78be4708 100644 --- a/src/commands/CmdDelete.cpp +++ b/src/commands/CmdDelete.cpp @@ -104,9 +104,9 @@ int CmdDelete::execute(std::string&) { if (!task.has("end")) task.setAsNow("end"); if (permission(question, filtered.size())) { + modifyRecurringTask(task, recurrenceMaskUpdatesPtr); + if (task.getStatus() != Task::deleted) continue; ++count; - Context::getContext().tdb2.modify(task); - updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); feedback_affected("Deleting task {1} '{2}'.", task); if (task.is_blocking) feedback_unblocked(task); dependencyChainOnComplete(task); @@ -119,19 +119,24 @@ int CmdDelete::execute(std::string&) { confirm(STRING_CMD_DELETE_CONFIRM_R)) || Context::getContext().config.getBoolean("recurrence.confirmation")) { std::vector siblings = Context::getContext().tdb2.siblings(task); + bool all_deleted = true; for (auto& sibling : siblings) { sibling.modify(Task::modAnnotate); sibling.setStatus(Task::deleted); if (!sibling.has("end")) sibling.setAsNow("end"); - Context::getContext().tdb2.modify(sibling); - updateRecurrenceMask(sibling, recurrenceMaskUpdatesPtr); + modifyRecurringTask(sibling, recurrenceMaskUpdatesPtr); + if (sibling.getStatus() != Task::deleted) { + all_deleted = false; + continue; + } feedback_affected(STRING_CMD_DELETE_TASK_R, sibling); feedback_unblocked(sibling); ++count; } // Delete the parent + if (!all_deleted) continue; Task parent; Context::getContext().tdb2.get(task.get("parent"), parent); parent.setStatus(Task::deleted); @@ -150,8 +155,8 @@ int CmdDelete::execute(std::string&) { child.setStatus(Task::deleted); if (!child.has("end")) child.setAsNow("end"); - Context::getContext().tdb2.modify(child); - updateRecurrenceMask(child, recurrenceMaskUpdatesPtr); + modifyRecurringTask(child, recurrenceMaskUpdatesPtr); + if (child.getStatus() != Task::deleted) continue; feedback_affected(STRING_CMD_DELETE_TASK_R, child); feedback_unblocked(child); ++count; diff --git a/src/commands/CmdDone.cpp b/src/commands/CmdDone.cpp index ed424018a..52edfaaf1 100644 --- a/src/commands/CmdDone.cpp +++ b/src/commands/CmdDone.cpp @@ -102,8 +102,8 @@ int CmdDone::execute(std::string&) { } if (permission(before.diff(task) + question, filtered.size())) { - Context::getContext().tdb2.modify(task); - updateRecurrenceMask(task, recurrenceMaskUpdatesPtr); + modifyRecurringTask(task, recurrenceMaskUpdatesPtr); + if (task.getStatus() != Task::completed) continue; ++count; feedback_affected("Completed task {1} '{2}'.", task); if (task.is_blocking) feedback_unblocked(task); diff --git a/src/recur.cpp b/src/recur.cpp index a0f04adb7..7593027bf 100644 --- a/src/recur.cpp +++ b/src/recur.cpp @@ -367,7 +367,7 @@ std::optional getNextRecurrence(Datetime& current, std::string& period //////////////////////////////////////////////////////////////////////////////// // When the status of a recurring child task changes, the parent task must // update it's mask. -static void updateRecurrenceMaskValue(Task& task, Task& parent) { +static void updateRecurrenceMaskValue(const Task& task, Task& parent) { const auto& imask = task.get_ref("imask"); unsigned int index = 0; auto parsed = std::from_chars(imask.data(), imask.data() + imask.size(), index); @@ -408,6 +408,21 @@ void updateRecurrenceMask(Task& task, RecurrenceMaskUpdates* updates) { } } +void modifyRecurringTask(Task& task, RecurrenceMaskUpdates* updates) { + if (updates) { + Context::getContext().tdb2.modify(task); + updateRecurrenceMask(task, updates); + } else { + Context::getContext().tdb2.modify(task, [](const Task& accepted) -> std::optional { + const auto& uuid = accepted.get_ref("parent"); + Task parent; + if (uuid.empty() || !Context::getContext().tdb2.get(uuid, parent)) return std::nullopt; + updateRecurrenceMaskValue(accepted, parent); + return parent; + }); + } +} + void commitRecurrenceMaskUpdates(RecurrenceMaskUpdates& updates) { for (auto& update : updates) Context::getContext().tdb2.modify(update.second); updates.clear(); diff --git a/src/recur.h b/src/recur.h index 7ae82f6a4..c0880e1f9 100644 --- a/src/recur.h +++ b/src/recur.h @@ -54,6 +54,7 @@ bool generateDueDates(Task& parent, std::vector& allDue); std::optional getNextRecurrence(Datetime& current, std::string& period); void updateRecurrenceMask(Task& task, RecurrenceMaskUpdates* updates = nullptr); void commitRecurrenceMaskUpdates(RecurrenceMaskUpdates& updates); +void modifyRecurringTask(Task& task, RecurrenceMaskUpdates* updates); void handleUntil(); #endif