Manual de NCurses en Rust

Introducción a NCurses en Rust

NCurses es una biblioteca para el desarrollo de interfaces de usuario en terminales. Proporciona una API para crear interfaces textuales eficientes y portátiles.

En Rust, podemos usar el crate pancurses o ncurses para interactuar con la biblioteca NCurses. Este manual se enfocará en pancurses por su mayor facilidad de uso y seguridad.

Nota: NCurses es especialmente útil para aplicaciones que necesitan:

  • Interfaces interactivas en terminal
  • Juegos basados en texto
  • Herramientas de administración de sistemas
  • Aplicaciones que deben funcionar en entornos sin GUI

Instalación y configuración

Para comenzar, añade la siguiente dependencia a tu Cargo.toml:

[dependencies]
pancurses = { version = "0.17", features = ["win32"] }  # Incluye soporte para Windows

En sistemas Unix, necesitarás tener instalada la biblioteca NCurses:

Instalación en Linux (Debian/Ubuntu)

sudo apt-get install libncurses5-dev libncursesw5-dev

Instalación en macOS

brew install ncurses

Advertencia: En Windows, pancurses usará la API de consola de Windows en lugar de NCurses nativo. El comportamiento puede diferir ligeramente.

Inicialización básica

El siguiente ejemplo muestra cómo inicializar NCurses y crear una ventana básica:

use pancurses::{initscr, endwin, Input, noecho, curs_set};

fn main() {
    // Inicializar NCurses
    let window = initscr();
    
    // Configurar la pantalla
    noecho();  // No mostrar caracteres tecleados
    curs_set(0);  // Ocultar el cursor
    
    // Imprimir un mensaje
    window.printw("¡Hola, NCurses en Rust!");
    window.refresh();
    
    // Esperar entrada del usuario
    window.getch();
    
    // Limpiar NCurses
    endwin();
}

Funciones clave de inicialización:

Función Descripción
initscr() Inicializa NCurses y devuelve la ventana principal (stdscr)
endwin() Finaliza correctamente la aplicación NCurses
noecho() Desactiva el eco de caracteres tecleados
curs_set(visibility) Controla la visibilidad del cursor (0=invisible, 1=normal, 2=muy visible)
refresh() Actualiza la pantalla para mostrar cambios

Consejo: Siempre llama a endwin() antes de salir de tu aplicación para restaurar el estado original del terminal.

Trabajo con ventanas

NCurses permite crear múltiples ventanas independientes. Cada ventana tiene su propio buffer de pantalla.

Creación de ventanas

use pancurses::{newwin, Window};

fn main() {
    let main_win = pancurses::initscr();
    pancurses::noecho();
    
    // Crear una nueva ventana de 10 filas x 30 columnas
    // Comenzando en la posición (5, 10)
    let sub_win = newwin(10, 30, 5, 10);
    
    // Dibujar en la ventana principal
    main_win.printw("Ventana principal");
    main_win.refresh();
    
    // Dibujar en la subventana
    sub_win.printw("Subventana");
    sub_win.refresh();
    
    main_win.getch();
    pancurses::endwin();
}

Métodos comunes de ventana

Método Descripción
mv(y, x) Mueve el cursor a la posición (y, x)
mvprintw(y, x, text) Mueve el cursor e imprime texto
border(ls, rs, ts, bs, tl, tr, bl, br) Dibuja un borde alrededor de la ventana
clear() Limpia la ventana
erase() Borra el contenido de la ventana
getmaxyx() Devuelve las dimensiones de la ventana (filas, columnas)

Ejemplo avanzado con múltiples ventanas

use pancurses::{initscr, endwin, newwin, Input};

