Skip to main content

tidepool_gvm/
ui_flat.rs

1//! Simplified UI system
2//!
3//! Provides a clean, cross-platform compatible user interface,
4//! using the colored crate for cross-platform color support.
5
6use colored::*;
7use std::io::{self, Write};
8
9/// Simplified UI manager
10pub struct SimpleUI {
11    use_colors: bool,
12}
13
14impl SimpleUI {
15    /// Creates a new UI instance
16    pub fn new() -> Self {
17        let use_colors = Self::should_use_colors();
18        Self { use_colors }
19    }
20
21    /// Detects if colors should be used
22    fn should_use_colors() -> bool {
23        // colored crate handles this automatically, but we can add custom logic
24        std::env::var("NO_COLOR").is_err() && std::env::var("TERM").unwrap_or_default() != "dumb"
25    }
26
27    /// Displays a success message
28    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    /// Displays an error message
37    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    /// Displays a warning message
46    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    /// Displays an informational message
55    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    /// Displays a hint message
64    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    /// Displays a title
73    pub fn title(&self, text: &str) {
74        println!();
75        println!("{}", "=".repeat(60));
76        println!("{text}");
77        println!("{}", "=".repeat(60));
78        println!();
79    }
80
81    /// Displays a section header
82    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    /// Displays a list item
93    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    /// Displays a key-value pair
106    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    /// Displays a colored key-value pair
115    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    /// Displays progress information
133    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    /// Displays a concise status message
147    pub fn status(&self, message: &str) {
148        if self.use_colors {
149            println!("{}", message.dimmed());
150        } else {
151            println!("{message}");
152        }
153    }
154
155    /// Displays a separator line
156    pub fn separator(&self) {
157        println!("{}", "-".repeat(50));
158    }
159
160    /// Displays a newline
161    pub fn newline(&self) {
162        println!();
163    }
164
165    /// Displays a suggestion
166    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
181/// A simple progress bar
182pub 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    /// Updates the progress
194    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    /// Finishes the progress
223    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    /// Fails the progress
233    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
243/// Formats a file size in a human-readable format
244pub 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
261/// Formats a duration in a human-readable format
262pub 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}