You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tauri-plugins-workspace/plugins/fs-watch/src/lib.rs

144 lines
3.8 KiB

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<T> = std::result::Result<T, Error>;
type Id = u32;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Watch(#[from] notify::Error),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
#[derive(Default)]
struct WatcherCollection(Mutex<HashMap<Id, (WatcherKind, Vec<PathBuf>)>>);
enum WatcherKind {
Debouncer(Debouncer<RecommendedWatcher>),
Watcher(RecommendedWatcher),
}
fn watch_raw<R: Runtime>(window: Window<R>, rx: Receiver<notify::Result<Event>>, 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<R: Runtime>(window: Window<R>, rx: Receiver<DebounceEventResult>, 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<u64>,
recursive: bool,
}
#[command]
async fn watch<R: Runtime>(
window: Window<R>,
watchers: State<'_, WatcherCollection>,
id: Id,
paths: Vec<PathBuf>,
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), None, 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<R: Runtime>() -> TauriPlugin<R> {
PluginBuilder::new("fs-watch")
.invoke_handler(tauri::generate_handler![watch, unwatch])
.setup(|app| {
app.manage(WatcherCollection::default());
Ok(())
})
.build()
}