Skip to content

Commit

Permalink
Don't redraw identical text (#5)
Browse files Browse the repository at this point in the history
Fixes #4
  • Loading branch information
sourcefrog authored Jul 26, 2022
1 parent 743d7c5 commit d14c2ba
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 14 deletions.
27 changes: 27 additions & 0 deletions examples/identical_updates_suppressed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! This model repeatedly generates the same text: it's a counter that
//! only shows the hundreds.
//!
//! Nutmeg avoids redrawing the bar on every update to avoid flickering
//! (on terminals that don't handle this well themselves.)
use std::thread::sleep;
use std::time::Duration;

struct Model {
i: usize,
}

impl nutmeg::Model for Model {
fn render(&mut self, _width: usize) -> String {
format!("count: {}", self.i / 100)
}
}

fn main() {
let options = nutmeg::Options::default();
let view = nutmeg::View::new(Model { i: 0 }, options);
for _i in 1..=5000 {
view.update(|state| state.i += 1);
sleep(Duration::from_millis(5));
}
}
51 changes: 37 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ Not released yet.
* New: Output can be captured for inspection in tests using [Options::destination],
[Destination::Capture], and [View::captured_output].
* Improved: Nutmeg avoids redrawing if the model renders identical output to what
is already displayed, to avoid flicker.
## 0.1.1
Released 2022-03-22
Expand Down Expand Up @@ -636,19 +639,22 @@ struct InnerView<M: Model> {
capture_buffer: Option<Arc<Mutex<String>>>,
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[derive(Debug, PartialEq, Eq, Clone)]
enum State {
/// Progress is not visible and nothing was recently printed.
None,
/// Progress bar is currently displayed.
ProgressDrawn {
since: Instant,
/// Last time it was drawn.
last_drawn_time: Instant,
/// Number of lines the cursor is below the line where the progress bar
/// should next be drawn.
cursor_y: usize,
/// The rendered string last drawn.
last_drawn_string: String,
},
/// Messages were written, and the progress bar is not visible.
Printed { since: Instant },
Printed { last_printed: Instant },
/// An incomplete message line has been printed, so the progress bar can't
/// be drawn until it's removed.
IncompleteLine,
Expand Down Expand Up @@ -710,31 +716,48 @@ impl<M: Model> InnerView<M> {
match self.state {
State::IncompleteLine => return Ok(()),
State::None => (),
State::Printed { since } => {
if now - since < self.options.print_holdoff {
State::Printed { last_printed } => {
if now - last_printed < self.options.print_holdoff {
return Ok(());
}
}
State::ProgressDrawn { since, .. } => {
if now - since < self.options.update_interval {
State::ProgressDrawn {
last_drawn_time, ..
} => {
if now - last_drawn_time < self.options.update_interval {
return Ok(());
}
}
}
if let Some(width) = self.width_strategy.width() {
let rendered = self.model.render(width);
let rendered = rendered.strip_suffix('\n').unwrap_or(&rendered);
let mut rendered = self.model.render(width);
if rendered.ends_with('\n') {
// Handle models that incorrectly add a trailing newline, rather than
// leaving a blank line. (Maybe we should just let them fix it, and
// be simpler?)
rendered.pop();
}
let mut buf = String::new();
if let State::ProgressDrawn { cursor_y, .. } = self.state {
if let State::ProgressDrawn {
ref last_drawn_string,
cursor_y,
..
} = self.state
{
if *last_drawn_string == rendered {
return Ok(());
}
buf.push_str(&ansi::up_n_lines_and_home(cursor_y));
}
buf.push_str(ansi::DISABLE_LINE_WRAP);
buf.push_str(ansi::CLEAR_TO_END_OF_SCREEN);
buf.push_str(rendered);
buf.push_str(&rendered);
self.write_output(&buf);
let cursor_y = rendered.as_bytes().iter().filter(|b| **b == b'\n').count();
self.state = State::ProgressDrawn {
since: now,
cursor_y: rendered.as_bytes().iter().filter(|b| **b == b'\n').count(),
last_drawn_time: now,
last_drawn_string: rendered,
cursor_y,
};
}
Ok(())
Expand Down Expand Up @@ -785,7 +808,7 @@ impl<M: Model> InnerView<M> {
self.hide()?;
self.state = if buf.ends_with(b"\n") {
State::Printed {
since: self.clock(),
last_printed: self.clock(),
}
} else {
State::IncompleteLine
Expand Down
34 changes: 34 additions & 0 deletions tests/api/identical_output_suppressed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! Test that Nutmeg avoids redrawing the same text repeatedly.
use std::time::Duration;

use nutmeg::{Destination, Options, View};

struct Hundreds(usize);

impl nutmeg::Model for Hundreds {
fn render(&mut self, _width: usize) -> String {
format!("hundreds={}", self.0 / 100)
}
}

#[test]
fn identical_output_suppressed() {
let options = Options::default()
.destination(Destination::Capture)
.update_interval(Duration::ZERO);
let view = View::new(Hundreds(0), options);
let output = view.captured_output();

for i in 0..200 {
// We change the model, but not in a way that will change what's displayed.
view.update(|model| model.0 = i);
}
view.abandon();

// No erasure commands, just a newline after the last painted view.
assert_eq!(
output.lock().as_str(),
"\x1b[?7l\x1b[0Jhundreds=0\x1b[1G\x1b[?7l\x1b[0Jhundreds=1\n"
);
}
2 changes: 2 additions & 0 deletions tests/api.rs → tests/api/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use std::time::{Duration, Instant};

use nutmeg::{Destination, Options, View};

mod identical_output_suppressed;

struct MultiLineModel {
i: usize,
}
Expand Down

0 comments on commit d14c2ba

Please sign in to comment.