No description
  • Rust 99.2%
  • Shell 0.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
manuel d4207be5a0 Licencia AGPL-3.0-or-later
Software libre: usar, estudiar, modificar y redistribuir, con la condición de
que lo redistribuido siga con la misma licencia y su código.

Las 225 dependencias son permisivas —MIT, Apache-2.0, ISC, BSD, Zlib, MPL-2.0,
Unicode-3.0— así que ninguna choca. Lo revisé con `cargo metadata`; la única
que aparece con GPL en el nombre es r-efi, que ofrece MIT o Apache-2.0 como
alternativa y encima es de un target que no usamos.

El paquete ahora lleva /usr/share/doc/taiga-tui/copyright con la nota y el
texto completo, porque la AGPL no está en /usr/share/common-licenses como sí
lo están la GPL y la Apache.

Sube la patch y no la minor: no agrega ninguna capacidad, pero cambia lo que
se distribuye.
2026-09-18 12:47:58 -03:00
scripts Licencia AGPL-3.0-or-later 2026-09-18 12:47:58 -03:00
src Asignar historias, tareas e issues con A 2026-09-16 13:43:30 -03:00
.gitignore CLI y TUI en Rust para Taiga 2026-09-08 00:09:02 -03:00
Cargo.lock Licencia AGPL-3.0-or-later 2026-09-18 12:47:58 -03:00
Cargo.toml Licencia AGPL-3.0-or-later 2026-09-18 12:47:58 -03:00
LICENSE Licencia AGPL-3.0-or-later 2026-09-18 12:47:58 -03:00
README.md Licencia AGPL-3.0-or-later 2026-09-18 12:47:58 -03:00

taiga-tui

CLI + TUI en Rust (ratatui) para llevar tus historias de usuario y tareas de Taiga desde la terminal.

El paquete se llama taiga-tui y el binario taiga, que es lo que se tipea. (taiga-cli sería el nombre obvio, pero ya está tomado en crates.io por otra herramienta.) La carpeta del repo sigue siendo taiga-rust.

Compilar

cargo build --release
./target/release/taiga --help

Para tenerlo en el PATH:

cargo install --path .   # queda en ~/.cargo/bin/taiga

Uso

Primero se guarda una sesión. Se piden usuario y contraseña, y solo se persisten los tokens (nunca la contraseña) en ~/.config/taiga/config.json, con permisos 600:

taiga login

Después, sin argumentos abre la interfaz interactiva:

taiga

Subcomandos

Comando Qué hace
taiga / taiga tui Interfaz interactiva: kanban, historias, tareas e issues
login Guarda una sesión (-u <usuario> para no tipearlo)
logout Borra los tokens guardados
whoami Sesión activa y ruta del config
projects Proyectos donde sos miembro
stories (alias hu) Tus historias asignadas (--all incluye cerradas)
tasks Tus tareas asignadas (--all incluye cerradas)
notifications (alias notificaciones) Tus notificaciones sin leer (--all incluye las leídas)
kanban [proyecto] Kanban por columna, en texto plano

De dónde sale la URL del servidor

De mayor a menor prioridad:

# Fuente Alcance
1 --url <URL> Esa invocación. Si va junto con login, queda guardada
2 TAIGA_URL Ese entorno. También queda guardada si hacés login
3 base_url del config Lo que dejó el último login
4 DEFAULT_BASE_URL https://api.taiga.io, la instancia pública

En el TUI, la pantalla de login trae un campo Servidor editable, precargado con el valor vigente. Lo que quede ahí al ingresar es lo que se persiste.

taiga --url https://taiga.miempresa.com login   # lo fija de ahí en más
TAIGA_URL=https://otro.taiga.com taiga          # solo para este comando

Atajos del TUI

