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)>, messages: Vec, for_next_copy: Option, /// The size of the message origin, tracking the terminal height so things potentially off screen don't influence width anymore. message_origin_size: VecDeque, /// The maximum progress midpoint (point till progress bar starts) seen at the last tick last_progress_midpoint: Option, /// The amount of blocks per line we have written last time. blocks_per_line: VecDeque, pub throughput: Option, } impl State { pub(crate) fn update_from_progress(&mut self, progress: &impl Root) -> bool { let mut hasher = DefaultHasher::new(); self.tree.hash(&mut hasher); let prev_hash = hasher.finish(); 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(); prev_hash != cur_hash } pub(crate) fn clear(&mut self) { self.tree.clear(); self.messages.clear(); self.for_next_copy.take(); } } pub struct Options { pub level_filter: Option>, 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> = 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> = 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 { 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<'a>( p: &Value, style: Style, mut blocks_available: u16, colored: bool, buf: &mut Vec>, ) { 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!("{:=")); buf.push(styled_brush.style(style.dimmed()).paint(format!( "{:- { 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::(), ), ); } } 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, throughput: Option, buf: &mut Vec>, ) -> Option { 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 } } }