xacli-components 0.2.1

Interactive components for XaCLI
Documentation
use std::{
    io::Write,
    sync::mpsc::{self, Receiver, RecvTimeoutError, Sender},
    time::Duration,
};

use crossterm::{
    cursor, queue,
    style::Print,
    terminal::{Clear, ClearType},
};
use xacli_core::Context;

use crate::Result;

pub struct ProgressBar {
    total: u64,
    message: String,
    rx: Receiver<ProgressEvent>,
}

pub struct ProgressHandle {
    tx: Sender<ProgressEvent>,
}

pub enum ProgressEvent {
    Increment(u64),
    SetProgress(u64),
    SetMessage(String),
    Finish,
}

impl ProgressBar {
    pub fn new(total: u64, message: impl Into<String>) -> (Self, ProgressHandle) {
        let (tx, rx) = mpsc::channel();
        let bar = Self {
            total,
            message: message.into(),
            rx,
        };
        let handle = ProgressHandle { tx };
        (bar, handle)
    }

    pub fn run(mut self, ctx: &mut dyn Context) -> Result<()> {
        let mut current = 0u64;

        let stdout = &mut ctx.stdout();

        self.render(stdout, current)?;

        loop {
            match self.rx.recv_timeout(Duration::from_millis(100)) {
                Ok(ProgressEvent::Increment(n)) => {
                    current = (current + n).min(self.total);
                    self.render(stdout, current)?;
                }
                Ok(ProgressEvent::SetProgress(n)) => {
                    current = n.min(self.total);
                    self.render(stdout, current)?;
                }
                Ok(ProgressEvent::SetMessage(msg)) => {
                    self.message = msg;
                    self.render(stdout, current)?;
                }
                Ok(ProgressEvent::Finish) => {
                    current = self.total;
                    self.render(stdout, current)?;
                    println!();
                    break;
                }
                Err(RecvTimeoutError::Timeout) => {
                    self.render(stdout, current)?;
                }
                Err(RecvTimeoutError::Disconnected) => {
                    println!();
                    break;
                }
            }
        }

        Ok(())
    }

    fn render(&self, stdout: &mut impl Write, current: u64) -> Result<()> {
        let percent = if self.total > 0 {
            (current as f64 / self.total as f64) * 100.0
        } else {
            0.0
        };

        let bar_width = 40;
        let filled = if self.total > 0 {
            ((current as f64 / self.total as f64) * bar_width as f64) as usize
        } else {
            0
        };
        let bar = format!("[{}{}]", "=".repeat(filled), " ".repeat(bar_width - filled));

        queue!(
            stdout,
            cursor::MoveToColumn(0),
            Clear(ClearType::CurrentLine),
            Print(&self.message),
            Print(" "),
            Print(&bar),
            Print(format!(" {}/{} ({:.1}%)", current, self.total, percent)),
        )?;

        stdout.flush()?;
        Ok(())
    }
}

impl ProgressHandle {
    pub fn inc(&self, n: u64) {
        let _ = self.tx.send(ProgressEvent::Increment(n));
    }

    pub fn set(&self, n: u64) {
        let _ = self.tx.send(ProgressEvent::SetProgress(n));
    }

    pub fn set_message(&self, msg: impl Into<String>) {
        let _ = self.tx.send(ProgressEvent::SetMessage(msg.into()));
    }

    pub fn finish(&self) {
        let _ = self.tx.send(ProgressEvent::Finish);
    }
}