···11use async_trait::async_trait;
22-use color_eyre::eyre::Result;
22+use color_eyre::eyre::{ContextCompat, Result};
33use crossterm::event::KeyEvent;
44use dto::{QueryOrder, TagEntity, ZettelColumns, ZettelEntity};
55use ratatui::{prelude::*, widgets::ListState};
···58585959impl Zk<'_> {
6060 pub async fn new(kh: KastenHandle) -> Result<Self> {
6161- let kt = kh.read().await;
6262- let ws = kt.ws.clone();
6363-6461 let fetch_all = async || -> Result<Vec<Zettel>> {
6562 Ok(ZettelEntity::load()
6663 .with(TagEntity)
6764 .order_by_desc(ZettelColumns::ModifiedAt)
6868- .all(&ws.db)
6565+ .all(&kh.read().await.db)
6966 .await?
7067 .into_iter()
7168 .map(Into::into)
···74717572 let mut zettels: Vec<Zettel> = fetch_all().await?;
76737777- drop(kt);
7874 if zettels.is_empty() {
7979- let z = Zettel::new("Welcome!", &ws).await?;
8080- kh.write().await.process_path(&z.absolute_path(&ws)).await?;
7575+ let _ = Zettel::new("Welcome!", &mut *kh.write().await).await?;
8176 zettels = fetch_all().await?;
8277 }
8378···8580 // stuff inside the init function
8681 let mut l_state = ListState::default();
8782 l_state.select_first();
8888- let zettel_list = ZettelList::new(zettels, l_state, 0);
8383+ let zettel_list = ZettelList::new(zettels.clone(), l_state, 0);
89849085 let selected_zettel = zettel_list
9186 .id_list
···10398 info!("{selected_zettel:#?}");
10499 info!("{kt:#?}");
105100106106- let zettel = kt
107107- .get_node_by_zettel_id(selected_zettel)
108108- .expect("kasten should have the selected zettel")
109109- .payload();
101101+ let zettel = zettels
102102+ .iter()
103103+ .find(|&z| &z.id == selected_zettel)
104104+ .expect("we selected it out of the list so it must exist");
110105111111- let preview = Preview::from(
112112- zettel
113113- .content(&kt.ws)
114114- .await
115115- .expect("This thing cannot be parsed properly..."),
116116- );
106106+ let preview = Preview::from(zettel.content(&kt.index).clone());
117107118108 // okay now that we have the zettel we need to construct the zettel out of this id
119119- let zettel_view: ZettelView = kt
120120- .get_node_by_zettel_id(selected_zettel)
121121- .expect("must exist, handle case where it doesnt later...")
122122- .payload()
123123- .into();
124124-125125- let ws = kt.ws.clone();
109109+ let zettel_view: ZettelView = zettel.into();
126110127111 drop(kt);
128112129113 Ok(Self {
130114 signal_tx: None,
115115+ search: Search::new(kh.clone()),
131116 kh,
132117 layouts: Layouts::default(),
133118 zettel_list,
134119 zettel_view,
135120 preview,
136136- search: Search::new(ws),
137121 })
138122 }
139123···146130147131 // sometimes the selection we get is over the length of the thing, so its
148132 // actually fine if this is none, just means we reached the end of the list
149149- let Some(z_id) = self.zettel_list.id_list.get(selection_idx) else {
133133+ let Some(zid) = self.zettel_list.id_list.get(selection_idx) else {
150134 return Ok(());
151135 };
152136153137 let kh = self.kh.read().await;
154138155155- self.zettel_view = kh
156156- .get_node_by_zettel_id(z_id)
157157- .expect("this should be valid unless the kasten changed out underneath us")
158158- .payload()
159159- .into();
160160-161161- self.preview = kh
162162- .get_node_by_zettel_id(z_id)
163163- .expect("this should be valid unless the kasten changed out underneath us")
164164- .payload()
165165- .content(&kh.ws)
139139+ let zettel = &Zettel::fetch_from_db(zid, &kh.db)
166140 .await?
167167- .into();
141141+ .context("Unknown Behaviour, A selected zettel got deleted somehow.")?;
142142+143143+ self.zettel_view = zettel.into();
144144+145145+ self.preview = zettel.content(&kh.index).clone().into();
146146+168147 drop(kh);
169148170149 Ok(())
···175154 let models = ZettelEntity::load()
176155 .with(TagEntity)
177156 .order_by_desc(ZettelColumns::ModifiedAt)
178178- .all(&kt.ws.db)
157157+ .all(&kt.db)
179158 .await?;
180159181160 // im being a good boy and dropping this as soon as im done with the db
···250229 };
251230252231 let kh = self.kh.read().await;
253253- let path = kh
254254- .get_node_by_zettel_id(zid)
255255- .expect(
256256- "This should not have
257257- change dout underneath us",
258258- )
259259- .payload()
260260- .absolute_path(&kh.ws);
232232+233233+ let path = kh.index.get_zod(zid).path.clone();
261234262235 drop(kh);
263236···267240 Signal::NewZettel => {
268241 // what the fuck am i going to do in here
269242270270- let ws = &self.kh.read().await.ws;
243243+ let mut kt = self.kh.write().await;
271244272245 // we create the zettel with the query as the
273273- let z = Zettel::new(self.search.query(), ws).await?;
246246+ let z = Zettel::new(self.search.query(), &mut kt).await?;
247247+ let path = z.absolute_path(&kt.index).to_path_buf();
274248275275- let path = z.absolute_path(ws);
249249+ drop(kt);
276250277251 return Ok(Some(Signal::Helix { path }));
278252 }
···284258 .selected()
285259 .expect("This must be the zettel we just edited");
286260287287- let Some(id) = self.zettel_list.id_list.get(selected) else {
261261+ let Some(zid) = self.zettel_list.id_list.get(selected) else {
288262 return Ok(None);
289263 };
290264291265 let kt = self.kh.read().await;
292266293293- let node = kt
294294- .get_node_by_zettel_id(id)
295295- .expect("Invariant broken, this must exist.");
267267+ let zettel = Zettel::fetch_from_db(zid, &kt.db)
268268+ .await?
269269+ .expect("invariant broken, we just closed this zettel");
296270297271 // reset the state of the component
298272 self.search.clear_query();
···305279 self.zettel_list.width,
306280 );
307281308308- self.zettel_view = ZettelView::from(node.payload());
309309- self.preview = Preview::from(node.payload().content(&kt.ws).await?);
282282+ self.zettel_view = ZettelView::from(&zettel);
283283+ self.preview = Preview::from(zettel.content(&kt.index).clone());
310284 drop(kt);
311285 }
312286
···11+use std::{
22+ collections::HashMap,
33+ path::{Path, PathBuf},
44+};
55+66+use color_eyre::eyre::Result;
77+use dto::{
88+ ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel,
99+ QueryFilter, TagActiveModel, TagEntity, ZettelEntity, ZettelModelEx, ZettelTagActiveModel,
1010+ ZettelTagColumns, ZettelTagEntity,
1111+};
1212+use rayon::iter::{ParallelBridge, ParallelIterator};
1313+use tracing::info;
1414+1515+use crate::types::{FrontMatter, ZettelId, frontmatter::Body};
1616+1717+#[derive(Debug, Clone)]
1818+pub struct Index {
1919+ pub(super) zods: HashMap<ZettelId, ZettelOnDisk>,
2020+}
2121+2222+#[derive(Debug, Clone)]
2323+pub struct ZettelOnDisk {
2424+ pub fm: FrontMatter,
2525+ pub body: Body,
2626+ pub path: PathBuf,
2727+}
2828+2929+impl Index {
3030+ /// Parses the `root` path to construct an `Index`.
3131+ pub fn tabulate(root: &Path) -> Result<Self> {
3232+ let root = root.canonicalize()?;
3333+3434+ let mut zods = HashMap::new();
3535+3636+ std::fs::read_dir(root)?
3737+ .par_bridge()
3838+ .flatten()
3939+ .filter(|entry| {
4040+ entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
4141+ && entry
4242+ .path()
4343+ .extension()
4444+ .and_then(|ext| ext.to_str())
4545+ .is_some_and(|ext| ext == "md")
4646+ })
4747+ .map(|entry| -> Result<(ZettelId, ZettelOnDisk)> {
4848+ let path = entry.path();
4949+ let id: ZettelId = path.as_path().try_into()?;
5050+ let (fm, body) = FrontMatter::extract_from_file(&path)?;
5151+5252+ Ok((id, ZettelOnDisk { fm, body, path }))
5353+ })
5454+ .collect::<Result<Vec<_>>>()?
5555+ .into_iter()
5656+ // .par_bridge()
5757+ .for_each(|(id, zod)| {
5858+ zods.insert(id, zod);
5959+ });
6060+6161+ Ok(Self { zods })
6262+ }
6363+6464+ pub fn update_path_for_zid(&mut self, zid: &ZettelId, new_path: PathBuf) {
6565+ self.get_zod_mut(zid).path = new_path;
6666+ }
6767+6868+ /// Updates the interal state of the `Index` for the provided `Zid`.
6969+ pub fn process_zid(&mut self, zid: &ZettelId) -> Result<()> {
7070+ let zod = self.get_zod_mut(zid);
7171+7272+ let (fm, body) = FrontMatter::extract_from_file(&zod.path)?;
7373+7474+ zod.fm = fm;
7575+ zod.body = body;
7676+7777+ Ok(())
7878+ }
7979+8080+ /// Sync's the curren title of the `Zettel` with the
8181+ /// provided `zid` with the `DB`
8282+ pub async fn sync_title_with_db(
8383+ &mut self,
8484+ zid: &ZettelId,
8585+ db: &DatabaseConnection,
8686+ ) -> Result<()> {
8787+ let fm = &mut self.get_zod_mut(zid).fm;
8888+8989+ let mut model = ZettelEntity::find_by_nano_id(zid.clone())
9090+ .one(db)
9191+ .await?
9292+ .expect("this must exist")
9393+ .into_active_model();
9494+9595+ model.title = ActiveValue::Set(fm.title.clone());
9696+9797+ model.update(db).await?;
9898+9999+ info!("We updated the zettel: {zid:#?}");
100100+101101+ Ok(())
102102+ }
103103+104104+ /// Sync's `Tag`'s that are present in the frontmatter of this
105105+ /// `Zettel` to the database.
106106+ pub async fn sync_tags_with_db(
107107+ &mut self,
108108+ zid: &ZettelId,
109109+ db: &DatabaseConnection,
110110+ ) -> Result<()> {
111111+ let fm = &mut self.get_zod_mut(zid).fm;
112112+113113+ let mut tag_strings = fm.tag_strings.clone();
114114+115115+ tag_strings.sort();
116116+117117+ let db_zettel: ZettelModelEx = ZettelEntity::load()
118118+ .with(TagEntity)
119119+ .filter_by_nano_id(zid.clone())
120120+ .one(db)
121121+ .await?
122122+ .expect("Invariant broken, zettel should not be deleted");
123123+124124+ for db_tag in db_zettel.tags {
125125+ if let Ok(idx) = tag_strings.binary_search(&db_tag.name) {
126126+ // we remove tags we have already processed
127127+ tag_strings.remove(idx);
128128+ } else {
129129+ // the db says the file has tag `x`, but that tag is missing from the
130130+ // front matter, we can assume its gone, lets delete that link
131131+ let to_remove = ZettelTagEntity::find()
132132+ .filter(ZettelTagColumns::ZettelNanoId.eq(zid.to_string()))
133133+ .filter(ZettelTagColumns::TagNanoId.eq(db_tag.nano_id))
134134+ .one(db)
135135+ .await?
136136+ .expect("this link must exist");
137137+138138+ to_remove.into_active_model().delete(db).await?;
139139+ }
140140+ }
141141+142142+ // now any tags that are left inside zettel_tag_strings,
143143+ // we have to look up the tags in the db and then reset them?
144144+ for tag_str in tag_strings {
145145+ // this is the tag that either already exists with this name, or we just created this new one
146146+ let tag =
147147+ if let Some(existing) = TagEntity::load().filter_by_name(&tag_str).one(db).await? {
148148+ existing
149149+ } else {
150150+ let am = TagActiveModel {
151151+ name: ActiveValue::Set(tag_str.clone()),
152152+ ..Default::default()
153153+ };
154154+155155+ am.insert(db).await?.into()
156156+ };
157157+158158+ // this zettel has this tag now
159159+ let _ = ZettelTagActiveModel {
160160+ zettel_nano_id: ActiveValue::Set(zid.to_string()),
161161+ tag_nano_id: ActiveValue::Set(tag.nano_id.to_string()),
162162+ }
163163+ .insert(db)
164164+ .await?;
165165+ }
166166+ Ok(())
167167+ }
168168+169169+ //TODO:need to process
170170+171171+ pub fn get_zod(&self, zid: &ZettelId) -> &ZettelOnDisk {
172172+ self.zods.get(zid).expect("Invariant broken. Any zid we lookup must exist in the index, otherwise the db is corrupt or not sync'd.")
173173+ }
174174+175175+ fn get_zod_mut(&mut self, zid: &ZettelId) -> &mut ZettelOnDisk {
176176+ self.zods.get_mut(zid).expect("Invariant broken. Any zid we lookup must exist in the index, otherwise the db is corrupt or not sync'd.")
177177+ }
178178+179179+ pub const fn zods(&self) -> &HashMap<ZettelId, ZettelOnDisk> {
180180+ &self.zods
181181+ }
182182+183183+ pub fn sync_with_db(&self, _db: &DatabaseConnection) {
184184+ todo!()
185185+ }
186186+187187+ //NOTE: we dont support links just yet
188188+ // fn parse_links(src: &ZettelId, body: Body) -> Result<Vec<Link>> {
189189+ // let parsed = Parser::new(&body);
190190+191191+ // let mut links = vec![];
192192+193193+ // for event in parsed {
194194+ // if let Event::Start(MkTag::Link { dest_url, .. }) = event {
195195+ // info!("Found dest_url: {dest_url:#?}");
196196+197197+ // let dest_path = {
198198+ // // remove leading "./"
199199+ // let without_prefix = dest_url.strip_prefix("./").unwrap_or(&dest_url);
200200+201201+ // // remove "#" and everything after it
202202+ // let without_anchor = without_prefix.split('#').next().unwrap();
203203+204204+ // // add .md if not present
205205+ // let normalized = if std::path::Path::new(without_anchor)
206206+ // .extension()
207207+ // .is_some_and(|ext| ext.eq_ignore_ascii_case("md"))
208208+ // {
209209+ // without_anchor.to_string()
210210+ // } else {
211211+ // format!("{without_anchor}.md")
212212+ // };
213213+214214+ // let mut tmp_root = self
215215+ // .zods
216216+ // .get(src)
217217+ // .expect("Invariant Broken! src must exist inside index")
218218+ // .path
219219+ // .clone();
220220+ // tmp_root.push(normalized);
221221+ // tmp_root
222222+ // };
223223+ // // simplest way to validate that the path exists
224224+ // let Ok(canon_url) = dest_path.canonicalize() else {
225225+ // error!("Link not found!: {dest_path:?}");
226226+ // continue;
227227+ // };
228228+229229+ // // TODO: check that the thing actually exists inside the ws.db
230230+ // // instead of just seeing if we can turn it into a ZettelId
231231+ // let dst_id = ZettelId::try_from(canon_url)?;
232232+233233+ // let link = Link::new(src.clone(), dst_id);
234234+235235+ // links.push(link);
236236+ // }
237237+ // }
238238+239239+ // Ok(links)
240240+ // }
241241+}
+67-176
src/types/kasten.rs
···11-use crate::types::{Link, Zettel, ZettelId};
22-use color_eyre::eyre::Result;
33-use dto::{TagEntity, ZettelEntity};
44-use eframe::emath;
55-use egui_graphs::{
66- Graph, Node,
77- petgraph::{Directed, Direction, graph::NodeIndex, prelude::StableGraph, visit::EdgeRef},
11+use std::{
22+ path::{Path, PathBuf},
33+ sync::Arc,
44+};
55+66+use color_eyre::eyre::{Context, Result};
77+use dto::{Database, DatabaseConnection, Migrator, MigratorTrait};
88+use tokio::{
99+ fs::{File, create_dir_all},
1010+ sync::RwLock,
811};
99-use rayon::iter::{ParallelBridge as _, ParallelIterator as _};
1010-use std::{cmp::max, collections::HashMap, path::Path, sync::Arc};
1111-use tokio::sync::RwLock;
1212-use tracing::{debug, error};
1212+use tracing::debug;
13131414-use crate::types::Workspace;
1414+use crate::types::{Index, ZettelId};
15151616#[derive(Debug, Clone)]
1717-#[expect(dead_code)]
1817pub struct Kasten {
1918 /// Private field so it can only be instantiated from a `Path`
2019 _private: (),
21202222- /// The workspace this `Kasten` is in
2323- pub ws: Workspace,
2121+ pub root: PathBuf,
24222525- /// the graph of `Zettel`s and the `Links` between them
2626- pub graph: ZkGraph,
2323+ pub index: Index,
27242828- /// simple conversions
2929- zid_to_gid: HashMap<ZettelId, NodeIndex>,
3030-3131- pub most_recently_edited: Option<NodeIndex>,
2525+ pub db: DatabaseConnection,
3226}
33273434-pub type ZkGraph = Graph<Zettel, Link, Directed>;
3535-3636-/// Minimum number of nodes in our graph.
3737-const GRAPH_MIN_NODES: usize = 128;
3838-/// Arbitrarily chosen minimum number of edges
3939-const GRAPH_MIN_EDGES: usize = GRAPH_MIN_NODES * 3;
4040-4128pub type KastenHandle = Arc<RwLock<Kasten>>;
42294330impl Kasten {
4444- /// Indexes the `Workspace` and constructs a `Kasten`
4545- pub async fn index(ws: Workspace) -> Result<Self> {
4646- let paths = std::fs::read_dir(&ws.root)?
4747- .par_bridge()
4848- .flatten()
4949- .filter(|entry| {
5050- entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
5151- && entry
5252- .path()
5353- .extension()
5454- .and_then(|ext| ext.to_str())
5555- .is_some_and(|ext| ext == "md")
5656- })
5757- .map(|entry| entry.path())
5858- .collect::<Vec<_>>();
3131+ /// Given a path, try to construct a `Kasten` based on its contents.
3232+ ///
3333+ /// Note: this means that there should already exist a valid `Kasten`
3434+ /// at that path.
3535+ pub async fn instansiate(root: impl Into<PathBuf>) -> Result<Self> {
3636+ let root = root.into();
59376060- debug!(
6161- "indexing the following paths {paths:#?} at root {:#?}",
6262- ws.root
3838+ let db_conn_string = format!(
3939+ "sqlite://{}",
4040+ root.clone()
4141+ .join(".filaments/filaments.db")
4242+ .canonicalize()
4343+ .context("Invalid Filaments workspace!!")?
4444+ .to_string_lossy()
6345 );
64466565- let zettel_tasks = paths
6666- .into_iter()
6767- .map(|path| {
6868- let ws = ws.clone();
6969- tokio::spawn(async move { Zettel::from_path(path, &ws).await })
7070- })
7171- .collect::<Vec<_>>();
4747+ debug!("connecting to {db_conn_string}");
72487373- // await all of them
7474- let zettels = futures::future::join_all(zettel_tasks)
4949+ let conn = Database::connect(db_conn_string)
7550 .await
7676- .into_iter()
7777- .filter_map(|result| {
7878- result
7979- .inspect_err(|e| error!("Failed to join on zettel task parsing: {e:#?}"))
8080- .ok()?
8181- .inspect_err(|e| error!("Failed to parse file into zettel: {e:#?}"))
8282- .ok()
8383- })
8484- .collect::<Vec<Zettel>>();
5151+ .context("Failed to connect to the database in the filaments workspace!")?;
5252+5353+ let index = Index::tabulate(&root)?;
5454+5555+ // run da migrations every time we connect, just in case
5656+ Migrator::up(&conn, None).await?;
5757+5858+ Ok(Self {
5959+ _private: (),
6060+ db: conn,
6161+ root,
6262+ index,
6363+ })
6464+ }
85658686- debug!("parsed zettels: {zettels:#?}");
6666+ /// Create a new `Kasten` at the provided `path`.
6767+ pub async fn initialize(path: impl Into<PathBuf>) -> Result<Self> {
6868+ let path = path.into();
87698888- // capacity!
8989- let mut graph: ZkGraph = ZkGraph::from(&StableGraph::with_capacity(
9090- max(zettels.len() * 2, GRAPH_MIN_EDGES),
9191- max(zettels.len() * 3, GRAPH_MIN_EDGES),
9292- ));
7070+ let filaments_dir = path.join(".filaments");
93719494- let mut zid_to_gid = HashMap::new();
9595- for zettel in &zettels {
9696- let fm = zettel.front_matter(&ws).await?;
9797- let id = graph.add_node_custom(zettel.clone(), |node| {
9898- fm.apply_node_transform(node);
9999- let x = rand::random_range(0.0..=100.0);
100100- let y = rand::random_range(0.0..=100.0);
101101- node.set_location(emath::Pos2 { x, y });
102102- });
103103- zid_to_gid.insert(zettel.id.clone(), id);
104104- }
7272+ // create the dir
7373+ create_dir_all(&filaments_dir)
7474+ .await
7575+ .context("Failed to create the filaments directory!")?;
10576106106- for zettel in &zettels {
107107- let src = zid_to_gid.get(&zettel.id).expect("must exist");
108108- for link in &zettel.links(&ws).await? {
109109- let dst = zid_to_gid.get(&link.dest).expect("must exist");
110110- graph.add_edge(*src, *dst, link.clone());
111111- }
112112- }
7777+ let filaments_dir = filaments_dir.canonicalize()?;
11378114114- debug!("parsed graph: {graph:#?}");
7979+ File::create(filaments_dir.join("filaments.db")).await?;
11580116116- Ok(Self {
117117- _private: (),
118118- ws,
119119- graph,
120120- zid_to_gid,
121121- most_recently_edited: None,
122122- })
8181+ Ok(Self::instansiate(&path).await.expect(
8282+ "Invariant broken. This instantiation call must always work \
8383+ since we just initialized the workspace.",
8484+ ))
12385 }
1248612587 /// processes the `Zettel` for the provided `ZettelId`,
12688 /// meaning it updates the internal state of the `Kasten`
12789 /// with the changes in `Zettel`.
128128- pub async fn process_path(&mut self, path: &Path) -> Result<()> {
9090+ pub async fn process_path(&mut self, path: impl AsRef<Path>) -> Result<()> {
12991 //NOTE: need to clone to get around borrowing rules but
13092 // ideally we dont have to do this, kind of cringe imo.
131131- let ws = self.ws.clone();
132939494+ let path = path.as_ref();
13395 let zid = ZettelId::try_from(path)?;
13496135135- let mut gid = self.zid_to_gid.get(&zid).copied();
136136- // sometimes this zid is new, so it wont be in the kasten
137137- let zettel = if let Some(existing) = self.get_node_by_zettel_id_mut(&zid) {
138138- existing.payload_mut()
139139- } else {
140140- // this should aleady be in the database though so lets get it from there first
141141- let zettel: Zettel = ZettelEntity::load()
142142- .filter_by_nano_id(zid)
143143- .with(TagEntity)
144144- .one(&ws.db)
145145- .await?
146146- .expect("This should be in the database already")
147147- .into();
148148-149149- let zid = zettel.id.clone();
150150- let idx = self.graph.add_node(zettel);
151151-152152- self.zid_to_gid.insert(zid.clone(), idx);
153153-154154- gid = Some(idx);
155155-156156- self.get_node_by_zettel_id_mut(&zid)
157157- .expect("we just inserted it")
158158- .payload_mut()
159159- };
160160-161161- // and then we sync with the file
162162- zettel.sync_with_file(&ws).await?;
163163-164164- // to get past borrowchecker rules
165165- let zettel = zettel.clone();
166166-167167- // gid must be set
168168- let gid = gid.unwrap();
169169-170170- // and now we manage the links going out of the file
171171-172172- // remove all the old shit
173173- self.graph
174174- .edges_directed(gid, Direction::Outgoing)
175175- .map(|e| e.id())
176176- .collect::<Vec<_>>()
177177- .into_iter()
178178- .for_each(|e| {
179179- let _ = self.graph.remove_edge(e);
180180- });
181181-182182- // add the links that actually exist
183183- zettel.links(&ws).await?.into_iter().for_each(|link| {
184184- // this is an option because a user c
185185- let dest = self
186186- .zid_to_gid
187187- .get(&link.dest)
188188- .expect("Links should be valid");
189189-190190- self.graph.add_edge(gid, *dest, link);
191191- });
9797+ // incase the path of the zettel changed
9898+ self.index.update_path_for_zid(&zid, path.to_path_buf());
9999+ // let the index process the zettel, basically update the internal state of the zod
100100+ self.index.process_zid(&zid)?;
101101+ // and then we sync tags
102102+ self.index.sync_tags_with_db(&zid, &self.db).await?;
103103+ self.index.sync_title_with_db(&zid, &self.db).await?;
192104193105 Ok(())
194194- }
195195-196196- pub fn get_node_by_zettel_id(&self, id: &ZettelId) -> Option<&Node<Zettel, Link>> {
197197- let idx = self.zid_to_gid.get(id)?;
198198-199199- let node = self.graph.node(*idx).expect(
200200- "invariant broken if internal hashmap is not uptodate with
201201- the state of the graph...",
202202- );
203203- Some(node)
204204- }
205205-206206- pub fn get_node_by_zettel_id_mut(&mut self, id: &ZettelId) -> Option<&mut Node<Zettel, Link>> {
207207- let idx = self.zid_to_gid.get(id)?;
208208-209209- let node = self.graph.node_mut(*idx).expect(
210210- "invariant broken if internal hashmap is not uptodate with the
211211- state of the graph...",
212212- );
213213-214214- Some(node)
215106 }
216107}
+7-3
src/types/mod.rs
···1818#[expect(unused_imports)]
1919pub use task::Task;
20202121-mod workspace;
2222-pub use workspace::Workspace;
2323-2421mod link;
2522pub use link::Link;
2323+2424+mod filaments;
2525+#[expect(unused_imports)]
2626+pub use filaments::Filaments;
2727+2828+mod index;
2929+pub use index::Index;
26302731mod kasten;
2832pub use kasten::Kasten;
-95
src/types/workspace.rs
···11-use std::path::PathBuf;
22-33-use color_eyre::eyre::{Context, Result};
44-use dto::{Database, DatabaseConnection, Migrator, MigratorTrait};
55-use tokio::fs::{File, create_dir_all};
66-use tracing::debug;
77-88-/// The `Workspace` in which the filaments exist.
99-#[derive(Debug, Clone)]
1010-pub struct Workspace {
1111- /// Private field so it can only be instantiated from a `Path`
1212- _private: (),
1313- /// Connection to the sqlite database inside the `Workspace`
1414- pub db: DatabaseConnection,
1515- /// The path to the root of this workspace
1616- pub root: PathBuf,
1717-}
1818-1919-impl Workspace {
2020- /// Given a path, try to construct a `Workspace` based on its contents.
2121- ///
2222- /// Note: this means that there should already exist a valid `Workspace`
2323- /// at that path.
2424- pub async fn instansiate(path: impl Into<PathBuf>) -> Result<Self> {
2525- let path = path.into();
2626-2727- let db_conn_string = format!(
2828- "sqlite://{}",
2929- path.clone()
3030- .join(".filaments/filaments.db")
3131- .canonicalize()
3232- .context("Invalid Filaments workspace!!")?
3333- .to_string_lossy()
3434- );
3535-3636- debug!("connecting to {db_conn_string}");
3737-3838- let conn = Database::connect(db_conn_string)
3939- .await
4040- .context("Failed to connect to the database in the filaments workspace!")?;
4141-4242- // run da migrations every time we connect, just in case
4343- Migrator::up(&conn, None).await?;
4444-4545- Ok(Self {
4646- _private: (),
4747- db: conn,
4848- root: path,
4949- })
5050- }
5151-5252- pub async fn initialize(path: impl Into<PathBuf>) -> Result<Self> {
5353- let path = path.into();
5454-5555- let filaments_dir = path.join(".filaments");
5656-5757- // create the dir
5858- create_dir_all(&filaments_dir)
5959- .await
6060- .context("Failed to create the filaments directory!")?;
6161-6262- let filaments_dir = filaments_dir.canonicalize()?;
6363-6464- File::create(filaments_dir.join("filaments.db")).await?;
6565-6666- Ok(Self::instansiate(&path).await.expect(
6767- "Invariant broken. This instantiation call must always work \
6868- since we just initialized the workspace.",
6969- ))
7070- }
7171-}
7272-7373-#[cfg(test)]
7474-mod tests {
7575-7676- use crate::types::Workspace;
7777-7878- #[tokio::test]
7979- async fn test_instantiation() {
8080- let tmp = tempfile::tempdir().unwrap();
8181- let filaments_dir = tmp.path().join(".filaments");
8282- std::fs::create_dir_all(&filaments_dir).unwrap();
8383- std::fs::File::create(filaments_dir.join("filaments.db")).unwrap();
8484- let _ws = Workspace::instansiate(tmp.path()).await.unwrap();
8585- }
8686-8787- #[tokio::test]
8888- async fn test_initialization() {
8989- let tmp = tempfile::tempdir().unwrap();
9090- let path = tmp.path().join("workspace");
9191- Workspace::initialize(path)
9292- .await
9393- .expect("Should initialize just fine");
9494- }
9595-}
-414
src/types/zettel.rs
···11-use dto::{
22- ActiveModelTrait, ActiveValue, ColumnTrait, DateTime, EntityTrait as _, IntoActiveModel,
33- QueryFilter, TagActiveModel, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModel,
44- ZettelModelEx, 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};
1313-1414-use color_eyre::eyre::{Error, Result, eyre};
1515-use dto::NanoId;
1616-use tokio::{fs::File, io::AsyncWriteExt};
1717-1818-use crate::types::{FrontMatter, Link, Tag, Workspace, frontmatter};
1919-2020-/// 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-#[derive(Debug, Clone)]
2424-pub struct Zettel {
2525- /// Should only be constructed from models.
2626- _private: (),
2727- pub id: ZettelId,
2828- pub title: String,
2929- /// a workspace-local file path, needs to be canonicalized before usage
3030- pub file_path: PathBuf,
3131- pub created_at: DateTime,
3232- pub modified_at: DateTime,
3333- pub tags: Vec<Tag>,
3434-}
3535-3636-/// 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-4242-impl Zettel {
4343- pub async fn new(title: impl Into<String>, ws: &Workspace) -> Result<Self> {
4444- // fn new(title: impl Into<String>) -> Result<Self> {
4545- let title = title.into();
4646-4747- // make a file that has a random identifier, and then
4848- // also has the name "title"
4949- let nano_id = NanoId::default();
5050-5151- let local_file_path = format!("{nano_id}.md");
5252-5353- // now we have to create the file
5454- let mut file = File::create_new(ws.root.clone().join(&local_file_path)).await?;
5555-5656- let inserted = ZettelActiveModel::builder()
5757- .set_title(title.clone())
5858- .set_file_path(local_file_path)
5959- .set_nano_id(nano_id)
6060- .insert(&ws.db)
6161- .await?;
6262-6363- // need to load tags...
6464- let zettel = ZettelEntity::load()
6565- .filter_by_nano_id(inserted.nano_id)
6666- .with(TagEntity)
6767- .one(&ws.db)
6868- .await?
6969- .expect("This must exist since we just inserted it");
7070-7171- let front_matter = FrontMatter::new(
7272- title,
7373- zettel.created_at,
7474- zettel.tags.iter().map(|t| t.name.clone()).collect(),
7575- );
7676-7777- file.write_all(front_matter.to_string().as_bytes()).await?;
7878-7979- Ok(zettel.into())
8080- }
8181-8282- 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-182182- /// Returns the most up-to-date `FrontMatter` for this
183183- /// `Zettel`
184184- pub async fn front_matter(&self, ws: &Workspace) -> Result<FrontMatter> {
185185- let path = self.absolute_path(ws);
186186- let (fm, _) = FrontMatter::extract_from_file(path).await?;
187187- Ok(fm)
188188- }
189189-190190- /// Returns the content of this `Zettel`, which is everything
191191- /// but the `FrontMatter`
192192- pub async fn content(&self, ws: &Workspace) -> Result<String> {
193193- let path = self.absolute_path(ws);
194194- let (_, content) = FrontMatter::extract_from_file(path).await?;
195195- Ok(content)
196196- }
197197-198198- #[expect(dead_code)]
199199- async fn open_file(&self, ws: &Workspace) -> Result<File> {
200200- let path = self.absolute_path(ws);
201201- Ok(File::open(path).await?)
202202- }
203203-204204- pub fn absolute_path(&self, ws: &Workspace) -> PathBuf {
205205- ws.root.clone().join(&self.file_path)
206206- }
207207-208208- /// uses the id and root to parse out of the root directory
209209- pub async fn from_id(id: &ZettelId, ws: &Workspace) -> Result<Self> {
210210- let mut path = ws.root.clone();
211211- path.push(id.0.to_string());
212212- Self::from_path(path, ws).await
213213- }
214214-215215- 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-227227- pub async fn from_path(path: impl Into<PathBuf>, ws: &Workspace) -> Result<Self> {
228228- let path: PathBuf = path.into();
229229-230230- let id = ZettelId::try_from(path.as_path())?;
231231-232232- let (front_matter, _) = FrontMatter::extract_from_file(&ws.root.clone().join(path)).await?;
233233-234234- // get the zettel from the db
235235- let db_zettel: ZettelModelEx = if let Some(existing_zettel) = ZettelEntity::load()
236236- .with(TagEntity)
237237- .filter_by_nano_id(id.clone())
238238- .one(&ws.db)
239239- .await?
240240- {
241241- existing_zettel
242242- } else {
243243- // if zettel is missing from db, we just add it here
244244- info!("adding zettel to db");
245245- let am = ZettelActiveModel {
246246- nano_id: ActiveValue::Set(id.clone().into()),
247247- title: ActiveValue::Set(front_matter.title.clone()),
248248- ..Default::default()
249249- };
250250-251251- am.insert(&ws.db).await?;
252252-253253- ZettelEntity::load()
254254- .with(TagEntity)
255255- .filter_by_nano_id(id.clone())
256256- .one(&ws.db)
257257- .await?
258258- .expect("we just inserted the zettel")
259259- };
260260-261261- let mut temp_zettel: Self = db_zettel.clone().into();
262262- temp_zettel.sync_tags(ws).await?;
263263-264264- if front_matter.title != db_zettel.title {
265265- let mut am = db_zettel.into_active_model();
266266- am.title = ActiveValue::Set(front_matter.title.clone());
267267- am.update(&ws.db).await?;
268268- }
269269-270270- Ok(ZettelEntity::load()
271271- .with(TagEntity)
272272- .filter_by_nano_id(id.clone())
273273- .one(&ws.db)
274274- .await?
275275- .expect("We just inserted it right above")
276276- .into())
277277- }
278278-279279- /// The `Link`s that are going out of this `Zettel`
280280- pub async fn links(&self, ws: &Workspace) -> Result<Vec<Link>> {
281281- let content = self.content(ws).await?;
282282- let parsed = Parser::new(&content);
283283-284284- let mut links = vec![];
285285-286286- for event in parsed {
287287- if let Event::Start(MkTag::Link { dest_url, .. }) = event {
288288- info!("Found dest_url: {dest_url:#?}");
289289-290290- let dest_path = {
291291- // remove leading "./"
292292- let without_prefix = dest_url.strip_prefix("./").unwrap_or(&dest_url);
293293-294294- // remove "#" and everything after it
295295- let without_anchor = without_prefix.split('#').next().unwrap();
296296-297297- // add .md if not present
298298- let normalized = if std::path::Path::new(without_anchor)
299299- .extension()
300300- .is_some_and(|ext| ext.eq_ignore_ascii_case("md"))
301301- {
302302- without_anchor.to_string()
303303- } else {
304304- format!("{without_anchor}.md")
305305- };
306306-307307- let mut tmp_root = ws.root.clone();
308308- tmp_root.push(normalized);
309309- tmp_root
310310- };
311311- // simplest way to validate that the path exists
312312- let Ok(canon_url) = dest_path.canonicalize() else {
313313- error!("Link not found!: {dest_path:?}");
314314- continue;
315315- };
316316-317317- // TODO: check that the thing actually exists inside the ws.db
318318- // instead of just seeing if we can turn it into a ZettelId
319319- let dst_id = ZettelId::try_from(canon_url)?;
320320-321321- let link = Link::new(self.id.clone(), dst_id);
322322-323323- links.push(link);
324324- }
325325- }
326326-327327- Ok(links)
328328- }
329329-}
330330-331331-impl From<ZettelModelEx> for Zettel {
332332- fn from(value: ZettelModelEx) -> Self {
333333- assert!(
334334- !value.tags.is_unloaded(),
335335- "When fetching a Zettel from the database, we expect
336336- to always have the tags loaded!!"
337337- );
338338-339339- Self {
340340- _private: (),
341341- id: value.nano_id.into(),
342342- title: value.title,
343343- file_path: value.file_path.into(),
344344- created_at: value.created_at,
345345- modified_at: value.modified_at,
346346- tags: value.tags.into_iter().map(Into::into).collect(),
347347- }
348348- }
349349-}
350350-351351-impl From<&str> for ZettelId {
352352- fn from(value: &str) -> Self {
353353- Self(NanoId::from(value))
354354- }
355355-}
356356-357357-impl From<&NanoId> for ZettelId {
358358- fn from(value: &NanoId) -> Self {
359359- value.clone().into()
360360- }
361361-}
362362-363363-impl From<NanoId> for ZettelId {
364364- fn from(value: NanoId) -> Self {
365365- Self(value)
366366- }
367367-}
368368-369369-impl TryFrom<PathBuf> for ZettelId {
370370- type Error = Error;
371371-372372- fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
373373- let path = value.as_path();
374374- path.try_into()
375375- }
376376-}
377377-378378-impl TryFrom<&Path> for ZettelId {
379379- type Error = Error;
380380-381381- fn try_from(value: &Path) -> Result<Self, Self::Error> {
382382- let extension = value
383383- .extension()
384384- .and_then(|ext| ext.to_str())
385385- .ok_or_else(|| eyre!("Unable to turn file extension into string".to_owned(),))?;
386386-387387- if extension != "md" {
388388- return Err(eyre!(format!("Wrong extension: {extension}, expected .md")));
389389- }
390390-391391- let id: Self = value
392392- .file_name()
393393- .ok_or_else(|| eyre!("Invalid File Name!".to_owned()))?
394394- .to_str()
395395- .ok_or_else(|| eyre!("File Name cannot be translated into str!".to_owned(),))?
396396- .strip_suffix(".md")
397397- .expect("we statically verify this right above")
398398- .into();
399399-400400- Ok(id)
401401- }
402402-}
403403-404404-impl Display for ZettelId {
405405- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406406- f.write_str(&self.0.to_string())
407407- }
408408-}
409409-410410-impl From<ZettelId> for NanoId {
411411- fn from(value: ZettelId) -> Self {
412412- value.0
413413- }
414414-}
+100
src/types/zettel/id.rs
···11+use std::{
22+ fmt::Display,
33+ path::{Path, PathBuf},
44+};
55+66+use color_eyre::eyre::{Error, eyre};
77+use dto::NanoId;
88+use serde::{Deserialize, Serialize};
99+1010+/// A `ZettelId` is essentially a `NanoId`,
1111+/// with some `Zettel` specific helpers written
1212+/// onto it
1313+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1414+pub struct ZettelId(pub(super) NanoId);
1515+1616+impl From<&str> for ZettelId {
1717+ fn from(value: &str) -> Self {
1818+ Self(NanoId::from(value))
1919+ }
2020+}
2121+2222+impl From<&NanoId> for ZettelId {
2323+ fn from(value: &NanoId) -> Self {
2424+ value.clone().into()
2525+ }
2626+}
2727+2828+impl From<NanoId> for ZettelId {
2929+ fn from(value: NanoId) -> Self {
3030+ Self(value)
3131+ }
3232+}
3333+3434+impl TryFrom<PathBuf> for ZettelId {
3535+ type Error = Error;
3636+3737+ fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
3838+ let path = value.as_path();
3939+ path.try_into()
4040+ }
4141+}
4242+4343+impl TryFrom<&Path> for ZettelId {
4444+ type Error = Error;
4545+4646+ fn try_from(value: &Path) -> Result<Self, Self::Error> {
4747+ let extension = value
4848+ .extension()
4949+ .and_then(|ext| ext.to_str())
5050+ .ok_or_else(|| eyre!("Unable to turn file extension into string".to_owned(),))?;
5151+5252+ if extension != "md" {
5353+ return Err(eyre!(format!("Wrong extension: {extension}, expected .md")));
5454+ }
5555+5656+ let id: Self = (value
5757+ .file_name()
5858+ .ok_or_else(|| eyre!("Invalid File Name!".to_owned()))?
5959+ .to_str()
6060+ .ok_or_else(|| eyre!("File Name cannot be translated into str!".to_owned(),))?
6161+ .strip_suffix(".md")
6262+ .expect("we statically verify this right above")
6363+ .split('-'))
6464+ .next()
6565+ .ok_or_else(|| eyre!("Unable to get the first part of the file name!"))?
6666+ .into();
6767+6868+ Ok(id)
6969+ }
7070+}
7171+7272+impl Display for ZettelId {
7373+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7474+ f.write_str(&self.0.to_string())
7575+ }
7676+}
7777+7878+impl From<ZettelId> for NanoId {
7979+ fn from(value: ZettelId) -> Self {
8080+ value.0
8181+ }
8282+}
8383+8484+#[cfg(test)]
8585+mod tests {
8686+ use std::path::PathBuf;
8787+8888+ use super::*;
8989+9090+ #[tokio::test]
9191+ async fn test_zettel_id_parsing_from_path() {
9292+ let path = PathBuf::from("/what/the/fuck/are/you/abcdef-doing-monkey.md");
9393+9494+ let id: ZettelId = path
9595+ .try_into()
9696+ .expect("Should be able to parse the test path just file");
9797+9898+ assert_eq!(id.0, "abcdef".into());
9999+ }
100100+}
+134
src/types/zettel/mod.rs
···11+use std::path::{Path, PathBuf};
22+33+use dto::{
44+ DatabaseConnection, DateTime, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx,
55+};
66+77+use color_eyre::eyre::Result;
88+use dto::NanoId;
99+use tokio::{fs::File, io::AsyncWriteExt};
1010+1111+use crate::types::{
1212+ FrontMatter, Index, Kasten, Tag,
1313+ frontmatter::{self, Body},
1414+};
1515+1616+mod id;
1717+pub use id::ZettelId;
1818+1919+/// A `Zettel` is a note about a single idea.
2020+/// It can have many `Tag`s, just meaning it can fall under many
2121+/// categories.
2222+#[derive(Debug, Clone)]
2323+pub struct Zettel {
2424+ /// Should only be constructed from models.
2525+ _private: (),
2626+ pub id: ZettelId,
2727+ pub title: String,
2828+ /// a workspace-local file path, needs to be canonicalized before usage
2929+ pub file_path: PathBuf,
3030+ pub created_at: DateTime,
3131+ pub modified_at: DateTime,
3232+ pub tags: Vec<Tag>,
3333+}
3434+3535+impl Zettel {
3636+ /// fetches the `Zettel` with the provided `ZettelId`, returning `None` if not found.
3737+ pub async fn fetch_from_db(zid: &ZettelId, db: &DatabaseConnection) -> Result<Option<Self>> {
3838+ Ok(ZettelEntity::load()
3939+ .filter_by_nano_id(zid.0.clone())
4040+ .with(TagEntity)
4141+ .one(db)
4242+ .await?
4343+ .map(Into::into))
4444+ }
4545+4646+ pub async fn new(title: impl Into<String>, kt: &mut Kasten) -> Result<Self> {
4747+ // fn new(title: impl Into<String>) -> Result<Self> {
4848+ let title = title.into();
4949+5050+ // make a file that has a random identifier, and then
5151+ // also has the name "title"
5252+ let nano_id = NanoId::default();
5353+5454+ let local_file_path = format!("{nano_id}.md");
5555+5656+ // now we have to create the file
5757+ let mut file = File::create_new(kt.root.clone().join(&local_file_path)).await?;
5858+5959+ let inserted = ZettelActiveModel::builder()
6060+ .set_title(title.clone())
6161+ .set_file_path(local_file_path)
6262+ .set_nano_id(nano_id)
6363+ .insert(&kt.db)
6464+ .await?;
6565+6666+ // need to load tags...
6767+ let zettel = ZettelEntity::load()
6868+ .filter_by_nano_id(inserted.nano_id)
6969+ .with(TagEntity)
7070+ .one(&kt.db)
7171+ .await?
7272+ .expect("This must exist since we just inserted it");
7373+7474+ let front_matter = FrontMatter::new(
7575+ title,
7676+ zettel.created_at,
7777+ zettel.tags.iter().map(|t| t.name.clone()).collect(),
7878+ );
7979+8080+ file.write_all(front_matter.to_string().as_bytes()).await?;
8181+8282+ kt.process_path(zettel.file_path.clone()).await?;
8383+8484+ Ok(zettel.into())
8585+ }
8686+8787+ /// Returns the most up-to-date `FrontMatter` for this
8888+ /// `Zettel`
8989+ pub fn front_matter<'index>(&self, idx: &'index Index) -> &'index FrontMatter {
9090+ &idx.get_zod(&self.id).fm
9191+ }
9292+9393+ /// Returns the content of this `Zettel`, which is everything
9494+ /// but the `FrontMatter`
9595+ pub fn content<'index>(&self, idx: &'index Index) -> &'index Body {
9696+ &idx.get_zod(&self.id).body
9797+ }
9898+ /// Get the absolute path to this `Zettel`
9999+ pub fn absolute_path<'index>(&self, idx: &'index Index) -> &'index Path {
100100+ &idx.get_zod(&self.id).path
101101+ }
102102+103103+ pub fn created_at(&self) -> String {
104104+ self.created_at
105105+ .format(frontmatter::DATE_FMT_STR)
106106+ .to_string()
107107+ }
108108+109109+ pub fn modified_at(&self) -> String {
110110+ self.modified_at
111111+ .format(frontmatter::DATE_FMT_STR)
112112+ .to_string()
113113+ }
114114+}
115115+116116+impl From<ZettelModelEx> for Zettel {
117117+ fn from(value: ZettelModelEx) -> Self {
118118+ assert!(
119119+ !value.tags.is_unloaded(),
120120+ "When fetching a Zettel from the database, we expect
121121+ to always have the tags loaded!!"
122122+ );
123123+124124+ Self {
125125+ _private: (),
126126+ id: value.nano_id.into(),
127127+ title: value.title,
128128+ file_path: value.file_path.into(),
129129+ created_at: value.created_at,
130130+ modified_at: value.modified_at,
131131+ tags: value.tags.into_iter().map(Into::into).collect(),
132132+ }
133133+ }
134134+}