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

synchronized output #618

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 35 additions & 3 deletions src/draw_target.rs
Expand Up @@ -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
///
Expand Down Expand Up @@ -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(),
Expand All @@ -83,9 +86,12 @@ impl ProgressDrawTarget {

/// Draw to a boxed object that implements the [`TermLike`] trait.
pub fn term_like(term_like: Box<dyn TermLike>) -> 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(),
Expand All @@ -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<dyn TermLike>, 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(),
Expand Down Expand Up @@ -151,6 +160,7 @@ impl ProgressDrawTarget {
match &mut self.kind {
TargetKind::Term {
term,
supports_ansi_codes,
last_line_count,
rate_limiter,
draw_state,
Expand All @@ -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,
}),
Expand All @@ -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,
}),
Expand Down Expand Up @@ -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,
Expand All @@ -241,6 +255,7 @@ enum TargetKind {
Hidden,
TermLike {
inner: Box<dyn TermLike>,
supports_ansi_codes: bool,
last_line_count: VisualLines,
rate_limiter: Option<RateLimiter>,
draw_state: DrawState,
Expand Down Expand Up @@ -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,
},
Expand All @@ -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,
},
Expand Down Expand Up @@ -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,
Expand All @@ -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),
}
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(())
Expand Down
45 changes: 39 additions & 6 deletions src/in_memory.rs
Expand Up @@ -13,14 +13,16 @@ use crate::TermLike;
#[derive(Debug, Clone)]
pub struct InMemoryTerm {
state: Arc<Mutex<InMemoryTermState>>,
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,
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -234,6 +240,8 @@ enum Move {

#[cfg(test)]
mod test {
use crate::term_like;

use super::*;

fn cursor_pos(in_mem: &InMemoryTerm) -> (u16, u16) {
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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();
Expand All @@ -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));

Expand All @@ -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")
"#
);
}
}
38 changes: 38 additions & 0 deletions src/term_like.rs
@@ -1,3 +1,4 @@
use std::cell::Cell;
use std::fmt::Debug;
use std::io;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Option<&'a T>>,
}

impl<'a, T: TermLike + ?Sized> SyncGuard<'a, T> {
pub(crate) fn begin_sync(term_like: &'a T) -> io::Result<Self> {
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<T: TermLike + ?Sized> Drop for SyncGuard<'_, T> {
fn drop(&mut self) {
let _ = self.finish_sync_inner();
}
}