Termion按键监听中断终端编辑器的解决方案咨询
问题描述
现有一段Rust代码,通过循环检测子进程状态,配合Termion在终端raw模式下实现用Ctrl+C终止普通命令(比如sleep 4 && echo test),代码如下:
use std::{process::Command, thread, time::Duration}; use termion::{event::Key, input::TermRead}; pub struct CmdRunner {} impl CmdRunner { fn run(&mut self) { let mut command = Command::new("script"); command.arg("-qec").arg("sleep 4 && echo test").arg("/dev/null"); let mut child = command.spawn().expect("failed to spawn command"); let mut stdin = termion::async_stdin().keys(); loop { match child.try_wait() { Ok(Some(_)) => { print!("Child process has exited\r\n"); break; } Ok(None) => { let input = stdin.next(); if let Some(Ok(key)) = input { print!("A key was pressed!\r\n"); match key { Key::Ctrl('c') => { print!("Ctrl + C was pressed!") // 实际代码中会在这里设置终止标记 } _ => {} } } print!("Still running...\r\n"); } Err(e) => { eprint!("Error while waiting for child process: {}", e); break; } } thread::sleep(Duration::from_millis(100)); } } } fn main() { let mut command = CmdRunner {}; command.run(); }
但运行vi test.txt这类交互式编辑器命令时,Termion的按键监听会中断编辑器的正常运行;如果把run()改成直接调用child.wait(),编辑器能正常工作,但又无法通过Ctrl+C终止进程:
fn run(&mut self) { let mut command = Command::new("script"); command.arg("-qec").arg("vi test.txt").arg("/dev/null"); let mut child = command.spawn().expect("failed to spawn command"); child.wait().expect("editor exited with a non-zero exit"); }
请问:是否需要区分处理普通命令与编辑器命令?或者能否修改原代码使其兼容两种命令类型,具体该怎么实现?
解决方案
不需要硬性区分命令类型,核心思路是根据子进程是否需要终端交互,动态切换终端控制权:
实现步骤
- 避免提前抢占终端输入:原代码提前初始化
termion::async_stdin().keys()会抢占终端输入流,导致交互式命令无法获取按键。需改为:仅在确认子进程是非交互式时,才启动按键监听;若为交互式进程,直接将终端完全交给子进程。 - 动态检测交互性:子进程启动后短暂等待,尝试获取终端raw模式权限——若失败,说明子进程已主动接管终端(如编辑器类命令),判定为交互式;若成功,则判定为非交互式。
- 分支处理逻辑:
- 非交互式命令:保留循环检测+按键监听逻辑,捕获
Ctrl+C后给子进程发送终止信号。 - 交互式命令:直接调用
child.wait(),让子进程完全接管终端,此时Ctrl+C由子进程自行处理(比如vi中Ctrl+C会退出插入模式),若需强制终止可使用Ctrl+\发送SIGQUIT信号。
- 非交互式命令:保留循环检测+按键监听逻辑,捕获
修改后的代码示例
use std::{ process::{Command, Stdio}, thread, time::Duration, }; use termion::{event::Key, input::TermRead, raw::IntoRawMode}; pub struct CmdRunner {} impl CmdRunner { fn run(&mut self, cmd: &str) { let mut command = Command::new("script"); command .arg("-qec") .arg(cmd) .arg("/dev/null") // 让子进程继承当前终端的标准IO .stdin(Stdio::inherit()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()); let mut child = command.spawn().expect("failed to spawn command"); // 短暂等待,判断子进程是否立即退出 thread::sleep(Duration::from_millis(200)); match child.try_wait() { Ok(Some(_)) => { println!("Child process exited immediately"); return; } Ok(None) => { // 尝试进入raw模式,失败则说明子进程已接管终端 match termion::stdin().into_raw_mode() { Ok(_raw) => { // 非交互式命令:启动按键监听循环 let mut stdin = termion::async_stdin().keys(); loop { match child.try_wait() { Ok(Some(status)) => { println!("\rChild process exited with status: {:?}", status); break; } Ok(None) => { if let Some(Ok(Key::Ctrl('c'))) = stdin.next() { println!("\rSending SIGINT to child process..."); child.kill().expect("failed to kill child"); break; } // 降低CPU占用 thread::sleep(Duration::from_millis(100)); } Err(e) => { eprintln!("Error waiting for child: {}", e); break; } } } } Err(_) => { // 交互式命令:直接等待子进程结束 let status = child.wait().expect("failed to wait for child"); println!("Interactive process exited with status: {:?}", status); } } } Err(e) => { eprintln!("Error checking child status: {}", e); return; } } } } fn main() { let mut runner = CmdRunner {}; // 测试普通命令 // runner.run("sleep 4 && echo test"); // 测试编辑器命令 runner.run("vi test.txt"); }
关键说明
- 终端IO继承:通过
stdin(Stdio::inherit())等配置,确保子进程能获取当前终端的输入输出权限,是交互式命令正常运行的前提。 - 动态检测逻辑:尝试进入raw模式失败,意味着子进程已主动修改终端模式(如vi切换到cbreak模式),此时放弃按键监听,避免干扰子进程。
- 信号处理:非交互式命令下,捕获
Ctrl+C后调用child.kill()发送SIGINT终止子进程;交互式命令下,Ctrl+C由子进程自行处理,无需额外干预。
内容的提问来源于stack exchange,提问作者wyc
相关产品推荐
相关产品推荐

