···2929 .expect("This should not panic as the nodeid should exist inside"),
3030 )
3131 .filter(|(node, _)| {
3232- let TodoNodeKind::Task(_) = node.data().kind else {
3333- return false;
3434- };
3535- true
3232+ if let TodoNodeKind::Task(ref t) = node.data().kind
3333+ && t.finished_at().is_none()
3434+ {
3535+ return true;
3636+ }
3737+ false
3638 })
3739 .collect::<Vec<_>>();
3840
+3
src/tui/signal.rs
···7979 /// Only works with the inspector
8080 EditDue,
81818282+ /// Toggle whether a `Task` is finished or not.
8383+ ToggleFinish,
8484+8285 /// Internal Signal that tells the app to resume interpreting keys
8386 ExitRawText,
8487
+30-6
src/types/task.rs
···11+use chrono::Local;
12use color_eyre::eyre::{Context, Result, eyre};
23use dto::{
34 DateTime, GroupEntity, HasOne, IntoActiveModel as _, NanoId, TagEntity, TaskActiveModel,
···143144 Ok(())
144145 }
145146147147+ pub async fn toggle_finish(id: NanoId, kt: &Kasten) -> Result<()> {
148148+ let now = Local::now().naive_local();
149149+150150+ let model = TaskEntity::load()
151151+ .filter_by_nano_id(id)
152152+ .one(&kt.db)
153153+ .await?
154154+ .expect("Must exist");
155155+156156+ let new_finished_at = if model.finished_at.is_some() {
157157+ None
158158+ } else {
159159+ Some(now)
160160+ };
161161+162162+ model
163163+ .into_active_model()
164164+ .set_finished_at(new_finished_at)
165165+ .update(&kt.db)
166166+ .await?;
167167+168168+ Ok(())
169169+ }
170170+146171 /// Calcualtes the `p_score` of this `Task`
147172 //NOTE: formula from claude
148173 #[expect(clippy::cast_precision_loss)]
149174 pub fn p_score(&self, parent_score: f64) -> f64 {
150175 let priority_score = self.priority.p_score(); // [0.0, 1.0]
151151- let urgency = self.due.0.map_or(1.0, |due| {
176176+177177+ let urgency = self.due.0.map_or(0.0, |due| {
152178 let now = chrono::Local::now().naive_local();
153179 let hours_remaining = (due - now).num_minutes() as f64 / 60.0;
154154-155155- // Exponential urgency: peaks at/past due, approaches 0 far in future
156156- // Half-life of ~72 hours — tune this constant to taste
157180 let decay = 72.0_f64;
158181 (-hours_remaining / decay).exp2()
159182 });
160183161161- priority_score * urgency * parent_score
184184+ // base: priority alone. bonus: urgency on top, so any due date > no due date.
185185+ // urgency is in (0.0, ~inf] so having a due date always adds to the score.
186186+ (priority_score + urgency) * parent_score
162187 }
163163-164188 pub fn finished_at(&self) -> Option<String> {
165189 self.finished_at
166190 .map(|finished_at| finished_at.format(frontmatter::DATE_FMT_STR).to_string())