Manual Completo de ncurses en Python

Creación de interfaces de usuario en terminal con estilo Dracula

Introducción a ncurses

ncurses es una biblioteca de programación que proporciona una API para desarrollar interfaces de usuario en modo texto. Permite crear aplicaciones con menús, ventanas, colores y más, directamente en la terminal.

Nota: ncurses es la versión nueva de la biblioteca curses, que originalmente fue desarrollada para sistemas UNIX.

¿Por qué usar ncurses?

Instalación

Para usar ncurses en Python, necesitamos el módulo curses, que viene incluido en la biblioteca estándar en sistemas UNIX. Para Windows, puedes usar windows-curses:

# Instalación en Windows (si es necesario)
pip install windows-curses

Conceptos Básicos

Inicialización y Configuración

Antes de usar ncurses, necesitamos inicializar el sistema. La función curses.initscr() hace esto:

import curses

def main(stdscr):
    # Configuración inicial
    curses.curs_set(0)  # Ocultar cursor
    stdscr.clear()
    stdscr.refresh()
    
    # Tu código aquí...
    
    stdscr.getch()  # Esperar entrada de usuario

if __name__ == "__main__":
    curses.wrapper(main)

Mejor práctica: Siempre usa curses.wrapper() que maneja adecuadamente la inicialización y limpieza, incluso si ocurren errores.

Pantallas y Ventanas

En ncurses trabajamos con:

Sistema de Coordenadas

ncurses usa un sistema de coordenadas (y, x) donde:

Funciones Básicas

Mostrar Texto

Las funciones principales para mostrar texto son:

Función Descripción
addstr(y, x, text) Escribe texto en la posición (y, x)
addch(y, x, char) Escribe un solo carácter en (y, x)
insstr(y, x, text) Inserta texto desplazando el existente
delch(y, x) Borra un carácter

Ejemplo: Mostrar texto básico

import curses

def main(stdscr):
    curses.curs_set(0)
    stdscr.clear()
    
    # Mostrar texto en diferentes posiciones
    stdscr.addstr(5, 10, "¡Hola, mundo con ncurses!")
    stdscr.addstr(7, 10, "Presiona cualquier tecla para continuar...")
    
    stdscr.refresh()
    stdscr.getch()

curses.wrapper(main)

Atributos de Texto

Puedes aplicar atributos al texto para cambiar su apariencia:

Atributo Descripción
curses.A_BOLD Texto en negrita
curses.A_UNDERLINE Texto subrayado
curses.A_REVERSE Invertir colores de fondo/primer plano
curses.A_BLINK Texto parpadeante

Ejemplo: Atributos de texto

def main(stdscr):
    curses.curs_set(0)
    stdscr.clear()
    
    # Texto con diferentes atributos
    stdscr.addstr(5, 10, "Texto normal")
    stdscr.addstr(6, 10, "Texto en negrita", curses.A_BOLD)
    stdscr.addstr(7, 10, "Texto subrayado", curses.A_UNDERLINE)
    stdscr.addstr(8, 10, "Texto invertido", curses.A_REVERSE)
    stdscr.addstr(9, 10, "Texto parpadeante", curses.A_BLINK)
    
    stdscr.refresh()
    stdscr.getch()

curses.wrapper(main)

Trabajo con Colores

ncurses permite trabajar con colores. Primero debemos inicializar el sistema de colores:

curses.start_color()  # Inicializar sistema de colores

Pares de Color

Los colores se definen en pares (fondo, texto). Puedes crear tus propios pares:

curses.init_pair(pair_number, foreground, background)

Ejemplo: Configuración de colores estilo Dracula

def init_colors():
    # Definir colores Dracula
    curses.init_color(100, 40, 42, 54)    # Fondo: #282a36
    curses.init_color(101, 248, 248, 242) # Texto: #f8f8f2
    curses.init_color(102, 189, 147, 249)  # Purpura: #bd93f9
    curses.init_color(103, 255, 121, 198) # Rosa: #ff79c6
    curses.init_color(104, 80, 250, 123)   # Verde: #50fa7b
    
    # Definir pares de color
    curses.init_pair(1, 101, 100)  # Texto normal
    curses.init_pair(2, 102, 100)  # Destacado 1
    curses.init_pair(3, 103, 100)  # Destacado 2
    curses.init_pair(4, 104, 100)  # Éxito

