use crate::commands::build::{build, BuildOptions};
use crate::config::CobbleConfig;
use notify::{Event, EventKind, RecursiveMode, Watcher};
use std::path::PathBuf;
use std::sync::mpsc;
use std::time::Duration;
#[allow(clippy::too_many_arguments)]
pub fn watch(
input: Option<PathBuf>,
output: Option<PathBuf>,
namespace: Option<String>,
pack_format: Option<String>,
description: Option<String>,
verbose: bool,
zip: bool,
validate: bool,
commands_json: PathBuf,
) -> Result<(), String> {
let (config, config_dir, config_path) = if let Some(config_path) = find_config(&input) {
let config = if pack_format.is_some() {
CobbleConfig::load_unvalidated(&config_path)?
} else {
CobbleConfig::load(&config_path)?
};
let config_dir = config_path.parent().unwrap().to_path_buf();
(Some(config), config_dir, Some(config_path))
} else {
(
None,
std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
None,
)
};
let watch_path = if let Some(ref input_path) = input {
input_path.clone()
} else if let Some(ref cfg) = config {
config_dir.join(&cfg.build.source)
} else {
return Err("No input specified and no cobble.toml found".to_string());
};
if !watch_path.exists() {
return Err(format!("Watch path does not exist: {:?}", watch_path));
}
println!("Watching: {:?}", watch_path);
println!("Press Ctrl+C to stop watching");
println!();
let build_input = input.clone();
println!("Performing initial build...");
let build_result = build(BuildOptions {
input: build_input.clone(),
output: output.clone(),
namespace: namespace.clone(),
pack_format: pack_format.clone(),
description: description.clone(),
verbose,
quiet: false,
zip,
validate,
dry_run: false,
commands_json: commands_json.clone(),
});
match build_result {
Ok(()) => println!("✓ Initial build succeeded\n"),
Err(e) => println!("✗ Initial build failed: {}\n", e),
}
let (tx, rx) = mpsc::channel();
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
if let Ok(event) = res {
let _ = tx.send(event);
}
})
.map_err(|e| format!("Failed to create watcher: {}", e))?;
watcher
.watch(&watch_path, RecursiveMode::Recursive)
.map_err(|e| format!("Failed to watch path: {}", e))?;
let mut watched_paths = vec![watch_path.clone()];
if let Some(config_path) = &config_path {
watcher
.watch(config_path, RecursiveMode::NonRecursive)
.map_err(|e| format!("Failed to watch config file: {}", e))?;
}
loop {
match rx.recv_timeout(Duration::from_millis(100)) {
Ok(event) => {
let should_rebuild = match event.kind {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {
event.paths.iter().any(|p| {
if p.extension()
.map(|ext| ext == "cbl" || ext == "cobble")
.unwrap_or(false)
{
return true;
}
if p.file_name()
.map(|name| name == "cobble.toml")
.unwrap_or(false)
{
return true;
}
false
})
}
_ => false,
};
if should_rebuild {
let config_changed = event.paths.iter().any(|p| {
p.file_name()
.map(|name| name == "cobble.toml")
.unwrap_or(false)
});
if config_changed && input.is_none() {
if let Some(config_path) = &config_path {
let updated_config = if pack_format.is_some() {
CobbleConfig::load_unvalidated(config_path)
} else {
CobbleConfig::load(config_path)
};
if let Ok(updated_config) = updated_config {
let updated_watch_path =
config_dir.join(&updated_config.build.source);
if updated_watch_path.exists()
&& !watched_paths.contains(&updated_watch_path)
{
watcher
.watch(&updated_watch_path, RecursiveMode::Recursive)
.map_err(|e| {
format!(
"Failed to watch updated source path {:?}: {}",
updated_watch_path, e
)
})?;
println!("Watching: {:?}", updated_watch_path);
watched_paths.push(updated_watch_path);
}
}
}
}
let changed_file = event
.paths
.first()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
println!("File changed: {}", changed_file);
println!("Rebuilding...");
let build_result = build(BuildOptions {
input: build_input.clone(),
output: output.clone(),
namespace: namespace.clone(),
pack_format: pack_format.clone(),
description: description.clone(),
verbose,
quiet: false,
zip,
validate,
dry_run: false,
commands_json: commands_json.clone(),
});
match build_result {
Ok(()) => println!("✓ Build succeeded\n"),
Err(e) => println!("✗ Build failed: {}\n", e),
}
}
}
Err(mpsc::RecvTimeoutError::Timeout) => {
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
return Err("Watcher disconnected".to_string());
}
}
}
#[allow(unreachable_code)]
Ok(())
}
fn find_config(input: &Option<PathBuf>) -> Option<PathBuf> {
if let Some(path) = input {
if path.is_file() {
if let Some(parent) = path.parent() {
return CobbleConfig::find_in_path(parent);
}
} else {
return CobbleConfig::find_in_path(path);
}
}
CobbleConfig::find_in_path(".")
}