Skip to content

Instantly share code, notes, and snippets.

@osa1
Last active February 4, 2024 06:27
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save osa1/86015faeb92f840f9328080dd9276bd9 to your computer and use it in GitHub Desktop.
Save osa1/86015faeb92f840f9328080dd9276bd9 to your computer and use it in GitHub Desktop.
ncurses alt, ctrl etc. key events
// 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) };
}
@MonkeyToiletLadder
Copy link

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment