use std::{
io::{self, Write},
time::Duration,
};
use futures_lite::StreamExt;
use tui::layout::Rect;
use crate::{
render::tui::{draw, ticker},
Root, Throughput, WeakRoot,
};
#[derive(Clone)]
pub struct Options {
pub title: String,
pub frames_per_second: f32,
pub throughput: bool,
pub recompute_column_width_every_nth_frame: Option<usize>,
pub window_size: Option<Rect>,
pub stop_if_progress_missing: bool,
}
impl Default for Options {
fn default() -> Self {
Options {
title: "Progress Dashboard".into(),
frames_per_second: 10.0,
throughput: false,
recompute_column_width_every_nth_frame: None,
window_size: None,
stop_if_progress_missing: true,
}
}
}
#[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 = "render-tui-crossterm", feature = "render-tui-termion")))]
compile_error!(
"Please set either the 'render-tui-crossterm' or 'render-tui-termion' feature whne using the 'render-tui'"
);
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: impl WeakRoot,
options: Options,
events: impl futures_core::Stream<Item = Event> + Send + Unpin,
) -> Result<impl std::future::Future<Output = ()>, std::io::Error> {
let Options {
title,
frames_per_second,
window_size,
recompute_column_width_every_nth_frame,
throughput,
stop_if_progress_missing,
} = 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()
};
if throughput {
state.throughput = Some(Throughput::default());
}
let mut interrupt_mode = InterruptDrawInfo::Instantly;
let (entries_cap, messages_cap) = progress
.upgrade()
.map(|p| (p.num_tasks(), p.messages_capacity()))
.unwrap_or_default();
let mut entries = Vec::with_capacity(entries_cap);
let mut messages = Vec::with_capacity(messages_cap);
let mut events = ticker(duration_per_frame)
.map(|_| Event::Tick)
.or(key_receive.map(Event::Input))
.or(events);
let mut tick = 0usize;
let store_task_size_every = recompute_column_width_every_nth_frame.unwrap_or(1).max(1);
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 {
tick += 1;
let progress = match progress.upgrade() {
Some(progress) => progress,
None if stop_if_progress_missing => break,
None => continue,
};
progress.sorted_snapshot(&mut entries);
if stop_if_progress_missing && 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: impl WeakRoot,
config: Options,
) -> Result<impl std::future::Future<Output = ()>, std::io::Error> {
render_with_input(out, progress, config, futures_lite::stream::pending())
}