1use std::io::Write;
16use std::sync::mpsc::{self, RecvTimeoutError};
17use std::thread::{self, JoinHandle};
18use std::time::Duration;
19
20use crate::console::Console;
21use crate::control::Control;
22use crate::live_render::LiveRender;
23use crate::protocol::Renderable;
24
25pub struct Live<W: Write> {
29 live_render: LiveRender,
30 console: Console,
31 writer: W,
32 started: bool,
33}
34
35impl<W: Write> Live<W> {
36 pub fn new(renderable: Box<dyn Renderable>, console: Console, writer: W) -> Self {
39 Live {
40 live_render: LiveRender::new(renderable),
41 console,
42 writer,
43 started: false,
44 }
45 }
46
47 pub fn start(&mut self) {
49 if self.started {
50 return;
51 }
52 self.started = true;
53 if self.console.is_terminal() {
54 let _ = write!(self.writer, "{}", Control::show_cursor(false).as_str());
55 }
56 self.refresh();
57 }
58
59 pub fn update(&mut self, renderable: Box<dyn Renderable>) {
61 self.live_render.set_renderable(renderable);
62 self.refresh();
63 }
64
65 pub fn refresh(&mut self) {
68 if !self.console.is_terminal() {
69 return;
70 }
71 let position = self.live_render.position_cursor();
74 let content = self.console.render_to_string(&self.live_render);
75 let _ = write!(self.writer, "{}{}", position.as_str(), content);
76 }
77
78 pub fn stop(&mut self) {
80 if !self.started {
81 return;
82 }
83 if !self.console.is_terminal() {
84 let content = self.console.render_to_string(&self.live_render);
85 let _ = writeln!(self.writer, "{content}");
86 self.started = false;
87 return;
88 }
89 let position = self.live_render.position_cursor();
90 let content = self.console.render_to_string(&self.live_render);
91 let _ = write!(
92 self.writer,
93 "{}{}\n{}",
94 position.as_str(),
95 content,
96 Control::show_cursor(true).as_str()
97 );
98 self.started = false;
99 }
100
101 pub fn writer(&self) -> &W {
103 &self.writer
104 }
105
106 pub fn into_writer(self) -> W {
108 self.writer
109 }
110}
111
112enum LiveMessage {
114 Update(Box<dyn Renderable + Send>),
116 Refresh,
118 Stop,
120}
121
122impl<W: Write + Send + 'static> Live<W> {
123 pub fn spawn(
129 renderable: Box<dyn Renderable + Send>,
130 console: Console,
131 writer: W,
132 refresh_per_second: f64,
133 ) -> AutoLive<W> {
134 let (sender, receiver) = mpsc::channel::<LiveMessage>();
135 let interval = Duration::from_secs_f64(1.0 / refresh_per_second.max(f64::MIN_POSITIVE));
136 let handle = thread::spawn(move || {
137 let mut live = Live::new(renderable, console, writer);
140 live.start();
141 loop {
142 match receiver.recv_timeout(interval) {
143 Ok(LiveMessage::Update(renderable)) => live.update(renderable),
144 Ok(LiveMessage::Refresh) | Err(RecvTimeoutError::Timeout) => live.refresh(),
145 Ok(LiveMessage::Stop) | Err(RecvTimeoutError::Disconnected) => {
147 live.stop();
148 break;
149 }
150 }
151 }
152 live.into_writer()
153 });
154 AutoLive {
155 sender,
156 handle: Some(handle),
157 }
158 }
159}
160
161pub struct AutoLive<W: Write + Send + 'static> {
164 sender: mpsc::Sender<LiveMessage>,
165 handle: Option<JoinHandle<W>>,
166}
167
168impl<W: Write + Send + 'static> AutoLive<W> {
169 pub fn update(&self, renderable: Box<dyn Renderable + Send>) {
171 let _ = self.sender.send(LiveMessage::Update(renderable));
172 }
173
174 pub fn refresh(&self) {
176 let _ = self.sender.send(LiveMessage::Refresh);
177 }
178
179 pub fn stop(mut self) -> W {
181 let _ = self.sender.send(LiveMessage::Stop);
182 self.handle
183 .take()
184 .expect("thread handle present until stop/drop")
185 .join()
186 .expect("live refresh thread panicked")
187 }
188}
189
190impl<W: Write + Send + 'static> Drop for AutoLive<W> {
191 fn drop(&mut self) {
192 if let Some(handle) = self.handle.take() {
194 let _ = self.sender.send(LiveMessage::Stop);
195 let _ = handle.join();
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::color::ColorSystem;
204 use crate::text::Text;
205
206 fn console() -> Console {
207 Console::builder()
208 .force_terminal(true)
209 .color_system(Some(ColorSystem::Truecolor))
210 .width(20)
211 .no_color(false)
212 .build()
213 }
214
215 #[test]
216 fn redirected_live_emits_only_the_final_frame() {
217 let console = Console::builder().force_terminal(false).width(20).build();
218 let mut live = Live::new(Box::new(Text::new("first")), console, Vec::<u8>::new());
219 live.start();
220 live.update(Box::new(Text::new("last")));
221 live.refresh();
222 assert!(live.writer().is_empty());
223 live.stop();
224 assert_eq!(live.writer(), b"last\n");
225 }
226
227 #[test]
228 fn manual_refresh_stream_matches_upstream() {
229 let mut live = Live::new(
230 Box::new(Text::new("frame one")),
231 console(),
232 Vec::<u8>::new(),
233 );
234 live.start();
235 live.update(Box::new(Text::new("frame two")));
236 live.update(Box::new(Text::new("frame three")));
237 live.stop();
238
239 let expected = "\x1b[?25lframe one\r\x1b[2Kframe two\r\x1b[2Kframe three\r\x1b[2Kframe three\n\x1b[?25h";
242 assert_eq!(String::from_utf8(live.writer().clone()).unwrap(), expected);
243 }
244
245 #[test]
246 fn auto_refresh_thread_produces_the_same_stream() {
247 let auto = Live::spawn(
252 Box::new(Text::new("frame one")),
253 console(),
254 Vec::<u8>::new(),
255 0.1,
256 );
257 auto.update(Box::new(Text::new("frame two")));
258 auto.update(Box::new(Text::new("frame three")));
259 let output = auto.stop();
260
261 let expected = "\x1b[?25lframe one\r\x1b[2Kframe two\r\x1b[2Kframe three\r\x1b[2Kframe three\n\x1b[?25h";
262 assert_eq!(String::from_utf8(output).unwrap(), expected);
263 }
264
265 #[test]
266 fn dropping_the_handle_finalizes_the_display() {
267 let console = console();
270 let auto = Live::spawn(Box::new(Text::new("only")), console, Vec::<u8>::new(), 0.1);
272 drop(auto); }
277}