#[cfg(feature = "async")]
mod asynchronous;
#[cfg(feature = "async")]
pub use asynchronous::{AsyncRunner, Signal};
use std::io;
use std::ops::ControlFlow;
use std::time::{Duration, Instant};
use crossterm::event;
use ratatui_core::backend::Backend;
use ratatui_core::terminal::{Terminal, TerminalOptions};
use ratatui_crossterm::CrosstermBackend;
use crate::live::RedrawHandle;
use crate::screen::{ScreenMode, Scrollback, close_footer, pin_footer};
use crate::{Element, Event, TerminalSession, Theme, paint, translate_event};
#[derive(Clone, Copy, Debug)]
pub struct RunnerConfig {
pub tick_rate: Duration,
pub screen_mode: ScreenMode,
}
impl Default for RunnerConfig {
fn default() -> Self {
Self {
tick_rate: Duration::from_millis(100),
screen_mode: ScreenMode::default(),
}
}
}
pub struct Runner {
config: RunnerConfig,
redraw: RedrawHandle,
scrollback: Scrollback,
}
impl Runner {
pub fn new(mut config: RunnerConfig) -> Self {
config.tick_rate = config.tick_rate.max(Duration::from_millis(1));
Self {
config,
redraw: RedrawHandle::default(),
scrollback: Scrollback::new(),
}
}
pub fn scrollback(&self) -> Scrollback {
self.scrollback.clone()
}
pub fn redraw_handle(&self) -> RedrawHandle {
self.redraw.clone()
}
pub fn run(
&self,
theme: &Theme,
build: impl FnMut(u64) -> Element,
on_event: impl FnMut(Event) -> ControlFlow<()>,
) -> io::Result<()> {
self.run_with_backend(theme, CrosstermBackend::new(io::stdout()), build, on_event)
}
pub fn run_with_backend<B>(
&self,
theme: &Theme,
backend: B,
mut build: impl FnMut(u64) -> Element,
mut on_event: impl FnMut(Event) -> ControlFlow<()>,
) -> io::Result<()>
where
B: Backend<Error = io::Error>,
{
let mode = self.config.screen_mode;
let split = !mode.is_alternate();
let _session = TerminalSession::enter_with(mode)?;
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: mode.viewport(),
},
)?;
let mut frame = 0u64;
let mut last_draw = Instant::now();
self.redraw.request();
loop {
if split {
if self.scrollback.flush(&mut terminal, theme)? {
self.redraw.request();
}
} else {
self.scrollback.clear();
}
let due = last_draw.elapsed() >= self.config.tick_rate;
let requested = self.redraw.take();
if due || requested {
if split {
terminal.autoresize()?;
pin_footer(&mut terminal)?;
}
terminal.draw(|terminal_frame| {
let area = terminal_frame.area();
let root = build(frame);
paint(terminal_frame.buffer_mut(), area, theme, root.as_ref(), &[]);
})?;
frame = frame.wrapping_add(1);
last_draw = Instant::now();
}
let timeout = self.config.tick_rate.saturating_sub(last_draw.elapsed());
if event::poll(timeout)?
&& let Some(event) = translate_event(event::read()?)
&& on_event(event).is_break()
{
break;
}
}
if split {
let _ = close_footer(&mut terminal);
} else {
let _ = terminal.clear();
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_tick_rate_is_clamped() {
let runner = Runner::new(RunnerConfig {
tick_rate: Duration::ZERO,
..RunnerConfig::default()
});
assert_eq!(runner.config.tick_rate, Duration::from_millis(1));
}
#[test]
fn the_default_config_owns_the_alternate_screen() {
assert_eq!(RunnerConfig::default().screen_mode, ScreenMode::Alternate);
}
#[test]
fn the_scrollback_handle_shares_one_queue() {
let runner = Runner::new(RunnerConfig {
screen_mode: ScreenMode::split_footer(4),
..RunnerConfig::default()
});
let handle = runner.scrollback();
assert!(handle.is_empty());
handle.write(|_width| crate::element(crate::components::Text::raw("queued")));
assert!(
!runner.scrollback().is_empty(),
"every handle sees the same queue"
);
}
}