Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bar-less text output #659

Merged
merged 15 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 104 additions & 66 deletions src/draw_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ pub(crate) enum LineAdjust {

pub(crate) struct DrawStateWrapper<'a> {
state: &'a mut DrawState,
orphan_lines: Option<&'a mut Vec<String>>,
orphan_lines: Option<&'a mut Vec<LineType>>,
}

impl<'a> DrawStateWrapper<'a> {
Expand All @@ -373,7 +373,7 @@ impl<'a> DrawStateWrapper<'a> {
}
}

pub(crate) fn for_multi(state: &'a mut DrawState, orphan_lines: &'a mut Vec<String>) -> Self {
pub(crate) fn for_multi(state: &'a mut DrawState, orphan_lines: &'a mut Vec<LineType>) -> Self {
Self {
state,
orphan_lines: Some(orphan_lines),
Expand All @@ -398,8 +398,13 @@ impl std::ops::DerefMut for DrawStateWrapper<'_> {
impl Drop for DrawStateWrapper<'_> {
fn drop(&mut self) {
if let Some(orphaned) = &mut self.orphan_lines {
orphaned.extend(self.state.lines.drain(..self.state.orphan_lines_count));
self.state.orphan_lines_count = 0;
orphaned.extend(
spoutn1k marked this conversation as resolved.
Show resolved Hide resolved
self.state
.lines
.iter()
.filter(|l| matches!(l, LineType::Text(_) | LineType::Empty))
.cloned(),
);
}
}
}
Expand Down Expand Up @@ -460,34 +465,34 @@ const MAX_BURST: u8 = 20;
#[derive(Clone, Debug, Default)]
pub(crate) struct DrawState {
/// The lines to print (can contain ANSI codes)
pub(crate) lines: Vec<String>,
/// The number [`Self::lines`] entries that shouldn't be reaped by the next tick.
///
/// Note that this number may be different than the number of visual lines required to draw [`Self::lines`].
pub(crate) orphan_lines_count: usize,
pub(crate) lines: Vec<LineType>,
/// True if we should move the cursor up when possible instead of clearing lines.
pub(crate) move_cursor: bool,
/// Controls how the multi progress is aligned if some of its progress bars get removed, default is `Top`
pub(crate) alignment: MultiProgressAlignment,
}

impl DrawState {
/// Draw the current state to the terminal
/// We expect a few things:
/// - self.lines contains n lines of text/empty then m lines of bars
/// - None of those lines contain newlines
fn draw_to_term(
&mut self,
term: &(impl TermLike + ?Sized),
last_line_count: &mut VisualLines,
bar_count: &mut VisualLines, // The number of dynamic lines printed at the previous tick
) -> io::Result<()> {
if panicking() {
return Ok(());
}

if !self.lines.is_empty() && self.move_cursor {
// Move up to first line (assuming the last line doesn't contain a '\n') and then move to then front of the line
term.move_cursor_up(last_line_count.as_usize().saturating_sub(1))?;
term.move_cursor_up(bar_count.as_usize().saturating_sub(1))?;
term.write_str("\r")?;
} else {
// Fork of console::clear_last_lines that assumes that the last line doesn't contain a '\n'
let n = last_line_count.as_usize();
let n = bar_count.as_usize();
term.move_cursor_up(n.saturating_sub(1))?;
for i in 0..n {
term.clear_line()?;
Expand All @@ -498,11 +503,16 @@ impl DrawState {
term.move_cursor_up(n.saturating_sub(1))?;
}

let width = term.width() as usize;
let visual_lines = self.visual_line_count(.., width);
let term_width = term.width() as usize;

// Here we calculate the terminal vertical real estate that the state requires
let full_height = self.visual_line_count(.., term_width);

let shift = match self.alignment {
MultiProgressAlignment::Bottom if visual_lines < *last_line_count => {
let shift = *last_line_count - visual_lines;
// If we align to the bottom and the new height is less than before, clear the lines
// that are not used by the new content.
MultiProgressAlignment::Bottom if full_height < *bar_count => {
let shift = *bar_count - full_height;
for _ in 0..shift.as_usize() {
term.write_line("")?;
}
Expand All @@ -511,66 +521,53 @@ impl DrawState {
_ => VisualLines::default(),
};

let term_height = term.height() as usize;
let term_width = term.width() as usize;
let len = self.lines.len();
debug_assert!(self.orphan_lines_count <= self.lines.len());
let orphan_visual_line_count =
self.visual_line_count(..self.orphan_lines_count, term_width);
let mut real_len = VisualLines::default();
let mut last_line_filler = 0;
// Accumulate the displayed height in here. This differs from `full_height` in that it will
// accurately reflect the number of lines that have been displayed on the terminal, if the
// full height exceeds the terminal height.
let mut real_height = VisualLines::default();

for (idx, line) in self.lines.iter().enumerate() {
let line_width = console::measure_text_width(line);
let diff = if line.is_empty() {
// Empty line are new line
1
} else {
// Calculate real length based on terminal width
// This take in account linewrap from terminal
let terminal_len = (line_width as f64 / term_width as f64).ceil() as usize;

// If the line is effectively empty (for example when it consists
// solely of ANSI color code sequences, count it the same as a
// new line. If the line is measured to be len = 0, we will
// subtract with overflow later.
usize::max(terminal_len, 1)
}
.into();
// Have all orphan lines been drawn?
if self.orphan_lines_count <= idx {
// If so, then `real_len` should be at least `orphan_visual_line_count`.
debug_assert!(orphan_visual_line_count <= real_len);
// Don't consider orphan lines when comparing to terminal height.
if real_len - orphan_visual_line_count + diff > term_height.into() {
let line_height = line.wrapped_height(term_width);

// Check here for bar lines that exceed the terminal height
if matches!(line, LineType::Bar(_)) {
// Stop here if printing this bar would exceed the terminal height
if real_height + line_height > term.height().into() {
break;
}

real_height += line_height;
}
real_len += diff;

// Print a new line if this is not the first line printed this tick
// the first line will automatically wrap due to the filler below
if idx != 0 {
term.write_line("")?;
}
term.write_str(line)?;
if idx + 1 == len {
// Keep the cursor on the right terminal side
// So that next user writes/prints will happen on the next line
last_line_filler = term_width.saturating_sub(line_width);

term.write_str(line.as_ref())?;

if idx + 1 == self.lines.len() {
// For the last line of the output, keep the cursor on the right terminal
// side so that next user writes/prints will happen on the next line
let last_line_filler = line_height.as_usize() * term_width - line.console_width();
term.write_str(&" ".repeat(last_line_filler))?;
}
}
term.write_str(&" ".repeat(last_line_filler))?;

term.flush()?;
*last_line_count = real_len - orphan_visual_line_count + shift;
*bar_count = real_height + shift;

Ok(())
}

fn reset(&mut self) {
self.lines.clear();
self.orphan_lines_count = 0;
}

pub(crate) fn visual_line_count(
&self,
range: impl SliceIndex<[String], Output = [String]>,
range: impl SliceIndex<[LineType], Output = [LineType]>,
width: usize,
) -> VisualLines {
visual_line_count(&self.lines[range], width)
Expand Down Expand Up @@ -624,21 +621,55 @@ impl Sub for VisualLines {

/// Calculate the number of visual lines in the given lines, after
/// accounting for line wrapping and non-printable characters.
pub(crate) fn visual_line_count(lines: &[impl AsRef<str>], width: usize) -> VisualLines {
let mut real_lines = 0;
for line in lines {
let effective_line_length = console::measure_text_width(line.as_ref());
real_lines += usize::max(
(effective_line_length as f64 / width as f64).ceil() as usize,
1,
);
pub(crate) fn visual_line_count(lines: &[LineType], width: usize) -> VisualLines {
lines.iter().fold(VisualLines::default(), |acc, line| {
acc.saturating_add(line.wrapped_height(width))
djc marked this conversation as resolved.
Show resolved Hide resolved
})
}

#[derive(Clone, Debug)]
pub(crate) enum LineType {
Text(String),
Bar(String),
Empty,
}

impl LineType {
fn wrapped_height(&self, width: usize) -> VisualLines {
// Calculate real length based on terminal width
// This take in account linewrap from terminal
let terminal_len = (self.console_width() as f64 / width as f64).ceil() as usize;

// If the line is effectively empty (for example when it consists
// solely of ANSI color code sequences, count it the same as a
// new line. If the line is measured to be len = 0, we will
// subtract with overflow later.
usize::max(terminal_len, 1).into()
}

fn console_width(&self) -> usize {
console::measure_text_width(self.as_ref())
}
}

real_lines.into()
impl AsRef<str> for LineType {
fn as_ref(&self) -> &str {
match self {
LineType::Text(s) | LineType::Bar(s) => s,
LineType::Empty => "",
}
}
}

impl PartialEq<str> for LineType {
fn eq(&self, other: &str) -> bool {
self.as_ref() == other
}
}

#[cfg(test)]
mod tests {
use crate::draw_target::LineType;
use crate::{MultiProgress, ProgressBar, ProgressDrawTarget};

#[test]
Expand Down Expand Up @@ -732,7 +763,14 @@ mod tests {
];

for case in lines_and_expectations.iter() {
let result = super::visual_line_count(case.lines, case.width);
let result = super::visual_line_count(
&case
.lines
.iter()
.map(|s| LineType::Text(s.to_string()))
.collect::<Vec<_>>(),
case.width,
);
assert_eq!(result, case.expectation.into(), "case: {:?}", case);
}
}
Expand Down
15 changes: 7 additions & 8 deletions src/multi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use std::thread::panicking;
use std::time::Instant;

use crate::draw_target::{
visual_line_count, DrawState, DrawStateWrapper, LineAdjust, ProgressDrawTarget, VisualLines,
visual_line_count, DrawState, DrawStateWrapper, LineAdjust, LineType, ProgressDrawTarget,
VisualLines,
};
use crate::progress_bar::ProgressBar;
#[cfg(target_arch = "wasm32")]
Expand Down Expand Up @@ -217,7 +218,7 @@ pub(crate) struct MultiState {
alignment: MultiProgressAlignment,
/// Lines to be drawn above everything else in the MultiProgress. These specifically come from
/// calling `ProgressBar::println` on a pb that is connected to a `MultiProgress`.
orphan_lines: Vec<String>,
orphan_lines: Vec<LineType>,
/// The count of currently visible zombie lines.
zombie_lines_count: VisualLines,
}
Expand Down Expand Up @@ -267,7 +268,7 @@ impl MultiState {
pub(crate) fn draw(
&mut self,
mut force_draw: bool,
extra_lines: Option<Vec<String>>,
extra_lines: Option<Vec<LineType>>,
now: Instant,
) -> io::Result<()> {
if panicking() {
Expand Down Expand Up @@ -326,12 +327,10 @@ impl MultiState {
};

let mut draw_state = drawable.state();
draw_state.orphan_lines_count = self.orphan_lines.len();
draw_state.alignment = self.alignment;

if let Some(extra_lines) = &extra_lines {
draw_state.lines.extend_from_slice(extra_lines.as_slice());
draw_state.orphan_lines_count += extra_lines.len();
}

// Add lines from `ProgressBar::println` call.
Expand Down Expand Up @@ -365,9 +364,9 @@ impl MultiState {
let msg = msg.as_ref();

// If msg is "", make sure a line is still printed
let lines: Vec<String> = match msg.is_empty() {
false => msg.lines().map(Into::into).collect(),
true => vec![String::new()],
let lines: Vec<LineType> = match msg.is_empty() {
false => msg.lines().map(|l| LineType::Text(Into::into(l))).collect(),
true => vec![LineType::Empty],
};

self.draw(true, Some(lines), now)
Expand Down
7 changes: 3 additions & 4 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::time::Instant;
use instant::Instant;
use portable_atomic::{AtomicU64, AtomicU8, Ordering};

use crate::draw_target::ProgressDrawTarget;
use crate::draw_target::{LineType, ProgressDrawTarget};
use crate::style::ProgressStyle;

pub(crate) struct BarState {
Expand Down Expand Up @@ -144,15 +144,14 @@ impl BarState {
};

let mut draw_state = drawable.state();
let lines: Vec<String> = msg.lines().map(Into::into).collect();
let lines: Vec<LineType> = msg.lines().map(|l| LineType::Text(Into::into(l))).collect();
// Empty msg should trigger newline as we are in println
if lines.is_empty() {
draw_state.lines.push(String::new());
draw_state.lines.push(LineType::Empty);
} else {
draw_state.lines.extend(lines);
}

draw_state.orphan_lines_count = draw_state.lines.len();
if let Some(width) = width {
if !matches!(self.state.status, Status::DoneHidden) {
self.style
Expand Down
10 changes: 6 additions & 4 deletions src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use instant::Instant;
#[cfg(feature = "unicode-segmentation")]
use unicode_segmentation::UnicodeSegmentation;

use crate::draw_target::LineType;
use crate::format::{
BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, HumanCount, HumanDuration,
HumanFloatCount,
Expand Down Expand Up @@ -226,7 +227,7 @@ impl ProgressStyle {
pub(crate) fn format_state(
&self,
state: &ProgressState,
lines: &mut Vec<String>,
lines: &mut Vec<LineType>,
target_width: u16,
) {
let mut cur = String::new();
Expand Down Expand Up @@ -374,9 +375,10 @@ impl ProgressStyle {
}
}

/// This is used exclusively to add the bars built above to the lines to print
fn push_line(
&self,
lines: &mut Vec<String>,
lines: &mut Vec<LineType>,
cur: &mut String,
state: &ProgressState,
buf: &mut String,
Expand All @@ -394,11 +396,11 @@ impl ProgressStyle {
for (i, line) in expanded.split('\n').enumerate() {
// No newlines found in this case
if i == 0 && line.len() == expanded.len() {
lines.push(expanded);
lines.push(LineType::Bar(expanded));
break;
}

lines.push(line.to_string());
lines.push(LineType::Bar(line.to_string()));
}
}
}
Expand Down
Loading