-
-
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) }; | |
| } |
@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)
You got it right, this solution is not portable. I'm not aware of any standards that map e.g.
ctrl + shift + F1to 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_arrowisn'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.