envx_cli/
watch.rs

1use std::{path::PathBuf, time::Duration};
2
3use clap::{Args, ValueEnum};
4use color_eyre::Result;
5use envx_core::{ConflictStrategy, EnvVarManager, EnvWatcher, SyncMode, WatchConfig};
6
7#[derive(Debug, Clone, ValueEnum)]
8pub enum Direction {
9    /// Sync from files to system (default)
10    FileToSystem,
11    /// Sync from system to files
12    SystemToFile,
13    /// Bidirectional synchronization
14    Bidirectional,
15}
16
17#[derive(Args, Clone)]
18pub struct WatchArgs {
19    /// Files or directories to watch (defaults to current directory)
20    #[arg(value_name = "PATH")]
21    pub paths: Vec<PathBuf>,
22
23    /// Sync direction
24    #[arg(short, long, value_enum, default_value = "file-to-system")]
25    pub direction: Direction,
26
27    /// Output file for system-to-file sync
28    #[arg(short, long)]
29    pub output: Option<PathBuf>,
30
31    /// File patterns to watch
32    #[arg(short, long)]
33    pub pattern: Vec<String>,
34
35    /// Debounce duration in milliseconds
36    #[arg(long, default_value = "300")]
37    pub debounce: u64,
38
39    /// Log changes to file
40    #[arg(short, long)]
41    pub log: Option<PathBuf>,
42
43    /// Variables to sync (sync all if not specified)
44    #[arg(short, long)]
45    pub vars: Vec<String>,
46
47    /// Quiet mode - less output
48    #[arg(short, long)]
49    pub quiet: bool,
50}
51
52/// Handle file watching and synchronization operations.
53///
54/// # Errors
55///
56/// This function will return an error if:
57/// - Required output file is not specified for system-to-file or bidirectional sync
58/// - Environment variable manager operations fail (loading, setting)
59/// - Profile or project manager initialization fails
60/// - File watcher creation or operation fails
61/// - File I/O operations fail during synchronization
62/// - Ctrl+C signal handler setup fails
63/// - Change log export operations fail
64/// - Invalid watch configuration is provided
65/// - File system permissions prevent watching or writing to specified paths
66pub fn handle_watch(args: &WatchArgs) -> Result<()> {
67    // Validate arguments
68    if matches!(args.direction, Direction::SystemToFile | Direction::Bidirectional) && args.output.is_none() {
69        return Err(color_eyre::eyre::eyre!(
70            "Output file required for system-to-file synchronization. Use --output <file>"
71        ));
72    }
73
74    let sync_mode = match args.direction {
75        Direction::FileToSystem => SyncMode::FileToSystem,
76        Direction::SystemToFile => SyncMode::SystemToFile,
77        Direction::Bidirectional => SyncMode::Bidirectional,
78    };
79
80    let mut config = WatchConfig {
81        paths: if args.paths.is_empty() {
82            vec![PathBuf::from(".")]
83        } else {
84            args.paths.clone()
85        },
86        mode: sync_mode,
87        auto_reload: true,
88        debounce_duration: Duration::from_millis(args.debounce),
89        log_changes: !args.quiet,
90        conflict_strategy: ConflictStrategy::UseLatest,
91        ..Default::default()
92    };
93
94    if !args.pattern.is_empty() {
95        config.patterns.clone_from(&args.pattern);
96    }
97
98    // Add output file to watch paths if bidirectional
99    if let Some(output) = &args.output {
100        if matches!(args.direction, Direction::Bidirectional) {
101            config.paths.push(output.clone());
102        }
103    }
104
105    let mut manager = EnvVarManager::new();
106    manager.load_all()?;
107
108    let mut watcher = EnvWatcher::new(config.clone(), manager);
109
110    // Set up the watcher with variable filtering
111    if !args.vars.is_empty() {
112        watcher.set_variable_filter(args.vars.clone());
113    }
114
115    if let Some(output) = args.output.clone() {
116        watcher.set_output_file(output);
117    }
118
119    print_watch_header(args, &config);
120
121    watcher.start()?;
122
123    // Set up Ctrl+C handler
124    let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
125    let r = running.clone();
126
127    ctrlc::set_handler(move || {
128        r.store(false, std::sync::atomic::Ordering::SeqCst);
129    })?;
130
131    // Keep running until Ctrl+C
132    while running.load(std::sync::atomic::Ordering::SeqCst) {
133        std::thread::sleep(Duration::from_secs(1));
134
135        if let Some(log_file) = &args.log {
136            let _ = watcher.export_change_log(log_file);
137        }
138    }
139
140    watcher.stop()?;
141    println!("\nāœ… Watch mode stopped");
142
143    Ok(())
144}
145
146fn print_watch_header(args: &WatchArgs, config: &WatchConfig) {
147    println!("šŸ”„ Starting envx watch mode");
148    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
149
150    match args.direction {
151        Direction::FileToSystem => {
152            println!("šŸ“‚ → šŸ’» Syncing from files to system");
153            println!(
154                "Watching: {}",
155                config
156                    .paths
157                    .iter()
158                    .map(|p| p.display().to_string())
159                    .collect::<Vec<_>>()
160                    .join(", ")
161            );
162        }
163        Direction::SystemToFile => {
164            println!("šŸ’» → šŸ“‚ Syncing from system to file");
165            if let Some(output) = &args.output {
166                println!("Output: {}", output.display());
167            }
168        }
169        Direction::Bidirectional => {
170            println!("šŸ“‚ ā†”ļø šŸ’» Bidirectional sync");
171            println!(
172                "Watching: {}",
173                config
174                    .paths
175                    .iter()
176                    .map(|p| p.display().to_string())
177                    .collect::<Vec<_>>()
178                    .join(", ")
179            );
180            if let Some(output) = &args.output {
181                println!("Output: {}", output.display());
182            }
183        }
184    }
185
186    if !args.vars.is_empty() {
187        println!("Variables: {}", args.vars.join(", "));
188    }
189
190    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
191    println!("Press Ctrl+C to stop\n");
192}