fn main() {
    let main_win = initscr();
    pancurses::noecho();
    pancurses::curs_set(0);
    
    // Obtener dimensiones de la pantalla
    let (rows, cols) = main_win.get_max_yx();
    
    // Crear ventanas
    let header = newwin(3, cols, 0, 0);
    let body = newwin(rows - 6, cols, 3, 0);
    let footer = newwin(3, cols, rows - 3, 0);
    
    // Dibujar header
    header.border('|', '|', '-', '-', '+', '+', '+', '+');
    header.mvprintw(1, 1, "Aplicación NCurses en Rust");
    header.refresh();
    
    // Dibujar body
    body.mvprintw(1, 1, "Contenido principal");
    body.mvprintw(2, 1, &format!("Tamaño: {}x{}", rows, cols));
    body.refresh();
    
    // Dibujar footer
    footer.border('|', '|', '-', '-', '+', '+', '+', '+');
    footer.mvprintw(1, 1, "Presione cualquier tecla para salir");
    footer.refresh();
    
    main_win.getch();
    endwin();
}

Manejo de entrada del usuario

NCurses proporciona varias formas de capturar entrada del usuario, desde teclas simples hasta eventos de ratón.

Captura básica de teclas

use pancurses::{initscr, endwin, Input};

fn main() {
    let window = initscr();
    pancurses::noecho();
    pancurses::curs_set(0);
    
    window.printw("Presione una tecla (q para salir)");
    window.refresh();
    
    loop {
        match window.getch() {
            Some(Input::Character('q')) => break,
            Some(input) => {
                window.clear();
                window.printw(&format!("Presionaste: {:?}", input));
                window.refresh();
            },
            None => continue,
        }
    }
    
    endwin();
}

Tipos de entrada comunes

Tipo Ejemplo Descripción
Input::Character(c) 'a', '1', '@' Caracteres alfanuméricos
Input::KeyUp KEY_UP Flecha arriba
Input::KeyDown KEY_DOWN Flecha abajo
Input::KeyLeft KEY_LEFT Flecha izquierda
Input::KeyRight KEY_RIGHT Flecha derecha
Input::KeyEnter KEY_ENTER Tecla Enter
Input::KeyBackspace KEY_BACKSPACE Tecla Retroceso

Captura de ratón

use pancurses::{initscr, endwin, Input, mousemask, ALL_MOUSE_EVENTS};

fn main() {
    let window = initscr();
    pancurses::noecho();
    pancurses::curs_set(0);
    
    // Habilitar eventos de ratón
    mousemask(ALL_MOUSE_EVENTS, None);
    
    window.printw("Haga clic con el ratón (q para salir)");
    window.refresh();
    
    loop {
        match window.getch() {
            Some(Input::Character('q')) => break,
            Some(Input::KeyMouse(mouse_event)) => {
                window.clear();
                window.printw(&format!("Evento de ratón: {:?}", mouse_event));
                window.refresh();
            },
            Some(input) => {
                window.clear();
                window.printw(&format!("Entrada: {:?}", input));
                window.refresh();
            },
            None => continue,
        }
    }
    
    endwin();
}

Sistema de colores

NCurses permite trabajar con colores en terminales que los soportan. Primero debemos inicializar el sistema de colores.

Inicialización de colores

use pancurses::{initscr, endwin, start_color, init_pair, COLOR_PAIR, COLOR_BLACK, 
                 COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE};

fn main() {
    let window = initscr();
    start_color();  // Inicializar sistema de colores
    
    // Definir pares de colores (ID, foreground, background)
    init_pair(1, COLOR_RED, COLOR_BLACK);
    init_pair(2, COLOR_GREEN, COLOR_BLACK);
    init_pair(3, COLOR_BLUE, COLOR_WHITE);
    
    window.attron(COLOR_PAIR(1));
    window.printw("Texto en rojo\n");
    window.attroff(COLOR_PAIR(1));
    
    window.attron(COLOR_PAIR(2));
    window.printw("Texto en verde\n");
    window.attroff(COLOR_PAIR(2));
    
    window.attron(COLOR_PAIR(3));
    window.printw("Texto azul sobre blanco\n");
    window.attroff(COLOR_PAIR(3));
    
    window.refresh();
    window.getch();
    endwin();
}

Colores predefinidos

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

Definición de colores personalizados

En terminales que soportan 256 colores, puedes definir colores personalizados:

use pancurses::{initscr, endwin, start_color, init_color, init_pair, COLOR_PAIR};

fn main() {
    let window = initscr();
    start_color();
    
    // Definir color personalizado (ID, R, G, B) (0-1000)
    init_color(100, 1000, 500, 0);  // Naranja
    
    // Crear par de colores usando nuestro color personalizado
    init_pair(1, 100, COLOR_BLACK);
    
    window.attron(COLOR_PAIR(1));
    window.printw("Texto en color naranja personalizado");
    window.attroff(COLOR_PAIR(1));
    
    window.refresh();
    window.getch();
    endwin();
}

Atributos de texto

Además de colores, NCurses permite aplicar varios atributos de texto:

Atributos comunes

use pancurses::{initscr, endwin, A_BOLD, A_UNDERLINE, A_REVERSE, A_BLINK};

fn main() {
    let window = initscr();
    
    window.printw("Texto normal\n");
    
    window.attron(A_BOLD);
    window.printw("Texto en negrita\n");
    window.attroff(A_BOLD);
    
    window.attron(A_UNDERLINE);
    window.printw("Texto subrayado\n");
    window.attroff(A_UNDERLINE);
    
    window.attron(A_REVERSE);
    window.printw("Texto invertido\n");
    window.attroff(A_REVERSE);
    
    window.attron(A_BLINK);
    window.printw("Texto parpadeante\n");
    window.attroff(A_BLINK);
    
    window.refresh();
    window.getch();
    endwin();
}

Combinación de atributos

Puedes combinar múltiples atributos usando el operador OR bit a bit:

use pancurses::{initscr, endwin, A_BOLD, A_UNDERLINE, start_color, init_pair, COLOR_PAIR};

fn main() {
    let window = initscr();
    start_color();
    init_pair(1, COLOR_RED, COLOR_BLACK);
    
    // Combinar múltiples atributos
    window.attron(COLOR_PAIR(1) | A_BOLD | A_UNDERLINE);
    window.printw("Texto rojo, negrita y subrayado");
    window.attroff(COLOR_PAIR(1) | A_BOLD | A_UNDERLINE);
    
    window.refresh();
    window.getch();
    endwin();
}

Formularios y entrada de texto

Para crear formularios interactivos, necesitamos manejar la entrada de texto y validación:

Campo de texto básico

use pancurses::{initscr, endwin, Input, echo, nocbreak, curs_set};

fn get_input(window: &pancurses::Window, prompt: &str, y: i32, x: i32) -> String {
    window.mvprintw(y, x, prompt);
    window.refresh();
    
    let mut input = String::new();
    loop {
        match window.getch() {
            Some(Input::Character(c)) => {
                if c == '\n' {
                    break;
                } else {
                    input.push(c);
                    window.addch(c);
                }
            },
            Some(Input::KeyBackspace) if !input.is_empty() => {
                input.pop();
                let (y, x) = window.get_cur_yx();
                window.mv(y, x - 1);
                window.delch();
            },
            Some(Input::Character('q')) => break,
            _ => {}
        }
    }
    
    input
}

fn main() {
    let window = initscr();
    echo();  // Mostrar caracteres tecleados
    nocbreak();  // Permitir entrada línea a línea
    curs_set(1);  // Mostrar cursor
    
    let name = get_input(&window, "Nombre: ", 5, 5);
    let age = get_input(&window, "Edad: ", 7, 5);
    
    window.clear();
    window.mvprintw(10, 5, &format!("Hola {}, tienes {} años", name, age));
    window.refresh();
    window.getch();
    
    endwin();
}

Formulario con múltiples campos

use pancurses::{initscr, endwin, Input, echo, nocbreak, curs_set, A_UNDERLINE};

struct FormField {
    label: String,
    value: String,
    y: i32,
    x: i32,
    width: i32,
    active: bool,
}

impl FormField {
    fn new(label: &str, y: i32, x: i32, width: i32) -> Self {
        FormField {
            label: label.to_string(),
            value: String::new(),
            y,
            x,
            width,
            active: false,
        }
    }
    
    fn draw(&self, window: &pancurses::Window) {
        window.mvprintw(self.y, self.x, &self.label);
        
        let display_value = if self.value.is_empty() && !self.active {
            "_".repeat(self.width as usize)
        } else {
            self.value.clone()
        };
        
        if self.active {
            window.attron(A_UNDERLINE);
        }
        
        window.mvprintw(self.y, self.x + self.label.len() as i32 + 1, &display_value);
        
        if self.active {
            window.attroff(A_UNDERLINE);
            window.mv(self.y, self.x + self.label.len() as i32 + 1 + self.value.len() as i32);
        }
    }
    
    fn handle_input(&mut self, window: &pancurses::Window, input: Input) -> bool {
        match input {
            Input::Character(c) if c.is_ascii() && self.value.len() < self.width as usize => {
                self.value.push(c);
                true
            },
            Input::KeyBackspace if !self.value.is_empty() => {
                self.value.pop();
                true
            },
            Input::KeyEnter => false,
            _ => true,
        }
    }
}

fn main() {
    let window = initscr();
    echo();
    nocbreak();
    curs_set(1);
    
    let mut fields = vec![
        FormField::new("Nombre:", 5, 5, 20),
        FormField::new("Email:", 7, 5, 20),
        FormField::new("Teléfono:", 9, 5, 15),
    ];
    
    let mut current_field = 0;
    fields[current_field].active = true;
    
    loop {
        window.clear();
        
        // Dibujar título
        window.mvprintw(2, 5, "Formulario de contacto");
        
        // Dibujar campos
        for field in &fields {
            field.draw(&window);
        }
        
        window.refresh();
        
        match window.getch() {
            Some(Input::KeyDown) if current_field < fields.len() - 1 => {
                fields[current_field].active = false;
                current_field += 1;
                fields[current_field].active = true;
            },
            Some(Input::KeyUp) if current_field > 0 => {
                fields[current_field].active = false;
                current_field -= 1;
                fields[current_field].active = true;
            },
            Some(input) => {
                if !fields[current_field].handle_input(&window, input) {
                    // Enter presionado
                    if current_field < fields.len() - 1 {
                        fields[current_field].active = false;
                        current_field += 1;
                        fields[current_field].active = true;
                    } else {
                        break;  // Fin del formulario
                    }
                }
            },
            None => {}
        }
    }
    
    window.clear();
    window.mvprintw(12, 5, "Datos ingresados:");
    for (i, field) in fields.iter().enumerate() {
        window.mvprintw(14 + i as i32, 5, &format!("{}: {}", field.label, field.value));
    }
    window.refresh();
    window.getch();
    
    endwin();
}

Paneles (panels) para interfaces complejas

Los paneles son una extensión de NCurses que permiten manejar ventanas superpuestas de manera más eficiente.

Uso básico de paneles

use pancurses::{initscr, endwin, newwin, Input};
use pancurses::panel::{Panel, new_panel};

