Skip to main content

update_kit/ux/
progress.rs

1use crate::types::{ApplyProgress, ApplyResult, PostAction};
2use crate::ux::colors::{dim, green, red, yellow};
3
4/// Render a human-readable string for an apply progress update.
5pub fn render_progress(progress: &ApplyProgress) -> String {
6    match progress {
7        ApplyProgress::Downloading {
8            bytes_downloaded,
9            total_bytes,
10        } => {
11            let downloaded = format_bytes(*bytes_downloaded);
12            match total_bytes {
13                Some(total) => {
14                    let total_str = format_bytes(*total);
15                    let pct = if *total > 0 {
16                        (*bytes_downloaded as f64 / *total as f64 * 100.0) as u64
17                    } else {
18                        0
19                    };
20                    dim(&format!("Downloading... {} / {} ({}%)", downloaded, total_str, pct))
21                }
22                None => dim(&format!("Downloading... {}", downloaded)),
23            }
24        }
25        ApplyProgress::Verifying => yellow("Verifying checksum..."),
26        ApplyProgress::Extracting => yellow("Extracting archive..."),
27        ApplyProgress::Replacing => yellow("Replacing binary..."),
28        ApplyProgress::Executing { output, stream } => {
29            let prefix = match stream {
30                crate::types::OutputStream::Stdout => "stdout",
31                crate::types::OutputStream::Stderr => "stderr",
32            };
33            dim(&format!("[{}] {}", prefix, output))
34        }
35        ApplyProgress::Done => green("Done!"),
36    }
37}
38
39/// Render a human-readable string for an apply result.
40pub fn render_result(result: &ApplyResult) -> String {
41    match result {
42        ApplyResult::Success {
43            from_version,
44            to_version,
45            post_action,
46        } => {
47            let action_hint = match post_action {
48                PostAction::SuggestRestart => " Please restart the application.",
49                PostAction::ExitAfterApply => " The application will exit.",
50                PostAction::Reexec => " Re-executing...",
51                PostAction::None => "",
52            };
53            green(&format!(
54                "Successfully updated from {} to {}.{}",
55                from_version, to_version, action_hint
56            ))
57        }
58        ApplyResult::UpToDate { current } => {
59            green(&format!("Already up to date (v{}).", current))
60        }
61        ApplyResult::NeedsRestart { message } => {
62            yellow(&format!("Update applied. {}", message))
63        }
64        ApplyResult::Failed {
65            error,
66            rollback_succeeded,
67        } => {
68            let rollback_msg = if *rollback_succeeded {
69                " Rollback succeeded."
70            } else {
71                " Rollback failed."
72            };
73            red(&format!("Update failed: {}{}", error, rollback_msg))
74        }
75    }
76}
77
78fn format_bytes(bytes: u64) -> String {
79    if bytes < 1024 {
80        format!("{} B", bytes)
81    } else if bytes < 1024 * 1024 {
82        format!("{:.1} KB", bytes as f64 / 1024.0)
83    } else {
84        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::ux::colors::strip_ansi;
92
93    #[test]
94    fn render_progress_downloading_with_total() {
95        let progress = ApplyProgress::Downloading {
96            bytes_downloaded: 512_000,
97            total_bytes: Some(1_024_000),
98        };
99        let text = render_progress(&progress);
100        assert!(!text.is_empty());
101        let plain = strip_ansi(&text);
102        assert!(plain.contains("Downloading"));
103        assert!(plain.contains("50%"));
104    }
105
106    #[test]
107    fn render_progress_downloading_without_total() {
108        let progress = ApplyProgress::Downloading {
109            bytes_downloaded: 1024,
110            total_bytes: None,
111        };
112        let text = render_progress(&progress);
113        assert!(!text.is_empty());
114        let plain = strip_ansi(&text);
115        assert!(plain.contains("Downloading"));
116    }
117
118    #[test]
119    fn render_progress_verifying() {
120        let text = render_progress(&ApplyProgress::Verifying);
121        assert!(!text.is_empty());
122        let plain = strip_ansi(&text);
123        assert!(plain.contains("Verifying"));
124    }
125
126    #[test]
127    fn render_progress_extracting() {
128        let text = render_progress(&ApplyProgress::Extracting);
129        assert!(!text.is_empty());
130        let plain = strip_ansi(&text);
131        assert!(plain.contains("Extracting"));
132    }
133
134    #[test]
135    fn render_progress_replacing() {
136        let text = render_progress(&ApplyProgress::Replacing);
137        assert!(!text.is_empty());
138        let plain = strip_ansi(&text);
139        assert!(plain.contains("Replacing"));
140    }
141
142    #[test]
143    fn render_progress_executing() {
144        let progress = ApplyProgress::Executing {
145            output: "installing...".into(),
146            stream: crate::types::OutputStream::Stdout,
147        };
148        let text = render_progress(&progress);
149        assert!(!text.is_empty());
150        let plain = strip_ansi(&text);
151        assert!(plain.contains("stdout"));
152        assert!(plain.contains("installing"));
153    }
154
155    #[test]
156    fn render_progress_done() {
157        let text = render_progress(&ApplyProgress::Done);
158        assert!(!text.is_empty());
159        let plain = strip_ansi(&text);
160        assert!(plain.contains("Done"));
161    }
162
163    #[test]
164    fn render_result_success() {
165        let result = ApplyResult::Success {
166            from_version: "1.0.0".into(),
167            to_version: "2.0.0".into(),
168            post_action: PostAction::SuggestRestart,
169        };
170        let text = render_result(&result);
171        let plain = strip_ansi(&text);
172        assert!(plain.contains("Successfully updated"));
173        assert!(plain.contains("1.0.0"));
174        assert!(plain.contains("2.0.0"));
175        assert!(plain.contains("restart"));
176    }
177
178    #[test]
179    fn render_result_up_to_date() {
180        let result = ApplyResult::UpToDate {
181            current: "1.0.0".into(),
182        };
183        let text = render_result(&result);
184        let plain = strip_ansi(&text);
185        assert!(plain.contains("up to date"));
186    }
187
188    #[test]
189    fn render_result_failed() {
190        let result = ApplyResult::Failed {
191            error: Box::new(crate::errors::UpdateKitError::DownloadFailed(
192                "timeout".into(),
193            )),
194            rollback_succeeded: true,
195        };
196        let text = render_result(&result);
197        let plain = strip_ansi(&text);
198        assert!(plain.contains("failed"));
199        assert!(plain.contains("Rollback succeeded"));
200    }
201}