thread-safe drawing ✍🏼
Problem
There is probably a valid reason for why the terminal is not thread-safe, but humor me with the conversation...
I have a bit of an interesting use-case for tui-rs, one that I haven't seen replicated in any of the examples listed in the README. Essentially I want to draw the ui on each key press from the user, as well as every second after the user enters their first keypress. (you can imagine this as a input-aware countdown)
Unfortunately, trying to spawn a thread on first keypress results in the following error. I understand this error and what it's detailing, but after a few days of problem solving I haven't been able to solidly answer whether or not there is a viable workaround. Is what I'm trying to accomplish even possible given B is not thread safe?
Solution
If possible, I'd love to be able to draw in child threads.
Alternatives
I was able to hack together a simple working solution by intercepting the first keypress in the key_events function like so...
fn key_events() -> mpsc::Receiver<Key> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let stdin = io::stdin();
for key in stdin.keys().flatten() {
// if first keypress, spawn thread here and send a tick_event every 1 sec
tx.send(key).unwrap();
}
});
rx
}
The problem with this approach however, is if the user restarts the application (possible within the app itself, not having to close the app and re-open), I'd have to somehow close this key_events channel and start a new one since "first keypress" has already happened. I tried this and was getting errors about sending on closed channels.
Ultimately the approach I settled on, which I see implemented in many of the examples, is specifying a fast tick_rate (100ms) like so
fn get_events(should_tick: bool) -> mpsc::Receiver<Events> {
let (tx, rx) = mpsc::channel();
if should_tick {
let tick_x = tx.clone();
thread::spawn(move || loop {
tick_x.send(Events::Tick).unwrap();
thread::sleep(Duration::from_millis(100))
});
}
thread::spawn(move || {
let stdin = io::stdin();
for key in stdin.keys().flatten() {
tx.send(Events::Input(key)).unwrap();
}
});
rx
}
and then modifying the looping function to operate like this
match events.recv()? {
Events::Tick => {
if app.thok.has_started() && !app.thok.has_finished() {
app.thok.on_tick();
terminal.draw(|f| ui(f, app))?;
} else if app.thok.has_finished() && app.screen == Screen::Prompt {
app.thok.calc_results();
app.screen = Screen::Results;
}
}
...
This approach is super simple, and works great. The only piece that bugs me is that it doesn't start the countdown immediately. It just checks every 100ms to see if it should start. In other words you might get a free 99ms. Obviously this is such a short amount of time it's not noticeable to any user, but I'm not sure I have any other option given, again, B is not thread safe.
Additional context
Obviously this isn't working code, but this is essentially what I had originally envisioned as my ideal solution.
fn main() -> Result<(), Box<dyn Error>> {
simple_logging::log_to_file("out.log", log::LevelFilter::Info).unwrap();
// check for input on stdin here, if it exists, store it,
// otherwise continue
let args = Args::parse();
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new(args);
let result = run_app(&mut terminal, &mut app);
if let Err(err) = result {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: &mut App,
) -> Result<(), Box<dyn Error>> {
let events = key_events();
loop {
let mut exit_type: ExitType = ExitType::Quit;
terminal.draw(|f| ui(f, &mut app))?;
loop {
let app = &mut app;
let key = events.recv()?;
match key {
Key::Esc => {
break;
}
Key::Char(c) => match app.screen {
Screen::Prompt => {
if !app.thok.has_started() {
thread::spawn(move || loop {
app.thok.on_tick();
thread::sleep(Duration::from_secs(1));
terminal.draw(|f| ui(f, &mut app))?;
});
}
app.thok.write(c);
if app.thok.has_finished() {
app.thok.calc_results();
app.screen = Screen::Results;
}
}
Screen::Results => match key {
Key::Char('r') => {
exit_type = ExitType::Restart;
break;
}
Key::Char('n') => {
exit_type = ExitType::New;
break;
}
_ => {}
},
},
_ => {}
}
terminal.draw(|f| ui(f, app))?;
}
match exit_type {
ExitType::Restart => {
app.reset(Some(app.thok.prompt.clone()));
}
ExitType::New => {
app.reset(None);
}
ExitType::Quit => {
break;
}
}
}
Ok(())
}
fn key_events() -> mpsc::Receiver<Key> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let stdin = io::stdin();
for key in stdin.keys().flatten() {
tx.send(key).unwrap();
}
});
rx
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
match app.screen {
Screen::Prompt => {
app.thok.draw_prompt(f).unwrap();
}
Screen::Results => {
app.thok.draw_results(f).unwrap();
}
}
}