use crate::{
progress::{self, Key, Step, Task, Value},
render::tui::{
draw::State,
utils::{
block_width, draw_text_nowrap_fn, draw_text_with_ellipsis_nowrap, rect, sanitize_offset,
GraphemeCountWriter, VERTICAL_LINE,
},
InterruptDrawInfo,
},
time::format_now_datetime_seconds,
unit, Throughput,
};
use humantime::format_duration;
use std::{
fmt,
time::{Duration, SystemTime},
};
use tui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
};
use tui_react::fill_background;
const MIN_TREE_WIDTH: u16 = 20;
pub fn pane(entries: &[(Key, progress::Task)], mut bound: Rect, buf: &mut Buffer, state: &mut State) {
state.task_offset = sanitize_offset(state.task_offset, entries.len(), bound.height);
let needs_overflow_line =
if entries.len() > bound.height as usize || (state.task_offset).min(entries.len() as u16) > 0 {
bound.height = bound.height.saturating_sub(1);
true
} else {
false
};
state.task_offset = sanitize_offset(state.task_offset, entries.len(), bound.height);
if entries.is_empty() {
return;
}
let initial_column_width = bound.width / 3;
let desired_max_tree_draw_width = *state.next_tree_column_width.as_ref().unwrap_or(&initial_column_width);
{
if initial_column_width >= MIN_TREE_WIDTH {
let tree_bound = Rect {
width: desired_max_tree_draw_width,
..bound
};
let computed = draw_tree(entries, buf, tree_bound, state.task_offset);
state.last_tree_column_width = Some(computed);
} else {
state.last_tree_column_width = Some(0);
};
}
{
if let Some(tp) = state.throughput.as_mut() {
tp.update_elapsed();
}
let progress_area = rect::offset_x(bound, desired_max_tree_draw_width);
draw_progress(
entries,
buf,
progress_area,
state.task_offset,
state.throughput.as_mut(),
);
if let Some(tp) = state.throughput.as_mut() {
tp.reconcile(entries);
}
}
if needs_overflow_line {
let overflow_rect = Rect {
y: bound.height + 1,
height: 1,
..bound
};
draw_overflow(
entries,
buf,
overflow_rect,
desired_max_tree_draw_width,
bound.height,
state.task_offset,
);
}
}
pub(crate) fn headline(
entries: &[(Key, Task)],
interrupt_mode: InterruptDrawInfo,
duration_per_frame: Duration,
buf: &mut Buffer,
bound: Rect,
) {
let (num_running_tasks, num_blocked_tasks, num_groups) = entries.iter().fold(
(0, 0, 0),
|(mut running, mut blocked, mut groups), (_key, Task { progress, .. })| {
match progress.as_ref().map(|p| p.state) {
Some(progress::State::Running) => running += 1,
Some(progress::State::Blocked(_, _)) | Some(progress::State::Halted(_, _)) => blocked += 1,
None => groups += 1,
}
(running, blocked, groups)
},
);
let text = format!(
" {} {} {:3} running + {:3} blocked + {:3} groups = {} ",
match interrupt_mode {
InterruptDrawInfo::Instantly => "'q' or CTRL+c to quit",
InterruptDrawInfo::Deferred(interrupt_requested) => {
if interrupt_requested {
"interrupt requested - please wait"
} else {
"cannot interrupt current operation"
}
}
},
if duration_per_frame > Duration::from_secs(1) {
format!(
" Every {}s → {}",
duration_per_frame.as_secs(),
format_now_datetime_seconds()
)
} else {
"".into()
},
num_running_tasks,
num_blocked_tasks,
num_groups,
entries.len()
);
let bold = Style::default().add_modifier(Modifier::BOLD);
draw_text_with_ellipsis_nowrap(rect::snap_to_right(bound, block_width(&text) + 1), buf, text, bold);
}
struct ProgressFormat<'a>(&'a Option<Value>, u16, Option<unit::display::Throughput>);
impl<'a> fmt::Display for ProgressFormat<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Some(p) => match p.unit.as_ref() {
Some(unit) => write!(f, "{}", unit.display(p.step, p.done_at, self.2)),
None => match p.done_at {
Some(done_at) => write!(f, "{}/{}", p.step, done_at),
None => write!(f, "{}", p.step),
},
},
None => write!(f, "{:─<width$}", '─', width = self.1 as usize),
}
}
}
fn has_child(entries: &[(Key, Task)], index: usize) -> bool {
entries
.get(index + 1)
.and_then(|(other_key, other_val)| {
entries.get(index).map(|(cur_key, _)| {
cur_key.shares_parent_with(other_key, cur_key.level()) && other_val.progress.is_some()
})
})
.unwrap_or(false)
}
pub fn draw_progress(
entries: &[(Key, Task)],
buf: &mut Buffer,
bound: Rect,
offset: u16,
mut throughput: Option<&mut Throughput>,
) {
let title_spacing = 2u16 + 1;
let max_progress_label_width = entries
.iter()
.skip(offset as usize)
.take(bound.height as usize)
.map(|(_, Task { progress, .. })| progress)
.fold(0, |state, progress| match progress {
progress @ Some(_) => {
use std::io::Write;
let mut w = GraphemeCountWriter::default();
write!(w, "{}", ProgressFormat(progress, 0, None)).expect("never fails");
state.max(w.0)
}
None => state,
});
for (line, (entry_index, (key, Task { progress, name: title }))) in entries
.iter()
.enumerate()
.skip(offset as usize)
.take(bound.height as usize)
.enumerate()
{
let throughput = throughput
.as_mut()
.and_then(|tp| tp.update_and_get(key, progress.as_ref()));
let line_bound = rect::line_bound(bound, line);
let progress_text = format!(
" {progress}",
progress = ProgressFormat(
progress,
if has_child(entries, entry_index) {
bound.width.saturating_sub(title_spacing)
} else {
0
},
throughput
)
);
draw_text_with_ellipsis_nowrap(line_bound, buf, VERTICAL_LINE, None);
let tree_prefix = level_prefix(entries, entry_index);
let progress_rect = rect::offset_x(line_bound, block_width(&tree_prefix) as u16);
draw_text_with_ellipsis_nowrap(line_bound, buf, tree_prefix, None);
match progress.as_ref().map(|p| (p.fraction(), p.state, p.step)) {
Some((Some(fraction), state, _step)) => {
let mut progress_text = progress_text;
add_block_eta(state, &mut progress_text);
let (bound, style) = draw_progress_bar_fn(buf, progress_rect, fraction, |fraction| match state {
progress::State::Blocked(_, _) => Color::Red,
progress::State::Halted(_, _) => Color::LightRed,
progress::State::Running => {
if fraction >= 0.8 {
Color::Green
} else {
Color::Yellow
}
}
});
let style_fn = move |_t: &str, x: u16, _y: u16| {
if x < bound.right() {
style
} else {
Style::default()
}
};
draw_text_nowrap_fn(progress_rect, buf, progress_text, style_fn);
}
Some((None, state, step)) => {
let mut progress_text = progress_text;
add_block_eta(state, &mut progress_text);
draw_text_with_ellipsis_nowrap(progress_rect, buf, progress_text, None);
let bar_rect = rect::offset_x(line_bound, max_progress_label_width as u16);
draw_spinner(
buf,
bar_rect,
step,
line,
match state {
progress::State::Blocked(_, _) => Color::Red,
progress::State::Halted(_, _) => Color::LightRed,
progress::State::Running => Color::White,
},
);
}
None => {
let bold = Style::default().add_modifier(Modifier::BOLD);
draw_text_nowrap_fn(progress_rect, buf, progress_text, |_, _, _| Style::default());
draw_text_with_ellipsis_nowrap(progress_rect, buf, format!(" {} ", title), bold);
}
}
}
}
fn add_block_eta(state: progress::State, progress_text: &mut String) {
match state {
progress::State::Blocked(reason, maybe_eta) | progress::State::Halted(reason, maybe_eta) => {
progress_text.push_str(" [");
progress_text.push_str(reason);
progress_text.push_str("]");
if let Some(eta) = maybe_eta {
let now = SystemTime::now();
if eta > now {
progress_text.push_str(&format!(
" → {} to {}",
format_duration(eta.duration_since(now).expect("computation to work")),
if let progress::State::Blocked(_, _) = state {
"unblock"
} else {
"continue"
}
))
}
}
}
progress::State::Running => {}
}
}
fn draw_spinner(buf: &mut Buffer, bound: Rect, step: Step, seed: usize, color: Color) {
if bound.width == 0 {
return;
}
let step = step as usize;
let x = bound.x + ((step + seed) % bound.width as usize) as u16;
let width = 5;
let bound = rect::intersect(Rect { x, width, ..bound }, bound);
tui_react::fill_background(bound, buf, color);
}
fn draw_progress_bar_fn(
buf: &mut Buffer,
bound: Rect,
fraction: f32,
style: impl FnOnce(f32) -> Color,
) -> (Rect, Style) {
if bound.width == 0 {
return (Rect::default(), Style::default());
}
let mut fractional_progress_rect = Rect {
width: ((bound.width as f32 * fraction).floor() as u16).min(bound.width),
..bound
};
let color = style(fraction);
for y in fractional_progress_rect.top()..fractional_progress_rect.bottom() {
for x in fractional_progress_rect.left()..fractional_progress_rect.right() {
let cell = buf.get_mut(x, y);
cell.set_fg(color);
cell.set_symbol(tui::symbols::block::FULL);
}
}
if fractional_progress_rect.width < bound.width {
static BLOCK_SECTIONS: [&str; 9] = [
" ",
tui::symbols::block::ONE_EIGHTH,
tui::symbols::block::ONE_QUARTER,
tui::symbols::block::THREE_EIGHTHS,
tui::symbols::block::HALF,
tui::symbols::block::FIVE_EIGHTHS,
tui::symbols::block::THREE_QUARTERS,
tui::symbols::block::SEVEN_EIGHTHS,
tui::symbols::block::FULL,
];
let index = ((((bound.width as f32 * fraction) - fractional_progress_rect.width as f32) * 8f32).round()
as usize)
% BLOCK_SECTIONS.len();
let cell = buf.get_mut(fractional_progress_rect.right(), bound.y);
cell.set_symbol(BLOCK_SECTIONS[index]);
cell.set_fg(color);
fractional_progress_rect.width += 1;
}
(fractional_progress_rect, Style::default().bg(color).fg(Color::Black))
}
pub fn draw_tree(entries: &[(Key, Task)], buf: &mut Buffer, bound: Rect, offset: u16) -> u16 {
let mut max_prefix_len = 0;
for (line, (entry_index, entry)) in entries
.iter()
.enumerate()
.skip(offset as usize)
.take(bound.height as usize)
.enumerate()
{
let mut line_bound = rect::line_bound(bound, line);
line_bound.x = line_bound.x.saturating_sub(1);
line_bound.width = line_bound.width.saturating_sub(1);
let tree_prefix = format!("{} {} ", level_prefix(entries, entry_index), entry.1.name);
max_prefix_len = max_prefix_len.max(block_width(&tree_prefix));
let style = if entry.1.progress.is_none() {
Style::default().add_modifier(Modifier::BOLD).into()
} else {
None
};
draw_text_with_ellipsis_nowrap(line_bound, buf, tree_prefix, style);
}
max_prefix_len
}
fn level_prefix(entries: &[(Key, Task)], entry_index: usize) -> String {
let adj = Key::adjacency(entries, entry_index);
let key = entries[entry_index].0;
let key_level = key.level();
let is_orphan = adj.level() != key_level;
let mut buf = String::with_capacity(key_level as usize);
for level in 1..=key_level {
use crate::progress::key::SiblingLocation::*;
let is_child_level = level == key_level;
if level != 1 {
buf.push(' ');
}
if level == 1 && is_child_level {
buf.push(match adj[level] {
AboveAndBelow | Above => '├',
NotFound | Below => '│',
});
} else {
let c = if is_child_level {
match adj[level] {
NotFound => {
if is_orphan {
' '
} else {
'·'
}
}
Above => '└',
Below => '┌',
AboveAndBelow => '├',
}
} else {
match adj[level] {
NotFound => {
if level == 1 {
'│'
} else if is_orphan {
'·'
} else {
' '
}
}
Above => '└',
Below => '┌',
AboveAndBelow => '│',
}
};
buf.push(c)
}
}
buf
}
pub fn draw_overflow(
entries: &[(Key, Task)],
buf: &mut Buffer,
bound: Rect,
label_offset: u16,
num_entries_on_display: u16,
offset: u16,
) {
let (count, mut progress_fraction) = entries
.iter()
.take(offset as usize)
.chain(entries.iter().skip((offset + num_entries_on_display) as usize))
.fold((0usize, 0f32), |(count, progress_fraction), (_key, value)| {
let progress = value.progress.as_ref().and_then(|p| p.fraction()).unwrap_or_default();
(count + 1, progress_fraction + progress)
});
progress_fraction /= count as f32;
let label = format!(
"{} …{} skipped and {} more",
if label_offset == 0 { "" } else { VERTICAL_LINE },
offset,
entries
.len()
.saturating_sub((offset + num_entries_on_display + 1) as usize)
);
let (progress_rect, style) = draw_progress_bar_fn(buf, bound, progress_fraction, |_| Color::Green);
let bg_color = Color::Red;
fill_background(rect::offset_x(bound, progress_rect.right() - 1), buf, bg_color);
let color_text_according_to_progress = move |_g: &str, x: u16, _y: u16| {
if x < progress_rect.right() {
style
} else {
style.bg(bg_color)
}
};
draw_text_nowrap_fn(
rect::offset_x(bound, label_offset),
buf,
label,
color_text_according_to_progress,
);
let help_text = "⇊ = d|↓ = j|⇈ = u|↑ = k ";
draw_text_nowrap_fn(
rect::snap_to_right(bound, block_width(help_text)),
buf,
help_text,
color_text_according_to_progress,
);
}