Coding

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

LinuxJedi

Share
Published by
LinuxJedi
Tags: COpen Source

Recent Posts

Diagnosing an Amiga 1200 Data Path Fault

I recently acquired an Amiga 1200 motherboard in a spares/repairs condition for about £100 recently.…

2 days ago

Bare Metal “Hello World” on an STM32MP135F-DK

Whilst I do like working with STM32 development boards, some basic information I need can…

3 days ago

Two special Amiga 4000s: Diagnosing Jops

When I last left this blog series, the first of Stoo Cambridge's A4000s had gone…

6 days ago

Joining wolfSSL

At the beginning of November, I started working for wolfSSL. Now that I'm a week…

1 week ago

Two special Amiga 4000s: Rebuilding Jools

In my previous post, I had the Amiga 4000 known as Jools mostly repaired, there…

4 weeks ago

Two special Amiga 4000s: Repairing Jools

In my last post, I created a list of things I needed to repair on…

4 weeks ago