use std::path::Path;
use std::sync::mpsc::{RecvTimeoutError, channel};
use std::time::{Duration, Instant};
use anyhow::{Context, Result, bail};
use notify::{RecursiveMode, Watcher};
use ripsync_core::Filter;
use crate::args::Args;
use crate::run_local_once;
pub fn run_watch(args: &Args, threads: usize, filter: &Filter) -> Result<()> {
if args.dry_run {
bail!("--watch cannot be combined with --dry-run");
}
let debounce = Duration::from_millis(args.debounce.max(1));
sync_cycle(args, threads, filter);
let (tx, rx) = channel();
let mut watcher = notify::recommended_watcher(move |res| {
let _ = tx.send(res);
})
.context("creating filesystem watcher")?;
watcher
.watch(&args.src, RecursiveMode::Recursive)
.with_context(|| format!("watching {}", args.src.display()))?;
eprintln!(
"watching {} → {} (debounce {} ms; Ctrl-C to stop)",
args.src.display(),
args.dst.display(),
debounce.as_millis()
);
loop {
match rx.recv() {
Ok(Ok(event)) if !relevant(&event) => continue,
Ok(_) => {}
Err(_) => break, }
drain_for(&rx, debounce);
sync_cycle(args, threads, filter);
}
Ok(())
}
fn relevant(event: ¬ify::Event) -> bool {
!event
.paths
.iter()
.all(|p| p.components().any(|c| c.as_os_str() == ".ripsync"))
}
fn drain_for(rx: &std::sync::mpsc::Receiver<notify::Result<notify::Event>>, window: Duration) {
let deadline = Instant::now() + window;
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return;
}
match rx.recv_timeout(remaining) {
Ok(_) => {} Err(RecvTimeoutError::Timeout) => return,
Err(RecvTimeoutError::Disconnected) => return,
}
}
}
fn sync_cycle(args: &Args, threads: usize, filter: &Filter) {
let start = Instant::now();
match run_local_once(args, threads, filter) {
Ok(()) => log_synced(&args.dst, start),
Err(error) => eprintln!("sync failed: {error:#}"),
}
}
fn log_synced(dst: &Path, start: Instant) {
eprintln!(
"synced {} in {:.3}s",
dst.display(),
start.elapsed().as_secs_f64()
);
}