use crate::config::{Config, opencrabs_home};
use notify::{RecursiveMode, Watcher};
use std::sync::Arc;
use std::time::Duration;
pub type ReloadCallback = Arc<dyn Fn(Config) + Send + Sync>;
pub fn spawn(callbacks: Vec<ReloadCallback>) -> tokio::task::JoinHandle<()> {
tokio::task::spawn_blocking(move || {
let rt = tokio::runtime::Handle::current();
let base = opencrabs_home();
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher =
match notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
if let Ok(event) = res {
let relevant = event.paths.iter().any(|p| {
matches!(
p.file_name().and_then(|n| n.to_str()),
Some("config.toml" | "keys.toml" | "commands.toml")
)
});
if relevant {
let _ = tx.send(event);
}
}
}) {
Ok(w) => w,
Err(e) => {
tracing::error!("ConfigWatcher: failed to create watcher: {}", e);
return;
}
};
if base.exists()
&& let Err(e) = watcher.watch(&base, RecursiveMode::NonRecursive)
{
tracing::error!("ConfigWatcher: cannot watch {:?}: {}", base, e);
return;
}
tracing::info!(
"ConfigWatcher: watching config.toml, keys.toml and commands.toml in {:?}",
base
);
let debounce = Duration::from_millis(300);
while rx.recv().is_ok() {
let deadline = std::time::Instant::now() + debounce;
loop {
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
if remaining.is_zero() {
break;
}
match rx.recv_timeout(remaining) {
Ok(_) => {}
Err(_) => break,
}
}
match Config::load() {
Ok(new_config) => {
tracing::info!(
"ConfigWatcher: reloaded — firing {} callback(s)",
callbacks.len()
);
if Config::was_recovered() {
tracing::warn!(
"ConfigWatcher: config.toml failed to parse — running on \
last-known-good, snapshot left untouched"
);
} else {
crate::config::save_last_good_config();
}
Config::set_current(new_config.clone());
for cb in &callbacks {
let cb = cb.clone();
let cfg = new_config.clone();
rt.spawn(async move { cb(cfg) });
}
}
Err(e) => {
tracing::warn!(
"ConfigWatcher: reload failed, keeping current config: {}",
e
);
}
}
}
tracing::info!("ConfigWatcher: stopped");
})
}