1use rustbasic_core::colored::*;
2use std::io::{BufRead, Read, Write};
3
4pub fn to_snake_case(s: &str) -> String {
5 let mut snake = String::new();
6 for (i, ch) in s.chars().enumerate() {
7 if ch.is_uppercase() && i != 0 {
8 snake.push('_');
9 }
10 snake.push(ch.to_ascii_lowercase());
11 }
12 snake
13}
14pub fn to_pascal_case(s: &str) -> String {
15 let mut pascal = String::new();
16 let mut capitalize_next = true;
17 for ch in s.chars() {
18 if ch == '_' || ch == '-' {
19 capitalize_next = true;
20 } else if capitalize_next {
21 pascal.push(ch.to_ascii_uppercase());
22 capitalize_next = false;
23 } else {
24 pascal.push(ch);
25 }
26 }
27 pascal
28}
29
30pub fn open_browser(url: &str) {
31 let _ = match std::env::consts::OS {
32 "macos" => std::process::Command::new("open").arg(url).spawn(),
33 "windows" => std::process::Command::new("cmd").args(["/C", "start", url]).spawn(),
34 _ => std::process::Command::new("xdg-open").arg(url).spawn(),
35 };
36}
37
38pub fn wait_and_open(url: String) {
39 let addr = url.replace("http://", "").replace("https://", "");
40 let addr = addr.split('/').next().unwrap_or(&addr).to_string();
41
42 std::thread::spawn(move || {
43 for _ in 0..120 {
45 if std::net::TcpStream::connect(&addr).is_ok() {
46 open_browser(&url);
47 return;
48 }
49 std::thread::sleep(std::time::Duration::from_millis(500));
50 }
51 });
52}
53
54pub fn remove_dir_all_recursive(path: &std::path::Path) -> std::io::Result<()> {
55 if path.is_dir() {
56 for entry in std::fs::read_dir(path)? {
57 let entry = entry?;
58 let path = entry.path();
59 if path.is_dir() {
60 remove_dir_all_recursive(&path)?;
61 } else {
62 #[cfg(windows)]
63 {
64 let mut perms = std::fs::metadata(&path)?.permissions();
65 if perms.readonly() {
66 perms.set_readonly(false);
67 std::fs::set_permissions(&path, perms)?;
68 }
69 }
70 std::fs::remove_file(&path)?;
71 }
72 }
73 #[cfg(windows)]
74 {
75 let mut perms = std::fs::metadata(path)?.permissions();
76 if perms.readonly() {
77 perms.set_readonly(false);
78 std::fs::set_permissions(path, perms)?;
79 }
80 }
81 std::fs::remove_dir(path)?;
82 } else if path.exists() {
83 #[cfg(windows)]
84 {
85 let mut perms = std::fs::metadata(path)?.permissions();
86 if perms.readonly() {
87 perms.set_readonly(false);
88 std::fs::set_permissions(path, perms)?;
89 }
90 }
91 std::fs::remove_file(path)?;
92 }
93 Ok(())
94}
95
96struct CursorGuard;
97impl Drop for CursorGuard {
98 fn drop(&mut self) {
99 print!("\x1B[?25h");
100 let _ = std::io::stdout().flush();
101 }
102}
103
104pub fn parse_cargo_progress(line: &str) -> Option<(usize, usize, String)> {
105 let trimmed = line.trim_start();
106 if !trimmed.starts_with("Building [") {
107 return None;
108 }
109 let close_bracket = trimmed.find(']')?;
110 let after_bracket = trimmed[close_bracket + 1..].trim_start();
111
112 let colon = after_bracket.find(':')?;
113 let fraction_part = after_bracket[..colon].trim();
114
115 let slash = fraction_part.find('/')?;
116 let current = fraction_part[..slash].parse::<usize>().ok()?;
117 let total = fraction_part[slash + 1..].parse::<usize>().ok()?;
118
119 let details = after_bracket[colon + 1..].trim().to_string();
120
121 Some((current, total, details))
122}
123
124pub fn parse_compiling_crate(line: &str) -> Option<(String, String)> {
125 let trimmed = line.trim();
126 if trimmed.starts_with("Compiling ") || trimmed.starts_with("Checking ") || trimmed.starts_with("Documenting ") {
127 let parts: Vec<&str> = trimmed.split_whitespace().collect();
128 if parts.len() >= 2 {
129 return Some((parts[0].to_string(), parts[1].to_string()));
130 }
131 }
132 None
133}
134
135pub fn run_cargo_with_progress(mut cmd: std::process::Command) -> std::io::Result<std::process::ExitStatus> {
136 cmd.arg("--config").arg("term.progress.when=\"always\"");
138 cmd.arg("--config").arg("term.progress.width=100");
139
140 cmd.stdout(std::process::Stdio::piped());
141 cmd.stderr(std::process::Stdio::piped());
142 cmd.stdin(std::process::Stdio::inherit());
143
144 let mut child = cmd.spawn()?;
145
146 let child_stdout = child.stdout.take().unwrap();
147 let child_stderr = child.stderr.take().unwrap();
148
149 struct State {
150 current: usize,
151 total: usize,
152 last_crate_action: String,
153 last_crate: String,
154 active: bool,
155 }
156
157 let state = std::sync::Arc::new(std::sync::Mutex::new(State {
158 current: 0,
159 total: 0,
160 last_crate_action: "Compiling".to_string(),
161 last_crate: String::new(),
162 active: false,
163 }));
164
165 let _guard = CursorGuard;
167 print!("\x1B[?25l");
168 let _ = std::io::stdout().flush();
169
170 let draw_progress = |state: &State| {
172 if !state.active || state.total == 0 {
173 return;
174 }
175 let width = 30;
176 let completed = (state.current * width) / state.total;
177
178 let mut bar = String::new();
179 for i in 0..width {
180 if i < completed {
181 if i < 8 {
183 bar.push_str(&"█".magenta().to_string());
184 } else if i < 16 {
185 bar.push_str(&"█".cyan().to_string());
186 } else if i < 24 {
187 bar.push_str(&"█".yellow().to_string());
188 } else {
189 bar.push_str(&"█".green().to_string());
190 }
191 } else {
192 bar.push_str(&"░".dimmed().to_string());
193 }
194 }
195
196 let pct = (state.current * 100) / state.total;
197 let pct_colored = if pct < 33 {
198 format!("{:>3}%", pct).magenta().bold()
199 } else if pct < 66 {
200 format!("{:>3}%", pct).cyan().bold()
201 } else if pct < 90 {
202 format!("{:>3}%", pct).yellow().bold()
203 } else {
204 format!("{:>3}%", pct).green().bold()
205 };
206
207 let action_label = if state.last_crate_action.starts_with("Check") {
208 "Checking".yellow().bold()
209 } else if state.last_crate_action.starts_with("Doc") {
210 "Documenting".blue().bold()
211 } else {
212 "Compiling".magenta().bold()
213 };
214
215 let crate_desc = if state.last_crate.is_empty() {
216 "cargo...".white()
217 } else {
218 format!("{} {}", action_label, state.last_crate.white().bold().italic()).white()
219 };
220
221 let step_count = format!("({}/{})", state.current, state.total).cyan().dimmed();
222
223 print!(
224 "\r\x1B[2K ⚡ [{}] {} {} {}",
225 bar,
226 pct_colored,
227 step_count,
228 crate_desc
229 );
230 let _ = std::io::stdout().flush();
231 };
232
233 let state_clone_stdout = std::sync::Arc::clone(&state);
234 let stdout_thread = std::thread::spawn(move || {
235 let reader = std::io::BufReader::new(child_stdout);
236 for line_result in reader.lines() {
237 if let Ok(line) = line_result {
238 let s = state_clone_stdout.lock().unwrap();
239 print!("\r\x1B[2K");
241 println!("{}", line);
242 draw_progress(&s);
243 }
244 }
245 });
246
247 let state_clone_stderr = std::sync::Arc::clone(&state);
248 let stderr_thread = std::thread::spawn(move || {
249 let mut reader = std::io::BufReader::new(child_stderr);
250 let mut buffer = Vec::new();
251
252 loop {
253 let mut byte = [0u8; 1];
254 match reader.read_exact(&mut byte) {
255 Ok(_) => {
256 let b = byte[0];
257 if b == b'\n' || b == b'\r' {
258 if !buffer.is_empty() {
259 let line = String::from_utf8_lossy(&buffer).to_string();
260 buffer.clear();
261
262 let mut s = state_clone_stderr.lock().unwrap();
263 if let Some((current, total, _details)) = parse_cargo_progress(&line) {
264 s.current = current;
265 s.total = total;
266 s.active = true;
267 draw_progress(&s);
268 } else if let Some((action, crate_name)) = parse_compiling_crate(&line) {
269 s.last_crate_action = action;
270 s.last_crate = crate_name;
271 draw_progress(&s);
272 } else {
273 let trimmed = line.trim();
274 if trimmed.starts_with("Running ") || trimmed.starts_with("Doc-tests ") || trimmed.starts_with("Finished ") {
275 s.active = false;
276 print!("\r\x1B[2K");
277 let _ = std::io::stdout().flush();
278 }
279 if !trimmed.is_empty() && !trimmed.starts_with("Fresh ") && !trimmed.starts_with("Finished ") {
280 print!("\r\x1B[2K");
282 eprintln!("{}", line);
283 draw_progress(&s);
284 }
285 }
286 }
287 } else {
288 buffer.push(b);
289 }
290 }
291 Err(_) => {
292 break;
293 }
294 }
295 }
296 });
297
298 let status = child.wait()?;
299
300 let _ = stdout_thread.join();
302 let _ = stderr_thread.join();
303
304 print!("\r\x1B[2K");
306 let _ = std::io::stdout().flush();
307
308 Ok(status)
309}