summaryrefslogtreecommitdiffstats
path: root/src/utils/spinner.rs
blob: a04a18f27f668aff46e2ff3710320434f45497d4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use anyhow::Result;
use crossterm::{cursor, queue, style, terminal};
use is_terminal::IsTerminal;
use std::{
    io::{stdout, Write},
    time::Duration,
};
use tokio::{sync::mpsc, time::interval};

pub struct SpinnerInner {
    index: usize,
    message: String,
    is_not_terminal: bool,
}

impl SpinnerInner {
    const DATA: [&'static str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];

    fn new(message: &str) -> Self {
        SpinnerInner {
            index: 0,
            message: message.to_string(),
            is_not_terminal: !stdout().is_terminal(),
        }
    }

    fn step(&mut self) -> Result<()> {
        if self.is_not_terminal || self.message.is_empty() {
            return Ok(());
        }
        let mut writer = stdout();
        let frame = Self::DATA[self.index % Self::DATA.len()];
        let dots = ".".repeat((self.index / 5) % 4);
        let line = format!("{frame}{}{:<3}", self.message, dots);
        queue!(writer, cursor::MoveToColumn(0), style::Print(line),)?;
        if self.index == 0 {
            queue!(writer, cursor::Hide)?;
        }
        writer.flush()?;
        self.index += 1;
        Ok(())
    }

    fn set_message(&mut self, message: String) -> Result<()> {
        self.clear_message()?;
        if !message.is_empty() {
            self.message = format!(" {message}");
        }
        Ok(())
    }

    fn clear_message(&mut self) -> Result<()> {
        if self.is_not_terminal || self.message.is_empty() {
            return Ok(());
        }
        self.message.clear();
        let mut writer = stdout();
        queue!(
            writer,
            cursor::MoveToColumn(0),
            terminal::Clear(terminal::ClearType::FromCursorDown),
            cursor::Show
        )?;
        writer.flush()?;
        Ok(())
    }
}

#[derive(Clone)]
pub struct Spinner(mpsc::UnboundedSender<SpinnerEvent>);

impl Drop for Spinner {
    fn drop(&mut self) {
        self.stop();
    }
}

impl Spinner {
    pub fn set_message(&self, message: String) -> Result<()> {
        self.0.send(SpinnerEvent::SetMessage(message))?;
        Ok(())
    }

    pub fn stop(&self) {
        let _ = self.0.send(SpinnerEvent::Stop);
    }
}

enum SpinnerEvent {
    SetMessage(String),
    Stop,
}

pub async fn create_spinner(message: &str) -> Spinner {
    let message = format!(" {message}");
    let (tx, rx) = mpsc::unbounded_channel();
    tokio::spawn(run_spinner(message, rx));
    Spinner(tx)
}

async fn run_spinner(message: String, mut rx: mpsc::UnboundedReceiver<SpinnerEvent>) -> Result<()> {
    let mut spinner = SpinnerInner::new(&message);
    let mut interval = interval(Duration::from_millis(50));
    loop {
        tokio::select! {
            _ = interval.tick() => {
                let _ = spinner.step();
            }
            evt = rx.recv() => {
                if let Some(evt) = evt {
                    match evt {
                        SpinnerEvent::SetMessage(message) => {
                            spinner.set_message(message)?;
                        }
                        SpinnerEvent::Stop => {
                            spinner.clear_message()?;
                            break;
                        }
                    }

                }
            }
        }
    }
    Ok(())
}