7

My end goal is to distinguish between my pressing Esc (ASCII 27) on my keyboard, and me pressing the key on my keyboard (which translates to a sequence of 27 91 67). I am using termios to put my terminal into non-Canonical mode.

I think I understand there are two options:

  • Wait some arbitrary amount of time to see if something comes in (seems hacky)
  • Check STDIN to see if it is empty

I'm trying to do the latter. To that end, I'm trying to use select to see if stdin is empty or not.

The Problem

select always seems to return 0 (timeout expires). That seems odd for two reasons:

  1. I figured that if I didn't type anything after hitting Esc, then it would return -1 since it doesn't see anything left in stdin to read
  2. I figured that if I typed , then I would get a 1 returned since it sees that right after 27 there is a 91 and a 67 to read

Neither of those things happens, so I'm afraid I just don't understand select or standard in/out like I thought I did.

The Question(s)

Why doesn't select return anything except 0 in my example? Is it possible to check if stdin is empty? How do other libraries handle this?

Minimal, Complete, and Verifiable Example

I am running this on both MacOS High Sierra and Ubuntu 16 with equal results.

Source:

#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;
}

Output

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
Greg Schmit
  • 4,275
  • 2
  • 21
  • 36

1 Answers1

4

I think the problem is that you're using getchar() — a function from the standard I/O library — where you need to use file descriptor I/O (read()).

Simple example

Here's a straight-forward adaptation of your code (tested on a MacBook Pro running macOS High Sierra 10.13.2), that produces the answer you and I want.

#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;
}

Sample output (program 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)
$

I pressed the left/right arrow keys and got 'possible sequence' reported; I pressed the ESC on the touch strip and got 'ESC key standalone'. Other characters seem plausible, the code was rigged to break when control-D is pressed.

Complex example

This code reads up to 4 characters at a time, and processes those that are received. There are two nested loops, so I use a goto end_loops; (twice!) to break out of the both loops from the inner loop. I also use the atexit() function to do most of what can be done to ensure that the terminal attributes are reset to the sane state even if the program doesn't exit via the main() program. (We can debate whether the code should also use the at_quick_exit() function — it's a feature of C11 but not of POSIX.)

If the code reads multiple characters, it scans through them, looking for ESC (escape). If it finds one and there is any data left, then it reports the escape sequence (presumably a function key sequence). If it doesn't find any more characters, it uses select() as before to decide whether there are more characters in an ESC sequence or if this is a standalone ESC. In practice, the computer is hugely faster than a mere human, so it either reads a single character or the complete sequence. I use an array of length 4 since I think that's longer than the longest key sequence generated from the keyboard; I'd be happy to increase it to 8 (or any other larger number). The only downside to this is that the buffer must be available where characters need to be read in the unlikely event that several characters are read (e.g. because the program was computing while input was accumulating). There's also a chance that the ESC from a function key or arrow key will be the last character that fits in the buffer — in which case, extra reading is necessary. Good luck in demonstrating that with this program as written — you're not a fast enough typist. You'd need to add sleep code somewhere to allow characters to accumulate before it reads them.

So, this mainly shows a couple of extra techniques, but it could be useful as an alternative way of thinking about the processing.

#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;
}

Sample output (program 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
$
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • Can I ask you why you named the program esc29 and why you have a comment that says "27 isn't a 2-digit prime"? It seems like an easter egg, or something that is really obvious that I'm missing. Is it good luck to name a program with a 2 digit prime? – Greg Schmit Dec 31 '17 at 04:55
  • It's a joke — it's an accurate comment about the numeric properties of two numbers. If you look at my answers here on SO, or in my [SOQ — Stack Overflow Questions](https://github.com/jleffler/soq) repository on GitHub, you'll find that the programs there mostly have a few letters (usually 2-4) followed by a 2-digit prime. You'd also find (in `src/scripts`) a script `rfn.sh` which I use to create the file names, and `ddpr.sh` which generates a double-digit prime. It's a signature quirky thing I use. There's no other significance to it than that. – Jonathan Leffler Dec 31 '17 at 04:58
  • oh, haha, nice! – Greg Schmit Dec 31 '17 at 05:00
  • I've added an alternative way of looking at the processing which gets the whole key sequence for an arrow key at once. I'm not certain it's better than the first — there are ramifications both ways. It also shows the use of `atexit()` which could be important if the program might call `exit()` from somewhere other than the loops in `main()` — e.g. an error report and exit function. – Jonathan Leffler Dec 31 '17 at 05:53
  • Nice, I've never used atexit -- in the project I'm working on now, I registered signal handlers for sigint/sigkill to reset termios. – Greg Schmit Dec 31 '17 at 05:55
  • I like your method of grabbing potentially multiple bytes at once though. – Greg Schmit Dec 31 '17 at 05:56
  • And then some blighter comes along with a SIGPIPE or SIGHUP or SIGQUIT — and you weren't catching those. Actually, `atexit()` doesn't help there either; it only helps when the `exit()` function is called. – Jonathan Leffler Dec 31 '17 at 05:56
  • sighup I don't think matters (if signal hangup then the tty is gone I think), but yeah I actually registered a little list of signals and then in the handler I reset termios, and then set signal to SIG_DFL and then raise the signal to ensure the default stuff happens (like exit codes being correct). – Greg Schmit Dec 31 '17 at 05:58
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/162237/discussion-between-jonathan-leffler-and-greg-schmit). – Jonathan Leffler Dec 31 '17 at 05:59
  • The second solution forgets to account for pasting stuff e.g. from browser, as far as I can tell. – Adrijaned Jun 16 '20 at 16:40
  • @Adrijaned — I’m not sure what you mean. Can you explain? – Jonathan Leffler Jun 16 '20 at 19:09
  • In the description of your second code you claim that it may break if ESC was at the end of buffer, but that's highly improbable since no one is that quick typist - however, if one was copy-pasting stuff into the program for whatever reason, he may actually be "typing" fast enough, and e.g. (shitty) websites can easily contain junk invisible characters that may be copied. On closer inspection of the code I believe it would still work, however how would one deal with copy-pasting character sequences that may contain escapes in the middle into the program? (Or piping them in?) – Adrijaned Jun 17 '20 at 12:56
  • Example paste that does not actually contain ESC sequences, but your code detects them `iaaaiaaa:w` EDIT: Turns out StackOverflow autoatically strips them, put `696161611b696161611b3a770a` into https://www.rapidtables.com/convert/number/hex-to-ascii.html or similar. – Adrijaned Jun 17 '20 at 13:03