dcsimg

ncurses: Old School Interfaces

Even if a system can't provide raster graphics, there's no reason it can't offer the convenience of windowed interfaces. Perhaps you've never heard of it, or perhaps you've just forgotten about it -- in any case, the ncurses library lets you build windowed applications for character-based displays.

While the Linux desktop — via, X, KDE, and GNOME — makes strides toward wider acceptance, not all computer configurations support raster graphics. If you’re using a terminal connected to a serial line, or are using a point-of-sale system (like the one at your local supermarket), you probably don’t have the bandwidth or the hardware, respectively, to display raster graphics.

In some cases, raster graphics are even precluded. For example, embedded systems, which are typically realized with a minimum of hardware and software and regularly have custom displays, commonly provide only character-based consoles for administration and interaction.

And size doesn’t matter. Hardware troves like advanced blade servers have display constraints, too. In some racks, there’s little or no room for a display, let alone a raster display.

But even if a system can’t provide raster graphics, there’s no reason it can’t offer the convenience of windowed interfaces. Perhaps you’ve never heard of it, or perhaps you’ve just forgotten about it — in any case, the ncurses library lets you build windowed applications for character-based displays.

Originally written for BSD Unix’s rogue game, ncurses has evolved over many releases to include support for color terminals, mice (mouse support is specific to ncurses and may not be supported by other Unix versions of curses), character graphics, including window frames, and virtual screen space. You can even combine ncurses with related external libraries to implement overlapping, resizeable, and movable windows, as well as menu management and developer-defined form management.

