Recently, I had some free time, so I decided to improve my knowledge of the C language. So far, they are limited to the level of simple university labs and S. Prath's C Programming Language, so first I set myself a simple task - to write a console snake. By the time when the display of the playing field and the snake itself on the screen and the part responsible for its movement across the field was written, two problems appeared:
- data was sent to the program only after pressing Enter
- program stopped reading user input
You could solve them by using curses getch (), but it was boring and not interesting.
About how these problems were solved under the cut.
To begin with, it was necessary to cope with reading the user's keystrokes. I implemented this through a call to the getc () function, but when calling this function, the program began to read characters only after Enter was pressed. In the course of a short search, it was discovered that this is not a getc () problem, but a Linux terminal problem, more precisely a terminal driver, which by default works in canonical mode, i.e. This terminal driver waits for the end of the input string by pressing Enter, and then sends the given string to the stdin program. The solution to this problem was also found: transferring the driver to non-canonical mode. This is implemented very simply - the header file termios.h is connected and two functions are written:
The first function to switch to non-canonical mode:
void set_keypress(void) { struct termios new_settings; tcgetattr(0,&stored_settings); new_settings = stored_settings; new_settings.c_lflag &= (~ICANON); new_settings.c_lflag &= (~ECHO); new_settings.c_cc[VTIME] = 0; new_settings.c_cc[VMIN] = 1; tcsetattr(0,TCSANOW,&new_settings); return; }
The second function to return to the original state:
void reset_keypress(void) { tcsetattr(0,TCSANOW,&stored_settings); return; }
Variable stored_settings should be declared as global:
static struct termios stored_settings;
Then, at the beginning of the program, the first function is called, and at the end, respectively, the second is called.
In the final form, the test example looks like this:
#include <stdlib.h> #include <stdio.h> #include <termios.h> #include <string.h> static struct termios stored_settings; void set_keypress(void) { struct termios new_settings; tcgetattr(0,&stored_settings); new_settings = stored_settings; new_settings.c_lflag &= (~ICANON & ~ECHO); new_settings.c_cc[VTIME] = 0; new_settings.c_cc[VMIN] = 1; tcsetattr(0,TCSANOW,&new_settings); return; } void reset_keypress(void) { tcsetattr(0,TCSANOW,&stored_settings); return; } int main(void) { set_keypress(); printf("Test: "); while(1) { // putchar , putchar(getchar()); } return 0; }
Now, my snake was able to read user input character by character. But there was a second problem: the program still stopped and waited until the user presses a key. An Internet search began again, during which I often stumbled upon the “solution” of this problem in the form of what was described above. The result of the search was the advice to use the select () function.
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *utimeout);
Of the parameters taken by this function, we are only interested in n, readfds and utimeout:
- n - this parameter must not exceed one maximum file descriptor in any of the sets. In other words, you must define the maximum integer value for all your descriptors, increase it by 1, and pass the result as parameter n.
- readfds - this set is monitored for read data in one or more descriptors. After returning from select, the readfs set will be cleared of all descriptors, except those that have data available for immediate reading using the recv () function (for sockets) or read () (for pipe channels, files, and sockets).
- utimeout is the maximum time during which select will wait for a status change. If this parameter is set to NULL, select will be blocked indefinitely, waiting for events in the descriptors. When set to 0 seconds, select will return immediately.
Therefore, the select () call will look like this:
select(1, &rfds, NULL, NULL, &tv);
To indicate that events will only be considered for stdin, you need to call
FD_SET(0, &rfds);
The struct timeval structure is defined as follows:
struct timeval { time_t tv_sec; long tv_usec; };
As a result, we get this test example (let's not forget to switch the terminal to non-canonical mode, which has already been discussed above):
#include <stdio.h> #include <stdlib.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <termios.h> static struct termios stored_settings; void set_keypress(void) { struct termios new_settings; tcgetattr(0,&stored_settings); new_settings = stored_settings; new_settings.c_lflag &= (~ICANON & ~ECHO); new_settings.c_cc[VTIME] = 0; new_settings.c_cc[VMIN] = 1; tcsetattr(0,TCSANOW,&new_settings); return; } void reset_keypress(void) { tcsetattr(0,TCSANOW,&stored_settings); return; } int main(void) { fd_set rfds; struct timeval tv; int retval; set_keypress(); while(1) { FD_ZERO(&rfds); FD_SET(0, &rfds); tv.tv_sec = 0; tv.tv_usec = 0; retval = select(2, &rfds, NULL, NULL, &tv); if (retval) { printf("Data is available now.\n"); getc(stdin); } else { printf("No data available.\n"); } usleep(100000); } reset_keypress(); exit(0); }
Material used: