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 #5 from suri-codes/zettel

Zettel implementation

authored by

Surendra Jammishetti and committed by
GitHub
672a537e e3deba4b

+428 -44
+1
Cargo.lock
··· 2240 2240 version = "0.1.0" 2241 2241 dependencies = [ 2242 2242 "anyhow", 2243 + "async-trait", 2243 2244 "better-panic", 2244 2245 "clap", 2245 2246 "color-eyre",
+1
Cargo.toml
··· 74 74 kdl = "6.5.0" 75 75 dto = {path="./crates/dto"} 76 76 eframe = "0.34.1" 77 + async-trait = "0.1.89" 77 78 78 79 [build-dependencies] 79 80 anyhow = "1.0.102"
+2 -2
crates/dto/migration/src/m20260318_233726_group_table.rs
··· 31 31 .not_null() 32 32 .default(Priority::default().to_string()), 33 33 ) 34 - .col(timestamp(Group::CreatedAt).default(Expr::current_timestamp())) 35 - .col(timestamp(Group::ModifiedAt).default(Expr::current_timestamp())) 34 + .col(date_time(Group::CreatedAt).default(Expr::current_timestamp())) 35 + .col(date_time(Group::ModifiedAt).default(Expr::current_timestamp())) 36 36 .col(string(Group::ZettelId).not_null().unique_key()) 37 37 // foreign key for the zettel related to this group 38 38 .foreign_key(
+4 -4
crates/dto/migration/src/m20260319_002245_task_table.rs
··· 30 30 .not_null() 31 31 .default(Priority::default().to_string()), 32 32 ) 33 - .col(timestamp(Task::Due).null()) 34 - .col(timestamp(Task::FinishedAt).null()) 35 - .col(timestamp(Task::CreatedAt).default(Expr::current_timestamp())) 36 - .col(timestamp(Task::ModifiedAt).default(Expr::current_timestamp())) 33 + .col(date_time(Task::Due).null()) 34 + .col(date_time(Task::FinishedAt).null()) 35 + .col(date_time(Task::CreatedAt).default(Expr::current_timestamp())) 36 + .col(date_time(Task::ModifiedAt).default(Expr::current_timestamp())) 37 37 .col(string(Task::ZettelId).not_null().unique_key()) 38 38 // foreign key for the zettel related to this task 39 39 .foreign_key(
+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(date_time(Zettel::CreatedAt).default(Expr::current_timestamp())) 26 + .col(date_time(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 }
+14 -3
crates/dto/src/entity/group.rs
··· 1 1 //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 2 2 3 + use migration::prelude::Local; 3 4 use migration::types::*; 5 + use sea_orm::ActiveValue::Set; 4 6 use sea_orm::entity::prelude::*; 5 - use sea_orm::ActiveValue::Set; 6 7 use std::future::ready; 7 8 use std::pin::Pin; 8 9 ··· 17 18 pub name: String, 18 19 pub color: Color, 19 20 pub priority: Priority, 20 - pub created_at: DateTimeUtc, 21 - pub modified_at: DateTimeUtc, 21 + pub created_at: DateTime, 22 + pub modified_at: DateTime, 22 23 #[sea_orm(unique)] 23 24 pub zettel_id: NanoId, 24 25 pub parent_group_id: Option<NanoId>, ··· 54 55 Self: Send + 'async_trait, 55 56 'life0: 'async_trait, 56 57 { 58 + let now = Local::now().naive_local(); 59 + 60 + // set modified at 61 + self.modified_at = Set(now); 62 + 63 + // we set the timestamp to non-utc 64 + if insert && self.created_at.is_not_set() { 65 + self.created_at = Set(now); 66 + } 67 + 57 68 if insert && self.nano_id.is_not_set() { 58 69 self.nano_id = Set(NanoId::default()); 59 70 }
+1 -1
crates/dto/src/entity/prelude.rs
··· 1 1 //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 2 - #![expect(unused_imports)] 3 2 3 + #![expect(unused_imports)] 4 4 pub use super::group::Entity as Group; 5 5 pub use super::tag::Entity as Tag; 6 6 pub use super::task::Entity as Task;
+16 -5
crates/dto/src/entity/task.rs
··· 1 1 //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 2 2 3 + use migration::prelude::Local; 3 4 use migration::types::*; 5 + use sea_orm::ActiveValue::Set; 4 6 use sea_orm::entity::prelude::*; 5 - use sea_orm::ActiveValue::Set; 6 7 use std::future::ready; 7 8 use std::pin::Pin; 8 9 ··· 16 17 pub nano_id: NanoId, 17 18 pub name: String, 18 19 pub priority: Priority, 19 - pub due: Option<DateTimeUtc>, 20 - pub finished_at: Option<DateTimeUtc>, 21 - pub created_at: DateTimeUtc, 22 - pub modified_at: DateTimeUtc, 20 + pub due: Option<DateTime>, 21 + pub finished_at: Option<DateTime>, 22 + pub created_at: DateTime, 23 + pub modified_at: DateTime, 23 24 #[sea_orm(unique)] 24 25 pub zettel_id: NanoId, 25 26 pub group_id: NanoId, ··· 52 53 Self: Send + 'async_trait, 53 54 'life0: 'async_trait, 54 55 { 56 + let now = Local::now().naive_local(); 57 + 58 + // set modified at 59 + self.modified_at = Set(now); 60 + 61 + // we set the timestamp to non-utc 62 + if insert && self.created_at.is_not_set() { 63 + self.created_at = Set(now); 64 + } 65 + 55 66 if insert && self.nano_id.is_not_set() { 56 67 self.nano_id = Set(NanoId::default()); 57 68 }
+14 -2
crates/dto/src/entity/zettel.rs
··· 1 1 //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 2 2 3 - use migration::types::*; 4 - use sea_orm::entity::prelude::*; 3 + use migration::{prelude::Local, types::*}; 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 pub nano_id: NanoId, 17 17 pub title: String, 18 18 pub file_path: String, 19 + pub created_at: DateTime, 20 + pub modified_at: DateTime, 19 21 #[sea_orm(has_one)] 20 22 pub group: HasOne<super::group::Entity>, 21 23 #[sea_orm(has_one)] ··· 35 37 Self: Send + 'async_trait, 36 38 'life0: 'async_trait, 37 39 { 40 + let now = Local::now().naive_local(); 41 + 42 + // set modified at 43 + self.modified_at = Set(now); 44 + 45 + // we set the timestamp to non-utc 46 + if insert && self.created_at.is_not_set() { 47 + self.created_at = Set(now); 48 + } 49 + 38 50 if insert && self.nano_id.is_not_set() { 39 51 self.nano_id = Set(NanoId::default()); 40 52 }
+1 -1
crates/dto/src/lib.rs
··· 15 15 /// and add additional functionality to it. 16 16 pub use migration::types::Priority as PriorityDTO; 17 17 18 - pub use sea_orm::entity::prelude::DateTimeUtc; 18 + pub use sea_orm::entity::prelude::DateTime; 19 19 20 20 /// Color type, exporting as DTO because I might 21 21 /// want to newtype wrap this, might not have to, depending
+7 -1
crates/dto/tests/common/mod.rs
··· 3 3 path::PathBuf, 4 4 }; 5 5 6 + use dto::{Migrator, MigratorTrait}; 6 7 use rand::RngExt; 7 8 use sea_orm::{Database, DatabaseConnection}; 8 9 ··· 26 27 path.clone().canonicalize().unwrap().to_string_lossy() 27 28 ); 28 29 29 - Database::connect(db_conn_string).await.unwrap() 30 + let conn = Database::connect(db_conn_string).await.unwrap(); 31 + 32 + // run da migrations every time we connect, just in case 33 + Migrator::up(&conn, None).await.unwrap(); 34 + 35 + conn 30 36 }
+22
src/cli/mod.rs
··· 25 25 26 26 #[derive(Subcommand, Debug)] 27 27 pub enum Commands { 28 + /// Manage `Zettel`s 29 + #[command(subcommand)] 30 + Zettel(ZettelSubcommand), 31 + 28 32 // / Manage TARS groups. 29 33 // #[command(subcommand)] 30 34 // Group(GroupSubcommand), ··· 50 54 // Import(ImportArgs), 51 55 } 52 56 57 + #[derive(Subcommand, Debug)] 58 + /// Subcommand to manage tars groups. 59 + pub enum ZettelSubcommand { 60 + /// Add a group. 61 + New { 62 + #[arg(short, long)] 63 + /// The file-path for data to pe put into. 64 + title: String, 65 + }, 66 + /// List groups. 67 + List { 68 + /// Filter by tag 69 + #[arg(short = 't', long)] 70 + by_tag: String, 71 + }, 72 + } 73 + 53 74 // #[derive(Subcommand, Debug)] 54 75 // /// Subcommand to manage tars groups. 55 76 // pub enum GroupSubcommand { ··· 65 86 // /// The file-path for data to pe put into. 66 87 // pub out_file: PathBuf, 67 88 // } 89 + // 68 90 69 91 const VERSION_MESSAGE: &str = concat!( 70 92 env!("CARGO_PKG_VERSION"),
+15 -2
src/cli/process.rs
··· 7 7 use color_eyre::eyre::{Context, Result}; 8 8 9 9 use crate::{ 10 - cli::Commands, 10 + cli::{Commands, ZettelSubcommand}, 11 11 config::{Config, get_config_dir}, 12 - types::Workspace, 12 + types::{Workspace, Zettel}, 13 13 }; 14 14 15 15 impl Commands { ··· 40 40 41 41 // report status! 42 42 println!("Initialized successfully!"); 43 + } 44 + 45 + Self::Zettel(zettel_sub_command) => { 46 + let conf = Config::parse()?; 47 + let ws = Workspace::instansiate(conf.app_config.workspace).await?; 48 + 49 + match zettel_sub_command { 50 + ZettelSubcommand::New { title } => { 51 + let zettel = Zettel::new(title, &ws).await?; 52 + println!("Zettel Created! {zettel:#?}"); 53 + } 54 + ZettelSubcommand::List { by_tag: _by_tag } => {} 55 + } 43 56 } 44 57 } 45 58
+2 -3
src/config.rs
··· 38 38 #[expect(dead_code)] 39 39 pub struct AppConfig { 40 40 /// The directory where the single instance of the filaments exists. 41 - pub filaments: PathBuf, 41 + pub workspace: PathBuf, 42 42 #[serde(default)] 43 43 pub data: PathBuf, 44 44 #[serde(default)] ··· 46 46 } 47 47 48 48 /// Configuration for the App 49 - #[expect(dead_code)] 50 49 #[derive(Debug, Clone)] 51 50 pub struct Config { 52 51 pub app_config: AppConfig, ··· 114 113 115 114 Ok(Self { 116 115 app_config: AppConfig { 117 - filaments: filaments_dir, 116 + workspace: filaments_dir, 118 117 data: get_data_dir(), 119 118 config: get_config_dir(), 120 119 },
+6 -5
src/tui/app.rs
··· 10 10 11 11 use crate::{ 12 12 config::Config, 13 - tui::{Event, Tui}, 13 + tui::{Event, Tui, components::Zk}, 14 14 }; 15 15 16 16 use super::{components::Component, signal::Signal}; ··· 38 38 pub enum Region { 39 39 #[default] 40 40 Home, 41 + Zk, 41 42 } 42 43 43 44 impl App { ··· 48 49 Ok(Self { 49 50 tick_rate, 50 51 frame_rate, 51 - components: vec![], 52 + components: vec![Box::new(Zk::new())], 52 53 should_quit: false, 53 54 should_suspend: false, 54 55 config: Config::parse()?, ··· 80 81 81 82 loop { 82 83 self.handle_events(&mut tui).await?; 83 - self.handle_signals(&mut tui)?; 84 + self.handle_signals(&mut tui).await?; 84 85 if self.should_suspend { 85 86 tui.suspend()?; 86 87 ··· 150 151 Ok(()) 151 152 } 152 153 153 - fn handle_signals(&mut self, tui: &mut Tui) -> Result<()> { 154 + async fn handle_signals(&mut self, tui: &mut Tui) -> Result<()> { 154 155 while let Ok(signal) = self.signal_rx.try_recv() { 155 156 if signal != Signal::Tick && signal != Signal::Render { 156 157 debug!("handling signal: {signal:?}"); ··· 193 194 } 194 195 195 196 for component in &mut self.components { 196 - if let Some(signal) = component.update(signal.clone())? { 197 + if let Some(signal) = component.update(signal.clone()).await? { 197 198 self.signal_tx.send(signal)?; 198 199 } 199 200 }
+10 -2
src/tui/components/mod.rs
··· 1 + use async_trait::async_trait; 1 2 use crossterm::event::{KeyEvent, MouseEvent}; 2 3 use ratatui::{ 3 4 Frame, ··· 10 11 tui::{Event, Signal}, 11 12 }; 12 13 14 + /// The zk component 15 + mod zk; 16 + 17 + pub use zk::*; 18 + 13 19 /// `Component` is a trait that represents a visual and interactive element of the user interface. 14 20 /// 15 21 /// Implementers of this trait can be registered with the main application loop and will be able to 16 22 /// receive events, update state, and be rendered on the screen. 17 - pub trait Component: Send { 23 + /// 24 + #[async_trait] 25 + pub trait Component: Send + Sync { 18 26 /// Register a signal handler that can send signals for processing if necessary. 19 27 /// 20 28 /// # Arguments ··· 106 114 /// # Returns 107 115 /// 108 116 /// * [`color_eyre::Result<Option<signal>>`] - A signal to be processed or none. 109 - fn update(&mut self, signal: Signal) -> color_eyre::Result<Option<Signal>>; 117 + async fn update(&mut self, signal: Signal) -> color_eyre::Result<Option<Signal>>; 110 118 111 119 /// Render the component on the screen. (REQUIRED) 112 120 ///
+52
src/tui/components/zk/mod.rs
··· 1 + use async_trait::async_trait; 2 + use color_eyre::eyre::Result; 3 + use ratatui::widgets::Paragraph; 4 + use tokio::sync::mpsc::UnboundedSender; 5 + 6 + use crate::tui::{Signal, components::Component}; 7 + 8 + mod zettel; 9 + 10 + pub struct Zk { 11 + signal_tx: Option<UnboundedSender<Signal>>, 12 + } 13 + 14 + impl Zk { 15 + pub const fn new() -> Self { 16 + Self { signal_tx: None } 17 + } 18 + } 19 + 20 + #[async_trait] 21 + impl Component for Zk { 22 + fn register_signal_handler(&mut self, tx: UnboundedSender<Signal>) -> Result<()> { 23 + self.signal_tx = Some(tx); 24 + Ok(()) 25 + } 26 + 27 + async fn update(&mut self, _signal: Signal) -> Result<Option<crate::tui::Signal>> { 28 + // match signal { 29 + // Signal::Tick => todo!(), 30 + // Signal::Render => todo!(), 31 + // Signal::Resize(_, _) => todo!(), 32 + // Signal::Suspend => todo!(), 33 + // Signal::Resume => todo!(), 34 + // Signal::Quit => todo!(), 35 + // Signal::ClearScreen => todo!(), 36 + // Signal::Error(_) => todo!(), 37 + // Signal::Help => todo!(), 38 + // Signal::Helix => todo!(), 39 + // } 40 + Ok(None) 41 + } 42 + 43 + fn draw( 44 + &mut self, 45 + frame: &mut ratatui::Frame, 46 + area: ratatui::prelude::Rect, 47 + ) -> color_eyre::Result<()> { 48 + let hello = Paragraph::new("Hello Surendra"); 49 + frame.render_widget(hello, area); 50 + Ok(()) 51 + } 52 + }
src/tui/components/zk/zettel.rs

This is a binary file and will not be displayed.

+161
src/types/frontmatter.rs
··· 1 + use std::{fmt::Display, path::Path}; 2 + 3 + // use chrono::format::StrftimeItems; 4 + use color_eyre::eyre::{Result, eyre}; 5 + use serde::{Deserialize, Serialize}; 6 + use tokio::fs; 7 + 8 + use dto::DateTime; 9 + 10 + const DATE_FMT_STR: &str = "%Y-%m-%d %I:%M:%S %p"; 11 + 12 + #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)] 13 + pub struct FrontMatter { 14 + pub title: String, 15 + pub created_at: DateTime, 16 + pub tag_strings: Vec<String>, 17 + } 18 + 19 + impl FrontMatter { 20 + pub fn new( 21 + title: impl Into<String>, 22 + created_at: DateTime, 23 + tag_strings: Vec<impl Into<String>>, 24 + ) -> Self { 25 + let tag_strings = tag_strings.into_iter().map(Into::into).collect(); 26 + 27 + Self { 28 + title: title.into(), 29 + created_at, 30 + tag_strings, 31 + } 32 + } 33 + 34 + /// Reads in file and returns the front matter as well as the content after it. 35 + /// expected format for front matter as follows 36 + ///```md 37 + /// --- 38 + /// Title: LOL 39 + /// Date: 2025-01-01 12:50:19 AM 40 + /// Tags: Daily barber 41 + /// --- 42 + /// ``` 43 + pub async fn extract_from_file(path: impl AsRef<Path>) -> Result<(Self, String)> { 44 + let path = path.as_ref(); 45 + let string = fs::read_to_string(path).await?; 46 + Self::extract_from_str(&string) 47 + } 48 + 49 + /// Returns the front matter as well as the content after it. 50 + /// expected format for front matter as follows 51 + ///```md 52 + /// --- 53 + /// Title: LOL 54 + /// Date: 2025-01-01 12:50:19 AM 55 + /// Tags: Daily barber 56 + /// --- 57 + /// ``` 58 + pub fn extract_from_str(string: impl Into<String>) -> Result<(Self, String)> { 59 + let string: String = string.into(); 60 + // we just want to strictly match this, else we error 61 + 62 + let lines: Vec<_> = string.lines().collect(); 63 + 64 + let delim_check = |line_number: usize| -> Result<()> { 65 + let delim = lines 66 + .get(line_number) 67 + .ok_or_else(|| eyre!(format!("Line Number {line_number} doesnt exist!")))? 68 + .trim(); 69 + if delim != "---" { 70 + return Err(eyre!("FrontMatter Deliminator Corrupted!")); 71 + } 72 + Ok(()) 73 + }; 74 + 75 + // check first line 76 + delim_check(0)?; 77 + 78 + //extract name 79 + let title = lines 80 + .get(1) 81 + .ok_or_else(|| eyre!("Title line doesn't exist!".to_owned()))? 82 + .strip_prefix("Title: ") 83 + .ok_or_else(|| eyre!("Title line doesn't start with \"Title: \" ".to_owned(),))?; 84 + 85 + let created_at = lines 86 + .get(2) 87 + .ok_or_else(|| eyre!("Date line doesn't exist!".to_owned()))? 88 + .strip_prefix("Date: ") 89 + .ok_or_else(|| eyre!("Date line doesn't start with \"Date: \" ".to_owned(),)) 90 + .map(|date_str| DateTime::parse_from_str(date_str, DATE_FMT_STR))? 91 + .map_err(|err| eyre!(err.to_string()))?; 92 + 93 + let tag_strings: Vec<String> = lines 94 + .get(3) 95 + .ok_or_else(|| eyre!("Tag line doesn't exist!".to_owned()))? 96 + .strip_prefix("Tags: ") 97 + .ok_or_else(|| eyre!("Tag line doesn't start with \"Tags: \" ".to_owned(),))? 98 + .split_whitespace() 99 + .map(ToOwned::to_owned) 100 + .collect::<Vec<_>>(); 101 + 102 + delim_check(4)?; 103 + 104 + let remaining = lines[5..].join("\n"); 105 + 106 + Ok((Self::new(title, created_at, tag_strings), remaining)) 107 + } 108 + } 109 + 110 + impl Display for FrontMatter { 111 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 112 + writeln!(f, "---")?; 113 + writeln!(f, "Title: {}", self.title)?; 114 + writeln!(f, "Date: {}", self.created_at.format(DATE_FMT_STR))?; 115 + write!(f, "Tags: ")?; 116 + 117 + for tag in &self.tag_strings { 118 + write!(f, "{tag} ")?; 119 + } 120 + 121 + writeln!(f, "\n---") 122 + } 123 + } 124 + 125 + #[cfg(test)] 126 + mod tests { 127 + 128 + use dto::DateTime; 129 + 130 + use crate::types::{FrontMatter, frontmatter::DATE_FMT_STR}; 131 + 132 + // use crate::{FrontMatter, zettel::frontmatter::DATE_FMT_STR}; 133 + 134 + #[test] 135 + fn test_extract_from_string() { 136 + let test_suite: [(&'static str, (FrontMatter, &'static str)); 1] = [( 137 + r"--- 138 + Title: LOL 139 + Date: 2025-01-01 12:50:19 AM 140 + Tags: whoa barber 141 + --- 142 + ", 143 + ( 144 + FrontMatter::new( 145 + "LOL", 146 + DateTime::parse_from_str("2025-01-01 12:50:19 AM", DATE_FMT_STR).unwrap(), 147 + vec!["whoa", "barber"], 148 + ), 149 + "", 150 + ), 151 + )]; 152 + 153 + for (raw_text, (front_matter, remaining)) in &test_suite { 154 + let (extracted_front_matter, extracted_remaining) = 155 + FrontMatter::extract_from_str(*raw_text).unwrap(); 156 + 157 + assert_eq!(extracted_front_matter, *front_matter); 158 + assert_eq!(extracted_remaining, *remaining); 159 + } 160 + } 161 + }
+3 -3
src/types/group.rs
··· 1 - use dto::{DateTimeUtc, GroupModelEx, NanoId}; 1 + use dto::{DateTime, GroupModelEx, NanoId}; 2 2 3 3 use crate::types::{Color, Priority, Zettel}; 4 4 ··· 13 13 pub name: String, 14 14 pub color: Color, 15 15 pub priority: Priority, 16 - pub created_at: DateTimeUtc, 17 - pub modified_at: DateTimeUtc, 16 + pub created_at: DateTime, 17 + pub modified_at: DateTime, 18 18 /// The `Zettel` that is related to this `Group`. 19 19 /// Can store notes regarding this group in 20 20 /// the `Zettel`
+3
src/types/mod.rs
··· 19 19 20 20 mod workspace; 21 21 pub use workspace::Workspace; 22 + 23 + mod frontmatter; 24 + pub use frontmatter::FrontMatter;
+5 -5
src/types/task.rs
··· 1 - use dto::{DateTimeUtc, NanoId, TaskModelEx}; 1 + use dto::{ DateTime, NanoId, TaskModelEx}; 2 2 3 3 use crate::types::{Group, Priority, Zettel}; 4 4 ··· 12 12 pub id: NanoId, 13 13 pub name: String, 14 14 pub priority: Priority, 15 - pub due: Option<DateTimeUtc>, 16 - pub finished_at: Option<DateTimeUtc>, 17 - pub created_at: DateTimeUtc, 18 - pub modified_at: DateTimeUtc, 15 + pub due: Option<DateTime>, 16 + pub finished_at: Option<DateTime>, 17 + pub created_at: DateTime, 18 + pub modified_at: DateTime, 19 19 /// Each task has its own related `Zettel`. 20 20 pub zettel: Zettel, 21 21 pub group: Group,
+4 -2
src/types/workspace.rs
··· 1 1 use std::path::PathBuf; 2 2 3 3 use color_eyre::eyre::{Context, Result}; 4 - use dto::{Database, DatabaseConnection}; 4 + use dto::{Database, DatabaseConnection, Migrator, MigratorTrait}; 5 5 use tokio::fs::{File, create_dir_all}; 6 6 use tracing::debug; 7 7 8 8 /// The `Workspace` in which the filaments exist. 9 - #[expect(dead_code)] 10 9 #[derive(Debug, Clone)] 11 10 pub struct Workspace { 12 11 /// Private field so it can only be instantiated from a `Path` ··· 39 38 let conn = Database::connect(db_conn_string) 40 39 .await 41 40 .context("Failed to connect to the database in the filaments workspace!")?; 41 + 42 + // run da migrations every time we connect, just in case 43 + Migrator::up(&conn, None).await?; 42 44 43 45 Ok(Self { 44 46 _private: (),
+76 -3
src/types/zettel.rs
··· 1 + use dto::{DateTime, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx}; 1 2 use std::path::PathBuf; 2 3 3 - use dto::{NanoId, ZettelModelEx}; 4 + use color_eyre::eyre::Result; 5 + use dto::NanoId; 6 + use tokio::{fs::File, io::AsyncWriteExt}; 4 7 5 - use crate::types::Tag; 8 + use crate::types::{FrontMatter, Tag, Workspace}; 6 9 7 10 /// A `Zettel` is a note about a single idea. 8 11 /// It can have many `Tag`s, just meaning it can fall under many ··· 12 15 pub struct Zettel { 13 16 /// Should only be constructed from models. 14 17 _private: (), 15 - 16 18 pub id: NanoId, 17 19 pub title: String, 18 20 /// a workspace-local file path, needs to be canonicalized before usage 19 21 pub file_path: PathBuf, 22 + pub created_at: DateTime, 20 23 pub tags: Vec<Tag>, 21 24 } 22 25 26 + impl Zettel { 27 + pub async fn new(title: impl Into<String>, ws: &Workspace) -> Result<Self> { 28 + // fn new(title: impl Into<String>) -> Result<Self> { 29 + let title = title.into(); 30 + 31 + // make a file that has a random identifier, and then 32 + // also has the name "title" 33 + let nano_id = NanoId::default(); 34 + 35 + let local_file_path = format!("{nano_id}.md"); 36 + 37 + // now we have to create the file 38 + let mut file = File::create_new(ws.root.clone().join(&local_file_path)).await?; 39 + 40 + let inserted = ZettelActiveModel::builder() 41 + .set_title(title.clone()) 42 + .set_file_path(local_file_path) 43 + .set_nano_id(nano_id) 44 + .insert(&ws.db) 45 + .await?; 46 + 47 + // need to load tags... 48 + let zettel = ZettelEntity::load() 49 + .filter_by_nano_id(inserted.nano_id) 50 + .with(TagEntity) 51 + .one(&ws.db) 52 + .await? 53 + .expect("This must exist since we just inserted it"); 54 + 55 + let front_matter = FrontMatter::new( 56 + title, 57 + zettel.created_at, 58 + zettel.tags.iter().map(|t| t.name.clone()).collect(), 59 + ); 60 + 61 + file.write_all(front_matter.to_string().as_bytes()).await?; 62 + 63 + Ok(zettel.into()) 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 + } 93 + } 94 + 23 95 impl From<ZettelModelEx> for Zettel { 24 96 fn from(value: ZettelModelEx) -> Self { 25 97 assert!( ··· 33 105 id: value.nano_id, 34 106 title: value.title, 35 107 file_path: value.file_path.into(), 108 + created_at: value.created_at, 36 109 tags: value.tags.into_iter().map(Into::into).collect(), 37 110 } 38 111 }