def main(stdscr):
    curses.curs_set(0)
    curses.start_color()
    init_colors()
    
    stdscr.bkgd(' ', curses.color_pair(1))
    stdscr.clear()
    
    stdscr.addstr(5, 10, "Título importante", curses.color_pair(2) | curses.A_BOLD)
    stdscr.addstr(7, 10, "Mensaje informativo", curses.color_pair(1))
    stdscr.addstr(9, 10, "Acción exitosa!", curses.color_pair(4))
    
    stdscr.refresh()
    stdscr.getch()

curses.wrapper(main)

Colores Predefinidos

ncurses incluye algunos colores básicos:

Constante Color
curses.COLOR_BLACK Negro
curses.COLOR_RED Rojo
curses.COLOR_GREEN Verde
curses.COLOR_YELLOW Amarillo
curses.COLOR_BLUE Azul
curses.COLOR_MAGENTA Magenta
curses.COLOR_CYAN Cian
curses.COLOR_WHITE Blanco

Trabajo con Ventanas

Además de la ventana principal (stdscr), puedes crear ventanas independientes:

newwin(height, width, begin_y, begin_x)

Ejemplo: Creación de ventanas

def main(stdscr):
    curses.curs_set(0)
    curses.start_color()
    stdscr.clear()
    
    # Crear una ventana secundaria
    win = curses.newwin(10, 40, 5, 10)
    win.border()
    win.addstr(2, 2, "Esta es una ventana secundaria")
    win.addstr(4, 2, "Puede tener su propio contenido")
    win.refresh()
    
    stdscr.addstr(16, 10, "Presiona cualquier tecla para salir...")
    stdscr.refresh()
    stdscr.getch()

curses.wrapper(main)

Operaciones con Ventanas

