代码之家  ›  专栏  ›  技术社区  ›  Lassi

区分ansi终端上的esc键和其他特殊键[重复]

  •  0
  • Lassi  · 技术社区  · 7 年前

    我的最终目标是区分我的压力 ESC (ASCII) 27 )在我的键盘上,我按下 渐次 键盘上的键(它转换为 27 91 67 )我正在使用 termios 使我的终端进入非规范模式。

    我想我知道有两种选择:

    • 等待一些任意的时间,看看是否有东西进来(看起来很老套)
    • 检查stdin是否为空

    我正试着做后者。为此,我想利用 select 看看是否 stdin 是否为空。

    问题

    选择 似乎总是返回0(超时过期)。这似乎很奇怪,原因有二:

    1. 我想如果我打了之后什么也没打 ESC ,然后它将返回-1,因为它看不到stdin中的任何内容可供读取
    2. 我想如果我打字 渐次 ,然后我会得到一个 1 回来了,因为它看到了 二十七 有一个 91 和A 67 阅读

    这两件事都没有发生,所以我只是不明白 选择 或者像我想的那样标准进出。

    问题

    为什么不 选择 在我的示例中,返回除0以外的任何内容?是否可以检查 斯坦丁 是空的吗?其他库如何处理这个问题?

    最小、完整且可验证的示例

    我在MacOS High Sierra和Ubuntu16上运行这个程序,结果是一样的。

    来源:

    #include <stdio.h>
    #include <string.h>
    #include <termios.h>
    #include <sys/select.h>
    #include <sys/types.h>
    #include <sys/time.h>
    #include <unistd.h>
    #include <errno.h>
    
    int main() {
            // put terminal into non-canonical mode
            struct termios old;
            struct termios new;
            int fd = 0;  // stdin
            tcgetattr(fd, &old);
            memcpy(&new, &old, sizeof(old));
            new.c_lflag &= ~(ICANON | ECHO);
            tcsetattr(fd, TCSANOW, &new);
    
            // loop: get keypress and display (exit via 'x')
            char key;
            printf("Enter a key to see the ASCII value; press x to exit.\n");
            while (1) {
                    key = getchar();
    
                    // check if ESC
                    if (key == 27) {
                            fd_set set;
                            struct timeval timeout;
                            FD_ZERO(&set);
                            FD_SET(STDIN_FILENO, &set);
                            timeout.tv_sec = 0;
                            timeout.tv_usec = 0;
                            int selret = select(1, &set, NULL, NULL, &timeout);
                            printf("selret=%i\n", selret);
                            if (selret == 1) {
                                    // input available
                                    printf("possible sequence\n");
                            } else if (selret == -1) {
                                    // error
                                    printf("err=%s\n", strerror(errno));
                            } else {
                                    // just esc key
                                    printf("esc key standalone\n");
                            }
                    }
    
                    printf("%i\n", (int)key);
                    if (key == 'x') { break; }
            }
    
            // set terminal back to canonical
            tcsetattr(fd, TCSANOW, &old);
            return 0;
    }
    

    产量

    gns-mac1:sandbox gns$ ./seltest 
    Enter a key to see the ASCII value; press x to exit.
    selret=0
    esc key standalone
    27
    selret=0
    esc key standalone
    27
    91
    67
    120
    
    0 回复  |  直到 8 年前
        1
  •  3
  •   Jonathan Leffler    8 年前

    我想问题是你用的 getchar() 标准I/O库中需要使用文件描述符I/O的函数( read() )

    简单的例子

    下面是对您的代码的直接改编(在运行MacOS High Sierra 10.13.2的MacBook Pro上进行了测试),它产生了您和我想要的答案。

    #include <stdio.h>
    #include <string.h>
    #include <termios.h>
    #include <sys/select.h>
    #include <sys/time.h>
    #include <unistd.h>
    #include <errno.h>
    
    enum { ESC_KEY = 27 };
    enum { EOF_KEY = 4  };
    
    int main(void)
    {
        // put terminal into non-canonical mode
        struct termios old;
        struct termios new;
        int fd = 0;      // stdin
        tcgetattr(fd, &old);
        //memcpy(&new, &old, sizeof(old));
        new = old;
        new.c_lflag &= ~(ICANON | ECHO);
        tcsetattr(fd, TCSANOW, &new);
    
        // loop: get keypress and display (exit via 'x')
        //int key;
        printf("Enter a key to see the ASCII value; press x to exit.\n");
        while (1)
        {
            char key;
            if (read(STDIN_FILENO, &key, 1) != 1)
            {
                fprintf(stderr, "read error or EOF\n");
                break;
            }
            if (key == EOF_KEY)
            {
                fprintf(stderr, "%d (control-D or EOF)\n", key);
                break;
            }
    
            // check if ESC
            if (key == 27)
            {
                fd_set set;
                struct timeval timeout;
                FD_ZERO(&set);
                FD_SET(STDIN_FILENO, &set);
                timeout.tv_sec = 0;
                timeout.tv_usec = 0;
                int selret = select(1, &set, NULL, NULL, &timeout);
                printf("selret=%i\n", selret);
                if (selret == 1)
                    printf("Got ESC: possible sequence\n");
                else if (selret == -1)
                    printf("error %d: %s\n", errno, strerror(errno));
                else
                    printf("esc key standalone\n");
            }
            else 
                printf("%i\n", (int)key);
            if (key == 'x')
                break;
        }
    
        // set terminal back to canonical
        tcsetattr(fd, TCSANOW, &old);
        return 0;
    }
    

    样本输出(程序 esc29 ):

    $ ./esc29   # 27 isn't a 2-digit prime
    Enter a key to see the ASCII value; press x to exit.
    115
    100
    97
    115
    100
    selret=1
    Got ESC: possible sequence
    91
    68
    selret=1
    Got ESC: possible sequence
    91
    67
    selret=0
    esc key standalone
    selret=0
    esc key standalone
    selret=0
    esc key standalone
    100
    100
    4 (control-D or EOF)
    $
    

    我按下左/右箭头键,得到“可能的顺序”报告;我按下触摸条上的esc键,得到“esc键独立”。其他字符似乎是可信的,当按下control-d时,代码被篡改为中断。

    复杂的例子

    此代码一次最多读取4个字符,并处理接收到的字符。有两个嵌套循环,因此我使用 goto end_loops; (两次!)从内环中分离出两个环。我也使用 atexit() 函数执行大多数可以执行的操作,以确保即使程序未通过 main() 程序。(我们可以讨论代码是否也应该使用 at_quick_exit() 函数这是c11的特性,但不是posix的特性。)

    如果代码读取多个字符,它会扫描它们,查找 ESC (逃跑)如果它找到了一个,并且还剩下任何数据,那么它将报告转义序列(可能是一个功能键序列)。如果找不到更多的字符,它将使用 select() 与以前一样,决定esc序列中是否有更多字符,或者这是一个独立的esc。实际上,计算机比人类快得多,所以它要么读取单个字符,要么读取完整的序列。我使用一个长度为4的数组,因为我认为它比键盘生成的最长键序列长;我很乐意将其增加到8(或任何其他更大的数字)。这样做的唯一缺点是,缓冲区必须在需要读取字符的地方可用,而不太可能发生读取多个字符的情况(例如,因为程序在计算时正在累积输入)。还有一个可能性,从一个功能键或箭头键的esc将是最后一个字符,适合缓冲区在这种情况下,额外的阅读是必要的。祝你好运,用这个程序证明你不是一个足够快的打字员。你需要在某个地方添加睡眠代码,以允许字符在读取之前积累起来。

    因此,这主要展示了一些额外的技术,但它可以作为考虑处理的另一种方式。

    #include <errno.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/select.h>
    #include <sys/time.h>
    #include <termios.h>
    #include <unistd.h>
    
    enum { ESC_KEY = 27 };
    enum { EOF_KEY = 4  };
    
    /* These two need to be set in main() but accessed from reset_tty() */
    static int fd = STDIN_FILENO;
    static struct termios old;
    
    // set terminal back to canonical
    static void reset_tty(void)
    {
        tcsetattr(fd, TCSANOW, &old);
    }
    
    int main(void)
    {
        struct termios new;
        tcgetattr(fd, &old);
        new = old;
        new.c_lflag &= ~(ICANON | ECHO);
        tcsetattr(fd, TCSANOW, &new);
        atexit(reset_tty);      // Ensure the terminal is reset whenever possible
    
        printf("Enter a key to see the ASCII value; press x to exit.\n");
        char keys[4];
        int nbytes;
        while ((nbytes = read(fd, keys, sizeof(keys))) > 0)
        {
            for (int i = 0; i < nbytes; i++)
            {
                char key = keys[i];
                if (key == EOF_KEY)
                {
                    fprintf(stderr, "%d (control-D or EOF)\n", key);
                    goto end_loops;
                }
                else if (key == ESC_KEY && nbytes > i + 1)
                {
                    printf("Got ESC sequence:");
                    for (int j = i; j < nbytes; j++)
                        printf("%4d", keys[j]);
                    putchar('\n');
                    break;
                }
                else if (key == ESC_KEY)
                {
                    fd_set set;
                    struct timeval timeout;
                    FD_ZERO(&set);
                    FD_SET(fd, &set);
                    timeout.tv_sec = 0;
                    timeout.tv_usec = 0;
                    int selret = select(1, &set, NULL, NULL, &timeout);
                    printf("selret=%i\n", selret);
                    if (selret == 1)
                        printf("Got ESC: possible sequence\n");
                    else if (selret == -1)
                        printf("error %d: %s\n", errno, strerror(errno));
                    else
                        printf("esc key standalone\n");
                }
                else 
                    printf("%i\n", (int)key);
                if (key == 'x')
                    goto end_loops;
            }
        }
    
    end_loops:
        return 0;
    }
    

    样本输出(程序 esc67 ):

    $ ./esc67
    Enter a key to see the ASCII value; press x to exit.
    65
    90
    97
    122
    selret=0
    esc key standalone
    Got ESC sequence:  27  91  65
    Got ESC sequence:  27  91  66
    Got ESC sequence:  27  91  67
    Got ESC sequence:  27  91  68
    Got ESC sequence:  27  79  80
    selret=0
    esc key standalone
    97
    Got ESC sequence:  27  91  67
    97
    Got ESC sequence:  27  91  67
    120
    $