1use crate::EnvVarManager;
2use color_eyre::Result;
3use notify::{RecommendedWatcher, RecursiveMode};
4use notify_debouncer_mini::{DebounceEventResult, DebouncedEvent, Debouncer, new_debouncer};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::sync::mpsc::{Receiver, Sender, channel};
8use std::sync::{Arc, Mutex};
9use std::time::Duration;
10use std::{fs, thread};
11
12#[derive(Debug, Clone)]
13pub enum SyncMode {
14 WatchOnly,
16 FileToSystem,
18 SystemToFile,
20 Bidirectional,
22}
23
24#[derive(Debug, Clone)]
25pub struct WatchConfig {
26 pub paths: Vec<PathBuf>,
28 pub mode: SyncMode,
30 pub auto_reload: bool,
32 pub debounce_duration: Duration,
34 pub patterns: Vec<String>,
36 pub log_changes: bool,
38 pub conflict_strategy: ConflictStrategy,
40}
41
42#[derive(Debug, Clone)]
43pub enum ConflictStrategy {
44 UseLatest,
46 PreferFile,
48 PreferSystem,
50 AskUser,
52}
53
54impl Default for WatchConfig {
55 fn default() -> Self {
56 Self {
57 paths: vec![PathBuf::from(".")],
58 mode: SyncMode::FileToSystem,
59 auto_reload: true,
60 debounce_duration: Duration::from_millis(300),
61 patterns: vec![
62 "*.env".to_string(),
63 ".env.*".to_string(),
64 "*.yaml".to_string(),
65 "*.yml".to_string(),
66 "*.toml".to_string(),
67 ],
68 log_changes: true,
69 conflict_strategy: ConflictStrategy::UseLatest,
70 }
71 }
72}
73
74pub struct EnvWatcher {
75 config: WatchConfig,
76 debouncer: Option<Debouncer<RecommendedWatcher>>,
77 stop_signal: Option<Sender<()>>,
78 manager: Arc<Mutex<EnvVarManager>>,
79 change_log: Arc<Mutex<Vec<ChangeEvent>>>,
80 variable_filter: Option<Vec<String>>,
81 output_file: Option<PathBuf>,
82}
83
84#[derive(Debug, Clone, serde::Serialize)]
85pub struct ChangeEvent {
86 pub timestamp: chrono::DateTime<chrono::Utc>,
87 pub path: PathBuf,
88 pub change_type: ChangeType,
89 pub details: String,
90}
91
92#[derive(Debug, Clone, serde::Serialize)]
93pub enum ChangeType {
94 FileCreated,
95 FileModified,
96 FileDeleted,
97 VariableAdded(String),
98 VariableModified(String),
99 VariableDeleted(String),
100}
101
102impl EnvWatcher {
103 #[must_use]
104 pub fn new(config: WatchConfig, manager: EnvVarManager) -> Self {
105 Self {
106 config,
107 debouncer: None,
108 stop_signal: None,
109 manager: Arc::new(Mutex::new(manager)),
110 change_log: Arc::new(Mutex::new(Vec::new())),
111 variable_filter: None,
112 output_file: None,
113 }
114 }
115
116 pub fn start(&mut self) -> Result<()> {
125 let (tx, rx) = channel();
126 let (stop_tx, stop_rx) = channel();
127
128 let tx_clone = tx;
130 let log_changes = self.config.log_changes;
131
132 let mut debouncer = new_debouncer(
134 self.config.debounce_duration,
135 move |result: DebounceEventResult| match result {
136 Ok(events) => {
137 for event in events {
138 if log_changes {
139 println!("đ File system event detected: {}", event.path.display());
140 }
141 if let Err(e) = tx_clone.send(event) {
142 eprintln!("Failed to send event: {e:?}");
143 }
144 }
145 }
146 Err(errors) => {
147 eprintln!("Watch error: {errors:?}");
148 }
149 },
150 )?;
151
152 let watcher = debouncer.watcher();
154
155 for path in &self.config.paths {
157 if path.exists() {
158 if path.is_file() {
159 if let Some(parent) = path.parent() {
161 watcher.watch(parent, RecursiveMode::NonRecursive)?;
162 if self.config.log_changes {
163 println!("đ Watching file: {} (via parent directory)", path.display());
164 }
165 }
166 } else {
167 watcher.watch(path, RecursiveMode::Recursive)?;
168 if self.config.log_changes {
169 println!("đ Watching directory: {}", path.display());
170 }
171 }
172 } else {
173 eprintln!("â ī¸ Path does not exist: {}", path.display());
174 }
175 }
176
177 self.debouncer = Some(debouncer);
179 self.stop_signal = Some(stop_tx);
180
181 let config = self.config.clone();
183 let manager = Arc::clone(&self.manager);
184 let change_log = Arc::clone(&self.change_log);
185 let variable_filter = self.variable_filter.clone();
186 let output_file = self.output_file.clone();
187
188 thread::spawn(move || {
189 Self::handle_events(
190 &rx,
191 &stop_rx,
192 &config,
193 &manager,
194 &change_log,
195 variable_filter.as_ref(),
196 output_file.as_ref(),
197 );
198 });
199
200 if matches!(self.config.mode, SyncMode::SystemToFile | SyncMode::Bidirectional) {
201 self.start_system_monitor();
202 }
203
204 Ok(())
205 }
206
207 pub fn stop(&mut self) -> Result<()> {
214 if let Some(stop_signal) = self.stop_signal.take() {
216 let _ = stop_signal.send(());
217 }
218
219 self.debouncer = None;
221
222 if self.config.log_changes {
223 println!("đ Stopped watching");
224 }
225
226 Ok(())
227 }
228
229 fn handle_events(
230 rx: &Receiver<DebouncedEvent>,
231 stop_rx: &Receiver<()>,
232 config: &WatchConfig,
233 manager: &Arc<Mutex<EnvVarManager>>,
234 change_log: &Arc<Mutex<Vec<ChangeEvent>>>,
235 variable_filter: Option<&Vec<String>>,
236 output_file: Option<&PathBuf>,
237 ) {
238 loop {
239 if stop_rx.try_recv().is_ok() {
241 break;
242 }
243
244 match rx.recv_timeout(Duration::from_millis(100)) {
246 Ok(event) => {
247 if config.log_changes {
248 println!("đ Processing event for: {}", event.path.display());
249 }
250
251 let path = event.path.clone();
252
253 if let Some(output) = output_file {
255 if path == *output && matches!(config.mode, SyncMode::Bidirectional) {
256 if config.log_changes {
257 println!("âī¸ Skipping output file to avoid loop");
258 }
259 continue;
260 }
261 }
262
263 if !Self::matches_patterns(&path, &config.patterns) {
265 if config.log_changes {
266 println!("âī¸ File doesn't match patterns: {}", path.display());
267 }
268 continue;
269 }
270
271 let change_type = if path.exists() {
273 if config.log_changes {
274 println!("âī¸ Modified: {}", path.display());
275 }
276 ChangeType::FileModified
277 } else {
278 if config.log_changes {
279 println!("đī¸ Deleted: {}", path.display());
280 }
281 ChangeType::FileDeleted
282 };
283
284 match config.mode {
286 SyncMode::WatchOnly => {
287 Self::log_change(
288 change_log,
289 path,
290 change_type,
291 "File changed (watch only mode)".to_string(),
292 );
293 }
294 SyncMode::FileToSystem | SyncMode::Bidirectional => {
295 if matches!(change_type, ChangeType::FileModified | ChangeType::FileCreated) {
296 if let Err(e) = Self::handle_file_change(
297 &path,
298 change_type,
299 config,
300 manager,
301 change_log,
302 variable_filter,
303 ) {
304 eprintln!("Error handling file change: {e}");
305 }
306 }
307 }
308 SyncMode::SystemToFile => {
309 if config.log_changes {
311 println!("âšī¸ Ignoring file change in system-to-file mode");
312 }
313 }
314 }
315 }
316 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
317 }
319 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
320 break;
322 }
323 }
324 }
325 }
326
327 fn handle_file_change(
328 path: &Path,
329 _change_type: ChangeType,
330 config: &WatchConfig,
331 manager: &Arc<Mutex<EnvVarManager>>,
332 change_log: &Arc<Mutex<Vec<ChangeEvent>>>,
333 variable_filter: Option<&Vec<String>>,
334 ) -> Result<()> {
335 if !config.auto_reload {
336 return Ok(());
337 }
338
339 thread::sleep(Duration::from_millis(50));
341
342 let mut manager = manager.lock().unwrap();
344
345 let before_vars: HashMap<String, String> = manager
347 .list()
348 .into_iter()
349 .filter(|v| {
350 variable_filter
351 .as_ref()
352 .is_none_or(|filter| filter.iter().any(|f| v.name.contains(f)))
353 })
354 .map(|v| (v.name.clone(), v.value.clone()))
355 .collect();
356
357 let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
359
360 let load_result = match extension {
361 "env" => Self::load_env_file(path, &mut manager, variable_filter),
362 "yaml" | "yml" => Self::load_yaml_file(path, &mut manager, variable_filter),
363 "json" => Self::load_json_file(path, &mut manager, variable_filter),
364 _ => {
365 Self::load_env_file(path, &mut manager, variable_filter)
367 }
368 };
369
370 if let Err(e) = load_result {
371 eprintln!("Failed to load file: {e}");
372 return Err(e);
373 }
374
375 let after_vars = manager.list();
377 let mut changes_made = false;
378
379 for var in after_vars {
380 if let Some(filter) = variable_filter {
382 if !filter.iter().any(|f| var.name.contains(f)) {
383 continue;
384 }
385 }
386
387 if let Some(old_value) = before_vars.get(&var.name) {
388 if old_value != &var.value {
389 Self::log_change(
390 change_log,
391 path.to_path_buf(),
392 ChangeType::VariableModified(var.name.clone()),
393 format!("Changed {} from '{}' to '{}'", var.name, old_value, var.value),
394 );
395
396 if config.log_changes {
397 println!(" đ {} changed from '{}' to '{}'", var.name, old_value, var.value);
398 }
399 changes_made = true;
400 }
401 } else {
402 Self::log_change(
403 change_log,
404 path.to_path_buf(),
405 ChangeType::VariableAdded(var.name.clone()),
406 format!("Added {} = '{}'", var.name, var.value),
407 );
408
409 if config.log_changes {
410 println!(" â {} = '{}'", var.name, var.value);
411 }
412 changes_made = true;
413 }
414 }
415
416 for (name, _) in before_vars {
418 if manager.get(&name).is_none() {
419 Self::log_change(
420 change_log,
421 path.to_path_buf(),
422 ChangeType::VariableDeleted(name.clone()),
423 format!("Deleted {name}"),
424 );
425
426 if config.log_changes {
427 println!(" â {name} deleted");
428 }
429 changes_made = true;
430 }
431 }
432
433 if !changes_made && config.log_changes {
434 println!(" âšī¸ No changes detected");
435 }
436
437 Ok(())
438 }
439
440 fn load_env_file(path: &Path, manager: &mut EnvVarManager, variable_filter: Option<&Vec<String>>) -> Result<()> {
441 let content = fs::read_to_string(path)?;
442
443 for line in content.lines() {
444 let line = line.trim();
445 if line.is_empty() || line.starts_with('#') {
446 continue;
447 }
448
449 if let Some((key, value)) = line.split_once('=') {
450 let key = key.trim();
451 let value = value.trim().trim_matches('"').trim_matches('\'');
452
453 if let Some(filter) = variable_filter {
455 if !filter.iter().any(|f| key.contains(f)) {
456 continue;
457 }
458 }
459
460 manager.set(key, value, true)?;
461 }
462 }
463
464 Ok(())
465 }
466
467 fn load_yaml_file(path: &Path, manager: &mut EnvVarManager, variable_filter: Option<&Vec<String>>) -> Result<()> {
468 let content = fs::read_to_string(path)?;
469 let yaml: serde_yaml::Value = serde_yaml::from_str(&content)?;
470
471 if let serde_yaml::Value::Mapping(map) = yaml {
472 for (key, value) in map {
473 if let (Some(key_str), Some(value_str)) = (key.as_str(), value.as_str()) {
474 if let Some(filter) = variable_filter {
476 if !filter.iter().any(|f| key_str.contains(f)) {
477 continue;
478 }
479 }
480
481 manager.set(key_str, value_str, true)?;
482 }
483 }
484 }
485
486 Ok(())
487 }
488
489 fn load_json_file(path: &Path, manager: &mut EnvVarManager, variable_filter: Option<&Vec<String>>) -> Result<()> {
490 let content = fs::read_to_string(path)?;
491 let json: serde_json::Value = serde_json::from_str(&content)?;
492
493 if let serde_json::Value::Object(map) = json {
494 for (key, value) in map {
495 if let serde_json::Value::String(value_str) = value {
496 if let Some(filter) = variable_filter {
498 if !filter.iter().any(|f| key.contains(f)) {
499 continue;
500 }
501 }
502
503 manager.set(&key, &value_str, true)?;
504 }
505 }
506 }
507
508 Ok(())
509 }
510
511 fn start_system_monitor(&mut self) {
512 let manager = Arc::clone(&self.manager);
513 let config = self.config.clone();
514 let _change_log = Arc::clone(&self.change_log);
515 let variable_filter = self.variable_filter.clone();
516 let output_file = self.output_file.clone();
517
518 thread::spawn(move || {
519 let mut last_snapshot = HashMap::new();
520
521 loop {
522 thread::sleep(Duration::from_secs(1));
523
524 manager.lock().unwrap().load_all().ok();
525
526 let current_snapshot: HashMap<String, String> = manager
527 .lock()
528 .unwrap()
529 .list()
530 .iter()
531 .filter(|v| {
532 variable_filter
533 .as_ref()
534 .is_none_or(|filter| filter.iter().any(|f| v.name.contains(f)))
535 })
536 .map(|v| (v.name.clone(), v.value.clone()))
537 .collect();
538
539 if matches!(config.mode, SyncMode::SystemToFile | SyncMode::Bidirectional) {
541 if let Some(ref output) = output_file {
542 let mut changed = false;
543
544 for (name, value) in ¤t_snapshot {
545 if last_snapshot.get(name) != Some(value) {
546 changed = true;
547 if config.log_changes {
548 println!("đ System change detected: {name} changed");
549 }
550 }
551 }
552
553 for name in last_snapshot.keys() {
555 if !current_snapshot.contains_key(name) {
556 changed = true;
557 if config.log_changes {
558 println!("â System change detected: {name} deleted");
559 }
560 }
561 }
562
563 if changed {
564 let mut content = String::new();
566 #[allow(clippy::format_push_string)]
567 for (name, value) in ¤t_snapshot {
568 content.push_str(&format!("{name}={value}\n"));
569 }
570
571 if let Err(e) = fs::write(output, &content) {
572 eprintln!("Failed to write to output file: {e}");
573 } else if config.log_changes {
574 println!("đž Updated output file");
575 }
576 }
577 }
578 }
579
580 last_snapshot = current_snapshot;
581 }
582 });
583 }
584
585 fn matches_patterns(path: &Path, patterns: &[String]) -> bool {
586 let file_name = match path.file_name() {
587 Some(name) => name.to_string_lossy(),
588 None => return false,
589 };
590
591 patterns.iter().any(|pattern| {
592 if pattern.contains('*') {
593 let regex_pattern = pattern.replace('.', r"\.").replace('*', ".*");
594 if let Ok(re) = regex::Regex::new(&format!("^{regex_pattern}$")) {
595 return re.is_match(&file_name);
596 }
597 }
598 &file_name == pattern
599 })
600 }
601
602 fn log_change(change_log: &Arc<Mutex<Vec<ChangeEvent>>>, path: PathBuf, change_type: ChangeType, details: String) {
603 let event = ChangeEvent {
604 timestamp: chrono::Utc::now(),
605 path,
606 change_type,
607 details,
608 };
609
610 let mut log = change_log.lock().expect("Failed to lock change log");
611 log.push(event);
612
613 if log.len() > 1000 {
615 log.drain(0..100);
616 }
617 }
618
619 #[must_use]
625 pub fn get_change_log(&self) -> Vec<ChangeEvent> {
626 self.change_log.lock().expect("Failed to lock change log").clone()
627 }
628
629 pub fn export_change_log(&self, path: &Path) -> Result<()> {
637 let log = self.get_change_log();
638 let json = serde_json::to_string_pretty(&log)?;
639 fs::write(path, json)?;
640 Ok(())
641 }
642
643 pub fn set_variable_filter(&mut self, vars: Vec<String>) {
644 self.variable_filter = Some(vars);
645 }
646
647 pub fn set_output_file(&mut self, path: PathBuf) {
649 self.output_file = Some(path);
650 }
651}