Aplicación textual

Construyendo Interfaces de Usuario en Terminal (TUIs) con Textual

Como parte de mi maestría, creé un pequeño proyecto: una aplicación CRUD para gestionar tareas usando MongoDB como backend y Textual para la interfaz en terminal. El repositorio está disponible en GitHub, y en este post, usaré este proyecto como ejemplo para tener mis apuntes, así también con la finalidad que le sea útil a quien desee  desarrollar TUIs(Text User Interface) con Textual.

Este manual es lo suficientemente detallado para que paso a paso, desde cero. trataré de explicar los conceptos básicos; si ya lo conoces, verás una implementación real con validaciones, modales, tablas y más. ¡Vamos a ello!

Aplicaciones construidas con Textual

aplicaciones con Textual

 

1. Posting

Cliente HTTP/API completo y usable desde la terminal — una suerte de “Postman en consola”. Ideal para desarrollar, probar APIs o explorar endpoints sin salir de la terminal.

URL: https://posting.sh/


2. Dolphie

Herramienta de monitoreo y administración para bases de datos MySQL/MariaDB/ProxySQL con interfaz TUI. Perfecto para administradores DB o desarrolladores que necesitan supervisar bases en tiempo real.
URL (GitHub): https://github.com/charles-001/dolphie


3. Elia

Cliente de terminal para interactuar con modelos de lenguaje (LLMs / ChatGPT / similares). Útil para quien trabaja en ciencia de datos, NLP, prototipado rápido de consultas a LLMs sin salir de la terminal.

URL (GitHub): https://github.com/darrenburns/elia


4. Harlequin

IDE de base de datos en la terminal: permite trabajar con bases SQL desde TUI, similar a herramientas gráficas pero en consola. Ideal para desarrolladores que prefieren trabajar sin salir del editor o terminal.

URL (GitHub): https://github.com/tconbeer/harlequin


5. Toolong

Visualizador/manipulador de archivos de log, JSONL y otros streams de texto desde terminal con interfaz interactiva. Muy útil para desarrolladores y operaciones (DevOps) que necesitan analizar logs rápidamente desde consola.

URL (GitHub): https://github.com/Textualize/toolong


6. gupshup

Cliente de chat en terminal hecho con Textual — puede servir para mensajería, chat bots, o integración con servicios de mensajería desde consola. Útil para desarrolladores que crean apps de chat o integraciones desde la terminal.

URL (GitHub): https://github.com/kraanzu/gupshup


7. kupo

Explorador de archivos en terminal, con interfaz amigable y navegación visual. Útil para quienes trabajan frecuentemente en terminal y quieren una interfaz mejor que ls/cd simples.

URL (GitHub): https://github.com/darrenburns/kupo


8. Net-Textorial

Herramienta TUI para ingenieros de redes que parsea datos de dispositivos de red comparando salida CLI cruda vs estructurada. Útil para automatización de redes.

URL: https://github.com/dannywade/net-textorial


9. Trogon

Genera interfaces terminales amigables automáticamente para aplicaciones CLI basadas en Click. Perfecto para desarrollo de herramientas de línea de comandos.

URL: https://github.com/Textualize/trogon


10. termtyper

Aplicación de práctica de mecanografía (typing test) en terminal, construida con Textual. Buena para demos, para ver cómo Textual maneja eventos de teclado, widgets dinámicos y actualización de UI en tiempo real. ([GitHub][5])
URL (GitHub): https://github.com/jenniferdewan/termtyper

 

¿Qué es Textual?

Textual es un framework de desarrollo rápido de aplicaciones (RAD) para Python, creado por Textualize.io. Te permite construir interfaces de usuario sofisticadas en la terminal (TUIs) usando una API simple y Pythonica. Además, soporta ejecución en navegadores web, lo que lo hace versátil.

Características Principales

  • Apps: La clase principal que gestiona el ciclo de vida de la interfaz, como eventos y composición de elementos.
  • Widgets: Componentes reutilizables como botones, tablas, inputs y selects para armar la UI.
  • Screens: Permiten navegar entre vistas diferentes, como pantallas modales o formularios.
  • Estilos con CSS: Usa reglas similares a CSS para personalizar layouts, colores y responsividad, haciendo que las TUIs se vean profesionales sin lidiar con complejidades de la terminal.
  • Otras Ventajas: Bajo consumo de recursos, multiplataforma, integración con CLI, acceso remoto via SSH y licencia MIT open-source.

¿Por Qué Usar Textual para TUIs?

