You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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");
}

请问:是否需要区分处理普通命令与编辑器命令?或者能否修改原代码使其兼容两种命令类型,具体该怎么实现?


解决方案

不需要硬性区分命令类型,核心思路是根据子进程是否需要终端交互,动态切换终端控制权:

实现步骤

  1. 避免提前抢占终端输入:原代码提前初始化termion::async_stdin().keys()会抢占终端输入流,导致交互式命令无法获取按键。需改为:仅在确认子进程是非交互式时,才启动按键监听;若为交互式进程,直接将终端完全交给子进程。
  2. 动态检测交互性:子进程启动后短暂等待,尝试获取终端raw模式权限——若失败,说明子进程已主动接管终端(如编辑器类命令),判定为交互式;若成功,则判定为非交互式。
  3. 分支处理逻辑:
    • 非交互式命令:保留循环检测+按键监听逻辑,捕获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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 14:13:19