use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use anyhow::{Context, Result};
use clap::Parser;
use colored::Colorize;
use glob::Pattern;
use notify::{RecursiveMode, Watcher};
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
mod bench_results;
mod stats;
use bench_results::BenchResults;
use stats::StatsCollector;
#[derive(Parser, Debug)]
#[clap(author, version, about)]
struct Args {
#[clap(required = false)]
command: Vec<String>,
#[clap(short, long, default_value = ".")]
watch: Vec<String>,
#[clap(short, long)]
ext: Option<String>,
#[clap(short = 'p', long)]
pattern: Vec<String>,
#[clap(short, long)]
ignore: Vec<String>,
#[clap(short, long, default_value = "100")]
debounce: u64,
#[clap(short = 'n', long)]
initial: bool,
#[clap(short, long)]
clear: bool,
#[clap(short = 'f', long)]
config: Option<String>,
#[clap(short, long)]
restart: bool,
#[clap(long)]
stats: bool,
#[clap(long, default_value = "10")]
stats_interval: u64,
#[clap(long)]
bench: bool,
}
#[derive(Debug, Serialize, Deserialize)]
struct Config {
command: Vec<String>,
watch: Option<Vec<String>>,
ext: Option<String>,
pattern: Option<Vec<String>>,
ignore: Option<Vec<String>>,
debounce: Option<u64>,
initial: Option<bool>,
clear: Option<bool>,
restart: Option<bool>,
stats: Option<bool>,
stats_interval: Option<u64>,
}
struct CommandRunner {
command: Vec<String>,
restart: bool,
clear: bool,
current_process: Option<Child>,
}
impl CommandRunner {
fn new(command: Vec<String>, restart: bool, clear: bool) -> Self {
Self {
command,
restart,
clear,
current_process: None,
}
}
fn run(&mut self) -> Result<()> {
if self.restart {
if let Some(ref mut child) = self.current_process {
let _ = child.kill();
let _ = child.wait();
}
}
if self.clear {
print!("\x1B[2J\x1B[1;1H");
}
println!(
"{} {}",
"▶️ Running:".bright_blue(),
self.command.join(" ").bright_yellow()
);
let child = if cfg!(target_os = "windows") {
Command::new("cmd").arg("/C").args(&self.command).spawn()
} else {
Command::new("sh")
.arg("-c")
.arg(self.command.join(" "))
.spawn()
}
.context("Failed to execute command")?;
if self.restart {
self.current_process = Some(child);
} else {
let status = child.wait_with_output()?;
if !status.status.success() {
println!(
"{} {}",
"Command exited with code:".bright_red(),
status.status
);
}
}
Ok(())
}
}
fn main() -> Result<()> {
let mut args = Args::parse();
if let Some(config_path) = &args.config {
let config = load_config(config_path)?;
merge_config(&mut args, config);
}
if args.bench {
return run_benchmarks();
}
if args.command.is_empty() {
anyhow::bail!("No command specified. Use CLI arguments or a config file.");
}
println!("{}", "🔥 Flash watching for changes...".bright_green());
let (tx, rx) = std::sync::mpsc::channel();
let stats_collector = Arc::new(Mutex::new(StatsCollector::new()));
if args.stats {
let stats = Arc::clone(&stats_collector);
thread::spawn(move || loop {
thread::sleep(Duration::from_secs(args.stats_interval));
let mut stats = stats.lock().unwrap();
stats.update_resource_usage();
stats.display_stats();
});
}
let include_patterns = args
.pattern
.iter()
.map(|p| glob::Pattern::new(p))
.collect::<Result<Vec<_>, _>>()
.context("Invalid glob pattern")?;
let ignore_patterns = args
.ignore
.iter()
.map(|p| glob::Pattern::new(p))
.collect::<Result<Vec<_>, _>>()
.context("Invalid ignore pattern")?;
let mut runner = CommandRunner::new(args.command.clone(), args.restart, args.clear);
if args.initial {
if let Err(e) = runner.run() {
eprintln!("{} {}", "Error running initial command:".bright_red(), e);
}
}
setup_watcher(&args, tx.clone(), Arc::clone(&stats_collector))?;
println!("{}", "Ready! Waiting for changes...".bright_green());
let mut recently_processed = std::collections::HashMap::new();
for path in rx {
if should_process_path(&path, &args.ext, &include_patterns, &ignore_patterns) {
let path_key = path.to_string_lossy().to_string();
let now = std::time::Instant::now();
if let Some(last_time) = recently_processed.get(&path_key) {
if now.duration_since(*last_time).as_millis() < args.debounce as u128 {
continue;
}
}
recently_processed.insert(path_key, now);
let display_path = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_else(|| path.to_str().unwrap_or("unknown path"));
println!(
"{} {}",
"📝 Change detected:".bright_blue(),
display_path.bright_green()
);
if args.stats {
let mut stats = stats_collector.lock().unwrap();
stats.record_file_change();
}
if let Err(e) = runner.run() {
eprintln!("{} {}", "Error running command:".bright_red(), e);
}
recently_processed.retain(|_, time| now.duration_since(*time).as_millis() < 10000);
}
}
Ok(())
}
fn run_benchmarks() -> Result<()> {
println!("{}", "Running benchmarks...".bright_green());
println!(
"{}",
"This will compare Flash with other file watchers.".bright_yellow()
);
let has_criterion = Command::new("cargo")
.args(["bench", "--bench", "file_watcher", "--help"])
.output()
.map(|output| output.status.success())
.unwrap_or(false);
if has_criterion {
println!(
"{}",
"Running real benchmarks (this may take a few minutes)...".bright_blue()
);
let status = Command::new("cargo")
.args(["bench", "--bench", "file_watcher"])
.status()
.context("Failed to run benchmarks")?;
if !status.success() {
println!(
"{}",
"Benchmark run failed, showing sample data instead...".bright_yellow()
);
show_sample_results();
}
} else {
println!(
"{}",
"No benchmark suite detected, showing sample data...".bright_yellow()
);
show_sample_results();
}
Ok(())
}
fn show_sample_results() {
let results = BenchResults::with_sample_data();
results.print_report();
println!(
"\n{}",
"Note: These are simulated results for demonstration.".bright_yellow()
);
println!(
"{}",
"Run 'cargo bench --bench file_watcher' for real benchmarks.".bright_blue()
);
}
fn load_config(path: &str) -> Result<Config> {
let content =
fs::read_to_string(path).context(format!("Failed to read config file: {}", path))?;
serde_yaml::from_str(&content).context(format!("Failed to parse config file: {}", path))
}
fn merge_config(args: &mut Args, config: Config) {
if args.command.is_empty() && !config.command.is_empty() {
args.command = config.command;
}
if args.watch.len() == 1 && args.watch[0] == "." {
if let Some(watch_dirs) = config.watch {
args.watch = watch_dirs;
}
}
if args.ext.is_none() {
args.ext = config.ext;
}
if args.pattern.is_empty() {
if let Some(patterns) = config.pattern {
args.pattern = patterns;
}
}
if args.ignore.is_empty() {
if let Some(ignores) = config.ignore {
args.ignore = ignores;
}
}
if args.debounce == 100 {
if let Some(debounce) = config.debounce {
args.debounce = debounce;
}
}
if !args.initial {
if let Some(initial) = config.initial {
args.initial = initial;
}
}
if !args.clear {
if let Some(clear) = config.clear {
args.clear = clear;
}
}
if !args.restart {
if let Some(restart) = config.restart {
args.restart = restart;
}
}
if !args.stats {
if let Some(stats) = config.stats {
args.stats = stats;
}
}
if args.stats_interval == 10 {
if let Some(interval) = config.stats_interval {
args.stats_interval = interval;
}
}
}
fn setup_watcher(
args: &Args,
tx: Sender<PathBuf>,
stats: Arc<Mutex<StatsCollector>>,
) -> Result<()> {
let stats_enabled = args.stats;
let event_tx = tx.clone();
let mut watcher =
notify::recommended_watcher(move |res: Result<notify::Event, notify::Error>| {
match res {
Ok(event) => {
if stats_enabled {
let mut stats = stats.lock().unwrap();
stats.record_watcher_call();
}
match event.kind {
notify::EventKind::Create(_)
| notify::EventKind::Modify(_)
| notify::EventKind::Remove(_) => {
for path in event.paths {
event_tx.send(path).unwrap_or_else(|e| {
eprintln!("{} {}", "Error sending event:".bright_red(), e);
});
}
}
_ => {
}
}
}
Err(e) => eprintln!("{} {}", "Watcher error:".bright_red(), e),
}
})?;
let mut watched_paths = std::collections::HashSet::new();
let mut watch_count = 0;
for pattern_str in &args.watch {
let path_obj = Path::new(pattern_str);
if path_obj.exists() && path_obj.is_dir() {
if watched_paths.insert(path_obj.to_path_buf()) {
watcher
.watch(path_obj, RecursiveMode::Recursive)
.context(format!("Failed to watch path: {}", pattern_str))?;
println!("{} {}", "Watching:".bright_blue(), pattern_str);
watch_count += 1;
}
} else {
let pattern = glob::Pattern::new(pattern_str)
.context(format!("Invalid watch pattern: {}", pattern_str))?;
let base_dir = ".";
let walker = WalkDir::new(base_dir)
.follow_links(true)
.into_iter()
.filter_entry(|e| !should_skip_dir(e.path(), &args.ignore));
let mut matched = false;
for entry in walker.filter_map(Result::ok) {
let path = entry.path();
if path.is_dir() && pattern.matches_path(path) && watched_paths.insert(path.to_path_buf()) {
watcher
.watch(path, RecursiveMode::Recursive)
.context(format!("Failed to watch matched path: {}", path.display()))?;
println!(
"{} {} (from pattern: {})",
"Watching:".bright_blue(),
path.display(),
pattern_str
);
watch_count += 1;
matched = true;
}
}
if !matched {
println!(
"{} {}",
"Warning: No directories matched pattern:".bright_yellow(),
pattern_str
);
}
}
}
if watch_count == 0 {
println!("{}", "Warning: No paths are being watched!".bright_yellow());
} else {
println!("{} {}", "Total watched paths:".bright_blue(), watch_count);
}
std::mem::forget(watcher);
if let Some(ext) = &args.ext {
println!("{} {}", "File extensions:".bright_blue(), ext);
}
if !args.pattern.is_empty() {
println!(
"{} {}",
"Include patterns:".bright_blue(),
args.pattern.join(", ")
);
}
if !args.ignore.is_empty() {
println!(
"{} {}",
"Ignore patterns:".bright_blue(),
args.ignore.join(", ")
);
}
println!(
"{} {}",
"Will execute:".bright_blue(),
args.command.join(" ").bright_yellow()
);
if args.stats {
println!(
"{} {} seconds",
"Performance stats enabled, interval:".bright_blue(),
args.stats_interval
);
}
Ok(())
}
fn should_skip_dir(path: &Path, ignore_patterns: &[String]) -> bool {
for pattern_str in ignore_patterns {
if let Ok(pattern) = glob::Pattern::new(pattern_str) {
if pattern.matches_path(path) {
return true;
}
}
}
false
}
pub fn should_process_path(
path: &Path,
extensions: &Option<String>,
include_patterns: &[Pattern],
ignore_patterns: &[Pattern],
) -> bool {
for pattern in ignore_patterns {
if pattern.matches_path(path) {
return false;
}
let mut current = path;
while let Some(parent) = current.parent() {
if pattern.matches_path(parent) {
return false;
}
current = parent;
}
}
if !include_patterns.is_empty() {
let mut matches = false;
for pattern in include_patterns {
if pattern.matches_path(path) {
matches = true;
break;
}
}
if !matches {
return false;
}
}
let extensions = match extensions {
Some(ext) => ext,
None => return true,
};
if let Some(ext) = path.extension() {
if let Some(ext_str) = ext.to_str() {
return extensions.split(',').any(|e| e.trim() == ext_str);
}
}
false
}