use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use notify::RecursiveMode;
use notify::Watcher;
use crate::scan;
use crate::scan::BackgroundMsg;
const DEBOUNCE_DURATION: Duration = Duration::from_millis(500);
const MAX_WAIT: Duration = Duration::from_secs(1);
const POLL_INTERVAL: Duration = Duration::from_millis(500);
pub struct WatchRequest {
pub project_path: String,
pub abs_path: PathBuf,
}
pub fn spawn_disk_watcher(bg_tx: mpsc::Sender<BackgroundMsg>) -> mpsc::Sender<WatchRequest> {
let (watch_tx, watch_rx) = mpsc::channel();
thread::spawn(move || {
watcher_loop(bg_tx, watch_rx);
});
watch_tx
}
struct ProjectWatch {
project_path: String,
abs_path: PathBuf,
}
fn watcher_loop(bg_tx: mpsc::Sender<BackgroundMsg>, watch_rx: mpsc::Receiver<WatchRequest>) {
let (notify_tx, notify_rx) = mpsc::channel();
let handler = move |res| {
let _ = notify_tx.send(res);
};
let Ok(mut watcher) = notify::recommended_watcher(handler) else {
return;
};
let mut projects: HashMap<PathBuf, ProjectWatch> = HashMap::new();
let mut pending: HashMap<String, (Instant, Instant, PathBuf)> = HashMap::new();
loop {
loop {
match watch_rx.try_recv() {
Ok(req) => register(&mut watcher, &mut projects, req),
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => return,
}
}
while let Ok(result) = notify_rx.try_recv() {
if let Ok(event) = result {
handle_event(&event, &projects, &mut pending);
}
}
let now = Instant::now();
let ready: Vec<String> = pending
.iter()
.filter(|(_, (debounce, max, _))| now >= *debounce || now >= *max)
.map(|(key, _)| key.clone())
.collect();
for project_path in ready {
if let Some((_, _, abs_path)) = pending.remove(&project_path) {
let bytes = scan::dir_size(&abs_path);
if bg_tx
.send(BackgroundMsg::DiskUsage {
path: project_path,
bytes,
})
.is_err()
{
return; }
}
}
thread::sleep(POLL_INTERVAL);
}
}
fn register(
watcher: &mut impl Watcher,
projects: &mut HashMap<PathBuf, ProjectWatch>,
req: WatchRequest,
) {
let _ = watcher.watch(&req.abs_path, RecursiveMode::Recursive);
projects.insert(
req.abs_path.clone(),
ProjectWatch {
project_path: req.project_path,
abs_path: req.abs_path,
},
);
}
fn handle_event(
event: ¬ify::Event,
projects: &HashMap<PathBuf, ProjectWatch>,
pending: &mut HashMap<String, (Instant, Instant, PathBuf)>,
) {
let now = Instant::now();
let debounce_deadline = now + DEBOUNCE_DURATION;
for event_path in &event.paths {
let Some((_, project)) = projects
.iter()
.find(|(root, _)| event_path.starts_with(root))
else {
continue;
};
let relative = event_path
.strip_prefix(&project.abs_path)
.unwrap_or(event_path);
let is_target = relative
.components()
.next()
.is_some_and(|c| c.as_os_str() == "target");
if !is_target {
continue;
}
let max_deadline = pending
.get(&project.project_path)
.map_or(now + MAX_WAIT, |(_, max, _)| *max);
pending.insert(
project.project_path.clone(),
(debounce_deadline, max_deadline, project.abs_path.clone()),
);
}
}