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: Groups now have correlated tags

+301 -32
+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 + }
+10 -1
crates/dto/src/entity/group.rs
··· 16 16 #[sea_orm(unique)] 17 17 pub nano_id: NanoId, 18 18 pub name: String, 19 - pub color: Color, 20 19 pub priority: Priority, 21 20 pub created_at: DateTime, 22 21 pub modified_at: DateTime, 23 22 #[sea_orm(unique)] 24 23 pub zettel_id: NanoId, 25 24 pub parent_group_id: Option<NanoId>, 25 + #[sea_orm(unique)] 26 + pub tag_id: String, 26 27 #[sea_orm( 27 28 self_ref, 28 29 relation_enum = "SelfRef", ··· 32 33 on_delete = "Cascade" 33 34 )] 34 35 pub group: HasOne<Entity>, 36 + #[sea_orm( 37 + belongs_to, 38 + from = "tag_id", 39 + to = "nano_id", 40 + on_update = "Cascade", 41 + on_delete = "Cascade" 42 + )] 43 + pub tag: HasOne<super::tag::Entity>, 35 44 #[sea_orm(has_many)] 36 45 pub tasks: HasMany<super::task::Entity>, 37 46 #[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 }
+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()),
+19 -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 ··· 29 30 #[command(subcommand)] 30 31 Zettel(ZettelSubcommand), 31 32 33 + #[command(subcommand)] 34 + Todo(TodoSubcommand), 35 + 32 36 /// Spawn the `LSP` 33 37 Lsp, 34 38 ··· 58 62 } 59 63 60 64 #[derive(Subcommand, Debug)] 61 - /// Subcommand to manage tars groups. 65 + /// Subcommand to manage Zettels. 62 66 pub enum ZettelSubcommand { 63 67 /// Add a group. 64 68 New { ··· 71 75 /// Filter by tag 72 76 #[arg(short = 't', long)] 73 77 by_tag: String, 78 + }, 79 + } 80 + 81 + #[derive(Subcommand, Debug)] 82 + /// Subcommand to manage To-Do functionality 83 + pub enum TodoSubcommand { 84 + Group { 85 + /// The name of this group 86 + #[arg(short, long)] 87 + name: String, 88 + 89 + /// If this group has a parent, provide the parent id. 90 + #[arg(short, long)] 91 + parent_id: Option<NanoId>, 74 92 }, 75 93 } 76 94
+64 -2
src/cli/process.rs
··· 5 5 }; 6 6 7 7 use color_eyre::eyre::{Context, Result}; 8 + use dto::{ 9 + GroupActiveModel, GroupEntity, IntoActiveModel, TagActiveModel, TagEntity, ZettelEntity, 10 + }; 8 11 use tower_lsp::{LspService, Server}; 9 12 10 13 use crate::{ 11 14 cli::{Commands, ZettelSubcommand}, 12 15 config::{Config, get_config_dir}, 13 16 lsp::Backend, 14 - types::{Kasten, Zettel}, 17 + types::{Group, Kasten, Priority, Tag, Zettel}, 15 18 }; 16 19 17 20 impl Commands { ··· 50 53 51 54 match zettel_sub_command { 52 55 ZettelSubcommand::New { title } => { 53 - let zettel = Zettel::new(title, &mut kt).await?; 56 + let zettel = Zettel::new(title, &mut kt, vec![]).await?; 54 57 println!("Zettel Created! {zettel:#?}"); 55 58 } 56 59 ZettelSubcommand::List { by_tag: _by_tag } => {} ··· 68 71 let (service, socket) = LspService::new(|client| Backend::new(client, kt)); 69 72 70 73 Server::new(stdin, stdout, socket).serve(service).await; 74 + } 75 + 76 + Self::Todo(command) => { 77 + let conf = Config::parse()?; 78 + let mut kt = Kasten::instansiate(conf.fil_dir).await?; 79 + 80 + match command { 81 + super::TodoSubcommand::Group { name, parent_id } => { 82 + // lets create a tag for this first group first 83 + 84 + let tag: Tag = TagActiveModel::builder() 85 + .set_name(name.clone()) 86 + .insert(&kt.db) 87 + .await? 88 + .into(); 89 + 90 + //TODO: this zettel would need to be created with the parent of all 91 + // of its groups? 92 + let tag_id = tag.id.clone(); 93 + 94 + let zettel = Zettel::new(name.clone(), &mut kt, vec![tag]).await?; 95 + 96 + let inserted = GroupActiveModel::builder() 97 + .set_name(name) 98 + .set_parent_group_id(parent_id) 99 + .set_tag( 100 + TagEntity::load() 101 + .filter_by_nano_id(tag_id) 102 + .one(&kt.db) 103 + .await? 104 + .expect("Tag must exist since we just created it") 105 + .into_active_model(), 106 + ) 107 + .set_zettel( 108 + ZettelEntity::load() 109 + // .with(TagEntity) 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 + } 71 133 } 72 134 } 73 135
+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
+11 -3
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}; 4 4 5 5 /// A `Group` which contains tasks! 6 6 #[expect(dead_code)] ··· 11 11 12 12 pub id: NanoId, 13 13 pub name: String, 14 - pub color: Color, 15 14 pub priority: Priority, 16 15 pub created_at: DateTime, 17 16 pub modified_at: DateTime, ··· 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, 22 24 } 23 25 24 26 impl From<GroupModelEx> for Group { ··· 27 29 _private: (), 28 30 id: value.nano_id, 29 31 name: value.name, 30 - color: value.color.into(), 31 32 priority: value.priority.into(), 32 33 created_at: value.created_at, 33 34 modified_at: value.modified_at, ··· 38 39 "When fetching a Group from the database, we expect to always have the Zettel loaded!!", 39 40 ) 40 41 .into(), 42 + tag: value 43 + .tag 44 + .into_option() 45 + .expect( 46 + "When fetching a Group from the database, we expect to always have the Zettel loaded!!", 47 + ) 48 + .into(), 41 49 } 42 50 } 43 51 }
+11 -4
src/types/priority.rs
··· 2 2 3 3 /// An Enum for the various `Priority` levels 4 4 /// for `Task`s and `Group`s 5 - #[expect(dead_code)] 6 - #[derive(Debug, Clone)] 7 - pub struct Priority(PriorityDTO); 5 + #[derive(Debug, Clone, Default)] 6 + pub struct Priority { 7 + field1: PriorityDTO, 8 + } 8 9 9 10 impl From<PriorityDTO> for Priority { 10 11 fn from(value: PriorityDTO) -> Self { 11 - Self(value) 12 + Self { field1: value } 13 + } 14 + } 15 + 16 + impl From<Priority> for PriorityDTO { 17 + fn from(value: Priority) -> Self { 18 + value.field1 12 19 } 13 20 }
+17 -6
src/types/zettel/mod.rs
··· 41 41 .map(Into::into)) 42 42 } 43 43 44 - pub async fn new(title: impl Into<String>, kt: &mut Kasten) -> Result<Self> { 44 + pub async fn new(title: impl Into<String>, kt: &mut Kasten, tags: Vec<Tag>) -> Result<Self> { 45 45 // fn new(title: impl Into<String>) -> Result<Self> { 46 46 let title = title.into(); 47 47 ··· 60 60 format!("Failed to create file at local file path: {local_file_path}") 61 61 })?; 62 62 63 - let inserted = ZettelActiveModel::builder() 64 - .set_title(title.clone()) 65 - .set_nano_id(nano_id) 66 - .insert(&kt.db) 67 - .await?; 63 + let inserted = { 64 + let mut am = ZettelActiveModel::builder() 65 + .set_title(title.clone()) 66 + .set_nano_id(nano_id); 67 + 68 + for tag in tags { 69 + let tag = TagEntity::load() 70 + .filter_by_nano_id(tag.id) 71 + .one(&kt.db) 72 + .await? 73 + .expect("Invariant broken, tag must exist"); 74 + am = am.add_tag(tag); 75 + } 76 + 77 + am.insert(&kt.db).await? 78 + }; 68 79 69 80 // need to load tags... 70 81 let zettel = ZettelEntity::load()