Skip to main content

pkg/callback/
plain.rs

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    /// Set if user require to agree on terminal
34    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        // 20 FPS, same default with indicatif
58        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        // resuming after fetch_package_increment
104        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                // keep using MB for consistency
223                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}