xacli_components/components/spinner/
mod.rs

1use std::{
2    io::Write,
3    sync::mpsc::{self, Receiver, RecvTimeoutError, Sender},
4    time::Duration,
5};
6
7use crossterm::{
8    cursor, queue,
9    style::Print,
10    terminal::{Clear, ClearType},
11};
12use xacli_core::Context;
13
14use crate::Result;
15
16pub struct Spinner {
17    message: String,
18    rx: Receiver<SpinnerEvent>,
19    frames: Vec<&'static str>,
20}
21
22pub struct SpinnerHandle {
23    tx: Sender<SpinnerEvent>,
24}
25
26pub enum SpinnerEvent {
27    SetMessage(String),
28    Finish,
29    FinishWithMessage(String),
30}
31
32impl Spinner {
33    pub fn new(message: impl Into<String>) -> (Self, SpinnerHandle) {
34        let (tx, rx) = mpsc::channel();
35        let spinner = Self {
36            message: message.into(),
37            rx,
38            frames: vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
39        };
40        let handle = SpinnerHandle { tx };
41        (spinner, handle)
42    }
43
44    pub fn new_with_frames(
45        message: impl Into<String>,
46        frames: Vec<&'static str>,
47    ) -> (Self, SpinnerHandle) {
48        let (tx, rx) = mpsc::channel();
49        let spinner = Self {
50            message: message.into(),
51            rx,
52            frames,
53        };
54        let handle = SpinnerHandle { tx };
55        (spinner, handle)
56    }
57
58    pub fn run(mut self, ctx: &mut dyn Context) -> Result<()> {
59        let mut frame_index = 0;
60
61        let stdout = &mut ctx.stdout();
62
63        self.render(stdout, frame_index)?;
64
65        loop {
66            match self.rx.recv_timeout(Duration::from_millis(80)) {
67                Ok(SpinnerEvent::SetMessage(msg)) => {
68                    self.message = msg;
69                    self.render(stdout, frame_index)?;
70                }
71                Ok(SpinnerEvent::Finish) => {
72                    self.clear_render(stdout)?;
73                    break;
74                }
75                Ok(SpinnerEvent::FinishWithMessage(msg)) => {
76                    self.clear_render(stdout)?;
77                    println!("{}", msg);
78                    break;
79                }
80                Err(RecvTimeoutError::Timeout) => {
81                    // 切换到下一帧
82                    frame_index = (frame_index + 1) % self.frames.len();
83                    self.render(stdout, frame_index)?;
84                }
85                Err(RecvTimeoutError::Disconnected) => {
86                    self.clear_render(stdout)?;
87                    break;
88                }
89            }
90        }
91
92        Ok(())
93    }
94
95    fn render(&self, stdout: &mut impl Write, frame_index: usize) -> Result<()> {
96        queue!(
97            stdout,
98            cursor::MoveToColumn(0),
99            Clear(ClearType::CurrentLine),
100            Print(self.frames[frame_index]),
101            Print(" "),
102            Print(&self.message),
103        )?;
104
105        stdout.flush()?;
106        Ok(())
107    }
108
109    fn clear_render(&self, stdout: &mut impl Write) -> Result<()> {
110        queue!(
111            stdout,
112            cursor::MoveToColumn(0),
113            Clear(ClearType::CurrentLine),
114        )?;
115        stdout.flush()?;
116        Ok(())
117    }
118}
119
120impl SpinnerHandle {
121    pub fn set_message(&self, msg: impl Into<String>) {
122        let _ = self.tx.send(SpinnerEvent::SetMessage(msg.into()));
123    }
124
125    pub fn finish(&self) {
126        let _ = self.tx.send(SpinnerEvent::Finish);
127    }
128
129    pub fn finish_with_message(&self, msg: impl Into<String>) {
130        let _ = self.tx.send(SpinnerEvent::FinishWithMessage(msg.into()));
131    }
132}
133
134// Pre-defined spinner frames
135pub mod frames {
136    pub const DOTS: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
137    pub const LINE: &[&str] = &["-", "\\", "|", "/"];
138    pub const ARROW: &[&str] = &["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"];
139    pub const CIRCLE: &[&str] = &["◐", "◓", "◑", "◒"];
140    pub const SQUARE: &[&str] = &["◰", "◳", "◲", "◱"];
141    pub const BOUNCE: &[&str] = &["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"];
142}