一种区分“\e”和转义键的方法,如 C++ 中的“\e[A”

A way to distinguish `\e` from escaped keys like `\e[A` in C++

我正在用 C++ 编写 readline 替换,我想在原始模式下处理终端输入,包括 special/escaped 键,如“向上箭头”\e[A。但是,我还希望能够区分按一下转义键 \e 然后按 [ 和按 A 还是按向上箭头。

我假设这两种情况之间的主要区别在于,当按下向上箭头时,输入字符会在不到一毫秒的时间内进入,所以我想我可以这样做:

#include <termios.h>
#include <absl/strings/escaping.h>
#include <iostream>

termios enter_raw() {
    termios orig;
    termios raw;
    tcgetattr(STDOUT_FILENO, &orig);
    tcgetattr(STDOUT_FILENO, &raw);
    raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
    raw.c_oflag &= ~OPOST;
    raw.c_cflag |= CS8;
    raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
    raw.c_cc[VMIN]  = 1;
    raw.c_cc[VTIME] = 0;
    tcsetattr(STDOUT_FILENO, TCSAFLUSH, &raw);
    return orig;
}

int main() {
    termios orig = enter_raw();
    while(true) {
        char buf[10];
        memset(buf, 0, sizeof(buf));
        std::cin >> buf[0];
        usleep(1000);
        int actual = std::cin.readsome(buf + 1, sizeof(buf) - 2);
        std::cout << "Got string: \"" << absl::CEscape(buf) << "\"\n";
        if(buf[0] == 35) { break; } // received ctrl-c
    }
    tcsetattr(STDOUT_FILENO, TCSAFLUSH, &orig);
    return 0;
}

然而,这个输出并不是我希望的Got string: "3[A";相反,它会执行 Got string 三次,就像它只是对字符的简单循环一样。改变它休眠的微秒数似乎没有任何影响。

有没有办法用C++轻松实现这种东西?它适用于大多数终端吗?我不关心支持 Windows。答案不必使用<iostream>;只要完成工作,它就可以使用 C 风格的终端 IO。

raw.c_cc[VMIN]  = 1;
raw.c_cc[VTIME] = 0;

这是阻塞读取。来自 termios 手册页:

   MIN > 0, TIME == 0 (blocking read)
          read(2)  blocks until MIN bytes are available, and returns up to
          the number of bytes requested.

保证“自动”生成多字符密钥,例如 3[A。终端也可以自己从底层设备接收 3 密钥。满足阻塞读的条件,返回escape。下一个角色很快就到了,但为时已晚。

您似乎想要的是:

   MIN > 0, TIME > 0 (read with interbyte timeout)
          TIME specifies the limit for a timer  in  tenths  of  a  second.
          Once  an  initial  byte of input becomes available, the timer is
          restarted after each further byte is received.  read(2)  returns
          when any of the following conditions is met:

          *  MIN bytes have been received.

          *  The interbyte timer expires.

          *  The  number  of bytes requested by read(2) has been received.
             (POSIX does not specify this termination  condition,  and  on
             some  other  implementations  read(2) does not return in this
             case.)

从键序列中选择一个合理的最大字符数。 6 个字符是一个合理的建议。使用 6 表示 MIN。现在,您需要某种超时。可能是 2/10 秒。

所以,现在当 3 键到达时,终端层将再等待 2/10 秒以查看是否有东西进入。泡沫,冲洗重复。当计时器超时时,所有进来的东西都会被返回。

请注意,您无法保证多字符序列的第二个字符会在 2/10 秒内到达。或 3/10 秒。或一分钟。

即使在计时器到期之前出现另一个字符,也不能保证它是一个多字符键序列。即使是像 2/10 秒这样的短间隔,您也可能会在这个时间限制内通过用手掌有针对性地拍打键盘来生成几个字符。

这不是精确的科学。

似乎关键是将 termios 置于非阻塞模式,然后使用 usleep 进行轮询。将 std::cinread 混合似乎也打破了这一点;坚持 read.

termios enter_raw() { /* ... */ }

int main() {
    termios orig = enter_raw();
    while(true) {
        termios block; tcgetattr(STDOUT_FILENO, &block);
        termios nonblock = block;
        nonblock.c_cc[VMIN] = 0;
        
        char c0;
        read(STDIN_FILENO, &c0, 1);
        if(std::isprint(c0)) {
            std::cout << "Pressed: " << c0 << "\r\n";
        } else if(c0 == '\e') {
            tcsetattr(STDOUT_FILENO, TCSANOW, &nonblock);
            std::string result;
            result.push_back('\e');
            for(int i = 0; i < 20; i++) {
                char c;
                if(read(STDIN_FILENO, &c, 1) == 1) {
                    result.push_back(c);
                }
                usleep(5);
            }
            tcsetattr(STDOUT_FILENO, TCSANOW, &block);
            std::cout << "Pressed: " << absl::CEscape(result) << "\r\n";
        } else if(c0 == 35) {
            break; // received ctrl-c
        }
    }
    tcsetattr(STDOUT_FILENO, TCSAFLUSH, &orig);
    return 0;
}