Table of Contents

  1. Introduction
  2. Getting Started: Installation & Build Setup
  3. Core Concepts of ncurses
  4. First Program – “Hello, ncurses!”
  5. Managing Multiple Windows
  6. Working with Pads for Large Scrollable Views
  7. The Panels Extension – Layered Interfaces
  8. Forms and Menus – Ready‑Made Widgets
  9. Designing an Event Loop
  10. Real‑World Use Cases
  11. Performance & Portability Tips
  12. Building & Linking – Makefile Essentials
  13. Beyond ncurses: Alternatives & The Future
  14. Conclusion
  15. Resources

Introduction

When you think of modern software, graphical user interfaces (GUIs) dominate the conversation. Yet, for many system‑level tools, servers, embedded devices, or developers who simply love the elegance of a well‑crafted terminal UI, ncurses (new curses) remains the gold standard.

Originally derived from the BSD curses library of the early 1980s, ncurses matured into a portable, feature‑rich API that abstracts the quirks of various terminal emulators, providing developers with:

  • Window management – split the screen into independent drawing areas.
  • Color and attribute support – bold, underline, reverse video, and 256‑color palettes.
  • Keyboard & mouse handling – non‑blocking input, function keys, and mouse events.
  • Higher‑level widgets – forms, menus, panels, and even a “soft” GUI toolkit.

This article is a deep dive into ncurses for C programmers who want to build robust, maintainable text‑based applications. We’ll start from installation, walk through core concepts, explore practical examples, and finish with real‑world patterns and performance considerations. By the end, you’ll have a solid foundation to create anything from a simple status monitor to a full‑blown terminal IDE.

Note: While the examples use C, the underlying API is also accessible from C++ and many scripting languages (Python, Perl, Ruby). The concepts remain identical across bindings.


Getting Started: Installation & Build Setup

PlatformPackage ManagerCommand
Ubuntu / Debianaptsudo apt-get install libncurses5-dev libncursesw5-dev
Fedora / CentOSdnf / yumsudo dnf install ncurses-devel
macOSHomebrewbrew install ncurses
FreeBSDPortscd /usr/ports/devel/ncurses && make install clean
Windows (WSL)Ubuntu packageSame as Ubuntu command above

Tip: The -dev (or devel) package provides the header files (ncurses.h) and static/shared libraries needed for compilation.

2. Simple Makefile

Below is a minimal Makefile that compiles a single source file main.c with ncurses:

CC      = gcc
CFLAGS  = -Wall -Wextra -O2
LDFLAGS = -lncurses

all: ncurses_demo

ncurses_demo: main.o
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -f *.o ncurses_demo

Running make will produce an executable named ncurses_demo. For larger projects you can split source files into modules and add automatic dependency generation (gcc -MMD).


Core Concepts of ncurses

Before diving into code, let’s clarify the primary abstractions that ncurses offers.

3.1 Windows, Sub‑windows, and Pads

  • WINDOW * – The fundamental drawing surface. The library creates a default screen (stdscr) that represents the entire terminal. Additional windows can be created with newwin(rows, cols, y, x).
  • Sub‑windows – Created with subwin or derwin, they share the underlying buffer with a parent window, enabling efficient updates.
  • Pads – Off‑screen virtual windows that can be larger than the physical screen. They are useful for scrollable content (e.g., a log viewer). Pads are refreshed with prefresh specifying the region to display.

3.2 Attributes & Color Pairs

Attributes modify the appearance of characters:

AttributeMacro
BoldA_BOLD
UnderlineA_UNDERLINE
Reverse videoA_REVERSE
Blink (if supported)A_BLINK
DimA_DIM
Standout (often reverse + bold)A_STANDOUT

Colors are managed through color pairs. A pair combines a foreground and background color and receives an integer ID (1‑255). Example:

init_pair(1, COLOR_RED,   COLOR_BLACK);   // Pair 1: red on black
init_pair(2, COLOR_GREEN, COLOR_BLACK);   // Pair 2: green on black
attron(COLOR_PAIR(1) | A_BOLD);          // Apply pair 1 with bold

