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>;
fn profile_suffix() -> String {
profile_suffix_from(crate::config::profile::active_profile())
}
pub(crate) fn profile_suffix_from(profile: Option<&str>) -> String {
match profile {
Some(name) if !name.is_empty() && name != "default" => format!(" (profile: {name})"),
_ => String::new(),
}
}
pub type ReloadNotify = Arc<dyn Fn(String) + Send + Sync>;
pub fn spawn(
callbacks: Vec<ReloadCallback>,
notify: Option<ReloadNotify>,
) -> 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"
);
if let Some(ref notify) = notify {
notify(format!(
"⚠️ config.toml failed to parse{} — running on the previous \
config. Your on-disk edits are NOT active until the TOML parses \
cleanly. Fix {} and save; hot-reload will apply automatically.",
profile_suffix(),
base.join("config.toml").display(),
));
}
} 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
);
if let Some(ref notify) = notify {
notify(format!(
"⚠️ config reload failed{} — keeping the running config. Error: {}. \
Fix {} and save; hot-reload will apply automatically.",
profile_suffix(),
e,
base.join("config.toml").display(),
));
}
}
}
}
tracing::info!("ConfigWatcher: stopped");
})
}