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: toggle finished!

+99 -42
+30 -29
.config/config.ron
··· 2 2 directory: "/Users/suri/dev/projects/filaments/ZettelKasten", 3 3 global_key_binds: { 4 4 "down": MoveDown, 5 - "ctrl-c": Quit, 6 - "up": MoveUp, 7 5 "ctrl-z": Suspend, 6 + "up": MoveUp, 7 + "ctrl-c": Quit, 8 8 }, 9 9 zk: ( 10 10 keybinds: { 11 + "ctrl-n": NewZettel, 11 12 "enter": OpenZettel, 12 13 "tab": SwitchTo( 13 14 page: Todo(Explorer), 14 15 ), 15 - "ctrl-n": NewZettel, 16 16 }, 17 17 ), 18 18 todo: ( 19 19 explorer: ( 20 20 keybinds: { 21 - "j": MoveDown, 21 + "k": MoveUp, 22 22 "1": SwitchTo( 23 23 page: Todo(Explorer), 24 24 ), 25 - "t": NewTask, 26 - "2": SwitchTo( 27 - page: Todo(Inspector), 28 - ), 29 - "shift-g": NewGroup, 30 - "g": NewSubGroup, 31 - "enter": SwitchTo( 32 - page: Todo(Inspector), 33 - ), 34 25 "tab": SwitchTo( 35 26 page: Zk, 36 27 ), 37 28 "3": SwitchTo( 38 29 page: Todo(TaskList), 39 30 ), 40 - "k": MoveUp, 31 + "g": NewSubGroup, 32 + "enter": SwitchTo( 33 + page: Todo(Inspector), 34 + ), 35 + "shift-g": NewGroup, 36 + "t": NewTask, 37 + "j": MoveDown, 38 + "2": SwitchTo( 39 + page: Todo(Inspector), 40 + ), 41 41 }, 42 42 ), 43 43 inspector: ( 44 44 keybinds: { 45 + "p": EditPriority, 45 46 "tab": SwitchTo( 46 47 page: Zk, 47 48 ), 48 - "p": EditPriority, 49 49 "n": EditName, 50 50 "d": EditDue, 51 - "o": OpenZettel, 51 + "1": SwitchTo( 52 + page: Todo(Explorer), 53 + ), 52 54 "3": SwitchTo( 53 55 page: Todo(TaskList), 54 56 ), 55 57 "2": SwitchTo( 56 58 page: Todo(Inspector), 57 59 ), 58 - "1": SwitchTo( 59 - page: Todo(Explorer), 60 - ), 60 + "o": OpenZettel, 61 + "f": ToggleFinish, 61 62 }, 62 63 ), 63 64 tasklist: ( 64 65 keybinds: { 65 - "tab": SwitchTo( 66 - page: Zk, 67 - ), 68 - "3": SwitchTo( 69 - page: Todo(TaskList), 66 + "enter": SwitchTo( 67 + page: Todo(Inspector), 70 68 ), 71 69 "2": SwitchTo( 72 - page: Todo(Inspector), 73 - ), 74 - "enter": SwitchTo( 75 70 page: Todo(Inspector), 76 71 ), 77 72 "1": SwitchTo( 78 73 page: Todo(Explorer), 79 74 ), 80 - "k": MoveUp, 75 + "3": SwitchTo( 76 + page: Todo(TaskList), 77 + ), 81 78 "j": MoveDown, 79 + "tab": SwitchTo( 80 + page: Zk, 81 + ), 82 + "k": MoveUp, 82 83 }, 83 84 ), 84 85 ), 85 - ) 86 + )
+2 -1
.config/default_config.ron
··· 37 37 "n": EditName, 38 38 "p": EditPriority, 39 39 "d": EditDue, 40 - "o": OpenZettel 40 + "o": OpenZettel, 41 + "f": ToggleFinish 41 42 }, 42 43 ), 43 44 tasklist: (
+1 -1
justfile
··· 20 20 test: 21 21 cargo nextest r {{_cargo_flags}} 22 22 reset: 23 - rm -rf ZettleKasten 23 + rm -rf ./ZettelKasten 24 24 rm -rf ./.data 25 25 cargo run -- init 26 26 cargo run
+7 -1
src/tui/components/todo/explorer.rs
··· 125 125 .bg(g.tag.color) 126 126 .fg(Color::Black), 127 127 TodoNodeKind::Task(ref t) => { 128 - Span::from(format!(" {}", t.name.clone())).fg(t.group.tag.color) 128 + let mut name = Span::from(format!(" {}", t.name.clone())).fg(t.group.tag.color); 129 + 130 + if t.finished_at.is_some() { 131 + name = name.add_modifier(Modifier::CROSSED_OUT); 132 + } 133 + 134 + name 129 135 } 130 136 TodoNodeKind::Root => Span::from("THIS SHOULD NOT BE VISIBLE"), 131 137 };
+20
src/tui/components/todo/inspector/mod.rs
··· 169 169 Ok(()) 170 170 } 171 171 172 + #[expect(clippy::too_many_lines)] 172 173 async fn update(&mut self, signal: Signal) -> color_eyre::Result<Option<Signal>> { 173 174 match signal { 174 175 Signal::SwitchTo { ··· 289 290 let path = kt.index.get_zod(zid).path.clone(); 290 291 drop(kt); 291 292 return Ok(Some(Signal::Helix { path })); 293 + } 294 + 295 + Signal::ToggleFinish if self.is_active => { 296 + let Some(ref curr) = self.inspecting else { 297 + return Ok(None); 298 + }; 299 + 300 + let kt = self.kh.write().await; 301 + 302 + let node = kt.todo_tree.get_node_by_nano_id(curr).data(); 303 + 304 + let TodoNodeKind::Task(t) = &node.kind else { 305 + return Ok(None); 306 + }; 307 + 308 + Task::toggle_finish(t.id.clone(), &kt).await?; 309 + 310 + drop(kt); 311 + return Ok(Some(Signal::Refresh)); 292 312 } 293 313 294 314 _ => {}
+6 -4
src/tui/components/todo/tasklist.rs
··· 29 29 .expect("This should not panic as the nodeid should exist inside"), 30 30 ) 31 31 .filter(|(node, _)| { 32 - let TodoNodeKind::Task(_) = node.data().kind else { 33 - return false; 34 - }; 35 - true 32 + if let TodoNodeKind::Task(ref t) = node.data().kind 33 + && t.finished_at().is_none() 34 + { 35 + return true; 36 + } 37 + false 36 38 }) 37 39 .collect::<Vec<_>>(); 38 40
+3
src/tui/signal.rs
··· 79 79 /// Only works with the inspector 80 80 EditDue, 81 81 82 + /// Toggle whether a `Task` is finished or not. 83 + ToggleFinish, 84 + 82 85 /// Internal Signal that tells the app to resume interpreting keys 83 86 ExitRawText, 84 87
+30 -6
src/types/task.rs
··· 1 + use chrono::Local; 1 2 use color_eyre::eyre::{Context, Result, eyre}; 2 3 use dto::{ 3 4 DateTime, GroupEntity, HasOne, IntoActiveModel as _, NanoId, TagEntity, TaskActiveModel, ··· 143 144 Ok(()) 144 145 } 145 146 147 + pub async fn toggle_finish(id: NanoId, kt: &Kasten) -> Result<()> { 148 + let now = Local::now().naive_local(); 149 + 150 + let model = TaskEntity::load() 151 + .filter_by_nano_id(id) 152 + .one(&kt.db) 153 + .await? 154 + .expect("Must exist"); 155 + 156 + let new_finished_at = if model.finished_at.is_some() { 157 + None 158 + } else { 159 + Some(now) 160 + }; 161 + 162 + model 163 + .into_active_model() 164 + .set_finished_at(new_finished_at) 165 + .update(&kt.db) 166 + .await?; 167 + 168 + Ok(()) 169 + } 170 + 146 171 /// Calcualtes the `p_score` of this `Task` 147 172 //NOTE: formula from claude 148 173 #[expect(clippy::cast_precision_loss)] 149 174 pub fn p_score(&self, parent_score: f64) -> f64 { 150 175 let priority_score = self.priority.p_score(); // [0.0, 1.0] 151 - let urgency = self.due.0.map_or(1.0, |due| { 176 + 177 + let urgency = self.due.0.map_or(0.0, |due| { 152 178 let now = chrono::Local::now().naive_local(); 153 179 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 180 let decay = 72.0_f64; 158 181 (-hours_remaining / decay).exp2() 159 182 }); 160 183 161 - priority_score * urgency * parent_score 184 + // base: priority alone. bonus: urgency on top, so any due date > no due date. 185 + // urgency is in (0.0, ~inf] so having a due date always adds to the score. 186 + (priority_score + urgency) * parent_score 162 187 } 163 - 164 188 pub fn finished_at(&self) -> Option<String> { 165 189 self.finished_at 166 190 .map(|finished_at| finished_at.format(frontmatter::DATE_FMT_STR).to_string())