Las TUIs son ideales para herramientas CLI potentes pero amigables, como editores (Vim), gestores de paquetes o dashboards. Textual simplifica esto: en lugar de manejar manualmente escapes ANSI o bibliotecas como curses, usas Python puro. Es perfecto para devs que ya conocen Python, y permite crear apps interactivas rápidamente. En mi proyecto, usé Textual para una app CRUD intuitiva, con tablas, formularios y modales, todo en la terminal.

Ejemplos reales de apps con Textual incluyen clientes API, visores de logs, profilers de memoria y dashboards de analytics. Si buscas inspiración, ¡prueba a clonar repos como el mío y experimenta!

Configurando Tu Entorno

Antes de codificar, prepara tu setup.

Requisitos

  • Python 3.8+ (yo usé 3.14 la versión π).
  • MongoDB instalado y corriendo localmente (o usa un contenedor Docker: docker run -d -p 27017:27017 mongo).
  • Un editor de código (VS Code con extensión Python funciona genial).

Instalando Dependencias

Crea un entorno virtual:

python -m venv venv
source venv/bin/activate  # En Linux/Mac
# O en Windows: venv\Scripts\activate

Instala las dependencias del proyecto (del requirements.txt):

textual
pydantic
python-dotenv
pymongo
rich
pendulum

Usa pip install -r requirements.txt para instalar todo de una vez.

Crea un archivo .env para la conexión a MongoDB:

MONGO_URI=mongodb://localhost:27017
MONGO_DB=tareas

¡Listo! Ahora, veamos la arquitectura del proyecto.

Visión General del Proyecto: CRUD de Tareas con MongoDB

El app gestiona tareas con campos como título, descripción, status (pendiente, en progreso, completado), fecha e ID. Usa MongoDB como base NoSQL.

Estructura de archivos:

├ .env                  # Configuración de entorno
├ app.py                # Punto de entrada principal
├ conky_config.conf     # Configuración para widget Conky (opcional)
├ db.py                 # Conexión a MongoDB
├ TareaSchema.py        # Modelo de validación con Pydantic
├ MainScreen.py         # Pantalla principal con tabla
├ requirements.txt      # Dependencias
├ style.css             # Estilos CSS para Textual
├ TareaFormScreen.py    # Formulario para crear/editar tareas
└ TareaService.py       # Lógica CRUD

El flujo: La app inicia en app.py, muestra una tabla en MainScreen.py, usa TareaFormScreen.py para forms, valida con TareaSchema.py, opera en DB via TareaService.py y db.py. Estilos en style.css.

Hay un modo --conky para integrar con Conky (un widget de escritorio en Linux), pero nos enfocaremos en la TUI.

Paso 1: Conexión a la Base de Datos (db.py)

Comencemos por el backend. Textual no maneja datos directamente, así que usamos PyMongo para MongoDB.

En db.py, cargamos el .env, conectamos y creamos un índice único en «id».

Código clave:

import os
from dotenv import load_dotenv
from pymongo import MongoClient
from pymongo import ASCENDING
from pymongo.errors import ConnectionFailure, OperationFailure
from pymongo.database import Database

load_dotenv()

MONGO_URI = os.getenv("MONGO_URI")
MONGO_DB_NAME = os.getenv("MONGO_DB")

client: MongoClient | None = None
db: Database | None = None

if not MONGO_URI or not MONGO_DB_NAME:
    print("FATAL: Faltan variables MONGO_URI o MONGO_DB en el archivo .env")
else:
    try:
        client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000)
        client.admin.command('serverStatus')
        db = client[MONGO_DB_NAME]
        db.tareas.create_index(
            [("id", ASCENDING)], 
            unique=True,
            background=True
        )
    except ConnectionFailure:
        print(f"ERROR: No se pudo conectar a MongoDB en {MONGO_URI}.")
        client = None
        db = None
    # ... (manejo de otros errores)

Explicación Paso a Paso:

  1. Carga variables de entorno con dotenv.
  2. Intenta conectar con timeout para no colgar.
  3. Verifica conexión con serverStatus.
  4. Crea DB y colección lazy (solo cuando se usa).
  5. Agrega índice único en «id» para evitar duplicados.

Tip: Siempre maneja errores; si DB falla, la app no crashea totalmente.

Paso 2: Modelos de Datos y Validación (TareaSchema.py)

Usamos Pydantic para validar datos. Esto asegura que las tareas cumplan reglas antes de guardar.

Código:

from pydantic import BaseModel, Field, validator
from datetime import date
from enum import Enum

