Continuamos con la serie.
El post anterior nos dejó con una TUI funcional: eventos conectados, ciclo de vida de terminal manejado, los 12 tests pasando, cero cambios en el dominio. La promesa hexagonal se cumplió.
Pero “funcional” y “bueno” son cosas distintas. La interfaz era monocromática, los UUID consumían ancho de pantalla, las operaciones exitosas no daban feedback, y una lista de tareas vacía mostraba una tabla en blanco sin orientación. La rata ha construido la cocina. Ahora aprende a emplatar.
El último 20% que es el 80% de la experiencia #
Hay una regla conocida en desarrollo de producto: el último 20% de pulido requiere el 80% del esfuerzo. Lo inverso también es cierto: ese 20% representa el 80% de la experiencia del usuario. Una herramienta puede ser arquitectónicamente limpia y funcionalmente correcta, pero si la interfaz es plana, confusa o silenciosa, nadie disfrutará usándola.
La rata abordó cinco mejoras en un lote enfocado.
1. Codificación por colores: haciendo la interfaz escaneable #
Antes: todo era del mismo color. Estado, título, ID, timestamps: todo texto de terminal por defecto. El usuario tenía que leer cada celda para entender el estado de una tarea.
Después: el color codifica significado:
- Estado:
[ ] TODOen amarillo,[x] DONEen verde. Escaneo visual instantáneo: puedes ver de un vistazo cuántas tareas están pendientes sin leer una sola palabra. - ID y timestamps: gris oscuro. Metadatos de-enfatizados. Están ahí si los necesitas, pero no compiten con el título y el estado por la atención.
- Encabezado: cian y negrita. Claramente distinguible de las filas de datos.
- Fila seleccionada: fondo gris oscuro con texto en negrita. Reemplaza el enfoque anterior donde el color de resaltado podía chocar con el color del estado.
- Atajos de la barra de comandos: coloreados por categoría. Cian para creación (
[a]dd,[e]dit), rojo para destructivos ([d]el), amarillo para toggle de estado ([x]done/todo), magenta para filtro ([f]ilter). Hace los atajos escaneables de un vistazo.
Esto no es decoración. El color es información. Cuando el usuario mira la pantalla, debería poder responder “¿cuántas tareas están hechas?” en menos de un segundo, sin leer.
2. Mensajes de feedback positivo #
Antes: status_message solo se establecía en errores. Las operaciones exitosas daban cero feedback. El usuario pulsaba una tecla y tenía que escanear visualmente la tabla para confirmar que algo pasó.
Después: cada acción le dice al usuario lo que hizo:
| Acción | Mensaje de feedback |
|---|---|
| Añadir tarea | "Task added: Buy groceries" |
| Eliminar tarea | "Deleted: Buy groceries" |
| Marcar como hecho | "Done: Buy groceries" |
| Marcar como pendiente | "Todo: Buy groceries" |
| Tarea no encontrada | "Task not found" |
| Cualquier error | "Error: ..." |
Un pequeño detalle de implementación: el feedback de eliminación captura el título de la tarea antes de la eliminación. Si eliminas primero e intentas leer el título después, la tarea ya no existe. El orden importa.
Los mensajes de estado aparecen en la barra de estado y se limpian cuando el usuario navega (pulsa j/k o flechas). Esto los hace transitorios: visibles el tiempo suficiente para confirmar la acción, desaparecidos en cuanto te mueves. No son desorden persistente.
3. Manejo de estado vacío #
Antes: cuando la lista de tareas estaba vacía, la tabla se renderizaba solo con un encabezado y espacio en blanco. Sin indicación de qué hacer.
Después: cuando app.tasks.is_empty(), la tabla se reemplaza con un Paragraph centrado:
No tasks yet. Press [a] to add one.Texto gris oscuro, centrado en el área con borde. El usuario sabe inmediatamente qué hacer. Este es un cambio pequeño con impacto desproporcionado en usuarios nuevos. Una pantalla vacía confunde; un mensaje de guía da la bienvenida.
4. Auto-limpieza de mensajes de estado #
Antes: los mensajes de estado persistían indefinidamente hasta que otra acción los reemplazaba. Navega durante cinco minutos y sigue viendo “Task added: Buy groceries” de antes.
Después: un método clear_status() se llama desde event.rs cada vez que el usuario navega o entra en un nuevo modo. Los métodos de acción (add_task, delete_task, cycle_todo_done) gestionan sus propios mensajes de estado. El resultado: los mensajes aparecen brevemente tras una acción y desaparecen en la siguiente navegación. Feedback transitorio, no ruido persistente.
5. Cursor visual en modo Adding #
Antes: en modo Adding, el usuario escribía texto pero no había indicador de cursor. Solo texto apareciendo carácter por carácter.
Después: un carácter █ (bloque completo) en cian aparece después del texto del buffer de entrada. Simula un cursor parpadeante. El bloque completo es la forma de cursor de terminal universalmente reconocida, a diferencia de _ (guión bajo) que podría confundirse con un guión bajo real en el texto.
New: Buy groc█ [Enter] confirm [Esc] cancelEl usuario puede ver exactamente dónde está escribiendo. Un detalle pequeño, pero la entrada de texto sin cursor se siente rota.
El popup de entrada: de barra de comandos a modal #
Las cinco mejoras anteriores fueron victorias rápidas. El popup fue un cambio arquitectónico mayor en la capa View.
El problema con la entrada inline #
Originalmente, la entrada de texto ocurría en la barra de comandos: una sola fila en la parte inferior de la pantalla. Funcionaba, pero era estrecha, fácil de pasar por alto, y mezclaba texto de entrada con texto de ayuda. Para un título de tarea como “Escribir el informe trimestral para el departamento de marketing”, la barra de comandos de una sola fila truncaría o solaparía el texto de ayuda.
La solución: un popup modal centrado #
Un popup modal es el patrón UX estándar para entrada de texto en apps TUI. Dirige la atención del usuario, proporciona límites visuales claros, y tiene espacio tanto para el campo de entrada como para el texto de ayuda:
│ ┌──────────────────────┐ │
│ │ New Task │ │
│ │ │ │
│ │ Buy groceries█ │ │
│ │ │ │
│ │ [Enter] ok [Esc] no │ │
│ └──────────────────────┘ │Implementación: centered_rect y orden de renderizado #
El popup se construye en dos pasos:
Paso 1: calcular un rectángulo centrado.
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let vertical = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
]).split(area);
Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
]).split(vertical[1])[1]
}Splits anidados vertical + horizontal. Los márgenes exteriores se calculan como (100 - porcentaje_objetivo) / 2. El resultado es el Rect interior en el centro. Geometría limpia.
Paso 2: renderizar el popup encima de la UI normal.
pub fn draw(app: &App, frame: &mut Frame, table_state: &mut TableState) {
// Normal rendering: table, command bar, status bar
render_table(app, frame, table_state);
render_command_bar(app, frame);
render_status_bar(app, frame);
// Popup overlay (only in Adding or Editing mode)
if matches!(app.input_mode, InputMode::Adding | InputMode::Editing) {
render_input_popup(frame, app);
}
}El popup se renderiza después de la UI normal. El renderizado en modo inmediato de ratatui lo hace trivial: lo que dibujas último sobrescribe lo que hay debajo. Para asegurar un fondo limpio, un widget Clear se renderiza primero en el área del popup, borrando cualquier contenido de tabla que se pintó en la primera pasada.
El título y color del popup se adaptan al modo: cian para “New Task” (Adding), amarillo para “Edit Task” (Editing). El usuario ve de un vistazo si está creando o modificando.
Dónde estamos #
La rata sabe emplatar. Los colores codifican significado, el feedback confirma acciones, los estados vacíos guían a nuevos usuarios, y la entrada de texto ocurre en un popup enfocado en vez de una barra inferior estrecha.
Pero hay un problema sutil de rendimiento escondido en el bucle de eventos, y una operación CRUD faltante (editar) que pondrá a prueba la arquitectura una vez más. El próximo post cierra la serie con polling de eventos no bloqueante y la funcionalidad de edición como un corte vertical completo.
Código de referencia: