Skip to main content

rustbasic_cli/
utils.rs

1use rustbasic_core::colored::*;
2use std::io::{BufRead, Read, Write};
3
4/// Menampilkan prompt interaktif dan meminta user memilih angka dari min..=max
5pub 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        // Coba hubungkan ke port selama 60 detik (120 * 500ms)
59        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    // Force cargo term progress configuration
152    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    // Hide cursor using CursorGuard
181    let _guard = CursorGuard;
182    print!("\x1B[?25l");
183    let _ = std::io::stdout().flush();
184    
185    // Helper to draw progress bar
186    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                // Multicolored gradient blocks: Magenta -> Cyan -> Yellow -> Green
197                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            // Clear the progress bar, print stdout line, redraw progress bar
254            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                                    // It's a warning, error, or custom print. Clear bar, print it, redraw
294                                    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    // Wait for output threads to finish reading
314    let _ = stdout_thread.join();
315    let _ = stderr_thread.join();
316    
317    // Clear progress bar line at the end
318    print!("\r\x1B[2K");
319    let _ = std::io::stdout().flush();
320    
321    Ok(status)
322}