Tecla Acción
1 2 3 4 / Tab Cambiar de pestaña
↑ ↓ / k j Mover la selección
← → / h l Cambiar de columna (kanban)
g / G Ir al principio / al final
p / P Proyecto siguiente / anterior
o Alternar filtro abiertas / todas
Enter Abrir el detalle
s Cambiar el estado
A Asignar (Espacio marca, Enter aplica)
a Crear un issue (pestaña Issues)
v Convertir un issue en historia (pestaña Issues)
1…9 Abrir el adjunto n-ésimo (en el detalle)
e c t Editar descripción / comentar / editar asunto (en el detalle)
< > Mover la tarjeta de columna (kanban)
f / F Abrir / limpiar los filtros del kanban
n Ver las notificaciones (Espacio marca leída, Enter abre, a alterna sin leer / todas)
r Refrescar desde el servidor
L Cerrar sesión
? Ayuda
q / Esc Salir

El detalle

Enter abre la HU o la tarea en un popup; z lo pasa a pantalla completa y vuelve. ↑ ↓ scrollean, Espacio y PgDn de a página, g y G a los extremos. Muestra:

  • La descripción y los comentarios, renderizados como markdown en la terminal: títulos, negrita, tablas con bordes, listas, tareas [x] y código.
  • Los criterios de aceptación, que en Taiga no son un campo nativo sino un atributo personalizado del proyecto. Se muestran todos los que tengan valor.
  • Observadores y todos los asignados, no solo el principal: Taiga admite varios en assigned_users.

El listado de la API no trae la descripción, así que el detalle se pide al abrirlo. Mientras llega, la pantalla ya muestra los metadatos y avisa que está cargando.

Cambiar el estado

s abre un selector con los estados del proyecto, marcando el actual y cuáles cierran la historia. Funciona en las tres pestañas y también con el detalle abierto; usa los estados de HU o de tarea según corresponda, y los del proyecto de esa entidad, que no tiene por qué ser el que muestra el kanban.

En el kanban, < y > mueven la tarjeta seleccionada a la columna contigua. El cursor la sigue.

Taiga hace bloqueo optimista por campo: cada petición manda la version que teníamos y el servidor rechaza el cambio solo si ese mismo campo se modificó mientras tanto. Si pasa, el TUI avisa y no aplica nada; se refresca con r y se reintenta.

Las cuatro pestañas

Todas muestran el proyecto abierto y todas se filtran con f:

Kanban Las historias en columnas por estado
Historias Las mismas, como lista
Tareas Las tareas del proyecto
Issues Los issues, con tipo y severidad —teñida en Critical e Important—

Los filtros son independientes en cada pestaña, y se guardan así. No se comparten porque no se pueden: los estados de historias, tareas e issues son catálogos distintos y un id de uno no significa nada en otro. En la práctica sirve: podés tener el kanban por sprint y la lista de tareas por lo tuyo.

Cada entidad ofrece las dimensiones que soporta —las historias tienen épica, los issues tipo, severidad y prioridad, las tareas ninguna de las dos— y en las listas de personas tu usuario aparece primero, justo después de «sin asignar», que es lo que más se filtra.

Issues

Enter abre el detalle igual que una historia, y s cambia el estado.

v convierte el issue en historia de usuario. Pide confirmación porque escribe en el proyecto. Taiga copia el asunto y la descripción, y deja el issue abierto: si corresponde cerrarlo, hay que hacerlo a mano.

Asignar

A abre el selector de asignados sobre lo que esté seleccionado, o sobre la entidad del detalle si está abierto —igual que s con el estado—. Espacio marca, Enter aplica, Esc descarta. Sin nadie marcado queda sin asignar.

Las historias admiten varios asignados y las tareas y los issues uno solo, que es lo que permite cada campo en Taiga: donde va uno, marcar a alguien reemplaza al anterior en vez de sumarlo. En las historias se manda la lista completa, así que no se pierde un compañero sin querer al cambiar la asignación.

Tu usuario aparece primero, que asignarse algo es lo que más se hace; el resto va por orden alfabético. Si alguien cambió la asignación mientras tanto, Taiga rechaza la escritura y el aviso dice que refresques con r.