ncurses is available in source form from the GNU Project (http://www.gnu.org/software/ncurses/ncurses.html), but all Linux distributions include it in their core installations. Important tools such as vim and Midnight Commander (mc) require ncurses, and the kernel source includes an ncurses-based configuration tool. Of course, console logins would be unavailable without the termcap library, on which ncurses is based.

In this article, we’ll learn how to build a simple tabbed folder interface that includes both color and function key support.

“Hello, ncurses

The simplest ncurses program is one that provides a single window, without drawn borders. Listing One shows the ncurses version of the “Hello, World” program.




Listing One: hello.c: the “Hello, World” program, using ncurses


1 #include <ncurses.h>
2 #include <stdlib.h>
3
4 WINDOW *my_wins; /* ncurses window */
5
6 int main()
7 {
8 int ch;
9
10 /* Initialize ncurses */
11 initscr();
12 cbreak();
13 noecho();
14
15 my_wins = newwin(LINES, COLS, 0, 0);
16 wrefresh(my_wins);
17 keypad(my_wins, TRUE);
18
19 /* Print something in the window. */
20 mvwprintw(my_wins, 0, 0, “Hello World!”);
21 mvwprintw(my_wins, 1, 0, “Press F1 to quit.”);
22
23 /* Force window updates. */
24 doupdate();
25
26 /* Scan for the users input. */
27 while ((ch = wgetch(my_wins)) != KEY_F(1)) {
28 /* Do nothing, just wait for the F1 to exit. */
29 }
30
31 /* Free up our ncurses allocated storage */
32 endwin();
33
34 /* Exit gracefully */
35 exit(0);
36 }

The structure of Listing One reflects the basic construction of any ncurses program. Namely:



  • It includes the ncurses header file, ncurses.h.

  • It initializes the ncurses “system” with initscr(), along with other optional initialization routines cbreak() and noecho().

  • The program allocates window structures with newwin().

  • The display is updated with doupdate().

  • Events are processed in the loop while (wgetch()) …

  • ncurses resources are released (freed) with endwin().

This basic structure exists in all ncurses programs. Of course, the complexity of the event processing loop (or loops) and the allocation of window structures varies, depending on the application.

Compiling this program only requires the ncurses library.


% gcc -c -o hello.o hello.c
% gcc -o hello hello.o -lncurses

There are also additional, extended ncurses libraries that add features like menus, panels, and forms. Those user interface controls can be added to an application by specifying the names of the appropriate libraries in the correct order:


% gcc -c -o hello.o hello.c
% gcc -o hello hello.o -lpanel -lmenu -lncurses

Note that both -lpanel and -lmenu should precede -lncurses to keep your application portable to other Unix environments. The order of the individual extension libraries, -lpanel and -lmenu, does not matter.

We’ll discuss use of the panels library in this article, but leave the form and menu libraries as research for the inquisitive reader.

Window References

The basic data structure for ncurses applications is the WINDOW, an opaque structure (meaning that you don’t normally access its contents directly) that describes a logical window. Each window has attributes like width, height, and content, where the latter can include borders if the terminal can draw them. You allocate WINDOW structures using the newwin() function, which returns a pointer to the new structure.

ncurses provides two default windows, referenced as stdscr and curscr, which are generated when you call the initscr() function. The stdscr window is equivalent to stdout and represents the entire visible display area. Curscr represents the last referenced screen in which the cursor was moved (we’ll discuss cursor movement shortly).

While it’s possible to create ncurses applications that do nothing but manipulate the stdscr, this seldom happens in practice. Instead, window instances are typically created and deleted throughout the life of a running application.

Because ncurses offers default windows and the ability to generate your own windows, the ncurses library offers multiple versions of window functions. The primary difference between these versions is whether or not a WINDOW pointer is supplied. Functions that don’t take a WINDOW argument reference the stdscr; those that do take a WINDOW argument reference the specified window.

For example, you can move to a location in the stdscr with move(row,col). You can move to a location in a specific window with wmove(window, row, col). Here, row and col are variables, and window is a pointer to a WINDOW data structure that was allocated previosly with newwin().

Windows are logical structures, which means filling them with data doesn’t actually “draw” that data to the screen. When you write data to a window, that data is saved off screen. Other ncurses functions check windows for updates and push changes to the screen. This two step process allows ncurses to manage the screen more efficiently, “drawing” only modified window contents.

ncurses provides multiple methods of getting data to the screen: refresh() and wrefresh() update the entire display immediately; wnoutrefresh() prepares updates to a single window to be displayed; and doupdate(), which actually makes the updates to the display.

To display changes to windows, the changes have to be copied from the logical window structures to the physical window structure (stdscr) in an order that makes sense (for example, overlapping windows have to be drawn bottom to top). The difference between wrefresh() and the wnoutrefresh()/doupdate() pair is that the former copies to the physical screen and makes the changes visible all in a single step, while the latter pair allow multiple window changes to be copied to the physical screen (wnoutrefresh()) first before having those changes made visible (doupdate()).

Dissecting the First Example

Let’s quickly walk through the code of the first example. Lines 11-13 initialize ncurses. In this example, cbreak() and noecho() function aren’t really needed — cbreak() disables line buffering and erase/kill character processing, and noecho() prevents typed characters from being displayed when retrieved by getchar() — since we aren’t going to deal with any user input other than the F1 key to exit. However, if you intend to manage all user input from the keyboard (and not let ncurses or the terminal itself handle any of it for you), then you’ll want to use cbreak() and noecho(). Documentation for the initialization routines can be found in the man page for initscr().

Line 15 creates a new window, my_wins. wrefresh() updates the window, and the call to keypad() on line 17 enables function key input in my_wins. This is a very useful feature; here we use it to have the F1 key exit the program.

Next, in lines 20 and 21, we add some text at a specific row and column to our new window with mvwprintw().

WHAT’S IN A FUNCTION NAME?

ncurses function names follow a common format throughout the library, but we need to break down an example to make the format apparent.

First, imagine what a printf() function might look like in ncurses: printw() would expect to use the same formatting rules as printf(), but would apply to a window instead of stdout. Indeed, ncurses includes just that very function. printw() prints the text specified (in printf() format) in the stdscr at the current cursor location.

But what if you want to print at the current cursor location in some other window? Then we need to use the window pointer argument form: wprintw(). In general, functions that expect a particular window are prefixed with a “w” (wprintw(), for example).

Neither printw() and wprintw() allow us to specify where we print in a window, so they need to be used in combination with a move() function, such as:


move(row, col);
wprintw(“I’m in row %d, column %d!”, row, col);

Of course, there is a wmove() function that’s called like wmove(my_wins, row, col). And, since moving and printing are such common occurrences in ncurses programs, the library provides functions that combine them: mvprintw() and mvwprintw().

As you can see, the ncurses function names makes sense, as long as you understand what they’re trying to do for you. (Note that some of these functions are actually macros, but in practice you probably won’t care whether it’s a real function or just a macro).

LOOP THE LOOP

After printing content to the logical screen, we update the physical screen with doupdate(). We then enter our event loop. The event loop, in general, is where we catch user input from the keyboard (we’re not using a mouse in this example, though ncurses can handle that), and do processing when there’s no input available. In this first example, the loop isn’t doing anything. We’ll get slightly more creative with the event loop in our other examples.

Finally, endwin() releases the resources allocated for our windows, and returns the terminal to it’s normal state.

While we don’t do so in this example, you should consider remapping the stdout/stderr file descriptors to /dev/null to prevent spurious output on your console. This isn’t necessary if you’re careful with your development process (and how you handle debug output), but remapping can be useful in some cases. Remapping is very simple:


FILE*fd;
fd = freopen(“/dev/null”, “w”, stderr);

The freopen() function remaps an existing file descriptor to another file. See the man page for freopen() for more details.

Example Two: “Hello, World” Wrapped In a Box

The first example left much to be desired: it didn’t write to anything other than the default screen, and didn’t even create a box around the window. Let’s wrap the window in a box in a new example.

The first step is to generate a frame box for the window that will hold each folder’s contents. Listing Two shows the code for this example.




Listing Two: example2.c: “Hello, World” in a box


1 #include <ncurses.h>
2
3 /* === Global Variables === */
4 WINDOW *my_wins; /* bordered window with box outline */
5 WINDOW *my_subwins; /* inside window where content will be
6 * displayed. */
7
8 WINDOW *CreateWin(int height, int width, \ int starty, int startx, int dobox)
9 {
10 WINDOW *local_win;
11
12 /* Allocate a new window */
13 local_win = newwin(height, width, starty, startx);
14
15 /* If requested, draw a box around it. */
16 if (dobox)
17 box(local_win, 0, 0);
18
19 /* Update the window’s virtual structure. */
20 wrefresh(local_win);
21 return local_win;
22 }
23
24 int main()
25 {
26 int ch;
27
28 /* Initialize ncurses */
29 initscr();
30 cbreak();
31 noecho();
32
33 my_wins = CreateWin(LINES, COLS, 0, 0, 1);
34 my_subwins = CreateWin(LINES – 2, COLS – 2, 1, 1, 0);
35 keypad(my_subwins, TRUE);
36
37 /* Print something in the window. */
38 mvwprintw(my_subwins, 0, 0, “Hello World!”);
39 mvwprintw(my_subwins, 1, 0, “Press F1 to quit.”);
40
41 /* Force window updates. */
42 doupdate();
43
44 /* Scan for input. \ Only the folder subwindow accepts input. */
45 while ((ch = wgetch(my_subwins)) != KEY_F(1)) {
46 /* Do nothing, just wait for the F1 to exit. */
47 }
48
49 /* Free up our ncurses allocated storage */
50 endwin();
51
52 /* Exit gracefully */
53 exit(0);
54 }

The first difference that should stand out in this version versus the original “Hello, World” is the subroutine CreateWin() (on lines 8-22). This function creates an ncurses window using the ncurses function newwin(), and optionally, adds a border around that window.

We call CreateWin() twice after initialization, once to create a bordered window (my_wins) and once to create a sub-window, my_subwins, inside of that. Two windows are used so that content displayed in my_subwins is restricted to the boundaries of that window, preventing accidental overwrites of the framed border of my_wins. This trick of using subwindows is very common in ncurses, and you’ll find you use it so often you may create subroutines to generate both windows for you every time you need a new window.

When you compile and run this program, you’ll see that the only visible difference is that your window now has a frame around it.

Both hello.c and example2.c use two ncurses-defined values: LINES and COLS. These two values can be used to determine the width and height of a terminal windows stdscr, respectively.

In example2.c we use LINES and COLS on lines 33 and 34 to create the bordered window the same size as the stdscr, while the subwindow, where we do our text printing, is two lines and two rows smaller. These two lines and rows account for the lines and rows used by the border of the first window (one on top and bottom, and one on each side).

Sub-windows can be made a number of ways. In this example we simply create a smaller window inside a larger one. However, these two windows are completely distinct from each other.

ncurses also provides functions for creating subwindows that are associated with parent windows. The subwin() function creates a child window to a parent that shares memory so that updates to either window affect the other. derwin() is similar, but positions the subwindow relative to the parent window. There are several other related functions for managing subwindows, all of which can be found in the newwin() man page.

Variables my_wins and my_subwins are globals. (The use of global variables is arguably a poor choice, but we’ll leave such details to the reader to decide.) Initialization creates multiple windows — one window and one subwindow — for each folder by calling a new subroutine, init_wins(). We also create one tab (which is a simple one line, eleven column window) for each folder, using the new init_tabs() routine.

The tabs introduce another feature of ncurses: text highlighting. There are eight different types of highlighting available, including reverse video and underlining (we use those two here). Character attributes can be turned on for one or more characters at a time. In init_tabs(), we enable underlining of text, print the text to a window, and then disable underlining.

It’s important to note that text attributes apply to the window as a whole. Once you turn a text attribute on, its applies to any text printed in that window. For this reason, we turn on attributes only when we need them, and then immediately turn them off again. Also, using different highlighting for other text does not affect the highlights used on previously written text in the same window so long as the text affected does not overlap.

Another new function in this listing is make_active _tab(), which changes the text attributes of a window to make it stand out. In this way, the user can tell which folder is active. This function is essentially like init_tabs() except it uses the reverse video attribute. We also keep track of which folder is active using a global variable named active_tab.

PANELS

Listing Three also shows how panels simplify window management. By assigning each window to a panel (inside the init_wins() function), we can then use the panels library to handle displaying the window of interest.

The magic of panels becomes apparent in the make_ active_tab() function. Here, we simply raise the panel of interest to the top of the stack, update it’s contents and refresh the screen. The show_panel() function actually raises the panel specified to the top of the stack, and since our panels are stacked like a deck of cards, only the top panel will be visible. That’s perfect for our tabbed folders.

There is one caveat to using panels: order is important. When you update your panels, any windows that are not associated with a panel are not shown. In our example, we’ve placed our panel handling functions at the start of make_ active_tab(), and then were forced to manually refresh the tabs and window border.

If we put the update_panel() call after the updates to the tabs and window border, we’d lose the window border. The key is to either manage panels first then update other non-panel windows, or to assign all windows to a panel.

THE EVENT LOOP EXPANDS

With panels in place, we now need to provide the user some way of actually rotating through the tabbed folders. This is where event loop handling starts to expand.

In our examples so far, the event loop — the while() statement in our main() function — simply checked to see if the F1 key had been pressed. In this example we want to do a little more. First, we want to check for the TAB key, which wgetc() returns as a numeric 9 value. When this key is pressed, we update the active_tab variable to reflect what the new active folder should be, and then call make_ active_tab() to raise the appropriate panel and highlight the corresponding folder tab.

As an added complexity, we want the event loop to increment a counter on a continuous basis. The counter will always be displayed in the active folder in the same location, no matter which folder is active.

To make this work, we have to change the event loop from waiting on user input to scanning for it. To do this we add a call to nodelay() for each subwindow we create in init_wins(). The difference is subtle: with nodelay() set for these windows, the wgetch() call in our while() statement won’t block waiting for user input. Instead, it simply checks if any input is ready to be processed. If it is, wgetch() returns the data. If not, the switch() statement falls through, letting the call to UpdateCounter() be called immediately.

This new function prints the counter value in the currently active panel window. When the user changes folders, the counter continues to be shown being updated.

Notice that we added the same text to each panel as we created them in init_wins() but at offset locations. When the user presses TAB to advance through each folder, you can see the text “move,” a subtle hint that the folder has changed. The folder tabs also change highlights, with the active folder tab being in reverse video to make it stand out.

Moving and Printing

As stated earlier, ncurses uses a common naming scheme for it’s functions. The functions for moving the cursor and handling character input and output come in three forms: the separate move and I/O versions, the combined move and I/O function for the standard screen, and the combined version with a specified window.

For example, you can first move to location (x,y) with move() and then add a character to the standard screen with addch(). Alternatively, you can combine this operation with mvaddch(), which takes a row, column and character to add as it’s arguments. However, it assumes this all takes place in the standard screen.

The third form adds a window parameter to allow the developer to specify one of their own windows instead of stdscr, such as wmvaddch (window, x, y, ch).

It is important to remember this naming scheme – it will speed your understanding of the ncurses API.

Clearing and coloring

Two things we didn’t cover in our examples are clearing content from windows, and color. Content can be cleared in any window, starting at any location (row and column), and clearing any number of characters. There are functions for clearing to the end of the line, clearing to bottom of the window, and clearing the whole window. These functions are described in detail in the man page for werase().

One subtle issue with clearing content comes with deciding on the use of either clear()/wclear() or erase()/werase(). The erase()/werase() routines copy blanks to every position in a window, effectively clearing it. However they don’t actually force the window to be cleared the next time it is updated. The clear()/wclear() functions also copy blanks to the window, but they also call clearok(), which forces the entire display to be cleared the next time an update to the screen is done.

This means that with clear()/wclear() you need to make sure to refresh all your screen contents after your update. erase()/werase() are faster, but may not clear all the content from the screen in the way you expect. There are situations where you might want to use either the clear() or erase() functions, so choose carefully.

Color is another area not covered in the example. We could easily have made each folder have different color text or different color background. To do so we’d add a call to start_color() during our initialization, and then set a number of init_pairs(), each of which is a foreground and background color.

To Go From Here

If you want to see an application that makes extensive use of ncurses, take a look at the command mc, the Midnight Commander (try “locate mc | grep bin” to find it). mc lets you browse your file system with a windowed interface and perform many useful administrative tasks. mc also makes good use of terminal characteristics including color and highlights, and provides menus and function keys galore.

While your applications may not need to be as complex as mc, you can certainly see the potential of ncurses. Even if it’s the elder statesman in windowing on Linux, ncurses still has lots of power.



Michael J. Hammel is an author, a graphics artist, and a software developer current working for a storage startup in Houston, TX. You can download all of the source code used in this article from http://www.linux-mag.com/downloads/2002-11/ncurses.

Fatal error: Call to undefined function aa_author_bios() in /opt/apache/dms/b2b/linux-mag.com/site/www/htdocs/wp-content/themes/linuxmag/single.php on line 62