All tests run on an 8-year-old MacBook Air.
All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.
HiyokoAutoSync watches directories for changes and triggers sync automatically. notify-rs is the Rust crate for this. Here's what I learned using it in a shipping Tauri app.
Basic setup
toml[dependencies]
notify = "6"
rustuse notify::{RecommendedWatcher, RecursiveMode, Watcher, Config};
use std::sync::mpsc;
fn watch_directory(path: &str) -> Result<(), AppError> {
let (tx, rx) = mpsc::channel();
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
watcher.watch(path.as_ref(), RecursiveMode::Recursive)?;
for res in rx {
match res {
Ok(event) => handle_event(event),
Err(e) => log::error!("Watch error: {:?}", e),
}
}
Ok(())
}
RecommendedWatcher uses FSEvents on macOS — the native file system event API. Low overhead, fast notification.
The double-fire problem
File save operations often trigger multiple events. A text editor saving a file might fire: Modify, Modify, Create, Modify. You want one sync trigger, not four.
Debounce:
rustuse std::time::{Duration, Instant};
use std::collections::HashMap;
struct Debouncer {
last_seen: HashMap,
delay: Duration,
}
impl Debouncer {
fn should_process(&mut self, path: &PathBuf) -> bool {
let now = Instant::now();
let last = self.last_seen.entry(path.clone()).or_insert(Instant::now() - self.delay * 2);
if now.duration_since(*last) >= self.delay {
*last = now;
true
} else {
false
}
}
}
300-500ms debounce window covers most editor save behaviors without feeling slow.
Filtering what to watch
Not every file change should trigger a sync. Skip:
rustfn should_ignore(path: &Path) -> bool {
let name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
// Hidden files
name.starts_with('.')
// Temp files
|| name.ends_with(".tmp")
|| name.ends_with('~')
// DS_Store
|| name == ".DS_Store"
// Already syncing
|| name.ends_with(".sync")
}
Running the watcher in Tauri
The watcher needs to run in a background thread, not blocking the Tokio runtime:
ruststd:🧵:spawn(move || {
if let Err(e) = watch_directory(&path) {
log::error!("Watcher failed: {:?}", e);
}
});
Use std:🧵:spawn for the blocking watcher loop. Communicate back to Tauri via channels or the app handle's emit system.
The verdict
notify-rs with FSEvents on macOS is solid. The double-fire problem needs debouncing — build it in from the start. Filter aggressively to avoid triggering on irrelevant changes.
If this was useful, a ❤️ helps more than you'd think — thanks!
Hiyoko PDF Vault → https://hiyokoko.gumroad.com/l/HiyokoPDFVault
X → @hiyoyok
Top comments (0)