From 39fcc8ae0987b16438304fdbd5447a05559d11ff Mon Sep 17 00:00:00 2001 From: suri-codes Date: Sun, 19 Apr 2026 15:22:52 -0700 Subject: [PATCH] feat: sort tasks by priority score! --- src/tui/components/todo/tasklist.rs | 74 ++++++++++++++++++----------- src/types/due.rs | 2 +- src/types/group.rs | 5 ++ src/types/kasten/todo_tree.rs | 48 ++++++++++++++++--- src/types/priority.rs | 12 +++++ src/types/task.rs | 18 +++++++ 6 files changed, 123 insertions(+), 36 deletions(-) diff --git a/src/tui/components/todo/tasklist.rs b/src/tui/components/todo/tasklist.rs index f1a8d59..7b03134 100644 --- a/src/tui/components/todo/tasklist.rs +++ b/src/tui/components/todo/tasklist.rs @@ -19,28 +19,39 @@ impl TaskList<'_> { pub fn new(tree: &TodoTree, scope: &NodeId, state: ListState, width: u16) -> Self { let mut id_list = vec![]; - let render_list = List::new( - tree.tree - .traverse_pre_order(scope) - .expect("This should not panic as the node id should exist inside") - .zip( - tree.tree - .traverse_pre_order_ids(scope) - .expect("This should not panic as the nodeid should exist inside"), - ) - .filter_map(|(node, id)| { - let TodoNodeKind::Task(_) = node.data().kind else { - return None; - }; - - let mut tli: TaskListItem<'_> = node.data().into(); - - id_list.push(id); - - tli.width = width; - Some(Text::from(tli)) - }), - ) + let mut items = tree + .tree + .traverse_pre_order(scope) + .expect("This should not panic as the node id sohuld exist inside") + .zip( + tree.tree + .traverse_pre_order_ids(scope) + .expect("This should not panic as the nodeid should exist inside"), + ) + .filter(|(node, _)| { + let TodoNodeKind::Task(_) = node.data().kind else { + return false; + }; + true + }) + .collect::>(); + + items.sort_by(|(a, _), (b, _)| a.data().p_score.total_cmp(&b.data().p_score)); + + items.reverse(); + + let render_list = List::new(items.into_iter().map(|(node, id)| { + let TodoNodeKind::Task(_) = node.data().kind else { + unreachable!("we already filtered for this earlier") + }; + + let mut tli: TaskListItem<'_> = node.data().into(); + + id_list.push(id); + + tli.width = width; + Text::from(tli) + })) .style(Color::White) .highlight_style(Style::new().on_dark_gray()); @@ -79,6 +90,7 @@ pub struct TaskListItem<'text> { name: Span<'text>, group: Span<'text>, due_priority: Span<'text>, + p_score: Span<'text>, width: u16, } @@ -101,11 +113,15 @@ impl From<&TodoNode> for TaskListItem<'_> { }) .style(Style::new().fg(color.into())); + let p_score = + Span::from(format!("{:.3}", value.p_score)).style(Style::new().fg(color.into())); + Self { name, group, due_priority, width: 0, + p_score, } } } @@ -113,19 +129,21 @@ impl From<&TodoNode> for TaskListItem<'_> { impl<'text> From> for Text<'text> { fn from(value: TaskListItem<'text>) -> Self { let total_width = value.width.saturating_sub(2) as usize; - let name_col = total_width / 2; - let due_content = value.due_priority.content.as_ref(); - let due_col = due_content.len(); - let group_col = total_width.saturating_sub(name_col + due_col); + let name_col = 5 * total_width / 9; + let p_score_col = 10; // e.g. "0.103" — fixed width + let due_col = 22; // enough for "2026-04-22 11:59:59 PM" or a priority label + let group_col = total_width.saturating_sub(name_col + p_score_col + due_col); let name_str = format!("{:due_col$}"); + let p_score_str = format!("{:width$}", value.due_priority.content, width = due_col); let name = Span::styled(name_str, value.name.style); let group = Span::styled(group_str, value.group.style); + let p_score = Span::styled(p_score_str, value.p_score.style); let due = Span::styled(due_str, value.due_priority.style); - Line::from(vec![name, group, due]).into() + Line::from(vec![name, group, p_score, due]).into() } } diff --git a/src/types/due.rs b/src/types/due.rs index b2095c8..c21abbf 100644 --- a/src/types/due.rs +++ b/src/types/due.rs @@ -3,7 +3,7 @@ use chrono::Datelike; use std::fmt::Display; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Due(Option); +pub struct Due(pub Option); impl Due { pub const fn has_date(&self) -> bool { diff --git a/src/types/group.rs b/src/types/group.rs index f8eee09..9d259e6 100644 --- a/src/types/group.rs +++ b/src/types/group.rs @@ -129,6 +129,11 @@ impl Group { Ok(()) } + /// Calcualtes the `p_score` of this `Group` + pub fn p_score(&self, parent_score: f64) -> f64 { + self.priority.p_score() * parent_score + } + pub fn created_at(&self) -> String { self.created_at .format(frontmatter::DATE_FMT_STR) diff --git a/src/types/kasten/todo_tree.rs b/src/types/kasten/todo_tree.rs index 4210663..b09dfad 100644 --- a/src/types/kasten/todo_tree.rs +++ b/src/types/kasten/todo_tree.rs @@ -20,11 +20,16 @@ pub enum TodoNodeKind { pub struct TodoNode { pub depth: usize, pub kind: TodoNodeKind, + pub p_score: f64, } impl TodoNode { - pub const fn new(kind: TodoNodeKind, depth: usize) -> Self { - Self { depth, kind } + pub const fn new(kind: TodoNodeKind, depth: usize, pscore: f64) -> Self { + Self { + depth, + kind, + p_score: pscore, + } } } @@ -40,7 +45,7 @@ impl TodoTree { let mut tree = Tree::::new(); let root_id = tree .insert( - Node::new(TodoNode::new(TodoNodeKind::Root, 0)), + Node::new(TodoNode::new(TodoNodeKind::Root, 0, 1.0)), InsertBehavior::AsRoot, ) .with_context(|| "Could not create root node.")?; @@ -64,7 +69,7 @@ impl TodoTree { for group in root_groups { todo_tree - .add_group_to_tree(db, &root_id, Box::new(group), 0) + .add_group_to_tree(db, &root_id, Box::new(group), 0, 1.0) .await?; } @@ -78,11 +83,14 @@ impl TodoTree { parent_node_id: &NodeId, group: Box, depth: usize, + parent_p_score: f64, ) -> Result<()> { let group_id = group.id.clone(); + let p_score = group.p_score(parent_p_score); + let group_node_id = self.tree.insert( - Node::new(TodoNode::new(TodoNodeKind::Group(group), depth)), + Node::new(TodoNode::new(TodoNodeKind::Group(group), depth, p_score)), InsertBehavior::UnderNode(parent_node_id), )?; @@ -110,9 +118,15 @@ impl TodoTree { .collect(); for task in tasks { + let p_score = task.p_score(p_score); + let task_id = task.id.clone(); let task_node_id = self.tree.insert( - Node::new(TodoNode::new(TodoNodeKind::Task(Box::new(task)), depth + 1)), + Node::new(TodoNode::new( + TodoNodeKind::Task(Box::new(task)), + depth + 1, + p_score, + )), InsertBehavior::UnderNode(&group_node_id), )?; @@ -131,7 +145,7 @@ impl TodoTree { .collect(); for group in children_groups { - self.add_group_to_tree(db, &group_node_id, Box::new(group), depth + 1) + self.add_group_to_tree(db, &group_node_id, Box::new(group), depth + 1, p_score) .await?; } @@ -157,12 +171,22 @@ impl TodoTree { + 1 }; + let parent_p_score = self + .tree + .get(&parent_node_id) + .expect("must exist") + .data() + .p_score; + + let my_pscore = parent_p_score * group.priority.p_score(); + let inserted_node_id = self .tree .insert( Node::new(TodoNode::new( super::TodoNodeKind::Group(Box::new(group.clone())), my_depth, + my_pscore, )), tree::InsertBehavior::UnderNode(&parent_node_id), ) @@ -189,12 +213,22 @@ impl TodoTree { .depth + 1; + let parent_p_score = self + .tree + .get(&parent_node_id) + .expect("must exist") + .data() + .p_score; + + let my_pscore = task.p_score(parent_p_score); + let inserted_node_id = self .tree .insert( Node::new(TodoNode::new( super::TodoNodeKind::Task(Box::new(task.clone())), my_depth, + my_pscore, )), tree::InsertBehavior::UnderNode(&parent_node_id), ) diff --git a/src/types/priority.rs b/src/types/priority.rs index a53bc49..f5f9bfc 100644 --- a/src/types/priority.rs +++ b/src/types/priority.rs @@ -10,6 +10,18 @@ pub struct Priority { field1: PriorityDTO, } +impl Priority { + pub const fn p_score(&self) -> f64 { + match self.field1 { + PriorityDTO::Asap => 1.0, + PriorityDTO::High => 0.9, + PriorityDTO::Medium => 0.75, + PriorityDTO::Low => 0.5, + PriorityDTO::Far => 0.25, + } + } +} + impl From for Priority { fn from(value: PriorityDTO) -> Self { Self { field1: value } diff --git a/src/types/task.rs b/src/types/task.rs index ca88025..4b38d4f 100644 --- a/src/types/task.rs +++ b/src/types/task.rs @@ -143,6 +143,24 @@ impl Task { Ok(()) } + /// Calcualtes the `p_score` of this `Task` + //NOTE: formula from claude + #[expect(clippy::cast_precision_loss)] + pub fn p_score(&self, parent_score: f64) -> f64 { + let priority_score = self.priority.p_score(); // [0.0, 1.0] + let urgency = self.due.0.map_or(1.0, |due| { + let now = chrono::Local::now().naive_local(); + let hours_remaining = (due - now).num_minutes() as f64 / 60.0; + + // Exponential urgency: peaks at/past due, approaches 0 far in future + // Half-life of ~72 hours — tune this constant to taste + let decay = 72.0_f64; + (-hours_remaining / decay).exp2() + }); + + priority_score * urgency * parent_score + } + pub fn finished_at(&self) -> Option { self.finished_at .map(|finished_at| finished_at.format(frontmatter::DATE_FMT_STR).to_string()) -- 2.51.2