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.
En la mayoría de distribuciones Linux, ncurses ya está instalado. Para desarrollo:
sudo apt-get install libncurses5-dev libncursesw5-dev # Debian/Ubuntu
sudo dnf install ncurses-devel # Fedora
sudo pacman -S ncurses # Arch Linux
Para compilar un programa que usa ncurses:
g++ programa.cpp -o programa -lncurses
#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;
}
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 |
#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;
}
ncurses permite modificar la apariencia del texto con atributos:
// 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
ncurses permite crear y manipular múltiples ventanas independientes. Cada ventana tiene su propio buffer y puede ser manipulada independientemente.
// 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);
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 |
#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;
}
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.
// 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();
ncurses tiene 8 colores básicos:
COLOR_BLACK
COLOR_RED
COLOR_GREEN
COLOR_YELLOW
COLOR_BLUE
COLOR_MAGENTA
COLOR_CYAN
COLOR_WHITE
// 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));
#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.
ncurses proporciona varias funciones para manejar la entrada del teclado (y en algunos casos, del ratón).
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 |
Cuando keypad()
está habilitado, ncurses puede detectar teclas especiales:
// 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();
}
Puedes configurar un tiempo de espera para la entrada:
// 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);
}
#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;
}
La biblioteca de formularios de ncurses permite crear interfaces de entrada de datos complejas.
#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;
}
La biblioteca de paneles extiende ncurses con capacidades de ventanas superpuestas y manejo de profundidad (z-order).
#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;
}
#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;
}
Usa clases para manejar la inicialización y limpieza automática:
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;
};
Siempre verifica los errores en funciones críticas:
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;
}
Minimiza las llamadas a refresh()
y usa wnoutrefresh()
+ doupdate()
cuando sea posible:
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();
}
curs_set(0)
para ocultar el cursor cuando no sea necesarionodelay()
para aplicaciones que necesiten procesamiento en segundo plano