Método Descripción
border([ls[, rs[, ts[, bs[, tl[, tr[, bl[, br]]]]]]]) Dibuja un borde alrededor de la ventana
box([vertch, horch]) Dibuja un borde simple
clear() Limpia la ventana
refresh() Actualiza la ventana
move(y, x) Mueve el cursor a (y, x)
nodelay(bool) Hace que getch() sea no bloqueante

Manejo de Entrada del Usuario

Captura de Teclas

La función principal para obtener entrada es getch():

Ejemplo: Captura de teclas básica

def main(stdscr):
    curses.curs_set(0)
    stdscr.clear()
    
    stdscr.addstr(5, 10, "Presiona una tecla (q para salir)")
    
    while True:
        key = stdscr.getch()
        stdscr.addstr(7, 10, f"Tecla presionada: {key} (carácter: {chr(key) if 32 <= key <= 126 else '?'})")
        stdscr.clrtoeol()  # Limpiar hasta el final de la línea
        
        if key == ord('q'):
            break
    
    stdscr.addstr(9, 10, "Saliendo...")
    stdscr.refresh()
    curses.napms(1000)  # Esperar 1 segundo

curses.wrapper(main)

Teclas Especiales

ncurses define constantes para teclas especiales:

Constante Tecla
curses.KEY_UP Flecha arriba
curses.KEY_DOWN Flecha abajo
curses.KEY_LEFT Flecha izquierda
curses.KEY_RIGHT Flecha derecha
curses.KEY_HOME Home
curses.KEY_END End
curses.KEY_BACKSPACE Backspace

Ejemplo: Manejo de teclas especiales

def main(stdscr):
    curses.curs_set(0)
    stdscr.keypad(True)  # Habilitar detección de teclas especiales
    stdscr.clear()
    
    y, x = 10, 10
    stdscr.addstr(y, x, "★")
    
    stdscr.addstr(20, 10, "Usa las flechas para mover la estrella. Q para salir")
    
    while True:
        key = stdscr.getch()
        
        if key == ord('q'):
            break
        elif key == curses.KEY_UP and y > 0:
            y -= 1
        elif key == curses.KEY_DOWN and y < curses.LINES - 1:
            y += 1
        elif key == curses.KEY_LEFT and x > 0:
            x -= 1
        elif key == curses.KEY_RIGHT and x < curses.COLS - 1:
            x += 1
        
        stdscr.clear()
        stdscr.addstr(y, x, "★")
        stdscr.addstr(20, 10, "Usa las flechas para mover la estrella. Q para salir")
        stdscr.refresh()
    
    stdscr.addstr(22, 10, "Saliendo...")
    stdscr.refresh()
    curses.napms(500)

curses.wrapper(main)

Ejemplo Completo: Editor de Texto Simple

Vamos a crear un editor de texto básico con ncurses:

import curses

class TextEditor:
    def __init__(self, stdscr):
        self.stdscr = stdscr
        self.text = []
        self.cursor_y = 0
        self.cursor_x = 0
        self.init_ui()
    
    def init_ui(self):
        curses.curs_set(1)  # Cursor visible
        self.stdscr.keypad(True)
        self.stdscr.clear()
        
        # Configurar colores
        curses.start_color()
        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
        curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
        
        self.status_win = curses.newwin(1, curses.COLS, curses.LINES-1, 0)
    
    def draw_status(self):
        self.status_win.clear()
        status = f"Línea: {self.cursor_y+1}/{len(self.text)} | Col: {self.cursor_x+1}"
        self.status_win.addstr(0, 0, status, curses.color_pair(2))
        self.status_win.refresh()
    
    def draw_text(self):
        self.stdscr.clear()
        for y, line in enumerate(self.text):
            if y < curses.LINES - 2:  # Dejar espacio para status
                self.stdscr.addstr(y, 0, line)
        self.stdscr.move(self.cursor_y, self.cursor_x)
        self.stdscr.refresh()
    
    def handle_input(self, key):
        if key == curses.KEY_UP:
            self.cursor_y = max(0, self.cursor_y - 1)
            self.cursor_x = min(self.cursor_x, len(self.text[self.cursor_y]))
        elif key == curses.KEY_DOWN:
            self.cursor_y = min(len(self.text) - 1, self.cursor_y + 1)
            self.cursor_x = min(self.cursor_x, len(self.text[self.cursor_y]))
        elif key == curses.KEY_LEFT:
            if self.cursor_x > 0:
                self.cursor_x -= 1
            elif self.cursor_y > 0:
                self.cursor_y -= 1
                self.cursor_x = len(self.text[self.cursor_y])
        elif key == curses.KEY_RIGHT:
            if self.cursor_x < len(self.text[self.cursor_y]):
                self.cursor_x += 1
            elif self.cursor_y < len(self.text) - 1:
                self.cursor_y += 1
                self.cursor_x = 0
        elif key == curses.KEY_BACKSPACE or key == 127:
            if self.cursor_x > 0:
                line = self.text[self.cursor_y]
                self.text[self.cursor_y] = line[:self.cursor_x-1] + line[self.cursor_x:]
                self.cursor_x -= 1
            elif self.cursor_y > 0:
                current_line = self.text[self.cursor_y]
                self.text.pop(self.cursor_y)
                self.cursor_y -= 1
                self.cursor_x = len(self.text[self.cursor_y])
                self.text[self.cursor_y] += current_line
        elif key == curses.KEY_ENTER or key == 10:
            line = self.text[self.cursor_y]
            new_line = line[self.cursor_x:]
            self.text[self.cursor_y] = line[:self.cursor_x]
            self.text.insert(self.cursor_y + 1, new_line)
            self.cursor_y += 1
            self.cursor_x = 0
        elif 32 <= key <= 126:  # Caracteres imprimibles
            line = self.text[self.cursor_y]
            self.text[self.cursor_y] = line[:self.cursor_x] + chr(key) + line[self.cursor_x:]
            self.cursor_x += 1
    
    def run(self):
        if not self.text:
            self.text.append("")
        
        while True:
            self.draw_text()
            self.draw_status()
            key = self.stdscr.getch()
            
            if key == 27:  # ESC
                break
            
            self.handle_input(key)

def main(stdscr):
    editor = TextEditor(stdscr)
    editor.run()

if __name__ == "__main__":
    curses.wrapper(main)

Conclusión

ncurses es una biblioteca poderosa para crear aplicaciones de terminal interactivas. Con Python, podemos aprovechar su sintaxis clara para construir interfaces complejas de manera relativamente sencilla.

Algunos consejos finales:

Recursos Adicionales