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: show Color field

+252 -47
+1
Cargo.lock
··· 4392 4392 dependencies = [ 4393 4393 "async-std", 4394 4394 "nanoid", 4395 + "rand 0.10.0", 4395 4396 "rgb", 4396 4397 "sea-orm", 4397 4398 "sea-orm-migration",
+2
crates/dto/migration/Cargo.toml
··· 13 13 nanoid = "0.4.0" 14 14 sea-orm = "2.0.0-rc" 15 15 rgb = "0.8.53" 16 + 16 17 serde = {workspace = true} 18 + rand = "0.10.0" 17 19 18 20 [dependencies.sea-orm-migration] 19 21 version = "2.0.0-rc"
+12
crates/dto/migration/src/types/color.rs
··· 1 + use rand::RngExt; 1 2 use rgb::RGB8; 2 3 use sea_orm::DeriveValueType; 3 4 use std::fmt::{Debug, Display}; ··· 49 50 c.to_rgb8() 50 51 } 51 52 } 53 + 54 + impl Default for Color { 55 + fn default() -> Self { 56 + let mut rng = rand::rng(); 57 + let r = rng.random_range(0..=255); 58 + let g = rng.random_range(0..=255); 59 + let b = rng.random_range(0..=255); 60 + 61 + Self::new(r, g, b) 62 + } 63 + }
+6 -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] ··· 33 33 if insert && self.nano_id.is_not_set() { 34 34 self.nano_id = Set(NanoId::default()); 35 35 } 36 + 37 + if insert && self.color.is_not_set() { 38 + self.color = Set(Color::default()) 39 + } 40 + 36 41 Box::pin(ready(Ok(self))) 37 42 } 38 43 }
+1 -1
src/tui/app.rs
··· 77 77 } 78 78 79 79 for component in &mut self.components { 80 - component.init(tui.size()?)?; 80 + component.init(tui.size()?).await?; 81 81 } 82 82 83 83 let signal_tx = self.signal_tx.clone();
+1 -1
src/tui/components/mod.rs
··· 58 58 /// # Returns 59 59 /// 60 60 /// * [`color_eyre::Result<()>`] - An Ok result or an error. 61 - fn init(&mut self, area: Size) -> color_eyre::Result<()> { 61 + async fn init(&mut self, area: Size) -> color_eyre::Result<()> { 62 62 let _ = area; // to appease clippy 63 63 Ok(()) 64 64 }
+41 -34
src/tui/components/zk/mod.rs
··· 2 2 use color_eyre::eyre::Result; 3 3 use ratatui::{ 4 4 prelude::*, 5 - widgets::{Block, List, ListState}, 5 + widgets::{Block, ListState}, 6 6 }; 7 7 use tokio::sync::mpsc::UnboundedSender; 8 8 9 9 use crate::{ 10 10 tui::{Signal, components::Component}, 11 - types::{KastenHandle, ZettelId}, 11 + types::KastenHandle, 12 12 }; 13 13 14 14 mod preview; 15 + mod zettel_list; 15 16 mod zettel_view; 16 17 17 18 use preview::Preview; 19 + use zettel_list::ZettelList; 18 20 use zettel_view::ZettelView; 19 21 22 + /// in theory we could do some fancy `type_state` encoding stuff 23 + /// to make this work cleanly (so we know when the widgets are properly 24 + /// initialized) 20 25 pub struct Zk<'text> { 21 26 signal_tx: Option<UnboundedSender<Signal>>, 22 27 kh: KastenHandle, ··· 32 37 z_preview: Layout, 33 38 } 34 39 35 - struct ZettelList<'text> { 36 - render_list: List<'text>, 37 - id_list: Vec<ZettelId>, 38 - state: ListState, 39 - } 40 - 41 40 impl Default for Layouts { 42 41 fn default() -> Self { 43 42 Self { ··· 63 62 64 63 let nodes = kt.graph.nodes_iter().collect::<Vec<_>>(); 65 64 66 - let zettel_list = { 67 - let render_list = List::new(nodes.iter().map(|(_, n)| { 68 - let z = n.payload(); 69 - let title = z.title.clone(); 70 - let _tags = z.tags.clone(); 71 - // let _last_modified = z.modified_at; 72 - Text::from(title) 73 - })) 74 - .style(Color::White) 75 - .highlight_style(Modifier::REVERSED) 76 - .highlight_symbol("> "); 77 - 78 - let id_list = nodes 79 - .iter() 80 - .map(|(_, n)| n.payload().id.clone()) 81 - .collect::<Vec<_>>(); 82 - 83 - let mut state = ListState::default(); 84 - state.select_first(); 85 - 86 - ZettelList { 87 - render_list, 88 - id_list, 89 - state, 90 - } 91 - }; 65 + // in theory this is wasted compute, we should be initializing all our 66 + // stuff inside the init function 67 + let mut l_state = ListState::default(); 68 + l_state.select_first(); 69 + let zettel_list = ZettelList::new(nodes.as_slice(), l_state, 0); 92 70 93 71 let selected_zettel = zettel_list 94 72 .id_list ··· 167 145 168 146 #[async_trait] 169 147 impl Component for Zk<'_> { 148 + /// this tells us how big the space we have for this is 149 + async fn init(&mut self, area: Size) -> color_eyre::Result<()> { 150 + let total_width = area.width; 151 + 152 + let kt = self.kh.read().await; 153 + 154 + let nodes = kt.graph.nodes_iter().collect::<Vec<_>>(); 155 + 156 + let mut l_state = ListState::default(); 157 + l_state.select_first(); 158 + 159 + let zettel_list = ZettelList::new(nodes.as_slice(), l_state, total_width / 2); 160 + 161 + self.zettel_list = zettel_list; 162 + 163 + drop(kt); 164 + 165 + Ok(()) 166 + } 167 + 170 168 fn register_signal_handler(&mut self, tx: UnboundedSender<Signal>) -> Result<()> { 171 169 self.signal_tx = Some(tx); 172 170 Ok(()) ··· 223 221 .get_node_by_zettel_id(id) 224 222 .expect("Invariant broken, this must exist."); 225 223 224 + //TODO: we would actually want to create the new zettel_list_item 225 + // and insert it into the list where the selected id is there, that 226 + // is what makes the most sense imo 227 + self.zettel_list = ZettelList::new( 228 + // ideally we dont want to do this right? 229 + kh.graph.nodes_iter().collect::<Vec<_>>().as_slice(), 230 + self.zettel_list.state, 231 + self.zettel_list.width, 232 + ); 226 233 self.zettel_view = ZettelView::from(node.payload()); 227 234 self.preview = Preview::from(node.payload().content(&kh.ws).await?); 228 235 drop(kh);
+92
src/tui/components/zk/zettel_list.rs
··· 1 + use egui_graphs::{Node, petgraph::graph::NodeIndex}; 2 + use ratatui::{ 3 + style::{Color, Modifier, Style}, 4 + text::{Line, Span, Text}, 5 + widgets::{List, ListState}, 6 + }; 7 + 8 + use crate::types::{Link, Zettel, ZettelId}; 9 + 10 + pub struct ZettelList<'text> { 11 + pub render_list: ratatui::widgets::List<'text>, 12 + pub id_list: Vec<ZettelId>, 13 + pub state: ListState, 14 + pub width: u16, 15 + } 16 + 17 + pub struct ZettelListItem<'text> { 18 + title: Span<'text>, 19 + tags: Vec<Span<'text>>, 20 + date: Span<'text>, 21 + width: u16, 22 + } 23 + 24 + impl From<&Zettel> for ZettelListItem<'_> { 25 + fn from(value: &Zettel) -> Self { 26 + Self { 27 + title: Span::from(value.title.clone()), 28 + tags: value 29 + .tags 30 + .iter() 31 + .map(|t| { 32 + Span::from(format!("{} ", t.name)) 33 + .style(Style::new().fg(t.color.into()).italic()) 34 + }) 35 + .collect(), 36 + date: Span::from(value.created_at()), 37 + width: 0, 38 + } 39 + } 40 + } 41 + 42 + impl<'item, 'text> From<ZettelListItem<'item>> for Text<'text> 43 + where 44 + 'item: 'text, 45 + { 46 + fn from(value: ZettelListItem<'item>) -> Self { 47 + let title_width: usize = value.title.width(); 48 + let tags_width: usize = value.tags.iter().map(Span::width).sum(); 49 + let date_width: usize = value.date.width(); 50 + 51 + // tags start 2 tabs after the title 52 + let gap_after_title = 2; 53 + let used = title_width + gap_after_title + tags_width + date_width; 54 + let padding = (value.width as usize).saturating_sub(used); 55 + 56 + let line = std::iter::once(value.title) 57 + .chain(std::iter::once(Span::raw(" "))) 58 + .chain(value.tags) 59 + .chain(std::iter::once(Span::raw(" ".repeat(padding)))) 60 + .chain(std::iter::once(value.date)) 61 + .collect::<Line>(); 62 + 63 + line.into() 64 + } 65 + } 66 + 67 + impl ZettelList<'_> { 68 + pub fn new(nodes: &[(NodeIndex, &Node<Zettel, Link>)], state: ListState, width: u16) -> Self { 69 + let render_list = List::new(nodes.iter().map(|(_, n)| { 70 + let z = n.payload(); 71 + let mut zli: ZettelListItem<'_> = z.into(); 72 + zli.width = width; 73 + 74 + Text::from(zli) 75 + })) 76 + .style(Color::White) 77 + .highlight_style(Modifier::REVERSED) 78 + .highlight_symbol("> "); 79 + 80 + let id_list = nodes 81 + .iter() 82 + .map(|(_, n)| n.payload().id.clone()) 83 + .collect::<Vec<_>>(); 84 + 85 + ZettelList { 86 + render_list, 87 + id_list, 88 + state, 89 + width, 90 + } 91 + } 92 + }
+10 -2
src/types/color.rs
··· 1 1 use dto::ColorDTO; 2 + use ratatui::style::Color as RatColor; 2 3 3 4 /// Agnostic Color type, 4 5 /// internally represented as rgb 5 - #[expect(dead_code)] 6 - #[derive(Debug, Clone)] 6 + #[derive(Debug, Copy, Clone, Default)] 7 7 pub struct Color(ColorDTO); 8 8 9 9 impl From<ColorDTO> for Color { ··· 11 11 Self(value) 12 12 } 13 13 } 14 + 15 + impl From<Color> for RatColor { 16 + fn from(value: Color) -> Self { 17 + let rgb = value.0.to_rgb8(); 18 + 19 + Self::Rgb(rgb.r, rgb.g, rgb.b) 20 + } 21 + }
-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 - #[expect(dead_code)] 8 7 #[derive(Debug, Clone)] 9 8 pub struct Tag { 10 9 /// Should only be constructed from models.
+86 -7
src/types/zettel.rs
··· 1 1 use dto::{ 2 2 ActiveModelTrait, ActiveValue, ColumnTrait, DateTime, EntityTrait as _, IntoActiveModel, 3 - QueryFilter, TagActiveModel, TagEntity, ZettelActiveModel, ZettelActiveModelEx, ZettelEntity, 3 + QueryFilter, TagActiveModel, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModel, 4 4 ZettelModelEx, ZettelTagActiveModel, ZettelTagColumns, ZettelTagEntity, 5 5 }; 6 6 use pulldown_cmark::{Event, Parser, Tag as MkTag}; ··· 15 15 use dto::NanoId; 16 16 use tokio::{fs::File, io::AsyncWriteExt}; 17 17 18 - use crate::types::{FrontMatter, Kasten, Link, Tag, Workspace, frontmatter}; 18 + use crate::types::{FrontMatter, Link, Tag, Workspace, frontmatter}; 19 19 20 20 /// A `Zettel` is a note about a single idea. 21 21 /// It can have many `Tag`s, just meaning it can fall under many ··· 80 80 } 81 81 82 82 pub async fn sync_with_file(&mut self, ws: &Workspace) -> Result<()> { 83 - let (fm, content) = FrontMatter::extract_from_file(self.absolute_path(ws)).await?; 83 + let (fm, _) = FrontMatter::extract_from_file(self.absolute_path(ws)).await?; 84 + 85 + let mut model = ZettelEntity::find_by_nano_id(self.id.clone()) 86 + .one(&ws.db) 87 + .await? 88 + .expect("this must exist") 89 + .into_active_model(); 90 + 91 + model.title = ActiveValue::Set(fm.title); 92 + 93 + let updated: ZettelModel = model.update(&ws.db).await?; 94 + 95 + self.title = updated.title; 96 + self.modified_at = updated.modified_at; 97 + self.created_at = updated.created_at; 98 + 99 + self.sync_tags(ws).await?; 100 + 101 + Ok(()) 102 + } 103 + 104 + /// Sync's `Tag`'s that are present in the frontmatter of this 105 + /// `Zettel` to the database, and then updates the `Tag`s on the 106 + /// `Zettel` to reflect the changes. 107 + pub async fn sync_tags(&mut self, ws: &Workspace) -> Result<()> { 108 + let mut fm = self.front_matter(ws).await?; 109 + fm.tag_strings.sort(); 110 + 111 + let mut tag_strings = fm.tag_strings; 112 + 113 + let Some(db_zettel): Option<ZettelModelEx> = ZettelEntity::load() 114 + .with(TagEntity) 115 + .filter_by_nano_id(self.id.clone()) 116 + .one(&ws.db) 117 + .await? 118 + else { 119 + panic!("how the fuck was this deleted"); 120 + }; 121 + 122 + for db_tag in db_zettel.tags { 123 + if let Ok(idx) = tag_strings.binary_search(&db_tag.name) { 124 + // we remove tags we have already processed 125 + tag_strings.remove(idx); 126 + } else { 127 + // the db says the file has tag `x`, but that tag is missing from the 128 + // front matter, we can assume its gone, lets delete that link 129 + let to_remove = ZettelTagEntity::find() 130 + .filter(ZettelTagColumns::ZettelNanoId.eq(self.id.0.clone())) 131 + .filter(ZettelTagColumns::TagNanoId.eq(db_tag.nano_id)) 132 + .one(&ws.db) 133 + .await? 134 + .expect("this link must exist"); 84 135 85 - self.title = fm.title; 136 + to_remove.into_active_model().delete(&ws.db).await?; 137 + } 138 + } 86 139 87 - // it could have new tags and stuff... 140 + // now any tags that are left inside zettel_tag_strings, 141 + // we have to put them inside the db 142 + for new_tag in tag_strings { 143 + // create a new tag 144 + let tag = TagActiveModel { 145 + name: ActiveValue::Set(new_tag), 146 + ..Default::default() 147 + } 148 + .insert(&ws.db) 149 + .await?; 88 150 89 - // todo!(); 151 + // this zettel has this tag now 152 + let _ = ZettelTagActiveModel { 153 + zettel_nano_id: ActiveValue::Set(self.id.to_string()), 154 + tag_nano_id: ActiveValue::Set(tag.nano_id.to_string()), 155 + } 156 + .insert(&ws.db) 157 + .await?; 158 + } 159 + 160 + let entity = ZettelEntity::load() 161 + .with(TagEntity) 162 + .filter_by_nano_id(self.id.clone()) 163 + .one(&ws.db) 164 + .await? 165 + .expect("this exists"); 166 + 167 + let temp_zettel: Self = entity.into(); 168 + 169 + self.tags = temp_zettel.tags; 90 170 91 171 Ok(()) 92 172 } ··· 118 198 } 119 199 120 200 /// uses the id and root to parse out of the root directory 121 - #[expect(dead_code)] 122 201 pub async fn from_id(id: &ZettelId, ws: &Workspace) -> Result<Self> { 123 202 let mut path = ws.root.clone(); 124 203 path.push(id.0.to_string());