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.

Merge pull request #15 from suri-codes/todo

todo

authored by

Surendra Jammishetti and committed by
GitHub
1c9a2043 d98cceb3

+1609 -187
+57 -12
.config/config.ron
··· 1 1 ( 2 2 directory: "/Users/suri/dev/projects/filaments/ZettelKasten", 3 3 global_key_binds: { 4 + "ctrl-z": Suspend, 5 + "down": MoveDown, 4 6 "up": MoveUp, 5 7 "ctrl-c": Quit, 6 - "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 - region: Todo, 12 + page: Todo(Explorer), 15 13 ), 14 + "ctrl-n": NewZettel, 15 + "enter": OpenZettel, 16 16 }, 17 17 ), 18 18 todo: ( 19 - keybinds: { 20 - "tab": SwitchTo( 21 - region: Zk, 22 - ), 23 - "k": MoveUp, 24 - "j": MoveDown, 25 - }, 19 + explorer: ( 20 + keybinds: { 21 + "2": SwitchTo( 22 + page: Todo(Inspector), 23 + ), 24 + "k": MoveUp, 25 + "1": SwitchTo( 26 + page: Todo(Explorer), 27 + ), 28 + "tab": SwitchTo( 29 + page: Zk, 30 + ), 31 + "3": SwitchTo( 32 + page: Todo(TaskList), 33 + ), 34 + "j": MoveDown, 35 + }, 36 + ), 37 + inspector: ( 38 + keybinds: { 39 + "3": SwitchTo( 40 + page: Todo(TaskList), 41 + ), 42 + "1": SwitchTo( 43 + page: Todo(Explorer), 44 + ), 45 + "tab": SwitchTo( 46 + page: Zk, 47 + ), 48 + "2": SwitchTo( 49 + page: Todo(Inspector), 50 + ), 51 + }, 52 + ), 53 + tasklist: ( 54 + keybinds: { 55 + "1": SwitchTo( 56 + page: Todo(Explorer), 57 + ), 58 + "tab": SwitchTo( 59 + page: Zk, 60 + ), 61 + "2": SwitchTo( 62 + page: Todo(Inspector), 63 + ), 64 + "3": SwitchTo( 65 + page: Todo(TaskList), 66 + ), 67 + "j": MoveDown, 68 + "k": MoveUp, 69 + }, 70 + ), 26 71 ), 27 72 )
+35 -15
.config/default_config.ron
··· 1 1 ( 2 2 directory: "{INSERT_ROOT_HERE}", 3 3 global_key_binds: { 4 - "up": MoveUp, 4 + "up": MoveUp, 5 + "down": MoveDown, 6 + "ctrl-z": Suspend, 5 7 "ctrl-c": Quit, 6 - "ctrl-z": Suspend, 7 - "down": MoveDown, 8 8 }, 9 9 zk: ( 10 10 keybinds: { 11 - "enter": OpenZettel, 12 - "tab": SwitchTo( 13 - region: Todo, 14 - ), 15 - "<Ctrl-n>": NewZettel, 11 + "tab": SwitchTo(page: Todo(Explorer)), 12 + "ctrl-n": NewZettel, 13 + "enter": OpenZettel, 16 14 }, 17 15 ), 18 16 todo: ( 19 - keybinds: { 20 - "k": MoveUp, 21 - "j": MoveDown, 22 - "tab": SwitchTo( 23 - region: Zk, 24 - ), 25 - }, 17 + explorer: ( 18 + keybinds: { 19 + "tab": SwitchTo(page: Zk), 20 + "1": SwitchTo(page: Todo(Explorer)), 21 + "2": SwitchTo(page: Todo(Inspector)), 22 + "3": SwitchTo(page: Todo(TaskList)), 23 + "j": MoveDown, 24 + "k": MoveUp, 25 + }, 26 + ), 27 + inspector: ( 28 + keybinds: { 29 + "tab": SwitchTo(page: Zk), 30 + "1": SwitchTo(page: Todo(Explorer)), 31 + "2": SwitchTo(page: Todo(Inspector)), 32 + "3": SwitchTo(page: Todo(TaskList)), 33 + }, 34 + ), 35 + tasklist: ( 36 + keybinds: { 37 + "tab": SwitchTo(page: Zk), 38 + "1": SwitchTo(page: Todo(Explorer)), 39 + "2": SwitchTo(page: Todo(Inspector)), 40 + "3": SwitchTo(page: Todo(TaskList)), 41 + "j": MoveDown, 42 + "k": MoveUp, 43 + }, 44 + ), 26 45 ), 27 46 ) 47 +
+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"
+2
crates/dto/migration/src/lib.rs
··· 8 8 mod m20260327_175853_tag_table; 9 9 mod m20260327_180618_zettel_tag_table; 10 10 mod m20260406_200424_remove_path_from_zettel; 11 + mod m20260410_223725_add_tag_to_group; 11 12 12 13 pub struct Migrator; 13 14 ··· 21 22 Box::new(m20260327_175853_tag_table::Migration), 22 23 Box::new(m20260327_180618_zettel_tag_table::Migration), 23 24 Box::new(m20260406_200424_remove_path_from_zettel::Migration), 25 + Box::new(m20260410_223725_add_tag_to_group::Migration), 24 26 ] 25 27 } 26 28 }
+3
crates/dto/migration/src/m20260318_233726_group_table.rs
··· 110 110 111 111 /// Last modified 112 112 ModifiedAt, 113 + 114 + /// Tag Id 115 + TagId, 113 116 }
+143
crates/dto/migration/src/m20260410_223725_add_tag_to_group.rs
··· 1 + use crate::{ 2 + m20260318_233726_group_table::Group, m20260323_002518_zettel_table::Zettel, 3 + m20260327_175853_tag_table::Tag, types::NANO_ID_LEN, 4 + }; 5 + use sea_orm_migration::{prelude::*, schema::*}; 6 + 7 + #[derive(DeriveMigrationName)] 8 + pub struct Migration; 9 + 10 + #[async_trait::async_trait] 11 + impl MigrationTrait for Migration { 12 + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 13 + manager 14 + .drop_table(Table::drop().table(Group::Table).to_owned()) 15 + .await?; 16 + 17 + // just recreate the groups table! 18 + manager 19 + .create_table( 20 + Table::create() 21 + .table(Group::Table) 22 + .col(pk_auto(Group::Id)) 23 + .col( 24 + string(Group::NanoId) 25 + .string_len(NANO_ID_LEN as u32) 26 + .unique_key() 27 + .not_null(), 28 + ) 29 + .col(string(Group::Name).not_null()) 30 + .col(string(Group::Priority).not_null()) 31 + .col(date_time(Group::CreatedAt).default(Expr::current_timestamp())) 32 + .col(date_time(Group::ModifiedAt).default(Expr::current_timestamp())) 33 + .col( 34 + string(Group::ZettelId) 35 + .string_len(NANO_ID_LEN as u32) 36 + .unique_key() 37 + .not_null(), 38 + ) 39 + .col(string_null(Group::ParentGroupId).string_len(NANO_ID_LEN as u32)) 40 + .col( 41 + string(Group::TagId) 42 + .string_len(NANO_ID_LEN as u32) 43 + .unique_key() 44 + .not_null(), 45 + ) 46 + .foreign_key( 47 + ForeignKey::create() 48 + .name("fk_group_zettel_id") 49 + .from(Group::Table, Group::ZettelId) 50 + .to(Zettel::Table, Zettel::NanoId) 51 + .on_update(ForeignKeyAction::Cascade) 52 + .on_delete(ForeignKeyAction::Cascade), 53 + ) 54 + .foreign_key( 55 + ForeignKey::create() 56 + .name("fk_group_parent_id") 57 + .from(Group::Table, Group::ParentGroupId) 58 + .to(Group::Table, Group::NanoId) 59 + .on_update(ForeignKeyAction::Cascade) 60 + .on_delete(ForeignKeyAction::Cascade), 61 + ) 62 + .foreign_key( 63 + ForeignKey::create() 64 + .name("fk_group_tag_id") 65 + .from(Group::Table, Group::TagId) 66 + .to(Tag::Table, Tag::NanoId) 67 + .on_update(ForeignKeyAction::Cascade) 68 + .on_delete(ForeignKeyAction::Cascade), 69 + ) 70 + .to_owned(), 71 + ) 72 + .await?; 73 + 74 + manager 75 + .create_index( 76 + Index::create() 77 + .name("idx_groups_pub_id") 78 + .table(Group::Table) 79 + .col(Group::NanoId) 80 + .to_owned(), 81 + ) 82 + .await 83 + } 84 + 85 + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 86 + manager 87 + .drop_index(Index::drop().name("idx_groups_pub_id").to_owned()) 88 + .await?; 89 + 90 + manager 91 + .drop_table(Table::drop().table(Group::Table).to_owned()) 92 + .await?; 93 + 94 + // just create the shitty old table 95 + manager 96 + .create_table( 97 + Table::create() 98 + .table(Group::Table) 99 + .col(pk_auto(Group::Id)) 100 + .col( 101 + string(Group::NanoId) 102 + .string_len(NANO_ID_LEN as u32) 103 + .unique_key() 104 + .not_null(), 105 + ) 106 + .col(string(Group::Name).not_null()) 107 + .col(integer(Group::Color).not_null()) 108 + .col(string(Group::Priority).not_null()) 109 + .col(date_time(Group::CreatedAt).default(Expr::current_timestamp())) 110 + .col(date_time(Group::ModifiedAt).default(Expr::current_timestamp())) 111 + .col(string(Group::ZettelId).not_null().unique_key()) 112 + .col(string_null(Group::ParentGroupId)) 113 + .foreign_key( 114 + ForeignKey::create() 115 + .name("fk_task_zettel_id") 116 + .from(Group::Table, Group::ZettelId) 117 + .to(Zettel::Table, Zettel::NanoId) 118 + .on_update(ForeignKeyAction::Cascade) 119 + .on_delete(ForeignKeyAction::Cascade), 120 + ) 121 + .foreign_key( 122 + ForeignKey::create() 123 + .name("fk_group_parent_id") 124 + .from(Group::Table, Group::ParentGroupId) 125 + .to(Group::Table, Group::NanoId) 126 + .on_update(ForeignKeyAction::Cascade) 127 + .on_delete(ForeignKeyAction::Cascade), 128 + ) 129 + .to_owned(), 130 + ) 131 + .await?; 132 + 133 + manager 134 + .create_index( 135 + Index::create() 136 + .name("idx_groups_pub_id") 137 + .table(Group::Table) 138 + .col(Group::NanoId) 139 + .to_owned(), 140 + ) 141 + .await 142 + } 143 + }
+1 -1
crates/dto/migration/src/types/color.rs
··· 6 6 /// Color type 7 7 /// 8 8 /// We store it as a u32, but its actually 00000000rrrrrrrrggggggggbbbbbbbb 9 - #[derive(Clone, Copy, PartialEq, Eq, DeriveValueType)] 9 + #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Copy, DeriveValueType)] 10 10 pub struct Color(u32); 11 11 12 12 impl Color {
+1 -1
crates/dto/migration/src/types/priority.rs
··· 1 1 use sea_orm::DeriveValueType; 2 2 3 - #[derive(Clone, Debug, PartialEq, Eq, DeriveValueType, Default)] 3 + #[derive(Clone, Debug, PartialEq, Eq, DeriveValueType, Default, PartialOrd, Ord)] 4 4 #[sea_orm(value_type = "String")] 5 5 pub enum Priority { 6 6 Asap,
+12 -2
crates/dto/src/entity/group.rs
··· 2 2 3 3 use migration::prelude::Local; 4 4 use migration::types::*; 5 + 6 + use sea_orm::ActiveValue::Set; 5 7 use sea_orm::entity::prelude::*; 6 - use sea_orm::ActiveValue::Set; 7 8 use std::future::ready; 8 9 use std::pin::Pin; 9 10 ··· 16 17 #[sea_orm(unique)] 17 18 pub nano_id: NanoId, 18 19 pub name: String, 19 - pub color: Color, 20 20 pub priority: Priority, 21 21 pub created_at: DateTime, 22 22 pub modified_at: DateTime, 23 23 #[sea_orm(unique)] 24 24 pub zettel_id: NanoId, 25 25 pub parent_group_id: Option<NanoId>, 26 + #[sea_orm(unique)] 27 + pub tag_id: String, 26 28 #[sea_orm( 27 29 self_ref, 28 30 relation_enum = "SelfRef", ··· 32 34 on_delete = "Cascade" 33 35 )] 34 36 pub group: HasOne<Entity>, 37 + #[sea_orm( 38 + belongs_to, 39 + from = "tag_id", 40 + to = "nano_id", 41 + on_update = "Cascade", 42 + on_delete = "Cascade" 43 + )] 44 + pub tag: HasOne<super::tag::Entity>, 35 45 #[sea_orm(has_many)] 36 46 pub tasks: HasMany<super::task::Entity>, 37 47 #[sea_orm(
+9 -1
crates/dto/src/entity/tag.rs
··· 1 1 //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 2 2 3 3 use migration::types::*; 4 - use sea_orm::entity::prelude::*; 5 4 use sea_orm::ActiveValue::Set; 5 + use sea_orm::entity::prelude::*; 6 6 use std::{future::ready, pin::Pin}; 7 7 8 8 #[sea_orm::model] ··· 16 16 #[sea_orm(unique)] 17 17 pub name: String, 18 18 pub color: Color, 19 + #[sea_orm(has_one)] 20 + pub group: HasOne<super::group::Entity>, 19 21 #[sea_orm(has_many, via = "zettel_tag")] 20 22 pub zettels: HasMany<super::zettel::Entity>, 21 23 } ··· 36 38 } 37 39 if insert && self.color.is_not_set() { 38 40 self.color = Set(Color::default()); 41 + } 42 + 43 + if let Set(ref mut name) = self.name 44 + && insert 45 + { 46 + *name = name.replace(' ', "_"); 39 47 } 40 48 Box::pin(ready(Ok(self))) 41 49 }
+4
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; ··· 20 22 /// and add additional functionality to it. 21 23 pub use migration::types::Priority as PriorityDTO; 22 24 25 + pub use sea_orm::entity::prelude::Date; 23 26 pub use sea_orm::entity::prelude::DateTime; 27 + pub use sea_orm::entity::prelude::Time; 24 28 25 29 /// Color type, exporting as DTO because I might 26 30 /// want to newtype wrap this, might not have to, depending
+10 -12
crates/dto/tests/task.rs
··· 1 1 //! Testing task functionality with the database abstraction. 2 2 3 3 use dto::{ 4 - ActiveModelTrait as _, ActiveValue::Set, GroupActiveModel, GroupEntity, GroupModel, 5 - TaskActiveModel, TaskEntity, TaskModel, ZettelActiveModel, ZettelEntity, ZettelModel, 4 + ActiveModelTrait as _, ActiveValue::Set, GroupActiveModel, GroupEntity, GroupModelEx, 5 + TagActiveModel, TaskActiveModel, TaskEntity, TaskModel, ZettelActiveModel, ZettelEntity, 6 + ZettelModel, 6 7 }; 7 - use migration::types::Color; 8 8 mod common; 9 9 10 10 #[tokio::test] ··· 19 19 .await 20 20 .unwrap(); 21 21 22 - let group: GroupModel = GroupActiveModel { 23 - name: Set("something".to_owned()), 24 - color: Set(Color::new(255, 0, 0)), 25 - zettel_id: Set(group_zettel.nano_id.clone()), 26 - ..Default::default() 27 - } 28 - .insert(&db) 29 - .await 30 - .unwrap(); 22 + let group: GroupModelEx = GroupActiveModel::builder() 23 + .set_name("Something") 24 + .set_tag(TagActiveModel::builder().set_name("something")) 25 + .set_zettel_id(group_zettel.nano_id) 26 + .insert(&db) 27 + .await 28 + .unwrap(); 31 29 32 30 let task_zettel: ZettelModel = ZettelActiveModel { 33 31 // nano_id: Set(NanoId::default()),
+5 -2
crates/tree/Cargo.toml
··· 8 8 license.workspace = true 9 9 10 10 [dependencies] 11 - automerge = "0.7.4" 12 - autosurgeon = "0.10.1" 11 + automerge = {version = "0.7.4", optional = true} 12 + autosurgeon = {version = "0.10.1", optional = true} 13 13 serde.workspace = true 14 14 15 + 16 + [features] 17 + automerge = ["dep:automerge", "dep:autosurgeon"] 15 18 16 19 [lints.clippy] 17 20 pedantic = "deny"
+2 -4
crates/tree/src/lib.rs
··· 5 5 //! 6 6 //! TODO: add example usage 7 7 8 - use autosurgeon::{Hydrate, Reconcile}; 9 8 use serde::{Deserialize, Serialize}; 10 9 11 10 mod behaviors; ··· 32 31 pub use error::NodeIdError; 33 32 34 33 /// A Node Id 35 - #[derive( 36 - Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Reconcile, Hydrate, 37 - )] 34 + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 35 + #[cfg_attr(feature = "automerge", derive(Reconcile, Hydrate))] 38 36 pub struct NodeId { 39 37 index: u32, 40 38 }
+2 -2
crates/tree/src/node.rs
··· 1 - use autosurgeon::{Hydrate, Reconcile}; 2 1 use serde::{Deserialize, Serialize}; 3 2 4 3 use crate::NodeId; 5 4 6 - #[derive(Debug, Serialize, Deserialize, Reconcile, Hydrate, Ord, Eq, PartialOrd)] 5 + #[derive(Debug, Serialize, Deserialize, Ord, Eq, PartialOrd)] 6 + #[cfg_attr(feature = "automerge", derive(Reconcile, Hydrate))] 7 7 pub struct Node<T> { 8 8 pub(crate) data: T, 9 9 pub(crate) parent: Option<NodeId>,
+5 -2
crates/tree/src/tree.rs
··· 1 1 use std::cmp::Ordering; 2 2 3 - use autosurgeon::{Hydrate, Reconcile}; 4 3 use serde::{Deserialize, Serialize}; 5 4 6 5 use crate::{ ··· 156 155 /// Any function that takes a `NodeId` can `panic`, but this should 157 156 /// only happen with improper `NodeId` management within `tree`, and 158 157 /// should have nothing to do with library user's code. 159 - #[derive(Debug, Serialize, Deserialize, Reconcile, Hydrate)] 158 + #[derive(Debug, Serialize, Deserialize)] 159 + #[cfg_attr( 160 + feature = "automerge", 161 + derive(autosurgeon::Reconcile, autosurgeon::Hydrate) 162 + )] 160 163 pub struct Tree<T> { 161 164 root: Option<NodeId>, 162 165 pub(crate) nodes: Vec<Option<Node<T>>>,
+30 -1
src/cli/mod.rs
··· 1 1 use clap::{Parser, Subcommand}; 2 + use dto::NanoId; 2 3 3 4 use crate::config::{get_config_dir, get_data_dir}; 4 5 ··· 28 29 /// Manage `Zettel`s 29 30 #[command(subcommand)] 30 31 Zettel(ZettelSubcommand), 32 + 33 + #[command(subcommand)] 34 + Todo(TodoSubcommand), 31 35 32 36 /// Spawn the `LSP` 33 37 Lsp, ··· 55 59 // default values if they arent present / aren't able to be 56 60 // parsed properly 57 61 // Import(ImportArgs), 62 + Test, 58 63 } 59 64 60 65 #[derive(Subcommand, Debug)] 61 - /// Subcommand to manage tars groups. 66 + /// Subcommand to manage Zettels. 62 67 pub enum ZettelSubcommand { 63 68 /// Add a group. 64 69 New { ··· 71 76 /// Filter by tag 72 77 #[arg(short = 't', long)] 73 78 by_tag: String, 79 + }, 80 + } 81 + 82 + #[derive(Subcommand, Debug)] 83 + /// Subcommand to manage To-Do functionality 84 + pub enum TodoSubcommand { 85 + Group { 86 + /// The name of this group 87 + #[arg(short, long)] 88 + name: String, 89 + 90 + /// If this group has a parent, provide the parent id. 91 + #[arg(short, long)] 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, 74 103 }, 75 104 } 76 105
+129 -3
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 + use dto::{ 9 + Date, DateTime, GroupActiveModel, GroupEntity, HasOne, IntoActiveModel, TagActiveModel, 10 + TagEntity, TaskActiveModel, TaskEntity, Time, ZettelEntity, 11 + }; 8 12 use tower_lsp::{LspService, Server}; 9 13 10 14 use crate::{ 11 15 cli::{Commands, ZettelSubcommand}, 12 16 config::{Config, get_config_dir}, 13 17 lsp::Backend, 14 - types::{Kasten, Zettel}, 18 + types::{Group, Kasten, Priority, Tag, Task, Zettel}, 15 19 }; 16 20 17 21 impl Commands { 22 + #[expect(clippy::too_many_lines)] 18 23 pub async fn process(self) -> Result<()> { 19 24 match self { 20 25 Self::Init { name } => { ··· 50 55 51 56 match zettel_sub_command { 52 57 ZettelSubcommand::New { title } => { 53 - let zettel = Zettel::new(title, &mut kt).await?; 58 + let zettel = Zettel::new(title, &mut kt, vec![]).await?; 54 59 println!("Zettel Created! {zettel:#?}"); 55 60 } 56 61 ZettelSubcommand::List { by_tag: _by_tag } => {} ··· 68 73 let (service, socket) = LspService::new(|client| Backend::new(client, kt)); 69 74 70 75 Server::new(stdin, stdout, socket).serve(service).await; 76 + } 77 + 78 + Self::Todo(command) => { 79 + let conf = Config::parse()?; 80 + let mut kt = Kasten::instansiate(conf.fil_dir).await?; 81 + 82 + match command { 83 + super::TodoSubcommand::Group { name, parent_id } => { 84 + // lets create a tag for this first group first 85 + let tag: Tag = TagActiveModel::builder() 86 + .set_name(name.clone()) 87 + .insert(&kt.db) 88 + .await? 89 + .into(); 90 + 91 + let tag_id = tag.id.clone(); 92 + 93 + // then create the zettel for the group 94 + let zettel = Zettel::new(name.clone(), &mut kt, vec![tag]).await?; 95 + 96 + // then insert that shi 97 + let inserted = GroupActiveModel::builder() 98 + .set_name(name) 99 + .set_parent_group_id(parent_id) 100 + .set_tag( 101 + TagEntity::load() 102 + .filter_by_nano_id(tag_id) 103 + .one(&kt.db) 104 + .await? 105 + .expect("Tag must exist since we just created it") 106 + .into_active_model(), 107 + ) 108 + .set_zettel( 109 + ZettelEntity::load() 110 + .filter_by_nano_id(zettel.id) 111 + .one(&kt.db) 112 + .await? 113 + .expect("Zettel must exist since we just created it") 114 + .into_active_model(), 115 + ) 116 + .set_priority(Priority::default()) 117 + .insert(&kt.db) 118 + .await?; 119 + 120 + // group should also have the accompanying tag for it. 121 + let group: Group = GroupEntity::load() 122 + .with(TagEntity) 123 + .with((ZettelEntity, TagEntity)) 124 + .filter_by_nano_id(inserted.nano_id) 125 + .one(&kt.db) 126 + .await? 127 + .expect("We just inserted it") 128 + .into(); 129 + 130 + println!("created group {group:#?}"); 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 + .set_due(Some(DateTime::new( 162 + Date::from_ymd_opt(2026, 1, 31).unwrap(), 163 + Time::from_hms_opt(10, 10, 10).unwrap(), 164 + ))) 165 + .insert(&kt.db) 166 + .await?; 167 + 168 + let group = GroupEntity::load() 169 + .with(TagEntity) 170 + .with((ZettelEntity, TagEntity)) 171 + .filter_by_nano_id(parent.nano_id) 172 + .one(&kt.db) 173 + .await? 174 + .expect("We just inserted it"); 175 + 176 + let mut task = TaskEntity::load() 177 + .with((ZettelEntity, TagEntity)) 178 + .filter_by_nano_id(inserted.nano_id) 179 + .one(&kt.db) 180 + .await? 181 + .expect("We just inserted it"); 182 + 183 + task.group = HasOne::Loaded(Box::new(group)); 184 + 185 + println!("task: {task:#?}"); 186 + 187 + let task: Task = task.into(); 188 + 189 + println!("created task: {task:#?}"); 190 + } 191 + } 192 + } 193 + Self::Test => { 194 + let conf = Config::parse()?; 195 + let kt = Kasten::instansiate(conf.fil_dir).await?; 196 + println!("kt: {kt:#?}"); 71 197 } 72 198 } 73 199
+33 -4
src/config/file.rs
··· 19 19 20 20 #[derive(Debug, Deserialize, Serialize)] 21 21 pub struct TodoConfig { 22 + pub explorer: ExplorerConfig, 23 + pub inspector: InspectorConfig, 24 + pub tasklist: TaskListConfig, 25 + } 26 + 27 + #[derive(Debug, Deserialize, Serialize)] 28 + pub struct ExplorerConfig { 29 + pub keybinds: HashMap<String, Signal>, 30 + } 31 + #[derive(Debug, Deserialize, Serialize)] 32 + pub struct InspectorConfig { 33 + pub keybinds: HashMap<String, Signal>, 34 + } 35 + #[derive(Debug, Deserialize, Serialize)] 36 + pub struct TaskListConfig { 22 37 pub keybinds: HashMap<String, Signal>, 23 38 } 24 39 ··· 40 55 ]), 41 56 }, 42 57 todo: TodoConfig { 43 - keybinds: HashMap::from([ 44 - ("<Space>".to_string(), Signal::NewZettel), 45 - ("<Esc>".to_string(), Signal::MoveUp), 46 - ]), 58 + explorer: ExplorerConfig { 59 + keybinds: HashMap::from([ 60 + ("<Space>".to_string(), Signal::NewZettel), 61 + ("<Esc>".to_string(), Signal::MoveUp), 62 + ]), 63 + }, 64 + inspector: InspectorConfig { 65 + keybinds: HashMap::from([ 66 + ("<Space>".to_string(), Signal::NewZettel), 67 + ("<Esc>".to_string(), Signal::MoveUp), 68 + ]), 69 + }, 70 + tasklist: TaskListConfig { 71 + keybinds: HashMap::from([ 72 + ("<Space>".to_string(), Signal::NewZettel), 73 + ("<Esc>".to_string(), Signal::MoveUp), 74 + ]), 75 + }, 47 76 }, 48 77 }; 49 78
+58 -30
src/config/keymap.rs
··· 10 10 11 11 use crate::{ 12 12 config::file::RonConfig, 13 - tui::{Region, Signal}, 13 + tui::{Page, Signal, TodoRegion}, 14 14 }; 15 + 15 16 #[derive(Debug, Clone)] 16 - pub struct KeyMap(pub HashMap<Region, HashMap<Vec<KeyEvent>, Signal>>); 17 + pub struct KeyMap(pub HashMap<Page, HashMap<Vec<KeyEvent>, Signal>>); 17 18 18 19 impl TryFrom<&RonConfig> for KeyMap { 19 20 type Error = color_eyre::Report; ··· 21 22 fn try_from(value: &RonConfig) -> Result<Self, Self::Error> { 22 23 let mut binds = HashMap::new(); 23 24 24 - for region in Region::iter() { 25 - let mut region_binds = HashMap::new(); 26 - 27 - let mut parse_and_insert = |str: &str, bind: &Signal| -> Result<()> { 28 - let key_seq = parse_key_sequence(str).map_err(|e| { 29 - eyre!(format!( 30 - "Failed to parse the following keybind as valid keybind: {e}" 31 - )) 32 - })?; 25 + let all_pages = std::iter::once(Page::Zk).chain(TodoRegion::iter().map(Page::Todo)); 33 26 34 - region_binds.insert(key_seq, bind.clone()); 35 - Ok(()) 36 - }; 37 - 38 - // first thing we have to do is insert the global binds for this region 27 + for page in all_pages { 28 + let mut page_binds = HashMap::new(); 39 29 40 30 for (str, bind) in &value.global_key_binds { 41 - parse_and_insert(str, bind)?; 31 + parse_and_insert(str, bind, &mut page_binds)?; 42 32 } 43 33 44 - // now we insert the region specific binds 45 - for (str, bind) in match region { 46 - Region::Zk => value.zk.keybinds.iter(), 47 - Region::Todo => value.todo.keybinds.iter(), 48 - } { 49 - parse_and_insert(str, bind)?; 34 + let page_specific: &HashMap<String, Signal> = match &page { 35 + Page::Zk => &value.zk.keybinds, 36 + Page::Todo(TodoRegion::Inspector) => &value.todo.inspector.keybinds, 37 + Page::Todo(TodoRegion::Explorer) => &value.todo.explorer.keybinds, 38 + Page::Todo(TodoRegion::TaskList) => &value.todo.tasklist.keybinds, 39 + }; 40 + 41 + for (str, bind) in page_specific { 42 + parse_and_insert(str, bind, &mut page_binds)?; 50 43 } 51 44 52 - binds.insert(region, region_binds); 45 + binds.insert(page, page_binds); 53 46 } 54 47 55 48 Ok(Self(binds)) 56 49 } 57 50 } 51 + fn parse_and_insert( 52 + str: &str, 53 + bind: &Signal, 54 + page_binds: &mut HashMap<Vec<KeyEvent>, Signal>, 55 + ) -> Result<()> { 56 + let key_seq = parse_key_sequence(str) 57 + .map_err(|e| eyre!("Failed to parse the following keybind as valid keybind: {e}"))?; 58 + page_binds.insert(key_seq, bind.clone()); 59 + Ok(()) 60 + } 58 61 59 62 impl Deref for KeyMap { 60 - type Target = HashMap<Region, HashMap<Vec<KeyEvent>, Signal>>; 63 + type Target = HashMap<Page, HashMap<Vec<KeyEvent>, Signal>>; 61 64 62 65 fn deref(&self) -> &Self::Target { 63 66 &self.0 ··· 182 185 183 186 use crate::{ 184 187 config::{file::RonConfig, keymap::KeyMap}, 185 - tui::{Region, Signal}, 188 + tui::{Page, Signal, TodoRegion}, 186 189 }; 187 190 188 191 #[test] ··· 201 204 "<Ctrl-n>": NewZettel, 202 205 "enter": OpenZettel, 203 206 "tab": SwitchTo ( 204 - region: Todo 207 + page: Todo(Explorer) 205 208 ), 206 209 207 210 }, 208 211 ), 209 212 todo: ( 213 + inspector: ( 210 214 keybinds: { 211 215 "j": MoveDown, 212 216 "k": MoveUp, 213 217 "tab": SwitchTo ( 214 - region: Zk 218 + page: Zk 219 + ), 220 + 221 + }, 222 + 223 + ), 224 + tasklist: ( 225 + keybinds: { 226 + "j": MoveDown, 227 + "k": MoveUp, 228 + "tab": SwitchTo ( 229 + page: Zk 230 + ), 231 + 232 + }, 233 + 234 + ), 235 + explorer: ( 236 + keybinds: { 237 + "j": MoveDown, 238 + "k": MoveUp, 239 + "tab": SwitchTo ( 240 + page: Zk 215 241 ), 216 242 217 243 }, 244 + 245 + ), 218 246 ), 219 247 ) 220 248 "#; ··· 223 251 let keymap: KeyMap = (&config).try_into().unwrap(); 224 252 225 253 let map = keymap 226 - .get(&Region::Todo) 254 + .get(&Page::Todo(TodoRegion::Inspector)) 227 255 .expect("Home region must exist in keymap"); 228 256 229 257 let signal = map ··· 237 265 assert_eq!(*signal, Signal::Quit); 238 266 239 267 let map = keymap 240 - .get(&Region::Zk) 268 + .get(&Page::Zk) 241 269 .expect("Home region must exist in keymap"); 242 270 243 271 let signal = map
+3
src/main.rs
··· 41 41 // create the kasten handle 42 42 let kh: KastenHandle = rt.block_on(async { 43 43 let cfg = Config::parse()?; 44 + 45 + debug!("Config: {cfg:#?}"); 46 + 44 47 Ok::<KastenHandle, color_eyre::Report>(Arc::new(RwLock::new( 45 48 Kasten::instansiate(cfg.fil_dir).await?, 46 49 )))
+16 -10
src/tui/app.rs
··· 6 6 use serde::{Deserialize, Serialize}; 7 7 use strum::{Display, EnumIter}; 8 8 use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; 9 - use tracing::{debug, trace}; 9 + use tracing::{debug, info, trace}; 10 10 11 11 use crate::{ 12 12 config::Config, 13 13 tui::{Event, Tui, components::Viewport}, 14 14 types::{KastenHandle, ZettelId}, 15 15 }; 16 + 17 + pub use crate::tui::components::TodoRegion; 16 18 17 19 use super::{components::Component, signal::Signal}; 18 20 ··· 24 26 should_quit: bool, 25 27 should_suspend: bool, 26 28 #[allow(dead_code)] 27 - region: Region, 29 + page: Page, 28 30 last_tick_key_events: Vec<KeyEvent>, 29 31 kh: KastenHandle, 30 32 signal_tx: UnboundedSender<Signal>, ··· 38 40 #[derive( 39 41 Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter, Display, 40 42 )] 41 - pub enum Region { 43 + pub enum Page { 42 44 #[default] 43 45 Zk, 44 - Todo, 46 + Todo(TodoRegion), 45 47 } 46 48 47 49 impl App { ··· 62 64 should_quit: false, 63 65 should_suspend: false, 64 66 config: Config::parse()?, 65 - region: Region::default(), 67 + page: Page::default(), 66 68 last_tick_key_events: Vec::new(), 67 69 kh, 68 70 signal_tx, ··· 148 150 149 151 let signal_tx = self.signal_tx.clone(); 150 152 151 - let Some(region_keymap) = self.config.keymap.get(&self.region) else { 153 + let Some(page_keymap) = self.config.keymap.get(&self.page) else { 152 154 return Ok(()); 153 155 }; 154 156 155 - if let Some(signal) = region_keymap.get(&vec![key]) { 157 + info!("page: {:#?}, page_keymap: {page_keymap:#?}", self.page); 158 + 159 + if let Some(signal) = page_keymap.get(&vec![key]) { 156 160 signal_tx.send(signal.clone())?; 157 161 } else { 158 162 self.last_tick_key_events.push(key); 159 - if let Some(signal) = region_keymap.get(&self.last_tick_key_events) { 163 + if let Some(signal) = page_keymap.get(&self.last_tick_key_events) { 160 164 debug!("Got signal: {signal:?}"); 161 165 signal_tx.send(signal.clone())?; 162 166 } ··· 167 171 168 172 async fn handle_signals(&mut self, tui: &mut Tui) -> Result<()> { 169 173 while let Ok(signal) = self.signal_rx.try_recv() { 174 + // debug!("handling signal: {signal:?}"); 170 175 if signal != Signal::Tick && signal != Signal::Render { 171 176 debug!("handling signal: {signal:?}"); 172 177 // we dont care if the receiver is dropped, its fine ··· 226 231 tui.enter()?; 227 232 } 228 233 229 - Signal::SwitchTo { region } => { 230 - self.region = region; 234 + Signal::SwitchTo { page } => { 235 + info!("Switched page to {page:#?}"); 236 + self.page = page; 231 237 } 232 238 233 239 Signal::Suspend => self.should_suspend = true,
+131
src/tui/components/todo/explorer.rs
··· 1 + use ratatui::{ 2 + style::{Color, Modifier, Style, Stylize}, 3 + text::{Line, Span, Text}, 4 + widgets::{Block, BorderType, Borders, List, ListState}, 5 + }; 6 + use tracing::info; 7 + use tree::NodeId; 8 + 9 + use crate::types::{TodoNode, TodoNodeKind, TodoTree}; 10 + 11 + pub struct Explorer<'text> { 12 + pub render_list: ratatui::widgets::List<'text>, 13 + #[allow(dead_code)] 14 + pub id_list: Vec<NodeId>, 15 + pub state: ListState, 16 + #[allow(dead_code)] 17 + pub width: u16, 18 + } 19 + 20 + impl Explorer<'_> { 21 + pub fn new(tree: &TodoTree, scope: &NodeId, state: ListState, width: u16) -> Self { 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 + .filter_map(|node| { 27 + // we dont want to show the root 28 + if node.data().kind == TodoNodeKind::Root { 29 + return None; 30 + } 31 + let mut eli: ExplorerListItem<'_> = node.data().into(); 32 + eli.width = width; 33 + Some(Text::from(eli)) 34 + }), 35 + ) 36 + .style(Color::White) 37 + .highlight_style(Modifier::ITALIC | Modifier::BOLD) 38 + .highlight_symbol("> "); 39 + 40 + let id_list = tree 41 + .tree 42 + .traverse_pre_order_ids(scope) 43 + .expect("This should not panic as the node id should exist inside") 44 + .filter(|node| { 45 + if *node == tree.root_id { 46 + return false; 47 + } 48 + true 49 + }) 50 + .collect::<Vec<_>>(); 51 + 52 + Self { 53 + render_list, 54 + id_list, 55 + state, 56 + width, 57 + } 58 + } 59 + 60 + pub fn set_active(&mut self) { 61 + self.render_list = self.render_list.clone().block( 62 + Block::new() 63 + .title("[1]") 64 + .title("Explorer") 65 + .borders(Borders::TOP | Borders::RIGHT) 66 + .border_style(Style::new().fg(Color::Green)) 67 + .border_type(BorderType::Rounded), 68 + ); 69 + } 70 + 71 + pub fn set_inactive(&mut self) { 72 + self.render_list = self.render_list.clone().block( 73 + Block::new() 74 + .title("[1]") 75 + .title("Explorer") 76 + .borders(Borders::TOP | Borders::RIGHT) 77 + .border_style(Style::new().fg(Color::Gray)) 78 + .border_type(BorderType::Rounded), 79 + ); 80 + } 81 + } 82 + 83 + pub struct ExplorerListItem<'text> { 84 + spacer: Span<'text>, 85 + name: Span<'text>, 86 + width: u16, 87 + } 88 + 89 + impl From<&TodoNode> for ExplorerListItem<'_> { 90 + fn from(value: &TodoNode) -> Self { 91 + let spacer = Span::from(" ".repeat(value.depth)); 92 + let name = match value.kind { 93 + TodoNodeKind::Group(ref g) => Span::from(format!(" {}", g.name.clone())) 94 + .bg(g.tag.color) 95 + .fg(Color::Black), 96 + TodoNodeKind::Task(ref t) => { 97 + Span::from(format!(" {}", t.name.clone())).fg(t.group.tag.color) 98 + } 99 + TodoNodeKind::Root => Span::from("THIS SHOULD NOT BE VISIBLE"), 100 + }; 101 + 102 + Self { 103 + spacer, 104 + name, 105 + width: 0, 106 + } 107 + } 108 + } 109 + 110 + impl<'text> From<ExplorerListItem<'text>> for Text<'text> { 111 + fn from(value: ExplorerListItem<'text>) -> Self { 112 + let bg_color = value.name.style.bg; 113 + let spacer_width = value.spacer.content.len(); 114 + let name_width = value.name.content.len(); 115 + let used = spacer_width + name_width; 116 + 117 + let mut spans = vec![value.spacer, value.name]; 118 + 119 + if let Some(color) = bg_color { 120 + let padding = (value.width as usize).saturating_sub(used); 121 + spans.push(Span::styled( 122 + " ".repeat(padding), 123 + Style::default().bg(color), 124 + )); 125 + } 126 + 127 + info!("{spans:#?}"); 128 + 129 + Line::from(spans).into() 130 + } 131 + }
+65
src/tui/components/todo/inspector/groupview.rs
··· 1 + use ratatui::{ 2 + layout::{Constraint, Layout}, 3 + widgets::{Paragraph, Widget}, 4 + }; 5 + 6 + use crate::types::Group; 7 + 8 + #[derive(Debug, Clone)] 9 + pub struct GroupView<'text> { 10 + name: Paragraph<'text>, 11 + priority: Paragraph<'text>, 12 + created_at: Paragraph<'text>, 13 + layouts: Layouts, 14 + } 15 + 16 + #[derive(Debug, Clone)] 17 + struct Layouts { 18 + left_content: Layout, 19 + name_priority_created_at: Layout, 20 + } 21 + 22 + impl Default for Layouts { 23 + fn default() -> Self { 24 + Self { 25 + left_content: Layout::horizontal(vec![ 26 + Constraint::Percentage(50), 27 + Constraint::Fill(100), 28 + ]), 29 + name_priority_created_at: Layout::vertical(vec![ 30 + Constraint::Percentage(33), 31 + Constraint::Percentage(33), 32 + Constraint::Percentage(33), 33 + ]), 34 + } 35 + } 36 + } 37 + 38 + impl From<&Group> for GroupView<'_> { 39 + fn from(value: &Group) -> Self { 40 + Self { 41 + name: Paragraph::new(value.name.clone()), 42 + priority: Paragraph::new(value.priority.to_string()), 43 + created_at: Paragraph::new(value.created_at()), 44 + layouts: Layouts::default(), 45 + } 46 + } 47 + } 48 + 49 + impl Widget for GroupView<'_> { 50 + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) 51 + where 52 + Self: Sized, 53 + { 54 + let (name_rect, priority_rect, created_at, _content_rect) = { 55 + let rects = self.layouts.left_content.split(area); 56 + let l_rects = self.layouts.name_priority_created_at.split(rects[0]); 57 + 58 + (l_rects[0], l_rects[1], l_rects[2], rects[1]) 59 + }; 60 + 61 + self.name.render(name_rect, buf); 62 + self.priority.render(priority_rect, buf); 63 + self.created_at.render(created_at, buf); 64 + } 65 + }
+103
src/tui/components/todo/inspector/mod.rs
··· 1 + use ratatui::{ 2 + layout::{Constraint, Direction, Layout}, 3 + style::{Color, Style}, 4 + widgets::{Block, BorderType, Borders, Widget}, 5 + }; 6 + 7 + use crate::types::{TodoNode, TodoNodeKind}; 8 + 9 + mod rootview; 10 + use rootview::RootView; 11 + 12 + mod taskview; 13 + use taskview::TaskView; 14 + 15 + mod groupview; 16 + use groupview::GroupView; 17 + 18 + pub struct Inspector<'text> { 19 + render_data: RenderData<'text>, 20 + margins: Layout, 21 + block: Block<'text>, 22 + } 23 + 24 + impl Inspector<'_> { 25 + pub fn set_active(&mut self) { 26 + self.block = Block::new() 27 + .title("[2]") 28 + .title("Inspector") 29 + .borders(Borders::LEFT | Borders::TOP | Borders::BOTTOM) 30 + .border_style(Style::new().fg(Color::Green)) 31 + .border_type(BorderType::Rounded); 32 + } 33 + 34 + pub fn set_inactive(&mut self) { 35 + self.block = Block::new() 36 + .title("[2]") 37 + .title("Inspector") 38 + .borders(Borders::LEFT | Borders::TOP | Borders::BOTTOM) 39 + .border_style(Style::new().fg(Color::Gray)) 40 + .border_type(BorderType::Rounded); 41 + } 42 + } 43 + 44 + enum RenderData<'text> { 45 + Root { widget: Box<RootView<'text>> }, 46 + Task { widget: Box<TaskView<'text>> }, 47 + Group { widget: Box<GroupView<'text>> }, 48 + } 49 + 50 + impl From<&TodoNode> for Inspector<'_> { 51 + fn from(value: &TodoNode) -> Self { 52 + let margins = Layout::new(Direction::Horizontal, [Constraint::Percentage(100)]) 53 + .horizontal_margin(3) 54 + .vertical_margin(2); 55 + 56 + let block = Block::new() 57 + .title("[2]") 58 + .title("Inspector") 59 + .borders(Borders::LEFT | Borders::TOP | Borders::BOTTOM) 60 + .border_style(Style::new().fg(Color::Gray)) 61 + .border_type(BorderType::Rounded); 62 + match value.kind { 63 + TodoNodeKind::Root => Self { 64 + render_data: RenderData::Root { 65 + widget: Box::new(RootView::default()), 66 + }, 67 + margins, 68 + block, 69 + }, 70 + TodoNodeKind::Group(ref group) => Self { 71 + render_data: RenderData::Group { 72 + widget: Box::new(GroupView::from(&**group)), 73 + }, 74 + margins, 75 + block, 76 + }, 77 + TodoNodeKind::Task(ref task) => Self { 78 + render_data: RenderData::Task { 79 + widget: Box::new(TaskView::from(&**task)), 80 + }, 81 + margins, 82 + block, 83 + }, 84 + } 85 + } 86 + } 87 + 88 + impl Widget for &Inspector<'_> { 89 + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) 90 + where 91 + Self: Sized, 92 + { 93 + self.block.clone().render(area, buf); 94 + 95 + let area = self.margins.split(area)[0]; 96 + 97 + match &self.render_data { 98 + RenderData::Root { widget } => widget.clone().render(area, buf), 99 + RenderData::Task { widget } => widget.clone().render(area, buf), 100 + RenderData::Group { widget } => widget.clone().render(area, buf), 101 + } 102 + } 103 + }
+23
src/tui/components/todo/inspector/rootview.rs
··· 1 + use ratatui::widgets::{Paragraph, Widget}; 2 + 3 + #[derive(Debug, Clone)] 4 + pub struct RootView<'text> { 5 + help: Paragraph<'text>, 6 + } 7 + 8 + impl Default for RootView<'_> { 9 + fn default() -> Self { 10 + Self { 11 + help: Paragraph::new("Try making a selection!"), 12 + } 13 + } 14 + } 15 + 16 + impl Widget for RootView<'_> { 17 + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) 18 + where 19 + Self: Sized, 20 + { 21 + self.help.render(area, buf); 22 + } 23 + }
+73
src/tui/components/todo/inspector/taskview.rs
··· 1 + use ratatui::{ 2 + layout::{Constraint, Layout}, 3 + widgets::{Paragraph, Widget}, 4 + }; 5 + 6 + use crate::types::Task; 7 + 8 + #[derive(Debug, Clone)] 9 + pub struct TaskView<'text> { 10 + name: Paragraph<'text>, 11 + priority: Paragraph<'text>, 12 + #[expect(dead_code)] 13 + created_at: Paragraph<'text>, 14 + #[expect(dead_code)] 15 + parent_group: Paragraph<'text>, 16 + 17 + due: Paragraph<'text>, 18 + 19 + layouts: Layouts, 20 + } 21 + 22 + #[derive(Debug, Clone)] 23 + struct Layouts { 24 + left_content: Layout, 25 + name_priority_due: Layout, 26 + } 27 + 28 + impl Default for Layouts { 29 + fn default() -> Self { 30 + Self { 31 + left_content: Layout::horizontal(vec![ 32 + Constraint::Percentage(50), 33 + Constraint::Fill(100), 34 + ]), 35 + name_priority_due: Layout::vertical(vec![ 36 + Constraint::Percentage(33), 37 + Constraint::Percentage(33), 38 + Constraint::Percentage(33), 39 + ]), 40 + } 41 + } 42 + } 43 + 44 + impl From<&Task> for TaskView<'_> { 45 + fn from(value: &Task) -> Self { 46 + Self { 47 + name: Paragraph::new(value.name.clone()), 48 + priority: Paragraph::new(value.priority.to_string()), 49 + created_at: Paragraph::new(value.created_at()), 50 + parent_group: Paragraph::new(value.group.name.clone()), 51 + due: Paragraph::new(value.due().unwrap_or_else(|| "None".to_owned())), 52 + layouts: Layouts::default(), 53 + } 54 + } 55 + } 56 + 57 + impl Widget for TaskView<'_> { 58 + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) 59 + where 60 + Self: Sized, 61 + { 62 + let (name_rect, priority_rect, due_rect, _content_rect) = { 63 + let rects = self.layouts.left_content.split(area); 64 + let l_rects = self.layouts.name_priority_due.split(rects[0]); 65 + 66 + (l_rects[0], l_rects[1], l_rects[2], rects[1]) 67 + }; 68 + 69 + self.name.render(name_rect, buf); 70 + self.priority.render(priority_rect, buf); 71 + self.due.render(due_rect, buf); 72 + } 73 + }
+224 -13
src/tui/components/todo/mod.rs
··· 1 1 use async_trait::async_trait; 2 2 use ratatui::{ 3 3 Frame, 4 - layout::{Constraint, Layout, Rect}, 5 - style::{Color, Stylize}, 6 - widgets::Block, 4 + layout::{Constraint, Layout, Rect, Size}, 5 + widgets::ListState, 7 6 }; 7 + use serde::{Deserialize, Serialize}; 8 + use strum::{Display, EnumIter}; 8 9 use tokio::sync::mpsc::UnboundedSender; 9 10 10 11 use crate::{ 11 - tui::{Signal, components::Component}, 12 + tui::{Page, Signal, components::Component}, 12 13 types::KastenHandle, 13 14 }; 14 15 15 - #[expect(dead_code)] 16 - pub struct Todo { 16 + mod explorer; 17 + use explorer::Explorer; 18 + 19 + mod tasklist; 20 + use tasklist::TaskList; 21 + 22 + mod inspector; 23 + use inspector::Inspector; 24 + 25 + pub struct Todo<'text> { 26 + #[expect(dead_code)] 17 27 signal_tx: Option<UnboundedSender<Signal>>, 18 28 kh: KastenHandle, 19 29 layouts: Layouts, 30 + explorer: Option<Explorer<'text>>, 31 + task_list: Option<TaskList<'text>>, 32 + inspector: Option<Inspector<'text>>, 33 + 34 + active: TodoRegion, 20 35 } 21 36 22 - impl Todo { 37 + /// The different regions inside the `Todo` component 38 + #[derive( 39 + Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter, Display, 40 + )] 41 + pub enum TodoRegion { 42 + Inspector, 43 + TaskList, 44 + #[default] 45 + Explorer, 46 + } 47 + 48 + impl Todo<'_> { 23 49 pub fn new(kh: KastenHandle) -> Self { 24 50 Self { 25 51 kh, 26 52 layouts: Layouts::default(), 27 53 signal_tx: None, 54 + explorer: None, 55 + task_list: None, 56 + inspector: None, 57 + active: TodoRegion::default(), 28 58 } 29 59 } 60 + 61 + async fn update_inspector_from_selection(&mut self) { 62 + let explorer = self 63 + .explorer 64 + .as_mut() 65 + .expect("This should have already been init.ialized"); 66 + let task_list = self 67 + .task_list 68 + .as_mut() 69 + .expect("This should have already been initialized"); 70 + let inspector = self 71 + .inspector 72 + .as_mut() 73 + .expect("This should have already been initialized"); 74 + let selected_node_id = match self.active { 75 + TodoRegion::TaskList => { 76 + let Some(idx) = task_list.state.selected() else { 77 + return; 78 + }; 79 + task_list.id_list.get(idx) 80 + } 81 + TodoRegion::Explorer => { 82 + let Some(idx) = explorer.state.selected() else { 83 + return; 84 + }; 85 + explorer.id_list.get(idx) 86 + } 87 + TodoRegion::Inspector => return, 88 + }; 89 + 90 + let Some(selected_node_id) = selected_node_id else { 91 + return; 92 + }; 93 + let tree = &self.kh.read().await.todo_tree.tree; 94 + 95 + *inspector = tree 96 + .get(selected_node_id) 97 + .expect("Nodeid must be valid") 98 + .data() 99 + .into(); 100 + } 30 101 } 31 102 32 - #[expect(dead_code)] 33 103 struct Layouts { 34 - main: Layout, 104 + explorer_right: Layout, 105 + inspector_task_list: Layout, 35 106 } 36 107 37 108 impl Default for Layouts { 38 109 fn default() -> Self { 39 110 Self { 40 - main: Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)]), 111 + explorer_right: Layout::horizontal(vec![ 112 + Constraint::Percentage(40), 113 + Constraint::Fill(100), 114 + ]), 115 + inspector_task_list: Layout::vertical(vec![ 116 + Constraint::Percentage(30), 117 + Constraint::Fill(100), 118 + ]), 119 + } 120 + } 121 + } 122 + 123 + struct LayoutSplit { 124 + explorer: Rect, 125 + inspector: Rect, 126 + task_list: Rect, 127 + } 128 + 129 + impl Layouts { 130 + fn split(&self, area: Rect) -> LayoutSplit { 131 + let rects = self.explorer_right.split(area); 132 + let r_rects = self.inspector_task_list.split(rects[1]); 133 + 134 + LayoutSplit { 135 + explorer: rects[0], 136 + inspector: r_rects[0], 137 + task_list: r_rects[1], 41 138 } 42 139 } 43 140 } 44 141 45 142 #[async_trait] 46 - impl Component for Todo { 47 - async fn update(&mut self, _signal: Signal) -> color_eyre::Result<Option<Signal>> { 143 + impl Component for Todo<'_> { 144 + async fn init(&mut self, area: Size) -> color_eyre::Result<()> { 145 + let tree = &self.kh.read().await.todo_tree; 146 + let splits = self.layouts.split(Rect::new(0, 0, area.width, area.height)); 147 + 148 + let mut l_state = ListState::default(); 149 + 150 + l_state.select_first(); 151 + 152 + let mut explorer = Explorer::new(tree, &tree.root_id, l_state, splits.explorer.width); 153 + let mut task_list = TaskList::new(tree, &tree.root_id, l_state, splits.task_list.width); 154 + 155 + let first = tree 156 + .tree 157 + .get( 158 + task_list 159 + .id_list 160 + .first() 161 + .unwrap_or_else(|| tree.tree.root_node_id().expect("Root node must exist")), 162 + ) 163 + .expect("Node id must be valid"); 164 + 165 + let mut inspector: Inspector<'_> = first.data().into(); 166 + 167 + explorer.set_inactive(); 168 + inspector.set_inactive(); 169 + task_list.set_inactive(); 170 + 171 + self.explorer = Some(explorer); 172 + self.task_list = Some(task_list); 173 + self.inspector = Some(inspector); 174 + 175 + Ok(()) 176 + } 177 + 178 + async fn update(&mut self, signal: Signal) -> color_eyre::Result<Option<Signal>> { 179 + let explorer = self 180 + .explorer 181 + .as_mut() 182 + .expect("This should have already been init.ialized"); 183 + 184 + let task_list = self 185 + .task_list 186 + .as_mut() 187 + .expect("This should have already been initialized"); 188 + let inspector = self 189 + .inspector 190 + .as_mut() 191 + .expect("This should have already been initialized"); 192 + 193 + match signal { 194 + Signal::SwitchTo { 195 + page: Page::Todo(region), 196 + } => { 197 + self.active = region; 198 + match region { 199 + TodoRegion::Inspector => { 200 + inspector.set_active(); 201 + explorer.set_inactive(); 202 + task_list.set_inactive(); 203 + } 204 + TodoRegion::TaskList => { 205 + inspector.set_inactive(); 206 + explorer.set_inactive(); 207 + task_list.set_active(); 208 + } 209 + TodoRegion::Explorer => { 210 + explorer.set_active(); 211 + task_list.set_inactive(); 212 + inspector.set_inactive(); 213 + } 214 + } 215 + 216 + self.update_inspector_from_selection().await; 217 + } 218 + Signal::MoveDown => { 219 + match self.active { 220 + TodoRegion::TaskList => { 221 + task_list.state.select_next(); 222 + } 223 + TodoRegion::Explorer => { 224 + explorer.state.select_next(); 225 + } 226 + TodoRegion::Inspector => { 227 + return Ok(None); 228 + } 229 + } 230 + 231 + self.update_inspector_from_selection().await; 232 + } 233 + 234 + Signal::MoveUp => { 235 + match self.active { 236 + TodoRegion::TaskList => { 237 + task_list.state.select_previous(); 238 + } 239 + TodoRegion::Explorer => { 240 + explorer.state.select_previous(); 241 + } 242 + TodoRegion::Inspector => return Ok(None), 243 + } 244 + 245 + self.update_inspector_from_selection().await; 246 + } 247 + _ => {} 248 + } 48 249 Ok(None) 49 250 } 50 251 51 252 fn draw(&mut self, frame: &mut Frame, area: Rect) -> color_eyre::Result<()> { 52 - frame.render_widget(Block::new().bg(Color::Red), area); 253 + let explorer = self.explorer.as_mut().unwrap(); 254 + let task_list = self.task_list.as_mut().unwrap(); 255 + 256 + let splits = self.layouts.split(area); 53 257 258 + frame.render_stateful_widget(&explorer.render_list, splits.explorer, &mut explorer.state); 259 + frame.render_stateful_widget( 260 + &task_list.render_list, 261 + splits.task_list, 262 + &mut task_list.state, 263 + ); 264 + frame.render_widget(self.inspector.as_ref().unwrap(), splits.inspector); 54 265 Ok(()) 55 266 } 56 267 }
+129
src/tui/components/todo/tasklist.rs
··· 1 + #![expect(dead_code)] 2 + use ratatui::{ 3 + style::{Color, Style}, 4 + text::{Line, Span, Text}, 5 + widgets::{Block, BorderType, Borders, List, ListState}, 6 + }; 7 + use tree::NodeId; 8 + 9 + use crate::types::{TodoNode, TodoNodeKind, TodoTree}; 10 + 11 + pub struct TaskList<'text> { 12 + pub render_list: List<'text>, 13 + pub id_list: Vec<NodeId>, 14 + pub state: ListState, 15 + pub width: u16, 16 + } 17 + 18 + impl TaskList<'_> { 19 + pub fn new(tree: &TodoTree, scope: &NodeId, state: ListState, width: u16) -> Self { 20 + let mut id_list = vec![]; 21 + 22 + let render_list = List::new( 23 + tree.tree 24 + .traverse_pre_order(scope) 25 + .expect("nthis 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 + }; 35 + 36 + let mut tli: TaskListItem<'_> = node.data().into(); 37 + 38 + id_list.push(id); 39 + 40 + tli.width = width; 41 + Some(Text::from(tli)) 42 + }), 43 + ) 44 + .style(Color::White) 45 + .highlight_style(Style::new().on_dark_gray()); 46 + 47 + Self { 48 + render_list, 49 + id_list, 50 + state, 51 + width, 52 + } 53 + } 54 + 55 + pub fn set_active(&mut self) { 56 + self.render_list = self.render_list.clone().block( 57 + Block::new() 58 + .title("[3]") 59 + .title("TaskList") 60 + .borders(Borders::TOP | Borders::LEFT) 61 + .border_style(Style::new().fg(Color::Green)) 62 + .border_type(BorderType::Rounded), 63 + ); 64 + } 65 + 66 + pub fn set_inactive(&mut self) { 67 + self.render_list = self.render_list.clone().block( 68 + Block::new() 69 + .title("[3]") 70 + .title("TaskList") 71 + .borders(Borders::TOP | Borders::LEFT) 72 + .border_style(Style::new().fg(Color::Gray)) 73 + .border_type(BorderType::Rounded), 74 + ); 75 + } 76 + } 77 + 78 + pub struct TaskListItem<'text> { 79 + name: Span<'text>, 80 + group: Span<'text>, 81 + due_priority: Span<'text>, 82 + width: u16, 83 + } 84 + 85 + // its fine because if it fails, its my fault, not the users. 86 + #[expect(clippy::fallible_impl_from)] 87 + impl From<&TodoNode> for TaskListItem<'_> { 88 + fn from(value: &TodoNode) -> Self { 89 + let TodoNodeKind::Task(ref task) = value.kind else { 90 + panic!("Should not be possible"); 91 + }; 92 + 93 + let color = task.group.tag.color; 94 + 95 + let name = Span::from(task.name.clone()).style(Style::new().fg(color.into())); 96 + let group = Span::from(task.group.name.clone()).style(Style::new().fg(color.into())); 97 + let due_priority = task 98 + .due() 99 + .map_or_else(|| Span::from(task.priority.to_string()), Span::from) 100 + .style(Style::new().fg(color.into())); 101 + 102 + Self { 103 + name, 104 + group, 105 + due_priority, 106 + width: 0, 107 + } 108 + } 109 + } 110 + 111 + impl<'text> From<TaskListItem<'text>> for Text<'text> { 112 + fn from(value: TaskListItem<'text>) -> Self { 113 + let total_width = value.width.saturating_sub(2) as usize; 114 + let name_col = total_width / 2; 115 + let due_content = value.due_priority.content.as_ref(); 116 + let due_col = due_content.len(); 117 + let group_col = total_width.saturating_sub(name_col + due_col); 118 + 119 + let name_str = format!("{:<width$}", value.name.content, width = name_col); 120 + let group_str = format!("{:<width$}", value.group.content, width = group_col); 121 + let due_str = format!("{due_content:>due_col$}"); 122 + 123 + let name = Span::styled(name_str, value.name.style); 124 + let group = Span::styled(group_str, value.group.style); 125 + let due = Span::styled(due_str, value.due_priority.style); 126 + 127 + Line::from(vec![name, group, due]).into() 128 + } 129 + }
+22 -23
src/tui/components/viewport/mod.rs
··· 11 11 use crate::{ 12 12 tui::{ 13 13 Signal, 14 - app::Region, 14 + app::Page, 15 15 components::{Component, Todo, Zk}, 16 16 }, 17 17 types::KastenHandle, ··· 23 23 kh: KastenHandle, 24 24 _layouts: Layouts, 25 25 switcher: Switcher<'text>, 26 - active_region: Region, 26 + active_page: Page, 27 27 zk: Zk<'text>, 28 - todo: Todo, 28 + todo: Todo<'text>, 29 29 } 30 30 31 31 mod switcher; ··· 34 34 impl Viewport<'_> { 35 35 pub async fn new(kh: KastenHandle) -> Result<Self> { 36 36 let mut switcher = Switcher::default(); 37 - switcher.select_region(Region::default()); 37 + switcher.select_region(Page::default()); 38 38 39 39 Ok(Self { 40 40 signal_tx: None, ··· 42 42 switcher, 43 43 zk: Zk::new(kh.clone()).await?, 44 44 todo: Todo::new(kh.clone()), 45 - active_region: Region::default(), 45 + active_page: Page::default(), 46 46 kh, 47 47 }) 48 48 } ··· 63 63 #[async_trait] 64 64 impl Component for Viewport<'_> { 65 65 async fn init(&mut self, area: Size) -> color_eyre::Result<()> { 66 - match self.active_region { 67 - Region::Zk => self.zk.init(area).await, 68 - Region::Todo => self.todo.init(area).await, 69 - } 66 + self.zk.init(area).await?; 67 + self.todo.init(area).await?; 68 + Ok(()) 70 69 } 71 70 72 71 fn register_signal_handler(&mut self, tx: UnboundedSender<Signal>) -> Result<()> { ··· 78 77 79 78 async fn update(&mut self, signal: Signal) -> color_eyre::Result<Option<Signal>> { 80 79 // switch active region 81 - if let Signal::SwitchTo { region } = signal { 82 - self.active_region = region; 83 - self.switcher.select_region(region); 84 - debug!("active region switched to : {region}"); 80 + if let Signal::SwitchTo { page } = signal { 81 + self.active_page = page; 82 + self.switcher.select_region(page); 83 + debug!("active page switched to : {page}"); 85 84 } 86 85 87 - match self.active_region { 88 - Region::Zk => self.zk.update(signal).await, 89 - Region::Todo => self.todo.update(signal).await, 86 + match self.active_page { 87 + Page::Zk => self.zk.update(signal).await, 88 + Page::Todo(_) => self.todo.update(signal).await, 90 89 } 91 90 } 92 91 93 92 async fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result<Option<Signal>> { 94 - match self.active_region { 95 - Region::Zk => self.zk.handle_key_event(key).await, 96 - Region::Todo => self.todo.handle_key_event(key).await, 93 + match self.active_page { 94 + Page::Zk => self.zk.handle_key_event(key).await, 95 + Page::Todo(_) => self.todo.handle_key_event(key).await, 97 96 } 98 97 } 99 98 ··· 103 102 // let rects = self.layouts.main_switcher.split(area); 104 103 // (rects[0], rects[1]) 105 104 // }; 105 + // 106 106 107 - match self.active_region { 108 - Region::Zk => self.zk.draw(frame, area), 109 - Region::Todo => self.todo.draw(frame, area), 107 + match self.active_page { 108 + Page::Zk => self.zk.draw(frame, area), 109 + Page::Todo(_) => self.todo.draw(frame, area), 110 110 }?; 111 111 112 - // frame.render_widget(self.switcher.clone(), area); 113 112 Ok(()) 114 113 } 115 114 }
+4 -4
src/tui/components/viewport/switcher.rs
··· 6 6 }; 7 7 use strum::IntoEnumIterator; 8 8 9 - use crate::tui::app::Region; 9 + use crate::tui::app::Page; 10 10 11 11 #[derive(Debug, Clone)] 12 12 pub struct Switcher<'text> { ··· 15 15 } 16 16 17 17 impl Switcher<'_> { 18 - pub fn select_region(&mut self, region: Region) { 19 - self.line = Region::iter() 18 + pub fn select_region(&mut self, region: Page) { 19 + self.line = Page::iter() 20 20 .map(|r| { 21 21 Span::from(format!(" {r} ")).style({ 22 22 if r == region { ··· 45 45 46 46 impl Default for Switcher<'_> { 47 47 fn default() -> Self { 48 - let line = Region::iter() 48 + let line = Page::iter() 49 49 .map(|r| Span::from(format!(" {r} ")).style(Style::default().bg(Color::DarkGray))) 50 50 .collect::<Line>(); 51 51
+2 -2
src/tui/components/zk/mod.rs
··· 72 72 let mut zettels: Vec<Zettel> = fetch_all().await?; 73 73 74 74 if zettels.is_empty() { 75 - let _ = Zettel::new("Welcome!", &mut *kh.write().await).await?; 75 + let _ = Zettel::new("Welcome!", &mut *kh.write().await, vec![]).await?; 76 76 zettels = fetch_all().await?; 77 77 } 78 78 ··· 236 236 let mut kt = self.kh.write().await; 237 237 238 238 // we create the zettel with the query as the 239 - let z = Zettel::new(self.search.query(), &mut kt) 239 + let z = Zettel::new(self.search.query(), &mut kt, vec![]) 240 240 .await 241 241 .with_context(|| "Failed to create a new Zettel!")?; 242 242
+2 -1
src/tui/mod.rs
··· 1 1 /// The tui app 2 2 mod app; 3 3 pub use app::App as TuiApp; 4 - pub use app::Region; 4 + pub use app::Page; 5 + pub use app::TodoRegion; 5 6 6 7 /// Tui components 7 8 mod components;
+2 -2
src/tui/signal.rs
··· 6 6 use serde::{Deserialize, Serialize}; 7 7 8 8 use crate::{ 9 - tui::Region, 9 + tui::Page, 10 10 types::{Link, ZettelId}, 11 11 }; 12 12 ··· 24 24 Help, 25 25 26 26 SwitchTo { 27 - region: Region, 27 + page: Page, 28 28 }, 29 29 30 30 // movement
+1 -1
src/types/color.rs
··· 5 5 6 6 /// Agnostic Color type, 7 7 /// internally represented as rgb 8 - #[derive(Debug, Copy, Clone, Default)] 8 + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Default)] 9 9 pub struct Color(ColorDTO); 10 10 11 11 impl Display for Color {
+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
+27 -5
src/types/group.rs
··· 1 1 use dto::{DateTime, GroupModelEx, NanoId}; 2 2 3 - use crate::types::{Color, Priority, Zettel}; 3 + use crate::types::{Priority, Tag, Zettel, frontmatter}; 4 4 5 5 /// A `Group` which contains tasks! 6 - #[expect(dead_code)] 7 - #[derive(Debug, Clone)] 6 + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 8 7 pub struct Group { 9 8 /// Should only be constructed from models. 10 9 _private: (), 11 10 12 11 pub id: NanoId, 13 12 pub name: String, 14 - pub color: Color, 15 13 pub priority: Priority, 14 + pub parent_id: Option<NanoId>, 16 15 pub created_at: DateTime, 17 16 pub modified_at: DateTime, 18 17 /// The `Zettel` that is related to this `Group`. 19 18 /// Can store notes regarding this group in 20 19 /// the `Zettel` 21 20 pub zettel: Zettel, 21 + 22 + /// The `Tag` that is related to this `Group` 23 + pub tag: Tag, 24 + } 25 + 26 + impl Group { 27 + pub fn created_at(&self) -> String { 28 + self.created_at 29 + .format(frontmatter::DATE_FMT_STR) 30 + .to_string() 31 + } 32 + pub fn modified_at(&self) -> String { 33 + self.modified_at 34 + .format(frontmatter::DATE_FMT_STR) 35 + .to_string() 36 + } 22 37 } 23 38 24 39 impl From<GroupModelEx> for Group { ··· 27 42 _private: (), 28 43 id: value.nano_id, 29 44 name: value.name, 30 - color: value.color.into(), 31 45 priority: value.priority.into(), 46 + parent_id: value.parent_group_id, 32 47 created_at: value.created_at, 33 48 modified_at: value.modified_at, 34 49 zettel: value ··· 38 53 "When fetching a Group from the database, we expect to always have the Zettel loaded!!", 39 54 ) 40 55 .into(), 56 + tag: value 57 + .tag 58 + .into_option() 59 + .expect( 60 + "When fetching a Group from the database, we expect to always have the Tag loaded!!", 61 + ) 62 + .into(), 41 63 } 42 64 } 43 65 }
+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 }
+14 -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 + 16 + mod index; 17 + pub use index::Index; 18 + pub use index::ZettelOnDisk; 19 + mod todo_tree; 20 + 21 + pub use todo_tree::{TodoNode, TodoNodeKind, TodoTree}; 15 22 16 - #[derive(Debug, Clone)] 23 + #[derive(Debug)] 17 24 pub struct Kasten { 18 25 /// Private field so it can only be instantiated from a `Path` 19 26 _private: (), ··· 21 28 pub root: PathBuf, 22 29 23 30 pub index: Index, 31 + 32 + pub todo_tree: TodoTree, 24 33 25 34 pub db: DatabaseConnection, 26 35 } ··· 54 63 // run da migrations every time we connect, just in case 55 64 Migrator::up(&conn, None).await?; 56 65 66 + let todo_tree = TodoTree::construct(&conn).await?; 67 + 57 68 Ok(Self { 58 69 _private: (), 59 70 db: conn, 60 71 root, 61 72 index, 73 + todo_tree, 62 74 }) 63 75 } 64 76
+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 + }
+140
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 + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 13 + pub enum TodoNodeKind { 14 + Root, 15 + Group(Box<Group>), 16 + Task(Box<Task>), 17 + } 18 + 19 + #[derive(Debug, Clone)] 20 + pub struct TodoNode { 21 + pub depth: usize, 22 + pub kind: TodoNodeKind, 23 + } 24 + 25 + impl TodoNode { 26 + pub const fn new(kind: TodoNodeKind, depth: usize) -> Self { 27 + Self { depth, kind } 28 + } 29 + } 30 + 31 + #[derive(Debug)] 32 + pub struct TodoTree { 33 + pub tree: Tree<TodoNode>, 34 + pub nanoid_to_nodeid: HashMap<NanoId, NodeId>, 35 + pub root_id: NodeId, 36 + } 37 + 38 + impl TodoTree { 39 + pub async fn construct(db: &DatabaseConnection) -> Result<Self> { 40 + let mut tree = Tree::<TodoNode>::new(); 41 + let root_id = tree 42 + .insert( 43 + Node::new(TodoNode::new(TodoNodeKind::Root, 0)), 44 + InsertBehavior::AsRoot, 45 + ) 46 + .with_context(|| "Could not create root node.")?; 47 + 48 + let root_groups: Vec<Group> = GroupEntity::load() 49 + .with(TagEntity) 50 + .with(TaskEntity) 51 + .with((ZettelEntity, TagEntity)) 52 + .filter(GroupColumns::ParentGroupId.is_null()) 53 + .all(db) 54 + .await? 55 + .into_iter() 56 + .map(Into::into) 57 + .collect(); 58 + 59 + let mut todo_tree = Self { 60 + tree, 61 + nanoid_to_nodeid: HashMap::new(), 62 + root_id: root_id.clone(), 63 + }; 64 + 65 + for group in root_groups { 66 + todo_tree 67 + .add_group_to_tree(db, &root_id, Box::new(group), 0) 68 + .await?; 69 + } 70 + 71 + Ok(todo_tree) 72 + } 73 + 74 + #[async_recursion::async_recursion] 75 + async fn add_group_to_tree( 76 + &mut self, 77 + db: &DatabaseConnection, 78 + parent_node_id: &NodeId, 79 + group: Box<Group>, 80 + depth: usize, 81 + ) -> Result<()> { 82 + let group_id = group.id.clone(); 83 + 84 + let group_node_id = self.tree.insert( 85 + Node::new(TodoNode::new(TodoNodeKind::Group(group), depth)), 86 + InsertBehavior::UnderNode(parent_node_id), 87 + )?; 88 + 89 + self.nanoid_to_nodeid 90 + .insert(group_id.clone(), group_node_id.clone()); 91 + 92 + let group_model = GroupEntity::load() 93 + .with(TagEntity) 94 + .with((ZettelEntity, TagEntity)) 95 + .filter_by_nano_id(group_id.clone()) 96 + .one(db) 97 + .await? 98 + .expect("We just inserted it"); 99 + 100 + let tasks: Vec<Task> = TaskEntity::load() 101 + .with((ZettelEntity, TagEntity)) 102 + .filter(TaskColumns::GroupId.eq(group_id.clone())) 103 + .all(db) 104 + .await? 105 + .into_iter() 106 + .map(|mut am| { 107 + am.group = dto::HasOne::Loaded(Box::new(group_model.clone())); 108 + am.into() 109 + }) 110 + .collect(); 111 + 112 + for task in tasks { 113 + let task_id = task.id.clone(); 114 + let task_node_id = self.tree.insert( 115 + Node::new(TodoNode::new(TodoNodeKind::Task(Box::new(task)), depth + 1)), 116 + InsertBehavior::UnderNode(&group_node_id), 117 + )?; 118 + 119 + self.nanoid_to_nodeid.insert(task_id, task_node_id); 120 + } 121 + 122 + let children_groups: Vec<Group> = GroupEntity::load() 123 + .with(TagEntity) 124 + .with(TaskEntity) 125 + .with((ZettelEntity, TagEntity)) 126 + .filter(GroupColumns::ParentGroupId.eq(group_id)) 127 + .all(db) 128 + .await? 129 + .into_iter() 130 + .map(Into::into) 131 + .collect(); 132 + 133 + for group in children_groups { 134 + self.add_group_to_tree(db, &group_node_id, Box::new(group), depth + 1) 135 + .await?; 136 + } 137 + 138 + Ok(()) 139 + } 140 + }
+4 -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 + pub use kasten::Index; 31 28 pub use kasten::Kasten; 32 29 pub use kasten::KastenHandle; 30 + pub use kasten::TodoNode; 31 + pub use kasten::TodoNodeKind; 32 + pub use kasten::TodoTree; 33 33 34 34 mod frontmatter; 35 35 pub use frontmatter::FrontMatter;
+19 -4
src/types/priority.rs
··· 1 + use std::fmt::Display; 2 + 1 3 use dto::PriorityDTO; 2 4 3 5 /// An Enum for the various `Priority` levels 4 6 /// for `Task`s and `Group`s 5 - #[expect(dead_code)] 6 - #[derive(Debug, Clone)] 7 - pub struct Priority(PriorityDTO); 7 + #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] 8 + pub struct Priority { 9 + field1: PriorityDTO, 10 + } 8 11 9 12 impl From<PriorityDTO> for Priority { 10 13 fn from(value: PriorityDTO) -> Self { 11 - Self(value) 14 + Self { field1: value } 15 + } 16 + } 17 + 18 + impl From<Priority> for PriorityDTO { 19 + fn from(value: Priority) -> Self { 20 + value.field1 21 + } 22 + } 23 + 24 + impl Display for Priority { 25 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 + write!(f, "{}", self.field1) 12 27 } 13 28 }
+1 -1
src/types/tag.rs
··· 4 4 5 5 /// Represents a `Tag` in a `ZettelKasten` note taking method. 6 6 /// Easy way to link multiple notes under one simple word. 7 - #[derive(Debug, Clone)] 7 + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 8 8 pub struct Tag { 9 9 /// Should only be constructed from models. 10 10 _private: (),
+20 -3
src/types/task.rs
··· 1 1 use dto::{ DateTime, NanoId, TaskModelEx}; 2 2 3 - use crate::types::{Group, Priority, Zettel}; 3 + use crate::types::{Group, Priority, Zettel, frontmatter}; 4 4 5 5 /// a `Task` that you have to complete! 6 - #[expect(dead_code)] 7 - #[derive(Debug, Clone)] 6 + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 8 7 pub struct Task { 9 8 /// Should only be constructed from models. 10 9 _private:(), ··· 13 12 pub name: String, 14 13 pub priority: Priority, 15 14 pub due: Option<DateTime>, 15 + pub group_id: NanoId, 16 16 pub finished_at: Option<DateTime>, 17 17 pub created_at: DateTime, 18 18 pub modified_at: DateTime, ··· 21 21 pub group: Group, 22 22 } 23 23 24 + impl Task { 25 + pub fn due(&self) -> Option<String> { 26 + self.due.map(|due|due.format(frontmatter::DATE_FMT_STR).to_string()) 27 + } 28 + pub fn finished_at(&self) -> Option<String> { 29 + self.finished_at. 30 + map(|finished_at|finished_at.format(frontmatter::DATE_FMT_STR).to_string()) 31 + } 32 + pub fn created_at(&self) -> String { 33 + self.created_at.format(frontmatter::DATE_FMT_STR).to_string() 34 + } 35 + pub fn modified_at(&self) -> String { 36 + self.modified_at.format(frontmatter::DATE_FMT_STR).to_string() 37 + } 38 + } 39 + 24 40 impl From<TaskModelEx> for Task { 25 41 fn from(value: TaskModelEx) -> Self { 26 42 Self { ··· 29 45 name: value.name, 30 46 priority: value.priority.into(), 31 47 due: value.due, 48 + group_id: value.group_id, 32 49 finished_at: value.finished_at, 33 50 created_at: value.created_at, 34 51 modified_at: value.modified_at,
+19 -7
src/types/zettel/mod.rs
··· 19 19 /// A `Zettel` is a note about a single idea. 20 20 /// It can have many `Tag`s, just meaning it can fall under many 21 21 /// categories. 22 - #[derive(Debug, Clone)] 22 + 23 + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 23 24 pub struct Zettel { 24 25 /// Should only be constructed from models. 25 26 _private: (), ··· 41 42 .map(Into::into)) 42 43 } 43 44 44 - pub async fn new(title: impl Into<String>, kt: &mut Kasten) -> Result<Self> { 45 + pub async fn new(title: impl Into<String>, kt: &mut Kasten, tags: Vec<Tag>) -> Result<Self> { 45 46 // fn new(title: impl Into<String>) -> Result<Self> { 46 47 let title = title.into(); 47 48 ··· 60 61 format!("Failed to create file at local file path: {local_file_path}") 61 62 })?; 62 63 63 - let inserted = ZettelActiveModel::builder() 64 - .set_title(title.clone()) 65 - .set_nano_id(nano_id) 66 - .insert(&kt.db) 67 - .await?; 64 + let inserted = { 65 + let mut am = ZettelActiveModel::builder() 66 + .set_title(title.clone()) 67 + .set_nano_id(nano_id); 68 + 69 + for tag in tags { 70 + let tag = TagEntity::load() 71 + .filter_by_nano_id(tag.id) 72 + .one(&kt.db) 73 + .await? 74 + .expect("Invariant broken, tag must exist"); 75 + am = am.add_tag(tag); 76 + } 77 + 78 + am.insert(&kt.db).await? 79 + }; 68 80 69 81 // need to load tags... 70 82 let zettel = ZettelEntity::load()