xacli_components/components/spinner/
mod.rs1use 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 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
134pub 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}