use crate::{tree::Root, tui::draw, tui::ticker};
use futures_util::StreamExt;
use std::{
io::{self, Write},
time::Duration,
};
use tui::layout::Rect;
#[derive(Clone)]
pub struct Options {
pub title: String,
pub frames_per_second: f32,
pub recompute_column_width_every_nth_frame: Option<usize>,
pub window_size: Option<Rect>,
pub redraw_only_on_state_change: bool,
pub stop_if_empty_progress: bool,
}
impl Default for Options {
fn default() -> Self {
Options {
title: "Progress Dashboard".into(),
frames_per_second: 10.0,
recompute_column_width_every_nth_frame: None,
window_size: None,
redraw_only_on_state_change: false,
stop_if_empty_progress: false,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Line {
Title(String),
Text(String),
}
#[derive(Debug, Clone, Copy)]
pub enum Interrupt {
Instantly,
Deferred,
}
#[derive(Clone, Copy)]
pub(crate) enum InterruptDrawInfo {
Instantly,
Deferred(bool),
}
#[cfg(not(any(feature = "tui-renderer-crossterm", feature = "tui-renderer-termion")))]
compile_error!(
"Please set either the 'tui-renderer-crossterm' or 'tui-renderer-termion' feature whne using the 'tui-renderer'"
);
use crosstermion::{
input::{key_input_stream, Key},
terminal::{tui::new_terminal, AlternateRawScreen},
};
#[derive(Debug, Clone)]
pub enum Event {
Tick,
Input(Key),
SetWindowSize(Rect),
SetTitle(String),
SetInformation(Vec<Line>),
SetInterruptMode(Interrupt),
}
pub fn render_with_input(
out: impl std::io::Write,
progress: Root,
options: Options,
events: impl futures_core::Stream<Item = Event> + Send,
) -> Result<impl std::future::Future<Output = ()>, std::io::Error> {
let Options {
title,
frames_per_second,
window_size,
recompute_column_width_every_nth_frame,
redraw_only_on_state_change,
stop_if_empty_progress,
} = options;
let mut terminal = new_terminal(AlternateRawScreen::try_from(out)?)?;
terminal.hide_cursor()?;
let duration_per_frame = Duration::from_secs_f32(1.0 / frames_per_second);
let key_receive = key_input_stream();
let render_fut = async move {
let mut state = draw::State {
title,
duration_per_frame,
..draw::State::default()
};
let mut interrupt_mode = InterruptDrawInfo::Instantly;
let mut entries = Vec::with_capacity(progress.num_tasks());
let mut messages = Vec::with_capacity(progress.messages_capacity());
let mut events = futures_util::stream::select_all(vec![
ticker(duration_per_frame).map(|_| Event::Tick).boxed(),
key_receive.map(Event::Input).boxed(),
events.boxed(),
]);
let mut tick = 0usize;
let store_task_size_every = recompute_column_width_every_nth_frame.unwrap_or(1).max(1);
let mut previous_root = None::<Root>;
let mut previous_state = None::<draw::State>;
while let Some(event) = events.next().await {
let mut skip_redraw = false;
match event {
Event::Tick => {}
Event::Input(key) => match key {
Key::Esc | Key::Char('q') | Key::Ctrl('c') | Key::Ctrl('[') => match interrupt_mode {
InterruptDrawInfo::Instantly => break,
InterruptDrawInfo::Deferred(_) => interrupt_mode = InterruptDrawInfo::Deferred(true),
},
Key::Char('`') => state.hide_messages = !state.hide_messages,
Key::Char('~') => state.messages_fullscreen = !state.messages_fullscreen,
Key::Char('J') => state.message_offset = state.message_offset.saturating_add(1),
Key::Char('D') => state.message_offset = state.message_offset.saturating_add(10),
Key::Char('j') => state.task_offset = state.task_offset.saturating_add(1),
Key::Char('d') => state.task_offset = state.task_offset.saturating_add(10),
Key::Char('K') => state.message_offset = state.message_offset.saturating_sub(1),
Key::Char('U') => state.message_offset = state.message_offset.saturating_sub(10),
Key::Char('k') => state.task_offset = state.task_offset.saturating_sub(1),
Key::Char('u') => state.task_offset = state.task_offset.saturating_sub(10),
Key::Char('[') => state.hide_info = !state.hide_info,
Key::Char('{') => state.maximize_info = !state.maximize_info,
_ => skip_redraw = true,
},
Event::SetWindowSize(bound) => state.user_provided_window_size = Some(bound),
Event::SetTitle(title) => state.title = title,
Event::SetInformation(info) => state.information = info,
Event::SetInterruptMode(mode) => {
interrupt_mode = match mode {
Interrupt::Instantly => {
if let InterruptDrawInfo::Deferred(true) = interrupt_mode {
break;
}
InterruptDrawInfo::Instantly
}
Interrupt::Deferred => InterruptDrawInfo::Deferred(match interrupt_mode {
InterruptDrawInfo::Deferred(interrupt_requested) => interrupt_requested,
_ => false,
}),
};
}
}
if !skip_redraw && redraw_only_on_state_change {
let (new_prev_state, state_changed) = match previous_state.take() {
Some(prev) if prev == state => (Some(prev), false),
None | Some(_) => (Some(state.clone()), true),
};
previous_state = new_prev_state;
if !state_changed {
previous_root = match previous_root.take() {
Some(prev) if prev.deep_eq(&progress) => {
skip_redraw = true;
Some(prev)
}
None | Some(_) => Some(progress.deep_clone()),
};
}
}
if !skip_redraw {
tick += 1;
progress.sorted_snapshot(&mut entries);
if stop_if_empty_progress && entries.is_empty() {
break;
}
let terminal_window_size = terminal.pre_render().expect("pre-render to work");
let window_size = state
.user_provided_window_size
.or(window_size)
.unwrap_or(terminal_window_size);
let buf = terminal.current_buffer_mut();
if !state.hide_messages {
progress.copy_messages(&mut messages);
}
draw::all(&mut state, interrupt_mode, &entries, &messages, window_size, buf);
if tick == 1 || tick % store_task_size_every == 0 || state.last_tree_column_width.unwrap_or(0) == 0 {
state.next_tree_column_width = state.last_tree_column_width;
}
terminal.post_render().expect("post render to work");
}
}
drop(terminal);
io::stdout().flush().ok();
};
Ok(render_fut)
}
pub fn render(
out: impl std::io::Write,
progress: Root,
config: Options,
) -> Result<impl std::future::Future<Output = ()>, std::io::Error> {
render_with_input(out, progress, config, futures_lite::stream::pending())
}