fn main() {
    let window = initscr();
    pancurses::noecho();
    pancurses::curs_set(0);
    
    // Crear ventanas base
    let main_win = newwin(20, 50, 0, 0);
    main_win.printw("Ventana principal - Presione 'p' para mostrar panel, 'q' para salir");
    main_win.refresh();
    
    // Crear panel (inicialmente oculto)
    let panel_win = newwin(10, 30, 5, 10);
    panel_win.border('|', '|', '-', '-', '+', '+', '+', '+');
    panel_win.mvprintw(2, 2, "Este es un panel");
    panel_win.mvprintw(3, 2, "Presione cualquier tecla");
    panel_win.mvprintw(4, 2, "para ocultarlo");
    
    let panel = new_panel(panel_win);
    panel.hide();
    
    pancurses::update_panels();
    pancurses::doupdate();
    
    loop {
        match window.getch() {
            Some(Input::Character('p')) => {
                panel.show();
                pancurses::update_panels();
                pancurses::doupdate();
            },
            Some(Input::Character('q')) => break,
            Some(_) => {
                panel.hide();
                pancurses::update_panels();
                pancurses::doupdate();
            },
            None => {}
        }
    }
    
    endwin();
}

Sistema de paneles apilados

use pancurses::{initscr, endwin, newwin, Input};
use pancurses::panel::{Panel, new_panel};

fn main() {
    let window = initscr();
    pancurses::noecho();
    pancurses::curs_set(0);
    
    // Crear ventana principal
    let main_win = newwin(20, 50, 0, 0);
    main_win.printw("Ventana principal - Use 1-3 para mostrar paneles, q para salir");
    main_win.refresh();
    
    // Crear tres paneles
    let panel1 = new_panel(newwin(8, 25, 3, 5));
    panel1.window().border('|', '|', '-', '-', '+', '+', '+', '+');
    panel1.window().mvprintw(2, 2, "Panel 1 (Fondo)");
    panel1.window().mvprintw(3, 2, "Tecla: 1");
    
    let panel2 = new_panel(newwin(8, 25, 6, 15));
    panel2.window().border('|', '|', '-', '-', '+', '+', '+', '+');
    panel2.window().mvprintw(2, 2, "Panel 2 (Medio)");
    panel2.window().mvprintw(3, 2, "Tecla: 2");
    
    let panel3 = new_panel(newwin(8, 25, 9, 25));
    panel3.window().border('|', '|', '-', '-', '+', '+', '+', '+');
    panel3.window().mvprintw(2, 2, "Panel 3 (Frente)");
    panel3.window().mvprintw(3, 2, "Tecla: 3");
    
    // Inicialmente ocultar todos los paneles
    panel1.hide();
    panel2.hide();
    panel3.hide();
    
    pancurses::update_panels();
    pancurses::doupdate();
    
    loop {
        match window.getch() {
            Some(Input::Character('1')) => {
                panel1.show();
                panel1.top();  // Mover al frente
                pancurses::update_panels();
                pancurses::doupdate();
            },
            Some(Input::Character('2')) => {
                panel2.show();
                panel2.top();
                pancurses::update_panels();
                pancurses::doupdate();
            },
            Some(Input::Character('3')) => {
                panel3.show();
                panel3.top();
                pancurses::update_panels();
                pancurses::doupdate();
            },
            Some(Input::Character('q')) => break,
            Some(Input::Character('h')) => {
                // Ocultar todos los paneles
                panel1.hide();
                panel2.hide();
                panel3.hide();
                pancurses::update_panels();
                pancurses::doupdate();
            },
            _ => {}
        }
    }
    
    endwin();
}

Ejemplos completos

Editor de texto simple

use pancurses::{initscr, endwin, Input, noecho, cbreak, curs_set, A_UNDERLINE};

struct TextEditor {
    content: String,
    cursor_pos: usize,
}

impl TextEditor {
    fn new() -> Self {
        TextEditor {
            content: String::new(),
            cursor_pos: 0,
        }
    }
    
    fn insert_char(&mut self, c: char) {
        self.content.insert(self.cursor_pos, c);
        self.cursor_pos += 1;
    }
    
    fn delete_char(&mut self) {
        if self.cursor_pos > 0 && !self.content.is_empty() {
            self.content.remove(self.cursor_pos - 1);
            self.cursor_pos -= 1;
        }
    }
    
    fn move_left(&mut self) {
        if self.cursor_pos > 0 {
            self.cursor_pos -= 1;
        }
    }
    
