//! Watches the vault for zettels edited outside the daemon (e.g. by hand in //! an editor), re-indexes them and publishes the change, so clients see //! those edits too. Adapted from `Deimos::watch`, which feeds the old TUI. use std::path::Path; use color_eyre::eyre::{Context as _, Result}; use fil_core::{ZettelId, frontmatter::FrontMatter}; use notify::{ Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher as _, event::ModifyKind, }; use tracing::{debug, error, info}; use super::{FilState, event::zettel_changes}; /// Runs until the watcher's channel closes; spawn it alongside the server. /// /// # Errors /// /// Returns an error if the vault can't be watched. pub async fn watch(state: FilState) -> Result<()> { let (tx, mut rx) = tokio::sync::mpsc::channel::>(64); // keep the watcher alive for as long as the loop runs let mut watcher = RecommendedWatcher::new( move |res| { let _ = tx.blocking_send(res); }, Config::default(), )?; let root = state.kh.read().await.root.clone(); watcher .watch(&root, RecursiveMode::Recursive) .with_context(|| format!("failed to watch {}", root.display()))?; info!("watching {} for outside edits", root.display()); while let Some(res) = rx.recv().await { let Ok(event) = res.inspect_err(|e| error!("watcher error: {e:?}")) else { continue; }; // editors either write in place or write elsewhere and rename over let relevant = matches!( event.kind, EventKind::Create(_) | EventKind::Modify(ModifyKind::Data(_) | ModifyKind::Name(_)) ); if !relevant { continue; } for path in event.paths.iter().filter(|path| is_zettel_file(path)) { reindex(&state, path).await; } } Ok(()) } /// Only existing markdown files can be zettels; this also skips the /// database, which changes on every write the daemon makes. fn is_zettel_file(path: &Path) -> bool { path.extension().is_some_and(|ext| ext == "md") && path.exists() } async fn reindex(state: &FilState, path: &Path) { let Ok(zid) = ZettelId::try_from(path.to_path_buf()) else { debug!("not a zettel: {}", path.display()); return; }; let mut kt = state.kh.write().await; // the daemon's own writes come back as events too; those files already // match the index, so there is nothing to re-index or publish let unchanged = kt.index.zods().get(&zid).is_some_and(|zod| { FrontMatter::extract_from_file(path) .is_ok_and(|(fm, body)| fm == zod.fm && body == zod.body) }); if unchanged { drop(kt); return; } if let Err(e) = kt.process_path(path).await { drop(kt); error!("failed to re-index {}: {e}", path.display()); return; } // fails for e.g. a markdown file that was never added as a zettel if let Ok(changes) = zettel_changes(&zid, &kt).await { state.publish(&mut kt, changes); } drop(kt); }