如何在Rust中实现可被事件中断的stdin命令行输入提示?
问题描述
我正在开发一款命令行终端/解析器,它会显示提示并接收用户命令,目前通过以下Rust代码实现:
'prompt: loop{ // 显示提示、读取输入、执行命令的逻辑... }
我希望实现这样的功能:当有触发器(如定时器等)触发时,输出文本以“中断”当前的输入提示,之后立即恢复接收stdin输入。效果示例如下:
[11:39:00] Enter your command: Alert! AAPL has decreased by greater than 5% [11:40:00] Enter your command:
核心需求是循环需持续运行并检查事件,同时接收用户输入。请问实现该功能的最佳方式是什么?
解决方案
在Rust中实现该功能的核心是避免阻塞式输入读取,同时处理终端输出与输入的同步问题,以下是两种实用方案:
方案一:异步IO(Tokio 生态)
异步IO可以让你同时监听用户输入和触发器事件,无需额外线程调度,适合大多数场景。
- 添加依赖到
Cargo.toml:
[dependencies] tokio = { version = "1.0", features = ["full"] } chrono = "0.4"
- 实现代码示例:
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::time::{interval, Duration}; use chrono::Local; #[tokio::main] async fn main() { let stdin = BufReader::new(tokio::io::stdin()); let mut lines = stdin.lines(); // 模拟1分钟一次的触发器事件 let mut trigger_interval = interval(Duration::from_secs(60)); // 初始显示提示 print_prompt().await; loop { tokio::select! { // 处理用户输入 input_result = lines.next_line() => { match input_result { Ok(Some(cmd)) => { println!("执行命令:{}", cmd); print_prompt().await; } Ok(None) => break, // stdin 关闭 Err(e) => eprintln!("读取输入失败:{}", e), } } // 处理触发器事件 _ = trigger_interval.tick() => { // 清除当前输入行(避免和用户输入重叠) print!("\r\x1B[K"); println!("Alert! AAPL has decreased by greater than 5%"); print_prompt().await; } } } } async fn print_prompt() { let timestamp = Local::now().format("%H:%M:%S"); print!("[{}] Enter your command: ", timestamp); tokio::io::stdout().flush().await.unwrap(); }
这里用tokio::select!同时等待输入和定时器事件,\r\x1B[K是ANSI转义序列,用于清除当前行,确保触发器输出和新提示不会与用户未完成的输入混在一起。
方案二:线程 + 终端原始模式
如果不想依赖异步生态,可以用主线程处理输入,单独线程管理触发器,同时借助终端原始模式避免阻塞式读取的问题。
- 添加依赖到
Cargo.toml:
[dependencies] termion = "1.5" chrono = "0.4"
- 实现代码示例:
use chrono::Local; use std::io::{self, Write}; use std::sync::mpsc; use std::thread; use std::time::Duration; use termion::input::TermRead; use termion::raw::IntoRawMode; fn main() { let (trigger_tx, trigger_rx) = mpsc::channel(); // 触发器线程:模拟定时事件 thread::spawn(move || { loop { thread::sleep(Duration::from_secs(60)); let _ = trigger_tx.send(()); } }); // 切换到终端原始模式:禁用行缓冲,支持实时输入处理 let mut stdout = io::stdout().into_raw_mode().unwrap(); let stdin = io::stdin(); loop { // 显示输入提示 print_prompt(&mut stdout); // 先检查是否有触发器事件 if trigger_rx.try_recv().is_ok() { handle_trigger_event(&mut stdout); continue; } // 逐字符读取输入(原始模式下无行缓冲) for key in stdin.keys() { let key = key.unwrap(); match key { termion::event::Key::Char('\n') => { writeln!(stdout, "\n执行命令").unwrap(); stdout.flush().unwrap(); break; } termion::event::Key::Char(c) => { // 回显用户输入的字符 write!(stdout, "{}", c).unwrap(); stdout.flush().unwrap(); } _ => {} } // 每读取一个字符就检查一次触发器事件 if trigger_rx.try_recv().is_ok() { handle_trigger_event(&mut stdout); break; } } } } fn print_prompt(stdout: &mut impl Write) { let timestamp = Local::now().format("%H:%M:%S"); write!(stdout, "[{}] Enter your command: ", timestamp).unwrap(); stdout.flush().unwrap(); } fn handle_trigger_event(stdout: &mut impl Write) { // 清除当前行并输出告警 write!(stdout, "\r\x1B[KAlert! AAPL has decreased by greater than 5%\n").unwrap(); stdout.flush().unwrap(); }
该方案通过termion切换到终端原始模式,避免了read_line的阻塞问题,同时用线程间通信传递触发器事件,主线程在输入间隙检查事件并处理。
内容的提问来源于stack exchange,提问作者Nick S.
相关产品推荐
相关产品推荐

