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: tarstree indexing + cli group task creation

+316 -36
+4 -4
.config/config.ron
··· 2 2 directory: "/Users/suri/dev/projects/filaments/ZettelKasten", 3 3 global_key_binds: { 4 4 "up": MoveUp, 5 + "ctrl-z": Suspend, 5 6 "ctrl-c": Quit, 6 7 "down": MoveDown, 7 - "ctrl-z": Suspend, 8 8 }, 9 9 zk: ( 10 10 keybinds: { 11 - "<Ctrl-n>": NewZettel, 12 - "enter": OpenZettel, 13 11 "tab": SwitchTo( 14 12 region: Todo, 15 13 ), 14 + "<Ctrl-n>": NewZettel, 15 + "enter": OpenZettel, 16 16 }, 17 17 ), 18 18 todo: ( 19 19 keybinds: { 20 + "j": MoveDown, 20 21 "tab": SwitchTo( 21 22 region: Zk, 22 23 ), 23 24 "k": MoveUp, 24 - "j": MoveDown, 25 25 }, 26 26 ), 27 27 )
+2
Cargo.lock
··· 2315 2315 version = "0.1.0" 2316 2316 dependencies = [ 2317 2317 "anyhow", 2318 + "async-recursion", 2318 2319 "async-trait", 2319 2320 "better-panic", 2320 2321 "clap", ··· 2344 2345 "tracing", 2345 2346 "tracing-error", 2346 2347 "tracing-subscriber", 2348 + "tree", 2347 2349 "vergen-gix", 2348 2350 ] 2349 2351
+3
Cargo.toml
··· 33 33 serde = "1.0.228" 34 34 tracing = "0.1.44" 35 35 tokio = { version = "1.51.1", features = ["full"] } 36 + tree = {path = "crates/tree"} 36 37 37 38 [package] 38 39 name = "filaments" ··· 83 84 ron = "0.12.1" 84 85 tower-lsp = "0.20.0" 85 86 notify = "8.2.0" 87 + tree = {workspace = true} 88 + async-recursion = "1.1.1" 86 89 87 90 [build-dependencies] 88 91 anyhow = "1.0.102"
+1 -1
crates/dto/src/entity/group.rs
··· 2 2 3 3 use migration::prelude::Local; 4 4 use migration::types::*; 5 - use sea_orm::entity::prelude::*; 6 5 use sea_orm::ActiveValue::Set; 6 + use sea_orm::entity::prelude::*; 7 7 use std::future::ready; 8 8 use std::pin::Pin; 9 9
+2
crates/dto/src/lib.rs
··· 13 13 pub use sea_orm::IntoActiveModel; 14 14 pub use sea_orm::QueryFilter; 15 15 pub use sea_orm::QueryOrder; 16 + pub use sea_orm::entity::compound::HasMany; 17 + pub use sea_orm::entity::compound::HasOne; 16 18 17 19 /// Exporting this as a generic NanoId. 18 20 pub use migration::types::NanoId;
+11
src/cli/mod.rs
··· 59 59 // default values if they arent present / aren't able to be 60 60 // parsed properly 61 61 // Import(ImportArgs), 62 + Test, 62 63 } 63 64 64 65 #[derive(Subcommand, Debug)] ··· 89 90 /// If this group has a parent, provide the parent id. 90 91 #[arg(short, long)] 91 92 parent_id: Option<NanoId>, 93 + }, 94 + 95 + Task { 96 + /// Name of the task 97 + #[arg(short, long)] 98 + name: String, 99 + 100 + /// Parent group of the task 101 + #[arg(short, long)] 102 + parent_id: NanoId, 92 103 }, 93 104 } 94 105
+67 -7
src/cli/process.rs
··· 4 4 io::Write, 5 5 }; 6 6 7 - use color_eyre::eyre::{Context, Result}; 7 + use color_eyre::eyre::{Context, Result, eyre}; 8 8 use dto::{ 9 - GroupActiveModel, GroupEntity, IntoActiveModel, TagActiveModel, TagEntity, ZettelEntity, 9 + GroupActiveModel, GroupEntity, HasOne, IntoActiveModel, TagActiveModel, TagEntity, 10 + TaskActiveModel, TaskEntity, ZettelEntity, 10 11 }; 11 12 use tower_lsp::{LspService, Server}; 12 13 ··· 14 15 cli::{Commands, ZettelSubcommand}, 15 16 config::{Config, get_config_dir}, 16 17 lsp::Backend, 17 - types::{Group, Kasten, Priority, Tag, Zettel}, 18 + types::{Group, Kasten, Priority, Tag, Task, Zettel}, 18 19 }; 19 20 20 21 impl Commands { 22 + #[expect(clippy::too_many_lines)] 21 23 pub async fn process(self) -> Result<()> { 22 24 match self { 23 25 Self::Init { name } => { ··· 80 82 match command { 81 83 super::TodoSubcommand::Group { name, parent_id } => { 82 84 // lets create a tag for this first group first 83 - 84 85 let tag: Tag = TagActiveModel::builder() 85 86 .set_name(name.clone()) 86 87 .insert(&kt.db) 87 88 .await? 88 89 .into(); 89 90 90 - //TODO: this zettel would need to be created with the parent of all 91 - // of its groups? 92 91 let tag_id = tag.id.clone(); 93 92 93 + // then create the zettel for the group 94 94 let zettel = Zettel::new(name.clone(), &mut kt, vec![tag]).await?; 95 95 96 + // then insert that shi 96 97 let inserted = GroupActiveModel::builder() 97 98 .set_name(name) 98 99 .set_parent_group_id(parent_id) ··· 106 107 ) 107 108 .set_zettel( 108 109 ZettelEntity::load() 109 - // .with(TagEntity) 110 110 .filter_by_nano_id(zettel.id) 111 111 .one(&kt.db) 112 112 .await? ··· 129 129 130 130 println!("created group {group:#?}"); 131 131 } 132 + super::TodoSubcommand::Task { name, parent_id } => { 133 + // need to create the task 134 + let parent = GroupEntity::load() 135 + .with(TagEntity) 136 + .filter_by_nano_id(parent_id) 137 + .one(&kt.db) 138 + .await 139 + .with_context(|| "failed to communicate with db")? 140 + .ok_or_else(|| eyre!("could not find the group"))?; 141 + 142 + let HasOne::Loaded(tag) = parent.tag else { 143 + panic!("this has to be loaded since we just loaded it right above") 144 + }; 145 + 146 + let zettel = 147 + Zettel::new(name.clone(), &mut kt, vec![(*tag).into()]).await?; 148 + 149 + let inserted = TaskActiveModel::builder() 150 + .set_name(name) 151 + .set_group_id(parent.nano_id.clone()) 152 + .set_priority(Priority::default()) 153 + .set_zettel( 154 + ZettelEntity::load() 155 + .filter_by_nano_id(zettel.id) 156 + .one(&kt.db) 157 + .await? 158 + .expect("Zettel must exist since we just created it") 159 + .into_active_model(), 160 + ) 161 + .insert(&kt.db) 162 + .await?; 163 + 164 + let group = GroupEntity::load() 165 + .with(TagEntity) 166 + .with((ZettelEntity, TagEntity)) 167 + .filter_by_nano_id(parent.nano_id) 168 + .one(&kt.db) 169 + .await? 170 + .expect("We just inserted it"); 171 + 172 + let mut task = TaskEntity::load() 173 + .with((ZettelEntity, TagEntity)) 174 + .filter_by_nano_id(inserted.nano_id) 175 + .one(&kt.db) 176 + .await? 177 + .expect("We just inserted it"); 178 + 179 + task.group = HasOne::Loaded(Box::new(group)); 180 + 181 + println!("task: {task:#?}"); 182 + 183 + let task: Task = task.into(); 184 + 185 + println!("created task: {task:#?}"); 186 + } 132 187 } 188 + } 189 + Self::Test => { 190 + let conf = Config::parse()?; 191 + let kt = Kasten::instansiate(conf.fil_dir).await?; 192 + println!("kt: {kt:#?}"); 133 193 } 134 194 } 135 195
+28
src/tui/components/todo/explorer.rs
··· 1 + #![allow(dead_code)] 2 + 3 + use dto::NanoId; 4 + use ratatui::{text::Span, widgets::ListState}; 5 + 6 + pub struct Explorer<'text> { 7 + pub render_list: ratatui::widgets::List<'text>, 8 + pub id_list: Vec<NanoId>, 9 + pub state: ListState, 10 + pub width: u16, 11 + } 12 + 13 + pub struct ExplorerListItem<'text> { 14 + name: Span<'text>, 15 + } 16 + 17 + // impl From<&Task> for ExplorerListItem { 18 + // fn from(value: &Task) -> Self { 19 + // Self { 20 + // name: Span { style: (), content: () } 21 + // } 22 + // } 23 + // } 24 + 25 + // impl Explorer { 26 + // pub async fn 27 + 28 + // }
+29 -4
src/tui/components/todo/mod.rs
··· 1 1 use async_trait::async_trait; 2 + use color_eyre::eyre::Result; 3 + use dto::{ 4 + ColumnTrait as _, GroupColumns, GroupEntity, QueryFilter as _, TagEntity, TaskEntity, 5 + ZettelEntity, 6 + }; 2 7 use ratatui::{ 3 8 Frame, 4 - layout::{Constraint, Layout, Rect}, 9 + layout::{Constraint, Layout, Rect, Size}, 5 10 style::{Color, Stylize}, 6 11 widgets::Block, 7 12 }; ··· 11 16 tui::{Signal, components::Component}, 12 17 types::KastenHandle, 13 18 }; 19 + 20 + mod explorer; 14 21 15 22 #[expect(dead_code)] 16 23 pub struct Todo { ··· 20 27 } 21 28 22 29 impl Todo { 23 - pub fn new(kh: KastenHandle) -> Self { 24 - Self { 30 + pub async fn new(kh: KastenHandle) -> Result<Self> { 31 + let kt = kh.read().await; 32 + 33 + let _roots = GroupEntity::load() 34 + .with(TagEntity) 35 + .with(TaskEntity) 36 + .with((ZettelEntity, TagEntity)) 37 + .filter(GroupColumns::ParentGroupId.is_null()) 38 + .all(&kt.db) 39 + .await?; 40 + 41 + drop(kt); 42 + 43 + Ok(Self { 25 44 kh, 26 45 layouts: Layouts::default(), 27 46 signal_tx: None, 28 - } 47 + }) 29 48 } 30 49 } 31 50 ··· 44 63 45 64 #[async_trait] 46 65 impl Component for Todo { 66 + async fn init(&mut self, area: Size) -> color_eyre::Result<()> { 67 + let _ = area; // to appease clippy 68 + 69 + Ok(()) 70 + } 71 + 47 72 async fn update(&mut self, _signal: Signal) -> color_eyre::Result<Option<Signal>> { 48 73 Ok(None) 49 74 }
+1 -1
src/tui/components/viewport/mod.rs
··· 41 41 _layouts: Layouts::default(), 42 42 switcher, 43 43 zk: Zk::new(kh.clone()).await?, 44 - todo: Todo::new(kh.clone()), 44 + todo: Todo::new(kh.clone()).await?, 45 45 active_region: Region::default(), 46 46 kh, 47 47 })
+1 -1
src/types/filaments.rs
··· 6 6 petgraph::{Directed, graph::NodeIndex, prelude::StableGraph}, 7 7 }; 8 8 9 - use crate::types::{Index, Link, ZettelId, index::ZettelOnDisk}; 9 + use crate::types::{Index, Link, ZettelId, kasten::ZettelOnDisk}; 10 10 11 11 pub type ZkGraph = Graph<ZettelId, Link, Directed>; 12 12
+2
src/types/group.rs
··· 12 12 pub id: NanoId, 13 13 pub name: String, 14 14 pub priority: Priority, 15 + pub parent_id: Option<NanoId>, 15 16 pub created_at: DateTime, 16 17 pub modified_at: DateTime, 17 18 /// The `Zettel` that is related to this `Group`. ··· 30 31 id: value.nano_id, 31 32 name: value.name, 32 33 priority: value.priority.into(), 34 + parent_id: value.parent_group_id, 33 35 created_at: value.created_at, 34 36 modified_at: value.modified_at, 35 37 zettel: value
+12 -12
src/types/index.rs src/types/kasten/index/mod.rs
··· 18 18 #[derive(Debug, Clone)] 19 19 pub struct Index { 20 20 pub(super) zods: HashMap<ZettelId, ZettelOnDisk>, 21 - pub(super) outgoing_links: HashMap<ZettelId, Vec<Link>>, 21 + pub outgoing_links: HashMap<ZettelId, Vec<Link>>, 22 22 // pub(super) incoming_links: HashMap<ZettelId, Vec<Link>>, 23 23 } 24 24 ··· 230 230 Ok(()) 231 231 } 232 232 233 + pub fn get_links(&self, zid: &ZettelId) -> &Vec<Link> { 234 + self.outgoing_links 235 + .get(zid) 236 + .expect("Invariant broken. Any zid we look up exist inside this map") 237 + } 238 + 239 + pub fn sync_with_db(&self, _db: &DatabaseConnection) { 240 + todo!() 241 + } 242 + 233 243 pub fn get_zod(&self, zid: &ZettelId) -> &ZettelOnDisk { 234 244 self.zods.get(zid).expect("Invariant broken. Any zid we lookup must exist in the index, otherwise the db is corrupt or not sync'd.") 235 245 } 236 246 237 - fn get_zod_mut(&mut self, zid: &ZettelId) -> &mut ZettelOnDisk { 247 + pub fn get_zod_mut(&mut self, zid: &ZettelId) -> &mut ZettelOnDisk { 238 248 self.zods.get_mut(zid).expect("Invariant broken. Any zid we lookup must exist in the index, otherwise the db is corrupt or not sync'd.") 239 249 } 240 250 241 - pub fn get_links(&self, zid: &ZettelId) -> &Vec<Link> { 242 - self.outgoing_links 243 - .get(zid) 244 - .expect("Invariant broken. Any zid we look up exist inside this map") 245 - } 246 - 247 251 pub const fn zods(&self) -> &HashMap<ZettelId, ZettelOnDisk> { 248 252 &self.zods 249 - } 250 - 251 - pub fn sync_with_db(&self, _db: &DatabaseConnection) { 252 - todo!() 253 253 } 254 254 }
+13 -2
src/types/kasten.rs src/types/kasten/mod.rs
··· 11 11 }; 12 12 use tracing::debug; 13 13 14 - use crate::types::{FrontMatter, Index, ZettelId, index::ZettelOnDisk}; 14 + use crate::types::{FrontMatter, ZettelId}; 15 15 16 - #[derive(Debug, Clone)] 16 + mod index; 17 + pub use index::Index; 18 + pub use index::ZettelOnDisk; 19 + mod todo_tree; 20 + pub use todo_tree::{TodoNode, TodoTree}; 21 + 22 + #[derive(Debug)] 17 23 pub struct Kasten { 18 24 /// Private field so it can only be instantiated from a `Path` 19 25 _private: (), ··· 21 27 pub root: PathBuf, 22 28 23 29 pub index: Index, 30 + 31 + pub todo_tree: TodoTree, 24 32 25 33 pub db: DatabaseConnection, 26 34 } ··· 54 62 // run da migrations every time we connect, just in case 55 63 Migrator::up(&conn, None).await?; 56 64 65 + let todo_tree = TodoTree::construct(&conn).await?; 66 + 57 67 Ok(Self { 58 68 _private: (), 59 69 db: conn, 60 70 root, 61 71 index, 72 + todo_tree, 62 73 }) 63 74 } 64 75
+6
src/types/kasten/index/zod.rs
··· 1 + #[derive(Debug, Clone)] 2 + pub struct ZettelOnDisk { 3 + pub fm: FrontMatter, 4 + pub body: Body, 5 + pub path: PathBuf, 6 + }
+126
src/types/kasten/todo_tree.rs
··· 1 + use std::collections::HashMap; 2 + 3 + use color_eyre::eyre::{Context, Result}; 4 + use dto::{ 5 + ColumnTrait as _, DatabaseConnection, GroupColumns, GroupEntity, NanoId, QueryFilter as _, 6 + TagEntity, TaskColumns, TaskEntity, ZettelEntity, 7 + }; 8 + use tree::{InsertBehavior, Node, NodeId, Tree}; 9 + 10 + use crate::types::{Group, Task}; 11 + 12 + #[expect(dead_code)] 13 + #[derive(Debug, Clone)] 14 + pub enum TodoNode { 15 + Root, 16 + Group(Box<Group>), 17 + Task(Box<Task>), 18 + } 19 + 20 + #[derive(Debug)] 21 + pub struct TodoTree { 22 + tree: Tree<TodoNode>, 23 + nanoid_to_nodeid: HashMap<NanoId, NodeId>, 24 + #[expect(dead_code)] 25 + root_id: NodeId, 26 + } 27 + 28 + impl TodoTree { 29 + pub async fn construct(db: &DatabaseConnection) -> Result<Self> { 30 + let mut tree = Tree::<TodoNode>::new(); 31 + let root_id = tree 32 + .insert(Node::new(TodoNode::Root), InsertBehavior::AsRoot) 33 + .with_context(|| "Could not create root node.")?; 34 + 35 + let root_groups: Vec<Group> = GroupEntity::load() 36 + .with(TagEntity) 37 + .with(TaskEntity) 38 + .with((ZettelEntity, TagEntity)) 39 + .filter(GroupColumns::ParentGroupId.is_null()) 40 + .all(db) 41 + .await? 42 + .into_iter() 43 + .map(Into::into) 44 + .collect(); 45 + 46 + let mut todo_tree = Self { 47 + tree, 48 + nanoid_to_nodeid: HashMap::new(), 49 + root_id: root_id.clone(), 50 + }; 51 + 52 + for group in root_groups { 53 + todo_tree 54 + .add_group_to_tree(db, &root_id, Box::new(group)) 55 + .await?; 56 + } 57 + 58 + Ok(todo_tree) 59 + } 60 + 61 + #[async_recursion::async_recursion] 62 + async fn add_group_to_tree( 63 + &mut self, 64 + db: &DatabaseConnection, 65 + parent_node_id: &NodeId, 66 + group: Box<Group>, 67 + ) -> Result<()> { 68 + let group_id = group.id.clone(); 69 + 70 + let group_node_id = self.tree.insert( 71 + Node::new(TodoNode::Group(group)), 72 + InsertBehavior::UnderNode(parent_node_id), 73 + )?; 74 + 75 + self.nanoid_to_nodeid 76 + .insert(group_id.clone(), group_node_id.clone()); 77 + 78 + let group_model = GroupEntity::load() 79 + .with(TagEntity) 80 + .with((ZettelEntity, TagEntity)) 81 + .filter_by_nano_id(group_id.clone()) 82 + .one(db) 83 + .await? 84 + .expect("We just inserted it"); 85 + 86 + let tasks: Vec<Task> = TaskEntity::load() 87 + .with((ZettelEntity, TagEntity)) 88 + .filter(TaskColumns::GroupId.eq(group_id.clone())) 89 + .all(db) 90 + .await? 91 + .into_iter() 92 + .map(|mut am| { 93 + am.group = dto::HasOne::Loaded(Box::new(group_model.clone())); 94 + am.into() 95 + }) 96 + .collect(); 97 + 98 + for task in tasks { 99 + let task_id = task.id.clone(); 100 + let task_node_id = self.tree.insert( 101 + Node::new(TodoNode::Task(Box::new(task))), 102 + InsertBehavior::UnderNode(&group_node_id), 103 + )?; 104 + 105 + self.nanoid_to_nodeid.insert(task_id, task_node_id); 106 + } 107 + 108 + let children_groups: Vec<Group> = GroupEntity::load() 109 + .with(TagEntity) 110 + .with(TaskEntity) 111 + .with((ZettelEntity, TagEntity)) 112 + .filter(GroupColumns::ParentGroupId.eq(group_id)) 113 + .all(db) 114 + .await? 115 + .into_iter() 116 + .map(Into::into) 117 + .collect(); 118 + 119 + for group in children_groups { 120 + self.add_group_to_tree(db, &group_node_id, Box::new(group)) 121 + .await?; 122 + } 123 + 124 + Ok(()) 125 + } 126 + }
+6 -4
src/types/mod.rs
··· 15 15 pub use group::Group; 16 16 17 17 mod task; 18 - #[expect(unused_imports)] 19 18 pub use task::Task; 20 19 21 20 mod link; ··· 24 23 mod filaments; 25 24 pub use filaments::Filaments; 26 25 27 - mod index; 28 - pub use index::Index; 29 - 30 26 mod kasten; 27 + 28 + pub use kasten::Index; 31 29 pub use kasten::Kasten; 32 30 pub use kasten::KastenHandle; 31 + #[expect(unused_imports)] 32 + pub use kasten::TodoNode; 33 + #[expect(unused_imports)] 34 + pub use kasten::TodoTree; 33 35 34 36 mod frontmatter; 35 37 pub use frontmatter::FrontMatter;
+2
src/types/task.rs
··· 13 13 pub name: String, 14 14 pub priority: Priority, 15 15 pub due: Option<DateTime>, 16 + pub group_id: NanoId, 16 17 pub finished_at: Option<DateTime>, 17 18 pub created_at: DateTime, 18 19 pub modified_at: DateTime, ··· 29 30 name: value.name, 30 31 priority: value.priority.into(), 31 32 due: value.due, 33 + group_id: value.group_id, 32 34 finished_at: value.finished_at, 33 35 created_at: value.created_at, 34 36 modified_at: value.modified_at,