| use std::{ |
| collections::{hash_map::DefaultHasher, VecDeque}, |
| hash::{Hash, Hasher}, |
| io, |
| ops::RangeInclusive, |
| sync::atomic::Ordering, |
| }; |
| |
| use crosstermion::{ |
| ansi_term::{ANSIString, ANSIStrings, Color, Style}, |
| color, |
| }; |
| use unicode_width::UnicodeWidthStr; |
| |
| use crate::{ |
| messages::{Message, MessageCopyState, MessageLevel}, |
| progress::{self, Value}, |
| unit, Root, Throughput, |
| }; |
| |
| #[derive(Default)] |
| pub struct State { |
| tree: Vec<(progress::Key, progress::Task)>, |
| tree_hash: u64, |
| messages: Vec<Message>, |
| for_next_copy: Option<MessageCopyState>, |
| /// The size of the message origin, tracking the terminal height so things potentially off screen don't influence width anymore. |
| message_origin_size: VecDeque<usize>, |
| /// The maximum progress midpoint (point till progress bar starts) seen at the last tick |
| last_progress_midpoint: Option<u16>, |
| /// The amount of blocks per line we have written last time. |
| blocks_per_line: VecDeque<u16>, |
| pub throughput: Option<Throughput>, |
| } |
| |
| impl State { |
| pub(crate) fn update_from_progress(&mut self, progress: &impl Root) -> bool { |
| progress.sorted_snapshot(&mut self.tree); |
| let mut hasher = DefaultHasher::new(); |
| self.tree.hash(&mut hasher); |
| let cur_hash = hasher.finish(); |
| |
| self.for_next_copy = progress |
| .copy_new_messages(&mut self.messages, self.for_next_copy.take()) |
| .into(); |
| let changed = self.tree_hash != cur_hash; |
| self.tree_hash = cur_hash; |
| changed |
| } |
| pub(crate) fn clear(&mut self) { |
| self.tree.clear(); |
| self.messages.clear(); |
| self.for_next_copy.take(); |
| } |
| } |
| |
| pub struct Options { |
| pub level_filter: Option<RangeInclusive<progress::key::Level>>, |
| pub terminal_dimensions: (u16, u16), |
| pub keep_running_if_progress_is_empty: bool, |
| pub output_is_terminal: bool, |
| pub colored: bool, |
| pub timestamp: bool, |
| pub hide_cursor: bool, |
| } |
| |
| fn messages( |
| out: &mut impl io::Write, |
| state: &mut State, |
| colored: bool, |
| max_height: usize, |
| timestamp: bool, |
| ) -> io::Result<()> { |
| let mut brush = color::Brush::new(colored); |
| fn to_color(level: MessageLevel) -> Color { |
| use crate::messages::MessageLevel::*; |
| match level { |
| Info => Color::White, |
| Success => Color::Green, |
| Failure => Color::Red, |
| } |
| } |
| let mut tokens: Vec<ANSIString<'_>> = Vec::with_capacity(6); |
| let mut current_maximum = state.message_origin_size.iter().max().cloned().unwrap_or(0); |
| for Message { |
| time, |
| level, |
| origin, |
| message, |
| } in &state.messages |
| { |
| tokens.clear(); |
| let blocks_drawn_during_previous_tick = state.blocks_per_line.pop_front().unwrap_or(0); |
| let message_block_len = origin.width(); |
| current_maximum = current_maximum.max(message_block_len); |
| if state.message_origin_size.len() == max_height { |
| state.message_origin_size.pop_front(); |
| } |
| state.message_origin_size.push_back(message_block_len); |
| |
| let color = to_color(*level); |
| tokens.push(" ".into()); |
| if timestamp { |
| tokens.push( |
| brush |
| .style(color.dimmed().on(Color::Yellow)) |
| .paint(crate::time::format_time_for_messages(*time)), |
| ); |
| tokens.push(Style::default().paint(" ")); |
| } else { |
| tokens.push("".into()); |
| }; |
| tokens.push(brush.style(Style::default().dimmed()).paint(format!( |
| "{:>fill_size$}{}", |
| "", |
| origin, |
| fill_size = current_maximum - message_block_len, |
| ))); |
| tokens.push(" ".into()); |
| tokens.push(brush.style(color.bold()).paint(message)); |
| let message_block_count = block_count_sans_ansi_codes(&tokens); |
| write!(out, "{}", ANSIStrings(tokens.as_slice()))?; |
| |
| if blocks_drawn_during_previous_tick > message_block_count { |
| newline_with_overdraw(out, &tokens, blocks_drawn_during_previous_tick)?; |
| } else { |
| writeln!(out)?; |
| } |
| } |
| Ok(()) |
| } |
| |
| pub fn all(out: &mut impl io::Write, show_progress: bool, state: &mut State, config: &Options) -> io::Result<()> { |
| if !config.keep_running_if_progress_is_empty && state.tree.is_empty() { |
| return Err(io::Error::new(io::ErrorKind::Other, "stop as progress is empty")); |
| } |
| messages( |
| out, |
| state, |
| config.colored, |
| config.terminal_dimensions.1 as usize, |
| config.timestamp, |
| )?; |
| |
| if show_progress && config.output_is_terminal { |
| if let Some(tp) = state.throughput.as_mut() { |
| tp.update_elapsed(); |
| } |
| let level_range = config |
| .level_filter |
| .clone() |
| .unwrap_or(RangeInclusive::new(0, progress::key::Level::max_value())); |
| let lines_to_be_drawn = state |
| .tree |
| .iter() |
| .filter(|(k, _)| level_range.contains(&k.level())) |
| .count(); |
| if state.blocks_per_line.len() < lines_to_be_drawn { |
| state.blocks_per_line.resize(lines_to_be_drawn, 0); |
| } |
| let mut tokens: Vec<ANSIString<'_>> = Vec::with_capacity(4); |
| let mut max_midpoint = 0; |
| for ((key, value), ref mut blocks_in_last_iteration) in state |
| .tree |
| .iter() |
| .filter(|(k, _)| level_range.contains(&k.level())) |
| .zip(state.blocks_per_line.iter_mut()) |
| { |
| max_midpoint = max_midpoint.max( |
| format_progress( |
| key, |
| value, |
| config.terminal_dimensions.0, |
| config.colored, |
| state.last_progress_midpoint, |
| state |
| .throughput |
| .as_mut() |
| .and_then(|tp| tp.update_and_get(key, value.progress.as_ref())), |
| &mut tokens, |
| ) |
| .unwrap_or(0), |
| ); |
| write!(out, "{}", ANSIStrings(tokens.as_slice()))?; |
| |
| **blocks_in_last_iteration = newline_with_overdraw(out, &tokens, **blocks_in_last_iteration)?; |
| } |
| if let Some(tp) = state.throughput.as_mut() { |
| tp.reconcile(&state.tree); |
| } |
| state.last_progress_midpoint = Some(max_midpoint); |
| // overwrite remaining lines that we didn't touch naturally |
| let lines_drawn = lines_to_be_drawn; |
| if state.blocks_per_line.len() > lines_drawn { |
| for blocks_in_last_iteration in state.blocks_per_line.iter().skip(lines_drawn) { |
| writeln!(out, "{:>width$}", "", width = *blocks_in_last_iteration as usize)?; |
| } |
| // Move cursor back to end of the portion we have actually drawn |
| crosstermion::execute!(out, crosstermion::cursor::MoveUp(state.blocks_per_line.len() as u16))?; |
| state.blocks_per_line.resize(lines_drawn, 0); |
| } else if lines_drawn > 0 { |
| crosstermion::execute!(out, crosstermion::cursor::MoveUp(lines_drawn as u16))?; |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Must be called directly after `tokens` were drawn, without newline. Takes care of adding the newline. |
| fn newline_with_overdraw( |
| out: &mut impl io::Write, |
| tokens: &[ANSIString<'_>], |
| blocks_in_last_iteration: u16, |
| ) -> io::Result<u16> { |
| let current_block_count = block_count_sans_ansi_codes(tokens); |
| if blocks_in_last_iteration > current_block_count { |
| // fill to the end of line to overwrite what was previously there |
| writeln!( |
| out, |
| "{:>width$}", |
| "", |
| width = (blocks_in_last_iteration - current_block_count) as usize |
| )?; |
| } else { |
| writeln!(out)?; |
| }; |
| Ok(current_block_count) |
| } |
| |
| fn block_count_sans_ansi_codes(strings: &[ANSIString<'_>]) -> u16 { |
| strings.iter().map(|s| s.width() as u16).sum() |
| } |
| |
| fn draw_progress_bar(p: &Value, style: Style, mut blocks_available: u16, colored: bool, buf: &mut Vec<ANSIString<'_>>) { |
| let mut brush = color::Brush::new(colored); |
| let styled_brush = brush.style(style); |
| |
| blocks_available = blocks_available.saturating_sub(3); // account for…I don't really know it's magic |
| buf.push(" [".into()); |
| match p.fraction() { |
| Some(mut fraction) => { |
| fraction = fraction.min(1.0); |
| blocks_available = blocks_available.saturating_sub(1); // account for '>' apparently |
| let progress_blocks = (blocks_available as f32 * fraction).floor() as usize; |
| buf.push(styled_brush.paint(format!("{:=<width$}", "", width = progress_blocks))); |
| buf.push(styled_brush.paint(">")); |
| buf.push(styled_brush.style(style.dimmed()).paint(format!( |
| "{:-<width$}", |
| "", |
| width = (blocks_available - progress_blocks as u16) as usize |
| ))); |
| } |
| None => { |
| const CHARS: [char; 6] = ['=', '=', '=', ' ', ' ', ' ']; |
| buf.push( |
| styled_brush.paint( |
| (p.step.load(Ordering::SeqCst)..std::usize::MAX) |
| .take(blocks_available as usize) |
| .map(|idx| CHARS[idx % CHARS.len()]) |
| .rev() |
| .collect::<String>(), |
| ), |
| ); |
| } |
| } |
| buf.push("]".into()); |
| } |
| |
| fn progress_style(p: &Value) -> Style { |
| use crate::progress::State::*; |
| match p.state { |
| Running => if let Some(fraction) = p.fraction() { |
| if fraction > 0.8 { |
| Color::Green |
| } else { |
| Color::Yellow |
| } |
| } else { |
| Color::White |
| } |
| .normal(), |
| Halted(_, _) => Color::Red.dimmed(), |
| Blocked(_, _) => Color::Red.normal(), |
| } |
| } |
| |
| fn format_progress<'a>( |
| key: &progress::Key, |
| value: &'a progress::Task, |
| column_count: u16, |
| colored: bool, |
| midpoint: Option<u16>, |
| throughput: Option<unit::display::Throughput>, |
| buf: &mut Vec<ANSIString<'a>>, |
| ) -> Option<u16> { |
| let mut brush = color::Brush::new(colored); |
| buf.clear(); |
| |
| buf.push(Style::new().paint(format!("{:>level$}", "", level = key.level() as usize))); |
| match value.progress.as_ref() { |
| Some(progress) => { |
| let style = progress_style(progress); |
| buf.push(brush.style(Color::Cyan.bold()).paint(&value.name)); |
| buf.push(" ".into()); |
| |
| let pre_unit = buf.len(); |
| let values_brush = brush.style(Style::new().bold().dimmed()); |
| match progress.unit.as_ref() { |
| Some(unit) => { |
| let mut display = unit.display(progress.step.load(Ordering::SeqCst), progress.done_at, throughput); |
| buf.push(values_brush.paint(display.values().to_string())); |
| buf.push(" ".into()); |
| buf.push(display.unit().to_string().into()); |
| } |
| None => { |
| buf.push(values_brush.paint(match progress.done_at { |
| Some(done_at) => format!("{}/{}", progress.step.load(Ordering::SeqCst), done_at), |
| None => format!("{}", progress.step.load(Ordering::SeqCst)), |
| })); |
| } |
| } |
| let desired_midpoint = block_count_sans_ansi_codes(buf.as_slice()); |
| let actual_midpoint = if let Some(midpoint) = midpoint { |
| let padding = midpoint.saturating_sub(desired_midpoint); |
| if padding > 0 { |
| buf.insert(pre_unit, " ".repeat(padding as usize).into()); |
| } |
| block_count_sans_ansi_codes(buf.as_slice()) |
| } else { |
| desired_midpoint |
| }; |
| let blocks_left = column_count.saturating_sub(actual_midpoint); |
| if blocks_left > 0 { |
| draw_progress_bar(progress, style, blocks_left, colored, buf); |
| } |
| Some(desired_midpoint) |
| } |
| None => { |
| // headline only - FIXME: would have to truncate it if it is too long for the line… |
| buf.push(brush.style(Color::White.bold()).paint(&value.name)); |
| None |
| } |
| } |
| } |