-
-
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) }; | |
} |
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)
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