1use chrono::Local;
2use clap::Args;
3use clap::ValueEnum;
4use color_eyre::Result;
5use comfy_table::Table;
6use comfy_table::presets::UTF8_FULL;
7use envx_core::EnvVarManager;
8use envx_core::EnvVarSource;
9use serde::Serialize;
10use std::collections::HashMap;
11use std::path::PathBuf;
12use std::time::Duration;
13
14#[derive(Debug, Clone, ValueEnum)]
15pub enum OutputFormat {
16 Live,
18 Compact,
20 JsonLines,
22}
23
24#[derive(Debug, Clone, ValueEnum)]
25pub enum SourceFilter {
26 #[value(name = "system")]
27 System,
28 #[value(name = "user")]
29 User,
30 #[value(name = "process")]
31 Process,
32 #[value(name = "shell")]
33 Shell,
34}
35
36impl From<SourceFilter> for EnvVarSource {
37 fn from(filter: SourceFilter) -> Self {
38 match filter {
39 SourceFilter::System => EnvVarSource::System,
40 SourceFilter::User => EnvVarSource::User,
41 SourceFilter::Process => EnvVarSource::Process,
42 SourceFilter::Shell => EnvVarSource::Shell,
43 }
44 }
45}
46
47#[derive(Args)]
48pub struct MonitorArgs {
49 #[arg(value_name = "VARIABLE")]
51 pub vars: Vec<String>,
52
53 #[arg(short, long)]
55 pub log: Option<PathBuf>,
56
57 #[arg(long)]
59 pub changes_only: bool,
60
61 #[arg(short, long, value_enum)]
63 pub source: Option<SourceFilter>,
64
65 #[arg(short, long, value_enum, default_value = "live")]
67 pub format: OutputFormat,
68
69 #[arg(long, default_value = "2")]
71 pub interval: u64,
72
73 #[arg(long)]
75 pub show_initial: bool,
76
77 #[arg(long)]
79 pub export_report: Option<PathBuf>,
80}
81
82struct MonitorState {
83 initial: HashMap<String, String>,
84 current: HashMap<String, String>,
85 changes: Vec<ChangeRecord>,
86 start_time: chrono::DateTime<Local>,
87}
88
89#[derive(Debug, Clone, Serialize)]
90struct ChangeRecord {
91 timestamp: chrono::DateTime<Local>,
92 variable: String,
93 change_type: String,
94 old_value: Option<String>,
95 new_value: Option<String>,
96}
97
98pub fn handle_monitor(args: MonitorArgs) -> Result<()> {
108 let mut manager = EnvVarManager::new();
109 manager.load_all()?;
110
111 let mut state = MonitorState {
112 initial: collect_variables(&manager, &args),
113 current: HashMap::new(),
114 changes: Vec::new(),
115 start_time: Local::now(),
116 };
117
118 print_monitor_header(&args);
119
120 if args.show_initial {
121 print_initial_state(&state.initial);
122 }
123
124 let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
126 let r = running.clone();
127
128 ctrlc::set_handler(move || {
129 r.store(false, std::sync::atomic::Ordering::SeqCst);
130 })?;
131
132 while running.load(std::sync::atomic::Ordering::SeqCst) {
134 std::thread::sleep(Duration::from_secs(args.interval));
135
136 let mut current_manager = EnvVarManager::new();
137 current_manager.load_all()?;
138
139 state.current = collect_variables(¤t_manager, &args);
140
141 let changes = detect_changes(&state);
142
143 if !changes.is_empty() || !args.changes_only {
144 display_changes(&changes, &args);
145
146 for change in changes {
148 state.changes.push(change.clone());
149
150 if let Some(log_path) = &args.log {
151 log_change(log_path, &change)?;
152 }
153 }
154 }
155
156 for (name, value) in &state.current {
158 state.initial.insert(name.clone(), value.clone());
159 }
160 }
161
162 if let Some(report_path) = args.export_report {
164 export_report(&state, &report_path)?;
165 println!("\nš Report exported to: {}", report_path.display());
166 }
167
168 print_monitor_summary(&state);
169
170 Ok(())
171}
172
173fn collect_variables(manager: &EnvVarManager, args: &MonitorArgs) -> HashMap<String, String> {
174 manager
175 .list()
176 .into_iter()
177 .filter(|var| {
178 (args.vars.is_empty() || args.vars.iter().any(|v| var.name.contains(v))) &&
180 (args.source.is_none() || args.source.as_ref().map(|s| EnvVarSource::from(s.clone())) == Some(var.source.clone()))
182 })
183 .map(|var| (var.name.clone(), var.value.clone()))
184 .collect()
185}
186
187fn detect_changes(state: &MonitorState) -> Vec<ChangeRecord> {
188 let mut changes = Vec::new();
189 let timestamp = Local::now();
190
191 for (name, value) in &state.current {
193 match state.initial.get(name) {
194 Some(old_value) if old_value != value => {
195 changes.push(ChangeRecord {
196 timestamp,
197 variable: name.clone(),
198 change_type: "modified".to_string(),
199 old_value: Some(old_value.clone()),
200 new_value: Some(value.clone()),
201 });
202 }
203 None => {
204 changes.push(ChangeRecord {
205 timestamp,
206 variable: name.clone(),
207 change_type: "added".to_string(),
208 old_value: None,
209 new_value: Some(value.clone()),
210 });
211 }
212 _ => {} }
214 }
215
216 for (name, value) in &state.initial {
218 if !state.current.contains_key(name) {
219 changes.push(ChangeRecord {
220 timestamp,
221 variable: name.clone(),
222 change_type: "deleted".to_string(),
223 old_value: Some(value.clone()),
224 new_value: None,
225 });
226 }
227 }
228
229 changes
230}
231
232fn display_changes(changes: &[ChangeRecord], args: &MonitorArgs) {
233 match args.format {
234 OutputFormat::Live => {
235 for change in changes {
236 let time = change.timestamp.format("%H:%M:%S");
237 match change.change_type.as_str() {
238 "added" => {
239 println!(
240 "[{}] ā {} = '{}'",
241 time,
242 change.variable,
243 change.new_value.as_ref().unwrap_or(&String::new())
244 );
245 }
246 "modified" => {
247 println!(
248 "[{}] š {} changed from '{}' to '{}'",
249 time,
250 change.variable,
251 change.old_value.as_ref().unwrap_or(&String::new()),
252 change.new_value.as_ref().unwrap_or(&String::new())
253 );
254 }
255 "deleted" => {
256 println!(
257 "[{}] ā {} deleted (was: '{}')",
258 time,
259 change.variable,
260 change.old_value.as_ref().unwrap_or(&String::new())
261 );
262 }
263 _ => {}
264 }
265 }
266 }
267 OutputFormat::Compact => {
268 for change in changes {
269 println!(
270 "{} {} {}",
271 change.timestamp.format("%Y-%m-%d %H:%M:%S"),
272 change.change_type.to_uppercase(),
273 change.variable
274 );
275 }
276 }
277 OutputFormat::JsonLines => {
278 for change in changes {
279 if let Ok(json) = serde_json::to_string(change) {
280 println!("{json}");
281 }
282 }
283 }
284 }
285}
286
287fn log_change(path: &PathBuf, change: &ChangeRecord) -> Result<()> {
288 use std::fs::OpenOptions;
289 use std::io::Write;
290
291 let mut file = OpenOptions::new().create(true).append(true).open(path)?;
292
293 writeln!(file, "{}", serde_json::to_string(change)?)?;
294 Ok(())
295}
296
297fn print_monitor_header(args: &MonitorArgs) {
298 println!("š Environment Variable Monitor");
299 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
300
301 if args.vars.is_empty() {
302 println!("Monitoring: All variables");
303 } else {
304 println!("Monitoring: {}", args.vars.join(", "));
305 }
306
307 if let Some(source) = &args.source {
308 println!("Source filter: {source:?}");
309 }
310
311 println!("Check interval: {} seconds", args.interval);
312 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
313 println!("Press Ctrl+C to stop\n");
314}
315
316fn print_initial_state(vars: &HashMap<String, String>) {
317 if vars.is_empty() {
318 println!("No variables match the criteria\n");
319 return;
320 }
321
322 let mut table = Table::new();
323 table.load_preset(UTF8_FULL);
324 table.set_header(vec!["Variable", "Initial Value"]);
325
326 for (name, value) in vars {
327 let display_value = if value.len() > 50 {
328 format!("{}...", &value[..47])
329 } else {
330 value.clone()
331 };
332 table.add_row(vec![name.clone(), display_value]);
333 }
334
335 println!("Initial State:\n{table}\n");
336}
337
338fn print_monitor_summary(state: &MonitorState) {
339 let duration = Local::now().signed_duration_since(state.start_time);
340
341 println!("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
342 println!("š Monitoring Summary");
343 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
344 println!("Duration: {}", format_duration(duration));
345 println!("Total changes: {}", state.changes.len());
346
347 let mut added = 0;
348 let mut modified = 0;
349 let mut deleted = 0;
350
351 for change in &state.changes {
352 match change.change_type.as_str() {
353 "added" => added += 1,
354 "modified" => modified += 1,
355 "deleted" => deleted += 1,
356 _ => {}
357 }
358 }
359
360 println!(" ā Added: {added}");
361 println!(" š Modified: {modified}");
362 println!(" ā Deleted: {deleted}");
363}
364
365fn format_duration(duration: chrono::Duration) -> String {
366 let hours = duration.num_hours();
367 let minutes = duration.num_minutes() % 60;
368 let seconds = duration.num_seconds() % 60;
369
370 if hours > 0 {
371 format!("{hours}h {minutes}m {seconds}s")
372 } else if minutes > 0 {
373 format!("{minutes}m {seconds}s")
374 } else {
375 format!("{seconds}s")
376 }
377}
378
379fn export_report(state: &MonitorState, path: &PathBuf) -> Result<()> {
380 #[derive(Serialize)]
381 struct Report {
382 start_time: chrono::DateTime<Local>,
383 end_time: chrono::DateTime<Local>,
384 duration_seconds: i64,
385 total_changes: usize,
386 changes_by_type: HashMap<String, usize>,
387 changes: Vec<ChangeRecord>,
388 }
389
390 let mut changes_by_type = HashMap::new();
391 for change in &state.changes {
392 *changes_by_type.entry(change.change_type.clone()).or_insert(0) += 1;
393 }
394
395 let report = Report {
396 start_time: state.start_time,
397 end_time: Local::now(),
398 duration_seconds: Local::now().signed_duration_since(state.start_time).num_seconds(),
399 total_changes: state.changes.len(),
400 changes_by_type,
401 changes: state.changes.clone(),
402 };
403
404 let json = serde_json::to_string_pretty(&report)?;
405 std::fs::write(path, json)?;
406
407 Ok(())
408}