Search code examples
rust

How to reduce flicker in terminal re-drawing?


I have a program that displays the state of some commands ran in parallel

fmt    ✔
clippy cargo clippy --tests --color always ... 
tests  cargo test --color always ..

The program is my first one that relies on multi-threading, and I have some threads running those programs as soon as they are "available", and I have one thread (the main one) dedicated to waiting for new results (which are pretty rare, given that jobs tend to run for at leat a few seconds, and there a relatively few jobs, 10 in parallel at most) and deleting & reprinting in a loop the state of things.

In this part of the software, I don't print the output of the commands, just the commands being ran and some ascii spinner.

I don't know how these things should be done, so I managed to limit redraws to at least 40ms :

const AWAIT_TIME: Duration = std::time::Duration::from_millis(40);
fn delay(&mut self) -> usize {
    let time_for = AWAIT_TIME
        - SystemTime::now()
            .duration_since(self.last_occurence)
            .unwrap();
    let millis: usize = std::cmp::max(time_for.as_millis() as usize, 0);
    if millis != 0 {
        sleep(time_for);
    }
    self.last_occurence = SystemTime::now();
    millis
}
while let Some(progress) = read(&rx) { ... }    
job_display.refresh(&tracker, delay);
delay = job_starter.delay();

So I end up tracking the number of lines and chars written and delete them all :


struct TermWrapper {
    term: Box<StdoutTerminal>,
    written_lines: u16,
    written_chars: usize,
}

...

pub fn clear(&mut self) {
    (0..self.written_lines as usize).for_each(|_| {
        self.term.cursor_up().unwrap();
        self.term.carriage_return().unwrap();
        self.term.delete_line().unwrap();
    });
    self.written_lines = 0;
    self.written_chars = 0;
}

It works, but it tends to flicker, especially in embedded terminals.

My next idea is to store the hash of printed string and skip the redraw if I can.

Are there some known patterns I can apply to get some nicer output ?

What are the common strategies I can use ?


Solution

  • The minimum requirement to guarantee no flicker when updating a terminal is: don't send one thing and then overwrite it with something else (within a single 'frame' of drawing). In the case of clearing, we can restate that rule more specifically: don't clear the regions that you're going to put text in. Instead, clear only regions that you know you aren't putting text in (in case there is previous text there).

    The conventional terminal command set contains a very useful tool for this: the “clear to end of line” command. The way you can use it is:

    1. Move the cursor to the beginning of a line you want to replace the text in.
    2. Write the text, without any newline or CRLF at the end
    3. Write “clear to end of line”. (In crossterm, that's ClearType::UntilNewLine.)

    After sending the clear command, the rest of the line is cleared (just as if you had happened to write the exact number of spaces to completely fill the line). In this way, you need to keep track of which lines you're writing on, but you don't need to keep track of the exact width of each string you wrote.


    The next step beyond this, useful for arbitrary 2D screen layouts, is to remember what text has previously been sent to the terminal, and only send what needs to be changed — in Rust, the ratatui library provides this, and you can also find bindings to the well-known C library curses for the same purpose.