diff --git a/src/draw_target.rs b/src/draw_target.rs index b99b2396..2dae730e 100644 --- a/src/draw_target.rs +++ b/src/draw_target.rs @@ -12,7 +12,7 @@ use console::Term; use instant::Instant; use crate::multi::{MultiProgressAlignment, MultiState}; -use crate::TermLike; +use crate::{term_like, TermLike}; /// Target for draw operations /// @@ -71,9 +71,12 @@ impl ProgressDrawTarget { /// /// Will panic if `refresh_rate` is `0`. pub fn term(term: Term, refresh_rate: u8) -> Self { + let supports_ansi_codes = term.supports_ansi_codes(); + Self { kind: TargetKind::Term { term, + supports_ansi_codes, last_line_count: VisualLines::default(), rate_limiter: RateLimiter::new(refresh_rate), draw_state: DrawState::default(), @@ -83,9 +86,12 @@ impl ProgressDrawTarget { /// Draw to a boxed object that implements the [`TermLike`] trait. pub fn term_like(term_like: Box) -> Self { + let supports_ansi_codes = term_like.supports_ansi_codes(); + Self { kind: TargetKind::TermLike { inner: term_like, + supports_ansi_codes, last_line_count: VisualLines::default(), rate_limiter: None, draw_state: DrawState::default(), @@ -96,9 +102,12 @@ impl ProgressDrawTarget { /// Draw to a boxed object that implements the [`TermLike`] trait, /// with a specific refresh rate. pub fn term_like_with_hz(term_like: Box, refresh_rate: u8) -> Self { + let supports_ansi_codes = term_like.supports_ansi_codes(); + Self { kind: TargetKind::TermLike { inner: term_like, + supports_ansi_codes, last_line_count: VisualLines::default(), rate_limiter: Option::from(RateLimiter::new(refresh_rate)), draw_state: DrawState::default(), @@ -151,6 +160,7 @@ impl ProgressDrawTarget { match &mut self.kind { TargetKind::Term { term, + supports_ansi_codes, last_line_count, rate_limiter, draw_state, @@ -162,6 +172,7 @@ impl ProgressDrawTarget { match force_draw || rate_limiter.allow(now) { true => Some(Drawable::Term { term, + supports_ansi_codes: *supports_ansi_codes, last_line_count, draw_state, }), @@ -179,12 +190,14 @@ impl ProgressDrawTarget { } TargetKind::TermLike { inner, + supports_ansi_codes, last_line_count, rate_limiter, draw_state, } => match force_draw || rate_limiter.as_mut().map_or(true, |r| r.allow(now)) { true => Some(Drawable::TermLike { term_like: &**inner, + supports_ansi_codes: *supports_ansi_codes, last_line_count, draw_state, }), @@ -230,6 +243,7 @@ impl ProgressDrawTarget { enum TargetKind { Term { term: Term, + supports_ansi_codes: bool, last_line_count: VisualLines, rate_limiter: RateLimiter, draw_state: DrawState, @@ -241,6 +255,7 @@ enum TargetKind { Hidden, TermLike { inner: Box, + supports_ansi_codes: bool, last_line_count: VisualLines, rate_limiter: Option, draw_state: DrawState, @@ -270,6 +285,7 @@ impl TargetKind { pub(crate) enum Drawable<'a> { Term { term: &'a Term, + supports_ansi_codes: bool, last_line_count: &'a mut VisualLines, draw_state: &'a mut DrawState, }, @@ -281,6 +297,7 @@ pub(crate) enum Drawable<'a> { }, TermLike { term_like: &'a dyn TermLike, + supports_ansi_codes: bool, last_line_count: &'a mut VisualLines, draw_state: &'a mut DrawState, }, @@ -326,9 +343,10 @@ impl<'a> Drawable<'a> { match self { Drawable::Term { term, + supports_ansi_codes, last_line_count, draw_state, - } => draw_state.draw_to_term(term, last_line_count), + } => draw_state.draw_to_term(term, supports_ansi_codes, last_line_count), Drawable::Multi { mut state, force_draw, @@ -337,9 +355,10 @@ impl<'a> Drawable<'a> { } => state.draw(force_draw, None, now), Drawable::TermLike { term_like, + supports_ansi_codes, last_line_count, draw_state, - } => draw_state.draw_to_term(term_like, last_line_count), + } => draw_state.draw_to_term(term_like, supports_ansi_codes, last_line_count), } } } @@ -466,12 +485,20 @@ impl DrawState { fn draw_to_term( &mut self, term: &(impl TermLike + ?Sized), + supports_ansi_codes: bool, last_line_count: &mut VisualLines, ) -> io::Result<()> { if panicking() { return Ok(()); } + // Begin synchronized update + let sync_guard = if supports_ansi_codes { + Some(term_like::SyncGuard::begin_sync(term)?) + } else { + None + }; + if !self.lines.is_empty() && self.move_cursor { term.move_cursor_up(last_line_count.as_usize())?; } else { @@ -547,6 +574,11 @@ impl DrawState { } term.write_str(&" ".repeat(last_line_filler))?; + // End synchronized update + if let Some(sync_guard) = sync_guard { + sync_guard.finish_sync()?; + } + term.flush()?; *last_line_count = real_len - orphan_visual_line_count + shift; Ok(()) diff --git a/src/in_memory.rs b/src/in_memory.rs index 046ae14a..3286e4a2 100644 --- a/src/in_memory.rs +++ b/src/in_memory.rs @@ -13,14 +13,16 @@ use crate::TermLike; #[derive(Debug, Clone)] pub struct InMemoryTerm { state: Arc>, + supports_ansi_codes: bool, } impl InMemoryTerm { - pub fn new(rows: u16, cols: u16) -> InMemoryTerm { + pub fn new(rows: u16, cols: u16, supports_ansi_codes: bool) -> InMemoryTerm { assert!(rows > 0, "rows must be > 0"); assert!(cols > 0, "cols must be > 0"); InMemoryTerm { state: Arc::new(Mutex::new(InMemoryTermState::new(rows, cols))), + supports_ansi_codes, } } @@ -190,6 +192,10 @@ impl TermLike for InMemoryTerm { state.history.push(Move::Flush); state.parser.flush() } + + fn supports_ansi_codes(&self) -> bool { + self.supports_ansi_codes + } } struct InMemoryTermState { @@ -234,6 +240,8 @@ enum Move { #[cfg(test)] mod test { + use crate::term_like; + use super::*; fn cursor_pos(in_mem: &InMemoryTerm) -> (u16, u16) { @@ -248,7 +256,7 @@ mod test { #[test] fn line_wrapping() { - let in_mem = InMemoryTerm::new(10, 5); + let in_mem = InMemoryTerm::new(10, 5, false); assert_eq!(cursor_pos(&in_mem), (0, 0)); in_mem.write_str("ABCDE").unwrap(); @@ -282,7 +290,7 @@ mod test { #[test] fn write_line() { - let in_mem = InMemoryTerm::new(10, 5); + let in_mem = InMemoryTerm::new(10, 5, false); assert_eq!(cursor_pos(&in_mem), (0, 0)); in_mem.write_line("A").unwrap(); @@ -318,7 +326,7 @@ NewLine #[test] fn basic_functionality() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); in_mem.write_line("This is a test line").unwrap(); assert_eq!(in_mem.contents(), "This is a test line"); @@ -352,7 +360,7 @@ Str("TEST") #[test] fn newlines() { - let in_mem = InMemoryTerm::new(10, 10); + let in_mem = InMemoryTerm::new(10, 10, false); in_mem.write_line("LINE ONE").unwrap(); in_mem.write_line("LINE TWO").unwrap(); in_mem.write_line("").unwrap(); @@ -376,7 +384,7 @@ NewLine #[test] fn cursor_zero_movement() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); in_mem.write_line("LINE ONE").unwrap(); assert_eq!(cursor_pos(&in_mem), (1, 0)); @@ -396,4 +404,29 @@ NewLine in_mem.move_cursor_right(0).unwrap(); assert_eq!(cursor_pos(&in_mem), (1, 1)); } + + #[test] + fn sync_update() { + let in_mem = InMemoryTerm::new(10, 80, true); + assert_eq!(cursor_pos(&in_mem), (0, 0)); + + let sync_guard = term_like::SyncGuard::begin_sync(&in_mem).unwrap(); + in_mem.write_line("LINE ONE").unwrap(); + assert_eq!(cursor_pos(&in_mem), (1, 0)); + assert_eq!( + in_mem.moves_since_last_check(), + r#"Str("\u{1b}[?2026h") +Str("LINE ONE") +NewLine +"# + ); + + sync_guard.finish_sync().unwrap(); + assert_eq!(cursor_pos(&in_mem), (1, 0)); + assert_eq!( + in_mem.moves_since_last_check(), + r#"Str("\u{1b}[?2026l") +"# + ); + } } diff --git a/src/term_like.rs b/src/term_like.rs index b489b655..38133b10 100644 --- a/src/term_like.rs +++ b/src/term_like.rs @@ -1,3 +1,4 @@ +use std::cell::Cell; use std::fmt::Debug; use std::io; @@ -34,6 +35,9 @@ pub trait TermLike: Debug + Send + Sync { fn clear_line(&self) -> io::Result<()>; fn flush(&self) -> io::Result<()>; + + // Whether ANSI escape sequences are supported + fn supports_ansi_codes(&self) -> bool; } impl TermLike for Term { @@ -76,4 +80,38 @@ impl TermLike for Term { fn flush(&self) -> io::Result<()> { self.flush() } + + fn supports_ansi_codes(&self) -> bool { + self.features().colors_supported() + } +} + +pub(crate) struct SyncGuard<'a, T: TermLike + ?Sized> { + term_like: Cell>, +} + +impl<'a, T: TermLike + ?Sized> SyncGuard<'a, T> { + pub(crate) fn begin_sync(term_like: &'a T) -> io::Result { + term_like.write_str("\x1b[?2026h")?; + Ok(Self { + term_like: Cell::new(Some(term_like)), + }) + } + + pub(crate) fn finish_sync(self) -> io::Result<()> { + self.finish_sync_inner() + } + + fn finish_sync_inner(&self) -> io::Result<()> { + if let Some(term_like) = self.term_like.take() { + term_like.write_str("\x1b[?2026l")?; + } + Ok(()) + } +} + +impl Drop for SyncGuard<'_, T> { + fn drop(&mut self) { + let _ = self.finish_sync_inner(); + } } diff --git a/tests/render.rs b/tests/render.rs index bd275af7..ae97ef36 100644 --- a/tests/render.rs +++ b/tests/render.rs @@ -10,7 +10,7 @@ use pretty_assertions::assert_eq; #[test] fn basic_progress_bar() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let pb = ProgressBar::with_draw_target( Some(10), ProgressDrawTarget::term_like(Box::new(in_mem.clone())), @@ -39,7 +39,7 @@ fn basic_progress_bar() { #[test] fn progress_bar_builder_method_order() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); // Test that `with_style` doesn't overwrite the message or prefix let pb = ProgressBar::with_draw_target( Some(10), @@ -63,7 +63,7 @@ fn progress_bar_builder_method_order() { #[test] fn progress_bar_percent_with_no_length() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let pb = ProgressBar::with_draw_target( None, ProgressDrawTarget::term_like(Box::new(in_mem.clone())), @@ -96,7 +96,7 @@ fn progress_bar_percent_with_no_length() { #[test] fn multi_progress_single_bar_and_leave() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -119,7 +119,7 @@ fn multi_progress_single_bar_and_leave() { #[test] fn multi_progress_single_bar_and_clear() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -138,7 +138,7 @@ fn multi_progress_single_bar_and_clear() { } #[test] fn multi_progress_two_bars() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -182,7 +182,7 @@ fn multi_progress_two_bars() { #[test] fn multi_progress() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -247,7 +247,7 @@ fn multi_progress() { #[test] fn multi_progress_println() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -313,7 +313,7 @@ one last message"# #[test] fn multi_progress_suspend() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -386,7 +386,7 @@ Another line printed"# #[test] fn ticker_drop() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -411,7 +411,7 @@ fn ticker_drop() { #[test] fn manually_inc_ticker() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -433,7 +433,7 @@ fn manually_inc_ticker() { #[test] fn multi_progress_prune_zombies() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -472,7 +472,7 @@ fn multi_progress_prune_zombies() { #[test] fn multi_progress_prune_zombies_2() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -627,7 +627,7 @@ don't erase me either"# #[test] fn basic_tab_expansion() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -643,7 +643,7 @@ fn basic_tab_expansion() { #[test] fn tab_expansion_in_template() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -666,7 +666,7 @@ fn tab_expansion_in_template() { #[test] fn progress_style_tab_width_unification() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -687,7 +687,7 @@ fn progress_style_tab_width_unification() { #[test] fn multi_progress_clear_println() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -715,7 +715,7 @@ fn multi_progress_clear_zombies_two_ticks() { // In the old (broken) implementation, zombie handling sometimes worked differently depending on // how many draws were between certain operations. Let's make sure that doesn't happen again. fn _multi_progress_clear_zombies(ticks: usize) { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); let style = ProgressStyle::with_template("{msg}").unwrap(); @@ -751,7 +751,7 @@ fn _multi_progress_clear_zombies(ticks: usize) { // This test reproduces examples/multi.rs in a simpler form #[test] fn multi_zombie_handling() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); let style = ProgressStyle::with_template("{msg}").unwrap(); @@ -811,7 +811,7 @@ fn multi_zombie_handling() { #[test] fn multi_progress_multiline_msg() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -897,7 +897,7 @@ fn multi_progress_multiline_msg() { #[test] fn multi_progress_bottom_alignment() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); mp.set_alignment(MultiProgressAlignment::Bottom); @@ -921,7 +921,7 @@ fn multi_progress_bottom_alignment() { #[test] fn progress_bar_terminal_wrap() { use std::cmp::min; - let in_mem = InMemoryTerm::new(10, 20); + let in_mem = InMemoryTerm::new(10, 20, false); let mut downloaded = 0; let total_size = 231231231; @@ -984,7 +984,7 @@ s"# #[test] fn spinner_terminal_cleared_log_line_with_ansi_codes() { - let in_mem = InMemoryTerm::new(10, 100); + let in_mem = InMemoryTerm::new(10, 100, false); let pb = ProgressBar::with_draw_target( Some(10), @@ -1003,7 +1003,7 @@ fn spinner_terminal_cleared_log_line_with_ansi_codes() { #[test] fn multi_progress_println_terminal_wrap() { - let in_mem = InMemoryTerm::new(10, 48); + let in_mem = InMemoryTerm::new(10, 48, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -1067,7 +1067,7 @@ n terminal width"# #[test] fn basic_progress_bar_newline() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let pb = ProgressBar::with_draw_target( Some(10), ProgressDrawTarget::term_like(Box::new(in_mem.clone())), @@ -1106,7 +1106,7 @@ hello #[test] fn multi_progress_many_bars() { - let in_mem = InMemoryTerm::new(4, 80); + let in_mem = InMemoryTerm::new(4, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -1425,7 +1425,7 @@ Flush #[test] fn multi_progress_many_spinners() { - let in_mem = InMemoryTerm::new(4, 80); + let in_mem = InMemoryTerm::new(4, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); @@ -1765,7 +1765,7 @@ Flush #[test] fn orphan_lines() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let pb = ProgressBar::with_draw_target( Some(10), @@ -1788,7 +1788,7 @@ fn orphan_lines() { #[test] fn orphan_lines_message_above_progress_bar() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let pb = ProgressBar::with_draw_target( Some(10), @@ -1800,7 +1800,7 @@ fn orphan_lines_message_above_progress_bar() { #[test] fn orphan_lines_message_above_multi_progress_bar() { - let in_mem = InMemoryTerm::new(10, 80); + let in_mem = InMemoryTerm::new(10, 80, false); let mp = MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone())));