For some of my terminal videogames I need to accept input in raw mode, which can be done in Windows by using the nonstandard function getch
coming from the header conio.h
but needs a little bit more effort for Linux and MacOS.
So I decided some time (more than a year) ago to write a small reusable header called cross_platform.hpp
by reading some documentation and copypasting some code from various old forums, and the result is here and is reported below.
#include <cstdio>
#ifdef _WIN32
#include <windows.h>
#include <mmsystem.h>
// #pragma comment(lib,"winmm.lib")
#include <conio.h>
void flushInput() {
// Flush the input buffer (discard data not read yet)
HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE);
FlushConsoleInputBuffer(hInput);
}
#elif __APPLE__
#include <termios.h>
#include <unistd.h>
struct termios orig_termios;
void term_echooff() {
struct termios noecho;
tcgetattr(0, &orig_termios);
noecho = orig_termios;
noecho.c_lflag &= ~ECHO;
tcsetattr(0, TCSANOW, &noecho);
}
void flushInput() {
// Flush stdin (discard data not read yet)
tcflush(STDIN_FILENO, TCIFLUSH);
}
#elif __linux__
#include <unistd.h>
#include <termios.h>
char getch(void) {
char buf = 0;
struct termios old = {0};
fflush(stdout);
if(tcgetattr(0, &old) < 0)
perror("tcsetattr()");
old.c_lflag &= ~ICANON;
old.c_lflag &= ~ECHO;
old.c_cc[VMIN] = 1;
old.c_cc[VTIME] = 0;
if(tcsetattr(0, TCSANOW, &old) < 0)
perror("tcsetattr ICANON");
if(read(0, &buf, 1) < 0)
perror("read()");
old.c_lflag |= ICANON;
old.c_lflag |= ECHO;
if(tcsetattr(0, TCSADRAIN, &old) < 0)
perror("tcsetattr ~ICANON");
// printf("%c\n", buf);
return buf;
}
void flushInput() {
// Flush stdin (discard data not read yet)
tcflush(STDIN_FILENO, TCIFLUSH);
}
#endif
The need for a cross platform function, flushInput
, which empties and flushes the input stream came later.
MacOS
I also have to say that the part for MacOS doesn't work at all, I know that I could use the following code...
system("stty raw -echo")
...but for some reason in the projects based on my library Sista it results in what seems to be echo mode, no matter what combination of raw and echo I try.
As you can see instead of a rectangular grid the result is closer to the mess that you would expect in echo mode or when double printing some characters.
I am aware that in this community the submitted code should be already working: that's why I write this in a separate section and I don't include it in the questions, but an hint would be greatly appreciated if the issue is so obvious.
Here is the code without the MacOS related part.
#include <cstdio>
#ifdef _WIN32
#include <windows.h>
#include <mmsystem.h>
// #pragma comment(lib,"winmm.lib")
#include <conio.h>
void flushInput() {
// Flush the input buffer (discard data not read yet)
HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE);
FlushConsoleInputBuffer(hInput);
}
#elif __linux__
#include <unistd.h>
#include <termios.h>
char getch(void) {
char buf = 0;
struct termios old = {0};
fflush(stdout);
if(tcgetattr(0, &old) < 0)
perror("tcsetattr()");
old.c_lflag &= ~ICANON;
old.c_lflag &= ~ECHO;
old.c_cc[VMIN] = 1;
old.c_cc[VTIME] = 0;
if(tcsetattr(0, TCSANOW, &old) < 0)
perror("tcsetattr ICANON");
if(read(0, &buf, 1) < 0)
perror("read()");
old.c_lflag |= ICANON;
old.c_lflag |= ECHO;
if(tcsetattr(0, TCSADRAIN, &old) < 0)
perror("tcsetattr ~ICANON");
// printf("%c\n", buf);
return buf;
}
void flushInput() {
// Flush stdin (discard data not read yet)
tcflush(STDIN_FILENO, TCIFLUSH);
}
#endif
Questions
- Is there a smarter and highly compatible way of achieving this in Linux?
- Are there some totally unnecessary parts in this "old" code that may have negative side effects (such as leaving the terminal unable to display (but not to accept) input if the program using the function crashes)?
- Is there a "standard" way (so without
conio.h
'sgetch
) to achieve this in Windows?
Also general learning tips on the topic of terminals are appreciated.
2 Answers 2
Use an existing terminal I/O library
Instead of reinventing the wheel and writing all this non-portable, low-level I/O code by hand, consider looking for already existing libraries that take care of this for you.
In particular, consider using a curses library. This is basically a standard, platform-independent API to read raw input from, as well as move the cursor and write characters to a text console. There are several implementations of the curses API; for DOS/Windows PDCurses is a popular one, for all the other operating systems ncurses is the most widely used one.
There are also other libraries with different APIs. For example, if you want something that is better suited for C++, then look at FTXUI. I strongly recommend that you check out which is best suited for your needs.
-
\$\begingroup\$ Thank you for your answer, I didn't know PDCurses existed, this was helpful; I will try to see if this solves the MacOS problem as well. \$\endgroup\$FLAK-ZOSO– FLAK-ZOSO2024年12月09日 07:26:06 +00:00Commented Dec 9, 2024 at 7:26
-
\$\begingroup\$ Oh, wait. @G. Sliepen, may I ask? If I want to use ncurses, I need to do all I/O with ncurses functions? I cannot use the standard ones anymore until I do endwin()? \$\endgroup\$FLAK-ZOSO– FLAK-ZOSO2024年12月09日 09:40:30 +00:00Commented Dec 9, 2024 at 9:40
-
1\$\begingroup\$ @FLAK-ZOSO You can in principle still use standard I/O functions, but that would interfere with ncurses handling. Unless you really know what you're doing, it's best to stick with one way. \$\endgroup\$G. Sliepen– G. Sliepen2024年12月09日 16:31:04 +00:00Commented Dec 9, 2024 at 16:31
I finally found a solution to the MacOS issue, that I posted here. In the revisions you can see what I changed.
Edit to include canonical mode
What changed is that now non-canonical mode is set. This avoids the behaviour described in the question.
noecho.c_lflag &= ~(ECHO | ICANON);
I was already doing the same for Linux, but for some reason I had forgotten to do it for MacOS as well.
old.c_lflag &= ~ICANON;
This allows me not to depend on any other library and still handle the input with iostream
or such.
The answer by @G. Sliepen
is extremely useful, definitely more than mine for what concerns the general public of all those developers who don't use their own library. Thus I will keep it as the accepted answer despite this solution being the one I was actually looking for.
#include <csdtio>
. Is this really necessary vs.#include <stdio.h>
? \$\endgroup\$