1mod error;
4mod painter;
5mod utils;
6
7pub use self::error::{Error, Result};
8
9use crossterm::{self, queue, QueueableCommand};
10use futures::stream::{Stream, StreamExt};
11use std::{
12 io::{self, BufWriter, Stdout, Write},
13 pin::Pin,
14 time::{Duration, Instant},
15};
16use tokio::{
17 self,
18 runtime::{Builder as RuntimeBuilder, Runtime},
19 sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
20};
21
22use self::{
23 painter::{FullPainter, IncrementalPainter, PaintOperation, Painter},
24 utils::MeteredWriter,
25};
26use zi::{
27 app::{App, ComponentMessage, MessageSender},
28 terminal::{Canvas, Colour, Key, Size, Style},
29 Layout,
30};
31
32pub fn incremental() -> Result<Crossterm<IncrementalPainter>> {
44 Crossterm::<IncrementalPainter>::new()
45}
46
47pub fn full() -> Result<Crossterm<FullPainter>> {
59 Crossterm::<FullPainter>::new()
60}
61
62pub struct Crossterm<PainterT: Painter = IncrementalPainter> {
74 target: MeteredWriter<BufWriter<Stdout>>,
75 painter: PainterT,
76 events: Option<EventStream>,
77 link: LinkChannel,
78}
79
80impl<PainterT: Painter> Crossterm<PainterT> {
81 pub fn new() -> Result<Self> {
87 let mut backend = Self {
88 target: MeteredWriter::new(BufWriter::with_capacity(1 << 20, io::stdout())),
89 painter: PainterT::create(
90 crossterm::terminal::size()
91 .map(|(width, height)| Size::new(width as usize, height as usize))?,
92 ),
93 events: Some(new_event_stream()),
94 link: LinkChannel::new(),
95 };
96 initialise_tty::<PainterT, _>(&mut backend.target)?;
97 Ok(backend)
98 }
99
100 pub fn run_event_loop(&mut self, layout: Layout) -> Result<()> {
115 let mut tokio_runtime = RuntimeBuilder::new_current_thread().enable_all().build()?;
116 let mut app = App::new(
117 UnboundedMessageSender(self.link.sender.clone()),
118 self.size()?,
119 layout,
120 );
121
122 while !app.poll_state().exit() {
123 let canvas = app.draw();
124
125 let last_drawn = Instant::now();
126 let num_bytes_presented = self.present(canvas)?;
127 let presented_time = last_drawn.elapsed();
128
129 log::debug!(
130 "Frame: pres {:.1}ms diff {}b",
131 presented_time.as_secs_f64() * 1000.0,
132 num_bytes_presented,
133 );
134
135 self.poll_events_batch(&mut tokio_runtime, &mut app, last_drawn)?;
136 }
137
138 Ok(())
139 }
140
141 #[inline]
147 pub fn suspend(&mut self) -> Result<()> {
148 self.events = None;
149 Ok(())
150 }
151
152 #[inline]
163 pub fn resume(&mut self) -> Result<()> {
164 self.painter = PainterT::create(self.size()?);
165 self.events = Some(new_event_stream());
166 initialise_tty::<PainterT, _>(&mut self.target)
167 }
168
169 #[inline]
171 fn poll_events_batch(
172 &mut self,
173 runtime: &mut Runtime,
174 app: &mut App,
175 last_drawn: Instant,
176 ) -> Result<()> {
177 let Self {
178 ref mut link,
179 ref mut events,
180 ..
181 } = *self;
182 let mut force_redraw = false;
183 let mut first_event_time: Option<Instant> = None;
184
185 while !force_redraw && !app.poll_state().exit() {
186 let timeout_duration = {
187 let since_last_drawn = last_drawn.elapsed();
188 if app.poll_state().dirty() && since_last_drawn >= REDRAW_LATENCY {
189 Duration::from_millis(0)
190 } else if app.poll_state().dirty() {
191 REDRAW_LATENCY - since_last_drawn
192 } else {
193 Duration::from_millis(if app.is_tickable() { 60 } else { 60_000 })
194 }
195 };
196 (runtime.block_on(async {
197 tokio::select! {
198 link_message = link.receiver.recv() => {
199 app.handle_message(
200 link_message.expect("at least one sender exists"),
201 );
202 Ok(())
203 }
204 input_event = events.as_mut().expect("backend events are suspended").next() => {
205 match input_event.expect(
206 "at least one sender exists",
207 )? {
208 FilteredEvent::Input(input_event) => app.handle_input(input_event),
209 FilteredEvent::Resize(size) => app.handle_resize(size),
210 };
211 force_redraw = app.poll_state().dirty()
212 && (first_event_time.get_or_insert_with(Instant::now).elapsed()
213 >= SUSTAINED_IO_REDRAW_LATENCY
214 || app.poll_state().resized());
215 Ok(())
216 }
217 _ = tokio::time::sleep(timeout_duration) => {
218 app.tick();
219 force_redraw = true;
220 Ok(())
221 }
222 }
223 }) as Result<()>)?;
224 }
225
226 Ok(())
227 }
228
229 #[inline]
231 fn size(&self) -> Result<Size> {
232 Ok(crossterm::terminal::size()
233 .map(|(width, height)| Size::new(width as usize, height as usize))?)
234 }
235
236 #[inline]
238 fn present(&mut self, canvas: &Canvas) -> Result<usize> {
239 let Self {
240 ref mut target,
241 ref mut painter,
242 ..
243 } = *self;
244 let initial_num_bytes_written = target.num_bytes_written();
245 painter.paint(canvas, |operation| {
246 match operation {
247 PaintOperation::WriteContent(grapheme) => {
248 queue!(target, crossterm::style::Print(grapheme))?
249 }
250 PaintOperation::SetStyle(style) => queue_set_style(target, style)?,
251 PaintOperation::MoveTo(position) => queue!(
252 target,
253 crossterm::cursor::MoveTo(position.x as u16, position.y as u16)
254 )?, }
256 Ok(())
257 })?;
258 target.flush()?;
259 Ok(target.num_bytes_written() - initial_num_bytes_written)
260 }
261}
262
263impl<PainterT: Painter> Drop for Crossterm<PainterT> {
264 fn drop(&mut self) {
265 queue!(
266 self.target,
267 crossterm::style::ResetColor,
268 crossterm::terminal::Clear(crossterm::terminal::ClearType::All),
269 crossterm::cursor::Show,
270 crossterm::terminal::LeaveAlternateScreen
271 )
272 .expect("Failed to clear screen when closing `crossterm` backend");
273 crossterm::terminal::disable_raw_mode()
274 .expect("Failed to disable raw mode when closing `crossterm` backend");
275 self.target
276 .flush()
277 .expect("Failed to flush when closing `crossterm` backend");
278 }
279}
280
281const REDRAW_LATENCY: Duration = Duration::from_millis(10);
282const SUSTAINED_IO_REDRAW_LATENCY: Duration = Duration::from_millis(100);
283
284struct LinkChannel {
285 sender: UnboundedSender<ComponentMessage>,
286 receiver: UnboundedReceiver<ComponentMessage>,
287}
288
289impl LinkChannel {
290 fn new() -> Self {
291 let (sender, receiver) = mpsc::unbounded_channel();
292 Self { sender, receiver }
293 }
294}
295
296#[derive(Debug, Clone)]
297struct UnboundedMessageSender(UnboundedSender<ComponentMessage>);
298
299impl MessageSender for UnboundedMessageSender {
300 fn send(&self, message: ComponentMessage) {
301 self.0
302 .send(message)
303 .map_err(|_| ()) .expect("App receiver needs to outlive senders for inter-component messages");
305 }
306
307 fn clone_box(&self) -> Box<dyn MessageSender> {
308 Box::new(self.clone())
309 }
310}
311
312#[inline]
313fn initialise_tty<PainterT: Painter, TargetT: Write>(target: &mut TargetT) -> Result<()> {
314 target
315 .queue(crossterm::terminal::EnterAlternateScreen)?
316 .queue(crossterm::cursor::Hide)?;
317 crossterm::terminal::enable_raw_mode()?;
318 queue_set_style(target, &PainterT::INITIAL_STYLE)?;
319 target.flush()?;
320 Ok(())
321}
322
323#[inline]
324fn queue_set_style(target: &mut impl Write, style: &Style) -> Result<()> {
325 use crossterm::style::{
326 Attribute, Color, SetAttribute, SetBackgroundColor, SetForegroundColor,
327 };
328
329 if style.bold {
331 queue!(target, SetAttribute(Attribute::Bold))?;
332 } else {
333 queue!(target, SetAttribute(Attribute::Reset))?;
338 }
339
340 if style.underline {
342 queue!(target, SetAttribute(Attribute::Underlined))?;
343 } else {
344 queue!(target, SetAttribute(Attribute::NoUnderline))?;
345 }
346
347 {
349 let Colour { red, green, blue } = style.background;
350 queue!(
351 target,
352 SetBackgroundColor(Color::Rgb {
353 r: red,
354 g: green,
355 b: blue
356 })
357 )?;
358 }
359
360 {
362 let Colour { red, green, blue } = style.foreground;
363 queue!(
364 target,
365 SetForegroundColor(Color::Rgb {
366 r: red,
367 g: green,
368 b: blue
369 })
370 )?;
371 }
372
373 Ok(())
374}
375
376enum FilteredEvent {
377 Input(zi::terminal::Event),
378 Resize(Size),
379}
380
381type EventStream = Pin<Box<dyn Stream<Item = Result<FilteredEvent>> + Send + 'static>>;
382
383#[inline]
384fn new_event_stream() -> EventStream {
385 Box::pin(
386 crossterm::event::EventStream::new()
387 .filter_map(|event| async move {
388 match event {
389 Ok(crossterm::event::Event::Key(key_event)) => Some(Ok(FilteredEvent::Input(
390 zi::terminal::Event::KeyPress(map_key(key_event)),
391 ))),
392 Ok(crossterm::event::Event::Resize(width, height)) => Some(Ok(
393 FilteredEvent::Resize(Size::new(width as usize, height as usize)),
394 )),
395 Ok(_) => None,
396 Err(error) => Some(Err(error.into())),
397 }
398 })
399 .fuse(),
400 )
401}
402
403#[inline]
404fn map_key(key: crossterm::event::KeyEvent) -> Key {
405 use crossterm::event::{KeyCode, KeyModifiers};
406 match key.code {
407 KeyCode::Backspace => Key::Backspace,
408 KeyCode::Left => Key::Left,
409 KeyCode::Right => Key::Right,
410 KeyCode::Up => Key::Up,
411 KeyCode::Down => Key::Down,
412 KeyCode::Home => Key::Home,
413 KeyCode::End => Key::End,
414 KeyCode::PageUp => Key::PageUp,
415 KeyCode::PageDown => Key::PageDown,
416 KeyCode::BackTab => Key::BackTab,
417 KeyCode::Delete => Key::Delete,
418 KeyCode::Insert => Key::Insert,
419 KeyCode::F(u8) => Key::F(u8),
420 KeyCode::Null => Key::Null,
421 KeyCode::Esc => Key::Esc,
422 KeyCode::Char(char) if key.modifiers.contains(KeyModifiers::CONTROL) => Key::Ctrl(char),
423 KeyCode::Char(char) if key.modifiers.contains(KeyModifiers::ALT) => Key::Alt(char),
424 KeyCode::Char(char) => Key::Char(char),
425 KeyCode::Enter => Key::Char('\n'),
426 KeyCode::Tab => Key::Char('\t'),
427 }
428}