Modern terminals support up to 256 colors (use_default_colors() + init_color). ncurses can also detect true‑color support via the ncursesw wide‑character variant.

3.3 Input Handling

  • getch() – Blocking read of a single keystroke.
  • nodelay(win, TRUE) – Makes getch non‑blocking, returning ERR if no input is available.
  • keypad(win, TRUE) – Enables translation of function keys (F1‑F12), arrow keys, and keypad numbers into symbolic constants (KEY_F(1), KEY_LEFT, …).
  • Mouse – Activate with mousemask(ALL_MOUSE_EVENTS, NULL). Mouse events are reported as MEVENT structures via getch() when KEY_MOUSE is returned.

First Program – “Hello, ncurses!”

Let’s start with the classic “Hello, world!” but using ncurses to demonstrate initialization, basic drawing, and cleanup.

/* hello_ncurses.c */
#include <ncurses.h>

int main(void) {
    // 1. Initialise the library and set up the screen
    initscr();              // Start curses mode
    cbreak();               // Disable line buffering
    noecho();               // Do not echo typed characters
    keypad(stdscr, TRUE);   // Enable function keys and arrows

    // 2. Optional: use colours if the terminal supports them
    if (has_colors()) {
        start_color();
        init_pair(1, COLOR_YELLOW, COLOR_BLUE);
        attron(COLOR_PAIR(1));
    }

    // 3. Print a centered message
    const char *msg = "Hello, ncurses!";
    int row = LINES / 2;               // LINES & COLS are global size vars
    int col = (COLS - (int)strlen(msg)) / 2;
    mvprintw(row, col, "%s", msg);

    // 4. Refresh to make the message appear
    refresh();

    // 5. Wait for user input before exiting
    getch();

    // 6. Clean up and restore terminal state
    if (has_colors())
        attroff(COLOR_PAIR(1));
    endwin();               // End curses mode

    return 0;
}

Explanation of key steps:

  1. initscr() creates stdscr and determines the terminal size.
  2. cbreak() passes characters to the program immediately, while noecho() prevents them from being echoed automatically.
  3. has_colors() checks for colour capability; start_color() activates colour handling.
  4. mvprintw() moves the cursor and prints a string.
  5. refresh() pushes the buffer to the screen.
  6. endwin() restores the original terminal mode (important for not leaving the terminal in a weird state).

Compile with:

gcc -o hello_ncurses hello_ncurses.c -lncurses

Running the program should display a yellow‑on‑blue “Hello, ncurses!” centered on the screen, waiting for any key before exiting.


Managing Multiple Windows

A single screen quickly becomes cluttered for complex applications. ncurses lets you partition the terminal into logical windows, each with its own coordinate system and refresh semantics.

5.1 Creating and Positioning Windows

WINDOW *create_boxed_window(int height, int width, int starty, int startx) {
    WINDOW *win = newwin(height, width, starty, startx);
    box(win, 0, 0);            // Draw a border using default characters
    wrefresh(win);             // Show the window immediately
    return win;
}

Example usage:

int main(void) {
    initscr(); cbreak(); noecho(); keypad(stdscr, TRUE);
    start_color(); init_pair(1, COLOR_WHITE, COLOR_BLUE);
    bkgd(COLOR_PAIR(1));      // Set default background for stdscr
    refresh();

    // Create a header window (full width, 3 rows)
    WINDOW *header = create_boxed_window(3, COLS, 0, 0);
    mvwprintw(header, 1, 2, "System Monitor");
    wrefresh(header);

    // Create a main content window below the header
    int main_h = LINES - 6;    // Leave space for footer
    WINDOW *main_win = create_boxed_window(main_h, COLS, 3, 0);
    mvwprintw(main_win, 1, 2, "CPU usage: 12%%");
    wrefresh(main_win);

    // Footer window
    WINDOW *footer = create_boxed_window(3, COLS, LINES - 3, 0);
    mvwprintw(footer, 1, 2, "Press any key to exit...");
    wrefresh(footer);

    getch();
    delwin(header); delwin(main_win); delwin(footer);
    endwin();
    return 0;
}

