【问题标题】:A way to distinguish `\e` from escaped keys like `\e[A` in C++一种区分 `\e` 和 C++ 中的 `\e[A` 等转义键的方法
【发布时间】:2021-03-26 23:28:45
【问题描述】:

我正在用 C++ 编写 readline 替换,我想以原始模式处理终端输入,包括特殊/转义键,如“向上箭头”\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: "\033[A";相反,它会执行三次Got string,就像它只是一个简单的字符循环一样。改变它休眠的微秒数似乎不会影响任何事情。

有没有办法在 C++ 中轻松实现这种东西?它可以移植到大多数终端吗?我不在乎支持Windows。答案不用&lt;iostream&gt;;只要能完成工作,它就可以使用 C 风格的终端 IO。

【问题讨论】:

    标签: c++ terminal ansi-escape vt100


    【解决方案1】:
    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.
    

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

    你似乎想要的是:

       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 秒。

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

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

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

    这不是精确的科学。

    【讨论】:

    • 这是一种方法,但我想看看这些多字节按键在实践中的速度;如果它比 0.1 秒快得多,那么更好的方法是保持完全原始模式,并在每个字符进入时为其添加时间戳。
    • 在我的笔记本电脑上使用 xfce4-terminal 时,多字节按键中字符之间的挂钟延迟似乎小于 30 微秒。所以我会等待其他答案,如果没有人提出任何问题,请写下我自己的带时间戳的答案并接受它。
    【解决方案2】:

    似乎关键是将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;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-05-19
      • 1970-01-01
      • 1970-01-01
      • 2015-02-19
      • 2018-05-30
      • 2012-01-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多