···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.
+86-25
src/types/kasten.rs
···11-use std::path::PathBuf;
22-11+use crate::types::{Link, Zettel, ZettelId};
32use 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};
410511use crate::types::Workspace;
61277-/// The `Kasten` that stores the `Link`s between `Zettel`s
813#[derive(Debug, Clone)]
914#[expect(dead_code)]
1015pub struct Kasten {
1116 /// Private field so it can only be instantiated from a `Path`
1217 _private: (),
1313- // / Connection to the sqlite database inside the `Workspace`
1818+1919+ /// The workspace this `Kasten` is in
1420 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>,
1529}
16303131+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+1738impl Kasten {
1818- /// Given a path, try to construct a `Kasten`.
1919- pub async fn index(root: impl Into<PathBuf>) -> Result<Self> {
2020- let ws = Workspace::instansiate(root).await?;
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>>();
21702222- Ok(Self { _private: (), ws })
2323- }
2424-}
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+ ));
25762626-#[cfg(test)]
2727-mod tests {
2828- use std::{
2929- fs::{File, create_dir_all},
3030- path::PathBuf,
3131- };
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+ }
32883333- use crate::types::Kasten;
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+ }
34963535- #[tokio::test]
3636- async fn test_instantiation() {
3737- let path = PathBuf::from("/tmp/filaments/.filaments/filaments.db");
3838- create_dir_all(path.parent().unwrap()).unwrap();
3939- let _ = File::create(&path).unwrap();
4040- let _k = Kasten::index(dbg!(&path.parent().unwrap().parent().unwrap()))
4141- .await
4242- .unwrap();
9797+ Ok(Self {
9898+ _private: (),
9999+ ws,
100100+ graph,
101101+ zid_to_gid,
102102+ most_recently_edited: None,
103103+ })
43104 }
44105}
+1
src/types/mod.rs
···2525pub use link::Link;
26262727mod kasten;
2828+#[expect(unused_imports)]
2829pub use kasten::Kasten;
29303031mod 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 _k = Workspace::instansiate(dbg!(&path.parent().unwrap().parent().unwrap()))
8888+ let _ws = Workspace::instansiate(&path.parent().unwrap().parent().unwrap())
8989 .await
9090 .unwrap();
9191 }
+47-51
src/types/zettel.rs
···33 QueryFilter, TagActiveModel, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx,
44 ZettelTagActiveModel, ZettelTagColumns, ZettelTagEntity,
55};
66+use pulldown_cmark::{Event, Parser, Tag as MkTag};
67use serde::{Deserialize, Serialize};
78use std::{
89 fmt::Display,
910 path::{Path, PathBuf},
1011};
1111-use tracing::info;
1212+use tracing::{error, info};
12131314use color_eyre::eyre::{Error, Result, eyre};
1415use dto::NanoId;
1516use tokio::{fs::File, io::AsyncWriteExt};
16171717-use crate::types::{FrontMatter, Tag, Workspace};
1818+use crate::types::{FrontMatter, Link, Tag, Workspace};
18191920/// A `Zettel` is a note about a single idea.
2021/// It can have many `Tag`s, just meaning it can fall under many
···3536/// A `ZettelId` is essentially a `NanoId`,
3637/// with some `Zettel` specific helpers written
3738/// onto it
3838-#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
3939+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3940pub struct ZettelId(NanoId);
40414142impl Zettel {
···80818182 /// Returns the most up-to-date `FrontMatter` for this
8283 /// `Zettel`
8383- #[expect(dead_code)]
8484 pub async fn front_matter(&self, ws: &Workspace) -> Result<FrontMatter> {
8585 let path = self.absolute_path(ws);
8686 let (fm, _) = FrontMatter::extract_from_file(path).await?;
···89899090 /// Returns the content of this `Zettel`, which is everything
9191 /// but the `FrontMatter`
9292- #[expect(dead_code)]
9392 pub async fn content(&self, ws: &Workspace) -> Result<String> {
9493 let path = self.absolute_path(ws);
9594 let (_, content) = FrontMatter::extract_from_file(path).await?;
···107106 }
108107109108 /// 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());
···125125 zettel_tag_strings.sort();
126126127127 // get the zettel from the db
128128- let db_zettel: ZettelModelEx = if let Some(z) = ZettelEntity::load()
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- z
134134+ existing_zettel
135135 } else {
136136 // if zettel is missing from db, we just add it here
137137 info!("adding zettel to db");
···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 x = ZettelTagEntity::find()
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");
168168169169- x.into_active_model().delete(&ws.db).await?;
169169+ to_remove.into_active_model().delete(&ws.db).await?;
170170 }
171171 }
172172···209209 .into())
210210 }
211211212212- // pub fn apply_node_transform(&self, node: &mut Node<Zettel, Link>) {
213213- // node.set_label(self.front_matter.title.to_owned());
214214- // let disp = node.display_mut();
215215- // disp.radius = 100.0;
216216- // }
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);
217216218218- // fn links_from_content(src_id: &ZettelId, content: &str, ws: &Workspace) -> ZkResult<Vec<Link>> {
219219- // let parsed = Parser::new(content);
220220-221221- // let mut links = vec![];
217217+ let mut links = vec![];
222218223223- // for event in parsed {
224224- // if let Event::Start(MkTag::Link { dest_url, .. }) = event {
225225- // info!("Found dest_url: {dest_url:#?}");
219219+ for event in parsed {
220220+ if let Event::Start(MkTag::Link { dest_url, .. }) = event {
221221+ info!("Found dest_url: {dest_url:#?}");
226222227227- // let dest_path = {
228228- // // remove leading "./"
229229- // let without_prefix = dest_url.strip_prefix("./").unwrap_or(&dest_url);
223223+ let dest_path = {
224224+ // remove leading "./"
225225+ let without_prefix = dest_url.strip_prefix("./").unwrap_or(&dest_url);
230226231231- // // remove "#" and everything after it
232232- // let without_anchor = without_prefix.split('#').next().unwrap();
227227+ // remove "#" and everything after it
228228+ let without_anchor = without_prefix.split('#').next().unwrap();
233229234234- // // add .md if not present
235235- // let normalized = if without_anchor.ends_with(".md") {
236236- // without_anchor.to_string()
237237- // } else {
238238- // format!("{}.md", without_anchor)
239239- // };
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+ };
240239241241- // let mut tmp_root = ws.root.clone();
242242- // tmp_root.push(normalized);
243243- // tmp_root
244244- // };
245245- // // simplest way to validate that the path exists
246246- // let canon_url = match dest_path.canonicalize() {
247247- // Ok(canon_url) => canon_url,
248248- // Err(_) => {
249249- // error!("Link not found!: {dest_path:?}");
250250- // continue;
251251- // }
252252- // };
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+ };
253249254254- // let dst_id = ZettelId::try_from(canon_url)?;
250250+ let dst_id = ZettelId::try_from(canon_url)?;
255251256256- // let link = Link::new(src_id, dst_id);
252252+ let link = Link::new(self.id.clone(), dst_id);
257253258258- // links.push(link)
259259- // }
260260- // }
254254+ links.push(link);
255255+ }
256256+ }
261257262262- // Ok(links)
263263- // }
258258+ Ok(links)
259259+ }
264260}
265261266262impl From<ZettelModelEx> for Zettel {