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 Spinner {
    message: String,
    rx: Receiver<SpinnerEvent>,
    frames: Vec<&'static str>,
}

pub struct SpinnerHandle {
    tx: Sender<SpinnerEvent>,
}

pub enum SpinnerEvent {
    SetMessage(String),
    Finish,
    FinishWithMessage(String),
}

impl Spinner {
    pub fn new(message: impl Into<String>) -> (Self, SpinnerHandle) {
        let (tx, rx) = mpsc::channel();
        let spinner = Self {
            message: message.into(),
            rx,
            frames: vec!["", "", "", "", "", "", "", "", "", ""],
        };
        let handle = SpinnerHandle { tx };
        (spinner, handle)
    }

    pub fn new_with_frames(
        message: impl Into<String>,
        frames: Vec<&'static str>,
    ) -> (Self, SpinnerHandle) {
        let (tx, rx) = mpsc::channel();
        let spinner = Self {
            message: message.into(),
            rx,
            frames,
        };
        let handle = SpinnerHandle { tx };
        (spinner, handle)
    }

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

        let stdout = &mut ctx.stdout();

        self.render(stdout, frame_index)?;

        loop {
            match self.rx.recv_timeout(Duration::from_millis(80)) {
                Ok(SpinnerEvent::SetMessage(msg)) => {
                    self.message = msg;
                    self.render(stdout, frame_index)?;
                }
                Ok(SpinnerEvent::Finish) => {
                    self.clear_render(stdout)?;
                    break;
                }
                Ok(SpinnerEvent::FinishWithMessage(msg)) => {
                    self.clear_render(stdout)?;
                    println!("{}", msg);
                    break;
                }
                Err(RecvTimeoutError::Timeout) => {
                    // 切换到下一帧
                    frame_index = (frame_index + 1) % self.frames.len();
                    self.render(stdout, frame_index)?;
                }
                Err(RecvTimeoutError::Disconnected) => {
                    self.clear_render(stdout)?;
                    break;
                }
            }
        }

        Ok(())
    }

    fn render(&self, stdout: &mut impl Write, frame_index: usize) -> Result<()> {
        queue!(
            stdout,
            cursor::MoveToColumn(0),
            Clear(ClearType::CurrentLine),
            Print(self.frames[frame_index]),
            Print(" "),
            Print(&self.message),
        )?;

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

    fn clear_render(&self, stdout: &mut impl Write) -> Result<()> {
        queue!(
            stdout,
            cursor::MoveToColumn(0),
            Clear(ClearType::CurrentLine),
        )?;
        stdout.flush()?;
        Ok(())
    }
}

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

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

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

// Pre-defined spinner frames
pub mod frames {
    pub const DOTS: &[&str] = &["", "", "", "", "", "", "", "", "", ""];
    pub const LINE: &[&str] = &["-", "\\", "|", "/"];
    pub const ARROW: &[&str] = &["", "", "", "", "", "", "", ""];
    pub const CIRCLE: &[&str] = &["", "", "", ""];
    pub const SQUARE: &[&str] = &["", "", "", ""];
    pub const BOUNCE: &[&str] = &["", "", "", "", "", "", "", ""];
}