1use std::io::Write;
13use std::sync::{Arc, Mutex};
14
15use crate::console::{Console, ConsoleOptions};
16use crate::file_proxy::{FileProxy, ProxyStream};
17use crate::live::{AutoLive, Live, LiveHandle, LiveOptions};
18use crate::measure::Measurement;
19use crate::protocol::Renderable;
20use crate::segment::Segment;
21use crate::spinner::{Spinner, SpinnerText};
22use crate::style::StyleType;
23use crate::text::Text;
24
25pub struct Status {
27 status: SpinnerText,
28 spinner_style: StyleType,
29 speed: f64,
30 spinner: Spinner,
31 refresh_per_second: f64,
32}
33
34impl Status {
35 pub fn new(message: impl Into<String>) -> Self {
38 Status::from_content(SpinnerText::Markup(message.into()))
39 }
40
41 pub fn from_text(message: Text) -> Self {
43 Status::from_content(SpinnerText::Text(message))
44 }
45
46 pub fn from_renderable(renderable: Box<dyn Renderable + Send>) -> Self {
49 Status::from_content(SpinnerText::Renderable(Arc::new(Mutex::new(renderable))))
50 }
51
52 fn from_content(status: SpinnerText) -> Self {
53 let spinner_style = StyleType::Name("status.spinner".to_string());
54 Status {
55 spinner: Spinner::new("dots")
56 .content(&status)
57 .style(spinner_style.clone()),
58 status,
59 spinner_style,
60 speed: 1.0,
61 refresh_per_second: 12.5,
62 }
63 }
64
65 pub fn refresh_per_second(mut self, refresh_per_second: f64) -> Self {
68 self.refresh_per_second = refresh_per_second;
69 self
70 }
71
72 fn rebuild(mut self, name: &str) -> Self {
73 self.spinner = Spinner::new(name)
74 .content(&self.status)
75 .style(self.spinner_style.clone())
76 .speed(self.speed);
77 self
78 }
79
80 pub fn spinner(self, name: &str) -> Self {
82 self.rebuild(name)
83 }
84
85 pub fn spinner_style(mut self, style: impl Into<StyleType>) -> Self {
87 self.spinner_style = style.into();
88 self.spinner = self.spinner.style(self.spinner_style.clone());
89 self
90 }
91
92 pub fn speed(mut self, speed: f64) -> Self {
94 self.speed = speed;
95 self.spinner = self.spinner.speed(speed);
96 self
97 }
98
99 pub fn update(
104 &mut self,
105 status: Option<&str>,
106 spinner: Option<&str>,
107 spinner_style: Option<StyleType>,
108 speed: Option<f64>,
109 ) {
110 self.update_content(
111 status.map(|status| SpinnerText::Markup(status.to_string())),
112 spinner,
113 spinner_style,
114 speed,
115 );
116 }
117
118 pub fn update_text(&mut self, status: Text) {
120 self.update_content(Some(SpinnerText::Text(status)), None, None, None);
121 }
122
123 pub fn update_renderable(&mut self, status: Box<dyn Renderable + Send>) {
125 let status = SpinnerText::Renderable(Arc::new(Mutex::new(status)));
126 self.update_content(Some(status), None, None, None);
127 }
128
129 fn update_content(
130 &mut self,
131 status: Option<SpinnerText>,
132 spinner: Option<&str>,
133 spinner_style: Option<StyleType>,
134 speed: Option<f64>,
135 ) {
136 if let Some(status) = status {
137 self.status = status;
138 }
139 if let Some(style) = spinner_style {
140 self.spinner_style = style;
141 }
142 if let Some(speed) = speed.filter(|s| *s != 0.0) {
143 self.speed = speed;
144 }
145 if let Some(name) = spinner {
146 self.spinner = Spinner::new(name)
147 .content(&self.status)
148 .style(self.spinner_style.clone())
149 .speed(self.speed);
150 } else {
151 self.spinner.update_content(&self.status);
152 self.spinner
153 .update(None, Some(self.spinner_style.clone()), Some(self.speed));
154 }
155 }
156
157 pub fn renderable(&self) -> &Spinner {
159 &self.spinner
160 }
161}
162
163struct StatusView(Arc<Mutex<Status>>);
165
166impl Renderable for StatusView {
167 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
168 lock(&self.0).rich_render(console, options)
169 }
170
171 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
172 lock(&self.0).measure(console, options)
173 }
174}
175
176fn lock(status: &Mutex<Status>) -> std::sync::MutexGuard<'_, Status> {
177 status
178 .lock()
179 .unwrap_or_else(|poisoned| poisoned.into_inner())
180}
181
182impl Status {
183 pub fn start<W: Write + Send + 'static>(self, console: Console, writer: W) -> LiveStatus<W> {
189 let refresh_per_second = self.refresh_per_second;
190 let shared = Arc::new(Mutex::new(self));
191 let live = Live::spawn_options(
192 Box::new(StatusView(shared.clone())),
193 console,
194 writer,
195 refresh_per_second,
196 LiveOptions {
197 transient: true,
198 refresh_on_start: false,
199 ..LiveOptions::default()
200 },
201 );
202 LiveStatus {
203 status: shared,
204 live,
205 }
206 }
207}
208
209pub struct LiveStatus<W: Write + Send + 'static> {
211 status: Arc<Mutex<Status>>,
212 live: AutoLive<W>,
213}
214
215impl<W: Write + Send + 'static> LiveStatus<W> {
216 pub fn update(
220 &self,
221 status: Option<&str>,
222 spinner: Option<&str>,
223 spinner_style: Option<StyleType>,
224 speed: Option<f64>,
225 ) {
226 lock(&self.status).update(status, spinner, spinner_style, speed);
227 if spinner.is_some() {
228 self.live.refresh_wait();
229 }
230 }
231
232 pub fn with<R>(&self, f: impl FnOnce(&mut Status) -> R) -> R {
235 f(&mut lock(&self.status))
236 }
237
238 pub fn refresh(&self) {
241 self.live.refresh_wait();
242 }
243
244 pub fn print(&self, renderable: Box<dyn Renderable + Send>) {
246 self.live.print(renderable);
247 }
248
249 pub fn print_str(&self, content: impl Into<String>) {
251 self.live.print_str(content);
252 }
253
254 pub fn stdout(&self) -> FileProxy<LiveHandle<W>, std::io::Stdout> {
257 self.live.stdout()
258 }
259
260 pub fn stderr(&self) -> FileProxy<LiveHandle<W>, std::io::Stderr> {
262 self.live.stderr()
263 }
264
265 pub fn file_proxy<F: Write>(
267 &self,
268 stream: ProxyStream,
269 file: F,
270 ) -> FileProxy<LiveHandle<W>, F> {
271 self.live.file_proxy(stream, file)
272 }
273
274 pub fn stop(self) -> W {
277 self.live.stop()
278 }
279}
280
281impl Renderable for Status {
284 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
285 self.spinner.rich_render(console, options)
286 }
287
288 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
289 self.spinner.measure(console, options)
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use crate::color::ColorSystem;
297
298 fn render(status: &Status) -> String {
299 Console::builder()
300 .force_terminal(true)
301 .color_system(Some(ColorSystem::Truecolor))
302 .width(30)
303 .no_color(false)
304 .build()
305 .render_to_string(status)
306 }
307
308 #[test]
309 fn default_status_frame() {
310 assert_eq!(
312 render(&Status::new("Loading data")),
313 "\x1b[32m⠋\x1b[0m Loading data"
314 );
315 }
316
317 #[test]
318 fn custom_spinner() {
319 assert_eq!(
320 render(&Status::new("Building").spinner("line")),
321 "\x1b[32m-\x1b[0m Building"
322 );
323 }
324}