Introduction to ncurses

This reading will introduce our first C library: ncurses. We’ll use this library to build programs that use the terminal to display graphics-like output, even though they’ll still just be using characters. The ncurses library also gives us some additional flexibility in how we handle user input. Together, these features allow us to build more interesting programs including interactive games that would be impossible (or at least clumsy) if all we had was a line-by-line text interface.

What is a library?

Before we get the specifics of ncurses, it’s important that we have some basic understanding of what a “library” is in the context of C development. At its core, a library is just a collection of code that you can use in your program without having to write it yourself. You’ve already been using the C standard library (also known as “libc”) in your C development. The standard library is available by default, but when we build software we can pull in additional libraries when we need to do something that goes beyond the basics provided by libc.

A C library will generally have two parts:

Include Files. These are files that we pull into our code with #include statements. When we want to call a function that’s provided by a library (e.g. strlen) we have to include the header file that declares that function (e.g. #include <string.h>).

Library Files. Include files allow us to call functions we didn’t write ourselves, but they generally only gives us function declarations; they don’t provide the actual function implementations. The compiled function implementations are stored in a library file somewhere on our computer. We need to ask our compiler to find and include implementations of library functions in our final program or we’ll get an error. The standard library is included automatically, but if we use additional libraries we do this by passing the -l flag to clang or gcc.

The example compiler invocation below shows how we can compile a program that uses ncurses:

$ clang -o demo demo.c -lncurses

Notice that -lncurses appears at the end of the line, and does not have a space between -l and the name of the library. Both these details can matter, so be careful when you call the compiler.

If you forget to pass -lncurses to the compiler you’ll see errors, but these happen later in the process than the compiler errors you are used to:

$ clang -o demo demo.c
/usr/bin/ld: /tmp/demo-a644d6.o: in function `main':
demo.c(.text+0x5): undefined reference to `initscr'
...
clang: error: linker command failed with exit code ` (use -v to see invocation)

Any time you see an error like the one above you should double check to be sure you are compiling the program with all the required libraries listed. There is nothing you can edit in your actual code to fix this error; it’s entirely caused by the way the compiler was run.

What is ncurses?

We’ve established that ncurses is a library, and we have some understanding of how to use a library in our programs, but what exactly does ncurses help us do? At the most basic level, ncurses allows us to use the terminal in new ways. In all our programs so far, we’ve printed text to the terminal or prompted for input from the user by printing to the next available line. If the screen is full, the terminal handles scrolling for us.

When we use ncurses, our programs can switch to a different mode where we have complete control over the terminal display. Using ncurses, we can place text anywhere in the terminal, clear the screen, switch colors, and more. This is not a graphical mode, since we’re still just drawing characters, but we can use characters (including some special characters provided by ncurses) to create graphics-like output.

Along with that extra output flexibility, ncurses also gives us new tools for dealing with user input. We can capture events like keys being pressed and released, including arrow keys or other special keys that don’t correspond to text input. The ncurses library also allows us to capture mouse input from the terminal.

A simple example

Before we go any further, let’s look at a simple example that uses ncurses:

#include <ncurses.h>

int main() {
  // Set up ncurses by initializing the default screen
  initscr();

  // Clear the screen
  clear();

  // Print a message and update the screen
  mvprintw(3, 5, "Hello, world!");
  refresh();

  // Wait for the user to press a key
  getch();

  // Shut down ncurses
  endwin();
}

If you put this code into a file called demo.c, you can compile and run it with the line:

$ clang -o demo demo.c -lncurses
$ ./demo

Test the program now to make sure it works. Once you have it running, try adusting the row and column numbers to see how the ncurses coordinate system works.

Dealing with crashes

The example program above should run without issues, but eventually you will probably write a program that crashes while it is using ncurses. This can be more disruptive than a regular program crash because it can leave the terminal in a somewhat broken state. The fix for this is to use the reset command:

$ reset

If you type this, it will re-initialize your terminal so it works as usual. You might need to type this into a terminal that isn’t working correctly, which means you may not see the usual output; you can generally trust that pressing ctrl+c to stop whatever is running, then typing “reset” and pressing enter should work. In the worst case you can always close the terminal and start a new one, but that’s not your only option.

Why use ncurses at all?

Based on the example above, it seems like ncurses adds some complexity to our programs. The added complexity poses a bit of a challenge for new C programmers, but there are a few reasons we’re going to start using ncurses in this class.

First, ncurses gives us some additional creative flexibility with our programs. Programs that use ncurses aren’t really producing graphics, but they do give us more options to display interesting things than a purely text-based program would. Sometimes the added challenge of figuring out how to display something interesting using characters can be fun or interesting on its own. True graphics programming brings a lot of extra challenges, so ncurses programming is a good bridge to that complexity.

Second, ncurses is a library that gives us powerful and interesting functionality that we don’t have to write ourselves. Most software you use today is built using libraries, so using ncurses is a chance to practice that. We’ll still write plenty of code on our own, but libraries allow us to work quickly and build on trusted code. Seeing how libraries are designed also helps us think about our own code from the standpoint of reuse and maintenance.

Third, ncurses allows us to write programs that deal with concurrency. If you think about any computer game you’ve played before, there are pretty good odds it didn’t follow a strict pattern of prompts and responses. Instead, interesting games (and most other applications) deal with many things happening at once. Many games change state without user input, but the player can take action at any time to influence the game’s state. Programs that behave this way appear to do many things at once: they update the state of the game at a regular rate and respond to user input, all while displaying the game to the user. Writing programs that behave this way is fairly natural with ncurses, and would be quite difficult without it.

The next section describes on how we can organize programs to behave in this way, and how ncurses works within that approach.

Handling Concurrent Events

Modern computers give is many different tools to deal with concurrency (i.e. multiple things happening at the same time), but in this class we will focus on a classic approach: the game loop. Game loops work well for games—you could probably guess that from the name—but they appear in sorts of different programs. A game loop doesn’t use any special computational power or new language features. Instead, the game loop is a basic pattern we’ll build using pieces we already understand:

The Game Loop Pattern
While the program is running:
    Check for user input
    Update program state
    Produce output
Repeat

The key idea behind a game loop is that the loop is always running. In a game, each iteration of the loop corresponds to a frame (i.e. one image presented to the user as part of a longer sequence of images that make up an animation). The program will update state and display output to the user whether there is input or not. But once there is input available, the program will handle it when it produces the next frame.

It may not be obvious how this pattern allows programs to deal with concurrent events, but we’ll walk through several versions of an example program to build something game-like using a game loop.

Version 0: the basic ncurses game loop

Let’s start with a simple ncurses program that sets up and runs a game loop:

#include <ncurses.h>
#include <stdbool.h>

int main() {
  // Set up ncurses
  initscr();  // Initialize the standard window

  // Start the game loop
  bool running = true;
  while (running) {
    // TODO: check for user input

    // TODO: update state

    // Display output
    clear();
    mvprintw(3, 5, "Hello, world!");
    refresh();
  }

  // Shut down
  endwin();
}

Much of this program should look familiar from the earlier ncurses example. There is no call to getch() to wait for user input, but the program just loops forever. You’ll need to type ctrl+c to exit instead of pressing an arbitrary key.

As a reminder, you can drop this code into a file named demo.c and compile and run it with the following terminal commands:

$ clang -o demo demo.c -lncurses
$ ./demo

We’ll get to the user input and state update phases of the game loop in the next few versions of the program, but we do have a complete output phase in this example. Notice that we start by clearing the screen with the clear() function, and finish by displaying the output to the user with refresh(). We could get away without these steps in this example because its output never changes, but both these pieces will be important in future versions of this program.

Version 1: handling basic user input

Now that we have a simple game loop up and running, let’s deal with user input. We’ll start by checking to see if the user has typed the q key to quit the program. We call the getch() function to read user input when we’re using ncurses, but there’s a problem: this function doesn’t check for user input, it waits for it. That’s going to be a problem if we want the game loop to run continuously.

Luckily, ncurses gives us an easy way to fix this. We can tell ncurses to change the behavior of getch() so it returns a keypress if one is available, but returns the constant ERR if there is no user input. We’ll do that with the nodelay function in the example below.

Once we’ve set getch() to be non-blocking we can call it to check for input. If the function returns ERR there is no input to handle, and if it returns something else we have at least one key press to deal with. But what if the user types multiple keys? We’ll handle multiple keys by calling getch() in a loop repeatedly until it returns ERR. That guarantees we’ll see all the keypresses available as soon as possible instead of limiting ourselves to one input per frame.

The example below puts all these pieces together along with a few other small improvements:

#include <ncurses.h>
#include <stdbool.h>

int main() {
  // Set up ncurses
  initscr();              // Initialize the standard window
  noecho();               // Don't display keys when they are typed
  nodelay(stdscr, true);  // Set up non-blocking input with getch()
  curs_set(false);        // Hide the cursor

  // Start the game loop
  bool running = true;
  while (running) {
    // Check for user input
    int input = getch();
    while (input != ERR) {
      // Exit when the user types q
      if (input == 'q') {
        running = false;
      }

      // Get the next user input
      input = getch();
    }

    // TODO: update state

    // Display output
    clear();
    mvprintw(3, 5, "Hello, world!");
    refresh();
  }

  // Shut down
  endwin();
}

Our initialization code is a bit longer than the previous example. The nodelay function tells getch() to check for input without blocking, but you’ll also notice we’ve hidden the cursor and asked the program not to display keys as the user types them. These are optional settings, but they’ll make sense for many games; displaying a cursor and printing out each key the user types is more reasonable for a program prompting for text input.

The loop that calls getch() above is fine, but consider what happens if we ever write continue inside the loop. In that case, we’d never read a new character and the loop would run forever. There’s a common way of writing this loop that avoids this error and saves a couple lines:

// Check for user input
int input;
while ((input = getch()) != ERR) {
  // Exit when the user types q
  if (input == 'q') {
    running = false;
  }
}

This version assigns to input inside the loop condition so there’s no way to avoid reading one new input on each iteration. The result of the assignment is the updated value of input, so we can compare the result to ERR just as before. We mark this assignment by wrapping input = getch() in an extra set of paranetheses. These parentheses tell the compiler and any humans who read this code later that we really did mean to use assignment (=) instead of comparison (==) in the loop condition. If you leave these parentheses off you’ll get a warning from many compilers because this is such a common convention.

Version 2: changing state

Now that we can handle basic user input, let’s add some state to the program that changes in response to user input. Instead of displaying the message at a fixed location, we’ll allow the user to move it around with arrow keys. The getch() function doesn’t report arrow keys by default, but we can ask it to report them with the keypad function.

We’d also like to keep the message on the screen, so any time the user moves the message past the edge of the screen we will move it back in bounds. We’re going to use the getmaxx and getmaxy functions to ask how large the terminal is on each frame. One nice byproduct of this approach is that it will also keep the message in bounds when the user resizes the terminal window.

The logic to move the message is quite simple, so we’ll handle that directly in the user input portion of the game loop. The code to keep the message inside the screen boundaries will be the only state update for now.

#include <ncurses.h>
#include <stdbool.h>
#include <string.h>

#define MESSAGE "Hello, world!"

int main() {
  // Set up ncurses
  initscr();              // Initialize the standard window
  noecho();               // Don't display keys when they are typed
  nodelay(stdscr, true);  // Set up non-blocking input with getch()
  keypad(stdscr, true);   // Allow arrow keys
  curs_set(false);        // Hide the cursor

  // Initialize the message position
  int message_x = 5;
  int message_y = 3;
  int message_length = strlen(MESSAGE);

  // Start the game loop
  bool running = true;
  while (running) {
    // Check for user input
    int input;
    while ((input = getch()) != ERR) {
      if (input == 'q') {
        // Exit when the user types q
        running = false;
      
      } else if (input == KEY_UP) {
        // Move the message up
        message_y--;
      
      } else if (input == KEY_DOWN) {
        // Move the message down
        message_y++;
      
      } else if (input == KEY_LEFT) {
        // Move the message left
        message_x--;

      } else if (input == KEY_RIGHT) {
        // Move the message right
        message_x++;
      }
    }

    // Get the bounds of the screen
    int max_x = getmaxx(stdscr);
    int max_y = getmaxy(stdscr);

    // Move the message in bounds on the y axis
    if (message_y < 0) {
      message_y = 0;
    } else if (message_y >= max_y) {
      // The getmaxy function returns one more line than we can actually see,
      // so limit message_y to one less than max_y
      message_y = max_y - 1;
    }

    // Move the message in bounds on the x axis
    if (message_x < 0) {
      message_x = 0;
    } else if (message_x + message_length > max_x) {
      // Keep the entire message on screen by factoring in the message length
      message_x = max_x - message_length;
    }

    // Display output
    clear();
    mvprintw(message_y, message_x, MESSAGE);
    refresh();
  }

  // Shut down
  endwin();
}

We now have several places where we are using the stdscr variable, which is provided by the ncurses library. This is a name for the top-level window that ncurses provides; for now we’ll only use this window, but ncurses does allow us to divide the screen up further into separate windows if we want to display multiple things side by side (e.g. text next to a game board).

Make sure you can match the parts of this example to the text description of this version above before moving on. You may find it helpful to turn off or modify parts of the code above to check whether a particular piece of code does what you expect.

Version 3: preparing for animations

You may have noticed that the terminal flickers when you run the demo programs we’ve seen so far. That’s because we have what is sometimes called a “free running” game loop where the loop runs as fast as possible with no limit on the frame rate. We aren’t doing anything computationally intensive inside the loop, so a large share of the program’s runtime is spent with the screen cleared. We can solve that by pausing for a short time after displaying each frame.

Adding this pause is also important for any time-based animations we add later. If the game runs at a predictable frame rate (say 60 frames a second) we can hard-code animations to move at a predictable speed. For our simple program we’ll do this by adding a fixed pause after each frame. This approach ignores the time it takes to handle input, update state, and draw the frame, but we can get away with that because these steps take so little time.

This updated demo is almost the same as the previous version, but with an added call to pause after each frame.

#include <ncurses.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>

#define FRAME_RATE 60
#define MESSAGE "Hello, world!"

/**
 * Sleep for a given number of milliseconds
 * \param   ms  The number of milliseconds to sleep for
 */
void sleep_ms(size_t ms) {
  struct timespec ts;
  size_t rem = ms % 1000;
  ts.tv_sec = (ms - rem) / 1000;
  ts.tv_nsec = rem * 1000000;

  // Sleep repeatedly as long as nanosleep is interrupted
  while (nanosleep(&ts, &ts) != 0) {}
}

int main() {
  // Set up ncurses
  initscr();              // Initialize the standard window
  noecho();               // Don't display keys when they are typed
  nodelay(stdscr, true);  // Set up non-blocking input with getch()
  keypad(stdscr, true);   // Allow arrow keys
  curs_set(false);        // Hide the cursor

  // Initialize the message position
  int message_x = 5;
  int message_y = 3;
  int message_length = strlen(MESSAGE);

  // Start the game loop
  bool running = true;
  while (running) {
    // Check for user input
    int input;
    while ((input = getch()) != ERR) {
      if (input == 'q') {
        // Exit when the user types q
        running = false;
      
      } else if (input == KEY_UP) {
        // Move the message up
        message_y--;
      
      } else if (input == KEY_DOWN) {
        // Move the message down
        message_y++;
      
      } else if (input == KEY_LEFT) {
        // Move the message left
        message_x--;

      } else if (input == KEY_RIGHT) {
        // Move the message right
        message_x++;
      }
    }

    // Keep the message in bounds
    int max_x = getmaxx(stdscr);
    int max_y = getmaxy(stdscr);

    // Move the message in bounds on the y axis
    if (message_y < 0) {
      message_y = 0;
    } else if (message_y >= max_y) {
      message_y = max_y - 1;
    }

    // Move the message in bounds on the x axis
    if (message_x < 0) {
      message_x = 0;
    } else if (message_x + message_length > max_x) {
      message_x = max_x - message_length;
    }

    // Display output
    clear();
    mvprintw(message_y, message_x, MESSAGE);
    refresh();

    // Pause to limit frame rate
    sleep_ms(1000 / FRAME_RATE);
  }

  // Shut down
  endwin();
}

The demo program should be largely unchanged with this modification, although if you noticed any flickering before it should have stopped with this change.

Version 4: adding an animated “ball”

All the versions up to this point give the user the ability to move a message around on the screen, but the resulting program doesn’t exactly feel “concurrent.” It’s not obvious that anything is happening during the periods between user keypresses so it isn’t clear that we need a game loop to do this. Let’s complete the demo with one more addition: a “ball” that bounces around the screen while the program is running, whether there is user input or not. The description below includes code fragments, but you can find the complete demo at the bottom of this section.

We’ll draw the ball with a lowercase 'o' character, but what state do we need to keep track of to animate it? We’ll certainly need its X and Y position, but we also need to know what direction the ball is moving. We’ll do this with some extra state:

// Initialize ball state
int ball_x = 0;
int ball_y = 0;
int ball_vx = 1;
int ball_vy = 1;

On each frame, we’ll update the ball’s position using its movement direction:

// Update ball position
ball_x += ball_vx;
ball_y += ball_vy;

What happens when the ball hits the edge of the window? We’ll need to move the ball back into the screen bounds in case it left the screen (which probably only happens if the user resizes the window). We also need to update the ball’s direction, but the exact update depends on which edge the ball hit:

// Check for collisions with screen edges
// Left edge
if (ball_x <= 0) {
  ball_x = 0;
  ball_vx = 1;
}

// Right edge
if (ball_x >= max_x - 1) {
  ball_x = max_x - 1;
  ball_vx = -1;
}

// Top edge
if (ball_y <= 0) {
  ball_y = 0;
  ball_vy = 1;
}

// Bottom edge
if (ball_y >= max_y - 1) {
  ball_y = max_y - 1;
  ball_vy = -1;
}

And finally, we need to actually display the ball:

// Display the ball
mvprintw(ball_y, ball_x, "o");

Here is the complete example with all the additions for the animated ball:

#include <ncurses.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>

#define FRAME_RATE 60
#define MESSAGE "Hello, world!"

/**
 * Sleep for a given number of milliseconds
 * \param   ms  The number of milliseconds to sleep for
 */
void sleep_ms(size_t ms) {
  struct timespec ts;
  size_t rem = ms % 1000;
  ts.tv_sec = (ms - rem) / 1000;
  ts.tv_nsec = rem * 1000000;

  // Sleep repeatedly as long as nanosleep is interrupted
  while (nanosleep(&ts, &ts) != 0) {}
}

int main() {
  // Set up ncurses
  initscr();              // Initialize the standard window
  noecho();               // Don't display keys when they are typed
  nodelay(stdscr, true);  // Set up non-blocking input with getch()
  keypad(stdscr, true);   // Allow arrow keys
  curs_set(false);        // Hide the cursor

  // Initialize the message position
  int message_x = 5;
  int message_y = 3;
  int message_length = strlen(MESSAGE);

  // Initialize ball state
  int ball_x = 0;
  int ball_y = 0;
  int ball_vx = 1;
  int ball_vy = 1;

  // Start the game loop
  bool running = true;
  while (running) {
    // Check for user input
    int input;
    while ((input = getch()) != ERR) {
      if (input == 'q') {
        // Exit when the user types q
        running = false;
      
      } else if (input == KEY_UP) {
        // Move the message up
        message_y--;
      
      } else if (input == KEY_DOWN) {
        // Move the message down
        message_y++;
      
      } else if (input == KEY_LEFT) {
        // Move the message left
        message_x--;

      } else if (input == KEY_RIGHT) {
        // Move the message right
        message_x++;
      }
    }

    // Keep the message in bounds
    int max_x = getmaxx(stdscr);
    int max_y = getmaxy(stdscr);

    // Move the message in bounds on the y axis
    if (message_y < 0) {
      message_y = 0;
    } else if (message_y >= max_y) {
      message_y = max_y - 1;
    }

    // Move the message in bounds on the x axis
    if (message_x < 0) {
      message_x = 0;
    } else if (message_x + message_length > max_x) {
      message_x = max_x - message_length;
    }

    // Update ball position
    ball_x += ball_vx;
    ball_y += ball_vy;

    // Check for collisions with screen edges
    // Left edge
    if (ball_x <= 0) {
      ball_x = 0;
      ball_vx = 1;
    }

    // Right edge
    if (ball_x >= max_x - 1) {
      ball_x = max_x - 1;
      ball_vx = -1;
    }

    // Top edge
    if (ball_y <= 0) {
      ball_y = 0;
      ball_vy = 1;
    }

    // Bottom edge
    if (ball_y >= max_y - 1) {
      ball_y = max_y - 1;
      ball_vy = -1;
    }

    // Display output
    clear();

    // Display the message
    mvprintw(message_y, message_x, MESSAGE);
    
    // Display the ball
    mvprintw(ball_y, ball_x, "o");

    refresh();

    // Pause to limit frame rate
    sleep_ms(1000 / FRAME_RATE);
  }

  // Shut down
  endwin();
}

Further Improvements

This is a relatively simple example, but hopefully it gives you a sense of how we can use a game loop to read and respond to user input while game state updates on its own. There are a few improvements we can make to this example, which we’ll leave as an exercise for the reader:

  1. Organize the code into seprate functions. The initialization code could be pulled out of main fairly easily. You could also create a struct that holds the ball state and pass it to an update_ball function to handle moving the ball and bouncing it off the screen edges. You may even want to do something similar for the message update, since the screen bounds checking makes it harder to follow the game loop structure.

  2. Limit the ball’s speed. The ball moves very fast in this example, which might not be what you want. You could slow it down by reducing the frame rate, but that also makes the program wait longer between checking for input. That could be an issue if you want a slow moving ball because there would be a long delay between the user pressing an arrow key and the message moving on screen. Instead of modifying the frame rate, we can update the ball every other frame, every three frames, or every N frames. You can do this by adding a counter that increments each frame. When the counter reaches N, reset it to zero and update the ball.

    You might instead choose to represent the ball position and speed with float variables, which you can then round down to ints when you display the ball. This works okay, but you’ll likely find that the ball “wiggles” when it moves because of slight inaccuracies in floating point representation.

  3. Bounce the ball off other objects. It would be nice if the ball bounced when it hit the message, not just the screen edges. You should be able to detect when the ball is touching the edge of the message and reverse its movement in the x or y direction depending on which edge it hit.