Crear un issue

a en la pestaña Issues abre un modal que pide el título y nada más. Con Enter se crea en el proyecto abierto y queda seleccionado arriba de todo, con su detalle abierto: ahí se le carga la descripción con e, se comenta con c y se le cambia el estado con s. Un título vacío no crea nada.

El resto lo completa el servidor: estado, tipo, severidad y prioridad salen de los valores por defecto del proyecto, igual que en el formulario de la web — son los únicos campos que Taiga exige además del proyecto y el asunto.

Editar con tu editor

Desde el detalle, e abre la descripción en tu $EDITOR, c un buffer vacío para comentar y t el asunto.

El editor no toma la terminal entera: corre adentro de una ventana flotante al 80%, con un encabezado que dice sobre qué estás escribiendo —«Comentario en Historia #1045»— y el asunto al lado como contexto. Por debajo hay un pseudo-terminal propio, así que el editor cree que esa ventana es toda su pantalla.

Guardás y salís como siempre (:wq, ^O+^X) y el cambio se aplica. Mientras la ventana está abierta todas las teclas son del editor, incluido Esc y Ctrl-C; la única excepción es Ctrl+Alt+Q, que mata el editor y descarta, por si algo quedó colgado.

Si el pseudo-terminal no se puede abrir, se cae al modo de siempre: suspender el TUI y darle la terminal completa al editor.

Se respeta $VISUAL sobre $EDITOR, y si no hay ninguno cae a vi. El comando puede llevar argumentos: EDITOR="code -w" funciona. El archivo temporal es .md, así que el editor enciende el resaltado de markdown.

Guardar sin cambios cancela. Un comentario o un asunto vacíos también cancelan; una descripción vacía sí se guarda, porque vaciarla es una edición legítima.

Si mientras editabas alguien tocó lo mismo, el guardado falla por el bloqueo optimista y tu texto no se pierde: queda en /tmp/taiga/borrador-<entidad>-<ref>-<campo>.md y el mensaje dice la ruta. Refrescás con r, volvés a editar y pegás.

Adjuntos

El detalle lista los adjuntos numerados. La tecla del número lo descarga en /tmp/taiga y lo abre con el visor del sistema (xdg-open, o open en macOS). Si el visor falla, el mensaje dice dónde quedó el archivo para abrirlo a mano.

Notificaciones

n abre la campanita: lo mismo que notifica la web —te asignaron algo, te mencionaron, comentaron una historia que seguís, te sumaron como observador o a un proyecto—, con la fecha, quién lo hizo y de qué proyecto viene. Son del usuario, no del proyecto abierto, así que la lista cruza todos.

Arranca mostrando solo las sin leer, que es lo que se va a mirar; a pasa al historial completo y r recarga. La cabecera muestra un contador de pendientes, que se pide al arrancar y al refrescar con r.

Espacio marca la seleccionada como leída. La fila se queda donde está —se apaga y baja el contador— en vez de desaparecer: sacarla correría el cursor abajo de los dedos. En la próxima carga ya no viene.

Enter abre lo que la notificación referencia en el mismo detalle de siempre, con descripción, comentarios y adjuntos. La entidad se pide por id, así que funciona aunque sea de otro proyecto y no esté en ninguna lista cargada; el detalle queda encima del panel y Esc vuelve. Las que no apuntan a nada —te sumaron a un proyecto— y las épicas, que no tienen vista, avisan y no hacen nada.

Cada dos minutos un hilo de fondo trae las pendientes y actualiza el contador solo. No aparece ningún cartel: la única señal es que el número cambie, y un aviso en el pie cuando llegó algo nuevo. El sondeo corre aparte del bucle de la interfaz a propósito —en el hilo principal un request sería un freeze sin explicación, peor que el cartel de cargando— y se saltea la tanda si hay algo en curso, para no pisar datos abajo de una escritura.

TAIGA_POLL_SECONDS cambia el intervalo; 0 apaga el sondeo:

TAIGA_POLL_SECONDS=600 taiga   # cada diez minutos
TAIGA_POLL_SECONDS=0 taiga     # sin sondeo

El mismo listado en texto plano:

taiga notifications          # solo las sin leer
taiga notifications --all    # el historial completo

Los filtros del kanban

El kanban muestra todas las historias del proyecto, no solo las propias: es un tablero de equipo. Para verlo como en el kanban web, f abre un panel con las mismas dimensiones —asignado a, estado, etiquetas, creada por, rol y épica— cada opción con su cantidad.

← → cambian de dimensión, Espacio marca, i alterna entre incluir y excluir, c limpia y Enter aplica; Esc descarta los cambios. El filtrado lo resuelve el servidor, con la misma semántica que la web. Con filtros activos la cabecera muestra un contador y el pie los detalla.

Cada proyecto recuerda sus propios filtros: los ids de estados y personas no valen en otro, así que al cambiar de proyecto se cargan los suyos.

Qué se recuerda entre sesiones

El proyecto abierto y los filtros de cada pestaña de cada proyecto se guardan en el config, así que la app vuelve a abrirse como la dejaste. Si el proyecto guardado ya no está, arranca en el primero sin protestar.

TAIGA_CONFIG apunta el config a otro archivo. Sirve para tener varias cuentas —una del trabajo y otra de api.taiga.io, por ejemplo— y es lo que usan los tests para no pisar el config real:

TAIGA_CONFIG=~/.config/taiga/personal.json taiga

Distribuir

scripts/empaquetar-deb.sh arma el .deb:

./scripts/empaquetar-deb.sh

Al terminar imprime la ruta del paquete y el comando para instalarlo. Se instala y se actualiza igual, sin desinstalar nada antes:

sudo apt install ./target/debian/taiga-tui_0.10.0_amd64.deb

Conviene apt install ./archivo.deb y no dpkg -i: apt resuelve las dependencias y dpkg no. La sesión guardada en ~/.config/taiga no la toca el paquete, así que sobrevive a la actualización.

No usa cargo-deb a propósito: la versión actual necesita rustc 1.88 y este proyecto está clavado al 1.85 de Debian. Un .deb es un ar con tres archivos adentro, así que el script lo arma con dpkg-deb, que ya viene instalado.

Las dependencias no van escritas a mano: salen de los símbolos versionados del binario, que es lo mismo que mira dpkg-shlibdeps. Hoy dan:

Depends: libc6 (>= 2.39), libgcc-s1 (>= 4.2)

Eso importa más de lo que parece. El binario compilado en Debian 13 no corre en Debian 12 ni en Ubuntu 22.04, que traen glibc más vieja. Con esa línea, apt se niega a instalarlo y lo dice; sin ella, se instala y falla recién al ejecutarse con un mensaje que no ayuda a nadie.

Distro glibc ¿Anda?
Debian 13 trixie 2.41 sí
Ubuntu 24.04 2.39 justo
Debian 12 bookworm 2.36 no
Ubuntu 22.04 2.35 no

Para que ande en cualquier lado haría falta un build estático con musl, que pide un toolchain que el rustc de apt no trae.

El binario solo se enlaza contra libc, libm y libgcc_s: usamos rustls-tls en vez de native-tls, así que no arrastra OpenSSL.

Estructura

scripts/
  empaquetar-deb.sh  arma el paquete Debian
src/
  main.rs        CLI (clap) y salida en texto plano
  config.rs      sesión persistida en ~/.config/taiga
  markdown.rs    markdown a líneas de ratatui
  editor.rs      archivo temporal y elección del $EDITOR
  pty.rs         terminal embebida y traducción de teclas
  poller.rs      sondeo de notificaciones en segundo plano
  api/
    mod.rs       cliente HTTP: login, refresh, endpoints
    models.rs    structs de la API de Taiga y estado de los filtros
  ui/
    mod.rs       terminal, bucle de eventos y teclas
    app.rs       estado y navegación
    draw.rs      renderizado de las pantallas
    tests.rs     tests de render con TestBackend

