1use std::{io::Write, time::Instant};
2
3#[cfg(feature = "library")]
4use crate::backend::Error;
5use crate::{callback::Callback, package::RemotePackage};
6
7#[derive(Clone)]
8pub struct PlainCallback {
9 size: u64,
10 unknown_size: bool,
11 pos: u64,
12 fetch_processed: usize,
13 fetch_total: usize,
14 interactive: bool,
15 download_file: Option<String>,
16 last_updated: Instant,
17}
18
19impl PlainCallback {
20 pub fn new() -> Self {
21 Self {
22 size: 0,
23 unknown_size: false,
24 pos: 0,
25 fetch_processed: 0,
26 fetch_total: 0,
27 interactive: false,
28 download_file: None,
29 last_updated: Instant::now(),
30 }
31 }
32
33 pub fn set_interactive(&mut self, enabled: bool) {
35 self.interactive = enabled;
36 }
37
38 fn flush(&self) {
39 let _ = std::io::stderr().flush();
40 }
41
42 pub fn format_size(bytes: u64) -> String {
43 if bytes == 0 {
44 return "0 B".to_string();
45 }
46 const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
47 let i = (bytes as f64).log(1024.0).floor() as usize;
48 let size = bytes as f64 / 1024.0_f64.powi(i as i32);
49 format!("{:.2} {}", size, UNITS[i])
50 }
51
52 fn should_update_progress<F>(&mut self, do_print: F, force: bool)
53 where
54 F: Fn(&PlainCallback),
55 {
56 let now = Instant::now();
57 if force || now.duration_since(self.last_updated).as_millis() >= 50 {
59 self.last_updated = now;
60 do_print(&self);
61 }
62 }
63
64 #[cfg(feature = "library")]
65 fn confirm_transaction(&self) -> Result<(), Error> {
66 if self.interactive {
67 eprint!("\nProceed with this transaction? [Y/n]: ");
68 self.flush();
69
70 let mut input = String::new();
71 std::io::stdin().read_line(&mut input).unwrap_or(0);
72 let input = input.trim().to_lowercase();
73
74 if input == "n" || input == "no" {
75 return Err(std::io::Error::new(
76 std::io::ErrorKind::Interrupted,
77 "Installation aborted by user",
78 )
79 .into());
80 }
81 } else {
82 eprintln!();
83 }
84
85 Ok(())
86 }
87
88 fn downloading_str(&self) -> &'static str {
89 "Downloading"
90 }
91}
92
93const RESET_LINE: &str = "\r\x1b[2K";
94
95impl Callback for PlainCallback {
96 fn fetch_start(&mut self, initial_count: usize) {
97 self.fetch_total = 0;
98 self.fetch_processed = 0;
99 self.fetch_package_increment(0, initial_count);
100 }
101
102 fn fetch_package_name(&mut self, pkg_name: &crate::PackageName) {
103 eprint!(" {}", pkg_name.as_str());
105 self.flush();
106 }
107
108 fn fetch_package_increment(&mut self, added_processed: usize, added_count: usize) {
109 self.fetch_processed += added_processed;
110 self.fetch_total += added_count;
111
112 self.should_update_progress(
113 |this| {
114 eprint!(
115 "{RESET_LINE}Fetching: [{}/{}]",
116 this.fetch_processed, this.fetch_total
117 );
118 this.flush();
119 },
120 self.fetch_processed == self.fetch_total,
121 );
122 }
123
124 fn fetch_end(&mut self) {
125 if self.fetch_processed == self.fetch_total {
126 eprintln!("{RESET_LINE}Fetch complete.");
127 } else {
128 eprintln!("{RESET_LINE}Fetch incomplete.");
129 }
130 }
131
132 #[cfg(feature = "library")]
133 fn install_prompt(&mut self, list: &crate::PackageList) -> Result<(), Error> {
134 eprintln!("");
135 if !list.install.is_empty() {
136 eprintln!("Packages to install:");
137 for pkg in &list.install {
138 eprintln!(" + {}", pkg);
139 }
140 }
141
142 if !list.update.is_empty() {
143 eprintln!("Packages to update:");
144 for pkg in &list.update {
145 eprintln!(" ~ {}", pkg);
146 }
147 }
148
149 if !list.uninstall.is_empty() {
150 eprintln!("Packages to uninstall:");
151 for pkg in &list.uninstall {
152 eprintln!(" - {}", pkg);
153 }
154 }
155
156 eprintln!();
157 if list.network_size > 0 {
158 eprintln!(" Download size: {}", Self::format_size(list.network_size));
159 }
160 if list.install_size > 0 {
161 eprintln!(" Install size: {}", Self::format_size(list.install_size));
162 }
163 if list.uninstall_size > 0 {
164 eprintln!(
165 " Uninstall size: {}",
166 Self::format_size(list.uninstall_size)
167 );
168 }
169
170 self.confirm_transaction()
171 }
172
173 #[cfg(feature = "library")]
174 fn install_check_conflict(
175 &mut self,
176 list: &Vec<pkgar::TransactionConflict>,
177 ) -> Result<(), Error> {
178 if list.is_empty() {
179 return Ok(());
180 }
181
182 eprintln!("Transaction conflict detected:");
183 for pkg in list {
184 eprintln!(
185 " -> {} (from {:?} replaced by {:?})",
186 pkg.conflicted_path.display(),
187 pkg.former_src.as_ref().map(|p| p.as_str()).unwrap_or("?"),
188 pkg.newer_src.as_ref().map(|p| p.as_str()).unwrap_or("?"),
189 );
190 }
191
192 self.confirm_transaction()
193 }
194
195 fn install_extract(&mut self, remote_pkg: &RemotePackage) {
196 eprintln!("Extracting {}...", remote_pkg.package.name);
197 self.flush();
198 }
199
200 fn download_start(&mut self, length: u64, file: &str) {
201 self.size = length;
202 self.unknown_size = length == 0;
203 self.pos = 0;
204 if !self.unknown_size {
205 eprint!("{RESET_LINE}{} {file}", self.downloading_str());
206 self.download_file = Some(file.to_string());
207 self.flush();
208 }
209 }
210
211 fn download_increment(&mut self, downloaded: u64) {
212 self.pos += downloaded;
213 if self.unknown_size {
214 self.size += downloaded;
215 }
216 if self.unknown_size {
217 return;
218 }
219
220 self.should_update_progress(
221 |this| {
222 let pos_mb = this.pos as f64 / 1_048_576.0;
224 let size_mb = this.size as f64 / 1_048_576.0;
225 let file_name = this
226 .download_file
227 .as_ref()
228 .map(|s| s.as_str())
229 .unwrap_or("");
230
231 eprint!(
232 "{RESET_LINE}{} {} [{:.2} MB / {:.2} MB]",
233 this.downloading_str(),
234 file_name,
235 pos_mb,
236 size_mb
237 );
238 this.flush();
239 },
240 self.pos == self.size,
241 );
242 }
243
244 fn download_end(&mut self) {
245 if !self.unknown_size {
246 eprintln!("");
247 self.download_file = None;
248 }
249 }
250
251 #[cfg(feature = "library")]
252 fn commit_start(&mut self, count: usize) {
253 eprintln!("Committing changes...");
254 self.size = count as u64;
255 self.unknown_size = false;
256 self.pos = 0;
257 self.flush();
258 }
259
260 #[cfg(feature = "library")]
261 fn commit_increment(&mut self, _file: &pkgar::Transaction) {
262 self.pos += 1;
263 if self.unknown_size {
264 self.size += 1;
265 }
266
267 self.should_update_progress(
268 |this| {
269 eprint!("{RESET_LINE}Committing: [{}/{}]", this.pos, this.size);
270 this.flush();
271 },
272 self.pos == self.size,
273 );
274 }
275
276 #[cfg(feature = "library")]
277 fn commit_end(&mut self) {
278 eprintln!("\nCommit done.");
279 }
280
281 #[cfg(feature = "library")]
282 fn abort_start(&mut self, count: usize) {
283 eprintln!("Aborting transaction...");
284 self.size = count as u64;
285 self.unknown_size = false;
286 self.pos = 0;
287 self.flush();
288 }
289
290 #[cfg(feature = "library")]
291 fn abort_increment(&mut self, _file: &pkgar::Transaction) {
292 self.pos += 1;
293 if self.unknown_size {
294 self.size += 1;
295 }
296
297 self.should_update_progress(
298 |this| {
299 eprint!("{RESET_LINE}Aborting: [{}/{}]", this.pos, this.size);
300 this.flush();
301 },
302 self.pos == self.size,
303 );
304 }
305
306 #[cfg(feature = "library")]
307 fn abort_end(&mut self) {
308 eprintln!("\nAbort done.");
309 }
310}