From 1303f7fb1a470cfbedcdb341e278f4aa33aa6038 Mon Sep 17 00:00:00 2001 From: suri-codes Date: Sat, 11 Apr 2026 21:56:00 -0700 Subject: [PATCH] feat: Basic todo explorer --- crates/dto/migration/src/types/color.rs | 2 +- crates/dto/migration/src/types/priority.rs | 2 +- src/tui/components/todo/explorer.rs | 82 ++++++++++++++++++---- src/tui/components/todo/mod.rs | 54 ++++++++------ src/tui/components/viewport/mod.rs | 2 +- src/types/color.rs | 2 +- src/types/group.rs | 3 +- src/types/kasten/mod.rs | 3 +- src/types/kasten/todo_tree.rs | 38 ++++++---- src/types/mod.rs | 4 +- src/types/priority.rs | 2 +- src/types/tag.rs | 2 +- src/types/task.rs | 3 +- src/types/zettel/mod.rs | 3 +- 14 files changed, 140 insertions(+), 62 deletions(-) diff --git a/crates/dto/migration/src/types/color.rs b/crates/dto/migration/src/types/color.rs index 42eeb76..ced59f4 100644 --- a/crates/dto/migration/src/types/color.rs +++ b/crates/dto/migration/src/types/color.rs @@ -6,7 +6,7 @@ use std::fmt::{Debug, Display}; /// Color type /// /// We store it as a u32, but its actually 00000000rrrrrrrrggggggggbbbbbbbb -#[derive(Clone, Copy, PartialEq, Eq, DeriveValueType)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Copy, DeriveValueType)] pub struct Color(u32); impl Color { diff --git a/crates/dto/migration/src/types/priority.rs b/crates/dto/migration/src/types/priority.rs index 7a05fce..8421500 100644 --- a/crates/dto/migration/src/types/priority.rs +++ b/crates/dto/migration/src/types/priority.rs @@ -1,6 +1,6 @@ use sea_orm::DeriveValueType; -#[derive(Clone, Debug, PartialEq, Eq, DeriveValueType, Default)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveValueType, Default, PartialOrd, Ord)] #[sea_orm(value_type = "String")] pub enum Priority { Asap, diff --git a/src/tui/components/todo/explorer.rs b/src/tui/components/todo/explorer.rs index 6971712..8fcca82 100644 --- a/src/tui/components/todo/explorer.rs +++ b/src/tui/components/todo/explorer.rs @@ -1,28 +1,82 @@ -#![allow(dead_code)] +use ratatui::{ + style::{Color, Modifier}, + text::{Line, Span, Text}, + widgets::{List, ListState}, +}; +use tree::NodeId; -use dto::NanoId; -use ratatui::{text::Span, widgets::ListState}; +use crate::types::{TodoNode, TodoNodeKind, TodoTree}; pub struct Explorer<'text> { pub render_list: ratatui::widgets::List<'text>, - pub id_list: Vec, + #[allow(dead_code)] + pub id_list: Vec, pub state: ListState, + #[allow(dead_code)] pub width: u16, } +impl Explorer<'_> { + pub fn new(tree: &TodoTree, scope: &NodeId, state: ListState, width: u16) -> Self { + let render_list = List::new( + tree.tree + .traverse_pre_order(scope) + .expect("This should not panic as the node id should exist inside") + .filter_map(|node| { + // we dont want to show the root + if node.data().kind == TodoNodeKind::Root { + return None; + } + let mut eli: ExplorerListItem<'_> = node.data().into(); + eli.width = width; + Some(Text::from(eli)) + }), + ) + .style(Color::White) + .highlight_style(Modifier::REVERSED) + .highlight_symbol("> "); + + let id_list = tree + .tree + .traverse_pre_order_ids(scope) + .expect("This should nto panic as the node id should exist inside") + .collect::>(); + + Self { + render_list, + id_list, + state, + width, + } + } +} + pub struct ExplorerListItem<'text> { + spacer: Span<'text>, name: Span<'text>, + width: u16, } -// impl From<&Task> for ExplorerListItem { -// fn from(value: &Task) -> Self { -// Self { -// name: Span { style: (), content: () } -// } -// } -// } +impl From<&TodoNode> for ExplorerListItem<'_> { + fn from(value: &TodoNode) -> Self { + let spacer = Span::from(" ".repeat(value.depth)); + let name = match value.kind { + TodoNodeKind::Group(ref g) => Span::from(g.name.clone()), + TodoNodeKind::Task(ref t) => Span::from(t.name.clone()), + TodoNodeKind::Root => Span::from("THIS SHOULD NOT BE VISIBLE"), + }; -// impl Explorer { -// pub async fn + Self { + spacer, + name, + width: 0, + } + } +} -// } +impl<'text> From> for Text<'text> { + fn from(value: ExplorerListItem<'text>) -> Self { + let line = Line::from(vec![value.spacer, value.name]); + line.into() + } +} diff --git a/src/tui/components/todo/mod.rs b/src/tui/components/todo/mod.rs index dc9da63..9096d8b 100644 --- a/src/tui/components/todo/mod.rs +++ b/src/tui/components/todo/mod.rs @@ -1,14 +1,9 @@ use async_trait::async_trait; use color_eyre::eyre::Result; -use dto::{ - ColumnTrait as _, GroupColumns, GroupEntity, QueryFilter as _, TagEntity, TaskEntity, - ZettelEntity, -}; use ratatui::{ Frame, layout::{Constraint, Layout, Rect, Size}, - style::{Color, Stylize}, - widgets::Block, + widgets::ListState, }; use tokio::sync::mpsc::UnboundedSender; @@ -18,25 +13,24 @@ use crate::{ }; mod explorer; +use explorer::Explorer; -#[expect(dead_code)] -pub struct Todo { +pub struct Todo<'text> { + #[expect(dead_code)] signal_tx: Option>, kh: KastenHandle, + #[expect(dead_code)] layouts: Layouts, + explorer: Explorer<'text>, } -impl Todo { +impl Todo<'_> { pub async fn new(kh: KastenHandle) -> Result { let kt = kh.read().await; - let _roots = GroupEntity::load() - .with(TagEntity) - .with(TaskEntity) - .with((ZettelEntity, TagEntity)) - .filter(GroupColumns::ParentGroupId.is_null()) - .all(&kt.db) - .await?; + let mut l_state = ListState::default(); + l_state.select_first(); + let explorer = Explorer::new(&kt.todo_tree, &kt.todo_tree.root_id, l_state, 0); drop(kt); @@ -44,6 +38,7 @@ impl Todo { kh, layouts: Layouts::default(), signal_tx: None, + explorer, }) } } @@ -62,20 +57,37 @@ impl Default for Layouts { } #[async_trait] -impl Component for Todo { +impl Component for Todo<'_> { async fn init(&mut self, area: Size) -> color_eyre::Result<()> { - let _ = area; // to appease clippy + let total_width = area.width; + + let mut l_state = ListState::default(); + l_state.select_first(); + let tree = &self.kh.read().await.todo_tree; + + let explorer = Explorer::new(tree, &tree.root_id, l_state, total_width); + self.explorer = explorer; Ok(()) } - async fn update(&mut self, _signal: Signal) -> color_eyre::Result> { + async fn update(&mut self, signal: Signal) -> color_eyre::Result> { + match signal { + Signal::MoveDown => { + self.explorer.state.select_next(); + // self.update_views_from_zettel_list_selection().await?; + } + + Signal::MoveUp => { + self.explorer.state.select_previous(); + } + _ => {} + } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> color_eyre::Result<()> { - frame.render_widget(Block::new().bg(Color::Red), area); - + frame.render_stateful_widget(&self.explorer.render_list, area, &mut self.explorer.state); Ok(()) } } diff --git a/src/tui/components/viewport/mod.rs b/src/tui/components/viewport/mod.rs index 575f684..4f3921f 100644 --- a/src/tui/components/viewport/mod.rs +++ b/src/tui/components/viewport/mod.rs @@ -25,7 +25,7 @@ pub struct Viewport<'text> { switcher: Switcher<'text>, active_region: Region, zk: Zk<'text>, - todo: Todo, + todo: Todo<'text>, } mod switcher; diff --git a/src/types/color.rs b/src/types/color.rs index 9acfdff..bba0d1c 100644 --- a/src/types/color.rs +++ b/src/types/color.rs @@ -5,7 +5,7 @@ use ratatui::style::Color as RatColor; /// Agnostic Color type, /// internally represented as rgb -#[derive(Debug, Copy, Clone, Default)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Default)] pub struct Color(ColorDTO); impl Display for Color { diff --git a/src/types/group.rs b/src/types/group.rs index 3b2043d..824736c 100644 --- a/src/types/group.rs +++ b/src/types/group.rs @@ -3,8 +3,7 @@ use dto::{DateTime, GroupModelEx, NanoId}; use crate::types::{Priority, Tag, Zettel}; /// A `Group` which contains tasks! -#[expect(dead_code)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Group { /// Should only be constructed from models. _private: (), diff --git a/src/types/kasten/mod.rs b/src/types/kasten/mod.rs index 391b79e..98ba941 100644 --- a/src/types/kasten/mod.rs +++ b/src/types/kasten/mod.rs @@ -17,7 +17,8 @@ mod index; pub use index::Index; pub use index::ZettelOnDisk; mod todo_tree; -pub use todo_tree::{TodoNode, TodoTree}; + +pub use todo_tree::{TodoNode, TodoNodeKind, TodoTree}; #[derive(Debug)] pub struct Kasten { diff --git a/src/types/kasten/todo_tree.rs b/src/types/kasten/todo_tree.rs index c144dfc..858b02d 100644 --- a/src/types/kasten/todo_tree.rs +++ b/src/types/kasten/todo_tree.rs @@ -9,27 +9,40 @@ use tree::{InsertBehavior, Node, NodeId, Tree}; use crate::types::{Group, Task}; -#[expect(dead_code)] -#[derive(Debug, Clone)] -pub enum TodoNode { +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum TodoNodeKind { Root, Group(Box), Task(Box), } +#[derive(Debug, Clone)] +pub struct TodoNode { + pub depth: usize, + pub kind: TodoNodeKind, +} + +impl TodoNode { + pub const fn new(kind: TodoNodeKind, depth: usize) -> Self { + Self { depth, kind } + } +} + #[derive(Debug)] pub struct TodoTree { - tree: Tree, - nanoid_to_nodeid: HashMap, - #[expect(dead_code)] - root_id: NodeId, + pub tree: Tree, + pub nanoid_to_nodeid: HashMap, + pub root_id: NodeId, } impl TodoTree { pub async fn construct(db: &DatabaseConnection) -> Result { let mut tree = Tree::::new(); let root_id = tree - .insert(Node::new(TodoNode::Root), InsertBehavior::AsRoot) + .insert( + Node::new(TodoNode::new(TodoNodeKind::Root, 0)), + InsertBehavior::AsRoot, + ) .with_context(|| "Could not create root node.")?; let root_groups: Vec = GroupEntity::load() @@ -51,7 +64,7 @@ impl TodoTree { for group in root_groups { todo_tree - .add_group_to_tree(db, &root_id, Box::new(group)) + .add_group_to_tree(db, &root_id, Box::new(group), 0) .await?; } @@ -64,11 +77,12 @@ impl TodoTree { db: &DatabaseConnection, parent_node_id: &NodeId, group: Box, + depth: usize, ) -> Result<()> { let group_id = group.id.clone(); let group_node_id = self.tree.insert( - Node::new(TodoNode::Group(group)), + Node::new(TodoNode::new(TodoNodeKind::Group(group), depth)), InsertBehavior::UnderNode(parent_node_id), )?; @@ -98,7 +112,7 @@ impl TodoTree { for task in tasks { let task_id = task.id.clone(); let task_node_id = self.tree.insert( - Node::new(TodoNode::Task(Box::new(task))), + Node::new(TodoNode::new(TodoNodeKind::Task(Box::new(task)), depth + 1)), InsertBehavior::UnderNode(&group_node_id), )?; @@ -117,7 +131,7 @@ impl TodoTree { .collect(); for group in children_groups { - self.add_group_to_tree(db, &group_node_id, Box::new(group)) + self.add_group_to_tree(db, &group_node_id, Box::new(group), depth + 1) .await?; } diff --git a/src/types/mod.rs b/src/types/mod.rs index ee73e81..f4ad5a3 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -24,13 +24,11 @@ mod filaments; pub use filaments::Filaments; mod kasten; - pub use kasten::Index; pub use kasten::Kasten; pub use kasten::KastenHandle; -#[expect(unused_imports)] pub use kasten::TodoNode; -#[expect(unused_imports)] +pub use kasten::TodoNodeKind; pub use kasten::TodoTree; mod frontmatter; diff --git a/src/types/priority.rs b/src/types/priority.rs index be9b791..93d6f7e 100644 --- a/src/types/priority.rs +++ b/src/types/priority.rs @@ -2,7 +2,7 @@ use dto::PriorityDTO; /// An Enum for the various `Priority` levels /// for `Task`s and `Group`s -#[derive(Debug, Clone, Default)] +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Priority { field1: PriorityDTO, } diff --git a/src/types/tag.rs b/src/types/tag.rs index 6ea22db..cf849ce 100644 --- a/src/types/tag.rs +++ b/src/types/tag.rs @@ -4,7 +4,7 @@ use crate::types::Color; /// Represents a `Tag` in a `ZettelKasten` note taking method. /// Easy way to link multiple notes under one simple word. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Tag { /// Should only be constructed from models. _private: (), diff --git a/src/types/task.rs b/src/types/task.rs index 2c2743e..e358442 100644 --- a/src/types/task.rs +++ b/src/types/task.rs @@ -3,8 +3,7 @@ use dto::{ DateTime, NanoId, TaskModelEx}; use crate::types::{Group, Priority, Zettel}; /// a `Task` that you have to complete! -#[expect(dead_code)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Task { /// Should only be constructed from models. _private:(), diff --git a/src/types/zettel/mod.rs b/src/types/zettel/mod.rs index f5a1a2b..1ac68fe 100644 --- a/src/types/zettel/mod.rs +++ b/src/types/zettel/mod.rs @@ -19,7 +19,8 @@ pub use id::ZettelId; /// A `Zettel` is a note about a single idea. /// It can have many `Tag`s, just meaning it can fall under many /// categories. -#[derive(Debug, Clone)] + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Zettel { /// Should only be constructed from models. _private: (), -- 2.51.2