fob_cli/ui/
format.rs

1//! Formatting utilities for sizes, durations, and build summaries.
2
3use console::Term;
4use owo_colors::OwoColorize;
5use std::time::Duration;
6
7/// Format file size in human-readable format.
8///
9/// Converts bytes to the most appropriate unit (B, KB, MB, GB).
10///
11/// # Arguments
12///
13/// * `bytes` - Size in bytes
14///
15/// # Returns
16///
17/// Formatted string (e.g., "1.50 MB")
18///
19/// # Examples
20///
21/// ```
22/// use fob_cli::ui::format_size;
23///
24/// assert_eq!(format_size(0), "0 B");
25/// assert_eq!(format_size(500), "500 B");
26/// assert_eq!(format_size(1024), "1.00 KB");
27/// assert_eq!(format_size(1_048_576), "1.00 MB");
28/// ```
29pub fn format_size(bytes: u64) -> String {
30    const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
31
32    if bytes == 0 {
33        return "0 B".to_string();
34    }
35
36    let mut size = bytes as f64;
37    let mut unit_idx = 0;
38
39    while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
40        size /= 1024.0;
41        unit_idx += 1;
42    }
43
44    if unit_idx == 0 {
45        format!("{} {}", size as u64, UNITS[unit_idx])
46    } else {
47        format!("{:.2} {}", size, UNITS[unit_idx])
48    }
49}
50
51/// Format duration in human-readable format.
52///
53/// Converts to the most appropriate unit (ms, s, m:s).
54///
55/// # Arguments
56///
57/// * `duration` - Duration to format
58///
59/// # Returns
60///
61/// Formatted string (e.g., "1.50s", "2m 30s")
62///
63/// # Examples
64///
65/// ```
66/// use std::time::Duration;
67/// use fob_cli::ui::format_duration;
68///
69/// assert_eq!(format_duration(Duration::from_millis(50)), "50ms");
70/// assert_eq!(format_duration(Duration::from_millis(1500)), "1.50s");
71/// assert_eq!(format_duration(Duration::from_secs(90)), "1m 30s");
72/// ```
73pub fn format_duration(duration: Duration) -> String {
74    let total_ms = duration.as_millis();
75
76    if total_ms < 1000 {
77        format!("{}ms", total_ms)
78    } else if total_ms < 60_000 {
79        format!("{:.2}s", duration.as_secs_f64())
80    } else {
81        let secs = duration.as_secs();
82        let mins = secs / 60;
83        let secs = secs % 60;
84        format!("{}m {}s", mins, secs)
85    }
86}
87
88/// Print a build summary table to stderr.
89///
90/// Displays a formatted table of build outputs with sizes and durations,
91/// plus a total summary.
92///
93/// # Arguments
94///
95/// * `entries` - Slice of (name, size_bytes, duration) tuples
96///
97/// # Examples
98///
99/// ```no_run
100/// use std::time::Duration;
101/// use fob_cli::ui::print_build_summary;
102///
103/// print_build_summary(&[
104///     ("index.js".to_string(), 15_234, Duration::from_millis(450)),
105///     ("vendor.js".to_string(), 234_567, Duration::from_millis(1200)),
106/// ]);
107/// ```
108pub fn print_build_summary(entries: &[(String, u64, Duration)]) {
109    let term = Term::stderr();
110    let width = term.size().1 as usize;
111
112    // Header
113    eprintln!("\n{}", "Build Summary".bold().underline());
114    eprintln!("{}", "─".repeat(width.min(80)));
115
116    // Table entries
117    for (name, size, duration) in entries {
118        let size_str = format_size(*size);
119        let dur_str = format_duration(*duration);
120
121        eprintln!(
122            "  {} {} {} {}",
123            "▸".blue(),
124            name.bright_white().bold(),
125            size_str.dimmed(),
126            format!("({})", dur_str).dimmed()
127        );
128    }
129
130    // Footer
131    eprintln!("{}", "─".repeat(width.min(80)));
132
133    let total_size: u64 = entries.iter().map(|(_, s, _)| s).sum();
134    let total_time: Duration = entries.iter().map(|(_, _, d)| d).sum();
135
136    eprintln!(
137        "  {} {} in {}",
138        "Total:".bold(),
139        format_size(total_size).green(),
140        format_duration(total_time).green()
141    );
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_format_size_zero() {
150        assert_eq!(format_size(0), "0 B");
151    }
152
153    #[test]
154    fn test_format_size_bytes() {
155        assert_eq!(format_size(1), "1 B");
156        assert_eq!(format_size(500), "500 B");
157        assert_eq!(format_size(1023), "1023 B");
158    }
159
160    #[test]
161    fn test_format_size_kilobytes() {
162        assert_eq!(format_size(1024), "1.00 KB");
163        assert_eq!(format_size(1536), "1.50 KB");
164        assert_eq!(format_size(10_240), "10.00 KB");
165    }
166
167    #[test]
168    fn test_format_size_megabytes() {
169        assert_eq!(format_size(1_048_576), "1.00 MB");
170        assert_eq!(format_size(1_572_864), "1.50 MB");
171        assert_eq!(format_size(10_485_760), "10.00 MB");
172    }
173
174    #[test]
175    fn test_format_size_gigabytes() {
176        assert_eq!(format_size(1_073_741_824), "1.00 GB");
177        assert_eq!(format_size(2_147_483_648), "2.00 GB");
178    }
179
180    #[test]
181    fn test_format_duration_milliseconds() {
182        assert_eq!(format_duration(Duration::from_millis(0)), "0ms");
183        assert_eq!(format_duration(Duration::from_millis(50)), "50ms");
184        assert_eq!(format_duration(Duration::from_millis(999)), "999ms");
185    }
186
187    #[test]
188    fn test_format_duration_seconds() {
189        assert_eq!(format_duration(Duration::from_millis(1000)), "1.00s");
190        assert_eq!(format_duration(Duration::from_millis(1500)), "1.50s");
191        assert_eq!(format_duration(Duration::from_millis(59_999)), "60.00s");
192    }
193
194    #[test]
195    fn test_format_duration_minutes() {
196        assert_eq!(format_duration(Duration::from_secs(60)), "1m 0s");
197        assert_eq!(format_duration(Duration::from_secs(90)), "1m 30s");
198        assert_eq!(format_duration(Duration::from_secs(125)), "2m 5s");
199        assert_eq!(format_duration(Duration::from_secs(3661)), "61m 1s");
200    }
201
202    #[test]
203    fn test_print_build_summary() {
204        let entries = vec![
205            ("index.js".to_string(), 15_234, Duration::from_millis(450)),
206            (
207                "vendor.js".to_string(),
208                234_567,
209                Duration::from_millis(1200),
210            ),
211        ];
212
213        // Should not panic
214        print_build_summary(&entries);
215    }
216
217    #[test]
218    fn test_print_build_summary_empty() {
219        // Should handle empty input gracefully
220        print_build_summary(&[]);
221    }
222}