use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer}; use serde::{ser::Serializer, Deserialize, Serialize}; use tauri::{ command, plugin::{Builder as PluginBuilder, TauriPlugin}, Manager, Runtime, State, Window, }; use std::{ collections::HashMap, path::PathBuf, sync::{ mpsc::{channel, Receiver}, Mutex, }, thread::spawn, time::Duration, }; type Result = std::result::Result; type Id = u32; #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] Watch(#[from] notify::Error), } impl Serialize for Error { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { serializer.serialize_str(self.to_string().as_ref()) } } #[derive(Default)] struct WatcherCollection(Mutex)>>); enum WatcherKind { Debouncer(Debouncer), Watcher(RecommendedWatcher), } fn watch_raw(window: Window, rx: Receiver>, id: Id) { spawn(move || { let event_name = format!("watcher://raw-event/{id}"); while let Ok(event) = rx.recv() { if let Ok(event) = event { // TODO: Should errors be emitted too? let _ = window.emit(&event_name, event); } } }); } fn watch_debounced(window: Window, rx: Receiver, id: Id) { spawn(move || { let event_name = format!("watcher://debounced-event/{id}"); while let Ok(event) = rx.recv() { if let Ok(event) = event { // TODO: Should errors be emitted too? let _ = window.emit(&event_name, event); } } }); } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct WatchOptions { delay_ms: Option, recursive: bool, } #[command] async fn watch( window: Window, watchers: State<'_, WatcherCollection>, id: Id, paths: Vec, options: WatchOptions, ) -> Result<()> { let mode = if options.recursive { RecursiveMode::Recursive } else { RecursiveMode::NonRecursive }; let watcher = if let Some(delay) = options.delay_ms { let (tx, rx) = channel(); let mut debouncer = new_debouncer(Duration::from_millis(delay), tx)?; let watcher = debouncer.watcher(); for path in &paths { watcher.watch(path, mode)?; } watch_debounced(window, rx, id); WatcherKind::Debouncer(debouncer) } else { let (tx, rx) = channel(); let mut watcher = RecommendedWatcher::new(tx, Config::default())?; for path in &paths { watcher.watch(path, mode)?; } watch_raw(window, rx, id); WatcherKind::Watcher(watcher) }; watchers.0.lock().unwrap().insert(id, (watcher, paths)); Ok(()) } #[command] async fn unwatch(watchers: State<'_, WatcherCollection>, id: Id) -> Result<()> { if let Some((watcher, paths)) = watchers.0.lock().unwrap().remove(&id) { match watcher { WatcherKind::Debouncer(mut debouncer) => { for path in paths { debouncer.watcher().unwatch(&path)? } } WatcherKind::Watcher(mut watcher) => { for path in paths { watcher.unwatch(&path)? } } }; } Ok(()) } pub fn init() -> TauriPlugin { PluginBuilder::new("fs-watch") .invoke_handler(tauri::generate_handler![watch, unwatch]) .setup(|app| { app.manage(WatcherCollection::default()); Ok(()) }) .build() }