I'm writing a readline replacement in C++, and I want to process terminal input in raw mode, including special/escaped keys like "up arrow" \e[A
. However, I also want to be able to distinguish between a single press of the escape key \e
followed by a press of [
and a press of A
vs a press of the up arrow.
I assume that the primary difference between those two situations is that when up arrow is pressed, the input characters come in within less than a millisecond, so I thought I could do something like:
#include <termios.h>
#include <absl/strings/escaping.h>
#include <iostream>
termios enter_raw() {
termios orig;
termios raw;
tcgetattr(STDOUT_FILENO, &orig);
tcgetattr(STDOUT_FILENO, &raw);
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
raw.c_oflag &= ~OPOST;
raw.c_cflag |= CS8;
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
tcsetattr(STDOUT_FILENO, TCSAFLUSH, &raw);
return orig;
}
int main() {
termios orig = enter_raw();
while(true) {
char buf[10];
memset(buf, 0, sizeof(buf));
std::cin >> buf[0];
usleep(1000);
int actual = std::cin.readsome(buf + 1, sizeof(buf) - 2);
std::cout << "Got string: \"" << absl::CEscape(buf) << "\"\n";
if(buf[0] == 35) { break; } // received ctrl-c
}
tcsetattr(STDOUT_FILENO, TCSAFLUSH, &orig);
return 0;
}
However, the output of this is not Got string: "\033[A"
as I hoped; instead, it does Got string
three times, as it would if it was just a naive loop over characters. Varying the number of microseconds for which it sleeps does not seem to affect anything.
Is there a way to implement this kind of thing easily in C++? Is it portable to most terminals? I don't care about supporting Windows. The answer need not use <iostream>
; it can use C-style terminal IO as long as it gets the job done.
Seems like the key is to put the termios in nonblocking mode and then poll with usleep
. Mixing std::cin
with read
also seems to break this; stick to read
.
termios enter_raw() { /* ... */ }
int main() {
termios orig = enter_raw();
while(true) {
termios block; tcgetattr(STDOUT_FILENO, &block);
termios nonblock = block;
nonblock.c_cc[VMIN] = 0;
char c0;
read(STDIN_FILENO, &c0, 1);
if(std::isprint(c0)) {
std::cout << "Pressed: " << c0 << "\r\n";
} else if(c0 == '\e') {
tcsetattr(STDOUT_FILENO, TCSANOW, &nonblock);
std::string result;
result.push_back('\e');
for(int i = 0; i < 20; i++) {
char c;
if(read(STDIN_FILENO, &c, 1) == 1) {
result.push_back(c);
}
usleep(5);
}
tcsetattr(STDOUT_FILENO, TCSANOW, &block);
std::cout << "Pressed: " << absl::CEscape(result) << "\r\n";
} else if(c0 == 35) {
break; // received ctrl-c
}
}
tcsetattr(STDOUT_FILENO, TCSAFLUSH, &orig);
return 0;
}