Biblioteca de componentes: Box/Input/Markdown/SelectList
El directorio components/ de @mariozechner/pi-tui contiene componentes reutilizables, todos implementando la interfaz Component (render(width): string[] + invalidate()). Hay tres familias: layout (Box, Text, TruncatedText, Spacer), interactivos (Input, SelectList, SettingsList, Editor) y display (Markdown, Image, Loader). TUI no implementa componentes concretos, sólo agendar el árbol de componentes.
Responsabilidades
components/ hace tres cosas:
- Interfaz unificada: todos los componentes implementan
Component;render(width)devuelve un array de líneas con escapes ANSI, einvalidate()limpia la caché al cambiar el tema. Verpackages/tui/src/tui.ts:39-63. - Render cacheado:
Text,Box,MarkdowncacheancachedLines; si el texto y el ancho no cambiaron, devuelven el resultado anterior, evitando recalcular word-wrap en cada frame. Verpackages/tui/src/components/text.ts:45-58. - Protocolo Focusable:
InputyEditorimplementanFocusable(campofocused);TUIal hacersetFocuslo fija y el componente renderizaCURSOR_MARKERen la posición del cursor. Verpackages/tui/src/tui.ts:74-82.
Motivación de diseño
¿Por qué todos los componentes devuelven string[] en vez de escribir directamente? Porque TUI tiene que hacer diff: necesita cada línea final (con ANSI) para comparar con el frame anterior. El componente sólo genera líneas, no se preocupa de cómo se escriben a la terminal; así son testeables (función pura render(width) → string[]) y componibles (Container concatena las líneas de los children). Cuando Box pone color de fondo con bgFn, usa applyBackgroundToLine para intercalar el color entre los ANSI existentes en vez de envolver la línea, de modo que visibleWidth sigue calculando bien.
Archivos clave
Directorio packages/tui/src/components/:
packages/tui/src/components/box.ts:14-60— ContenedorBox,paddingX/Y+bgFnpara color de fondo a un bloque, con caché de render.packages/tui/src/components/text.ts:7-58—Textmultilineal + word-wrap + función de fondo personalizada.packages/tui/src/components/truncated-text.ts:7-50—TruncatedTexttoma sólo la primera línea y la corta por ancho, para la barra de estado.packages/tui/src/components/input.ts:18-50—Inputentrada de una línea, scroll horizontal, gestiona paste/kill-ring/undo (igual queEditor).packages/tui/src/components/select-list.ts:40-100—SelectListlista de selección arriba/abajo, con filter, doble columna (principal + descripción) y scroll indicator.packages/tui/src/components/settings-list.ts:34-83—SettingsListpanel de configuración, conInputde búsqueda embebida + submenús recursivos.packages/tui/src/components/markdown.ts:78-159—Markdownusa marked lexer + estilo inline custom para producir líneas con ANSI.packages/tui/src/components/loader.ts/packages/tui/src/components/cancellable-loader.ts— spinner, cada frame mueve sólo una línea, justo por la ruta rápida del diff.
Text.render si hay caché lo devuelve, si no recalcula word-wrap:
// packages/tui/src/components/text.ts:45-58
render(width: number): string[] {
if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {
return this.cachedLines;
}
if (!this.text || this.text.trim() === "") {
const result: string[] = [];
this.cachedText = this.text;
this.cachedWidth = width;
this.cachedLines = result;
return result;
}
// Replace tabs with 3 spacesSelectList.render usa startIndex para desplazar la ventana y dibuja sólo maxVisible elementos; el seleccionado se destaca con prefijo:
// packages/tui/src/components/select-list.ts:74-100
render(width: number): string[] {
const lines: string[] = [];
if (this.filteredItems.length === 0) {
lines.push(this.theme.noMatch(" No matching commands"));
return lines;
}
const primaryColumnWidth = this.getPrimaryColumnWidth();
const startIndex = Math.max(
0,
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible),
);
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);Markdown.render primero lexica con marked, convierte cada token a líneas con estilo, luego wrapTextWithAnsi reenvuelve por ancho de columna y al final padding/background:
// packages/tui/src/components/markdown.ts:116-159
render(width: number): string[] {
if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {
return this.cachedLines;
}
const contentWidth = Math.max(1, width - this.paddingX * 2);
// ...
const tokens = markdownParser.lexer(normalizedText);
const renderedLines: string[] = [];
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
const tokenLines = this.renderToken(token, contentWidth, nextToken?.type);
renderedLines.push(...tokenLines);
}Flujo de datos
Cómo la salida de render de los componentes entra al diff de TUI:
Límites y fallos
- Invalidación por
invalidate: al cambiar el tema,TUIllamaContainer.invalidateque limpia recursivamente la caché de los children. Las subclases deben sobrescribirinvalidatepara limpiar su propiocachedLines, si no usarán líneas con el tema viejo. Verpackages/tui/src/components/markdown.ts:110-114. visibleWidthdebe calcular bien:Boxcon padding puede superarwidthyTUIcrashea al detectarlo.Box.matchCacheusabgSamplepara detectar cambios en la salida debgFny decidir si la caché aplica. Verpackages/tui/src/components/box.ts:56-65.SelectListlista vacía fallback: sifilteredItems.length === 0dibujatheme.noMatch("No matching commands"), sin lanzar, para que el llamador siga corriendo.Markdownno envuelve líneas de imagen: las líneas conisImageLine(line)se empujan sin pasar porwrapTextWithAnsi, porque romper las secuencias kitty graphics las corrompería.Inputuna sola línea sin word-wrap:Inputes una versión simplificada deEditor; el comportamiento ante un pegado con\ndepende dehandlePaste.Inputse usa sobre todo en la caja de búsqueda deSelectListy en los submenús deSettingsList, no sirve para editar multilínea.
Resumen
components/ es el conjunto de implementaciones de la interfaz Component: los de layout (Box/Text) para composición, los interactivos (Input/SelectList/Editor) para entrada, los de display (Markdown/Loader) para render. Todos se apoyan en el bucle diff de la clase TUI para escribir cambios; Editor es el más complejo, ver componente editor; el parseo del byte stream de teclado a handleInput en parseo de teclado: protocolo kitty.