//! File watcher for lore.db changes //! //! Watches the lore database file for modifications and emits events //! to trigger frontend refresh when lore syncs new data. use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use std::path::PathBuf; use std::sync::mpsc; use std::time::Duration; use tauri::AppHandle; use tauri_specta::Event as TauriEvent; use crate::events::LoreDataChanged; /// Get the path to lore's database file fn lore_db_path() -> Option { dirs::data_local_dir().map(|d| d.join("lore").join("lore.db")) } /// Start watching lore.db for changes. /// /// When the file is modified, emits a "lore-data-changed" event to the frontend. /// Returns the watcher handle (must be kept alive for watching to continue). pub fn start_lore_watcher(app: AppHandle) -> Option { let db_path = lore_db_path()?; // Check if the file exists if !db_path.exists() { tracing::warn!("lore.db not found at {:?}, skipping file watcher", db_path); return None; } let parent_dir = db_path.parent()?; // Create a channel for receiving events let (tx, rx) = mpsc::channel(); // Create the watcher with a debounce of 1 second let mut watcher = RecommendedWatcher::new( move |res: Result| { match res { Ok(event) => { if tx.send(event).is_err() { // Receiver dropped -- watcher thread has exited tracing::debug!("Watcher event channel closed, receiver dropped"); } } Err(e) => { tracing::warn!("File watcher error: {}", e); } } }, Config::default().with_poll_interval(Duration::from_secs(2)), ) .ok()?; // Watch the lore directory (not just the file, for atomic write detection) if let Err(e) = watcher.watch(parent_dir, RecursiveMode::NonRecursive) { tracing::error!("Failed to start lore.db watcher: {}", e); return None; } tracing::info!("Watching lore.db for changes at {:?}", db_path); // Spawn a thread to handle events std::thread::spawn(move || { for event in rx { // Only react to modify events on the actual db file if matches!( event.kind, EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_) ) { let affects_db = event.paths.iter().any(|p| p.ends_with("lore.db")); if affects_db { tracing::debug!("lore.db changed, emitting refresh event"); if let Err(e) = LoreDataChanged.emit(&app) { tracing::warn!("Failed to emit lore-data-changed event: {}", e); } } } } }); Some(watcher) } #[cfg(test)] mod tests { use super::*; #[test] fn test_lore_db_path_returns_expected_location() { let path = lore_db_path(); assert!(path.is_some()); let path = path.unwrap(); assert!(path.ends_with("lore/lore.db")); } }