Skip to main content

rustbasic_cli/
utils.rs

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        // Coba hubungkan ke port selama 60 detik (120 * 500ms)
44        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    // Force cargo term progress configuration
137    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    // Hide cursor using CursorGuard
166    let _guard = CursorGuard;
167    print!("\x1B[?25l");
168    let _ = std::io::stdout().flush();
169    
170    // Helper to draw progress bar
171    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                // Multicolored gradient blocks: Magenta -> Cyan -> Yellow -> Green
182                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                // Clear the progress bar, print stdout line, redraw progress bar
240                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                                    // It's a warning, error, or custom print. Clear bar, print it, redraw
281                                    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    // Wait for output threads to finish reading
301    let _ = stdout_thread.join();
302    let _ = stderr_thread.join();
303    
304    // Clear progress bar line at the end
305    print!("\r\x1B[2K");
306    let _ = std::io::stdout().flush();
307    
308    Ok(status)
309}