Skip to content

Commit

Permalink
synchronized output
Browse files Browse the repository at this point in the history
  • Loading branch information
Funami580 committed Jan 8, 2024
1 parent 9b533cc commit 00c7aa2
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 39 deletions.
38 changes: 35 additions & 3 deletions src/draw_target.rs
Expand Up @@ -10,7 +10,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 @@ -69,9 +69,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: 0,
rate_limiter: RateLimiter::new(refresh_rate),
draw_state: DrawState::default(),
Expand All @@ -81,9 +84,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: 0,
rate_limiter: None,
draw_state: DrawState::default(),
Expand All @@ -94,9 +100,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: 0,
rate_limiter: Option::from(RateLimiter::new(refresh_rate)),
draw_state: DrawState::default(),
Expand Down Expand Up @@ -149,6 +158,7 @@ impl ProgressDrawTarget {
match &mut self.kind {
TargetKind::Term {
term,
supports_ansi_codes,
last_line_count,
rate_limiter,
draw_state,
Expand All @@ -160,6 +170,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 @@ -177,12 +188,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 @@ -228,6 +241,7 @@ impl ProgressDrawTarget {
enum TargetKind {
Term {
term: Term,
supports_ansi_codes: bool,
last_line_count: usize,
rate_limiter: RateLimiter,
draw_state: DrawState,
Expand All @@ -239,6 +253,7 @@ enum TargetKind {
Hidden,
TermLike {
inner: Box<dyn TermLike>,
supports_ansi_codes: bool,
last_line_count: usize,
rate_limiter: Option<RateLimiter>,
draw_state: DrawState,
Expand Down Expand Up @@ -268,6 +283,7 @@ impl TargetKind {
pub(crate) enum Drawable<'a> {
Term {
term: &'a Term,
supports_ansi_codes: bool,
last_line_count: &'a mut usize,
draw_state: &'a mut DrawState,
},
Expand All @@ -279,6 +295,7 @@ pub(crate) enum Drawable<'a> {
},
TermLike {
term_like: &'a dyn TermLike,
supports_ansi_codes: bool,
last_line_count: &'a mut usize,
draw_state: &'a mut DrawState,
},
Expand Down Expand Up @@ -324,9 +341,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 @@ -335,9 +353,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 @@ -464,12 +483,20 @@ impl DrawState {
fn draw_to_term(
&mut self,
term: &(impl TermLike + ?Sized),
supports_ansi_codes: bool,
last_line_count: &mut usize,
) -> 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)?;
} else {
Expand Down Expand Up @@ -542,6 +569,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();
}
}

0 comments on commit 00c7aa2

Please sign in to comment.