class StatusEnum(str, Enum):
    pendiente = "pendiente"
    en_progreso = "en_progreso"
    completado = "completado"

class TareaSchema(BaseModel):
    titulo: str = Field(..., min_length=1)
    descripcion: str = Field(..., min_length=1)
    status: StatusEnum = Field(default=StatusEnum.pendiente)
    fecha: date
    id: int | None = None

# @validator("fecha")
def fecha_no_pasada(cls, v):
    if v < date.today():
        raise ValueError("La fecha no puede ser menor a la fecha actual.")
    return v

Explicación:

  • StatusEnum: Limita status a valores válidos.
  • TareaSchema: Define campos requeridos (... significa obligatorio).
  • Validación: Título y descripción no vacíos; fecha futura (comentada, pero puedes activarla).
  • En forms, instanciamos TareaSchema(**data) para validar.

Esto integra bien con Textual: Muestra notificaciones si falla.

Paso 3: Lógica CRUD (TareaService.py)

Esta clase encapsula operaciones DB usando aggregate para queries eficientes.

Código principal (extracto):

from db import db
from pymongo.collection import Collection

class TareaService:
    _collection: Collection | None = db.tareas if db is not None else None

    @classmethod
    def list(cls) -> list[dict]:
        if cls._collection is None:
            return []
        pipeline = [
            {"$project": {"_id": 0}}, 
            {"$sort": {"id": 1}}, 
        ]
        return list(cls._collection.aggregate(pipeline))

    @classmethod
    def next_id(cls) -> int:
        if cls._collection is None:
            return 1
        pipeline = [
            {"$sort": {"id": -1}}, 
            {"$limit": 1}, 
            {"$project": {"id": 1}}, 
        ]
        result = list(cls._collection.aggregate(pipeline))
        max_id = result[0]["id"] if result else 0
        return max_id + 1

    # ... (insert, get, update, delete similares)

Explicación Paso a Paso:

  1. Usa aggregate para listar (proyecta sin _id, ordena).
  2. next_id: Encuentra max ID +1 (emula auto-incremento).
  3. insert: Agrega ID, inserta, retorna la nueva.
  4. get/update/delete: Match por ID, opera y verifica.

Integra con Textual: Llama estos métodos desde screens para refrescar UI.

Paso 4: Pantalla Principal (MainScreen.py)

Aquí entra Textual. MainScreen es una Screen con tabla y bindings.

Código extracto:

from textual.app import ComposeResult
from textual.screen import Screen, ModalScreen
from textual.widgets import Header, Footer, DataTable, Static, Button
from textual.containers import Container, Grid
from textual.binding import Binding

from TareaFormScreen import TareaFormScreen
from TareaService import TareaService

class TareaModal(ModalScreen):
    # ... (modal para ver detalles)

class MainScreen(Screen):
    BINDINGS = [
        Binding("c", "create_task", "Create"),
        # ... (r,u,d)
    ]

    def compose(self) -> ComposeResult:
        yield Header(name="Tareas Mongo CRUD")
        self.table = DataTable(
            id="task_table",
            cursor_type="row",
            zebra_stripes=True,
        )
        self.table.add_column("ID", key="id")
        # ... (otras columnas)
        yield self.table
        yield Footer()

    def on_mount(self):
        self.refresh_table()

    def refresh_table(self):
        self.table.clear()
        tareas = TareaService.list()
        for tarea in tareas:
            self.table.add_row(
                str(tarea["id"]), tarea["titulo"], # ...
                key=str(tarea["id"])
            )
        # ... (mover cursor)

    # Acciones como action_create_task usando run_worker para async

Explicación Paso a Paso:

  1. BINDINGS: Atajos (C para create, etc.).
  2. compose: Define UI con Header, DataTable, Footer.
  3. on_mount: Hook para refrescar al cargar.
  4. refresh_table: Limpia y carga datos de DB en tabla.
  5. Acciones: Usan push_screen_wait async para modales; run_worker maneja concurrency.
  6. TareaModal: Modal reutilizable para ver/editar/borrar.

Textual maneja eventos automáticamente; tú solo defines métodos como action_*.

Paso 5: Formulario de Tareas (TareaFormScreen.py)

Otra ModalScreen para inputs.

Código extracto:

from textual.app import ComposeResult
from textual.screen import ModalScreen
from textual.widgets import Input, TextArea, Select, Button, Static
from textual.containers import Container, Grid
from datetime import date
import pendulum

from TareaSchema import TareaSchema
from pydantic import ValidationError

