Table of Contents
- Introduction
- Getting Started: Installation & Build Setup
- Core Concepts of ncurses
- First Program – “Hello, ncurses!”
- Managing Multiple Windows
- Working with Pads for Large Scrollable Views
- The Panels Extension – Layered Interfaces
- Forms and Menus – Ready‑Made Widgets
- Designing an Event Loop
- Real‑World Use Cases
- Performance & Portability Tips
- Building & Linking – Makefile Essentials
- Beyond ncurses: Alternatives & The Future
- Conclusion
- 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
1. Installing ncurses on Popular Platforms
| Platform | Package Manager | Command |
|---|---|---|
| Ubuntu / Debian | apt | sudo apt-get install libncurses5-dev libncursesw5-dev |
| Fedora / CentOS | dnf / yum | sudo dnf install ncurses-devel |
| macOS | Homebrew | brew install ncurses |
| FreeBSD | Ports | cd /usr/ports/devel/ncurses && make install clean |
| Windows (WSL) | Ubuntu package | Same as Ubuntu command above |
Tip: The
-dev(ordevel) 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 withnewwin(rows, cols, y, x).- Sub‑windows – Created with
subwinorderwin, 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
prefreshspecifying the region to display.
3.2 Attributes & Color Pairs
Attributes modify the appearance of characters:
| Attribute | Macro |
|---|---|
| Bold | A_BOLD |
| Underline | A_UNDERLINE |
| Reverse video | A_REVERSE |
| Blink (if supported) | A_BLINK |
| Dim | A_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)– Makesgetchnon‑blocking, returningERRif 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 asMEVENTstructures viagetch()whenKEY_MOUSEis 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:
initscr()createsstdscrand determines the terminal size.cbreak()passes characters to the program immediately, whilenoecho()prevents them from being echoed automatically.has_colors()checks for colour capability;start_color()activates colour handling.mvprintw()moves the cursor and prints a string.refresh()pushes the buffer to the screen.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:
boxdraws a simple border; you can customize withwborder.bkgdsets background attributes for the whole screen; windows inherit unless overridden.delwinfrees resources; always clean up beforeendwin.
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:
- Polls input (keyboard, mouse, or timers).
- Updates application state based on the input.
- 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:
nodelaymakesgetchreturn immediately;ERRindicates no input.KEY_RESIZEis emitted when the terminal size changes (important for responsive UIs).- Using
selectornanosleepprevents busy‑waiting, keeping CPU usage low. - Only redraw changing areas (
mvprintw+refresh) – avoidcleareach loop unless necessary.
Real‑World Use Cases
Ncurses isn’t just a teaching tool; it powers many essential utilities you may already use.
| Application | Primary ncurses Feature(s) | Description |
|---|---|---|
| htop | Multiple windows, colors, mouse support | Interactive process viewer with sortable columns and real‑time graphs. |
| vim | Full‑screen editing, syntax highlighting, custom key mappings | Classic modal editor built on top of ncurses’ low‑level screen handling. |
| midnight commander (mc) | Panels, dual‑pane file manager, menus | Uses panels for overlapping panes and menus for file operations. |
| ncdu | Pads for scrolling directory trees | Efficiently renders large directory listings using pads for virtual scrolling. |
| aptitude | Forms, menus, color | Provides a full package manager UI with forms for configuration and menus for selections. |
| nmtui (NetworkManager UI) | Forms and menus | Allows 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, thendoupdate()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 usingtic -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.
| Library | Language | Highlights |
|---|---|---|
| libtcod | C, Python, Rust | Designed for roguelike games; supports true‑color, advanced input, and a higher‑level console abstraction. |
| termbox | C, Go, Rust | Minimalist, event‑driven API; works on Windows via conio. |
| blessed (Node.js) | JavaScript | High‑level widget system for terminal web‑style apps. |
| tui-rs | Rust | Declarative UI with a React‑like component model. |
| curses‑compatible PDCurses | C, Windows | Provides 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
Official ncurses Documentation – Comprehensive reference, terminfo database, and API guide.
https://invisible-island.net/ncurses/ncurses Programming Howto – A classic tutorial covering windows, colors, panels, forms, and menus.
https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/PDCurses – ncurses for Windows – Source, build instructions, and API compatibility notes.
https://pdcurses.org/“The Linux Programming Interface” (Chapter on Terminals) – Provides context on terminal handling and ncurses integration.
https://man7.org/tlpi/GitHub – ncurses examples repository – Community‑contributed sample programs and Makefiles.
https://github.com/mirror/ncurses/tree/master/examples