1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
use std::path::{Path, PathBuf};
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 flash_watcher::{
compile_patterns, load_config, merge_config, run_benchmarks, should_process_path, Args,
CommandRunner,
};
use notify::{RecursiveMode, Watcher};
mod stats;
use stats::StatsCollector;
/// A blazingly fast file watcher that executes commands when files change
#[derive(Parser, Debug)]
#[clap(author, version, about)]
pub struct CliArgs {
/// The command to execute when files change
#[clap(required = false)]
pub command: Vec<String>,
/// Paths/patterns to watch (supports glob patterns like "src/**/*.js")
#[clap(short, long, default_value = ".")]
pub watch: Vec<String>,
/// File extensions to watch (e.g., "js,jsx,ts,tsx")
#[clap(short, long)]
pub ext: Option<String>,
/// Specific glob patterns to include (e.g., "src/**/*.{js,ts}")
#[clap(short = 'p', long)]
pub pattern: Vec<String>,
/// Glob patterns to ignore (e.g., "**/node_modules/**", "**/.git/**")
#[clap(short, long)]
pub ignore: Vec<String>,
/// Debounce time in milliseconds
#[clap(short, long, default_value = "100")]
pub debounce: u64,
/// Run command on startup
#[clap(short = 'n', long)]
pub initial: bool,
/// Clear console before each command run
#[clap(short, long)]
pub clear: bool,
/// Use configuration from file
#[clap(short = 'f', long)]
pub config: Option<String>,
/// Restart long-running processes instead of spawning new ones
#[clap(short, long)]
pub restart: bool,
/// Show performance statistics
#[clap(long)]
pub stats: bool,
/// Statistics update interval in seconds
#[clap(long, default_value = "10")]
pub stats_interval: u64,
/// Run benchmark against other file watchers
#[clap(long)]
pub bench: bool,
/// Fast startup mode - minimal output and optimizations
#[clap(long)]
pub fast: bool,
}
impl From<CliArgs> for Args {
fn from(cli: CliArgs) -> Self {
Args {
command: cli.command,
watch: cli.watch,
ext: cli.ext,
pattern: cli.pattern,
ignore: cli.ignore,
debounce: cli.debounce,
initial: cli.initial,
clear: cli.clear,
restart: cli.restart,
stats: cli.stats,
stats_interval: cli.stats_interval,
bench: cli.bench,
config: cli.config,
fast: cli.fast,
}
}
}
fn main() -> Result<()> {
let cli_args = CliArgs::parse();
let mut args: Args = cli_args.into();
// Load configuration file if specified
if let Some(config_path) = &args.config {
let config = load_config(config_path)?;
merge_config(&mut args, config);
}
// Run benchmarks if requested
if args.bench {
return run_benchmarks();
}
// Validate that we have a command to run
flash_watcher::validate_args(&args)?;
// Skip startup message for faster startup in fast mode
if !args.fast && !args.stats {
println!("{}", "🔥 Flash watching for changes...".bright_green());
}
// Create a channel to receive the events
let (tx, rx) = std::sync::mpsc::channel();
// Initialize stats collector only if needed
let stats_collector = if args.stats {
let collector = Arc::new(Mutex::new(StatsCollector::new()));
let stats = Arc::clone(&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();
});
Some(collector)
} else {
None
};
// Compile glob patterns for better filtering
let include_patterns = compile_patterns(&args.pattern)?;
let ignore_patterns = compile_patterns(&args.ignore)?;
// Create a command runner
let mut runner = CommandRunner::new(args.command.clone(), args.restart, args.clear);
// Run the command initially if requested
if args.initial {
if let Err(e) = runner.run() {
eprintln!("{} {}", "Error running initial command:".bright_red(), e);
}
}
// Set up the file watcher
setup_watcher(&args, tx.clone(), stats_collector.clone())?;
if !args.fast {
println!("{}", "Ready! Waiting for changes...".bright_green());
}
// Track recently processed paths to avoid duplicates - use PathBuf as key to avoid string allocation
let mut recently_processed = std::collections::HashMap::new();
// Listen for events in a loop
for path in rx {
if should_process_path(&path, &args.ext, &include_patterns, &ignore_patterns) {
// Check if we've seen this path recently - use PathBuf directly as key
let now = std::time::Instant::now();
if let Some(last_time) = recently_processed.get(&path) {
if now.duration_since(*last_time).as_millis() < args.debounce as u128 {
// Skip this event - too soon after the previous one
continue;
}
}
// Update the last processed time for this path
recently_processed.insert(path.clone(), now);
// Only format output if not in fast mode and not in stats mode
if !args.fast && !args.stats {
// Format the path to be more readable - just show the filename if possible
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()
);
}
// Record the file change in stats
if let Some(ref stats_collector) = stats_collector {
let mut stats = stats_collector.lock().unwrap();
stats.record_file_change();
}
if let Err(e) = runner.run() {
eprintln!("{} {}", "Error running command:".bright_red(), e);
}
// Clean up old entries in recently_processed
recently_processed.retain(|_, time| now.duration_since(*time).as_millis() < 10000);
}
}
Ok(())
}
fn setup_watcher(
args: &Args,
tx: Sender<PathBuf>,
stats: Option<Arc<Mutex<StatsCollector>>>,
) -> Result<()> {
// No need to capture stats_enabled since we check the Option directly
// Create a more direct event handler using standard notify
let event_tx = tx.clone();
let mut watcher =
notify::recommended_watcher(move |res: Result<notify::Event, notify::Error>| {
match res {
Ok(event) => {
// Record watcher call in stats
if let Some(ref stats) = stats {
let mut stats = stats.lock().unwrap();
stats.record_watcher_call();
}
// Process different event types
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);
});
}
}
_ => {
// Ignore other event types like access events
}
}
}
Err(e) => eprintln!("{} {}", "Watcher error:".bright_red(), e),
}
})?;
// Track watched paths to avoid duplicates
let mut watched_paths = std::collections::HashSet::new();
let mut watch_count = 0;
// Add paths to watch
for pattern_str in &args.watch {
// First check if it's a plain directory (for backward compatibility)
let path_obj = Path::new(pattern_str);
if path_obj.exists() && path_obj.is_dir() {
// It's a plain directory, watch it directly
if watched_paths.insert(path_obj.to_path_buf()) {
watcher
.watch(path_obj, RecursiveMode::Recursive)
.context(format!("Failed to watch path: {}", pattern_str))?;
if !args.fast {
println!("{} {}", "Watching:".bright_blue(), pattern_str);
}
watch_count += 1;
}
} else {
// For glob patterns, just watch the current directory and let filtering handle the rest
// This is much faster than walking the entire directory tree during startup
let current_dir = Path::new(".");
if watched_paths.insert(current_dir.to_path_buf()) {
watcher
.watch(current_dir, RecursiveMode::Recursive)
.context(format!(
"Failed to watch current directory for pattern: {}",
pattern_str
))?;
if !args.fast {
println!("{} . (pattern: {})", "Watching:".bright_blue(), pattern_str);
}
watch_count += 1;
}
}
}
if !args.fast {
if watch_count == 0 {
println!("{}", "Warning: No paths are being watched!".bright_yellow());
} else {
println!("{} {}", "Total watched paths:".bright_blue(), watch_count);
}
}
// Keep the watcher alive by storing it
std::mem::forget(watcher);
// Print other settings only if not in fast mode
if !args.fast {
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(", ")
);
}
// Print command
println!(
"{} {}",
"Will execute:".bright_blue(),
args.command.join(" ").bright_yellow()
);
// Print stats info if enabled
if args.stats {
println!(
"{} {} seconds",
"Performance stats enabled, interval:".bright_blue(),
args.stats_interval
);
}
}
Ok(())
}