1use std::path::Path;
2use std::sync::atomic::{AtomicBool, Ordering::Relaxed};
3use std::sync::{Arc, mpsc};
4use std::time::Instant;
5
6use anyhow::{Context, Result};
7use colored::Colorize;
8use notify::RecursiveMode;
9use notify_debouncer_mini::{DebouncedEventKind, new_debouncer};
10
11use crate::commands;
12use crate::config::RpcConfig;
13
14#[cfg(not(tarpaulin_include))]
19pub fn run(config: &RpcConfig) -> Result<()> {
20 let running = Arc::new(AtomicBool::new(true));
21 let running_clone = running.clone();
22
23 ctrlc::set_handler(move || {
24 running_clone.store(false, Relaxed);
25 })
26 .context("Failed to set Ctrl+C handler")?;
27
28 if config.watch.clear_screen {
29 clear_screen();
30 }
31 print_banner(config);
32
33 if let Err(e) = generate(config) {
35 print_error(&e);
36 }
37
38 let (tx, rx) = mpsc::channel();
40 let debounce_duration = std::time::Duration::from_millis(config.watch.debounce_ms);
41
42 let mut debouncer =
43 new_debouncer(debounce_duration, tx).context("Failed to create file watcher")?;
44
45 debouncer
46 .watcher()
47 .watch(config.input.dir.as_ref(), RecursiveMode::Recursive)
48 .with_context(|| format!("Failed to watch {}", config.input.dir.display()))?;
49
50 println!(
51 " {} for changes in {}\n",
52 "Watching".cyan().bold(),
53 config.input.dir.display().to_string().underline(),
54 );
55
56 while running.load(Relaxed) {
57 match rx.recv_timeout(std::time::Duration::from_millis(100)) {
58 Ok(Ok(events)) => {
59 let has_rs_change = events.iter().any(|e| {
60 e.kind == DebouncedEventKind::Any
61 && e.path.extension().is_some_and(|ext| ext == "rs")
62 });
63
64 if has_rs_change {
65 let changed: Vec<&Path> = events
66 .iter()
67 .filter(|e| e.path.extension().is_some_and(|ext| ext == "rs"))
68 .map(|e| e.path.as_path())
69 .collect();
70
71 if config.watch.clear_screen {
72 clear_screen();
73 print_banner(config);
74 }
75
76 print_change(&changed);
77
78 if let Err(e) = generate(config) {
79 print_error(&e);
80 }
81 }
82 }
83 Ok(Err(errs)) => {
84 eprintln!(" {} Watch error: {errs}", "✗".red().bold());
85 }
86 Err(mpsc::RecvTimeoutError::Timeout) => continue,
87 Err(mpsc::RecvTimeoutError::Disconnected) => break,
88 }
89 }
90
91 println!("\n {} Stopped watching.", "●".dimmed());
92 Ok(())
93}
94
95#[cfg(not(tarpaulin_include))]
97fn generate(config: &RpcConfig) -> Result<()> {
98 let start = Instant::now();
99
100 let manifest = commands::generate_all(config)?;
101
102 let elapsed = start.elapsed();
103
104 println!(
105 " {} Generated {} procedure(s), {} struct(s) in {:.0?}",
106 "✓".green().bold(),
107 manifest.procedures.len().to_string().bold(),
108 manifest.structs.len().to_string().bold(),
109 elapsed,
110 );
111 println!(
112 " {} {}",
113 "→".dimmed(),
114 config.output.types.display().to_string().dimmed(),
115 );
116 println!(
117 " {} {}",
118 "→".dimmed(),
119 config.output.client.display().to_string().dimmed(),
120 );
121 for path in [
122 &config.output.svelte,
123 &config.output.react,
124 &config.output.vue,
125 &config.output.solid,
126 ]
127 .into_iter()
128 .flatten()
129 {
130 println!(
131 " {} {}",
132 "→".dimmed(),
133 path.display().to_string().dimmed(),
134 );
135 }
136
137 Ok(())
138}
139
140#[cfg(not(tarpaulin_include))]
141fn print_banner(config: &RpcConfig) {
142 println!();
143 println!(" {} {}", "metaxy".bold(), "watch mode".cyan(),);
144 println!(" {} {}", "api dir:".dimmed(), config.input.dir.display(),);
145 println!(" {} {}", "types:".dimmed(), config.output.types.display(),);
146 println!(
147 " {} {}",
148 "client:".dimmed(),
149 config.output.client.display(),
150 );
151 if let Some(svelte) = &config.output.svelte {
152 println!(" {} {}", "svelte:".dimmed(), svelte.display(),);
153 }
154 if let Some(react) = &config.output.react {
155 println!(" {} {}", "react:".dimmed(), react.display(),);
156 }
157 if let Some(vue) = &config.output.vue {
158 println!(" {} {}", "vue:".dimmed(), vue.display(),);
159 }
160 if let Some(solid) = &config.output.solid {
161 println!(" {} {}", "solid:".dimmed(), solid.display(),);
162 }
163 println!();
164}
165
166#[cfg(not(tarpaulin_include))]
167fn print_change(paths: &[&Path]) {
168 for p in paths {
169 let name = p
170 .file_name()
171 .map(|n| n.to_string_lossy())
172 .unwrap_or_default();
173 println!("\n {} {}", "↻".yellow().bold(), name);
174 }
175}
176
177#[cfg(not(tarpaulin_include))]
178fn clear_screen() {
179 print!("\x1B[2J\x1B[H");
180}
181
182#[cfg(not(tarpaulin_include))]
183fn print_error(err: &anyhow::Error) {
184 eprintln!(" {} {err:#}", "✗".red().bold());
185 for cause in err.chain().skip(1) {
186 eprintln!(" {} {cause}", "caused by:".dimmed());
187 }
188}