Key points:

  • box draws a simple border; you can customize with wborder.
  • bkgd sets background attributes for the whole screen; windows inherit unless overridden.
  • delwin frees resources; always clean up before endwin.

5.2 Overlapping Windows & Z‑order

When windows overlap, the one refreshed last appears on top. However, this can cause flickering if you refresh many windows each frame. The panel library (discussed later) abstracts Z‑ordering and provides efficient stacking.

5.3 Sub‑windows for Shared Buffers

Suppose you have a scrolling text area within a larger window. Instead of creating a separate buffer, you can create a sub‑window that shares memory:

WINDOW *sub = derwin(parent, sub_h, sub_w, sub_y, sub_x);

Any changes to sub automatically affect the overlapping region of parent. This is ideal for implementing a viewport into a larger data structure (e.g., a log view).


Working with Pads for Large Scrollable Views

Pads are virtual windows that can be arbitrarily large (limited only by memory). They’re perfect for displaying long files, tables, or a scrolling game map.

6.1 Creating a Pad

#define PAD_ROWS 200
#define PAD_COLS 100

WINDOW *pad = newpad(PAD_ROWS, PAD_COLS);

6.2 Writing to a Pad

for (int i = 0; i < PAD_ROWS; ++i) {
    mvwprintw(pad, i, 0, "Line %03d: Lorem ipsum dolor sit amet...", i);
}

6.3 Displaying a Portion of the Pad

The prefresh function takes both the pad coordinates and the screen coordinates:

int pad_y = 0;            // Upper-left corner in the pad
int pad_x = 0;
int scr_y = 1;            // Where on the screen to start drawing
int scr_x = 2;
int scr_h = LINES - 2;   // Height of the displayed region
int scr_w = COLS - 4;    // Width of the displayed region

prefresh(pad, pad_y, pad_x, scr_y, scr_x, scr_y + scr_h, scr_x + scr_w);

6.4 Simple Scrolling Loop

int ch;
while ((ch = getch()) != 'q') {
    switch (ch) {
        case KEY_DOWN:
            if (pad_y < PAD_ROWS - scr_h - 1) ++pad_y;
            break;
        case KEY_UP:
            if (pad_y > 0) --pad_y;
            break;
        case KEY_NPAGE:   // Page down
            pad_y = MIN(pad_y + scr_h, PAD_ROWS - scr_h - 1);
            break;
        case KEY_PPAGE:   // Page up
            pad_y = MAX(pad_y - scr_h, 0);
            break;
    }
    prefresh(pad, pad_y, pad_x, scr_y, scr_x,
              scr_y + scr_h, scr_x + scr_w);
}

The pad remains in memory even when off‑screen, making it a powerful tool for log viewers (less-style navigation) or text editors.


The Panels Extension – Layered Interfaces

While you can manually manage Z‑order by careful refresh ordering, the panel library (panel.h) provides a clean API for stacking windows, hiding/showing them, and moving them without redrawing unrelated areas.

7.1 Basic Panel Operations

#include <panel.h>

WINDOW *win1 = newwin(10, 30, 5, 5);
WINDOW *win2 = newwin(8, 25, 8, 10);
PANEL  *pan1 = new_panel(win1);
PANEL  *pan2 = new_panel(win2);

/* The topmost panel is automatically displayed last */
update_panels();   // Synchronize internal panel stack with screen
doupdate();        // Refresh the screen (calls wrefresh on visible windows)

7.2 Changing Z‑order Dynamically

/* Bring win2 to the front */
top_panel(pan2);
update_panels();
doupdate();

7.3 Hiding and Showing Panels

hide_panel(pan1);   // Removes win1 from the visible stack
show_panel(pan1);   // Restores it at its previous position
update_panels(); doupdate();

