···44keymap {
5566 Home {
77- q Quit // Quit the application
87 <Ctrl-c> Quit // Another way to quit
98 <Ctrl-z> Suspend // Suspend the application
1010- h Helix
99+ up MoveUp
1010+ down MoveDown
1111+ enter OpenZettel
1212+ <Ctrl-n> NewZettel
1113 }
1214}
···11+use rand::RngExt;
12use rgb::RGB8;
23use sea_orm::DeriveValueType;
34use std::fmt::{Debug, Display};
···4950 c.to_rgb8()
5051 }
5152}
5353+5454+impl Default for Color {
5555+ fn default() -> Self {
5656+ let mut rng = rand::rng();
5757+ let r = rng.random_range(0..=255);
5858+ let g = rng.random_range(0..=255);
5959+ let b = rng.random_range(0..=255);
6060+6161+ Self::new(r, g, b)
6262+ }
6363+}
+1-1
crates/dto/src/entity/prelude.rs
···11//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
22-32#![expect(unused_imports)]
33+44pub use super::group::Entity as Group;
55pub use super::tag::Entity as Tag;
66pub use super::task::Entity as Task;
···1212pub use sea_orm::EntityTrait;
1313pub use sea_orm::IntoActiveModel;
1414pub use sea_orm::QueryFilter;
1515+pub use sea_orm::QueryOrder;
15161617/// Exporting this as a generic NanoId.
1718pub use migration::types::NanoId;
···11use crate::types::{Link, Zettel, ZettelId};
22use color_eyre::eyre::Result;
33+use dto::{TagEntity, ZettelEntity};
34use eframe::emath;
45use egui_graphs::{
55- Graph,
66- petgraph::{Directed, graph::NodeIndex, prelude::StableGraph},
66+ Graph, Node,
77+ petgraph::{Directed, Direction, graph::NodeIndex, prelude::StableGraph, visit::EdgeRef},
78};
89use rayon::iter::{ParallelBridge as _, ParallelIterator as _};
99-use std::{cmp::max, collections::HashMap};
1010+use std::{cmp::max, collections::HashMap, path::Path, sync::Arc};
1111+use tokio::sync::RwLock;
10121113use crate::types::Workspace;
1214···3537/// Arbitrarily chosen minimum number of edges
3638const GRAPH_MIN_EDGES: usize = GRAPH_MIN_NODES * 3;
37394040+pub type KastenHandle = Arc<RwLock<Kasten>>;
4141+3842impl Kasten {
3943 /// Indexes the `Workspace` and constructs a `Kasten`
4040- #[expect(dead_code)]
4144 pub async fn index(ws: Workspace) -> Result<Self> {
4245 let paths = std::fs::read_dir(&ws.root)?
4346 .par_bridge()
···101104 zid_to_gid,
102105 most_recently_edited: None,
103106 })
107107+ }
108108+109109+ /// processes the `Zettel` for the provided `ZettelId`,
110110+ /// meaning it updates the internal state of the `Kasten`
111111+ /// with the changes in `Zettel`.
112112+ pub async fn process_path(&mut self, path: &Path) -> Result<()> {
113113+ //NOTE: need to clone to get around borrowing rules but
114114+ // ideally we dont have to do this, kind of cringe imo.
115115+ let ws = self.ws.clone();
116116+117117+ let zid = ZettelId::try_from(path)?;
118118+119119+ let mut gid = self.zid_to_gid.get(&zid).copied();
120120+ // sometimes this zid is new, so it wont be in the kasten
121121+ let zettel = if let Some(existing) = self.get_node_by_zettel_id_mut(&zid) {
122122+ existing.payload_mut()
123123+ } else {
124124+ // this should aleady be in the database though so lets get it from there first
125125+ let zettel: Zettel = ZettelEntity::load()
126126+ .filter_by_nano_id(zid)
127127+ .with(TagEntity)
128128+ .one(&ws.db)
129129+ .await?
130130+ .expect("This should be in the database already")
131131+ .into();
132132+133133+ let zid = zettel.id.clone();
134134+ let idx = self.graph.add_node(zettel);
135135+136136+ self.zid_to_gid.insert(zid.clone(), idx);
137137+138138+ gid = Some(idx);
139139+140140+ self.get_node_by_zettel_id_mut(&zid)
141141+ .expect("we just inserted it")
142142+ .payload_mut()
143143+ };
144144+145145+ // and then we sync with the file
146146+ zettel.sync_with_file(&ws).await?;
147147+148148+ // to get past borrowchecker rules
149149+ let zettel = zettel.clone();
150150+151151+ // gid must be set
152152+ let gid = gid.unwrap();
153153+154154+ // and now we manage the links going out of the file
155155+156156+ // remove all the old shit
157157+ self.graph
158158+ .edges_directed(gid, Direction::Outgoing)
159159+ .map(|e| e.id())
160160+ .collect::<Vec<_>>()
161161+ .into_iter()
162162+ .for_each(|e| {
163163+ let _ = self.graph.remove_edge(e);
164164+ });
165165+166166+ // add the links that actually exist
167167+ zettel.links(&ws).await?.into_iter().for_each(|link| {
168168+ // this is an option because a user c
169169+ let dest = self
170170+ .zid_to_gid
171171+ .get(&link.dest)
172172+ .expect("Links should be valid");
173173+174174+ self.graph.add_edge(gid, *dest, link);
175175+ });
176176+177177+ Ok(())
178178+ }
179179+180180+ pub fn get_node_by_zettel_id(&self, id: &ZettelId) -> Option<&Node<Zettel, Link>> {
181181+ let idx = self.zid_to_gid.get(id)?;
182182+183183+ let node = self.graph.node(*idx).expect(
184184+ "invariant broken if internal hashmap is not uptodate with
185185+ the state of the graph...",
186186+ );
187187+ Some(node)
188188+ }
189189+190190+ pub fn get_node_by_zettel_id_mut(&mut self, id: &ZettelId) -> Option<&mut Node<Zettel, Link>> {
191191+ let idx = self.zid_to_gid.get(id)?;
192192+193193+ let node = self.graph.node_mut(*idx).expect(
194194+ "invariant broken if internal hashmap is not uptodate with the
195195+ state of the graph...",
196196+ );
197197+198198+ Some(node)
104199 }
105200}
+1-1
src/types/mod.rs
···2525pub use link::Link;
26262727mod kasten;
2828-#[expect(unused_imports)]
2928pub use kasten::Kasten;
2929+pub use kasten::KastenHandle;
30303131mod frontmatter;
3232pub use frontmatter::FrontMatter;
-1
src/types/tag.rs
···4455/// Represents a `Tag` in a `ZettelKasten` note taking method.
66/// Easy way to link multiple notes under one simple word.
77-#[expect(dead_code)]
87#[derive(Debug, Clone)]
98pub struct Tag {
109 /// Should only be constructed from models.
+124-54
src/types/zettel.rs
···11use dto::{
22 ActiveModelTrait, ActiveValue, ColumnTrait, DateTime, EntityTrait as _, IntoActiveModel,
33- QueryFilter, TagActiveModel, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx,
44- ZettelTagActiveModel, ZettelTagColumns, ZettelTagEntity,
33+ QueryFilter, TagActiveModel, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModel,
44+ ZettelModelEx, ZettelTagActiveModel, ZettelTagColumns, ZettelTagEntity,
55};
66use pulldown_cmark::{Event, Parser, Tag as MkTag};
77use serde::{Deserialize, Serialize};
···1515use dto::NanoId;
1616use tokio::{fs::File, io::AsyncWriteExt};
17171818-use crate::types::{FrontMatter, Link, Tag, Workspace};
1818+use crate::types::{FrontMatter, Link, Tag, Workspace, frontmatter};
19192020/// A `Zettel` is a note about a single idea.
2121/// It can have many `Tag`s, just meaning it can fall under many
2222/// categories.
2323-#[expect(dead_code)]
2423#[derive(Debug, Clone)]
2524pub struct Zettel {
2625 /// Should only be constructed from models.
···3029 /// a workspace-local file path, needs to be canonicalized before usage
3130 pub file_path: PathBuf,
3231 pub created_at: DateTime,
3232+ pub modified_at: DateTime,
3333 pub tags: Vec<Tag>,
3434}
3535···7979 Ok(zettel.into())
8080 }
81818282+ pub async fn sync_with_file(&mut self, ws: &Workspace) -> Result<()> {
8383+ let (fm, _) = FrontMatter::extract_from_file(self.absolute_path(ws)).await?;
8484+8585+ let mut model = ZettelEntity::find_by_nano_id(self.id.clone())
8686+ .one(&ws.db)
8787+ .await?
8888+ .expect("this must exist")
8989+ .into_active_model();
9090+9191+ model.title = ActiveValue::Set(fm.title);
9292+9393+ let updated: ZettelModel = model.update(&ws.db).await?;
9494+9595+ self.title = updated.title;
9696+ self.modified_at = updated.modified_at;
9797+ self.created_at = updated.created_at;
9898+9999+ self.sync_tags(ws).await?;
100100+101101+ Ok(())
102102+ }
103103+104104+ /// Sync's `Tag`'s that are present in the frontmatter of this
105105+ /// `Zettel` to the database, and then updates the `Tag`s on the
106106+ /// `Zettel` to reflect the changes.
107107+ pub async fn sync_tags(&mut self, ws: &Workspace) -> Result<()> {
108108+ let mut fm = self.front_matter(ws).await?;
109109+ fm.tag_strings.sort();
110110+111111+ let mut tag_strings = fm.tag_strings;
112112+113113+ let Some(db_zettel): Option<ZettelModelEx> = ZettelEntity::load()
114114+ .with(TagEntity)
115115+ .filter_by_nano_id(self.id.clone())
116116+ .one(&ws.db)
117117+ .await?
118118+ else {
119119+ panic!("how the fuck was this deleted");
120120+ };
121121+122122+ for db_tag in db_zettel.tags {
123123+ if let Ok(idx) = tag_strings.binary_search(&db_tag.name) {
124124+ // we remove tags we have already processed
125125+ tag_strings.remove(idx);
126126+ } else {
127127+ // the db says the file has tag `x`, but that tag is missing from the
128128+ // front matter, we can assume its gone, lets delete that link
129129+ let to_remove = ZettelTagEntity::find()
130130+ .filter(ZettelTagColumns::ZettelNanoId.eq(self.id.0.clone()))
131131+ .filter(ZettelTagColumns::TagNanoId.eq(db_tag.nano_id))
132132+ .one(&ws.db)
133133+ .await?
134134+ .expect("this link must exist");
135135+136136+ to_remove.into_active_model().delete(&ws.db).await?;
137137+ }
138138+ }
139139+140140+ // now any tags that are left inside zettel_tag_strings,
141141+ // we have to look up the tags in the db and then reset them?
142142+ for tag_str in tag_strings {
143143+ // this is the tag that either already exists with this name, or we just created this new one
144144+ let tag = if let Some(existing) = TagEntity::load()
145145+ .filter_by_name(&tag_str)
146146+ .one(&ws.db)
147147+ .await?
148148+ {
149149+ existing
150150+ } else {
151151+ let am = TagActiveModel {
152152+ name: ActiveValue::Set(tag_str),
153153+ ..Default::default()
154154+ };
155155+156156+ am.insert(&ws.db).await?.into()
157157+ };
158158+159159+ // this zettel has this tag now
160160+ let _ = ZettelTagActiveModel {
161161+ zettel_nano_id: ActiveValue::Set(self.id.to_string()),
162162+ tag_nano_id: ActiveValue::Set(tag.nano_id.to_string()),
163163+ }
164164+ .insert(&ws.db)
165165+ .await?;
166166+ }
167167+168168+ let entity = ZettelEntity::load()
169169+ .with(TagEntity)
170170+ .filter_by_nano_id(self.id.clone())
171171+ .one(&ws.db)
172172+ .await?
173173+ .expect("this exists");
174174+175175+ let temp_zettel: Self = entity.into();
176176+177177+ self.tags = temp_zettel.tags;
178178+179179+ Ok(())
180180+ }
181181+82182 /// Returns the most up-to-date `FrontMatter` for this
83183 /// `Zettel`
84184 pub async fn front_matter(&self, ws: &Workspace) -> Result<FrontMatter> {
···101201 Ok(File::open(path).await?)
102202 }
103203104104- fn absolute_path(&self, ws: &Workspace) -> PathBuf {
204204+ pub fn absolute_path(&self, ws: &Workspace) -> PathBuf {
105205 ws.root.clone().join(&self.file_path)
106206 }
107207108208 /// uses the id and root to parse out of the root directory
109109- #[expect(dead_code)]
110209 pub async fn from_id(id: &ZettelId, ws: &Workspace) -> Result<Self> {
111210 let mut path = ws.root.clone();
112211 path.push(id.0.to_string());
113212 Self::from_path(path, ws).await
114213 }
115214215215+ pub fn created_at(&self) -> String {
216216+ self.created_at
217217+ .format(frontmatter::DATE_FMT_STR)
218218+ .to_string()
219219+ }
220220+221221+ pub fn modified_at(&self) -> String {
222222+ self.modified_at
223223+ .format(frontmatter::DATE_FMT_STR)
224224+ .to_string()
225225+ }
226226+116227 pub async fn from_path(path: impl Into<PathBuf>, ws: &Workspace) -> Result<Self> {
117228 let path: PathBuf = path.into();
118229119230 let id = ZettelId::try_from(path.as_path())?;
120231121232 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();
126233127234 // get the zettel from the db
128235 let db_zettel: ZettelModelEx = if let Some(existing_zettel) = ZettelEntity::load()
···151258 .expect("we just inserted the zettel")
152259 };
153260154154- // 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- }
261261+ let mut temp_zettel: Self = db_zettel.clone().into();
262262+ temp_zettel.sync_tags(ws).await?;
192263193264 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-265265+ let mut am = db_zettel.into_active_model();
266266+ am.title = ActiveValue::Set(front_matter.title.clone());
200267 am.update(&ws.db).await?;
201268 }
202269···247314 continue;
248315 };
249316317317+ // TODO: check that the thing actually exists inside the ws.db
318318+ // instead of just seeing if we can turn it into a ZettelId
250319 let dst_id = ZettelId::try_from(canon_url)?;
251320252321 let link = Link::new(self.id.clone(), dst_id);
···273342 title: value.title,
274343 file_path: value.file_path.into(),
275344 created_at: value.created_at,
345345+ modified_at: value.modified_at,
276346 tags: value.tags.into_iter().map(Into::into).collect(),
277347 }
278348 }