1use colored::*;
7use std::io::{self, Write};
8
9pub struct SimpleUI {
11 use_colors: bool,
12}
13
14impl SimpleUI {
15 pub fn new() -> Self {
17 let use_colors = Self::should_use_colors();
18 Self { use_colors }
19 }
20
21 fn should_use_colors() -> bool {
23 std::env::var("NO_COLOR").is_err() && std::env::var("TERM").unwrap_or_default() != "dumb"
25 }
26
27 pub fn success(&self, message: &str) {
29 if self.use_colors {
30 println!("{} {}", "[OK]".green(), message);
31 } else {
32 println!("[OK] {message}");
33 }
34 }
35
36 pub fn error(&self, message: &str) {
38 if self.use_colors {
39 println!("{} {}", "[ERROR]".red(), message);
40 } else {
41 println!("[ERROR] {message}");
42 }
43 }
44
45 pub fn warning(&self, message: &str) {
47 if self.use_colors {
48 println!("{} {}", "[WARN]".yellow(), message);
49 } else {
50 println!("[WARN] {message}");
51 }
52 }
53
54 pub fn info(&self, message: &str) {
56 if self.use_colors {
57 println!("{} {}", "[INFO]".blue(), message);
58 } else {
59 println!("[INFO] {message}");
60 }
61 }
62
63 pub fn hint(&self, message: &str) {
65 if self.use_colors {
66 println!("{} {}", "[TIP]".cyan(), message);
67 } else {
68 println!("[TIP] {message}");
69 }
70 }
71
72 pub fn title(&self, text: &str) {
74 println!();
75 println!("{}", "=".repeat(60));
76 println!("{text}");
77 println!("{}", "=".repeat(60));
78 println!();
79 }
80
81 pub fn section(&self, text: &str) {
83 println!();
84 if self.use_colors {
85 println!("{}", format!("> {text}").cyan());
86 } else {
87 println!("> {text}");
88 }
89 println!("{}", "-".repeat(text.len() + 2));
90 }
91
92 pub fn list_item(&self, text: &str, is_current: bool) {
94 if is_current {
95 if self.use_colors {
96 println!(" {} {}", format!("* {text}").green(), "(active)".dimmed());
97 } else {
98 println!(" * {text} (active)");
99 }
100 } else {
101 println!(" - {text}");
102 }
103 }
104
105 pub fn key_value(&self, key: &str, value: &str) {
107 if self.use_colors {
108 println!(" {}: {}", key.dimmed(), value);
109 } else {
110 println!(" {key}: {value}");
111 }
112 }
113
114 pub fn key_value_colored(&self, key: &str, value: &str, color: &str) {
116 if self.use_colors {
117 let colored_value = match color {
118 "green" => value.green().to_string(),
119 "red" => value.red().to_string(),
120 "yellow" => value.yellow().to_string(),
121 "blue" => value.blue().to_string(),
122 "cyan" => value.cyan().to_string(),
123 "dim" => value.dimmed().to_string(),
124 _ => value.to_string(),
125 };
126 println!(" {}: {}", key.dimmed(), colored_value);
127 } else {
128 println!(" {key}: {value}");
129 }
130 }
131
132 pub fn progress(&self, current: usize, total: usize, description: &str) {
134 if self.use_colors {
135 println!(
136 "[{}/{}] {}",
137 current.to_string().cyan(),
138 total.to_string().cyan(),
139 description
140 );
141 } else {
142 println!("[{current}/{total}] {description}");
143 }
144 }
145
146 pub fn status(&self, message: &str) {
148 if self.use_colors {
149 println!("{}", message.dimmed());
150 } else {
151 println!("{message}");
152 }
153 }
154
155 pub fn separator(&self) {
157 println!("{}", "-".repeat(50));
158 }
159
160 pub fn newline(&self) {
162 println!();
163 }
164
165 pub fn suggest(&self, message: &str) {
167 if self.use_colors {
168 println!("{} Suggestion: {}", "->".cyan(), message);
169 } else {
170 println!("-> Suggestion: {message}");
171 }
172 }
173}
174
175impl Default for SimpleUI {
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181pub struct SimpleProgressBar {
183 label: String,
184 use_colors: bool,
185}
186
187impl SimpleProgressBar {
188 pub fn new(label: String) -> Self {
189 let use_colors = SimpleUI::should_use_colors();
190 Self { label, use_colors }
191 }
192
193 pub fn update(&self, percent: f64, message: Option<&str>) {
195 let bar_width = 30;
196 let filled = (percent * bar_width as f64) as usize;
197 let empty = bar_width - filled;
198
199 let bar = if self.use_colors {
200 format!("{}{}", "=".repeat(filled).green(), " ".repeat(empty))
201 } else {
202 format!("{}{}", "=".repeat(filled), " ".repeat(empty))
203 };
204
205 let display_message = message.unwrap_or("");
206
207 if self.use_colors {
208 print!(
209 "\r{} [{}] {} {}",
210 self.label.cyan(),
211 bar,
212 format!("{:.1}%", percent * 100.0).bold(),
213 display_message
214 );
215 } else {
216 print!("\r{} [{}] {:.1}% {}", self.label, bar, percent * 100.0, display_message);
217 }
218
219 io::stdout().flush().ok();
220 }
221
222 pub fn finish(&self, message: &str) {
224 println!();
225 if self.use_colors {
226 println!("{} {}", "[OK]".green(), message);
227 } else {
228 println!("[OK] {message}");
229 }
230 }
231
232 pub fn fail(&self, message: &str) {
234 println!();
235 if self.use_colors {
236 println!("{} {}", "[ERROR]".red(), message);
237 } else {
238 println!("[ERROR] {message}");
239 }
240 }
241}
242
243pub fn format_size(bytes: u64) -> String {
245 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
246 let mut size = bytes as f64;
247 let mut unit_index = 0;
248
249 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
250 size /= 1024.0;
251 unit_index += 1;
252 }
253
254 if unit_index == 0 {
255 format!("{} {}", bytes, UNITS[unit_index])
256 } else {
257 format!("{:.1} {}", size, UNITS[unit_index])
258 }
259}
260
261pub fn format_duration(seconds: u64) -> String {
263 if seconds < 60 {
264 format!("{seconds}s")
265 } else if seconds < 3600 {
266 let minutes = seconds / 60;
267 let secs = seconds % 60;
268 format!("{minutes}m{secs}s")
269 } else {
270 let hours = seconds / 3600;
271 let minutes = (seconds % 3600) / 60;
272 format!("{hours}h{minutes}m")
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn test_format_size() {
282 assert_eq!(format_size(1024), "1.0 KB");
283 assert_eq!(format_size(1048576), "1.0 MB");
284 assert_eq!(format_size(500), "500 B");
285 }
286
287 #[test]
288 fn test_format_duration() {
289 assert_eq!(format_duration(30), "30s");
290 assert_eq!(format_duration(90), "1m30s");
291 assert_eq!(format_duration(3661), "1h1m");
292 }
293}