···11use std::{process::Command, thread::spawn};
2233-use color_eyre::eyre::Result;
33+use color_eyre::eyre::{Context, Result};
44use crossterm::event::KeyEvent;
55use ratatui::layout::Rect;
66use serde::{Deserialize, Serialize};
···192192 // once we get out of the edit, we need to update the zettel for this
193193 // path and then update the db and the kasten for this stuff
194194195195- self.kh.write().await.process_path(&path).await?;
195195+ self.kh
196196+ .write()
197197+ .await
198198+ .process_path(&path)
199199+ .await
200200+ .with_context(|| {
201201+ format!(
202202+ "Failed to process the path
203203+ for this zettel: {}",
204204+ path.display()
205205+ )
206206+ })?;
207207+208208+ debug!("successfully processed path: {}", path.display());
196209197210 self.signal_tx.send(Signal::ClosedZettel)?;
198211
+41-68
src/tui/components/zk/mod.rs
···11use async_trait::async_trait;
22-use color_eyre::eyre::Result;
22+use color_eyre::eyre::{Context as _, 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- );
117117-118118- // 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();
106106+ let preview = Preview::from(zettel.content(&kt.index).clone());
126107127108 drop(kt);
128109129110 Ok(Self {
130111 signal_tx: None,
112112+ search: Search::new(kh.clone()),
131113 kh,
132114 layouts: Layouts::default(),
133115 zettel_list,
134134- zettel_view,
116116+ zettel_view: zettel.into(),
135117 preview,
136136- search: Search::new(ws),
137118 })
138119 }
139120···146127147128 // sometimes the selection we get is over the length of the thing, so its
148129 // 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 {
130130+ let Some(zid) = self.zettel_list.id_list.get(selection_idx) else {
150131 return Ok(());
151132 };
152133153134 let kh = self.kh.read().await;
154135155155- 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();
136136+ let zettel = &Zettel::fetch_from_db(zid, &kh.db)
137137+ .await?
138138+ .context("Unknown Behaviour, A selected zettel got deleted somehow.")?;
160139161161- 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)
166166- .await?
167167- .into();
140140+ self.preview = zettel.content(&kh.index).clone().into();
168141 drop(kh);
142142+143143+ self.zettel_view = zettel.into();
169144170145 Ok(())
171146 }
···175150 let models = ZettelEntity::load()
176151 .with(TagEntity)
177152 .order_by_desc(ZettelColumns::ModifiedAt)
178178- .all(&kt.ws.db)
153153+ .all(&kt.db)
179154 .await?;
180155181156 // im being a good boy and dropping this as soon as im done with the db
···250225 };
251226252227 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);
228228+229229+ let path = kh.index.get_zod(zid).path.clone();
261230262231 drop(kh);
263232···267236 Signal::NewZettel => {
268237 // what the fuck am i going to do in here
269238270270- let ws = &self.kh.read().await.ws;
239239+ let mut kt = self.kh.write().await;
271240272241 // we create the zettel with the query as the
273273- let z = Zettel::new(self.search.query(), ws).await?;
242242+ let z = Zettel::new(self.search.query(), &mut kt)
243243+ .await
244244+ .with_context(|| "Failed to create a new Zettel!")?;
274245275275- let path = z.absolute_path(ws);
246246+ let path = z.absolute_path(&kt.index).to_path_buf();
247247+248248+ drop(kt);
276249277250 return Ok(Some(Signal::Helix { path }));
278251 }
···284257 .selected()
285258 .expect("This must be the zettel we just edited");
286259287287- let Some(id) = self.zettel_list.id_list.get(selected) else {
260260+ // regenerate a fresh zettel list
261261+ self.zettel_list = ZettelList::new(
262262+ self.get_zettels_by_current_query().await?,
263263+ self.zettel_list.state,
264264+ self.zettel_list.width,
265265+ );
266266+267267+ let Some(zid) = self.zettel_list.id_list.get(selected) else {
288268 return Ok(None);
289269 };
290270291271 let kt = self.kh.read().await;
292272293293- let node = kt
294294- .get_node_by_zettel_id(id)
295295- .expect("Invariant broken, this must exist.");
273273+ let zettel = Zettel::fetch_from_db(zid, &kt.db)
274274+ .await?
275275+ .expect("invariant broken, we just closed this zettel");
296276297277 // reset the state of the component
298278 self.search.clear_query();
299279 self.zettel_list.state.select_first();
300280301301- // regenerate a fresh zettel list
302302- self.zettel_list = ZettelList::new(
303303- self.get_zettels_by_current_query().await?,
304304- self.zettel_list.state,
305305- self.zettel_list.width,
306306- );
307307-308308- self.zettel_view = ZettelView::from(node.payload());
309309- self.preview = Preview::from(node.payload().content(&kt.ws).await?);
281281+ self.zettel_view = ZettelView::from(&zettel);
282282+ self.preview = Preview::from(zettel.content(&kt.index).clone());
310283 drop(kt);
311284 }
312285
···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((
5353+ id,
5454+ ZettelOnDisk {
5555+ fm,
5656+ body,
5757+ path: path.canonicalize()?,
5858+ },
5959+ ))
6060+ })
6161+ .collect::<Result<Vec<_>>>()?
6262+ .into_iter()
6363+ // .par_bridge()
6464+ .for_each(|(id, zod)| {
6565+ zods.insert(id, zod);
6666+ });
6767+6868+ Ok(Self { zods })
6969+ }
7070+7171+ pub fn update_path_for_zid(&mut self, zid: &ZettelId, new_path: PathBuf) {
7272+ self.get_zod_mut(zid).path = new_path;
7373+ }
7474+7575+ /// Updates the interal state of the `Index` for the provided `Zid`.
7676+ pub fn process_zid(&mut self, zid: &ZettelId) -> Result<()> {
7777+ let zod = self.get_zod_mut(zid);
7878+7979+ let (fm, body) = FrontMatter::extract_from_file(&zod.path)?;
8080+8181+ zod.fm = fm;
8282+ zod.body = body;
8383+8484+ Ok(())
8585+ }
8686+8787+ /// Sync's the curren title of the `Zettel` with the
8888+ /// provided `zid` with the `DB`
8989+ pub async fn sync_title_with_db(
9090+ &mut self,
9191+ zid: &ZettelId,
9292+ db: &DatabaseConnection,
9393+ ) -> Result<()> {
9494+ let fm = &mut self.get_zod_mut(zid).fm;
9595+9696+ let mut model = ZettelEntity::find_by_nano_id(zid.clone())
9797+ .one(db)
9898+ .await?
9999+ .expect("this must exist")
100100+ .into_active_model();
101101+102102+ model.title = ActiveValue::Set(fm.title.clone());
103103+104104+ model.update(db).await?;
105105+106106+ info!("We updated the zettel: {zid:#?}");
107107+108108+ Ok(())
109109+ }
110110+111111+ /// Sync's `Tag`'s that are present in the frontmatter of this
112112+ /// `Zettel` to the database.
113113+ pub async fn sync_tags_with_db(
114114+ &mut self,
115115+ zid: &ZettelId,
116116+ db: &DatabaseConnection,
117117+ ) -> Result<()> {
118118+ let fm = &mut self.get_zod_mut(zid).fm;
119119+120120+ let mut tag_strings = fm.tag_strings.clone();
121121+122122+ tag_strings.sort();
123123+124124+ let db_zettel: ZettelModelEx = ZettelEntity::load()
125125+ .with(TagEntity)
126126+ .filter_by_nano_id(zid.clone())
127127+ .one(db)
128128+ .await?
129129+ .expect("Invariant broken, zettel should not be deleted");
130130+131131+ for db_tag in db_zettel.tags {
132132+ if let Ok(idx) = tag_strings.binary_search(&db_tag.name) {
133133+ // we remove tags we have already processed
134134+ tag_strings.remove(idx);
135135+ } else {
136136+ // the db says the file has tag `x`, but that tag is missing from the
137137+ // front matter, we can assume its gone, lets delete that link
138138+ let to_remove = ZettelTagEntity::find()
139139+ .filter(ZettelTagColumns::ZettelNanoId.eq(zid.to_string()))
140140+ .filter(ZettelTagColumns::TagNanoId.eq(db_tag.nano_id))
141141+ .one(db)
142142+ .await?
143143+ .expect("this link must exist");
144144+145145+ to_remove.into_active_model().delete(db).await?;
146146+ }
147147+ }
148148+149149+ // now any tags that are left inside zettel_tag_strings,
150150+ // we have to look up the tags in the db and then reset them?
151151+ for tag_str in tag_strings {
152152+ // this is the tag that either already exists with this name, or we just created this new one
153153+ let tag =
154154+ if let Some(existing) = TagEntity::load().filter_by_name(&tag_str).one(db).await? {
155155+ existing
156156+ } else {
157157+ let am = TagActiveModel {
158158+ name: ActiveValue::Set(tag_str.clone()),
159159+ ..Default::default()
160160+ };
161161+162162+ am.insert(db).await?.into()
163163+ };
164164+165165+ // this zettel has this tag now
166166+ let _ = ZettelTagActiveModel {
167167+ zettel_nano_id: ActiveValue::Set(zid.to_string()),
168168+ tag_nano_id: ActiveValue::Set(tag.nano_id.to_string()),
169169+ }
170170+ .insert(db)
171171+ .await?;
172172+ }
173173+ Ok(())
174174+ }
175175+176176+ pub fn get_zod(&self, zid: &ZettelId) -> &ZettelOnDisk {
177177+ 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.")
178178+ }
179179+180180+ fn get_zod_mut(&mut self, zid: &ZettelId) -> &mut ZettelOnDisk {
181181+ 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.")
182182+ }
183183+184184+ pub const fn zods(&self) -> &HashMap<ZettelId, ZettelOnDisk> {
185185+ &self.zods
186186+ }
187187+188188+ pub fn sync_with_db(&self, _db: &DatabaseConnection) {
189189+ todo!()
190190+ }
191191+}
+78-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,
84};
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};
1351414-use crate::types::Workspace;
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,
1111+};
1212+use tracing::debug;
1313+1414+use crate::types::{FrontMatter, Index, ZettelId, index::ZettelOnDisk};
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,
2424-2525- /// the graph of `Zettel`s and the `Links` between them
2626- pub graph: ZkGraph,
2121+ pub root: PathBuf,
27222828- /// simple conversions
2929- zid_to_gid: HashMap<ZettelId, NodeIndex>,
2323+ pub index: Index,
30243131- 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<_>>();
5959-6060- debug!(
6161- "indexing the following paths {paths:#?} at root {:#?}",
6262- ws.root
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();
3737+ let db_conn_string = format!(
3838+ "sqlite://{}",
3939+ root.clone()
4040+ .join(".filaments/filaments.db")
4141+ .canonicalize()
4242+ .context("Invalid Filaments workspace!!")?
4343+ .to_string_lossy()
6344 );
64456565- 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<_>>();
4646+ debug!("connecting to {db_conn_string}");
72477373- // await all of them
7474- let zettels = futures::future::join_all(zettel_tasks)
4848+ let conn = Database::connect(db_conn_string)
7549 .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>>();
5050+ .context("Failed to connect to the database in the filaments workspace!")?;
85518686- debug!("parsed zettels: {zettels:#?}");
8787-8888- // 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- ));
5252+ let index = Index::tabulate(&root)?;
93539494- 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- }
105105-106106- 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- }
113113-114114- debug!("parsed graph: {graph:#?}");
5454+ // run da migrations every time we connect, just in case
5555+ Migrator::up(&conn, None).await?;
1155611657 Ok(Self {
11758 _private: (),
118118- ws,
119119- graph,
120120- zid_to_gid,
121121- most_recently_edited: None,
5959+ db: conn,
6060+ root,
6161+ index,
12262 })
12363 }
12464125125- /// processes the `Zettel` for the provided `ZettelId`,
126126- /// meaning it updates the internal state of the `Kasten`
127127- /// with the changes in `Zettel`.
128128- pub async fn process_path(&mut self, path: &Path) -> Result<()> {
129129- //NOTE: need to clone to get around borrowing rules but
130130- // ideally we dont have to do this, kind of cringe imo.
131131- let ws = self.ws.clone();
6565+ /// Create a new `Kasten` at the provided `path`.
6666+ pub async fn initialize(path: impl Into<PathBuf>) -> Result<Self> {
6767+ let path = path.into();
13268133133- let zid = ZettelId::try_from(path)?;
6969+ let filaments_dir = path.join(".filaments");
13470135135- 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();
7171+ // create the dir
7272+ create_dir_all(&filaments_dir)
7373+ .await
7474+ .context("Failed to create the filaments directory!")?;
14875149149- let zid = zettel.id.clone();
150150- let idx = self.graph.add_node(zettel);
7676+ let filaments_dir = filaments_dir.canonicalize()?;
15177152152- self.zid_to_gid.insert(zid.clone(), idx);
7878+ File::create(filaments_dir.join("filaments.db")).await?;
15379154154- gid = Some(idx);
8080+ Ok(Self::instansiate(&path).await.expect(
8181+ "Invariant broken. This instantiation call must always work \
8282+ since we just initialized the workspace.",
8383+ ))
8484+ }
15585156156- self.get_node_by_zettel_id_mut(&zid)
157157- .expect("we just inserted it")
158158- .payload_mut()
159159- };
8686+ /// processes the `Zettel` for the provided `ZettelId`,
8787+ /// meaning it updates the internal state of the `Kasten`
8888+ /// with the changes in `Zettel`.
8989+ pub async fn process_path(&mut self, path: impl AsRef<Path>) -> Result<()> {
9090+ //NOTE: need to clone to get around borrowing rules but
9191+ // ideally we dont have to do this, kind of cringe imo.
16092161161- // and then we sync with the file
162162- zettel.sync_with_file(&ws).await?;
9393+ let path = path.as_ref().canonicalize()?;
9494+ let zid = ZettelId::try_from(path.as_path())?;
16395164164- // 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
9696+ if !self.index.zods.contains_key(&zid) {
9797+ let (fm, body) = FrontMatter::extract_from_file(&path)?;
9898+ self.index.zods.insert(
9999+ zid.clone(),
100100+ ZettelOnDisk {
101101+ fm,
102102+ body,
103103+ path: path.clone(),
104104+ },
105105+ );
106106+ }
171107172172- // 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- });
108108+ // incase the path of the zettel changed
109109+ self.index.update_path_for_zid(&zid, path.clone());
110110+ // let the index process the zettel, basically update the internal state of the zod
111111+ self.index.process_zid(&zid)?;
112112+ // and then we sync tags
113113+ self.index.sync_tags_with_db(&zid, &self.db).await?;
114114+ self.index.sync_title_with_db(&zid, &self.db).await?;
192115193116 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)
215117 }
216118}
+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-}
+108
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+ let path = PathBuf::from("/what/the/fuck/are/you/abcdef.md");
101101+102102+ let id: ZettelId = path
103103+ .try_into()
104104+ .expect("Should be able to parse the test path just file");
105105+106106+ assert_eq!(id.0, "abcdef".into());
107107+ }
108108+}
+145
src/types/zettel/mod.rs
···11+use std::path::Path;
22+33+use dto::{
44+ DatabaseConnection, DateTime, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx,
55+};
66+77+use color_eyre::eyre::{Context, 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+ pub created_at: DateTime,
2929+ pub modified_at: DateTime,
3030+ pub tags: Vec<Tag>,
3131+}
3232+3333+impl Zettel {
3434+ /// fetches the `Zettel` with the provided `ZettelId`, returning `None` if not found.
3535+ pub async fn fetch_from_db(zid: &ZettelId, db: &DatabaseConnection) -> Result<Option<Self>> {
3636+ Ok(ZettelEntity::load()
3737+ .filter_by_nano_id(zid.0.clone())
3838+ .with(TagEntity)
3939+ .one(db)
4040+ .await?
4141+ .map(Into::into))
4242+ }
4343+4444+ pub async fn new(title: impl Into<String>, kt: &mut Kasten) -> Result<Self> {
4545+ // fn new(title: impl Into<String>) -> Result<Self> {
4646+ let title = title.into();
4747+4848+ // make a file that has a random identifier, and then
4949+ // also has the name "title"
5050+ let nano_id = NanoId::default();
5151+5252+ let local_file_path = format!("{nano_id}.md");
5353+5454+ let absolute_file_path = kt.root.clone().join(&local_file_path);
5555+5656+ // now we have to create the file
5757+ let mut file = File::create_new(&absolute_file_path)
5858+ .await
5959+ .with_context(|| {
6060+ format!("Failed to create file at local file path: {local_file_path}")
6161+ })?;
6262+6363+ let inserted = ZettelActiveModel::builder()
6464+ .set_title(title.clone())
6565+ .set_nano_id(nano_id)
6666+ .insert(&kt.db)
6767+ .await?;
6868+6969+ // need to load tags...
7070+ let zettel = ZettelEntity::load()
7171+ .filter_by_nano_id(inserted.nano_id)
7272+ .with(TagEntity)
7373+ .one(&kt.db)
7474+ .await?
7575+ .expect("This must exist since we just inserted it");
7676+7777+ let front_matter = FrontMatter::new(
7878+ title,
7979+ zettel.created_at,
8080+ zettel.tags.iter().map(|t| t.name.clone()).collect(),
8181+ );
8282+8383+ file.write_all(front_matter.to_string().as_bytes()).await?;
8484+8585+ kt.process_path(&absolute_file_path)
8686+ .await
8787+ .with_context(|| {
8888+ format!(
8989+ "Kasten fails to process new Zettel at path: {}",
9090+ absolute_file_path.display(),
9191+ )
9292+ })?;
9393+9494+ Ok(zettel.into())
9595+ }
9696+9797+ /// Returns the most up-to-date `FrontMatter` for this
9898+ /// `Zettel`
9999+ pub fn front_matter<'index>(&self, idx: &'index Index) -> &'index FrontMatter {
100100+ &idx.get_zod(&self.id).fm
101101+ }
102102+103103+ /// Returns the content of this `Zettel`, which is everything
104104+ /// but the `FrontMatter`
105105+ pub fn content<'index>(&self, idx: &'index Index) -> &'index Body {
106106+ &idx.get_zod(&self.id).body
107107+ }
108108+ /// Get the absolute path to this `Zettel`
109109+ pub fn absolute_path<'index>(&self, idx: &'index Index) -> &'index Path {
110110+ &idx.get_zod(&self.id).path
111111+ }
112112+113113+ /// Get the formatted creation datetime for this `Zettel`
114114+ pub fn created_at(&self) -> String {
115115+ self.created_at
116116+ .format(frontmatter::DATE_FMT_STR)
117117+ .to_string()
118118+ }
119119+120120+ /// Get the formatted modified datetime for this `Zettel`
121121+ pub fn modified_at(&self) -> String {
122122+ self.modified_at
123123+ .format(frontmatter::DATE_FMT_STR)
124124+ .to_string()
125125+ }
126126+}
127127+128128+impl From<ZettelModelEx> for Zettel {
129129+ fn from(value: ZettelModelEx) -> Self {
130130+ assert!(
131131+ !value.tags.is_unloaded(),
132132+ "When fetching a Zettel from the database, we expect
133133+ to always have the tags loaded!!"
134134+ );
135135+136136+ Self {
137137+ _private: (),
138138+ id: value.nano_id.into(),
139139+ title: value.title,
140140+ created_at: value.created_at,
141141+ modified_at: value.modified_at,
142142+ tags: value.tags.into_iter().map(Into::into).collect(),
143143+ }
144144+ }
145145+}