1use std::fmt::Write as _;
3use std::io::{Write, stdout};
4use std::os::fd::RawFd;
5use std::pin::Pin;
6
7use cxx::{ExternType, UniquePtr};
8#[doc(inline)]
9pub use raw::{ReleaseInfoChange, ReleaseInfoChanges};
10
11use crate::config::Config;
12use crate::error::raw::pending_error;
13use crate::raw::{AcqTextStatus, ItemDesc, ItemState, PkgAcquire, acquire_status};
14use crate::util::{
15 NumSys, get_apt_progress_string, terminal_height, terminal_width, time_str, unit_str,
16};
17
18fn progress_fraction(current: u64, total: u64) -> f64 {
19 if total == 0 {
20 return 0.0;
21 }
22
23 (current as f64 / total as f64).clamp(0.0, 1.0)
24}
25
26fn terminal_content_width() -> usize { terminal_width().saturating_sub(1) }
27
28pub trait DynAcquireProgress {
30 fn pulse_interval(&self) -> usize;
32
33 fn release_info_changes(&mut self, info: ReleaseInfoChanges) -> bool {
39 info.changes.iter().all(|change| change.default_action)
40 }
41
42 fn hit(&mut self, item: &ItemDesc);
44
45 fn fetch(&mut self, item: &ItemDesc);
47
48 fn fail(&mut self, item: &ItemDesc);
50
51 fn pulse(&mut self, status: &AcqTextStatus, owner: &PkgAcquire);
53
54 fn done(&mut self, item: &ItemDesc);
56
57 fn start(&mut self);
59
60 fn stop(&mut self, status: &AcqTextStatus);
62}
63
64pub trait DynOperationProgress {
66 fn update(&mut self, operation: String, percent: f32);
67 fn done(&mut self);
68}
69
70pub trait DynInstallProgress {
72 fn status_changed(
73 &mut self,
74 pkgname: String,
75 steps_done: u64,
76 total_steps: u64,
77 action: String,
78 );
79 fn error(&mut self, pkgname: String, steps_done: u64, total_steps: u64, error: String);
80}
81
82pub struct AcquireProgress<'a> {
90 status: UniquePtr<AcqTextStatus>,
91 inner: Box<dyn DynAcquireProgress + 'a>,
92}
93
94impl<'a> AcquireProgress<'a> {
95 pub fn new(inner: impl DynAcquireProgress + 'a) -> Self {
98 Self {
99 status: unsafe { acquire_status() },
100 inner: Box::new(inner),
101 }
102 }
103
104 pub fn apt() -> Self { Self::new(AptAcquireProgress::new()) }
107
108 pub fn quiet() -> Self { Self::new(AptAcquireProgress::disable()) }
110
111 pub fn mut_status(&mut self) -> Pin<&mut AcqTextStatus> {
114 unsafe {
115 let raw_ptr = &mut *(self as *mut AcquireProgress);
117 let mut status = self.status.pin_mut();
120
121 status.as_mut().set_callback(raw_ptr);
128 status
129 }
130 }
131
132 pub(crate) fn pulse_interval(&mut self) -> usize { self.inner.pulse_interval() }
134
135 pub(crate) fn release_info_changes(&mut self, info: ReleaseInfoChanges) -> bool {
142 self.inner.release_info_changes(info)
143 }
144
145 pub(crate) fn hit(&mut self, item: &ItemDesc) { self.inner.hit(item) }
147
148 pub(crate) fn fetch(&mut self, item: &ItemDesc) { self.inner.fetch(item) }
150
151 pub(crate) fn fail(&mut self, item: &ItemDesc) { self.inner.fail(item) }
153
154 pub(crate) fn pulse(&mut self, owner: &PkgAcquire) { self.inner.pulse(&self.status, owner) }
156
157 pub(crate) fn start(&mut self) { self.inner.start() }
159
160 pub(crate) fn done(&mut self, item: &ItemDesc) { self.inner.done(item) }
162
163 pub(crate) fn stop(&mut self) { self.inner.stop(&self.status) }
165}
166
167impl Default for AcquireProgress<'_> {
168 fn default() -> Self { Self::apt() }
169}
170
171unsafe impl ExternType for AcquireProgress<'_> {
173 type Id = cxx::type_id!("AcquireProgress");
174 type Kind = cxx::kind::Trivial;
175}
176
177pub struct OperationProgress<'a> {
182 inner: Box<dyn DynOperationProgress + 'a>,
183}
184
185impl<'a> OperationProgress<'a> {
186 pub fn new(inner: impl DynOperationProgress + 'static) -> Self {
189 Self {
190 inner: Box::new(inner),
191 }
192 }
193
194 pub fn quiet() -> Self { Self::new(NoOpProgress {}) }
198
199 fn update(&mut self, operation: String, percent: f32) { self.inner.update(operation, percent) }
201
202 fn done(&mut self) { self.inner.done() }
204
205 pub fn pin(&mut self) -> Pin<&mut OperationProgress<'a>> { Pin::new(self) }
206}
207
208impl Default for OperationProgress<'_> {
209 fn default() -> Self { Self::quiet() }
210}
211
212unsafe impl ExternType for OperationProgress<'_> {
214 type Id = cxx::type_id!("OperationProgress");
215 type Kind = cxx::kind::Trivial;
216}
217
218pub enum InstallProgress<'a> {
222 Fancy(InstallProgressFancy<'a>),
223 Fd(RawFd),
224}
225
226impl InstallProgress<'_> {
227 pub fn new(inner: impl DynInstallProgress + 'static) -> Self {
230 Self::Fancy(InstallProgressFancy::new(inner))
231 }
232
233 pub fn fd(fd: RawFd) -> Self { Self::Fd(fd) }
236
237 pub fn apt() -> Self { Self::new(AptInstallProgress::new()) }
239}
240
241impl Default for InstallProgress<'_> {
242 fn default() -> Self { Self::apt() }
243}
244
245pub struct InstallProgressFancy<'a> {
249 inner: Box<dyn DynInstallProgress + 'a>,
250}
251
252impl<'a> InstallProgressFancy<'a> {
253 pub fn new(inner: impl DynInstallProgress + 'static) -> Self {
256 Self {
257 inner: Box::new(inner),
258 }
259 }
260
261 pub fn apt() -> Self { Self::new(AptInstallProgress::new()) }
263
264 fn status_changed(
265 &mut self,
266 pkgname: String,
267 steps_done: u64,
268 total_steps: u64,
269 action: String,
270 ) {
271 self.inner
272 .status_changed(pkgname, steps_done, total_steps, action)
273 }
274
275 fn error(&mut self, pkgname: String, steps_done: u64, total_steps: u64, error: String) {
276 self.inner.error(pkgname, steps_done, total_steps, error)
277 }
278
279 pub fn pin(&mut self) -> Pin<&mut InstallProgressFancy<'a>> { Pin::new(self) }
280}
281
282impl Default for InstallProgressFancy<'_> {
283 fn default() -> Self { Self::apt() }
284}
285
286unsafe impl ExternType for InstallProgressFancy<'_> {
288 type Id = cxx::type_id!("InstallProgressFancy");
289 type Kind = cxx::kind::Trivial;
290}
291
292struct NoOpProgress {}
298
299impl DynOperationProgress for NoOpProgress {
300 fn update(&mut self, _operation: String, _percent: f32) {}
301
302 fn done(&mut self) {}
303}
304
305#[derive(Default, Debug)]
309pub struct AptAcquireProgress {
310 lastline: usize,
311 pulse_interval: usize,
312 disable: bool,
313 config: Config,
314}
315
316impl AptAcquireProgress {
317 pub fn new() -> Self { Self::default() }
319
320 pub fn disable() -> Self {
322 AptAcquireProgress {
323 disable: true,
324 ..Default::default()
325 }
326 }
327
328 fn clear_last_line(&mut self, term_width: usize) {
330 if self.disable {
331 return;
332 }
333
334 if self.lastline == 0 {
335 return;
336 }
337
338 if self.lastline > term_width {
339 self.lastline = term_width
340 }
341
342 print!("\r{}", " ".repeat(self.lastline));
343 print!("\r");
344 stdout().flush().unwrap();
345 }
346}
347
348impl DynAcquireProgress for AptAcquireProgress {
349 fn pulse_interval(&self) -> usize { self.pulse_interval }
361
362 fn hit(&mut self, item: &ItemDesc) {
366 if self.disable {
367 return;
368 }
369
370 self.clear_last_line(terminal_content_width());
371
372 println!("\rHit:{} {}", item.owner().id(), item.description());
373 }
374
375 fn fetch(&mut self, item: &ItemDesc) {
379 if self.disable {
380 return;
381 }
382
383 self.clear_last_line(terminal_content_width());
384
385 let mut string = format!("\rGet:{} {}", item.owner().id(), item.description());
386
387 let file_size = item.owner().file_size();
388 if file_size != 0 {
389 string.push_str(&format!(" [{}]", unit_str(file_size, NumSys::Decimal)));
390 }
391
392 println!("{string}");
393 }
394
395 fn done(&mut self, _item: &ItemDesc) {
399 }
403
404 fn start(&mut self) { self.lastline = 0; }
411
412 fn stop(&mut self, owner: &AcqTextStatus) {
418 if self.disable {
419 return;
420 }
421
422 self.clear_last_line(terminal_content_width());
423
424 if pending_error() {
425 return;
426 }
427
428 if owner.fetched_bytes() != 0 {
429 println!(
430 "Fetched {} in {} ({}/s)",
431 unit_str(owner.fetched_bytes(), NumSys::Decimal),
432 time_str(owner.elapsed_time()),
433 unit_str(owner.current_cps(), NumSys::Decimal)
434 );
435 } else {
436 println!("Nothing to fetch.");
437 }
438 }
439
440 fn fail(&mut self, item: &ItemDesc) {
444 if self.disable {
445 return;
446 }
447
448 self.clear_last_line(terminal_content_width());
449
450 let mut show_error = true;
451 let error_text = item.owner().error_text();
452 let desc = format!("{} {}", item.owner().id(), item.description());
453
454 match item.owner().status() {
455 ItemState::StatIdle | ItemState::StatDone => {
456 println!("\rIgn: {desc}");
457 let key = "Acquire::Progress::Ignore::ShowErrorText";
458 if error_text.is_empty() || self.config.bool(key, false) {
459 show_error = false;
460 }
461 },
462 _ => {
463 println!("\rErr: {desc}");
464 },
465 }
466
467 if show_error {
468 println!("\r{error_text}");
469 }
470 }
471
472 fn pulse(&mut self, status: &AcqTextStatus, owner: &PkgAcquire) {
478 if self.disable {
479 return;
480 }
481
482 let term_width = terminal_content_width();
484
485 let mut string = String::new();
486 let mut percent_str = format!("\r{:.0}%", status.percent());
487 let mut eta_str = String::new();
488
489 let current_cps = status.current_cps();
491 if current_cps != 0 {
492 let _ = write!(
493 eta_str,
494 " {} {}",
495 unit_str(current_cps, NumSys::Decimal),
497 time_str(status.total_bytes().saturating_sub(status.current_bytes()) / current_cps,)
499 );
500 }
501
502 for worker in owner.workers().iter() {
503 let mut work_string = String::new();
504 work_string.push_str(" [");
505
506 let Ok(item) = worker.item() else {
507 if !worker.status().is_empty() {
508 work_string.push_str(&worker.status());
509 work_string.push(']');
510 }
511 continue;
512 };
513
514 let id = item.owner().id();
515 if id != 0 {
516 let _ = write!(work_string, " {id} ");
517 }
518 work_string.push_str(&item.short_desc());
519
520 let sub = item.owner().active_subprocess();
521 if !sub.is_empty() {
522 work_string.push(' ');
523 work_string.push_str(&sub);
524 }
525
526 work_string.push(' ');
527 work_string.push_str(&unit_str(worker.current_size(), NumSys::Decimal));
528
529 if worker.total_size() > 0 && !item.owner().complete() {
530 let _ = write!(
531 work_string,
532 "/{} {}%",
533 unit_str(worker.total_size(), NumSys::Decimal),
534 (progress_fraction(worker.current_size(), worker.total_size()) * 100.0) as u64
535 );
536 }
537
538 work_string.push(']');
539
540 if (string.len() + work_string.len() + percent_str.len() + eta_str.len()) > term_width {
541 break;
542 }
543
544 string.push_str(&work_string);
545 }
546
547 if string.is_empty() {
549 string = " [Working]".to_string()
550 }
551
552 percent_str.push_str(&string);
554
555 if !eta_str.is_empty() {
557 let fill_size = percent_str.len() + eta_str.len();
558 if fill_size < term_width {
559 percent_str.push_str(&" ".repeat(term_width - fill_size))
560 }
561 }
562
563 percent_str.push_str(&eta_str);
565
566 print!("{percent_str}");
568 stdout().flush().unwrap();
569
570 if self.lastline > percent_str.len() {
571 self.clear_last_line(term_width);
572 }
573
574 self.lastline = percent_str.len();
575 }
576}
577
578pub struct AptInstallProgress {
580 config: Config,
581}
582
583impl AptInstallProgress {
584 pub fn new() -> Self {
585 Self {
586 config: Config::new(),
587 }
588 }
589}
590
591impl Default for AptInstallProgress {
592 fn default() -> Self { Self::new() }
593}
594
595impl DynInstallProgress for AptInstallProgress {
596 fn status_changed(
597 &mut self,
598 _pkgname: String,
599 steps_done: u64,
600 total_steps: u64,
601 _action: String,
602 ) {
603 let term_height = terminal_height();
605 let term_width = terminal_width();
606
607 print!("\x1b7");
609
610 print!("\x1b[{term_height};0f");
612 std::io::stdout().flush().unwrap();
613
614 let percent = progress_fraction(steps_done, total_steps) as f32;
616 let mut percent_str = (percent * 100.0).round().to_string();
617
618 let percent_padding = match percent_str.len() {
619 1 => " ",
620 2 => " ",
621 3 => "",
622 _ => unreachable!(),
623 };
624
625 percent_str = percent_padding.to_owned() + &percent_str;
626
627 let bg_color = self
631 .config
632 .find("Dpkg::Progress-Fancy::Progress-fg", "\x1b[42m");
633 let fg_color = self
634 .config
635 .find("Dpkg::Progress-Fancy::Progress-bg", "\x1b[30m");
636 const BG_COLOR_RESET: &str = "\x1b[49m";
637 const FG_COLOR_RESET: &str = "\x1b[39m";
638
639 print!("{bg_color}{fg_color}Progress: [{percent_str}%]{BG_COLOR_RESET}{FG_COLOR_RESET} ");
640
641 const PROGRESS_STR_LEN: usize = 17;
643
644 if let Ok(progress_width) = u32::try_from(term_width.saturating_sub(PROGRESS_STR_LEN)) {
649 if progress_width > 0 {
650 print!("{}", get_apt_progress_string(percent, progress_width));
651 }
652 }
653 std::io::stdout().flush().unwrap();
654
655 print!("\x1b8");
662 std::io::stdout().flush().unwrap();
663 }
664
665 fn error(&mut self, _pkgname: String, _steps_done: u64, _total_steps: u64, _error: String) {}
667}
668
669#[allow(clippy::needless_lifetimes)]
670#[cxx::bridge]
671pub(crate) mod raw {
672 #[derive(Debug)]
674 struct ReleaseInfoChanges {
675 pub uri: String,
677 pub dist: String,
679 pub changes: Vec<ReleaseInfoChange>,
681 }
682
683 #[derive(Debug)]
685 struct ReleaseInfoChange {
686 pub field: String,
688 pub old_value: String,
690 pub new_value: String,
692 pub message: String,
694 pub default_action: bool,
696 }
697
698 extern "Rust" {
699 type AcquireProgress<'a>;
700 type OperationProgress<'a>;
701 type InstallProgressFancy<'a>;
702
703 fn update(self: &mut OperationProgress, operation: String, percent: f32);
705
706 fn done(self: &mut OperationProgress);
708
709 fn status_changed(
711 self: &mut InstallProgressFancy,
712 pkgname: String,
713 steps_done: u64,
714 total_steps: u64,
715 action: String,
716 );
717
718 fn error(
722 self: &mut InstallProgressFancy,
723 pkgname: String,
724 steps_done: u64,
725 total_steps: u64,
726 error: String,
727 );
728
729 fn pulse_interval(self: &mut AcquireProgress) -> usize;
731
732 fn release_info_changes(self: &mut AcquireProgress, info: ReleaseInfoChanges) -> bool;
734
735 fn hit(self: &mut AcquireProgress, item: &ItemDesc);
737
738 fn fetch(self: &mut AcquireProgress, item: &ItemDesc);
740
741 fn fail(self: &mut AcquireProgress, item: &ItemDesc);
743
744 fn pulse(self: &mut AcquireProgress, owner: &PkgAcquire);
746
747 fn done(self: &mut AcquireProgress, item: &ItemDesc);
749
750 fn start(self: &mut AcquireProgress);
752
753 fn stop(self: &mut AcquireProgress);
755 }
756
757 extern "C++" {
758 type ItemDesc = crate::acquire::raw::ItemDesc;
759 type PkgAcquire = crate::acquire::raw::PkgAcquire;
760 include!("rust-apt/apt-pkg-c/types.h");
761 }
762}
763
764#[cfg(test)]
765mod tests {
766 use super::{DynAcquireProgress, ReleaseInfoChange, ReleaseInfoChanges, progress_fraction};
767 use crate::raw::{AcqTextStatus, ItemDesc, PkgAcquire};
768
769 struct Progress;
770
771 impl DynAcquireProgress for Progress {
772 fn pulse_interval(&self) -> usize { 0 }
773
774 fn hit(&mut self, _: &ItemDesc) {}
775
776 fn fetch(&mut self, _: &ItemDesc) {}
777
778 fn fail(&mut self, _: &ItemDesc) {}
779
780 fn pulse(&mut self, _: &AcqTextStatus, _: &PkgAcquire) {}
781
782 fn done(&mut self, _: &ItemDesc) {}
783
784 fn start(&mut self) {}
785
786 fn stop(&mut self, _: &AcqTextStatus) {}
787 }
788
789 fn change(default_action: bool) -> ReleaseInfoChange {
790 ReleaseInfoChange {
791 field: "Origin".into(),
792 old_value: "Earth".into(),
793 new_value: "Mars".into(),
794 message: "Repository changed its Origin".into(),
795 default_action,
796 }
797 }
798
799 fn info(changes: Vec<ReleaseInfoChange>) -> ReleaseInfoChanges {
800 ReleaseInfoChanges {
801 uri: "https://deb.example.invalid".into(),
802 dist: "stable".into(),
803 changes,
804 }
805 }
806
807 #[test]
808 fn release_info_changes_use_apt_default_action() {
809 let mut progress = Progress;
810
811 assert!(progress.release_info_changes(info(vec![change(true)])));
812 assert!(!progress.release_info_changes(info(vec![change(true), change(false)])));
813 }
814
815 #[test]
816 fn progress_fractions_are_bounded() {
817 assert_eq!(progress_fraction(1, 0), 0.0);
818 assert_eq!(progress_fraction(50, 100), 0.5);
819 assert_eq!(progress_fraction(200, 100), 1.0);
820 assert_eq!(progress_fraction(u64::MAX, u64::MAX), 1.0);
821 }
822}