Manual Completo de ncurses en C++

Tabla de Contenidos

1. Introducción a ncurses

ncurses (New Curses) es una biblioteca de programación que proporciona una API para desarrollar interfaces de usuario basadas en texto en terminales. Es ampliamente utilizada para crear aplicaciones TUI (Text-based User Interface).

Nota: ncurses es la versión mejorada de la biblioteca original curses, desarrollada en los años 80 para BSD Unix.

Características principales:

2. Configuración del entorno

Instalación en Linux

En la mayoría de distribuciones Linux, ncurses ya está instalado. Para desarrollo:

bash
sudo apt-get install libncurses5-dev libncursesw5-dev  # Debian/Ubuntu
sudo dnf install ncurses-devel                     # Fedora
sudo pacman -S ncurses                             # Arch Linux

Compilación de programas con ncurses

Para compilar un programa que usa ncurses:

bash
g++ programa.cpp -o programa -lncurses

Estructura básica de un programa ncurses

C++
#include <ncurses.h>

int main() {
    // Inicializar ncurses
    initscr();
    
    // Desactivar buffering de línea
    cbreak();
    
    // No mostrar la entrada del teclado
    noecho();
    
    // Habilitar teclas especiales (F1, flechas, etc.)
    keypad(stdscr, TRUE);
    
    // Tu código aquí...
    printw("Hola, mundo con ncurses!");
    refresh();
    
    // Esperar entrada del usuario
    getch();
    
    // Finalizar ncurses
    endwin();
    
    return 0;
}

3. Funciones básicas

Funciones esenciales

Función Descripción
initscr() Inicializa ncurses y crea la ventana estándar stdscr
endwin() Finaliza la sesión de ncurses
refresh() Actualiza la pantalla física con los cambios en la pantalla virtual
printw() Similar a printf, pero para ncurses
getch() Obtiene un carácter del teclado
move(y, x) Mueve el cursor a la posición (y, x)
addch(ch) Añade un carácter en la posición actual

Ejemplo completo con funciones básicas

C++
#include <ncurses.h>
#include <string>

int main() {
    initscr();          // Iniciar ncurses
    cbreak();           // Desactivar buffering de línea
    noecho();           // No mostrar caracteres tecleados
    keypad(stdscr, TRUE); // Habilitar teclas especiales
    
    // Obtener dimensiones de la pantalla
    int height, width;
    getmaxyx(stdscr, height, width);
    
    // Centrar texto
    std::string message = "Bienvenido a ncurses!";
    int x = (width - message.length()) / 2;
    int y = height / 2;
    
    move(y, x);
    printw("%s", message.c_str());
    
    // Dibujar un borde
    border(0, 0, 0, 0, 0, 0, 0, 0);
    
    // Actualizar pantalla
    refresh();
    
    // Esperar entrada
    getch();
    
    endwin();           // Finalizar ncurses
    return 0;
}

Atributos de texto

ncurses permite modificar la apariencia del texto con atributos:

C++
// Activar atributos
attron(A_BOLD | A_UNDERLINE);
printw("Texto en negrita y subrayado");
attroff(A_BOLD | A_UNDERLINE);

// Atributos disponibles:
// A_NORMAL      - Texto normal
// A_STANDOUT    - Máxima visibilidad
// A_UNDERLINE   - Subrayado
// A_REVERSE     - Invertir colores
// A_BLINK       - Parpadeo (no soportado en todos los terminales)
// A_DIM         - Texto tenue
// A_BOLD        - Negrita
// A_PROTECT     - Modo protegido
// A_INVIS       - Texto invisible

4. Manejo de ventanas

ncurses permite crear y manipular múltiples ventanas independientes. Cada ventana tiene su propio buffer y puede ser manipulada independientemente.

Creación de ventanas

C++
// Crear una nueva ventana
WINDOW *win = newwin(height, width, start_y, start_x);

// Ejemplo: crear una ventana centrada
int height = 10;
int width = 40;
int start_y = (LINES - height) / 2;  // LINES es el total de filas
int start_x = (COLS - width) / 2;    // COLS es el total de columnas

WINDOW *my_win = newwin(height, width, start_y, start_x);

Funciones para ventanas

Función Descripción
newwin() Crea una nueva ventana
delwin() Elimina una ventana
box() Dibuja un borde alrededor de la ventana
wrefresh() Actualiza una ventana específica
mvwin() Mueve una ventana a una nueva posición
subwin() Crea una subventana dentro de otra ventana

Ejemplo completo con ventanas

C++
#include <ncurses.h>
#include <string>

int main() {
    initscr();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);
    
    // Crear ventana principal
    WINDOW *main_win = newwin(20, 50, 2, 5);
    box(main_win, 0, 0);
    mvwprintw(main_win, 1, 1, "Ventana Principal");
    wrefresh(main_win);
    
    // Crear ventana secundaria
    WINDOW *side_win = newwin(20, 20, 2, 60);
    box(side_win, 0, 0);
    mvwprintw(side_win, 1, 1, "Panel Lateral");
    wrefresh(side_win);
    
    // Actualizar contenido dinámicamente
    for(int i = 1; i <= 10; i++) {
        mvwprintw(main_win, i+2, 2, "Item %d", i);
        wrefresh(main_win);
        napms(300); // Esperar 300ms
    }
    
    getch();
    
    // Limpiar
    delwin(main_win);
    delwin(side_win);
    endwin();
    
    return 0;
}

5. Sistema de colores

ncurses soporta colores en terminales que lo permitan. El sistema de colores funciona con pares de colores (color pairs) que combinan un color de primer plano y un color de fondo.

Inicialización del sistema de colores

C++
// Verificar si el terminal soporta colores
if(has_colors() == FALSE) {
    endwin();
    printf("Tu terminal no soporta colores\n");
    return 1;
}

// Inicializar el sistema de colores
start_color();

Colores predefinidos

ncurses tiene 8 colores básicos:

Definir pares de colores

C++
// Definir un par de colores (ID, foreground, background)
init_pair(1, COLOR_RED, COLOR_BLACK);      // Rojo sobre negro
init_pair(2, COLOR_GREEN, COLOR_BLUE);     // Verde sobre azul
init_pair(3, COLOR_WHITE, COLOR_MAGENTA);  // Blanco sobre magenta

// Usar un par de colores
attron(COLOR_PAIR(1));
printw("Texto rojo sobre negro");
attroff(COLOR_PAIR(1));

Ejemplo completo con colores

C++
#include <ncurses.h>

int main() {
    initscr();
    start_color();
    
    // Definir nuestros pares de colores
    init_pair(1, COLOR_RED, COLOR_BLACK);
    init_pair(2, COLOR_GREEN, COLOR_BLACK);
    init_pair(3, COLOR_YELLOW, COLOR_BLACK);
    init_pair(4, COLOR_BLUE, COLOR_BLACK);
    init_pair(5, COLOR_MAGENTA, COLOR_BLACK);
    init_pair(6, COLOR_CYAN, COLOR_BLACK);
    init_pair(7, COLOR_WHITE, COLOR_BLACK);
    
    // Mostrar todos los colores
    for(int i = 1; i <= 7; i++) {
        attron(COLOR_PAIR(i));
        printw("Este es el color pair %d\n", i);
        attroff(COLOR_PAIR(i));
    }
    
    // Combinar atributos y colores
    attron(A_BOLD | COLOR_PAIR(2));
    printw("Texto en negrita y verde\n");
    attroff(A_BOLD | COLOR_PAIR(2));
    
    refresh();
    getch();
    endwin();
    
    return 0;
}

Importante: Algunos terminales permiten más de 8 colores básicos. Puedes verificar COLORS y COLOR_PAIRS después de llamar a start_color() para conocer los límites de tu terminal.

6. Manejo de entrada

ncurses proporciona varias funciones para manejar la entrada del teclado (y en algunos casos, del ratón).

Funciones básicas de entrada

Función Descripción
getch() Obtiene un carácter del teclado
getstr() Obtiene una cadena de caracteres
scanw() Similar a scanf, para entrada formateada
getnstr() Obtiene una cadena con límite de longitud

Teclas especiales

Cuando keypad() está habilitado, ncurses puede detectar teclas especiales:

C++
// Ejemplo de manejo de teclas especiales
int ch;
while((ch = getch()) != 'q') {
    switch(ch) {
        case KEY_UP:
            printw("Flecha arriba\n");
            break;
        case KEY_DOWN:
            printw("Flecha abajo\n");
            break;
        case KEY_LEFT:
            printw("Flecha izquierda\n");
            break;
        case KEY_RIGHT:
            printw("Flecha derecha\n");
            break;
        case KEY_HOME:
            printw("Tecla Home\n");
            break;
        case KEY_END:
            printw("Tecla End\n");
            break;
        case KEY_F(1):
            printw("Tecla F1\n");
            break;
        // ... otras teclas
        default:
            printw("Tecla normal: %c\n", ch);
    }
    refresh();
}

Entrada con timeout

Puedes configurar un tiempo de espera para la entrada:

C++
// Esperar entrada por máximo 1 segundo (1000ms)
timeout(1000);
int ch = getch();
if(ch == ERR) {
    printw("No se presionó ninguna tecla en 1 segundo\n");
} else {
    printw("Tecla presionada: %c\n", ch);
}

Ejemplo completo: Editor de texto simple

C++
#include <ncurses.h>
#include <string>
#include <vector>

int main() {
    initscr();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);
    
    std::vector<std::string> lines;
    lines.push_back(""); // Línea inicial vacía
    
    int y = 0, x = 0;
    bool running = true;
    
    while(running) {
        clear();
        
        // Mostrar todas las líneas
        for(size_t i = 0; i < lines.size(); i++) {
            mvprintw(i, 0, "%s", lines[i].c_str());
        }
        
        // Posicionar cursor
        move(y, x);
        refresh();
        
        // Manejar entrada
        int ch = getch();
        switch(ch) {
            case KEY_UP:
                if(y > 0) y--;
                break;
            case KEY_DOWN:
                if(y < (int)lines.size() - 1) y++;
                break;
            case KEY_LEFT:
                if(x > 0) x--;
                break;
            case KEY_RIGHT:
                if(x < (int)lines[y].length()) x++;
                break;
            case KEY_BACKSPACE:
            case 127: // Tecla delete en algunos sistemas
                if(x > 0) {
                    lines[y].erase(x-1, 1);
                    x--;
                } else if(y > 0) {
                    x = lines[y-1].length();
                    lines[y-1] += lines[y];
                    lines.erase(lines.begin() + y);
                    y--;
                }
                break;
            case '\n': // Enter
                lines.insert(lines.begin() + y + 1, lines[y].substr(x));
                lines[y] = lines[y].substr(0, x);
                y++;
                x = 0;
                break;
            case 27: // ESC
                running = false;
                break;
            default:
                if(isprint(ch)) {
                    lines[y].insert(x, 1, ch);
                    x++;
                }
        }
        
        // Ajustar x si es necesario
        if(x > (int)lines[y].length()) {
            x = lines[y].length();
        }
    }
    
    endwin();
    return 0;
}

8. Formularios

La biblioteca de formularios de ncurses permite crear interfaces de entrada de datos complejas.

Conceptos básicos

Ejemplo de formulario

C++
#include <ncurses.h>
#include <form.h>
#include <vector>

int main() {
    initscr();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);
    
    // Crear campos
    std::vector<FIELD*> fields = {
        new_field(1, 10, 0, 0, 0, 0),   // Nombre
        new_field(1, 10, 1, 0, 0, 0),   // Apellido
        new_field(1, 15, 2, 0, 0, 0),   // Email
        nullptr // El array debe terminar con nullptr
    };
    
    // Configurar campos
    set_field_back(fields[0], A_UNDERLINE);
    set_field_back(fields[1], A_UNDERLINE);
    set_field_back(fields[2], A_UNDERLINE);
    
    field_opts_off(fields[0], O_AUTOSKIP);
    field_opts_off(fields[1], O_AUTOSKIP);
    field_opts_off(fields[2], O_AUTOSKIP);
    
    // Crear etiquetas
    mvprintw(0, 12, "Nombre:");
    mvprintw(1, 12, "Apellido:");
    mvprintw(2, 12, "Email:");
    
    // Crear el formulario
    FORM *my_form = new_form(fields.data());
    
    // Crear ventana para el formulario
    WINDOW *form_win = newwin(5, 30, 2, 2);
    keypad(form_win, TRUE);
    
    // Asignar ventana al formulario
    set_form_win(my_form, form_win);
    set_form_sub(my_form, derwin(form_win, 3, 28, 1, 1));
    
    // Mostrar formulario
    post_form(my_form);
    wrefresh(form_win);
    
    // Manejar entrada
    int ch;
    while((ch = wgetch(form_win))) {
        switch(ch) {
            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_LEFT:
                form_driver(my_form, REQ_PREV_CHAR);
                break;
            case KEY_RIGHT:
                form_driver(my_form, REQ_NEXT_CHAR);
                break;
            case KEY_BACKSPACE:
            case 127:
                form_driver(my_form, REQ_DEL_PREV);
                break;
            case 10: // Enter
                form_driver(my_form, REQ_VALIDATION);
                goto end;
            case 27: // ESC
                goto end;
            default:
                form_driver(my_form, ch);
                break;
        }
        wrefresh(form_win);
    }
    
end:
    // Obtener valores
    mvprintw(10, 0, "Nombre: %s", field_buffer(fields[0], 0));
    mvprintw(11, 0, "Apellido: %s", field_buffer(fields[1], 0));
    mvprintw(12, 0, "Email: %s", field_buffer(fields[2], 0));
    refresh();
    getch();
    
    // Limpiar
    unpost_form(my_form);
    free_form(my_form);
    for(FIELD *field : fields) {
        if(field) free_field(field);
    }
    endwin();
    return 0;
}

9. Paneles (panels)

La biblioteca de paneles extiende ncurses con capacidades de ventanas superpuestas y manejo de profundidad (z-order).

Conceptos básicos

Ejemplo con paneles

C++
#include <ncurses.h>
#include <panel.h>
#include <vector>

int main() {
    initscr();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);
    
    // Crear ventanas
    WINDOW *win1 = newwin(10, 20, 2, 4);
    WINDOW *win2 = newwin(10, 20, 5, 8);
    WINDOW *win3 = newwin(10, 20, 8, 12);
    
    // Rellenar ventanas
    box(win1, 0, 0);
    mvwprintw(win1, 1, 1, "Panel 1 (Fondo)");
    
    box(win2, 0, 0);
    mvwprintw(win2, 1, 1, "Panel 2 (Medio)");
    
    box(win3, 0, 0);
    mvwprintw(win3, 1, 1, "Panel 3 (Superior)");
    
    // Crear paneles
    PANEL *panel1 = new_panel(win1);
    PANEL *panel2 = new_panel(win2);
    PANEL *panel3 = new_panel(win3);
    
    // Actualizar el stack de paneles
    update_panels();
    doupdate();
    
    // Instrucciones
    mvprintw(LINES-3, 0, "1, 2, 3: Traer panel al frente");
    mvprintw(LINES-2, 0, "q: Salir");
    refresh();
    
    // Manejar entrada
    int ch;
    while((ch = getch()) != 'q') {
        switch(ch) {
            case '1':
                top_panel(panel1);
                break;
            case '2':
                top_panel(panel2);
                break;
            case '3':
                top_panel(panel3);
                break;
        }
        update_panels();
        doupdate();
    }
    
    // Limpiar
    del_panel(panel1);
    del_panel(panel2);
    del_panel(panel3);
    delwin(win1);
    delwin(win2);
    delwin(win3);
    endwin();
    return 0;
}

10. Ejemplos avanzados

Editor de texto con múltiples ventanas

C++
#include <ncurses.h>
#include <string>
#include <vector>
#include <panel.h>

class TextEditor {
private:
    std::vector<std::string> content;
    int cursor_x, cursor_y;
    WINDOW *editor_win;
    PANEL *editor_panel;
    
public:
    TextEditor(int height, int width, int y, int x) {
        content.push_back("");
        cursor_x = cursor_y = 0;
        
        editor_win = newwin(height, width, y, x);
        editor_panel = new_panel(editor_win);
        keypad(editor_win, TRUE);
    }
    
    void handle_input(int ch) {
        switch(ch) {
            case KEY_UP:
                if(cursor_y > 0) cursor_y--;
                break;
            case KEY_DOWN:
                if(cursor_y < (int)content.size()-1) cursor_y++;
                break;
            case KEY_LEFT:
                if(cursor_x > 0) cursor_x--;
                break;
            case KEY_RIGHT:
                if(cursor_x < (int)content[cursor_y].length()) cursor_x++;
                break;
            case KEY_BACKSPACE:
            case 127:
                if(cursor_x > 0) {
                    content[cursor_y].erase(cursor_x-1, 1);
                    cursor_x--;
                } else if(cursor_y > 0) {
                    cursor_x = content[cursor_y-1].length();
                    content[cursor_y-1] += content[cursor_y];
                    content.erase(content.begin() + cursor_y);
                    cursor_y--;
                }
                break;
            case '\n':
                content.insert(content.begin() + cursor_y + 1, 
                              content[cursor_y].substr(cursor_x));
                content[cursor_y] = content[cursor_y].substr(0, cursor_x);
                cursor_y++;
                cursor_x = 0;
                break;
            default:
                if(isprint(ch)) {
                    content[cursor_y].insert(cursor_x, 1, ch);
                    cursor_x++;
                }
        }
        
        // Ajustar cursor_x si es necesario
        if(cursor_x > (int)content[cursor_y].length()) {
            cursor_x = content[cursor_y].length();
        }
    }
    
    void draw() {
        werase(editor_win);
        box(editor_win, 0, 0);
        
        // Mostrar contenido
        for(size_t i = 0; i < content.size(); i++) {
            mvwprintw(editor_win, i+1, 1, "%s", content[i].c_str());
        }
        
        // Posicionar cursor
        wmove(editor_win, cursor_y+1, cursor_x+1);
    }
    
    WINDOW* get_win() { return editor_win; }
};

int main() {
    initscr();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);
    
    // Crear editor principal
    TextEditor main_editor(LINES-2, COLS/2, 1, 1);
    
    // Crear ventana de estado
    WINDOW *status_win = newwin(1, COLS, LINES-1, 0);
    PANEL *status_panel = new_panel(status_win);
    wattron(status_win, A_REVERSE);
    wprintw(status_win, "Modo: Edición | F1: Ayuda | F2: Guardar | ESC: Salir");
    wattroff(status_win, A_REVERSE);
    
    // Actualizar paneles
    update_panels();
    doupdate();
    
    // Bucle principal
    bool running = true;
    while(running) {
        main_editor.draw();
        wrefresh(main_editor.get_win());
        
        int ch = wgetch(main_editor.get_win());
        switch(ch) {
            case 27: // ESC
                running = false;
                break;
            case KEY_F(1):
                // Mostrar ayuda
                break;
            case KEY_F(2):
                // Guardar archivo
                break;
            default:
                main_editor.handle_input(ch);
        }
    }
    
    endwin();
    return 0;
}

11. Buenas prácticas

Patrón RAII para manejo de recursos

Usa clases para manejar la inicialización y limpieza automática:

C++
class NcursesManager {
public:
    NcursesManager() {
        initscr();
        cbreak();
        noecho();
        keypad(stdscr, TRUE);
        start_color();
    }
    
    ~NcursesManager() {
        endwin();
    }
    
    // Eliminar copias
    NcursesManager(const NcursesManager&) = delete;
    NcursesManager& operator=(const NcursesManager&) = delete;
};

Manejo de errores

Siempre verifica los errores en funciones críticas:

C++
WINDOW *create_centered_window(int height, int width) {
    if(height > LINES || width > COLS) {
        return nullptr;
    }
    
    int y = (LINES - height) / 2;
    int x = (COLS - width) / 2;
    
    WINDOW *win = newwin(height, width, y, x);
    if(!win) {
        // Manejar error
    }
    
    return win;
}

Optimización de redibujado

Minimiza las llamadas a refresh() y usa wnoutrefresh() + doupdate() cuando sea posible:

C++
void update_interface(WINDOW *win1, WINDOW *win2) {
    // Actualizar ambas ventanas sin refrescar la pantalla física
    wnoutrefresh(win1);
    wnoutrefresh(win2);
    
    // Refrescar la pantalla física una sola vez
    doupdate();
}

Consejos generales