update_kit/ux/
progress.rs1use crate::types::{ApplyProgress, ApplyResult, PostAction};
2use crate::ux::colors::{dim, green, red, yellow};
3
4pub 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
39pub 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}