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 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 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 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}