I have a program running two threads, where one prints status message to the console and the other accepts user inputs. However, because they both use the same console, if I'm partway typing a command using one thread when the other thread prints, it takes what I've already written with it (only visually - the command will still execute properly).
This is an example of the code, where if you try to type into the console, it will constantly be interfered with by the second thread.
use std::{time,thread,io};
fn main() {
thread::spawn(move || {
loop {
println!("Interrupting line");
thread::sleep(time::Duration::from_millis(1000));
};
});
loop {
let mut userinput: String = String::new();
io::stdin().read_line(&mut userinput);
println!("{}",userinput)
}
}
As it is right now, this is what the console ends up looking like when trying to type "i am trying to write a full sentence here" into the console:
Interrupting line
i aInterrupting line
m trying Interrupting line
to write a fInterrupting line
ull senInterrupting line
tence hereInterrupting line
i am trying to write a full sentence here
Interrupting line
Interrupting line
As you can see, whatever I've written into the console when the second thread loops and prints "Interrupting line" is carried along with that line. Ideally, while I'm in the middle of typing, it would look like this (no matter how long it takes to type):
Interrupting line
Interrupting line
Interrupting line
i am trying to
Then, once I'm done typing and press enter, it would look like this:
Interrupting line
Interrupting line
Interrupting line
i am trying to write a full sentence here
i am trying to write a full sentence here
Where the first sentence is my actual typed input, and the second is when it prints what I entered back to the console.
Is there a way to print lines to the console that doesn't cause any in-progress user input to be mangled with the printing message?
As we mentioned in the comment section above, you very likely want to use an external library to deal with the intrinsic of each terminal.
However, unlike discussed above, you may not even need tui
for such a simple "UI", you could get away using termion
(the actual crate tui
uses under the hood).
The following code snippet does exactly what you described above and even a tiny bit more. But this is just a crude initial implementation, there are many things in there which need further refinement. (E.g. you may want to handle the resize-event of the terminal while your program is running, or you want to gracefully handle the poisoned mutex states, etc.)
Because the following snippet is rather long, let's go over it in small, digesteable chunks.
First, let's start with the boring part, all the imports and some type aliasing we will use throughout the code.
use std::{
time::Duration,
thread::{
spawn,
sleep,
JoinHandle,
},
sync::{
Arc,
Mutex,
TryLockError,
atomic::{
AtomicBool,
Ordering,
},
},
io::{
self,
stdin,
stdout,
Write,
},
};
use termion::{
terminal_size,
input::TermRead,
clear,
cursor::Goto,
raw::IntoRawMode,
};
type BgBuf = Arc<Mutex<Vec<String>>>;
type FgBuf = Arc<Mutex<String>>;
type Signal = Arc<AtomicBool>;
That's out of the way, we can focus on our background-thread. This is where all the "interrupting" lines should go. (In this snippet if you press RETURN then the typed in "command" will be added to these lines as well to demonstrate interthread communication.)
For easier debugging and demonstration purposes the lines are indexed. As the background-thread is actually just a secondary thread it is not as aggressive as the primary one which handles the user inputs (foreground-thread) so it only uses try_lock
. Because of that it is a good idea to use a thread-local buffer to store the entries which could not be put into the shared buffer when that was unavailable so we wouldn't miss any entries.
fn bg_thread(bg_buf: BgBuf,
terminate: Signal) -> JoinHandle<()>
{
spawn(move ||
{
let mut i = 0usize;
let mut local_buffer = Vec::new();
while !terminate.load(Ordering::Relaxed)
{
local_buffer.push(format!("[{}] Interrupting line", i));
match bg_buf.try_lock()
{
Ok(mut buffer) =>
{
buffer.extend_from_slice(&local_buffer);
local_buffer.clear();
},
Err(TryLockError::Poisoned(_)) => panic!("BgBuf is poisoned"),
_ => (),
}
i += 1;
sleep(Duration::from_millis(1000));
};
})
}
Then comes our foreground-thread which reads the input from the user. It has to be in a separate thread because it waits for key presses (aka events) from the user and while it does that it blocks its thread.
As you might noticed both threads are using terminate
(a shared AtomicBool
) as a signal. The background-thread and the main thread only read it, while this foreground-thread writes it. Because we handle all the keyboard inputs in the foreground-thread, naturally this is where we handle the CTRL + C interruption, thus we use terminate
to signal the other threads if our user wants to quit.
fn fg_thread(fg_buf: FgBuf,
bg_buf: BgBuf,
terminate: Signal) -> JoinHandle<()>
{
use termion::event::Key::*;
spawn(move ||
{
for key in stdin().keys()
{
match key.unwrap()
{
Ctrl('c') => break,
Backspace =>
{
fg_buf.lock().expect("FgBuf is poisoned").pop();
},
Char('\n') =>
{
let mut buf = fg_buf.lock().expect("FgBuf is poisoned");
bg_buf.lock().expect("BgBuf is poisoned").push(buf.clone());
buf.clear();
},
Char(c) => fg_buf.lock().expect("FgBuf is poisoned").push(c),
_ => continue,
};
}
terminate.store(true, Ordering::Relaxed);
})
}
And last but not least these are followed by our main-thread. We create the main data structures here which are shared among the three threads. We set the terminal to be in "raw" mode, so that we control what goes on the screen manually instead of relying on some internal buffering, thus we can implement the clipping mechanism.
We measure the size of the terminal window to determine how many lines we should print out from the background-buffer.
Before every successful frame render we clear the screen, then print out the last n entries of the background-buffer, then print the user input as the last line. And then to finally make these things appear on the screen, we flush the stdout
.
If we receive the termination-signal, we clean up the other two threads (i.e. wait for them to finish), clear the screen, reset the cursor position, and say good-bye to our user.
fn main() -> io::Result<()>
{
let bg_buf = Arc::new(Mutex::new(Vec::new()));
let fg_buf = Arc::new(Mutex::new(String::new()));
let terminate = Arc::new(AtomicBool::new(false));
let background = bg_thread(Arc::clone(&bg_buf),
Arc::clone(&terminate));
let foreground = fg_thread(Arc::clone(&fg_buf),
Arc::clone(&bg_buf),
Arc::clone(&terminate));
let mut stdout = stdout().into_raw_mode().unwrap();
let (_, height) = terminal_size().unwrap();
while !terminate.load(Ordering::Relaxed)
{
write!(stdout, "{}", clear::All)?;
{
let entries = bg_buf.lock().expect("BgBuf is poisoned");
let entries = entries.iter().rev().take(height as usize - 1);
for (i, entry) in entries.enumerate()
{
write!(stdout, "{}{}", Goto(1, height - i as u16 - 1), entry)?;
}
}
{
let command = fg_buf.lock().expect("FgBuf is poisoned");
write!(stdout, "{}{}", Goto(1, height), command)?;
}
stdout.flush().unwrap();
sleep(Duration::from_millis(50));
}
background.join().unwrap();
foreground.join().unwrap();
writeln!(stdout, "{0}{1}That's all folks!{1}", clear::All, Goto(1, 1))
}
And if we put all these things together, compile it and run it, we could get the following output:
[0] Interrupting line
[1] Interrupting line
[2] Interrupting line
[3] Interrupting line
This is one command..
[4] Interrupting line
[5] Interrupting line
..and here's another..
[6] Interrupting line
[7] Interrupting line
..and it can do even more!
[8] Interrupting line
[9] Interrupting line
Pretty cool, eh?
[10] Interrupting line
[11] Interrupting line
[12] Interrupting line
[13] Interrupting line
I think it is! :)