From 5d1f00e4d10ed04a70ab5d73e6e40fa326b40c42 Mon Sep 17 00:00:00 2001 From: suri-codes Date: Sat, 28 Mar 2026 14:27:40 -0700 Subject: [PATCH 1/4] feat: create new zettel --- crates/dto/tests/common/mod.rs | 8 ++++++- src/cli/mod.rs | 22 +++++++++++++++++++ src/cli/process.rs | 17 +++++++++++++-- src/config.rs | 5 ++--- src/types/mod.rs | 4 ++++ src/types/workspace.rs | 6 +++-- src/types/zettel.rs | 40 ++++++++++++++++++++++++++++++++-- 7 files changed, 92 insertions(+), 10 deletions(-) diff --git a/crates/dto/tests/common/mod.rs b/crates/dto/tests/common/mod.rs index 21f8f96..a8092ad 100644 --- a/crates/dto/tests/common/mod.rs +++ b/crates/dto/tests/common/mod.rs @@ -3,6 +3,7 @@ use std::{ path::PathBuf, }; +use dto::{Migrator, MigratorTrait}; use rand::RngExt; use sea_orm::{Database, DatabaseConnection}; @@ -26,5 +27,10 @@ pub async fn fresh_test_db() -> DatabaseConnection { path.clone().canonicalize().unwrap().to_string_lossy() ); - Database::connect(db_conn_string).await.unwrap() + let conn = Database::connect(db_conn_string).await.unwrap(); + + // run da migrations every time we connect, just in case + Migrator::up(&conn, None).await.unwrap(); + + conn } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 54fd221..5119690 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -25,6 +25,10 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Commands { + /// Manage `Zettel`s + #[command(subcommand)] + Zettel(ZettelSubcommand), + // / Manage TARS groups. // #[command(subcommand)] // Group(GroupSubcommand), @@ -50,6 +54,23 @@ pub enum Commands { // Import(ImportArgs), } +#[derive(Subcommand, Debug)] +/// Subcommand to manage tars groups. +pub enum ZettelSubcommand { + /// Add a group. + New { + #[arg(short, long)] + /// The file-path for data to pe put into. + title: String, + }, + /// List groups. + List { + /// The file-path for data to pe put into. + #[arg(short = 't', long)] + by_tag: String, + }, +} + // #[derive(Subcommand, Debug)] // /// Subcommand to manage tars groups. // pub enum GroupSubcommand { @@ -65,6 +86,7 @@ pub enum Commands { // /// The file-path for data to pe put into. // pub out_file: PathBuf, // } +// const VERSION_MESSAGE: &str = concat!( env!("CARGO_PKG_VERSION"), diff --git a/src/cli/process.rs b/src/cli/process.rs index f4514f7..7cf43f6 100644 --- a/src/cli/process.rs +++ b/src/cli/process.rs @@ -7,9 +7,9 @@ use std::{ use color_eyre::eyre::{Context, Result}; use crate::{ - cli::Commands, + cli::{Commands, ZettelSubcommand}, config::{Config, get_config_dir}, - types::Workspace, + types::{Workspace, Zettel}, }; impl Commands { @@ -41,6 +41,19 @@ impl Commands { // report status! println!("Initialized successfully!"); } + + Self::Zettel(zettel_sub_command) => { + let conf = Config::parse()?; + let ws = Workspace::instansiate(conf.app_config.workspace).await?; + + match zettel_sub_command { + ZettelSubcommand::New { title } => { + let zettel = Zettel::new(title, &ws).await?; + println!("Zettel Created! {zettel:#?}"); + } + ZettelSubcommand::List { by_tag: _by_tag } => {} + } + } } Ok(()) diff --git a/src/config.rs b/src/config.rs index 709143d..843fbbe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -38,7 +38,7 @@ const DEFAULT_CONFIG: &str = include_str!("../.config/config.kdl"); #[expect(dead_code)] pub struct AppConfig { /// The directory where the single instance of the filaments exists. - pub filaments: PathBuf, + pub workspace: PathBuf, #[serde(default)] pub data: PathBuf, #[serde(default)] @@ -46,7 +46,6 @@ pub struct AppConfig { } /// Configuration for the App -#[expect(dead_code)] #[derive(Debug, Clone)] pub struct Config { pub app_config: AppConfig, @@ -114,7 +113,7 @@ impl Config { Ok(Self { app_config: AppConfig { - filaments: filaments_dir, + workspace: filaments_dir, data: get_data_dir(), config: get_config_dir(), }, diff --git a/src/types/mod.rs b/src/types/mod.rs index d809ec6..3a14ad4 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -19,3 +19,7 @@ pub use task::Task; mod workspace; pub use workspace::Workspace; + + + + diff --git a/src/types/workspace.rs b/src/types/workspace.rs index 1141682..b209546 100644 --- a/src/types/workspace.rs +++ b/src/types/workspace.rs @@ -1,12 +1,11 @@ use std::path::PathBuf; use color_eyre::eyre::{Context, Result}; -use dto::{Database, DatabaseConnection}; +use dto::{Database, DatabaseConnection, Migrator, MigratorTrait}; use tokio::fs::{File, create_dir_all}; use tracing::debug; /// The `Workspace` in which the filaments exist. -#[expect(dead_code)] #[derive(Debug, Clone)] pub struct Workspace { /// Private field so it can only be instantiated from a `Path` @@ -40,6 +39,9 @@ impl Workspace { .await .context("Failed to connect to the database in the filaments workspace!")?; + // run da migrations every time we connect, just in case + Migrator::up(&conn, None).await?; + Ok(Self { _private: (), db: conn, diff --git a/src/types/zettel.rs b/src/types/zettel.rs index ea48014..3463b85 100644 --- a/src/types/zettel.rs +++ b/src/types/zettel.rs @@ -1,8 +1,11 @@ +use dto::{TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx}; use std::path::PathBuf; -use dto::{NanoId, ZettelModelEx}; +use color_eyre::eyre::Result; +use dto::NanoId; +use tokio::fs::File; -use crate::types::Tag; +use crate::types::{Tag, Workspace}; /// A `Zettel` is a note about a single idea. /// It can have many `Tag`s, just meaning it can fall under many @@ -20,6 +23,39 @@ pub struct Zettel { pub tags: Vec, } +impl Zettel { + pub async fn new(title: impl Into, ws: &Workspace) -> Result { + // fn new(title: impl Into) -> Result { + let title = title.into(); + + // make a file that has a random identifier, and then + // also has the name "title" + let nano_id = NanoId::default(); + + let local_file_path = format!("{nano_id}.md"); + + // now we have to create the file + File::create_new(ws.root.clone().join(&local_file_path)).await?; + + let inserted = ZettelActiveModel::builder() + .set_title(title) + .set_file_path(local_file_path) + .set_nano_id(nano_id) + .insert(&ws.db) + .await?; + + // need to load tags... + let zettel = ZettelEntity::load() + .filter_by_nano_id(inserted.nano_id) + .with(TagEntity) + .one(&ws.db) + .await? + .expect("This must exist since we just inserted it"); + + Ok(zettel.into()) + } +} + impl From for Zettel { fn from(value: ZettelModelEx) -> Self { assert!( -- 2.51.2 From 219ec4ed994a3d15ec38553350cf65737a700e17 Mon Sep 17 00:00:00 2001 From: suri-codes Date: Sat, 28 Mar 2026 23:03:09 -0700 Subject: [PATCH 2/4] feat: frontmatter integration! --- Cargo.lock | 3 + Cargo.toml | 1 + .../src/m20260323_002518_zettel_table.rs | 8 + crates/dto/src/entity/zettel.rs | 2 + src/types/frontmatter.rs | 164 ++++++++++++++++++ src/types/mod.rs | 5 +- src/types/zettel.rs | 49 +++++- 7 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 src/types/frontmatter.rs diff --git a/Cargo.lock b/Cargo.lock index 39a5a5d..d1b1e86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1210,8 +1210,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -2241,6 +2243,7 @@ version = "0.1.0" dependencies = [ "anyhow", "better-panic", + "chrono", "clap", "color-eyre", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index 9122607..c4cf730 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ clap = { version = "4.5.60", features = ["derive", "cargo", "wrap_help", "unicod kdl = "6.5.0" dto = {path="./crates/dto"} eframe = "0.34.1" +chrono = {version="0.4.41", features=["serde"]} [build-dependencies] anyhow = "1.0.102" diff --git a/crates/dto/migration/src/m20260323_002518_zettel_table.rs b/crates/dto/migration/src/m20260323_002518_zettel_table.rs index b2c3522..cd31e8d 100644 --- a/crates/dto/migration/src/m20260323_002518_zettel_table.rs +++ b/crates/dto/migration/src/m20260323_002518_zettel_table.rs @@ -22,6 +22,8 @@ impl MigrationTrait for Migration { ) .col(string(Zettel::Title).not_null()) .col(string(Zettel::FilePath).not_null()) + .col(timestamp(Zettel::CreatedAt).default(Expr::current_timestamp())) + .col(timestamp(Zettel::ModifiedAt).default(Expr::current_timestamp())) .to_owned(), ) .await?; @@ -63,4 +65,10 @@ pub enum Zettel { /// local file path to this `Zettel` FilePath, + + /// Creation time + CreatedAt, + + /// Last modified + ModifiedAt, } diff --git a/crates/dto/src/entity/zettel.rs b/crates/dto/src/entity/zettel.rs index 0ed74f9..10cd5e9 100644 --- a/crates/dto/src/entity/zettel.rs +++ b/crates/dto/src/entity/zettel.rs @@ -16,6 +16,8 @@ pub struct Model { pub nano_id: NanoId, pub title: String, pub file_path: String, + pub created_at: DateTimeUtc, + pub modified_at: DateTimeUtc, #[sea_orm(has_one)] pub group: HasOne, #[sea_orm(has_one)] diff --git a/src/types/frontmatter.rs b/src/types/frontmatter.rs new file mode 100644 index 0000000..f23fed2 --- /dev/null +++ b/src/types/frontmatter.rs @@ -0,0 +1,164 @@ +use std::{fmt::Display, path::Path}; + +use chrono::{NaiveDateTime, format::StrftimeItems}; +use color_eyre::eyre::{Result, eyre}; +use serde::{Deserialize, Serialize}; +use tokio::fs; + +const DATE_FMT_STR: &str = "%Y-%m-%d %I:%M:%S %p"; + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)] +pub struct FrontMatter { + pub title: String, + pub created_at: NaiveDateTime, + pub tag_strings: Vec, +} + +impl FrontMatter { + pub fn new( + title: impl Into, + created_at: NaiveDateTime, + tag_strings: Vec>, + ) -> Self { + let tag_strings = tag_strings.into_iter().map(Into::into).collect(); + + Self { + title: title.into(), + created_at, + tag_strings, + } + } + + /// Reads in file and returns the front matter as well as the content after it. + /// expected format for front matter as follows + ///```md + /// --- + /// Title: LOL + /// Date: 2025-01-01 12:50:19 AM + /// Tags: Daily barber + /// --- + /// ``` + pub async fn extract_from_file(path: impl AsRef) -> Result<(Self, String)> { + let path = path.as_ref(); + let string = fs::read_to_string(path).await?; + Self::extract_from_str(&string) + } + + /// Returns the front matter as well as the content after it. + /// expected format for front matter as follows + ///```md + /// --- + /// Title: LOL + /// Date: 2025-01-01 12:50:19 AM + /// Tags: Daily barber + /// --- + /// ``` + pub fn extract_from_str(string: impl Into) -> Result<(Self, String)> { + let string: String = string.into(); + // we just want to strictly match this, else we error + + let lines: Vec<_> = string.lines().collect(); + + let delim_check = |line_number: usize| -> Result<()> { + let delim = lines + .get(line_number) + .ok_or_else(|| eyre!(format!("Line Number {line_number} doesnt exist!")))? + .trim(); + if delim != "---" { + return Err(eyre!("FrontMatter Deliminator Corrupted!")); + } + Ok(()) + }; + + // check first line + delim_check(0)?; + + //extract name + let title = lines + .get(1) + .ok_or_else(|| eyre!("Title line doesn't exist!".to_owned()))? + .strip_prefix("Title: ") + .ok_or_else(|| eyre!("Title line doesn't start with \"Title: \" ".to_owned(),))?; + + let created_at = lines + .get(2) + .ok_or_else(|| eyre!("Date line doesn't exist!".to_owned()))? + .strip_prefix("Date: ") + .ok_or_else(|| eyre!("Date line doesn't start with \"Date: \" ".to_owned(),)) + .map(|date_str| NaiveDateTime::parse_from_str(date_str, DATE_FMT_STR))? + .map_err(|err| eyre!(err.to_string()))?; + + let tag_strings: Vec = lines + .get(3) + .ok_or_else(|| eyre!("Tag line doesn't exist!".to_owned()))? + .strip_prefix("Tags: ") + .ok_or_else(|| eyre!("Tag line doesn't start with \"Tags: \" ".to_owned(),))? + .split_whitespace() + .map(ToOwned::to_owned) + .collect::>(); + + delim_check(4)?; + + let remaining = lines[5..].join("\n"); + + Ok((Self::new(title, created_at, tag_strings), remaining)) + } +} + +impl Display for FrontMatter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let date_fmt_items = StrftimeItems::new(DATE_FMT_STR); + writeln!(f, "---")?; + writeln!(f, "Title: {}", self.title)?; + writeln!( + f, + "Date: {}", + self.created_at.format_with_items(date_fmt_items) + )?; + write!(f, "Tags: ")?; + + for tag in &self.tag_strings { + write!(f, "{tag} ")?; + } + + writeln!(f, "\n---") + } +} + +#[cfg(test)] +mod tests { + + use chrono::NaiveDateTime; + + use crate::types::{FrontMatter, frontmatter::DATE_FMT_STR}; + + // use crate::{FrontMatter, zettel::frontmatter::DATE_FMT_STR}; + + #[test] + fn test_extract_from_string() { + let test_suite: [(&'static str, (FrontMatter, &'static str)); 1] = [( + r"--- +Title: LOL +Date: 2025-01-01 12:50:19 AM +Tags: whoa barber +--- +", + ( + FrontMatter::new( + "LOL", + NaiveDateTime::parse_from_str("2025-01-01 12:50:19 AM", DATE_FMT_STR).unwrap(), + vec!["whoa", "barber"], + ), + "", + ), + )]; + + for (raw_text, (front_matter, remaining)) in &test_suite { + let (extracted_front_matter, extracted_remaining) = + FrontMatter::extract_from_str(*raw_text).unwrap(); + + assert_eq!(extracted_front_matter, *front_matter); + assert_eq!(extracted_remaining, *remaining); + } + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 3a14ad4..5f8e53b 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -20,6 +20,5 @@ pub use task::Task; mod workspace; pub use workspace::Workspace; - - - +mod frontmatter; +pub use frontmatter::FrontMatter; diff --git a/src/types/zettel.rs b/src/types/zettel.rs index 3463b85..4a87cd0 100644 --- a/src/types/zettel.rs +++ b/src/types/zettel.rs @@ -1,11 +1,11 @@ -use dto::{TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx}; +use dto::{DateTimeUtc, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx}; use std::path::PathBuf; use color_eyre::eyre::Result; use dto::NanoId; -use tokio::fs::File; +use tokio::{fs::File, io::AsyncWriteExt}; -use crate::types::{Tag, Workspace}; +use crate::types::{FrontMatter, Tag, Workspace}; /// A `Zettel` is a note about a single idea. /// It can have many `Tag`s, just meaning it can fall under many @@ -15,11 +15,11 @@ use crate::types::{Tag, Workspace}; pub struct Zettel { /// Should only be constructed from models. _private: (), - pub id: NanoId, pub title: String, /// a workspace-local file path, needs to be canonicalized before usage pub file_path: PathBuf, + pub created_at: DateTimeUtc, pub tags: Vec, } @@ -35,10 +35,10 @@ impl Zettel { let local_file_path = format!("{nano_id}.md"); // now we have to create the file - File::create_new(ws.root.clone().join(&local_file_path)).await?; + let mut file = File::create_new(ws.root.clone().join(&local_file_path)).await?; let inserted = ZettelActiveModel::builder() - .set_title(title) + .set_title(title.clone()) .set_file_path(local_file_path) .set_nano_id(nano_id) .insert(&ws.db) @@ -52,8 +52,44 @@ impl Zettel { .await? .expect("This must exist since we just inserted it"); + let front_matter = FrontMatter::new( + title, + zettel.created_at.naive_local(), + zettel.tags.iter().map(|t| t.name.clone()).collect(), + ); + + file.write_all(front_matter.to_string().as_bytes()).await?; + Ok(zettel.into()) } + + /// Returns the most up-to-date `FrontMatter` for this + /// `Zettel` + #[expect(dead_code)] + pub async fn front_matter(&self, ws: &Workspace) -> Result { + let path = self.absolute_path(ws); + let (fm, _) = FrontMatter::extract_from_file(path).await?; + Ok(fm) + } + + /// Returns the content of this `Zettel`, which is everything + /// but the `FrontMatter` + #[expect(dead_code)] + pub async fn content(&self, ws: &Workspace) -> Result { + let path = self.absolute_path(ws); + let (_, content) = FrontMatter::extract_from_file(path).await?; + Ok(content) + } + + #[expect(dead_code)] + async fn open_file(&self, ws: &Workspace) -> Result { + let path = self.absolute_path(ws); + Ok(File::open(path).await?) + } + + fn absolute_path(&self, ws: &Workspace) -> PathBuf { + ws.root.clone().join(&self.file_path) + } } impl From for Zettel { @@ -69,6 +105,7 @@ impl From for Zettel { id: value.nano_id, title: value.title, file_path: value.file_path.into(), + created_at: value.created_at, tags: value.tags.into_iter().map(Into::into).collect(), } } -- 2.51.2 From c19c8981d2386f6e16de32d6638fba2d6c524613 Mon Sep 17 00:00:00 2001 From: suri-codes Date: Sun, 29 Mar 2026 09:39:34 -0700 Subject: [PATCH 3/4] fix: date's are now all local --- Cargo.lock | 3 --- Cargo.toml | 1 - .../src/m20260318_233726_group_table.rs | 4 ++-- .../src/m20260319_002245_task_table.rs | 8 +++---- .../src/m20260323_002518_zettel_table.rs | 4 ++-- crates/dto/src/entity/group.rs | 17 ++++++++++++--- crates/dto/src/entity/prelude.rs | 2 +- crates/dto/src/entity/task.rs | 21 ++++++++++++++----- crates/dto/src/entity/zettel.rs | 18 ++++++++++++---- crates/dto/src/lib.rs | 2 +- src/types/frontmatter.rs | 21 ++++++++----------- src/types/group.rs | 6 +++--- src/types/task.rs | 10 ++++----- src/types/zettel.rs | 6 +++--- 14 files changed, 74 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1b1e86..39a5a5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1210,10 +1210,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", - "js-sys", "num-traits", "serde", - "wasm-bindgen", "windows-link 0.2.1", ] @@ -2243,7 +2241,6 @@ version = "0.1.0" dependencies = [ "anyhow", "better-panic", - "chrono", "clap", "color-eyre", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index c4cf730..9122607 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,6 @@ clap = { version = "4.5.60", features = ["derive", "cargo", "wrap_help", "unicod kdl = "6.5.0" dto = {path="./crates/dto"} eframe = "0.34.1" -chrono = {version="0.4.41", features=["serde"]} [build-dependencies] anyhow = "1.0.102" diff --git a/crates/dto/migration/src/m20260318_233726_group_table.rs b/crates/dto/migration/src/m20260318_233726_group_table.rs index ad91067..a66db47 100644 --- a/crates/dto/migration/src/m20260318_233726_group_table.rs +++ b/crates/dto/migration/src/m20260318_233726_group_table.rs @@ -31,8 +31,8 @@ impl MigrationTrait for Migration { .not_null() .default(Priority::default().to_string()), ) - .col(timestamp(Group::CreatedAt).default(Expr::current_timestamp())) - .col(timestamp(Group::ModifiedAt).default(Expr::current_timestamp())) + .col(date_time(Group::CreatedAt).default(Expr::current_timestamp())) + .col(date_time(Group::ModifiedAt).default(Expr::current_timestamp())) .col(string(Group::ZettelId).not_null().unique_key()) // foreign key for the zettel related to this group .foreign_key( diff --git a/crates/dto/migration/src/m20260319_002245_task_table.rs b/crates/dto/migration/src/m20260319_002245_task_table.rs index 35e2222..1737b73 100644 --- a/crates/dto/migration/src/m20260319_002245_task_table.rs +++ b/crates/dto/migration/src/m20260319_002245_task_table.rs @@ -30,10 +30,10 @@ impl MigrationTrait for Migration { .not_null() .default(Priority::default().to_string()), ) - .col(timestamp(Task::Due).null()) - .col(timestamp(Task::FinishedAt).null()) - .col(timestamp(Task::CreatedAt).default(Expr::current_timestamp())) - .col(timestamp(Task::ModifiedAt).default(Expr::current_timestamp())) + .col(date_time(Task::Due).null()) + .col(date_time(Task::FinishedAt).null()) + .col(date_time(Task::CreatedAt).default(Expr::current_timestamp())) + .col(date_time(Task::ModifiedAt).default(Expr::current_timestamp())) .col(string(Task::ZettelId).not_null().unique_key()) // foreign key for the zettel related to this task .foreign_key( diff --git a/crates/dto/migration/src/m20260323_002518_zettel_table.rs b/crates/dto/migration/src/m20260323_002518_zettel_table.rs index cd31e8d..800b355 100644 --- a/crates/dto/migration/src/m20260323_002518_zettel_table.rs +++ b/crates/dto/migration/src/m20260323_002518_zettel_table.rs @@ -22,8 +22,8 @@ impl MigrationTrait for Migration { ) .col(string(Zettel::Title).not_null()) .col(string(Zettel::FilePath).not_null()) - .col(timestamp(Zettel::CreatedAt).default(Expr::current_timestamp())) - .col(timestamp(Zettel::ModifiedAt).default(Expr::current_timestamp())) + .col(date_time(Zettel::CreatedAt).default(Expr::current_timestamp())) + .col(date_time(Zettel::ModifiedAt).default(Expr::current_timestamp())) .to_owned(), ) .await?; diff --git a/crates/dto/src/entity/group.rs b/crates/dto/src/entity/group.rs index 6750394..131eee5 100644 --- a/crates/dto/src/entity/group.rs +++ b/crates/dto/src/entity/group.rs @@ -1,8 +1,9 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 +use migration::prelude::Local; use migration::types::*; -use sea_orm::entity::prelude::*; use sea_orm::ActiveValue::Set; +use sea_orm::entity::prelude::*; use std::future::ready; use std::pin::Pin; @@ -17,8 +18,8 @@ pub struct Model { pub name: String, pub color: Color, pub priority: Priority, - pub created_at: DateTimeUtc, - pub modified_at: DateTimeUtc, + pub created_at: DateTime, + pub modified_at: DateTime, #[sea_orm(unique)] pub zettel_id: NanoId, pub parent_group_id: Option, @@ -54,6 +55,16 @@ impl ActiveModelBehavior for ActiveModel { Self: Send + 'async_trait, 'life0: 'async_trait, { + let now = Local::now().naive_local(); + + // set modified at + self.modified_at = Set(now); + + // we set the timestamp to non-utc + if insert && self.created_at.is_not_set() { + self.created_at = Set(now); + } + if insert && self.nano_id.is_not_set() { self.nano_id = Set(NanoId::default()); } diff --git a/crates/dto/src/entity/prelude.rs b/crates/dto/src/entity/prelude.rs index ef214b3..de2c434 100644 --- a/crates/dto/src/entity/prelude.rs +++ b/crates/dto/src/entity/prelude.rs @@ -1,6 +1,6 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 -#![expect(unused_imports)] +#![expect(unused_imports)] pub use super::group::Entity as Group; pub use super::tag::Entity as Tag; pub use super::task::Entity as Task; diff --git a/crates/dto/src/entity/task.rs b/crates/dto/src/entity/task.rs index 28408b8..3bf9550 100644 --- a/crates/dto/src/entity/task.rs +++ b/crates/dto/src/entity/task.rs @@ -1,8 +1,9 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 +use migration::prelude::Local; use migration::types::*; -use sea_orm::entity::prelude::*; use sea_orm::ActiveValue::Set; +use sea_orm::entity::prelude::*; use std::future::ready; use std::pin::Pin; @@ -16,10 +17,10 @@ pub struct Model { pub nano_id: NanoId, pub name: String, pub priority: Priority, - pub due: Option, - pub finished_at: Option, - pub created_at: DateTimeUtc, - pub modified_at: DateTimeUtc, + pub due: Option, + pub finished_at: Option, + pub created_at: DateTime, + pub modified_at: DateTime, #[sea_orm(unique)] pub zettel_id: NanoId, pub group_id: NanoId, @@ -52,6 +53,16 @@ impl ActiveModelBehavior for ActiveModel { Self: Send + 'async_trait, 'life0: 'async_trait, { + let now = Local::now().naive_local(); + + // set modified at + self.modified_at = Set(now); + + // we set the timestamp to non-utc + if insert && self.created_at.is_not_set() { + self.created_at = Set(now); + } + if insert && self.nano_id.is_not_set() { self.nano_id = Set(NanoId::default()); } diff --git a/crates/dto/src/entity/zettel.rs b/crates/dto/src/entity/zettel.rs index 10cd5e9..2a98089 100644 --- a/crates/dto/src/entity/zettel.rs +++ b/crates/dto/src/entity/zettel.rs @@ -1,8 +1,8 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 -use migration::types::*; -use sea_orm::entity::prelude::*; +use migration::{prelude::Local, types::*}; use sea_orm::ActiveValue::Set; +use sea_orm::entity::prelude::*; use std::{future::ready, pin::Pin}; #[sea_orm::model] @@ -16,8 +16,8 @@ pub struct Model { pub nano_id: NanoId, pub title: String, pub file_path: String, - pub created_at: DateTimeUtc, - pub modified_at: DateTimeUtc, + pub created_at: DateTime, + pub modified_at: DateTime, #[sea_orm(has_one)] pub group: HasOne, #[sea_orm(has_one)] @@ -37,6 +37,16 @@ impl ActiveModelBehavior for ActiveModel { Self: Send + 'async_trait, 'life0: 'async_trait, { + let now = Local::now().naive_local(); + + // set modified at + self.modified_at = Set(now); + + // we set the timestamp to non-utc + if insert && self.created_at.is_not_set() { + self.created_at = Set(now); + } + if insert && self.nano_id.is_not_set() { self.nano_id = Set(NanoId::default()); } diff --git a/crates/dto/src/lib.rs b/crates/dto/src/lib.rs index bdd6791..f88de67 100644 --- a/crates/dto/src/lib.rs +++ b/crates/dto/src/lib.rs @@ -15,7 +15,7 @@ pub use migration::types::NanoId; /// and add additional functionality to it. pub use migration::types::Priority as PriorityDTO; -pub use sea_orm::entity::prelude::DateTimeUtc; +pub use sea_orm::entity::prelude::DateTime; /// Color type, exporting as DTO because I might /// want to newtype wrap this, might not have to, depending diff --git a/src/types/frontmatter.rs b/src/types/frontmatter.rs index f23fed2..a93d129 100644 --- a/src/types/frontmatter.rs +++ b/src/types/frontmatter.rs @@ -1,23 +1,25 @@ use std::{fmt::Display, path::Path}; -use chrono::{NaiveDateTime, format::StrftimeItems}; +// use chrono::format::StrftimeItems; use color_eyre::eyre::{Result, eyre}; use serde::{Deserialize, Serialize}; use tokio::fs; +use dto::DateTime; + const DATE_FMT_STR: &str = "%Y-%m-%d %I:%M:%S %p"; #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)] pub struct FrontMatter { pub title: String, - pub created_at: NaiveDateTime, + pub created_at: DateTime, pub tag_strings: Vec, } impl FrontMatter { pub fn new( title: impl Into, - created_at: NaiveDateTime, + created_at: DateTime, tag_strings: Vec>, ) -> Self { let tag_strings = tag_strings.into_iter().map(Into::into).collect(); @@ -85,7 +87,7 @@ impl FrontMatter { .ok_or_else(|| eyre!("Date line doesn't exist!".to_owned()))? .strip_prefix("Date: ") .ok_or_else(|| eyre!("Date line doesn't start with \"Date: \" ".to_owned(),)) - .map(|date_str| NaiveDateTime::parse_from_str(date_str, DATE_FMT_STR))? + .map(|date_str| DateTime::parse_from_str(date_str, DATE_FMT_STR))? .map_err(|err| eyre!(err.to_string()))?; let tag_strings: Vec = lines @@ -107,14 +109,9 @@ impl FrontMatter { impl Display for FrontMatter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let date_fmt_items = StrftimeItems::new(DATE_FMT_STR); writeln!(f, "---")?; writeln!(f, "Title: {}", self.title)?; - writeln!( - f, - "Date: {}", - self.created_at.format_with_items(date_fmt_items) - )?; + writeln!(f, "Date: {}", self.created_at.format(DATE_FMT_STR))?; write!(f, "Tags: ")?; for tag in &self.tag_strings { @@ -128,7 +125,7 @@ impl Display for FrontMatter { #[cfg(test)] mod tests { - use chrono::NaiveDateTime; + use dto::DateTime; use crate::types::{FrontMatter, frontmatter::DATE_FMT_STR}; @@ -146,7 +143,7 @@ Tags: whoa barber ( FrontMatter::new( "LOL", - NaiveDateTime::parse_from_str("2025-01-01 12:50:19 AM", DATE_FMT_STR).unwrap(), + DateTime::parse_from_str("2025-01-01 12:50:19 AM", DATE_FMT_STR).unwrap(), vec!["whoa", "barber"], ), "", diff --git a/src/types/group.rs b/src/types/group.rs index ef7c6b6..7286b67 100644 --- a/src/types/group.rs +++ b/src/types/group.rs @@ -1,4 +1,4 @@ -use dto::{DateTimeUtc, GroupModelEx, NanoId}; +use dto::{DateTime, GroupModelEx, NanoId}; use crate::types::{Color, Priority, Zettel}; @@ -13,8 +13,8 @@ pub struct Group { pub name: String, pub color: Color, pub priority: Priority, - pub created_at: DateTimeUtc, - pub modified_at: DateTimeUtc, + pub created_at: DateTime, + pub modified_at: DateTime, /// The `Zettel` that is related to this `Group`. /// Can store notes regarding this group in /// the `Zettel` diff --git a/src/types/task.rs b/src/types/task.rs index 3c88ab3..7f9d2f5 100644 --- a/src/types/task.rs +++ b/src/types/task.rs @@ -1,4 +1,4 @@ -use dto::{DateTimeUtc, NanoId, TaskModelEx}; +use dto::{ DateTime, NanoId, TaskModelEx}; use crate::types::{Group, Priority, Zettel}; @@ -12,10 +12,10 @@ pub struct Task { pub id: NanoId, pub name: String, pub priority: Priority, - pub due: Option, - pub finished_at: Option, - pub created_at: DateTimeUtc, - pub modified_at: DateTimeUtc, + pub due: Option, + pub finished_at: Option, + pub created_at: DateTime, + pub modified_at: DateTime, /// Each task has its own related `Zettel`. pub zettel: Zettel, pub group: Group, diff --git a/src/types/zettel.rs b/src/types/zettel.rs index 4a87cd0..0b1cd8b 100644 --- a/src/types/zettel.rs +++ b/src/types/zettel.rs @@ -1,4 +1,4 @@ -use dto::{DateTimeUtc, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx}; +use dto::{DateTime, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx}; use std::path::PathBuf; use color_eyre::eyre::Result; @@ -19,7 +19,7 @@ pub struct Zettel { pub title: String, /// a workspace-local file path, needs to be canonicalized before usage pub file_path: PathBuf, - pub created_at: DateTimeUtc, + pub created_at: DateTime, pub tags: Vec, } @@ -54,7 +54,7 @@ impl Zettel { let front_matter = FrontMatter::new( title, - zettel.created_at.naive_local(), + zettel.created_at, zettel.tags.iter().map(|t| t.name.clone()).collect(), ); -- 2.51.2 From 114af516f1a4fbd0707ccbbefdfc4c7f1a784b71 Mon Sep 17 00:00:00 2001 From: suri-codes Date: Sun, 29 Mar 2026 12:34:34 -0700 Subject: [PATCH 4/4] feat: init zk component --- Cargo.lock | 1 + Cargo.toml | 1 + src/cli/mod.rs | 2 +- src/tui/app.rs | 11 +++---- src/tui/components/mod.rs | 12 ++++++-- src/tui/components/zk/mod.rs | 52 +++++++++++++++++++++++++++++++++ src/tui/components/zk/zettel.rs | 0 7 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 src/tui/components/zk/mod.rs create mode 100644 src/tui/components/zk/zettel.rs diff --git a/Cargo.lock b/Cargo.lock index 39a5a5d..3d18daa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2240,6 +2240,7 @@ name = "filaments" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "better-panic", "clap", "color-eyre", diff --git a/Cargo.toml b/Cargo.toml index 9122607..3e701ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ clap = { version = "4.5.60", features = ["derive", "cargo", "wrap_help", "unicod kdl = "6.5.0" dto = {path="./crates/dto"} eframe = "0.34.1" +async-trait = "0.1.89" [build-dependencies] anyhow = "1.0.102" diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 5119690..b73fed1 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -65,7 +65,7 @@ pub enum ZettelSubcommand { }, /// List groups. List { - /// The file-path for data to pe put into. + /// Filter by tag #[arg(short = 't', long)] by_tag: String, }, diff --git a/src/tui/app.rs b/src/tui/app.rs index 937e3b9..08423ce 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -10,7 +10,7 @@ use tracing::debug; use crate::{ config::Config, - tui::{Event, Tui}, + tui::{Event, Tui, components::Zk}, }; use super::{components::Component, signal::Signal}; @@ -38,6 +38,7 @@ pub struct App { pub enum Region { #[default] Home, + Zk, } impl App { @@ -48,7 +49,7 @@ impl App { Ok(Self { tick_rate, frame_rate, - components: vec![], + components: vec![Box::new(Zk::new())], should_quit: false, should_suspend: false, config: Config::parse()?, @@ -80,7 +81,7 @@ impl App { loop { self.handle_events(&mut tui).await?; - self.handle_signals(&mut tui)?; + self.handle_signals(&mut tui).await?; if self.should_suspend { tui.suspend()?; @@ -150,7 +151,7 @@ impl App { Ok(()) } - fn handle_signals(&mut self, tui: &mut Tui) -> Result<()> { + async fn handle_signals(&mut self, tui: &mut Tui) -> Result<()> { while let Ok(signal) = self.signal_rx.try_recv() { if signal != Signal::Tick && signal != Signal::Render { debug!("handling signal: {signal:?}"); @@ -193,7 +194,7 @@ impl App { } for component in &mut self.components { - if let Some(signal) = component.update(signal.clone())? { + if let Some(signal) = component.update(signal.clone()).await? { self.signal_tx.send(signal)?; } } diff --git a/src/tui/components/mod.rs b/src/tui/components/mod.rs index 8ef8d7d..8cf5f7a 100644 --- a/src/tui/components/mod.rs +++ b/src/tui/components/mod.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use crossterm::event::{KeyEvent, MouseEvent}; use ratatui::{ Frame, @@ -10,11 +11,18 @@ use crate::{ tui::{Event, Signal}, }; +/// The zk component +mod zk; + +pub use zk::*; + /// `Component` is a trait that represents a visual and interactive element of the user interface. /// /// Implementers of this trait can be registered with the main application loop and will be able to /// receive events, update state, and be rendered on the screen. -pub trait Component: Send { +/// +#[async_trait] +pub trait Component: Send + Sync { /// Register a signal handler that can send signals for processing if necessary. /// /// # Arguments @@ -106,7 +114,7 @@ pub trait Component: Send { /// # Returns /// /// * [`color_eyre::Result>`] - A signal to be processed or none. - fn update(&mut self, signal: Signal) -> color_eyre::Result>; + async fn update(&mut self, signal: Signal) -> color_eyre::Result>; /// Render the component on the screen. (REQUIRED) /// diff --git a/src/tui/components/zk/mod.rs b/src/tui/components/zk/mod.rs new file mode 100644 index 0000000..efbad1b --- /dev/null +++ b/src/tui/components/zk/mod.rs @@ -0,0 +1,52 @@ +use async_trait::async_trait; +use color_eyre::eyre::Result; +use ratatui::widgets::Paragraph; +use tokio::sync::mpsc::UnboundedSender; + +use crate::tui::{Signal, components::Component}; + +mod zettel; + +pub struct Zk { + signal_tx: Option>, +} + +impl Zk { + pub const fn new() -> Self { + Self { signal_tx: None } + } +} + +#[async_trait] +impl Component for Zk { + fn register_signal_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.signal_tx = Some(tx); + Ok(()) + } + + async fn update(&mut self, _signal: Signal) -> Result> { + // match signal { + // Signal::Tick => todo!(), + // Signal::Render => todo!(), + // Signal::Resize(_, _) => todo!(), + // Signal::Suspend => todo!(), + // Signal::Resume => todo!(), + // Signal::Quit => todo!(), + // Signal::ClearScreen => todo!(), + // Signal::Error(_) => todo!(), + // Signal::Help => todo!(), + // Signal::Helix => todo!(), + // } + Ok(None) + } + + fn draw( + &mut self, + frame: &mut ratatui::Frame, + area: ratatui::prelude::Rect, + ) -> color_eyre::Result<()> { + let hello = Paragraph::new("Hello Surendra"); + frame.render_widget(hello, area); + Ok(()) + } +} diff --git a/src/tui/components/zk/zettel.rs b/src/tui/components/zk/zettel.rs new file mode 100644 index 0000000..e69de29 -- 2.51.2