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: frontmatter integration!

+223 -9
+3
Cargo.lock
··· 1210 1210 checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" 1211 1211 dependencies = [ 1212 1212 "iana-time-zone", 1213 + "js-sys", 1213 1214 "num-traits", 1214 1215 "serde", 1216 + "wasm-bindgen", 1215 1217 "windows-link 0.2.1", 1216 1218 ] 1217 1219 ··· 2241 2243 dependencies = [ 2242 2244 "anyhow", 2243 2245 "better-panic", 2246 + "chrono", 2244 2247 "clap", 2245 2248 "color-eyre", 2246 2249 "crossterm",
+1
Cargo.toml
··· 74 74 kdl = "6.5.0" 75 75 dto = {path="./crates/dto"} 76 76 eframe = "0.34.1" 77 + chrono = {version="0.4.41", features=["serde"]} 77 78 78 79 [build-dependencies] 79 80 anyhow = "1.0.102"
+8
crates/dto/migration/src/m20260323_002518_zettel_table.rs
··· 22 22 ) 23 23 .col(string(Zettel::Title).not_null()) 24 24 .col(string(Zettel::FilePath).not_null()) 25 + .col(timestamp(Zettel::CreatedAt).default(Expr::current_timestamp())) 26 + .col(timestamp(Zettel::ModifiedAt).default(Expr::current_timestamp())) 25 27 .to_owned(), 26 28 ) 27 29 .await?; ··· 63 65 64 66 /// local file path to this `Zettel` 65 67 FilePath, 68 + 69 + /// Creation time 70 + CreatedAt, 71 + 72 + /// Last modified 73 + ModifiedAt, 66 74 }
+2
crates/dto/src/entity/zettel.rs
··· 16 16 pub nano_id: NanoId, 17 17 pub title: String, 18 18 pub file_path: String, 19 + pub created_at: DateTimeUtc, 20 + pub modified_at: DateTimeUtc, 19 21 #[sea_orm(has_one)] 20 22 pub group: HasOne<super::group::Entity>, 21 23 #[sea_orm(has_one)]
+164
src/types/frontmatter.rs
··· 1 + use std::{fmt::Display, path::Path}; 2 + 3 + use chrono::{NaiveDateTime, format::StrftimeItems}; 4 + use color_eyre::eyre::{Result, eyre}; 5 + use serde::{Deserialize, Serialize}; 6 + use tokio::fs; 7 + 8 + const DATE_FMT_STR: &str = "%Y-%m-%d %I:%M:%S %p"; 9 + 10 + #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)] 11 + pub struct FrontMatter { 12 + pub title: String, 13 + pub created_at: NaiveDateTime, 14 + pub tag_strings: Vec<String>, 15 + } 16 + 17 + impl FrontMatter { 18 + pub fn new( 19 + title: impl Into<String>, 20 + created_at: NaiveDateTime, 21 + tag_strings: Vec<impl Into<String>>, 22 + ) -> Self { 23 + let tag_strings = tag_strings.into_iter().map(Into::into).collect(); 24 + 25 + Self { 26 + title: title.into(), 27 + created_at, 28 + tag_strings, 29 + } 30 + } 31 + 32 + /// Reads in file and returns the front matter as well as the content after it. 33 + /// expected format for front matter as follows 34 + ///```md 35 + /// --- 36 + /// Title: LOL 37 + /// Date: 2025-01-01 12:50:19 AM 38 + /// Tags: Daily barber 39 + /// --- 40 + /// ``` 41 + pub async fn extract_from_file(path: impl AsRef<Path>) -> Result<(Self, String)> { 42 + let path = path.as_ref(); 43 + let string = fs::read_to_string(path).await?; 44 + Self::extract_from_str(&string) 45 + } 46 + 47 + /// Returns the front matter as well as the content after it. 48 + /// expected format for front matter as follows 49 + ///```md 50 + /// --- 51 + /// Title: LOL 52 + /// Date: 2025-01-01 12:50:19 AM 53 + /// Tags: Daily barber 54 + /// --- 55 + /// ``` 56 + pub fn extract_from_str(string: impl Into<String>) -> Result<(Self, String)> { 57 + let string: String = string.into(); 58 + // we just want to strictly match this, else we error 59 + 60 + let lines: Vec<_> = string.lines().collect(); 61 + 62 + let delim_check = |line_number: usize| -> Result<()> { 63 + let delim = lines 64 + .get(line_number) 65 + .ok_or_else(|| eyre!(format!("Line Number {line_number} doesnt exist!")))? 66 + .trim(); 67 + if delim != "---" { 68 + return Err(eyre!("FrontMatter Deliminator Corrupted!")); 69 + } 70 + Ok(()) 71 + }; 72 + 73 + // check first line 74 + delim_check(0)?; 75 + 76 + //extract name 77 + let title = lines 78 + .get(1) 79 + .ok_or_else(|| eyre!("Title line doesn't exist!".to_owned()))? 80 + .strip_prefix("Title: ") 81 + .ok_or_else(|| eyre!("Title line doesn't start with \"Title: \" ".to_owned(),))?; 82 + 83 + let created_at = lines 84 + .get(2) 85 + .ok_or_else(|| eyre!("Date line doesn't exist!".to_owned()))? 86 + .strip_prefix("Date: ") 87 + .ok_or_else(|| eyre!("Date line doesn't start with \"Date: \" ".to_owned(),)) 88 + .map(|date_str| NaiveDateTime::parse_from_str(date_str, DATE_FMT_STR))? 89 + .map_err(|err| eyre!(err.to_string()))?; 90 + 91 + let tag_strings: Vec<String> = lines 92 + .get(3) 93 + .ok_or_else(|| eyre!("Tag line doesn't exist!".to_owned()))? 94 + .strip_prefix("Tags: ") 95 + .ok_or_else(|| eyre!("Tag line doesn't start with \"Tags: \" ".to_owned(),))? 96 + .split_whitespace() 97 + .map(ToOwned::to_owned) 98 + .collect::<Vec<_>>(); 99 + 100 + delim_check(4)?; 101 + 102 + let remaining = lines[5..].join("\n"); 103 + 104 + Ok((Self::new(title, created_at, tag_strings), remaining)) 105 + } 106 + } 107 + 108 + impl Display for FrontMatter { 109 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 110 + let date_fmt_items = StrftimeItems::new(DATE_FMT_STR); 111 + writeln!(f, "---")?; 112 + writeln!(f, "Title: {}", self.title)?; 113 + writeln!( 114 + f, 115 + "Date: {}", 116 + self.created_at.format_with_items(date_fmt_items) 117 + )?; 118 + write!(f, "Tags: ")?; 119 + 120 + for tag in &self.tag_strings { 121 + write!(f, "{tag} ")?; 122 + } 123 + 124 + writeln!(f, "\n---") 125 + } 126 + } 127 + 128 + #[cfg(test)] 129 + mod tests { 130 + 131 + use chrono::NaiveDateTime; 132 + 133 + use crate::types::{FrontMatter, frontmatter::DATE_FMT_STR}; 134 + 135 + // use crate::{FrontMatter, zettel::frontmatter::DATE_FMT_STR}; 136 + 137 + #[test] 138 + fn test_extract_from_string() { 139 + let test_suite: [(&'static str, (FrontMatter, &'static str)); 1] = [( 140 + r"--- 141 + Title: LOL 142 + Date: 2025-01-01 12:50:19 AM 143 + Tags: whoa barber 144 + --- 145 + ", 146 + ( 147 + FrontMatter::new( 148 + "LOL", 149 + NaiveDateTime::parse_from_str("2025-01-01 12:50:19 AM", DATE_FMT_STR).unwrap(), 150 + vec!["whoa", "barber"], 151 + ), 152 + "", 153 + ), 154 + )]; 155 + 156 + for (raw_text, (front_matter, remaining)) in &test_suite { 157 + let (extracted_front_matter, extracted_remaining) = 158 + FrontMatter::extract_from_str(*raw_text).unwrap(); 159 + 160 + assert_eq!(extracted_front_matter, *front_matter); 161 + assert_eq!(extracted_remaining, *remaining); 162 + } 163 + } 164 + }
+2 -3
src/types/mod.rs
··· 20 20 mod workspace; 21 21 pub use workspace::Workspace; 22 22 23 - 24 - 25 - 23 + mod frontmatter; 24 + pub use frontmatter::FrontMatter;
+43 -6
src/types/zettel.rs
··· 1 - use dto::{TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx}; 1 + use dto::{DateTimeUtc, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx}; 2 2 use std::path::PathBuf; 3 3 4 4 use color_eyre::eyre::Result; 5 5 use dto::NanoId; 6 - use tokio::fs::File; 6 + use tokio::{fs::File, io::AsyncWriteExt}; 7 7 8 - use crate::types::{Tag, Workspace}; 8 + use crate::types::{FrontMatter, Tag, Workspace}; 9 9 10 10 /// A `Zettel` is a note about a single idea. 11 11 /// It can have many `Tag`s, just meaning it can fall under many ··· 15 15 pub struct Zettel { 16 16 /// Should only be constructed from models. 17 17 _private: (), 18 - 19 18 pub id: NanoId, 20 19 pub title: String, 21 20 /// a workspace-local file path, needs to be canonicalized before usage 22 21 pub file_path: PathBuf, 22 + pub created_at: DateTimeUtc, 23 23 pub tags: Vec<Tag>, 24 24 } 25 25 ··· 35 35 let local_file_path = format!("{nano_id}.md"); 36 36 37 37 // now we have to create the file 38 - File::create_new(ws.root.clone().join(&local_file_path)).await?; 38 + let mut file = File::create_new(ws.root.clone().join(&local_file_path)).await?; 39 39 40 40 let inserted = ZettelActiveModel::builder() 41 - .set_title(title) 41 + .set_title(title.clone()) 42 42 .set_file_path(local_file_path) 43 43 .set_nano_id(nano_id) 44 44 .insert(&ws.db) ··· 52 52 .await? 53 53 .expect("This must exist since we just inserted it"); 54 54 55 + let front_matter = FrontMatter::new( 56 + title, 57 + zettel.created_at.naive_local(), 58 + zettel.tags.iter().map(|t| t.name.clone()).collect(), 59 + ); 60 + 61 + file.write_all(front_matter.to_string().as_bytes()).await?; 62 + 55 63 Ok(zettel.into()) 56 64 } 65 + 66 + /// Returns the most up-to-date `FrontMatter` for this 67 + /// `Zettel` 68 + #[expect(dead_code)] 69 + pub async fn front_matter(&self, ws: &Workspace) -> Result<FrontMatter> { 70 + let path = self.absolute_path(ws); 71 + let (fm, _) = FrontMatter::extract_from_file(path).await?; 72 + Ok(fm) 73 + } 74 + 75 + /// Returns the content of this `Zettel`, which is everything 76 + /// but the `FrontMatter` 77 + #[expect(dead_code)] 78 + pub async fn content(&self, ws: &Workspace) -> Result<String> { 79 + let path = self.absolute_path(ws); 80 + let (_, content) = FrontMatter::extract_from_file(path).await?; 81 + Ok(content) 82 + } 83 + 84 + #[expect(dead_code)] 85 + async fn open_file(&self, ws: &Workspace) -> Result<File> { 86 + let path = self.absolute_path(ws); 87 + Ok(File::open(path).await?) 88 + } 89 + 90 + fn absolute_path(&self, ws: &Workspace) -> PathBuf { 91 + ws.root.clone().join(&self.file_path) 92 + } 57 93 } 58 94 59 95 impl From<ZettelModelEx> for Zettel { ··· 69 105 id: value.nano_id, 70 106 title: value.title, 71 107 file_path: value.file_path.into(), 108 + created_at: value.created_at, 72 109 tags: value.tags.into_iter().map(Into::into).collect(), 73 110 } 74 111 }