Panels are especially handy for modal dialogs, pop‑up menus, or tooltips that need to appear above the main UI without permanently altering window layout.


Forms and Menus – Ready‑Made Widgets

Ncurses ships with two optional libraries that accelerate UI development: forms (form.h) for data entry fields and menus (menu.h) for selectable lists.

8.1 Forms – Collecting Structured Input

#include <form.h>

/* Define three fields: name, age, and a hidden terminator */
FIELD *fields[4];
fields[0] = new_field(1, 20, 2, 10, 0, 0);   // label "Name"
fields[1] = new_field(1, 3, 4, 10, 0, 0);    // label "Age"
fields[2] = NULL;                           // Terminator

/* Set field options */
set_field_back(fields[0], A_UNDERLINE);
set_field_back(fields[1], A_UNDERLINE);
field_opts_off(fields[0], O_AUTOSKIP);
field_opts_off(fields[1], O_AUTOSKIP);

/* Create the form */
FORM *my_form = new_form(fields);
post_form(my_form);
refresh();

/* Simple navigation loop */
int c;
while ((c = getch()) != KEY_F(1)) { // F1 to quit
    switch (c) {
        case KEY_DOWN:
            form_driver(my_form, REQ_NEXT_FIELD);
            form_driver(my_form, REQ_END_LINE);
            break;
        case KEY_UP:
            form_driver(my_form, REQ_PREV_FIELD);
            form_driver(my_form, REQ_END_LINE);
            break;
        case KEY_BACKSPACE:
        case 127:
            form_driver(my_form, REQ_DEL_PREV);
            break;
        default:
            form_driver(my_form, c);
            break;
    }
}

/* Retrieve entered data */
char *name = field_buffer(fields[0], 0);
char *age  = field_buffer(fields[1], 0);

Forms handle cursor movement, field validation, and automatic scrolling when the form exceeds the visible area.

8.2 Menus – Selecting from a List

#include <menu.h>

char *choices[] = {
    "Start", "Settings", "Help", "Quit", (char *)NULL
};

ITEM **items;
MENU *my_menu;
int n_choices, i;

/* Build items array */
for (n_choices = 0; choices[n_choices]; ++n_choices);
items = (ITEM **)calloc(n_choices + 1, sizeof(ITEM *));
for (i = 0; i < n_choices; ++i)
    items[i] = new_item(choices[i], "");

items[n_choices] = (ITEM *)NULL;

/* Create menu */
my_menu = new_menu((ITEM **)items);
set_menu_mark(my_menu, " * ");          // Mark selected item
post_menu(my_menu);
refresh();

/* Simple navigation loop */
int c;
while ((c = getch()) != 'q') {
    switch (c) {
        case KEY_DOWN:
            menu_driver(my_menu, REQ_DOWN_ITEM);
            break;
        case KEY_UP:
            menu_driver(my_menu, REQ_UP_ITEM);
            break;
        case 10: // Enter key
            {
                ITEM *cur = current_item(my_menu);
                const char *selection = item_name(cur);
                if (strcmp(selection, "Quit") == 0) {
                    c = 'q';
                } else {
                    mvprintw(LINES-2, 0, "You chose %s", selection);
                    refresh();
                }
            }
            break;
    }
}

/* Cleanup */
unpost_menu(my_menu);
free_menu(my_menu);
for (i = 0; i < n_choices; ++i)
    free_item(items[i]);
free(items);

Menus automatically handle scrolling when the list exceeds the display region and integrate seamlessly with panels for modal dialogs.


Designing an Event Loop

A responsive ncurses application usually runs an event loop that:

  1. Polls input (keyboard, mouse, or timers).
  2. Updates application state based on the input.
  3. Redraws only the parts that changed to reduce flicker.

Below is a skeleton that demonstrates non‑blocking input with a simple timer using select().

#include <sys/select.h>
#include <unistd.h>
#include <time.h>

