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