use std::collections::VecDeque;
use std::io::{self, Read, Write};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant};
use rich::cells::cell_len;
use rich::progress::{BarColumn, ProgressColumn, TaskUpdate};
use rich::{Console, ConsoleOptions, ProgressBar, Renderable, Segment, Style};
use crate::a11y::{Status, SymbolSet};
use crate::cancel::CancelToken;
use crate::format;
use crate::layout::{fit_segments, OverflowPolicy};
pub const STYLES: &[(&str, &str)] = &[
("transfer.direction", "cyan"),
("transfer.name", "bold"),
("transfer.size", "green"),
("transfer.rate", "red"),
("transfer.eta", "cyan"),
("transfer.retry", "yellow"),
("transfer.paused", "dim"),
("transfer.done", "green"),
("transfer.failed", "bold red"),
("transfer.cancelled", "dim"),
("transfer.summary", "bold"),
];
pub(crate) fn keyed_style(console: &Console, table: &[(&str, &str)], key: &str) -> Style {
let fallback = table
.iter()
.find(|(name, _)| *name == key)
.map_or("none", |(_, spec)| spec);
crate::event::theme_style(console, key, fallback)
}
fn style(console: &Console, key: &str) -> Style {
keyed_style(console, STYLES, key)
}
pub(crate) fn has_color(console: &Console) -> bool {
!console.no_color() && console.color_system().is_some()
}
pub(crate) fn effective_symbols(console: &Console, set: SymbolSet) -> SymbolSet {
if set == SymbolSet::Unicode && console.ascii_only() {
SymbolSet::Ascii
} else {
set
}
}
pub(crate) fn bar_segments(
console: &Console,
options: &ConsoleOptions,
width: usize,
ratio: Option<f64>,
time: f64,
complete: Option<Style>,
) -> Vec<Segment> {
if !has_color(console) {
let inner = width.saturating_sub(2);
let body = match ratio {
Some(ratio) => {
let filled = ((inner as f64) * ratio.clamp(0.0, 1.0)).floor() as usize;
format!("{}{}", "#".repeat(filled), ".".repeat(inner - filled))
}
None => "-".repeat(inner),
};
return vec![Segment::new(format!("[{body}]"), None)];
}
let mut bar = match ratio {
Some(ratio) => ProgressBar::new(1000.0, (ratio.clamp(0.0, 1.0) * 1000.0).floor()),
None => ProgressBar::indeterminate(),
}
.width(width)
.animation_time(time);
if let Some(style) = complete {
bar = bar.complete_style(style.clone()).finished_style(style);
}
bar.rich_render(console, &options.update_width(width))
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Direction {
#[default]
Download,
Upload,
}
impl Direction {
pub fn symbol(self, set: SymbolSet) -> &'static str {
match (set, self) {
(SymbolSet::Unicode, Direction::Download) => "↓",
(SymbolSet::Unicode, Direction::Upload) => "↑",
(SymbolSet::Ascii, Direction::Download) => "v",
(SymbolSet::Ascii, Direction::Upload) => "^",
(SymbolSet::Words, Direction::Download) => "down",
(SymbolSet::Words, Direction::Upload) => "up",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum TransferState {
#[default]
Active,
Paused,
Retrying,
Done,
Failed,
Cancelled,
}
impl TransferState {
pub fn status(self) -> Status {
match self {
TransferState::Active => Status::Info,
TransferState::Paused => Status::Pending,
TransferState::Retrying => Status::Warning,
TransferState::Done => Status::Ok,
TransferState::Failed => Status::Error,
TransferState::Cancelled => Status::Skipped,
}
}
pub fn is_finished(self) -> bool {
matches!(
self,
TransferState::Done | TransferState::Failed | TransferState::Cancelled
)
}
fn marker(self, set: SymbolSet) -> (&'static str, &'static str) {
let (unicode, ascii, word) = match self {
TransferState::Active => ("", "", ""),
TransferState::Paused => ("‖ paused", "[PAUSED]", "paused"),
TransferState::Retrying => ("↻ retrying", "[RETRY]", "retrying"),
TransferState::Done => ("✔ done", "[DONE]", "done"),
TransferState::Failed => ("✖ failed", "[FAILED]", "failed"),
TransferState::Cancelled => ("↷ cancelled", "[CANCELLED]", "cancelled"),
};
let text = match set {
SymbolSet::Unicode => unicode,
SymbolSet::Ascii => ascii,
SymbolSet::Words => word,
};
let key = match self {
TransferState::Active => "transfer.eta",
TransferState::Paused => "transfer.paused",
TransferState::Retrying => "transfer.retry",
TransferState::Done => "transfer.done",
TransferState::Failed => "transfer.failed",
TransferState::Cancelled => "transfer.cancelled",
};
(text, key)
}
}
#[derive(Clone, Debug)]
pub struct RateMeter {
window: Duration,
samples: VecDeque<(Duration, u64)>,
}
impl Default for RateMeter {
fn default() -> Self {
RateMeter::new(Duration::from_secs(5))
}
}
impl RateMeter {
pub fn new(window: Duration) -> Self {
RateMeter {
window: window.max(Duration::from_millis(1)),
samples: VecDeque::new(),
}
}
pub fn record(&mut self, now: Duration, total: u64) {
if self
.samples
.back()
.is_some_and(|&(t, bytes)| now < t || total < bytes)
{
self.samples.clear();
}
self.samples.push_back((now, total));
let start = now.saturating_sub(self.window);
while self.samples.len() > 2 && self.samples[1].0 <= start {
self.samples.pop_front();
}
}
pub fn rate(&self) -> Option<f64> {
let (&(t0, b0), &(t1, b1)) = (self.samples.front()?, self.samples.back()?);
let span = t1.checked_sub(t0)?.as_secs_f64();
(span > 0.0).then(|| (b1 - b0) as f64 / span)
}
pub fn reset(&mut self) {
self.samples.clear();
}
}
#[derive(Clone, Debug)]
pub struct Transfer {
name: String,
direction: Direction,
total: Option<u64>,
completed: u64,
attempt: u32,
max_attempts: Option<u32>,
state: TransferState,
error: Option<String>,
meter: RateMeter,
now: Duration,
bar_width: usize,
symbols: SymbolSet,
}
impl Transfer {
pub fn new(name: impl Into<String>, direction: Direction) -> Self {
Transfer {
name: name.into(),
direction,
total: None,
completed: 0,
attempt: 1,
max_attempts: None,
state: TransferState::Active,
error: None,
meter: RateMeter::default(),
now: Duration::ZERO,
bar_width: 24,
symbols: SymbolSet::Unicode,
}
}
pub fn download(name: impl Into<String>) -> Self {
Transfer::new(name, Direction::Download)
}
pub fn upload(name: impl Into<String>) -> Self {
Transfer::new(name, Direction::Upload)
}
pub fn total(mut self, bytes: u64) -> Self {
self.total = Some(bytes);
self
}
pub fn max_attempts(mut self, max: u32) -> Self {
self.max_attempts = Some(max.max(1));
self
}
pub fn rate_window(mut self, window: Duration) -> Self {
self.meter = RateMeter::new(window);
self
}
pub fn bar_width(mut self, width: usize) -> Self {
self.bar_width = width;
self
}
pub fn symbols(mut self, set: SymbolSet) -> Self {
self.symbols = set;
self
}
pub fn set_total(&mut self, bytes: Option<u64>) {
self.total = bytes;
}
pub fn advance(&mut self, bytes: u64, now: Duration) {
self.set_completed(self.completed.saturating_add(bytes), now);
}
pub fn set_completed(&mut self, bytes: u64, now: Duration) {
if self.state.is_finished() {
return;
}
if matches!(self.state, TransferState::Paused | TransferState::Retrying) {
self.state = TransferState::Active;
}
self.completed = bytes;
self.now = now;
self.meter.record(now, bytes);
}
pub fn start(&mut self, now: Duration) {
self.set_completed(self.completed, now);
}
pub fn pause(&mut self, now: Duration) {
if !self.state.is_finished() {
self.state = TransferState::Paused;
self.now = now;
self.meter.reset();
}
}
pub fn retry(&mut self, reason: impl Into<String>) -> bool {
if self.state.is_finished() {
return false;
}
self.error = Some(reason.into());
self.meter.reset();
if self.max_attempts.is_some_and(|max| self.attempt >= max) {
self.state = TransferState::Failed;
return false;
}
self.attempt = self.attempt.saturating_add(1);
self.state = TransferState::Retrying;
true
}
pub fn finish(&mut self, now: Duration) {
if let Some(total) = self.total {
self.set_completed(total, now);
} else {
self.now = now;
}
if !self.state.is_finished() {
self.state = TransferState::Done;
}
}
pub fn fail(&mut self, reason: impl Into<String>) {
if !self.state.is_finished() {
self.error = Some(reason.into());
self.state = TransferState::Failed;
}
}
pub fn cancel(&mut self) {
if !self.state.is_finished() {
self.state = TransferState::Cancelled;
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn direction(&self) -> Direction {
self.direction
}
pub fn total_bytes(&self) -> Option<u64> {
self.total
}
pub fn completed(&self) -> u64 {
self.completed
}
pub fn attempt(&self) -> u32 {
self.attempt
}
pub fn state(&self) -> TransferState {
self.state
}
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
pub fn fraction(&self) -> Option<f64> {
match self.total {
Some(0) => Some(1.0),
Some(total) => Some((self.completed as f64 / total as f64).min(1.0)),
None => None,
}
}
pub fn rate(&self) -> Option<f64> {
if self.state == TransferState::Active {
self.meter.rate()
} else {
None
}
}
pub fn eta(&self) -> Option<Duration> {
let rate = self.rate().filter(|r| *r > 0.0)?;
let left = self.total?.saturating_sub(self.completed);
Some(Duration::from_secs((left as f64 / rate).ceil() as u64))
}
pub fn task_update(&self) -> TaskUpdate {
let mut update = TaskUpdate::default().completed(self.completed as f64);
if let Some(total) = self.total {
update = update.total(total as f64);
}
let (marker, _) = self.state.marker(SymbolSet::Words);
let description = match (self.state, marker) {
(TransferState::Retrying, _) => {
format!("{} (retry {})", self.name, self.attempt_label())
}
(_, "") => self.name.clone(),
(_, marker) => format!("{} ({marker})", self.name),
};
update.description(rich::markup::escape(&description))
}
fn attempt_label(&self) -> String {
match self.max_attempts {
Some(max) => format!("{}/{max}", self.attempt),
None => self.attempt.to_string(),
}
}
fn sizes(&self) -> String {
match self.total {
Some(total) => {
const UNITS: &[&str] = &["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
let (unit, suffix) = rich::filesize::pick_unit_and_suffix(total, UNITS, 1000);
let scale = |n: u64| {
if unit == 1 {
format::number(n.min(i64::MAX as u64) as i64)
} else {
format!("{:.1}", n as f64 / unit as f64)
}
};
format!("{}/{} {suffix}", scale(self.completed), scale(total))
}
None => format::bytes(self.completed),
}
}
fn tail(&self, set: SymbolSet) -> (String, &'static str) {
let (marker, key) = self.state.marker(set);
match self.state {
TransferState::Active => {
let eta = self.eta().map_or("-:--:--".to_string(), format::clock);
let mut tail = format!("ETA {eta}");
if self.attempt > 1 {
tail.push_str(&format!(" (attempt {})", self.attempt_label()));
}
(tail, key)
}
TransferState::Retrying => (format!("{marker} {}", self.attempt_label()), key),
TransferState::Failed => match &self.error {
Some(error) => (format!("{marker}: {error}"), key),
None => (marker.to_string(), key),
},
_ => (marker.to_string(), key),
}
}
fn cells(&self, set: SymbolSet) -> Cells {
let (tail, tail_key) = self.tail(set);
Cells {
arrow: self.direction.symbol(set),
sizes: self.sizes(),
rate: self.rate().map_or("-".to_string(), format::rate),
tail,
tail_key,
}
}
fn render_line(
&self,
console: &Console,
options: &ConsoleOptions,
widths: Widths,
set: SymbolSet,
) -> Vec<Segment> {
let cells = self.cells(set);
let pad = |text: &str, width: usize, right: bool| {
let fill = " ".repeat(width.saturating_sub(cell_len(text)));
if right {
format!("{fill}{text}")
} else {
format!("{text}{fill}")
}
};
let arrow = pad(cells.arrow, widths.arrow, false);
let name = pad(&self.name, widths.name, false);
let sizes = pad(&cells.sizes, widths.sizes, true);
let rate = pad(&cells.rate, widths.rate, true);
let fixed = cell_len(&arrow)
+ 1
+ cell_len(&name)
+ 2
+ cell_len(&sizes)
+ 2
+ cell_len(&rate)
+ 2
+ widths.tail.max(cell_len(&cells.tail));
let room = options.max_width.saturating_sub(fixed + 2);
let bar_width = self.bar_width.min(room);
let mut line = vec![
Segment::new(arrow, Some(style(console, "transfer.direction"))),
Segment::new(" ", None),
Segment::new(name, Some(style(console, "transfer.name"))),
Segment::new(" ", None),
];
if bar_width >= 8 {
let ratio = match self.state {
TransferState::Done => Some(1.0),
_ => self.fraction(),
};
let complete = match self.state {
TransferState::Failed => Some("transfer.failed"),
TransferState::Paused | TransferState::Cancelled => Some("transfer.paused"),
TransferState::Retrying => Some("transfer.retry"),
_ => None,
}
.map(|key| style(console, key));
line.extend(bar_segments(
console,
options,
bar_width,
ratio,
self.now.as_secs_f64(),
complete,
));
line.push(Segment::new(" ", None));
}
line.extend([
Segment::new(sizes, Some(style(console, "transfer.size"))),
Segment::new(" ", None),
Segment::new(rate, Some(style(console, "transfer.rate"))),
Segment::new(" ", None),
Segment::new(cells.tail, Some(style(console, cells.tail_key))),
]);
finish_line(line, options.max_width)
}
}
pub(crate) fn finish_line(line: Vec<Segment>, width: usize) -> Vec<Segment> {
fit_segments(&line, width.max(1), OverflowPolicy::Crop)
.into_iter()
.next()
.unwrap_or_default()
}
pub(crate) fn join_lines(lines: impl IntoIterator<Item = Vec<Segment>>) -> Vec<Segment> {
let mut out = Vec::new();
for (i, line) in lines.into_iter().enumerate() {
if i > 0 {
out.push(Segment::line());
}
out.extend(line);
}
out
}
struct Cells {
arrow: &'static str,
sizes: String,
rate: String,
tail: String,
tail_key: &'static str,
}
#[derive(Clone, Copy, Default)]
struct Widths {
arrow: usize,
name: usize,
sizes: usize,
rate: usize,
tail: usize,
}
impl Widths {
fn of<'a>(transfers: impl IntoIterator<Item = &'a Transfer>, set: SymbolSet) -> Self {
let mut widths = Widths::default();
for transfer in transfers {
let cells = transfer.cells(set);
widths.arrow = widths.arrow.max(cell_len(cells.arrow));
widths.name = widths.name.max(cell_len(&transfer.name));
widths.sizes = widths.sizes.max(cell_len(&cells.sizes));
widths.rate = widths.rate.max(cell_len(&cells.rate));
widths.tail = widths.tail.max(cell_len(&cells.tail));
}
widths
}
}
impl Renderable for Transfer {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let set = effective_symbols(console, self.symbols);
self.render_line(console, options, Widths::of([self], set), set)
}
}
#[derive(Clone, Debug, Default)]
pub struct Transfers {
items: Vec<Transfer>,
summary: bool,
symbols: SymbolSet,
}
impl Transfers {
pub fn new() -> Self {
Transfers::default()
}
pub fn summary(mut self, summary: bool) -> Self {
self.summary = summary;
self
}
pub fn symbols(mut self, set: SymbolSet) -> Self {
self.symbols = set;
self
}
pub fn push(&mut self, transfer: Transfer) -> usize {
self.items.push(transfer);
self.items.len() - 1
}
pub fn iter(&self) -> std::slice::Iter<'_, Transfer> {
self.items.iter()
}
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Transfer> {
self.items.iter_mut()
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn finished(&self) -> bool {
self.items.iter().all(|t| t.state.is_finished())
}
pub fn rate(&self) -> Option<f64> {
let rates: Vec<f64> = self.items.iter().filter_map(Transfer::rate).collect();
(!rates.is_empty()).then(|| rates.iter().sum())
}
fn summary_line(&self, console: &Console, width: usize) -> Vec<Segment> {
let count = self.items.len();
let mut words = vec![format!(
"{count} transfer{}",
if count == 1 { "" } else { "s" }
)];
for state in [
TransferState::Done,
TransferState::Retrying,
TransferState::Failed,
TransferState::Cancelled,
] {
let n = self.items.iter().filter(|t| t.state == state).count();
if n > 0 {
let (word, _) = state.marker(SymbolSet::Words);
words.push(format!("{n} {word}"));
}
}
let completed = self
.items
.iter()
.fold(0u64, |sum, t| sum.saturating_add(t.completed));
let totals = self
.items
.iter()
.try_fold(0u64, |sum, t| Some(sum.saturating_add(t.total?)));
let mut aggregate = Transfer::download("");
aggregate.completed = completed;
aggregate.total = totals;
let mut line = vec![
Segment::new(words.join(", "), Some(style(console, "transfer.summary"))),
Segment::new(" ", None),
Segment::new(aggregate.sizes(), Some(style(console, "transfer.size"))),
];
if let Some(rate) = self.rate() {
line.push(Segment::new(" ", None));
line.push(Segment::new(
format::rate(rate),
Some(style(console, "transfer.rate")),
));
}
finish_line(line, width)
}
}
impl std::ops::Index<usize> for Transfers {
type Output = Transfer;
fn index(&self, index: usize) -> &Transfer {
&self.items[index]
}
}
impl std::ops::IndexMut<usize> for Transfers {
fn index_mut(&mut self, index: usize) -> &mut Transfer {
&mut self.items[index]
}
}
impl Renderable for Transfers {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let set = effective_symbols(console, self.symbols);
let widths = Widths::of(&self.items, set);
let mut lines: Vec<Vec<Segment>> = self
.items
.iter()
.map(|transfer| transfer.render_line(console, options, widths, set))
.collect();
if self.summary && !self.items.is_empty() {
lines.push(self.summary_line(console, options.max_width));
}
join_lines(lines)
}
}
pub fn transfer_columns() -> Vec<ProgressColumn> {
vec![
ProgressColumn::Description,
ProgressColumn::BarWith(BarColumn::new().bar_width(Some(30))),
ProgressColumn::Download,
ProgressColumn::TransferSpeed,
ProgressColumn::time_remaining(),
]
}
pub type SharedTransfer = Arc<Mutex<Transfer>>;
pub type Clock = Arc<dyn Fn() -> Duration + Send + Sync>;
pub fn cancelled() -> io::Error {
io::Error::other(Cancelled)
}
pub fn is_cancelled(error: &io::Error) -> bool {
error
.get_ref()
.is_some_and(|inner| inner.downcast_ref::<Cancelled>().is_some())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cancelled;
impl std::fmt::Display for Cancelled {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("transfer cancelled")
}
}
impl std::error::Error for Cancelled {}
struct Meter {
transfer: SharedTransfer,
cancel: Option<CancelToken>,
clock: Clock,
}
impl Meter {
fn new(transfer: SharedTransfer) -> Self {
let origin = Instant::now();
Meter {
transfer,
cancel: None,
clock: Arc::new(move || origin.elapsed()),
}
}
fn lock(&self) -> MutexGuard<'_, Transfer> {
self.transfer.lock().unwrap_or_else(|e| e.into_inner())
}
fn check(&self) -> io::Result<()> {
if self.cancel.as_ref().is_some_and(CancelToken::is_cancelled) {
self.lock().cancel();
return Err(cancelled());
}
Ok(())
}
fn count(&self, result: io::Result<usize>, eof_finishes: bool) -> io::Result<usize> {
let now = (self.clock)();
let mut transfer = self.lock();
match &result {
Ok(0) if eof_finishes => transfer.finish(now),
Ok(n) => transfer.advance(*n as u64, now),
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => transfer.fail(e.to_string()),
}
result
}
}
pub struct TransferReader<R> {
inner: R,
meter: Meter,
}
impl<R> TransferReader<R> {
pub fn new(inner: R, transfer: SharedTransfer) -> Self {
TransferReader {
inner,
meter: Meter::new(transfer),
}
}
pub fn cancel(mut self, token: CancelToken) -> Self {
self.meter.cancel = Some(token);
self
}
pub fn clock(mut self, clock: Clock) -> Self {
self.meter.clock = clock;
self
}
pub fn transfer(&self) -> &SharedTransfer {
&self.meter.transfer
}
pub fn into_inner(self) -> R {
self.inner
}
}
impl<R: Read> Read for TransferReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.meter.check()?;
let result = self.inner.read(buf);
self.meter.count(result, !buf.is_empty())
}
}
pub struct TransferWriter<W> {
inner: W,
meter: Meter,
}
impl<W> TransferWriter<W> {
pub fn new(inner: W, transfer: SharedTransfer) -> Self {
TransferWriter {
inner,
meter: Meter::new(transfer),
}
}
pub fn cancel(mut self, token: CancelToken) -> Self {
self.meter.cancel = Some(token);
self
}
pub fn clock(mut self, clock: Clock) -> Self {
self.meter.clock = clock;
self
}
pub fn transfer(&self) -> &SharedTransfer {
&self.meter.transfer
}
pub fn into_inner(self) -> W {
self.inner
}
}
impl<W: Write> Write for TransferWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.meter.check()?;
let result = self.inner.write(buf);
self.meter.count(result, false)
}
fn flush(&mut self) -> io::Result<()> {
self.meter.check()?;
self.inner.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn meter_keeps_an_anchor_at_the_window_start() {
let s = Duration::from_secs;
let mut meter = RateMeter::new(s(2));
assert_eq!(meter.rate(), None);
meter.record(s(0), 0);
meter.record(s(1), 100);
meter.record(s(2), 200);
meter.record(s(3), 1200);
assert_eq!(meter.rate(), Some(550.0));
meter.record(s(4), 100);
assert_eq!(meter.rate(), None, "going backwards starts over");
}
#[test]
fn retry_counts_attempts_until_the_limit() {
let mut t = Transfer::download("x").max_attempts(2);
assert!(t.retry("reset"));
assert_eq!((t.attempt(), t.state()), (2, TransferState::Retrying));
t.advance(1, Duration::ZERO);
assert_eq!(t.state(), TransferState::Active);
assert!(!t.retry("reset again"));
assert_eq!(t.state(), TransferState::Failed);
assert_eq!(t.error(), Some("reset again"));
}
}