Event Loops and NCurses

Yesterday I wrote a blog post talking about Event Loops, this was a pre-cursor to discussing where I am using them in an application I’m currently developing which uses NCurses at its core.

Unfortunately the application is an R&D project and is not public yet (still very early in development), but it is written in C, has a TUI front end and handles many windows and network connections simultaneously so I am using libuv heavily underneath. For this post I’ll be discussing the integration between libuv and NCurses.

There are a few key places I have integrated the two libraries, primarily keyboard/mouse input, resize and refresh handling.

Keyboard / Mouse Input

Since my application may have multiple windows doing different things at the same time I wanted a way to have the input routines in the event loop so that input did not block everything else and vice versa. There are a couple of ways that this would be possible, this code snipped below is essentially how I did this:

// Set locale to environment
setlocale(LC_ALL, "");

// Allow mouse input (do before init or bad things happen)
printf("\033[?1003h\n");

// Initialise ncurses
initscr();

// Give NCurses control of input
keypad(stdscr, TRUE);

// Don't echo input to screen
noecho();

// Allow ncurses to capture ctrl-c / ctrl-z
raw();

// Make sure non-blocking mode is set on the keyboard
int val = fcntl(STDIN_FILENO, F_GETFL, 0);
if (val != -1)
{
    fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK);
}

// Create a polling handle
uv_poll_t *poll_handle;
poll_handle = malloc(sizeof(uv_poll_t));

// Arbitrary data for the callback
poll_handle->data = my_struct;

// Add the handle to the event loop for STDIN polling and start checking for readable events
uv_poll_init(loop, poll_handle, STDIN_FILENO);

// parse_key is the input handling callback
uv_poll_start(poll_handle, UV_READABLE, parse_key);

This is a simplified version of my code with the main loop (just called ‘loop’ here) already established and running at this point. Of course we need a function called ‘parse_key’ too. I’m going to greatly simplify that below:

// When a key is pressed the polling fires a callback to here
static void parse_key(uv_poll_t *handle, int status, int events)
{
    wint_t key;
    (void) status;
    (void) events;
    app_struct_t *my_struct = (app_struct_t*) handle->data;
    while (get_wch(&key) != ERR)
    {
        handle_keypress(my_struct, key);
    }
}

This basically gets the keypress into a wint_t type (I’m dealing with wide characters here) and pass it on. It is possible that a single event could hold multiple keypresses so we try to retrieve them all. handle_keypress() is very lightweight and can store keypresses if there are many to handle, but at a later date I’ll likely make this a FIFO buffer to another thread so as to not hold up the event loop longer than it needs to.

You’ll also need to close this loop when the execution ends as below:

static void free_handle(uv_handle_t *handle)
{
    free(handle);
}

// Free the key polling from the event loop when no longer required
static void stop_key_loop(uv_poll_t *handle)
{
    uv_poll_stop(handle);

    // When the polling has ended and the handle is no longer in use, call the free function to delete it
    uv_close((uv_handle_t*) handle, free_handle);
}

Resize

When a terminal is resized the signal SIGWINCH is fired. This is useful for NCurses applications because whilst the windows themselves can be automatically resized the content of windows may also need adjusting.

// Create an instance of a signal event handler
signal_handle = malloc(sizeof(uv_signal_t));
signal_handle->data = my_struct;

// Add to main loop and watch for SIGWINCH
uv_signal_init(loop, signal_handle);
uv_signal_start(signal_handle, resize_signal, SIGWINCH);

The resize_signal() function then handles everything with the new size calculations:

// Signal handler callback for SIGWINCH
static void resize_signal(uv_signal_t *handle, int signum)
{
    (void) signum;
    app_struct_t *my_struct = (app_struct_t*)handle->data;

    // Clear the screen
    clear();
    endwin();

    // Refresh to clear screen buffers too
    refresh();

    // NCurses internal resize functions, 0 gets new coordinates
    resizeterm(0, 0);

    // Set the background colour back to what it was
    bkgd(COLOR_PAIR(4));

    // Get the new coordinates
    getmaxyx(stdscr, my_struct->rows, my_struct->cols);

    // Redraw the status bar
    draw_status(my_struct);
}

Refresh

The application I’m writing could receive one byte or cursor movement to render to screen at a time, or KB of data in one go. It was getting difficult to predict and refresh the NCurses pad I was using appropriately without a large performance hit when there was a lot happening at the same time.

To mitigate this and simplify whenever there might be an action that required a refresh I would start a 10ms countdown timer to trigger a refresh. Further calls would essentially do a NULL operation and after the 10ms the update is triggered.

I may need to improve this in future, it is code I wrote earlier this week, but it has shown a huge improvement in UI performance already and a large reduction in CPU usage from when refreshes were triggering too often. For this code I’m going to skip the creation of the timer:

void start_refresh(app_struct_t *my_struct)
{
    uv_timer_stop(my_struct->refresh_handle);
    uv_timer_start(my_struct->refresh_handle, full_flush, 10, 0);
}

The full_flush() function includes calls to do prefresh() whilst calculating the part of the pad to render. Given how timers work in libuv it is likely the uv_timer_stop() is unnecessary and I should look at updating the loop’s concept of ‘now’ with uv_update_time(). But the improvement is huge already so this is something for the future.

Summary

Hopefully I’ve shown in this post that event loops have uses far beyond just handing network or file IO. I also really geeked out when I came up with the idea of the timer refresh trick. It has actually solved problems such as refresh handling for plugins that might interact with the UI, anything that modifies the pad will trigger the refresh timer so it no longer needs to be explicit.

Featured image: Steel wool loops by David Russo, used under a CC BY 2.0 license

Published by

LinuxJedi

Lead Software Engineer / Manager for the MariaDB Corporation and an Open Source Software advocate.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.