0

我正在用 C++ 编写 readline 替换,我想以原始模式处理终端输入,包括特殊/转义键,如 "up arrow" \e[A。但是,我还希望能够区分单次按下 Escape 键\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。答案不必使用<iostream>;只要能完成工作,它就可以使用 C 风格的终端 IO。

4

2 回答 2

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 秒这样的短间隔,您也可以在此时间限制内通过用手掌准确地拍打键盘来生成几个字符。

这不是精确的科学。

于 2021-03-27T00:22:52.063 回答
0

似乎关键是将 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;
}
于 2021-03-27T16:02:19.780 回答