我正在使用
ratatui
在 Rust 中编写一些 TUI,它需要与外部程序通信,捕获输出并渲染它。
我了解在其他线程中运行进程的基础知识,并通过
mpsc::channel()
发送结果或通过 Arc<Mutex<...>>
结构共享结果。但对于 TUI,我无法让它按照我需要的方式工作。
一般来说,应用程序由一个区域中的列表和另一区域中所选列表项的信息组成。我使用
curl
和 DOI 做了一个小例子。 DOI 代表该列表。在信息区域中,应使用 curl -LH "Accept: application/x-bibtex" <doi>
呈现所选 DOI 的 Bibtex 格式。
如果我不在不同线程中生成获取 Bibtex 信息的命令,只要
curl
获取结果,列表移动就会被阻止,这是运行更持久任务的单线程应用程序的正常行为。
现在,我生成另一个线程来在后台运行
fetch_bibtex()
函数并捕获输出(如果有)。从理论上讲,它是有效的,但是如果我快速移动列表,则渲染的信息不是所选项目之一。一段时间后,curl
退出并出现“请求太多”错误。
我想要什么,但无法开始工作:如果我移动列表的速度非常快,我只想加载我停止的项目的信息。这意味着,为了快速移动,尚未完成的正在运行的进程需要被终止。但不知道该怎么做。即使我不确定在哪个事件上调用
fetch_bibtex()
函数。我尝试了不同的方法,但没有任何效果。
只要信息不存在,最好会呈现“...正在加载”或类似内容。
这是我的最小示例,它使用
Arc<Mutex<String>>
作为信息内容。我也尝试发送信息(如果获取)并通过 mpsc
通道接收它,但结果是相似的:
use std::{
process::{Command, Stdio},
sync::{Arc, Mutex},
thread,
};
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)]
pub struct App {
running: bool,
info_text: Arc<Mutex<String>>,
list: Vec<String>,
state: ListState,
}
impl App {
pub fn new() -> Self {
Self {
running: false,
info_text: Arc::new(Mutex::new(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) {
self.fetch_bibtex();
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.lock().unwrap().clone().to_string())
.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<()> {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => self.on_key_event(key),
Event::Mouse(_) => {}
Event::Resize(_, _) => {}
_ => {}
}
Ok(())
}
fn fetch_bibtex(&mut self) {
let sel_doi = self.list[self.state.selected().unwrap_or(0)].clone();
let info_str = Arc::clone(&self.info_text);
thread::spawn(move || {
let output = Command::new("curl")
.arg("-LH")
.arg("Accept: application/x-bibtex")
.arg(&sel_doi)
.stdout(Stdio::piped())
.output()
.expect("Not running");
let output_str = String::from_utf8_lossy(&output.stdout).to_string();
let mut info = info_str.lock().unwrap();
*info = output_str;
});
}
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 {
self.state.scroll_down_by(1);
}
}
(_, KeyCode::Up | KeyCode::Char('k')) => {
self.state.scroll_up_by(1);
}
_ => {}
}
}
fn quit(&mut self) {
self.running = false;
}
}
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let result = App::new().run(terminal);
ratatui::restore();
result
}
当然,这只是一个例子。在真实的应用程序中,列表要长得多。 另外
curl
和 Bibtex 代码只是虚拟示例,因为它们应该在大多数 UNIX 系统上可用。 我在实际应用程序中使用的程序并不是那么广泛传播,因此,由于时间原因,我选择 curl
作为示例延迟它获取 Bibtex 密钥。
不幸的是,使用本机 Rust 库或具有编程良好的 API 的 C 库(我可以通过 Rust 连接到它)是不可能的,因为我尝试作为后台进程运行的程序不存在这样的库。
我感谢我需要改变的每一个帮助。我几个月前才开始学习 Rust,还没有专业的编程背景。所以像这样的错误是学习的机会!
不要为每个请求生成一个线程,只在程序启动时生成一个线程,然后使用邮箱将请求发送到该线程。这样您就不会并行启动多个
curl
请求,并且只会保留最后的结果。
Mutex
和来自 Condvar
的
std::sync
自己动手是很容易的。比如:
struct Mailbox {
data: Mutex<Option<String>>;
cond: Condvar;
}
fn curl_thread (mailbox: Arc<Mailbox>, reply: Sender<String>) {
loop {
let mut request = mailbox.data.lock().unwrap();
while request.is_none() { request = mailbox.cond.wait (request).unwrap(); }
let request = request.take().unwrap(); // Note: this releases the lock.
todo!("Call curl (or whatever) and send the result using the `reply` channel.");
}
}
// Send a new request to the `curl_thread`. Note that:
// - This will not interrupt the currently running request if there is one
// (left as an exercise to the reader)
// - This will only keep at most one pending request, so that if multiple
// requests are sent while the first one is running, only the last one
// will be kept.
let guard = mailbox.data.lock().unwrap();
*guard = Some (request);
mailbox.cond.notify_one();
drop (guard);