    fn move_right(&mut self) {
        if self.cursor_pos < self.content.len() {
            self.cursor_pos += 1;
        }
    }
    
    fn draw(&self, window: &pancurses::Window, y: i32, x: i32) {
        window.mv(y, x);
        
        // Mostrar contenido
        window.printw(&self.content);
        
        // Mostrar cursor
        let (mut cur_y, mut cur_x) = window.get_cur_yx();
        window.mv(cur_y, x + self.cursor_pos as i32);
        window.attron(A_UNDERLINE);
        window.printw(if self.cursor_pos < self.content.len() { 
            &self.content[self.cursor_pos..=self.cursor_pos] 
        } else { 
            " " 
        });
        window.attroff(A_UNDERLINE);
    }
}

fn main() {
    let window = initscr();
    noecho();
    cbreak();
    curs_set(0);  // Ocultamos el cursor nativo
    
    let mut editor = TextEditor::new();
    let mut running = true;
    
    window.mvprintw(1, 1, "Editor de texto simple (Ctrl+X para salir)");
    window.mv(3, 1);
    
    while running {
        window.clear();
        window.mvprintw(1, 1, "Editor de texto simple (Ctrl+X para salir)");
        editor.draw(&window, 3, 1);
        window.refresh();
        
        match window.getch() {
            Some(Input::Character(c)) => {
                match c {
                    '\x18' => running = false,  // Ctrl+X
                    _ => editor.insert_char(c),
                }
            },
            Some(Input::KeyBackspace) => editor.delete_char(),
            Some(Input::KeyLeft) => editor.move_left(),
            Some(Input::KeyRight) => editor.move_right(),
            Some(Input::KeyDC) => {  // Tecla Delete
                if editor.cursor_pos < editor.content.len() {
                    editor.cursor_pos += 1;
                    editor.delete_char();
                }
            },
            Some(Input::KeyHome) => editor.cursor_pos = 0,
            Some(Input::KeyEnd) => editor.cursor_pos = editor.content.len(),
            _ => {}
        }
    }
    
    endwin();
}

Juego Snake simple

use pancurses::{initscr, endwin, Input, noecho, cbreak, curs_set, napms};
use std::collections::VecDeque;

#[derive(Debug, Clone, Copy, PartialEq)]
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

struct SnakeGame {
    snake: VecDeque<(i32, i32)>,
    direction: Direction,
    food: (i32, i32),
    width: i32,
    height: i32,
    game_over: bool,
}

impl SnakeGame {
    fn new(width: i32, height: i32) -> Self {
        let mut snake = VecDeque::new();
        snake.push_back((width / 2, height / 2));
        
        SnakeGame {
            snake,
            direction: Direction::Right,
            food: (5, 5),
            width,
            height,
            game_over: false,
        }
    }
    
    fn change_direction(&mut self, new_dir: Direction) {
        match (self.direction, new_dir) {
            (Direction::Up, Direction::Down) |
            (Direction::Down, Direction::Up) |
            (Direction::Left, Direction::Right) |
            (Direction::Right, Direction::Left) => {},
            _ => self.direction = new_dir,
        }
    }
    
    fn update(&mut self) {
        if self.game_over {
            return;
        }
        
        let head = self.snake.front().unwrap();
        let new_head = match self.direction {
            Direction::Up => (head.0, head.1 - 1),
            Direction::Down => (head.0, head.1 + 1),
            Direction::Left => (head.0 - 1, head.1),
            Direction::Right => (head.0 + 1, head.1),
        };
        
        // Verificar colisiones
        if new_head.0 <= 0 || new_head.0 >= self.width - 1 || 
           new_head.1 <= 0 || new_head.1 >= self.height - 1 ||
           self.snake.contains(&new_head) {
            self.game_over = true;
            return;
        }
        
        // Mover la serpiente
        self.snake.push_front(new_head);
        
        // Verificar si comió la comida
        if new_head == self.food {
            // Generar nueva comida
            self.food = (
                (rand::random::() % (self.width as u32 - 2) + 1) as i32,
                (rand::random::() % (self.height as u32 - 2) + 1) as i32,
            );
        } else {
            self.snake.pop_back();
        }
    }
    
