Finally, I found a way to get it work. Thanks to all the advices from @Jmb and some trial and error.
Now after spawning the curl
request for the current item, I run an inner loop on matching bg_cmd.try_wait()
. If it finish the run successful, the result get assigned to the shared var holding the output. But if the process is still running and another list item is selected, an AtomicBool
is set which restarts the main loop of the bg process thread and, thus, the result of the former run is dismissed.
Here is the code. There might be ways to make this more efficient and I would be happy to hear about them. But at least it works now and I nevertheless already learned a lot about multi-threading and bg processes in Rust.
use std::{
io::{BufRead, BufReader},
process::{Command, Stdio},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Condvar, Mutex,
},
thread,
time::Duration,
};
use color_eyre::Result;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use ratatui::{
layout::{Constraint, Layout},
style::{Modifier, Style},
widgets::{Block, List, ListState, Paragraph},
DefaultTerminal, Frame,
};
#[derive(Debug, Clone)]
pub struct Mailbox {
finished: Arc<AtomicBool>,
data: Arc<Mutex<Option<String>>>,
cond: Arc<Condvar>,
output: Arc<Mutex<String>>,
kill_proc: Arc<AtomicBool>,
}
impl Mailbox {
fn new() -> Self {
Self {
finished: Arc::new(AtomicBool::new(false)),
data: Arc::new(Mutex::new(None)),
cond: Arc::new(Condvar::new()),
output: Arc::new(Mutex::new(String::new())),
kill_proc: Arc::new(AtomicBool::new(false)),
}
}
}
pub fn run_bg_cmd(
fetch_item: Arc<Mutex<Option<String>>>,
cond: Arc<Condvar>,
output_val: Arc<Mutex<String>>,
finished: Arc<AtomicBool>,
kill_bool: Arc<AtomicBool>,
) {
// Start the main loop which is running in the background as long as
// the TUI itself runs
'main: loop {
let mut request = fetch_item.lock().unwrap();
// Wait as long as their is no request sent. If one is send, the
// Condvar lets the loop run further
while request.is_none() {
request = cond.wait(request).unwrap();
}
let cur_request = request.take().unwrap();
// Drop MutexGuard to free up the main thread
drop(request);
// Spawn `curl` (or any other bg command) using the sent request as arg.
// To not flood the TUI I pipe stderr to /dev/null
let mut bg_cmd = Command::new("curl")
.arg("-LH")
.arg("Accept: application/x-bibtex")
.arg(&cur_request)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("Not running");
// Start inner loop to wait for process to end or dismiss the result if
// next item in the TUI is selected
'waiting: loop {
match bg_cmd.try_wait() {
// If bg process ends with exit code 0, break the inner loop
// to assign the result to the shared variable.
// If bg process ends with exit code not 0, restart main loop and
// drop the result from stdout.
Ok(Some(status)) => {
if status.success() {
break 'waiting;
} else {
continue 'main;
}
}
// If process is still running and the kill bool was set to true
// since another item was selected, immiditatley restart the main loop
// waiting for a new request and, therefore, drop the result
Ok(None) => {
if kill_bool.load(Ordering::Relaxed) {
continue 'main;
}
}
// If an error occurs, restart the main loop and drop all output
Err(e) => {
println!("Error {e} occured while trying to fetch infors");
continue 'main;
}
}
}
// If waiting loop was broken due to successful bg process, take the output
// parse it into a string (or whatever) and assign it to the shared var
// holding the result
let out = bg_cmd.stdout.take().unwrap();
let out_reader = BufReader::new(out);
let mut out_str = String::new();
for l in out_reader.lines() {
if let Ok(l) = l {
out_str.push_str(&l);
}
}
finished.store(true, Ordering::Relaxed);
let mut output_str = output_val.lock().unwrap();
*output_str = out_str;
}
}
#[derive(Debug)]
pub struct App {
mb: Mailbox,
running: bool,
fetch_info: bool,
info_text: String,
list: Vec<String>,
state: ListState,
}
impl App {
pub fn new(mb: Mailbox) -> Self {
Self {
mb,
running: false,
fetch_info: false,
info_text: String::new(),
list: vec![
"http://dx.doi.org/10.1163/9789004524774".into(),
"http://dx.doi.org/10.1016/j.algal.2015.04.001".into(),
"https://doi.org/10.1093/acprof:oso/9780199595006.003.0021".into(),
"https://doi.org/10.1007/978-94-007-4587-2_7".into(),
"https://doi.org/10.1093/acprof:oso/9780199595006.003.0022".into(),
],
state: ListState::default().with_selected(Some(0)),
}
}
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
self.running = true;
while self.running {
terminal.draw(|frame| self.draw(frame))?;
self.handle_crossterm_events()?;
}
Ok(())
}
fn draw(&mut self, frame: &mut Frame) {
let [left, right] =
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(frame.area());
let list = List::new(self.list.clone())
.block(Block::bordered().title_top("List"))
.highlight_style(Style::new().add_modifier(Modifier::REVERSED));
let info = Paragraph::new(self.info_text.as_str())
.block(Block::bordered().title_top("Bibtex-Style"));
frame.render_stateful_widget(list, left, &mut self.state);
frame.render_widget(info, right);
}
fn handle_crossterm_events(&mut self) -> Result<()> {
if event::poll(Duration::from_millis(500))? {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => self.on_key_event(key),
Event::Mouse(_) => {}
Event::Resize(_, _) => {}
_ => {}
}
} else {
if self.fetch_info {
self.update_info();
}
if self.mb.finished.load(Ordering::Relaxed) == true {
self.info_text = self.mb.output.lock().unwrap().to_string();
self.mb.finished.store(false, Ordering::Relaxed);
}
}
Ok(())
}
fn update_info(&mut self) {
// Select current item as request
let sel_doi = self.list[self.state.selected().unwrap_or(0)].clone();
let mut guard = self.mb.data.lock().unwrap();
// Send request to bg loop thread
*guard = Some(sel_doi);
// Notify the Condvar to break the hold of bg loop
self.mb.cond.notify_one();
drop(guard);
// Set bool to false, so no further process is started
self.fetch_info = false;
// Set kill bool to false to allow bg process to complete
self.mb.kill_proc.store(false, Ordering::Relaxed);
}
fn on_key_event(&mut self, key: KeyEvent) {
match (key.modifiers, key.code) {
(_, KeyCode::Esc | KeyCode::Char('q'))
| (KeyModifiers::CONTROL, KeyCode::Char('c') | KeyCode::Char('C')) => self.quit(),
(_, KeyCode::Down | KeyCode::Char('j')) => {
if self.state.selected().unwrap() <= 3 {
// Set kill bool to true to kill unfinished process from prev item
self.mb.kill_proc.store(true, Ordering::Relaxed);
// Set text of info box to "Loading" until bg loop sends result
self.info_text = "... Loading".to_string();
self.state.scroll_down_by(1);
// Set fetch bool to true to start fetching of info after set delay
self.fetch_info = true;
}
}
(_, KeyCode::Up | KeyCode::Char('k')) => {
// Set kill bool to true to kill unfinished process from prev item
self.mb.kill_proc.store(true, Ordering::Relaxed);
// Set text of info box to "Loading" until bg loop sends result
self.info_text = "... Loading".to_string();
self.state.scroll_up_by(1);
// Set fetch bool to true to start fetching of info after set delay
self.fetch_info = true;
}
_ => {}
}
}
fn quit(&mut self) {
self.running = false;
}
}
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
let mb = Mailbox::new();
let curl_data = Arc::clone(&mb.data);
let curl_cond = Arc::clone(&mb.cond);
let curl_output = Arc::clone(&mb.output);
let curl_bool = Arc::clone(&mb.finished);
let curl_kill_proc = Arc::clone(&mb.kill_proc);
thread::spawn(move || {
run_bg_cmd(curl_data, curl_cond, curl_output, curl_bool, curl_kill_proc);
});
let terminal = ratatui::init();
let result = App::new(mb).run(terminal);
ratatui::restore();
result
}