From b8965efb4b4b57bae3bb11d0207d1cfbd53e40f3 Mon Sep 17 00:00:00 2001 From: suri-codes Date: Sun, 19 Apr 2026 17:05:11 -0700 Subject: [PATCH] feat: toggle finished! --- .config/config.ron | 59 ++++++++++++------------ .config/default_config.ron | 3 +- justfile | 2 +- src/tui/components/todo/explorer.rs | 8 +++- src/tui/components/todo/inspector/mod.rs | 20 ++++++++ src/tui/components/todo/tasklist.rs | 10 ++-- src/tui/signal.rs | 3 ++ src/types/task.rs | 36 ++++++++++++--- 8 files changed, 99 insertions(+), 42 deletions(-) diff --git a/.config/config.ron b/.config/config.ron index 196ec40..554d03f 100644 --- a/.config/config.ron +++ b/.config/config.ron @@ -2,84 +2,85 @@ directory: "/Users/suri/dev/projects/filaments/ZettelKasten", global_key_binds: { "down": MoveDown, - "ctrl-c": Quit, - "up": MoveUp, "ctrl-z": Suspend, + "up": MoveUp, + "ctrl-c": Quit, }, zk: ( keybinds: { + "ctrl-n": NewZettel, "enter": OpenZettel, "tab": SwitchTo( page: Todo(Explorer), ), - "ctrl-n": NewZettel, }, ), todo: ( explorer: ( keybinds: { - "j": MoveDown, + "k": MoveUp, "1": SwitchTo( page: Todo(Explorer), ), - "t": NewTask, - "2": SwitchTo( - page: Todo(Inspector), - ), - "shift-g": NewGroup, - "g": NewSubGroup, - "enter": SwitchTo( - page: Todo(Inspector), - ), "tab": SwitchTo( page: Zk, ), "3": SwitchTo( page: Todo(TaskList), ), - "k": MoveUp, + "g": NewSubGroup, + "enter": SwitchTo( + page: Todo(Inspector), + ), + "shift-g": NewGroup, + "t": NewTask, + "j": MoveDown, + "2": SwitchTo( + page: Todo(Inspector), + ), }, ), inspector: ( keybinds: { + "p": EditPriority, "tab": SwitchTo( page: Zk, ), - "p": EditPriority, "n": EditName, "d": EditDue, - "o": OpenZettel, + "1": SwitchTo( + page: Todo(Explorer), + ), "3": SwitchTo( page: Todo(TaskList), ), "2": SwitchTo( page: Todo(Inspector), ), - "1": SwitchTo( - page: Todo(Explorer), - ), + "o": OpenZettel, + "f": ToggleFinish, }, ), tasklist: ( keybinds: { - "tab": SwitchTo( - page: Zk, - ), - "3": SwitchTo( - page: Todo(TaskList), - ), - "2": SwitchTo( + "enter": SwitchTo( page: Todo(Inspector), ), - "enter": SwitchTo( + "2": SwitchTo( page: Todo(Inspector), ), "1": SwitchTo( page: Todo(Explorer), ), - "k": MoveUp, + "3": SwitchTo( + page: Todo(TaskList), + ), "j": MoveDown, + "tab": SwitchTo( + page: Zk, + ), + "k": MoveUp, }, ), ), -) +) \ No newline at end of file diff --git a/.config/default_config.ron b/.config/default_config.ron index da5877d..706335b 100644 --- a/.config/default_config.ron +++ b/.config/default_config.ron @@ -37,7 +37,8 @@ "n": EditName, "p": EditPriority, "d": EditDue, - "o": OpenZettel + "o": OpenZettel, + "f": ToggleFinish }, ), tasklist: ( diff --git a/justfile b/justfile index dc3550f..4c4d8e8 100644 --- a/justfile +++ b/justfile @@ -20,7 +20,7 @@ run: test: cargo nextest r {{_cargo_flags}} reset: - rm -rf ZettleKasten + rm -rf ./ZettelKasten rm -rf ./.data cargo run -- init cargo run diff --git a/src/tui/components/todo/explorer.rs b/src/tui/components/todo/explorer.rs index a2f05c7..1f92879 100644 --- a/src/tui/components/todo/explorer.rs +++ b/src/tui/components/todo/explorer.rs @@ -125,7 +125,13 @@ impl From<&TodoNode> for ExplorerListItem<'_> { .bg(g.tag.color) .fg(Color::Black), TodoNodeKind::Task(ref t) => { - Span::from(format!(" {}", t.name.clone())).fg(t.group.tag.color) + let mut name = Span::from(format!(" {}", t.name.clone())).fg(t.group.tag.color); + + if t.finished_at.is_some() { + name = name.add_modifier(Modifier::CROSSED_OUT); + } + + name } TodoNodeKind::Root => Span::from("THIS SHOULD NOT BE VISIBLE"), }; diff --git a/src/tui/components/todo/inspector/mod.rs b/src/tui/components/todo/inspector/mod.rs index 5d8fdc5..0600c5d 100644 --- a/src/tui/components/todo/inspector/mod.rs +++ b/src/tui/components/todo/inspector/mod.rs @@ -169,6 +169,7 @@ impl Component for Inspector<'_> { Ok(()) } + #[expect(clippy::too_many_lines)] async fn update(&mut self, signal: Signal) -> color_eyre::Result> { match signal { Signal::SwitchTo { @@ -291,6 +292,25 @@ impl Component for Inspector<'_> { return Ok(Some(Signal::Helix { path })); } + Signal::ToggleFinish if self.is_active => { + let Some(ref curr) = self.inspecting else { + return Ok(None); + }; + + let kt = self.kh.write().await; + + let node = kt.todo_tree.get_node_by_nano_id(curr).data(); + + let TodoNodeKind::Task(t) = &node.kind else { + return Ok(None); + }; + + Task::toggle_finish(t.id.clone(), &kt).await?; + + drop(kt); + return Ok(Some(Signal::Refresh)); + } + _ => {} } Ok(None) diff --git a/src/tui/components/todo/tasklist.rs b/src/tui/components/todo/tasklist.rs index 7b03134..7e7a6fb 100644 --- a/src/tui/components/todo/tasklist.rs +++ b/src/tui/components/todo/tasklist.rs @@ -29,10 +29,12 @@ impl TaskList<'_> { .expect("This should not panic as the nodeid should exist inside"), ) .filter(|(node, _)| { - let TodoNodeKind::Task(_) = node.data().kind else { - return false; - }; - true + if let TodoNodeKind::Task(ref t) = node.data().kind + && t.finished_at().is_none() + { + return true; + } + false }) .collect::>(); diff --git a/src/tui/signal.rs b/src/tui/signal.rs index ea6b2b0..e46dfff 100644 --- a/src/tui/signal.rs +++ b/src/tui/signal.rs @@ -79,6 +79,9 @@ pub enum Signal { /// Only works with the inspector EditDue, + /// Toggle whether a `Task` is finished or not. + ToggleFinish, + /// Internal Signal that tells the app to resume interpreting keys ExitRawText, diff --git a/src/types/task.rs b/src/types/task.rs index 4b38d4f..af484a7 100644 --- a/src/types/task.rs +++ b/src/types/task.rs @@ -1,3 +1,4 @@ +use chrono::Local; use color_eyre::eyre::{Context, Result, eyre}; use dto::{ DateTime, GroupEntity, HasOne, IntoActiveModel as _, NanoId, TagEntity, TaskActiveModel, @@ -143,24 +144,47 @@ impl Task { Ok(()) } + pub async fn toggle_finish(id: NanoId, kt: &Kasten) -> Result<()> { + let now = Local::now().naive_local(); + + let model = TaskEntity::load() + .filter_by_nano_id(id) + .one(&kt.db) + .await? + .expect("Must exist"); + + let new_finished_at = if model.finished_at.is_some() { + None + } else { + Some(now) + }; + + model + .into_active_model() + .set_finished_at(new_finished_at) + .update(&kt.db) + .await?; + + 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 urgency = self.due.0.map_or(0.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 + // base: priority alone. bonus: urgency on top, so any due date > no due date. + // urgency is in (0.0, ~inf] so having a due date always adds to the score. + (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