Dos detalles de la API de Taiga que vale recordar:

  • Las listas paginan de a 30. El cliente manda x-disable-pagination: True para traer todo de una.
  • En los *_extra_info, las entidades traen name y las personas full_name_display / username.
  • La description sale solo del endpoint de detalle, nunca del listado.
  • Los comentarios viven en /history/{tipo}/{id}, mezclados con los cambios de campos: son las entradas con comment no vacío.
  • Los criterios de aceptación son un atributo personalizado (/userstory-custom-attributes), con los valores en /userstories/custom-attributes-values/{id}, que responde 404 si la entidad nunca tuvo ninguno.
  • Los filtros aceptan varios valores separados por coma; repetir el parámetro no funciona, se queda con el último. null es el literal para "sin asignar", y cada filtro tiene su variante exclude_.
  • El PATCH exige version (sin ella responde 400) pero el control es por campo: una versión vieja pasa si el atributo que tocamos no cambió, y da 400 con The version doesn't match with the current one si sí cambió.
  • En la nube de Taiga la API vive en api.taiga.io. tree.taiga.io sirve la web y responde 405 a las peticiones de la API.
  • El campo tasks de una historia viene vacío en el listado aunque tenga tareas; hay que pedirlas con GET /tasks?user_story={id}.
  • Los adjuntos no vienen en el listado: van por GET /{tipo}/attachments?object_id={id}&project={p}.
  • Los issues traen type, priority y severity como ids; los nombres salen de /issue-types, /priorities y /severities.
  • Promover es POST /issues/{id}/promote_to_user_story con project_id (no project), y devuelve las referencias de las historias creadas. Copia asunto y descripción, y no cierra el issue.
  • El editor embebido usa portable-pty para el pseudo-terminal y tui-term para dibujar su pantalla; las teclas de crossterm se traducen a secuencias de terminal en src/pty.rs, respetando el modo de cursor que pida el editor.
  • Un comentario se agrega con el mismo PATCH que el resto de los campos ({"comment": "...", "version": N}), no con un endpoint aparte.
  • El ref puede venir null: hay historias, tareas e issues reales sin referencia asignada. Se muestran como #s/r.

Licencia

AGPL-3.0-or-later. Software libre: podés usarlo, estudiarlo, modificarlo y redistribuirlo, y cualquier versión modificada que distribuyas tiene que venir con su código bajo la misma licencia.

Las 225 dependencias son permisivas —MIT, Apache-2.0, ISC, BSD, Zlib, MPL-2.0 y similares—, así que ninguna choca con la AGPL.

La cláusula de red de la AGPL (§13) obliga a ofrecer el código a quien use el programa a través de una red. Acá casi no se activa: esto es un cliente de terminal, no un servicio. Sí aplicaría si alguien lo expusiera por SSH o web para que terceros lo usen.

Cuando algo falla

No hay archivo de log. Los errores salen en el pie de la pantalla, y si lo que falla es decodificar una respuesta el mensaje incluye el trozo de JSON donde se cortó y guarda el cuerpo completo en /tmp/taiga/error-<endpoint>.json para poder mirarlo con calma:

✖ no se pudo decodificar la respuesta de issues: invalid type: null, expected i64
  cerca de: …"subject":"[REQ] Creación de adaptador","ref":null,…
  respuesta completa en /tmp/taiga/error-issues.json

Tests

cargo test

Los tests dibujan cada pantalla en el TestBackend de ratatui y verifican el contenido, más la lógica de navegación y filtros. No tocan la red.

Nota sobre el toolchain

Con rustc 1.85 (el de Debian trixie) hay dependencias transitivas que piden un MSRV mayor; el Cargo.lock del repo tiene las versiones pinneadas que compilan. Con un toolchain más nuevo vía rustup se puede correr cargo update sin restricciones.