-
-
Save osa1/86015faeb92f840f9328080dd9276bd9 to your computer and use it in GitHub Desktop.
// It turns out people don't really know how to handle Alt+ch, or F[1, 12] keys | |
// etc. in ncurses apps. Even StackOverflow is full of wrong answers and ideas. | |
// The key idea is to skip ncurses' key handling and read stuff from the stdin | |
// buffer manually. Here's a demo. Run this and start typing. ESC to exit. | |
// | |
// To compile: | |
// | |
// $ gcc demo.c -o demo -lncurses -std=gnu11 | |
#include <ncurses.h> | |
#include <signal.h> // sigaction, sigemptyset etc. | |
#include <stdlib.h> // exit() | |
#include <string.h> // memset() | |
#include <unistd.h> // read() | |
static volatile sig_atomic_t got_sigwinch = 0; | |
static void sigwinch_handler(int sig) | |
{ | |
(void)sig; | |
got_sigwinch = 1; | |
} | |
int read_stdin(); | |
int main() | |
{ | |
// Register SIGWINCH signal handler to handle resizes: select() fails on | |
// resize, but we want to know if it was a resize because don't want to | |
// abort on resize. | |
struct sigaction sa; | |
sa.sa_handler = sigwinch_handler; | |
sa.sa_flags = SA_RESTART; | |
sigemptyset(&sa.sa_mask); | |
if (sigaction(SIGWINCH, &sa, NULL) == -1) | |
{ | |
fprintf(stderr, "Can't register SIGWINCH action.\n"); | |
exit(1); | |
} | |
// Initialize ncurses | |
initscr(); | |
curs_set(1); | |
noecho(); | |
nodelay(stdscr, TRUE); | |
raw(); | |
// select() setup. You usually want to add more stuff here (sockets etc.). | |
fd_set readfds_orig; | |
memset(&readfds_orig, 0, sizeof(fd_set)); | |
FD_SET(0, &readfds_orig); | |
int max_fd = 0; | |
fd_set* writefds = NULL; | |
fd_set* exceptfds = NULL; | |
struct timeval* timeout = NULL; | |
// sigwinch counter, just to show how many SIGWINCHs caught. | |
int sigwinchs = 0; | |
// Main loop | |
for (;;) | |
{ | |
fd_set readfds = readfds_orig; | |
if (select(max_fd + 1, &readfds, writefds, exceptfds, timeout) == -1) | |
{ | |
// Handle errors. This is probably SIGWINCH. | |
if (got_sigwinch) | |
{ | |
endwin(); | |
clear(); | |
char sigwinch_msg[100]; | |
sprintf(sigwinch_msg, "got sigwinch (%d)", ++sigwinchs); | |
mvaddstr(0, 0, sigwinch_msg); | |
refresh(); | |
} | |
else | |
{ | |
break; | |
} | |
} | |
else if (FD_ISSET(0, &readfds)) | |
{ | |
// stdin is ready for read() | |
clear(); | |
int quit = read_stdin(); | |
if (quit) | |
break; | |
refresh(); | |
} | |
} | |
endwin(); | |
return 0; | |
} | |
static char* input_buffer_text = "input buffer: ["; | |
static int input_buffer_text_len = 15; // ugh | |
int read_stdin() | |
{ | |
char buffer[1024]; | |
int size = read(0, buffer, sizeof(buffer) - 1); | |
if (size == -1) | |
{ | |
// Error on read(), this shouldn't really happen as it was ready for | |
// reading before calling this. | |
return 1; | |
} | |
else | |
{ | |
// Check for ESC | |
if (size == 1 && buffer[0] == 0x1B) | |
return 1; | |
// Show the buffer contents in hex | |
mvaddstr(0, 0, input_buffer_text); | |
char byte_str_buf[2]; | |
for (int i = 0; i < size; ++i) | |
{ | |
sprintf(byte_str_buf, "%02X\0", buffer[i]); | |
int x = input_buffer_text_len + (i * 4); | |
mvaddnstr(0, x, byte_str_buf, 2); | |
if (i != size - 1) | |
mvaddch(0, x + 2, ','); | |
} | |
mvaddch(0, input_buffer_text_len + (size * 4) - 2, ']'); | |
// No errors so far | |
return 0; | |
} | |
} |
extern crate libc; | |
/// Read stdin contents if it's ready for reading. Returns true when it was able | |
/// to read. Buffer is not modified when return value is 0. | |
fn read_input_events(buf : &mut Vec<u8>) -> bool { | |
let mut bytes_available : i32 = 0; // this really needs to be a 32-bit value | |
let ioctl_ret = unsafe { libc::ioctl(libc::STDIN_FILENO, libc::FIONREAD, &mut bytes_available) }; | |
// println!("ioctl_ret: {}", ioctl_ret); | |
// println!("bytes_available: {}", bytes_available); | |
if ioctl_ret < 0 || bytes_available == 0 { | |
false | |
} else { | |
buf.clear(); | |
buf.reserve(bytes_available as usize); | |
let buf_ptr : *mut libc::c_void = buf.as_ptr() as *mut libc::c_void; | |
let bytes_read = unsafe { libc::read(libc::STDIN_FILENO, buf_ptr, bytes_available as usize) }; | |
debug_assert!(bytes_read == bytes_available as isize); | |
unsafe { buf.set_len(bytes_read as usize); } | |
true | |
} | |
} | |
fn main() { | |
// put the terminal in non-buffering, no-enchoing mode | |
let mut old_term : libc::termios = unsafe { std::mem::zeroed() }; | |
unsafe { libc::tcgetattr(libc::STDIN_FILENO, &mut old_term); } | |
let mut new_term : libc::termios = old_term.clone(); | |
new_term.c_lflag &= !(libc::ICANON | libc::ECHO); | |
unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &new_term) }; | |
// Set up the descriptors for select() | |
let mut fd_set : libc::fd_set = unsafe { std::mem::zeroed() }; | |
unsafe { libc::FD_SET(libc::STDIN_FILENO, &mut fd_set); } | |
loop { | |
let mut fd_set_ = fd_set.clone(); | |
let ret = | |
unsafe { | |
libc::select(1, | |
&mut fd_set_, // read fds | |
std::ptr::null_mut(), // write fds | |
std::ptr::null_mut(), // error fds | |
std::ptr::null_mut()) // timeval | |
}; | |
if unsafe { ret == -1 || libc::FD_ISSET(0, &mut fd_set_) } { | |
let mut buf : Vec<u8> = vec![]; | |
if read_input_events(&mut buf) { | |
println!("{:?}", buf); | |
} | |
} | |
} | |
// restore the old settings | |
// (FIXME: This is not going to work as we have no way of exiting the loop | |
// above) | |
unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &old_term) }; | |
} |
For other people trying to understand what is going on here and found the code too complicated because it touches stuff outside of the core topic (polling io to avoid wasting CPU cycles), I stripped down the file to its core concept. You can just read the read_stdin
function, the rest is just helpers so you can run it and test it on your machine, hope it helps:
#include <ncurses.h>
#include <stdbool.h>
#include <string.h> // strlen()
#include <unistd.h> // read()
void pretty_print_buffer(char buffer[static 1024], const size_t bufsize);
// ====== THE CORE CONCEPT =====
// Essentially the stdin is a buffered stream file, so it'll read as many bytes
// from the file as there are there and you'll get the amount of bytes read from it
//
// When you type something, the length that encodes the key might vary
// so you create a buffer and you attempt to `read` into it that will give you the input
// but certain keys and key combos have more than a single byte so you'll have to decode it
//
// Check the ANSI escape code wikipedia page for more info:
// https://en.wikipedia.org/wiki/ANSI_escape_code#Terminal_input_sequences
bool read_stdin(void) {
char buffer[1024] = {0};
int size = read(0, buffer, sizeof(buffer) - 1);
if (size == -1) {
return true;
}
// Check for ESC
if (size == 1 && buffer[0] == 0x1B) {
return true;
}
pretty_print_buffer(buffer, size);
return false;
}
void pretty_print_buffer(char buffer[static 1024], const size_t bufsize) {
const char *input_buffer_text = "input buffer: [";
const int input_buffer_text_len = strlen(input_buffer_text); // ugh
// Show the buffer contents in hex
mvaddstr(0, 0, input_buffer_text);
char byte_str_buf[3] = {0};
for (int i = 0; i < bufsize; ++i) {
sprintf(byte_str_buf, "%02X", buffer[i]);
int x = input_buffer_text_len + (i * 4);
mvaddnstr(0, x, byte_str_buf, 2);
if (i != bufsize - 1)
mvaddch(0, x + 2, ',');
}
mvaddch(0, input_buffer_text_len + (bufsize * 4) - 2, ']');
}
int main(void) {
// Initialize ncurses
initscr();
curs_set(1);
noecho();
nodelay(stdscr, TRUE);
raw();
printw("press a button to see it's byte representation");
refresh();
// Main loop
for (;;) {
clear();
bool err = read_stdin();
if (err) {
break;
}
refresh();
}
endwin();
return 0;
}
Is this crossplatform? I tested it on linux and pressed key up/down and it's codes looks like specific for each terminal/OS.
Is there any way to combine the raw asynchronous read approach with converting the received characters back to the values declared in ncurses (KEY_*)? And is it necessary? Your solution is very good in that you can use asynchronous input (even asio if I write in c++), but it seems to lose usability and portability. Maybe I'm wrong
You got it right, this solution is not portable. I'm not aware of any standards that map e.g. ctrl + shift + F1
to a byte sequence for the terminal implementers to use. However I tested this kind of complex key sequences many years ago and what I found was that some (maybe most) terminals follow xterm, so in tiny I decided to use the byte sequences used by xterm, in the term_input library.
I've been maintaining tiny since 2017 and we have some users, and no one complained so far that e.g. alt + right_arrow
isn't working in their terminal. It may be that we don't have too many users, or they all use the same few terminals, I'm not sure.
@osa1 Good. I figured out how to work with it. And yet, if I don't want to manually match sequences in the code, is there any library (possibly inside ncurses) that already knows how to do this? Thanks
@daniilrozanov In tiny I have my own macro for this.
Usage: https://github.com/osa1/tiny/blob/54fecca31b6d10d41d8f54c4b806674ddc76740f/crates/term_input/src/lib.rs#L99-L210
Macro implementation: https://github.com/osa1/tiny/tree/54fecca31b6d10d41d8f54c4b806674ddc76740f/crates/term_input_macros
The macro generates matching code that checks each input byte once.
I'm not aware of any other libraries that do this.
Hello again. I figured out how to do this portable.
#include <stdio.h>
#include <stdlib.h>
#include <termcap.h>
#define fatal(msg) \
{ \
printf(msg); \
exit(1); \
}
#define fatall(msg, arg) \
{ \
printf(msg, arg); \
exit(1); \
}
int main() {
#ifdef unix
static char term_buffer[2048];
#else
#define term_buffer 0
#endif
char *termtype = getenv("TERM");
int success;
success = tgetent(term_buffer, termtype);
if (success < 0)
fatal("Could not access the termcap data base.\n");
if (success == 0)
fatall("Terminal type `%s' is not defined.\n", termtype);
char *DW;
char *UP;
DW = tgetstr("kd", 0); // kd means key down.
UP = tgetstr("ku", 0); // ku - key up
printf("%s\n", BC);
printf("%s\n", UP);
return 0;
}
termcap is the library that help to access to all sorts of terminal's capabilities. Access gets through 2 char alias for capability. For example tgetstr("ku", 0)
will make UP contain [27, 79, 65]
on my terminal (same as in your code)
Hi, How did you acquire the knowledge and skill to write such a program? Im interested in making games with c++ and ncurses. I do get simliar results when reading in characters with getch just they are separate values but they match the hex that your program outputs. Im just having difficulty understanding your program. Are there any resources you would recomend?