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.
");
while (1)
{
char key;
if (read(STDIN_FILENO, &key, 1) != 1)
{
fprintf(stderr, "read error or EOF
");
break;
}
if (key == EOF_KEY)
{
fprintf(stderr, "%d (control-D or EOF)
", 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
", selret);
if (selret == 1)
printf("Got ESC: possible sequence
");
else if (selret == -1)
printf("error %d: %s
", errno, strerror(errno));
else
printf("esc key standalone
");
}
else
printf("%i
", (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.
");
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)
", 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('
');
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
", selret);
if (selret == 1)
printf("Got ESC: possible sequence
");
else if (selret == -1)
printf("error %d: %s
", errno, strerror(errno));
else
printf("esc key standalone
");
}
else
printf("%i
", (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
$