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
143 let mut child = cmd.spawn()?;
144
145 let child_stdout = child.stdout.take().unwrap();
146 let child_stderr = child.stderr.take().unwrap();
147
148 struct State {
149 current: usize,
150 total: usize,
151 last_crate_action: String,
152 last_crate: String,
153 active: bool,
154 }
155
156 let state = std::sync::Arc::new(std::sync::Mutex::new(State {
157 current: 0,
158 total: 0,
159 last_crate_action: "Compiling".to_string(),
160 last_crate: String::new(),
161 active: false,
162 }));
163
164 let _guard = CursorGuard;
166 print!("\x1B[?25l");
167 let _ = std::io::stdout().flush();
168
169 let draw_progress = |state: &State| {
171 if !state.active || state.total == 0 {
172 return;
173 }
174 let width = 30;
175 let completed = (state.current * width) / state.total;
176
177 let mut bar = String::new();
178 for i in 0..width {
179 if i < completed {
180 if i < 8 {
182 bar.push_str(&"█".magenta().to_string());
183 } else if i < 16 {
184 bar.push_str(&"█".cyan().to_string());
185 } else if i < 24 {
186 bar.push_str(&"█".yellow().to_string());
187 } else {
188 bar.push_str(&"█".green().to_string());
189 }
190 } else {
191 bar.push_str(&"░".dimmed().to_string());
192 }
193 }
194
195 let pct = (state.current * 100) / state.total;
196 let pct_colored = if pct < 33 {
197 format!("{:>3}%", pct).magenta().bold()
198 } else if pct < 66 {
199 format!("{:>3}%", pct).cyan().bold()
200 } else if pct < 90 {
201 format!("{:>3}%", pct).yellow().bold()
202 } else {
203 format!("{:>3}%", pct).green().bold()
204 };
205
206 let action_label = if state.last_crate_action.starts_with("Check") {
207 "Checking".yellow().bold()
208 } else if state.last_crate_action.starts_with("Doc") {
209 "Documenting".blue().bold()
210 } else {
211 "Compiling".magenta().bold()
212 };
213
214 let crate_desc = if state.last_crate.is_empty() {
215 "cargo...".white()
216 } else {
217 format!("{} {}", action_label, state.last_crate.white().bold().italic()).white()
218 };
219
220 let step_count = format!("({}/{})", state.current, state.total).cyan().dimmed();
221
222 print!(
223 "\r\x1B[2K ⚡ [{}] {} {} {}",
224 bar,
225 pct_colored,
226 step_count,
227 crate_desc
228 );
229 let _ = std::io::stdout().flush();
230 };
231
232 let state_clone_stdout = std::sync::Arc::clone(&state);
233 let stdout_thread = std::thread::spawn(move || {
234 let reader = std::io::BufReader::new(child_stdout);
235 for line_result in reader.lines() {
236 if let Ok(line) = line_result {
237 let s = state_clone_stdout.lock().unwrap();
238 print!("\r\x1B[2K");
240 println!("{}", line);
241 draw_progress(&s);
242 }
243 }
244 });
245
246 let state_clone_stderr = std::sync::Arc::clone(&state);
247 let stderr_thread = std::thread::spawn(move || {
248 let mut reader = std::io::BufReader::new(child_stderr);
249 let mut buffer = Vec::new();
250
251 loop {
252 let mut byte = [0u8; 1];
253 match reader.read_exact(&mut byte) {
254 Ok(_) => {
255 let b = byte[0];
256 if b == b'\n' || b == b'\r' {
257 if !buffer.is_empty() {
258 let line = String::from_utf8_lossy(&buffer).to_string();
259 buffer.clear();
260
261 let mut s = state_clone_stderr.lock().unwrap();
262 if let Some((current, total, _details)) = parse_cargo_progress(&line) {
263 s.current = current;
264 s.total = total;
265 s.active = true;
266 draw_progress(&s);
267 } else if let Some((action, crate_name)) = parse_compiling_crate(&line) {
268 s.last_crate_action = action;
269 s.last_crate = crate_name;
270 draw_progress(&s);
271 } else {
272 let trimmed = line.trim();
273 if trimmed.starts_with("Running ") || trimmed.starts_with("Doc-tests ") || trimmed.starts_with("Finished ") {
274 s.active = false;
275 print!("\r\x1B[2K");
276 let _ = std::io::stdout().flush();
277 }
278 if !trimmed.is_empty() && !trimmed.starts_with("Fresh ") && !trimmed.starts_with("Finished ") {
279 print!("\r\x1B[2K");
281 eprintln!("{}", line);
282 draw_progress(&s);
283 }
284 }
285 }
286 } else {
287 buffer.push(b);
288 }
289 }
290 Err(_) => {
291 break;
292 }
293 }
294 }
295 });
296
297 let status = child.wait()?;
298
299 let _ = stdout_thread.join();
301 let _ = stderr_thread.join();
302
303 print!("\r\x1B[2K");
305 let _ = std::io::stdout().flush();
306
307 Ok(status)
308}