class TareaFormScreen(ModalScreen):
    CSS_PATH = "style.css"
    BINDINGS = [("escape", "cancel", "Cancelar")]

    def __init__(self, mode: str = "create", tarea: dict | None = None, **kwargs):
        self.mode = mode
        self.tarea = tarea or {}
        super().__init__(**kwargs)

    def compose(self) -> ComposeResult:
        # ... (inputs para titulo, desc, select status, inputs fecha)
        yield Grid(
            Input(...), TextArea(...),
            Container(Select(...), Grid(Static("FECHA"), Input(year), # ...)),
            Container(Button("Aceptar"), Button("Cancelar")),
            id="form_grid",
        )

    def on_mount(self):
        self._check_form_validity()

    # Eventos on_input_changed, etc., llaman _check_form_validity para validar en real-time

    def _check_form_validity(self):
        # Verifica campos, habilita/desabilita botón

    def on_button_pressed(self, event):
        if event.button.id == "btn_accept":
            self._submit_form()

    def _submit_form(self):
        data = { # recolecta valores }
        try:
            TareaSchema(**data)
        except ValidationError as e:
            # Notifica errores
            return
        self.dismiss({"ok": True, "data": data})

Explicación:

  1. compose: Grid para layout de form.
  2. Inicializa con valores si es «update».
  3. Validación real-time: Habilita botón solo si válido.
  4. Submit: Valida con Pydantic, dismiss con data si OK.
  5. Usa Pendulum para fechas fáciles.

Textual’s events (on_input_changed) hacen la UI reactiva.

Paso 6: Estilos y Diseño (style.css)

Textual usa CSS para estilos. Aplica a IDs/clases.

Código:

Screen {
    layout: vertical;
    background: $background;
    color: $text;
}

#task_table {
    width: 100%;
    height: 1fr;
    border: solid $accent;
    margin: 1;
}

/* ... (estilos para header, rows, modal, buttons, form) */

Explicación:

  • Variables como $accent son themes de Textual.
  • Selectores: Por ID (#task_table), clase (.datatable–row-selected).
  • Layout: Grid, align, padding.
  • Hover: Cambios en botones.

Carga en app con CSS_PATH = "style.css".

Paso 7: Punto de Entrada (app.py)

La clase App une todo.

Código:

import sys
import argparse
from textual.app import App
from MainScreen import MainScreen
from TareaService import TareaService

class TareasApp(App):
    CSS_PATH = "style.css" 
    BINDINGS = [("q", "app.quit", "Quit")]

    def on_mount(self):
        self.push_screen(MainScreen())

def show_conky():
    # ... (imprime para Conky)

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('--conky', action='store_true')
    args = parser.parse_args()
    if args.conky:
        show_conky()
    else:
        try:
            TareasApp().run()
        except Exception as e:
            print(f"\nFATAL: {e}")
            sys.exit(1)

Explicación:

  1. App: Carga CSS, bindings globales.
  2. on_mount: Empuja MainScreen.
  3. Parser: Modo normal o Conky (imprime tareas para widget).
  4. Manejo errores: Mensaje amigable si falla.

Ejecuta: python app.py

Integración Opcional: Conky (conky_config.conf)

Para Linux, integra como widget de escritorio.

Config:

conky.config = {
    alignment = 'top',
    gap_x = -580,
    gap_y = 300,
    # ... (otras settings)
}
conky.text = [[ ${execpi 2 ./app.py --conky} ]]

Ejecuta conky -c conky_config.conf para mostrar tareas en desktop.

Ejecutando y Probando la App

  1. Asegura MongoDB corre.
  2. python app.py
  3. Usa teclas: C (crear), R (leer), U (update), D (delete), Q (quit).
  4. En form, llena campos; valida automáticamente.

Prueba errores: Fecha inválida muestra notif.

Conclusión y Tips Avanzados

¡Felicidades! Has aprendido a construir una TUI completa con Textual. Este proyecto demuestra cómo integrar DB, validación y UI reactiva. Clona el repo, modifícalo: Agrega filtros, exporta a CSV o integra Rust para perf.

Tips:

  • Usa run_worker para ops async sin bloquear UI.
  • Modales para feedback.
  • CSS para themes (light/dark).
  • Explora docs de Textual: textual.textualize.io.

¿Preguntas? Comenta abajo o conéctate en LinkedIn. ¡Sigue codificando!

#Textual #Python #TUI #MongoDB #OpenSource

About Author

0 comentarios

Dejar un comentario

¿Quieres unirte a la conversación?
Siéntete libre de contribuir!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *