How to reduce flicker in terminal re-drawing? - rust

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 ?

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:
Move the cursor to the beginning of a line you want to replace the text in.
Write the text, without any newline or CRLF at the end
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 tui crate provides this, and you can also find bindings to the well-known C library curses for the same purpose.

Related

How to get keyboard input with Termion in Rust?

I'm trying out Rust and I really like it so far. I'm working on a tool that needs to get arrow key input from the user. So far, I've got something half-working: if I hold a key for a while, the relevant function gets called. However, it's far from instantaneous.
What I've got so far:
let mut stdout = io::stdout().into_raw_mode();
let mut stdin = termion::async_stdin();
// let mut stdin = io::stdin();
let mut it = stdin.keys(); //iterator object
loop {
//copied straight from GitLab: https://gitlab.redox-os.org/redox-os/termion/-/issues/168
let b = it.next();
match b {
Some(x) => match x {
Ok(k) => {
match k {
Key::Left => move_cursor(&mut cursor_char, -1, &enc_chars, &mpt, &status),
Key::Right => move_cursor(&mut cursor_char, 1, &enc_chars, &mpt, &status),
Key::Ctrl('c') => break,
_ => {}
}
},
_ => {}
},
None => {}
}
//this loop might do nothing if no recognized key was pressed.
}
I don't quite understand it myself. I'm using the terminal raw mode, if that has anything to do with it. I've looked at the rustyline crate, but that's really no good as it's more of an interactive shell-thing, and I just want to detect keypresses.
If you're using raw input mode and reading key by key, you'll need to manually buffer the character keys using the same kind of match loop you already have. The Key::Char(ch) enum variant can be used to match regular characters. You can then use either a mutable String or an array like [u8; MAX_SIZE] to store the character data and append characters as they're typed. If the user moves the cursor, you'd need to keep track of the current position within your input buffer and make sure to insert the newly typed characters into the correct spot, moving the existing characters if needed. It is a lot of work, which is why there are crates that will do it for you, but you will have less chance to control how the input behaves. If you want to use an existing crate, then tui-rs might be a good one to check out for a complete solution, or linefeed for something much simpler.
As for the delay, I think it might be because you're using AsyncReader, which according to the docs is using a secondary thread to do blocking reads

How to animate the terminal in Rust

I need to profile several variables like frames per second being rendered in my app. Therefore I need a simple way to update variables in the terminal.
I've searched and found ascii_table for generating tables, and termion for updating the terminal. But I suspect termion here is simply being used to clear the terminal.
Anyways, I was able to draw a simple table and update its contents every 200 miliseconds:
use ascii_table::{Align, AsciiTable, Column};
extern crate termion;
use termion::{clear, color, cursor};
use std::fmt::Display;
use std::{thread, time};
fn main() {
let mut ascii_table = AsciiTable::default();
ascii_table.max_width = 40;
let mut column = Column::default();
column.header = "H1".into();
column.align = Align::Left;
ascii_table.columns.insert(0, column);
let mut column = Column::default();
column.header = "H2".into();
column.align = Align::Center;
ascii_table.columns.insert(1, column);
let mut column = Column::default();
column.header = "H3".into();
column.align = Align::Right;
ascii_table.columns.insert(2, column);
let mut i = 0;
while (true) {
let data: Vec<Vec<&dyn Display>> = vec![
vec![&i, &"hello", &789],
];
let s = ascii_table.format(data.clone());
println!(
"\n{}{}{}{}",
cursor::Hide,
clear::All,
cursor::Goto(1, 1),
s
);
println!("Hello");//couldn't make this appear on top.
i = i+1;
std::thread::sleep(std::time::Duration::from_millis(200));
}
}
Is this the way programs like top update data on the terminal? Or is there a better way? It'd be nice to have more complex structures.
There isn't a fundamentally better way to format complex data on a terminal than you're doing. There are some individual refinements that can be made to improve display quality.
In particular, in order to reduce flickering, it is best to overwrite text rather than clearing the entire terminal first, and only clear the parts that either need to become blank or are already blank, using narrower clear operations such as clear to end of line, which you would use when you're replacing a line and it might become shorter — by putting this clear at the end of the text, so that if the text is unchanged it doesn't disappear briefly.
Since you're starting with code that generates multiline text, you'll need to edit the string:
// Insert clear-to-end-of-line into the table
let s = ascii_table.format(data.clone())
.replace("\n", &format!("{}\n", clear::UntilNewline));
println!(
"\n{}{}{}{}",
cursor::Hide,
cursor::Goto(1, 1),
s,
clear::AfterCursor,
);
Notice that I have reordered the operations: first, we go to (1, 1) and draw the text (clearing to end of line as we go). Then when everything is done, we clear from cursor to end of screen. This way, we're never clearing any of the text we want to be still present, so there will be no flicker.
I notice you have another wish:
println!("Hello");//couldn't make this appear on top.
All you need to do here is do it after the goto and before the table, and include clearing, and it'll work as you'd like.
// Move cursor to top. This always goes first.
// Note print!, not println!, since we don't want to move down
// after the goto.
print!("{}{}", cursor::Hide, cursor::Goto(1, 1));
// Use clear::UntilNewline on intermediate things
println!("Hello{}", clear::UntilNewline);
// ...even if they are blank lines
println!("{}", clear::UntilNewline);
// Use clear::AfterCursor on the *last* thing printed
// Note print!, not println!, since if we are filling the entire terminal
// we don't want to cause it to scroll down.
print!("{}{}", s, clear::AfterCursor);
One thing I haven't covered is that you can also use termion::cursor::Goto to move to specific areas on the terminal to update them, instead of writing entire lines top-to-bottom. This is of course more complex since your program has to comprehend the entire layout to know what cursor position to go to, and know which parts need to be redrawn. In the days of actual serial terminals and modems that had very low data rates, this was a very important optimization to avoid wasting transmission time on characters that were the same — today, it's less critical.

Rust - nested loop going only once inside inner loop

I am taking data from text file
let fil1_json = File::open("fil1.json")?;
let mut fil1_json_reader = BufReader::new(fil1_json);
let fil2_json = File::open("fil2.json")?;
let mut fil2_json_reader = BufReader::new(fil2_json);
for fil1_line in fil1_json_reader.by_ref().lines() {
for fil2_line in fil2_json_reader.by_ref().lines() {
println!("{:#?} ----- {:#?}", fil1_line, fil2_line);
}
}
In the second nested loop, it is only going inside once. It looks like fil2_json_reader is getting emptied after first iteration.
Where it is changing as I am not changing anywhere?
Where it is changing as I am not changing anywhere?
Readers consume the data. In the case of File, this is the natural expectation, since file abstractions almost universally have a cursor that advances every time you read.
If you want to iterate several times over the same data, then the obvious option is saving it to memory (typically before splitting into lines(), but you can also save a vector of those even if it will be slower). However, since the reader is backed by an actual file, it is better to re-iterate over the file by seeking to its beginning:
fil2_json_reader.seek(SeekFrom::Start(0))

Parsing number with nom 5.0

I'm trying to parse a large file (tens of GB) streaming using Nom 5.0. One piece of the parser tries to parse numbers:
use nom::IResult;
use nom::character::streaming::{char, digit1};
// use nom::character::complete::{char, digit1};
use nom::combinator::{map, opt};
use nom::multi::many1;
use nom::sequence::{preceded, tuple};
pub fn number(input: &str) -> IResult<&str, &str> {
map(
tuple((
opt(char('-')),
many1(digit1),
opt(preceded(char('.'), many1(digit1)))
)),
|_| "0"
)(input)
}
(Obviously, it should not return "0" for all number; that's just to make the function as simple as possible.) For this parser, I wrote a test:
#[test]
fn match_positive_integer() {
let (_, res) = number("0").unwrap();
assert_eq!("0", res);
}
This test fails with Incomplete(Size(1)) because the "decimals" opt() wants to read data and it isn't there. If I switch to the complete versions of the matchers (as commented-out line), the test passes.
I assume this will actually work in production, because it will be fed additional data when complaining about incompleteness, but I would still like to create unit tests. Additionally, the issue would occur in production if a number happened to be the very last bit of input in a file. How do I convince a streaming Nom parser that there is no more data available?
One can argue that the test in its original form is correct: The parser can't decide whether the given input is a number or not, so the parsing-result is in fact undecided yet. In production, especially when reading large files as you do, the buffer of already-read-but-to-be-parsed bytes might end right in between what could be a number unless it's actually not. Then, the parser needs to preserve its current state and ask for more input so it can retry/continue. Think of Incomplete not as a final error but as I don't even know: This could be an error depending on the next byte, this problem is undecidable as of yet!.
You can use the complete-combinator on your top-level parser so when you do in fact reach EOF, you error out on that. Incomplete-results within the top-level parser should be handled e.g. by expanding the read-buffer by some margin and retrying.
You can wrap the parser in a complete()-parser local to the current unittest and test on that. Something to the tune of
#[test]
fn match_positive_integer() {
let (_, res) = complete(number("0")).unwrap();
assert_eq!("0", res);
}

Warning function should have a snake case identifier on by default

I'm trying to figure out what this warning actually means. The program works perfectly but during compile I get this warning:
main.rs:6:1: 8:2 warning: function 'isMultiple' should have a snake case identifier,
#[warn(non_snake_case_functions)] on by default
the code is very simple:
/*
Find the sum of all multiples of 3 or 5 below 1000
*/
fn isMultiple(num: int) -> bool {
num % 5 == 0 || num % 3 == 0
}
fn main() {
let mut sum_of_multiples = 0;
//loop from 0..999
for i in range(0,1000) {
sum_of_multiples +=
if isMultiple(i) {
i
}else{
0
};
}
println!("Sum is {}", sum_of_multiples);
}
You can turn it off by including this line in your file. Check out this thread
#![allow(non_snake_case)]
Rust style is for functions with snake_case names, i.e. the compiler is recommending you write fn is_multiple(...).
My take on it is that programmers have been debating the case of names and other formatting over and over and over again, for decades. Everyone has their preferences, they are all different.
It's time we all grew up and realized that it's better we all use the same case and formatting. That makes code much easier to read for everyone in the long run. It's time to quit the selfish bickering over preferences. Just do as everyone else.
Personally I'm not much into underscores so the Rust standard upset me a bit. But I'm prepared to get use to it. Wouldn't it be great if we all did that and never had to waste time thinking and arguing about it again.
To that end I use cargo-fmt and clippy and I accept whatever they say to do. Job done, move on to next thing.
There is a method in the Rust convention:
Structs get camel case.
Variables get snake case.
Constants get all upper case.
Makes it easy to see what is what at a glance.
-ZiCog
Link to thread : https://users.rust-lang.org/t/is-snake-case-better-than-camelcase-when-writing-rust-code/36277/2

Resources