···2233use migration::prelude::Local;
44use migration::types::*;
55-use sea_orm::ActiveValue::Set;
65use sea_orm::entity::prelude::*;
66+use sea_orm::ActiveValue::Set;
77use std::future::ready;
88use std::pin::Pin;
99···5656 'life0: 'async_trait,
5757 {
5858 let now = Local::now().naive_local();
5959-6060- // set modified at
6159 self.modified_at = Set(now);
6262-6363- // we set the timestamp to non-utc
6460 if insert && self.created_at.is_not_set() {
6561 self.created_at = Set(now);
6662 }
6767-6863 if insert && self.nano_id.is_not_set() {
6964 self.nano_id = Set(NanoId::default());
7065 }
+1-6
crates/dto/src/entity/task.rs
···2233use migration::prelude::Local;
44use migration::types::*;
55-use sea_orm::ActiveValue::Set;
65use sea_orm::entity::prelude::*;
66+use sea_orm::ActiveValue::Set;
77use std::future::ready;
88use std::pin::Pin;
99···5454 'life0: 'async_trait,
5555 {
5656 let now = Local::now().naive_local();
5757-5858- // set modified at
5957 self.modified_at = Set(now);
6060-6161- // we set the timestamp to non-utc
6258 if insert && self.created_at.is_not_set() {
6359 self.created_at = Set(now);
6460 }
6565-6661 if insert && self.nano_id.is_not_set() {
6762 self.nano_id = Set(NanoId::default());
6863 }
+1-6
crates/dto/src/entity/zettel.rs
···11//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
2233use migration::{prelude::Local, types::*};
44-use sea_orm::ActiveValue::Set;
54use sea_orm::entity::prelude::*;
55+use sea_orm::ActiveValue::Set;
66use std::{future::ready, pin::Pin};
7788#[sea_orm::model]
···3838 'life0: 'async_trait,
3939 {
4040 let now = Local::now().naive_local();
4141-4242- // set modified at
4341 self.modified_at = Set(now);
4444-4545- // we set the timestamp to non-utc
4642 if insert && self.created_at.is_not_set() {
4743 self.created_at = Set(now);
4844 }
4949-5045 if insert && self.nano_id.is_not_set() {
5146 self.nano_id = Set(NanoId::default());
5247 }
+16
crates/dto/src/lib.rs
···88/// exported traits for the database
99pub use sea_orm::ActiveModelTrait;
1010pub use sea_orm::ActiveValue;
1111+pub use sea_orm::ColumnTrait;
1212+pub use sea_orm::EntityTrait;
1313+pub use sea_orm::IntoActiveModel;
1414+pub use sea_orm::QueryFilter;
11151216/// Exporting this as a generic NanoId.
1317pub use migration::types::NanoId;
···2731/// Everything related to groups.
2832pub use entity::group::ActiveModel as GroupActiveModel;
2933pub use entity::group::ActiveModelEx as GroupActiveModelEx;
3434+pub use entity::group::Column as GroupColumns;
3035pub use entity::group::Entity as GroupEntity;
3136pub use entity::group::Model as GroupModel;
3237pub use entity::group::ModelEx as GroupModelEx;
···3439/// Everything related to tasks.
3540pub use entity::task::ActiveModel as TaskActiveModel;
3641pub use entity::task::ActiveModelEx as TaskActiveModelEx;
4242+pub use entity::task::Column as TaskColumns;
3743pub use entity::task::Entity as TaskEntity;
3844pub use entity::task::Model as TaskModel;
3945pub use entity::task::ModelEx as TaskModelEx;
···4147/// Everything related to zetetl's.
4248pub use entity::zettel::ActiveModel as ZettelActiveModel;
4349pub use entity::zettel::ActiveModelEx as ZettelActiveModelEx;
5050+pub use entity::zettel::Column as ZettelColumns;
4451pub use entity::zettel::Entity as ZettelEntity;
4552pub use entity::zettel::Model as ZettelModel;
4653pub use entity::zettel::ModelEx as ZettelModelEx;
···4855/// Everything related to tag's.
4956pub use entity::tag::ActiveModel as TagActiveModel;
5057pub use entity::tag::ActiveModelEx as TagActiveModelEx;
5858+pub use entity::tag::Column as TagColumns;
5159pub use entity::tag::Entity as TagEntity;
5260pub use entity::tag::Model as TagModel;
5361pub use entity::tag::ModelEx as TagModelEx;
6262+6363+/// Everything related to the zettel_tag entries.
6464+pub use entity::zettel_tag::ActiveModel as ZettelTagActiveModel;
6565+pub use entity::zettel_tag::ActiveModelEx as ZettelTagActiveModelEx;
6666+pub use entity::zettel_tag::Column as ZettelTagColumns;
6767+pub use entity::zettel_tag::Entity as ZettelTagEntity;
6868+pub use entity::zettel_tag::Model as ZettelTagModel;
6969+pub use entity::zettel_tag::ModelEx as ZettelTagModelEx;
···18181919# Run all tests
2020test:
2121- cargo test {{_cargo_flags}}
2121+ cargo nextest r {{_cargo_flags}}
222223232424# Only used to build / generate entities
+11
src/types/frontmatter.rs
···2233// use chrono::format::StrftimeItems;
44use color_eyre::eyre::{Result, eyre};
55+use egui_graphs::Node;
56use serde::{Deserialize, Serialize};
67use tokio::fs;
7889use dto::DateTime;
1010+1111+use crate::types::{Link, Zettel};
9121013const DATE_FMT_STR: &str = "%Y-%m-%d %I:%M:%S %p";
1114···2932 created_at,
3033 tag_strings,
3134 }
3535+ }
3636+3737+ /// Apply the features of `FrontMatter` onto a
3838+ /// `Node`
3939+ pub fn apply_node_transform(&self, node: &mut Node<Zettel, Link>) {
4040+ node.set_label(self.title.clone());
4141+ let disp = node.display_mut();
4242+ disp.radius = 100.0;
3243 }
33443445 /// Reads in file and returns the front matter as well as the content after it.
+105
src/types/kasten.rs
···11+use crate::types::{Link, Zettel, ZettelId};
22+use color_eyre::eyre::Result;
33+use eframe::emath;
44+use egui_graphs::{
55+ Graph,
66+ petgraph::{Directed, graph::NodeIndex, prelude::StableGraph},
77+};
88+use rayon::iter::{ParallelBridge as _, ParallelIterator as _};
99+use std::{cmp::max, collections::HashMap};
1010+1111+use crate::types::Workspace;
1212+1313+#[derive(Debug, Clone)]
1414+#[expect(dead_code)]
1515+pub struct Kasten {
1616+ /// Private field so it can only be instantiated from a `Path`
1717+ _private: (),
1818+1919+ /// The workspace this `Kasten` is in
2020+ pub ws: Workspace,
2121+2222+ /// the graph of `Zettel`s and the `Links` between them
2323+ pub graph: ZkGraph,
2424+2525+ /// simple conversions
2626+ zid_to_gid: HashMap<ZettelId, NodeIndex>,
2727+2828+ pub most_recently_edited: Option<NodeIndex>,
2929+}
3030+3131+pub type ZkGraph = Graph<Zettel, Link, Directed>;
3232+3333+/// Minimum number of nodes in our graph.
3434+const GRAPH_MIN_NODES: usize = 128;
3535+/// Arbitrarily chosen minimum number of edges
3636+const GRAPH_MIN_EDGES: usize = GRAPH_MIN_NODES * 3;
3737+3838+impl Kasten {
3939+ /// Indexes the `Workspace` and constructs a `Kasten`
4040+ #[expect(dead_code)]
4141+ pub async fn index(ws: Workspace) -> Result<Self> {
4242+ let paths = std::fs::read_dir(&ws.root)?
4343+ .par_bridge()
4444+ .flatten()
4545+ .filter(|entry| {
4646+ entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
4747+ && entry
4848+ .path()
4949+ .extension()
5050+ .and_then(|ext| ext.to_str())
5151+ .is_some_and(|ext| ext == "md")
5252+ })
5353+ .map(|entry| entry.path())
5454+ .collect::<Vec<_>>();
5555+5656+ let zettel_tasks = paths
5757+ .into_iter()
5858+ .map(|path| {
5959+ let ws = ws.clone();
6060+ tokio::spawn(async move { Zettel::from_path(path, &ws).await })
6161+ })
6262+ .collect::<Vec<_>>();
6363+6464+ // await all of them
6565+ let zettels = futures::future::join_all(zettel_tasks)
6666+ .await
6767+ .into_iter()
6868+ .filter_map(|result| result.ok()?.ok())
6969+ .collect::<Vec<Zettel>>();
7070+7171+ // capacity!
7272+ let mut graph: ZkGraph = ZkGraph::from(&StableGraph::with_capacity(
7373+ max(zettels.len() * 2, GRAPH_MIN_EDGES),
7474+ max(zettels.len() * 3, GRAPH_MIN_EDGES),
7575+ ));
7676+7777+ let mut zid_to_gid = HashMap::new();
7878+ for zettel in &zettels {
7979+ let fm = zettel.front_matter(&ws).await?;
8080+ let id = graph.add_node_custom(zettel.clone(), |node| {
8181+ fm.apply_node_transform(node);
8282+ let x = rand::random_range(0.0..=100.0);
8383+ let y = rand::random_range(0.0..=100.0);
8484+ node.set_location(emath::Pos2 { x, y });
8585+ });
8686+ zid_to_gid.insert(zettel.id.clone(), id);
8787+ }
8888+8989+ for zettel in &zettels {
9090+ let src = zid_to_gid.get(&zettel.id).expect("must exist");
9191+ for link in &zettel.links(&ws).await? {
9292+ let dst = zid_to_gid.get(&link.dest).expect("must exist");
9393+ graph.add_edge(*src, *dst, link.clone());
9494+ }
9595+ }
9696+9797+ Ok(Self {
9898+ _private: (),
9999+ ws,
100100+ graph,
101101+ zid_to_gid,
102102+ most_recently_edited: None,
103103+ })
104104+ }
105105+}
···991010mod zettel;
1111pub use zettel::Zettel;
1212+pub use zettel::ZettelId;
12131314mod group;
1415pub use group::Group;
···19202021mod workspace;
2122pub use workspace::Workspace;
2323+2424+mod link;
2525+pub use link::Link;
2626+2727+mod kasten;
2828+#[expect(unused_imports)]
2929+pub use kasten::Kasten;
22302331mod frontmatter;
2432pub use frontmatter::FrontMatter;
+1-1
src/types/workspace.rs
···8585 let path = PathBuf::from("/tmp/filaments/.filaments/filaments.db");
8686 create_dir_all(path.parent().unwrap()).unwrap();
8787 let _ = File::create(&path).unwrap();
8888- let _ws = Workspace::instansiate(dbg!(&path.parent().unwrap().parent().unwrap()))
8888+ let _ws = Workspace::instansiate(&path.parent().unwrap().parent().unwrap())
8989 .await
9090 .unwrap();
9191 }
+240-8
src/types/zettel.rs
···11-use dto::{DateTime, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx};
22-use std::path::PathBuf;
11+use dto::{
22+ ActiveModelTrait, ActiveValue, ColumnTrait, DateTime, EntityTrait as _, IntoActiveModel,
33+ QueryFilter, TagActiveModel, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx,
44+ ZettelTagActiveModel, ZettelTagColumns, ZettelTagEntity,
55+};
66+use pulldown_cmark::{Event, Parser, Tag as MkTag};
77+use serde::{Deserialize, Serialize};
88+use std::{
99+ fmt::Display,
1010+ path::{Path, PathBuf},
1111+};
1212+use tracing::{error, info};
31344-use color_eyre::eyre::Result;
1414+use color_eyre::eyre::{Error, Result, eyre};
515use dto::NanoId;
616use tokio::{fs::File, io::AsyncWriteExt};
71788-use crate::types::{FrontMatter, Tag, Workspace};
1818+use crate::types::{FrontMatter, Link, Tag, Workspace};
9191020/// A `Zettel` is a note about a single idea.
1121/// It can have many `Tag`s, just meaning it can fall under many
···1525pub struct Zettel {
1626 /// Should only be constructed from models.
1727 _private: (),
1818- pub id: NanoId,
2828+ pub id: ZettelId,
1929 pub title: String,
2030 /// a workspace-local file path, needs to be canonicalized before usage
2131 pub file_path: PathBuf,
···2333 pub tags: Vec<Tag>,
2434}
25353636+/// A `ZettelId` is essentially a `NanoId`,
3737+/// with some `Zettel` specific helpers written
3838+/// onto it
3939+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4040+pub struct ZettelId(NanoId);
4141+2642impl Zettel {
2743 pub async fn new(title: impl Into<String>, ws: &Workspace) -> Result<Self> {
2844 // fn new(title: impl Into<String>) -> Result<Self> {
···65816682 /// Returns the most up-to-date `FrontMatter` for this
6783 /// `Zettel`
6868- #[expect(dead_code)]
6984 pub async fn front_matter(&self, ws: &Workspace) -> Result<FrontMatter> {
7085 let path = self.absolute_path(ws);
7186 let (fm, _) = FrontMatter::extract_from_file(path).await?;
···74897590 /// Returns the content of this `Zettel`, which is everything
7691 /// but the `FrontMatter`
7777- #[expect(dead_code)]
7892 pub async fn content(&self, ws: &Workspace) -> Result<String> {
7993 let path = self.absolute_path(ws);
8094 let (_, content) = FrontMatter::extract_from_file(path).await?;
···90104 fn absolute_path(&self, ws: &Workspace) -> PathBuf {
91105 ws.root.clone().join(&self.file_path)
92106 }
107107+108108+ /// uses the id and root to parse out of the root directory
109109+ #[expect(dead_code)]
110110+ pub async fn from_id(id: &ZettelId, ws: &Workspace) -> Result<Self> {
111111+ let mut path = ws.root.clone();
112112+ path.push(id.0.to_string());
113113+ Self::from_path(path, ws).await
114114+ }
115115+116116+ pub async fn from_path(path: impl Into<PathBuf>, ws: &Workspace) -> Result<Self> {
117117+ let path: PathBuf = path.into();
118118+119119+ let id = ZettelId::try_from(path.as_path())?;
120120+121121+ let (front_matter, _) = FrontMatter::extract_from_file(&ws.root.clone().join(path)).await?;
122122+123123+ let mut zettel_tag_strings = front_matter.tag_strings.clone();
124124+125125+ zettel_tag_strings.sort();
126126+127127+ // get the zettel from the db
128128+ let db_zettel: ZettelModelEx = if let Some(existing_zettel) = ZettelEntity::load()
129129+ .with(TagEntity)
130130+ .filter_by_nano_id(id.clone())
131131+ .one(&ws.db)
132132+ .await?
133133+ {
134134+ existing_zettel
135135+ } else {
136136+ // if zettel is missing from db, we just add it here
137137+ info!("adding zettel to db");
138138+ let am = ZettelActiveModel {
139139+ nano_id: ActiveValue::Set(id.clone().into()),
140140+ title: ActiveValue::Set(front_matter.title.clone()),
141141+ ..Default::default()
142142+ };
143143+144144+ am.insert(&ws.db).await?;
145145+146146+ ZettelEntity::load()
147147+ .with(TagEntity)
148148+ .filter_by_nano_id(id.clone())
149149+ .one(&ws.db)
150150+ .await?
151151+ .expect("we just inserted the zettel")
152152+ };
153153+154154+ // get the tags for it
155155+ for db_tag in db_zettel.tags {
156156+ if let Ok(idx) = zettel_tag_strings.binary_search(&db_tag.name) {
157157+ // we remove tags we have already processed
158158+ zettel_tag_strings.remove(idx);
159159+ } else {
160160+ // the db says the file has tag `x`, but that tag is missing from the
161161+ // front matter, we can assume its gone, lets delete that link
162162+ let to_remove = ZettelTagEntity::find()
163163+ .filter(ZettelTagColumns::ZettelNanoId.eq(id.0.clone()))
164164+ .filter(ZettelTagColumns::TagNanoId.eq(db_tag.nano_id))
165165+ .one(&ws.db)
166166+ .await?
167167+ .expect("this link must exist");
168168+169169+ to_remove.into_active_model().delete(&ws.db).await?;
170170+ }
171171+ }
172172+173173+ // now any tags that are left inside zettel_tag_strings,
174174+ // we have to put them inside the db
175175+ for new_tag in zettel_tag_strings {
176176+ // create a new tag
177177+ let tag = TagActiveModel {
178178+ name: ActiveValue::Set(new_tag),
179179+ ..Default::default()
180180+ }
181181+ .insert(&ws.db)
182182+ .await?;
183183+184184+ // this zettel has this tag now
185185+ let _ = ZettelTagActiveModel {
186186+ zettel_nano_id: ActiveValue::Set(id.to_string()),
187187+ tag_nano_id: ActiveValue::Set(tag.nano_id.to_string()),
188188+ }
189189+ .insert(&ws.db)
190190+ .await?;
191191+ }
192192+193193+ if front_matter.title != db_zettel.title {
194194+ let am = ZettelActiveModel {
195195+ id: ActiveValue::Unchanged(db_zettel.id),
196196+ title: ActiveValue::Set(front_matter.title.clone()),
197197+ ..Default::default()
198198+ };
199199+200200+ am.update(&ws.db).await?;
201201+ }
202202+203203+ Ok(ZettelEntity::load()
204204+ .with(TagEntity)
205205+ .filter_by_nano_id(id.clone())
206206+ .one(&ws.db)
207207+ .await?
208208+ .expect("We just inserted it right above")
209209+ .into())
210210+ }
211211+212212+ /// The `Link`s that are going out of this `Zettel`
213213+ pub async fn links(&self, ws: &Workspace) -> Result<Vec<Link>> {
214214+ let content = self.content(ws).await?;
215215+ let parsed = Parser::new(&content);
216216+217217+ let mut links = vec![];
218218+219219+ for event in parsed {
220220+ if let Event::Start(MkTag::Link { dest_url, .. }) = event {
221221+ info!("Found dest_url: {dest_url:#?}");
222222+223223+ let dest_path = {
224224+ // remove leading "./"
225225+ let without_prefix = dest_url.strip_prefix("./").unwrap_or(&dest_url);
226226+227227+ // remove "#" and everything after it
228228+ let without_anchor = without_prefix.split('#').next().unwrap();
229229+230230+ // add .md if not present
231231+ let normalized = if std::path::Path::new(without_anchor)
232232+ .extension()
233233+ .is_some_and(|ext| ext.eq_ignore_ascii_case("md"))
234234+ {
235235+ without_anchor.to_string()
236236+ } else {
237237+ format!("{without_anchor}.md")
238238+ };
239239+240240+ let mut tmp_root = ws.root.clone();
241241+ tmp_root.push(normalized);
242242+ tmp_root
243243+ };
244244+ // simplest way to validate that the path exists
245245+ let Ok(canon_url) = dest_path.canonicalize() else {
246246+ error!("Link not found!: {dest_path:?}");
247247+ continue;
248248+ };
249249+250250+ let dst_id = ZettelId::try_from(canon_url)?;
251251+252252+ let link = Link::new(self.id.clone(), dst_id);
253253+254254+ links.push(link);
255255+ }
256256+ }
257257+258258+ Ok(links)
259259+ }
93260}
9426195262impl From<ZettelModelEx> for Zettel {
···102269103270 Self {
104271 _private: (),
105105- id: value.nano_id,
272272+ id: value.nano_id.into(),
106273 title: value.title,
107274 file_path: value.file_path.into(),
108275 created_at: value.created_at,
···110277 }
111278 }
112279}
280280+281281+impl From<&str> for ZettelId {
282282+ fn from(value: &str) -> Self {
283283+ Self(NanoId::from(value))
284284+ }
285285+}
286286+287287+impl From<&NanoId> for ZettelId {
288288+ fn from(value: &NanoId) -> Self {
289289+ value.clone().into()
290290+ }
291291+}
292292+293293+impl From<NanoId> for ZettelId {
294294+ fn from(value: NanoId) -> Self {
295295+ Self(value)
296296+ }
297297+}
298298+299299+impl TryFrom<PathBuf> for ZettelId {
300300+ type Error = Error;
301301+302302+ fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
303303+ let path = value.as_path();
304304+ path.try_into()
305305+ }
306306+}
307307+308308+impl TryFrom<&Path> for ZettelId {
309309+ type Error = Error;
310310+311311+ fn try_from(value: &Path) -> Result<Self, Self::Error> {
312312+ let extension = value
313313+ .extension()
314314+ .and_then(|ext| ext.to_str())
315315+ .ok_or_else(|| eyre!("Unable to turn file extension into string".to_owned(),))?;
316316+317317+ if extension != "md" {
318318+ return Err(eyre!(format!("Wrong extension: {extension}, expected .md")));
319319+ }
320320+321321+ let id: Self = value
322322+ .file_name()
323323+ .ok_or_else(|| eyre!("Invalid File Name!".to_owned()))?
324324+ .to_str()
325325+ .ok_or_else(|| eyre!("File Name cannot be translated into str!".to_owned(),))?
326326+ .strip_suffix(".md")
327327+ .expect("we statically verify this right above")
328328+ .into();
329329+330330+ Ok(id)
331331+ }
332332+}
333333+334334+impl Display for ZettelId {
335335+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336336+ f.write_str(&self.0.to_string())
337337+ }
338338+}
339339+340340+impl From<ZettelId> for NanoId {
341341+ fn from(value: ZettelId) -> Self {
342342+ value.0
343343+ }
344344+}