在 bg(其他线程)中运行外部命令,捕获可在 TUI 中渲染的输出

问题描述 投票:0回答:1

我正在使用

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,还没有专业的编程背景。所以像这样的错误是学习的机会!

multithreading rust background-process spawn ratatui
1个回答
0
投票

不要为每个请求生成一个线程,只在程序启动时生成一个线程,然后使用邮箱将请求发送到该线程。这样您就不会并行启动多个

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);
© www.soinside.com 2019 - 2024. All rights reserved.