My personal-knowledge-system, with deeply integrated task tracking and long term goal planning capabilities.
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: sort tasks by priority score!

+120 -33
+43 -25
src/tui/components/todo/tasklist.rs
··· 19 19 pub fn new(tree: &TodoTree, scope: &NodeId, state: ListState, width: u16) -> Self { 20 20 let mut id_list = vec![]; 21 21 22 - let render_list = List::new( 23 - tree.tree 24 - .traverse_pre_order(scope) 25 - .expect("This should not panic as the node id should exist inside") 26 - .zip( 27 - tree.tree 28 - .traverse_pre_order_ids(scope) 29 - .expect("This should not panic as the nodeid should exist inside"), 30 - ) 31 - .filter_map(|(node, id)| { 32 - let TodoNodeKind::Task(_) = node.data().kind else { 33 - return None; 34 - }; 22 + let mut items = tree 23 + .tree 24 + .traverse_pre_order(scope) 25 + .expect("This should not panic as the node id sohuld exist inside") 26 + .zip( 27 + tree.tree 28 + .traverse_pre_order_ids(scope) 29 + .expect("This should not panic as the nodeid should exist inside"), 30 + ) 31 + .filter(|(node, _)| { 32 + let TodoNodeKind::Task(_) = node.data().kind else { 33 + return false; 34 + }; 35 + true 36 + }) 37 + .collect::<Vec<_>>(); 35 38 36 - let mut tli: TaskListItem<'_> = node.data().into(); 39 + items.sort_by(|(a, _), (b, _)| a.data().p_score.total_cmp(&b.data().p_score)); 37 40 38 - id_list.push(id); 41 + items.reverse(); 39 42 40 - tli.width = width; 41 - Some(Text::from(tli)) 42 - }), 43 - ) 43 + let render_list = List::new(items.into_iter().map(|(node, id)| { 44 + let TodoNodeKind::Task(_) = node.data().kind else { 45 + unreachable!("we already filtered for this earlier") 46 + }; 47 + 48 + let mut tli: TaskListItem<'_> = node.data().into(); 49 + 50 + id_list.push(id); 51 + 52 + tli.width = width; 53 + Text::from(tli) 54 + })) 44 55 .style(Color::White) 45 56 .highlight_style(Style::new().on_dark_gray()); 46 57 ··· 79 90 name: Span<'text>, 80 91 group: Span<'text>, 81 92 due_priority: Span<'text>, 93 + p_score: Span<'text>, 82 94 width: u16, 83 95 } 84 96 ··· 101 113 }) 102 114 .style(Style::new().fg(color.into())); 103 115 116 + let p_score = 117 + Span::from(format!("{:.3}", value.p_score)).style(Style::new().fg(color.into())); 118 + 104 119 Self { 105 120 name, 106 121 group, 107 122 due_priority, 108 123 width: 0, 124 + p_score, 109 125 } 110 126 } 111 127 } ··· 113 129 impl<'text> From<TaskListItem<'text>> for Text<'text> { 114 130 fn from(value: TaskListItem<'text>) -> Self { 115 131 let total_width = value.width.saturating_sub(2) as usize; 116 - let name_col = total_width / 2; 117 - let due_content = value.due_priority.content.as_ref(); 118 - let due_col = due_content.len(); 119 - let group_col = total_width.saturating_sub(name_col + due_col); 132 + let name_col = 5 * total_width / 9; 133 + let p_score_col = 10; // e.g. "0.103" — fixed width 134 + let due_col = 22; // enough for "2026-04-22 11:59:59 PM" or a priority label 135 + let group_col = total_width.saturating_sub(name_col + p_score_col + due_col); 120 136 121 137 let name_str = format!("{:<width$}", value.name.content, width = name_col); 122 138 let group_str = format!("{:<width$}", value.group.content, width = group_col); 123 - let due_str = format!("{due_content:>due_col$}"); 139 + let p_score_str = format!("{:<width$}", value.p_score.content, width = p_score_col); 140 + let due_str = format!("{:>width$}", value.due_priority.content, width = due_col); 124 141 125 142 let name = Span::styled(name_str, value.name.style); 126 143 let group = Span::styled(group_str, value.group.style); 144 + let p_score = Span::styled(p_score_str, value.p_score.style); 127 145 let due = Span::styled(due_str, value.due_priority.style); 128 146 129 - Line::from(vec![name, group, due]).into() 147 + Line::from(vec![name, group, p_score, due]).into() 130 148 } 131 149 }
+1 -1
src/types/due.rs
··· 3 3 use std::fmt::Display; 4 4 5 5 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 6 - pub struct Due(Option<dto::DateTime>); 6 + pub struct Due(pub Option<dto::DateTime>); 7 7 8 8 impl Due { 9 9 pub const fn has_date(&self) -> bool {
+5
src/types/group.rs
··· 129 129 Ok(()) 130 130 } 131 131 132 + /// Calcualtes the `p_score` of this `Group` 133 + pub fn p_score(&self, parent_score: f64) -> f64 { 134 + self.priority.p_score() * parent_score 135 + } 136 + 132 137 pub fn created_at(&self) -> String { 133 138 self.created_at 134 139 .format(frontmatter::DATE_FMT_STR)
+41 -7
src/types/kasten/todo_tree.rs
··· 20 20 pub struct TodoNode { 21 21 pub depth: usize, 22 22 pub kind: TodoNodeKind, 23 + pub p_score: f64, 23 24 } 24 25 25 26 impl TodoNode { 26 - pub const fn new(kind: TodoNodeKind, depth: usize) -> Self { 27 - Self { depth, kind } 27 + pub const fn new(kind: TodoNodeKind, depth: usize, pscore: f64) -> Self { 28 + Self { 29 + depth, 30 + kind, 31 + p_score: pscore, 32 + } 28 33 } 29 34 } 30 35 ··· 40 45 let mut tree = Tree::<TodoNode>::new(); 41 46 let root_id = tree 42 47 .insert( 43 - Node::new(TodoNode::new(TodoNodeKind::Root, 0)), 48 + Node::new(TodoNode::new(TodoNodeKind::Root, 0, 1.0)), 44 49 InsertBehavior::AsRoot, 45 50 ) 46 51 .with_context(|| "Could not create root node.")?; ··· 64 69 65 70 for group in root_groups { 66 71 todo_tree 67 - .add_group_to_tree(db, &root_id, Box::new(group), 0) 72 + .add_group_to_tree(db, &root_id, Box::new(group), 0, 1.0) 68 73 .await?; 69 74 } 70 75 ··· 78 83 parent_node_id: &NodeId, 79 84 group: Box<Group>, 80 85 depth: usize, 86 + parent_p_score: f64, 81 87 ) -> Result<()> { 82 88 let group_id = group.id.clone(); 89 + 90 + let p_score = group.p_score(parent_p_score); 83 91 84 92 let group_node_id = self.tree.insert( 85 - Node::new(TodoNode::new(TodoNodeKind::Group(group), depth)), 93 + Node::new(TodoNode::new(TodoNodeKind::Group(group), depth, p_score)), 86 94 InsertBehavior::UnderNode(parent_node_id), 87 95 )?; 88 96 ··· 110 118 .collect(); 111 119 112 120 for task in tasks { 121 + let p_score = task.p_score(p_score); 122 + 113 123 let task_id = task.id.clone(); 114 124 let task_node_id = self.tree.insert( 115 - Node::new(TodoNode::new(TodoNodeKind::Task(Box::new(task)), depth + 1)), 125 + Node::new(TodoNode::new( 126 + TodoNodeKind::Task(Box::new(task)), 127 + depth + 1, 128 + p_score, 129 + )), 116 130 InsertBehavior::UnderNode(&group_node_id), 117 131 )?; 118 132 ··· 131 145 .collect(); 132 146 133 147 for group in children_groups { 134 - self.add_group_to_tree(db, &group_node_id, Box::new(group), depth + 1) 148 + self.add_group_to_tree(db, &group_node_id, Box::new(group), depth + 1, p_score) 135 149 .await?; 136 150 } 137 151 ··· 157 171 + 1 158 172 }; 159 173 174 + let parent_p_score = self 175 + .tree 176 + .get(&parent_node_id) 177 + .expect("must exist") 178 + .data() 179 + .p_score; 180 + 181 + let my_pscore = parent_p_score * group.priority.p_score(); 182 + 160 183 let inserted_node_id = self 161 184 .tree 162 185 .insert( 163 186 Node::new(TodoNode::new( 164 187 super::TodoNodeKind::Group(Box::new(group.clone())), 165 188 my_depth, 189 + my_pscore, 166 190 )), 167 191 tree::InsertBehavior::UnderNode(&parent_node_id), 168 192 ) ··· 189 213 .depth 190 214 + 1; 191 215 216 + let parent_p_score = self 217 + .tree 218 + .get(&parent_node_id) 219 + .expect("must exist") 220 + .data() 221 + .p_score; 222 + 223 + let my_pscore = task.p_score(parent_p_score); 224 + 192 225 let inserted_node_id = self 193 226 .tree 194 227 .insert( 195 228 Node::new(TodoNode::new( 196 229 super::TodoNodeKind::Task(Box::new(task.clone())), 197 230 my_depth, 231 + my_pscore, 198 232 )), 199 233 tree::InsertBehavior::UnderNode(&parent_node_id), 200 234 )
+12
src/types/priority.rs
··· 10 10 field1: PriorityDTO, 11 11 } 12 12 13 + impl Priority { 14 + pub const fn p_score(&self) -> f64 { 15 + match self.field1 { 16 + PriorityDTO::Asap => 1.0, 17 + PriorityDTO::High => 0.9, 18 + PriorityDTO::Medium => 0.75, 19 + PriorityDTO::Low => 0.5, 20 + PriorityDTO::Far => 0.25, 21 + } 22 + } 23 + } 24 + 13 25 impl From<PriorityDTO> for Priority { 14 26 fn from(value: PriorityDTO) -> Self { 15 27 Self { field1: value }
+18
src/types/task.rs
··· 143 143 Ok(()) 144 144 } 145 145 146 + /// Calcualtes the `p_score` of this `Task` 147 + //NOTE: formula from claude 148 + #[expect(clippy::cast_precision_loss)] 149 + pub fn p_score(&self, parent_score: f64) -> f64 { 150 + let priority_score = self.priority.p_score(); // [0.0, 1.0] 151 + let urgency = self.due.0.map_or(1.0, |due| { 152 + let now = chrono::Local::now().naive_local(); 153 + let hours_remaining = (due - now).num_minutes() as f64 / 60.0; 154 + 155 + // Exponential urgency: peaks at/past due, approaches 0 far in future 156 + // Half-life of ~72 hours — tune this constant to taste 157 + let decay = 72.0_f64; 158 + (-hours_remaining / decay).exp2() 159 + }); 160 + 161 + priority_score * urgency * parent_score 162 + } 163 + 146 164 pub fn finished_at(&self) -> Option<String> { 147 165 self.finished_at 148 166 .map(|finished_at| finished_at.format(frontmatter::DATE_FMT_STR).to_string())