use notify::{Watcher, RecursiveMode, watcher};
use std::sync::mpsc::channel;
use std::process::Command as ProcessCommand;
use std::time::{Duration, Instant};
use std::io::{self, Write};
use std::path::PathBuf;
use colored::*;
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
#[structopt(name = "cargomon", about = "A Rust implementation inspired by nodemon")]
struct Opt {
#[structopt(short, long, default_value = ".")]
watch_path: String,
#[structopt(short, long, default_value = "2")]
debounce_secs: u64,
#[structopt(subcommand)]
cmd: Option<SubCommand>,
}
#[derive(Debug, StructOpt)]
enum SubCommand {
Help,
}
pub fn run() {
let opt = Opt::from_args();
if let Some(SubCommand::Help) = opt.cmd {
display_help();
return;
}
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap();
watcher.watch(&opt.watch_path, RecursiveMode::Recursive).unwrap();
println!("{}", "Watching for changes. Press Ctrl+C to exit.".green());
let mut last_build_time = Instant::now();
loop {
match rx.recv() {
Ok(_) => {
if last_build_time.elapsed() < Duration::from_secs(opt.debounce_secs) {
continue;
}
last_build_time = Instant::now();
println!("{}", "Change detected. Rebuilding...".yellow());
let output = ProcessCommand::new("cargo")
.arg("build")
.output()
.expect("Failed to execute cargo build");
if output.status.success() {
println!("{}", "Build successful. Running the program...".green());
let executable_path = find_executable();
let run_output = ProcessCommand::new(&executable_path)
.output()
.expect("Failed to run the program");
if run_output.status.success() {
io::stdout().write_all(&run_output.stdout).unwrap();
println!("{}", "Program executed successfully.".green());
} else {
io::stderr().write_all(&run_output.stderr).unwrap();
println!("{}", "Program execution failed.".red());
}
} else {
println!("{}", "Build failed. Error output:".red());
io::stderr().write_all(&output.stderr).unwrap();
}
println!("\n{}", "Continuing to watch for changes...".green());
}
Err(e) => println!("{}", format!("Watch error: {:?}", e).red()),
}
}
}
fn display_help() {
println!("{}", "Cargomon: A Rust implementation inspired by nodemon".green());
println!("{}", "Usage: cargomon [OPTIONS] [SUBCOMMAND]".yellow());
println!("\nOptions:");
println!(" -w, --watch-path <PATH> The directory to watch for changes (default: \".\")");
println!(" -d, --debounce-secs <SECS> The debounce time in seconds (default: 2)");
println!(" -h, --help Print help information");
println!(" -V, --version Print version information");
println!("\nSubcommands:");
println!(" help Display this help message");
println!("\nDescription:");
println!("Cargomon watches your Rust project for file changes and automatically");
println!("rebuilds and runs your application. It helps streamline the development");
println!("process by eliminating the need to manually recompile and restart your");
println!("application after each change.");
println!("\nExamples:");
println!(" cargomon");
println!(" cargomon --watch-path ./src --debounce-secs 5");
println!(" cargomon help");
}
fn find_executable() -> String {
let cargo_toml = std::fs::read_to_string("Cargo.toml").expect("Failed to read Cargo.toml");
let package_name = cargo_toml
.lines()
.find(|line| line.starts_with("name ="))
.and_then(|line| line.split('=').nth(1))
.map(|name| name.trim().trim_matches('"'))
.expect("Failed to find package name in Cargo.toml");
let mut path = PathBuf::from("target");
path.push("debug");
path.push(if cfg!(windows) {
format!("{}.exe", package_name)
} else {
package_name.to_string()
});
path.to_str().expect("Failed to convert path to string").to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_find_executable() {
let temp_dir = tempdir().unwrap();
let temp_path = temp_dir.path();
let cargo_toml_path = temp_path.join("Cargo.toml");
let mut cargo_toml = File::create(cargo_toml_path).unwrap();
writeln!(cargo_toml, "[package]\nname = \"test_project\"").unwrap();
std::env::set_current_dir(temp_path).unwrap();
let executable_path = find_executable();
let expected_path = if cfg!(windows) {
String::from(r"target\debug\test_project.exe")
} else {
String::from("target/debug/test_project")
};
assert_eq!(executable_path, expected_path);
}
#[test]
#[should_panic(expected = "Failed to read Cargo.toml")]
fn test_find_executable_no_cargo_toml() {
let temp_dir = tempdir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
find_executable();
}
#[test]
#[should_panic(expected = "Failed to find package name in Cargo.toml")]
fn test_find_executable_invalid_cargo_toml() {
let temp_dir = tempdir().unwrap();
let temp_path = temp_dir.path();
let cargo_toml_path = temp_path.join("Cargo.toml");
let mut cargo_toml = File::create(cargo_toml_path).unwrap();
writeln!(cargo_toml, "[package]\n# Missing name field").unwrap();
std::env::set_current_dir(temp_path).unwrap();
find_executable();
}
#[test]
fn test_opt_default_values() {
let opt = Opt::from_iter(&["test"]);
assert_eq!(opt.watch_path, ".");
assert_eq!(opt.debounce_secs, 2);
}
#[test]
fn test_opt_custom_values() {
let opt = Opt::from_iter(&["test", "--watch-path", "./src", "--debounce-secs", "5"]);
assert_eq!(opt.watch_path, "./src");
assert_eq!(opt.debounce_secs, 5);
}
}