Skip to main content

pkg/callback/
plain.rs

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