use notify::{ raw_watcher, watcher, DebouncedEvent, Op, RawEvent, RecommendedWatcher, RecursiveMode, Watcher as _, }; 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)>>); #[derive(Clone, Serialize)] struct RawEventWrapper { path: Option, operation: u32, cookie: Option, } #[derive(Clone, Serialize)] #[serde(tag = "type", content = "payload")] enum DebouncedEventWrapper { NoticeWrite(PathBuf), NoticeRemove(PathBuf), Create(PathBuf), Write(PathBuf), Chmod(PathBuf), Remove(PathBuf), Rename(PathBuf, PathBuf), Rescan, Error { error: String, path: Option, }, } impl From for DebouncedEventWrapper { fn from(event: DebouncedEvent) -> Self { match event { DebouncedEvent::NoticeWrite(path) => Self::NoticeWrite(path), DebouncedEvent::NoticeRemove(path) => Self::NoticeRemove(path), DebouncedEvent::Create(path) => Self::Create(path), DebouncedEvent::Write(path) => Self::Write(path), DebouncedEvent::Chmod(path) => Self::Chmod(path), DebouncedEvent::Remove(path) => Self::Remove(path), DebouncedEvent::Rename(from, to) => Self::Rename(from, to), DebouncedEvent::Rescan => Self::Rescan, DebouncedEvent::Error(error, path) => Self::Error { error: error.to_string(), path, }, } } } 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() { let _ = window.emit( &event_name, RawEventWrapper { path: event.path, operation: event.op.unwrap_or_else(|_| Op::empty()).bits(), cookie: event.cookie, }, ); } }); } 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() { let _ = window.emit(&event_name, DebouncedEventWrapper::from(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 watcher = watcher(tx, Duration::from_millis(delay))?; for path in &paths { watcher.watch(path, mode)?; } watch_debounced(window, rx, id); watcher } else { let (tx, rx) = channel(); let mut watcher = raw_watcher(tx)?; for path in &paths { watcher.watch(path, mode)?; } watch_raw(window, rx, id); watcher }; watchers.0.lock().unwrap().insert(id, (watcher, paths)); Ok(()) } #[command] async fn unwatch(watchers: State<'_, WatcherCollection>, id: Id) -> Result<()> { if let Some((mut watcher, paths)) = watchers.0.lock().unwrap().remove(&id) { 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() }