#define REFRESH_MS 100   // UI refresh interval

int main(void) {
    initscr(); cbreak(); noecho(); keypad(stdscr, TRUE);
    nodelay(stdscr, TRUE);   // Non‑blocking getch()
    curs_set(0);             // Hide cursor

    int running = 1;
    struct timeval tv;
    long last_tick = 0;

    while (running) {
        /* 1️⃣ Input handling */
        int ch = getch();
        if (ch != ERR) {
            switch (ch) {
                case 'q':
                case KEY_F(10):
                    running = 0;
                    break;
                case KEY_RESIZE:
                    /* Terminal resized – update dimensions */
                    clear();
                    break;
                default:
                    /* Application‑specific handling */
                    break;
            }
        }

        /* 2️⃣ Timer – refresh UI every REFRESH_MS */
        struct timespec now;
        clock_gettime(CLOCK_MONOTONIC, &now);
        long elapsed = now.tv_sec * 1000 + now.tv_nsec / 1000000;
        if (elapsed - last_tick >= REFRESH_MS) {
            /* Update dynamic content (e.g., clock) */
            time_t t = time(NULL);
            char buf[64];
            strftime(buf, sizeof(buf), "%H:%M:%S", localtime(&t));
            mvprintw(0, COLS - 9, "%s", buf);
            refresh();
            last_tick = elapsed;
        }

        /* 3️⃣ Sleep briefly to avoid 100% CPU */
        tv.tv_sec = 0;
        tv.tv_usec = 5000;   // 5 ms
        select(0, NULL, NULL, NULL, &tv);
    }

    endwin();
    return 0;
}

Key takeaways:

  • nodelay makes getch return immediately; ERR indicates no input.
  • KEY_RESIZE is emitted when the terminal size changes (important for responsive UIs).
  • Using select or nanosleep prevents busy‑waiting, keeping CPU usage low.
  • Only redraw changing areas (mvprintw + refresh) – avoid clear each loop unless necessary.

Real‑World Use Cases

Ncurses isn’t just a teaching tool; it powers many essential utilities you may already use.

ApplicationPrimary ncurses Feature(s)Description
htopMultiple windows, colors, mouse supportInteractive process viewer with sortable columns and real‑time graphs.
vimFull‑screen editing, syntax highlighting, custom key mappingsClassic modal editor built on top of ncurses’ low‑level screen handling.
midnight commander (mc)Panels, dual‑pane file manager, menusUses panels for overlapping panes and menus for file operations.
ncduPads for scrolling directory treesEfficiently renders large directory listings using pads for virtual scrolling.
aptitudeForms, menus, colorProvides a full package manager UI with forms for configuration and menus for selections.
nmtui (NetworkManager UI)Forms and menusAllows network configuration through a ncurses interface.

These tools illustrate how ncurses scales from simple status bars to sophisticated, multi‑window applications with mouse interaction and dynamic layout.


Performance & Portability Tips

10.1 Minimize Full‑Screen Refreshes

Calling clear() or refresh() on stdscr forces the whole screen to be redrawn. Instead:

  • Use wnoutrefresh() on each window, then doupdate() once per frame.
  • Update only the regions that changed (mvaddch, mvaddstr).

10.2 Use nocbreak()/cbreak() Wisely

  • cbreak() is ideal for interactive programs that need immediate key response.
  • If you only need occasional input, consider halfdelay() with a timeout (e.g., halfdelay(1) for 0.1 s).

10.3 Detect Terminal Capabilities

Ncurses provides tigetstr, tigetnum, and tigetflag to query terminfo capabilities. Example:

if (tigetflag("km")) {
    // Terminal has a keypad; enable it
    keypad(stdscr, TRUE);
}

10.4 Wide‑Character Support

For internationalized applications, compile against ncursesw (-lncursesw). Use wchar_t functions (addwstr, mvaddwstr) and ensure locale is set:

#include <locale.h>
setlocale(LC_ALL, "");

10.5 Thread Safety

Ncurses is not thread‑safe by default. If you need to update UI from multiple threads, protect all ncurses calls with a mutex, or funnel UI updates through a single dedicated thread.

10.6 Cross‑Platform Considerations

  • Windows: Use Cygwin or WSL for native ncurses. Projects like PDCurses provide a Windows‑compatible implementation.
  • Embedded Linux: Ensure the target’s terminfo database exists (/usr/share/terminfo). Minimal builds can embed compiled terminfo entries using tic -x.

Building & Linking – Makefile Essentials

A typical project with multiple modules (ui.c, logic.c, main.c) and optional panels, forms, and menus might look like this:

CC      = gcc
CFLAGS  = -Wall -Wextra -O2 -std=c11
LDFLAGS = -lncurses -lpanel -lform -lmenu

SRC     = main.c ui.c logic.c
OBJ     = $(SRC:.c=.o)

TARGET  = myapp

all: $(TARGET)

$(TARGET): $(OBJ)
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -f $(OBJ) $(TARGET)

If you don’t need forms or menus, simply drop -lform -lmenu from LDFLAGS.
For static linking on systems where it’s desirable (e.g., embedded), replace -lncurses with the full path to libncurses.a.


Beyond ncurses: Alternatives & The Future

While ncurses remains the de‑facto standard for terminal UI on Unix‑like systems, several newer libraries address its limitations or target different paradigms.

LibraryLanguageHighlights
libtcodC, Python, RustDesigned for roguelike games; supports true‑color, advanced input, and a higher‑level console abstraction.
termboxC, Go, RustMinimalist, event‑driven API; works on Windows via conio.
blessed (Node.js)JavaScriptHigh‑level widget system for terminal web‑style apps.
tui-rsRustDeclarative UI with a React‑like component model.
curses‑compatible PDCursesC, WindowsProvides ncurses API on native Windows console.

These alternatives often offer:

  • True‑color (24‑bit) support out of the box.
  • Higher‑level layout managers (grid, flex).
  • Better mouse handling (drag, scroll events).

Nevertheless, ncurses’ maturity, extensive documentation, and ubiquity in system distributions keep it indispensable for low‑level tools, installers, and utilities where dependencies must remain minimal.


Conclusion

Ncurses remains a powerful, flexible toolkit for building sophisticated terminal interfaces. By mastering its core concepts—windows, pads, panels, forms, and menus—you can craft applications that feel native, responsive, and visually appealing even on the most modest terminals.

Key takeaways:

  • Initialize correctly (initscr, cbreak, noecho, keypad).
  • Leverage windows for modular UI and avoid full-screen redraws.
  • Use pads for any content that exceeds the visible area.
  • Adopt panels to manage overlapping components cleanly.
  • Employ forms and menus when you need ready‑made data entry or selection widgets.
  • Design an efficient event loop that balances input latency, UI refresh, and CPU usage.
  • Mind portability—test on various terminals, handle resizing, and consider wide‑character support.

With the examples and patterns presented here, you’re equipped to develop everything from a personal system monitor to a full‑featured text editor. The next step? Pick a small project, apply the techniques, and iterate. The terminal is a canvas; ncurses gives you the brush and palette.

Happy coding—may your windows never flicker, and your pads always scroll smoothly!


Resources

  1. Official ncurses Documentation – Comprehensive reference, terminfo database, and API guide.
    https://invisible-island.net/ncurses/

  2. ncurses Programming Howto – A classic tutorial covering windows, colors, panels, forms, and menus.
    https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/

  3. PDCurses – ncurses for Windows – Source, build instructions, and API compatibility notes.
    https://pdcurses.org/

  4. “The Linux Programming Interface” (Chapter on Terminals) – Provides context on terminal handling and ncurses integration.
    https://man7.org/tlpi/

  5. GitHub – ncurses examples repository – Community‑contributed sample programs and Makefiles.
    https://github.com/mirror/ncurses/tree/master/examples