    fn draw(&self, window: &pancurses::Window) {
        // Dibujar bordes
        window.border('|', '|', '-', '-', '+', '+', '+', '+');
        
        // Dibujar serpiente
        for &(x, y) in &self.snake {
            window.mvaddch(y, x, 'O');
        }
        
        // Dibujar cabeza
        if let Some(&(x, y)) = self.snake.front() {
            window.mvaddch(y, x, '@');
        }
        
        // Dibujar comida
        window.mvaddch(self.food.1, self.food.0, '*');
        
        // Dibujar puntuación
        window.mvprintw(0, 2, &format!("Puntuación: {}", self.snake.len() - 1));
        
        if self.game_over {
            window.mvprintw(self.height / 2, self.width / 2 - 5, "GAME OVER!");
        }
    }
}

fn main() {
    let window = initscr();
    noecho();
    cbreak();
    curs_set(0);
    
    let (height, width) = window.get_max_yx();
    let mut game = SnakeGame::new(width - 2, height - 2);
    
    window.timeout(100);  // Establecer tiempo de espera para entrada
    
    while !game.game_over {
        window.clear();
        game.draw(&window);
        window.refresh();
        
        // Manejar entrada
        match window.getch() {
            Some(Input::KeyUp) => game.change_direction(Direction::Up),
            Some(Input::KeyDown) => game.change_direction(Direction::Down),
            Some(Input::KeyLeft) => game.change_direction(Direction::Left),
            Some(Input::KeyRight) => game.change_direction(Direction::Right),
            Some(Input::Character('q')) => break,
            _ => {}
        }
        
        game.update();
        napms(150);  // Controlar velocidad del juego
    }
    
    window.getch();  // Esperar antes de salir
    endwin();
}

Mejores prácticas y consejos

Patrones comunes

1. Estructura básica de una aplicación NCurses:

fn main() {
    // 1. Inicialización
    let window = initscr();
    noecho();
    cbreak();
    curs_set(0);
    
    // 2. Configuración inicial
    start_color();
    init_pair(1, COLOR_RED, COLOR_BLACK);
    
    // 3. Bucle principal
    let mut running = true;
    while running {
        // Manejo de entrada
        match window.getch() {
            Some(Input::Character('q')) => running = false,
            _ => {}
        }
        
        // Lógica del programa
        
        // Renderizado
        window.clear();
        window.printw("Contenido");
        window.refresh();
    }
    
    // 4. Limpieza
    endwin();
}

Consejos de rendimiento

  • Usa window.noutrefresh() seguido de pancurses::doupdate() cuando tengas múltiples ventanas para actualizar. Esto reduce el parpadeo.
  • Evita llamar a refresh() o clear() más de lo necesario.
  • Para aplicaciones con mucha actualización, considera usar window.leaveok(true) para optimizar el movimiento del cursor.

Manejo de errores

NCurses puede fallar silenciosamente en algunas operaciones. Implementa comprobaciones:

fn safe_init() -> Result {
    let window = pancurses::initscr();
    if window.is_null() {
        return Err("No se pudo inicializar NCurses".to_string());
    }
    
    pancurses::noecho();
    pancurses::cbreak();
    pancurses::curs_set(0);
    
    if pancurses::start_color() != pancurses::OK {
        return Err("No se pudo inicializar colores".to_string());
    }
    
    Ok(window)
}

Compatibilidad entre plataformas

  • Algunas teclas pueden tener códigos diferentes en Windows vs Unix.
  • Los colores pueden verse diferente en distintos terminales.
  • Prueba tu aplicación en múltiples terminales (al menos en Windows Terminal, Linux console y macOS Terminal).