7 cosas que aprendí al crear un framework TUI moderno
(textualize.io)- Durante el desarrollo de Textual se comprobó que es posible crear animaciones TUI fluidas incluso sobre protocolos de terminal antiguos, pero hay que manejar con cuidado la forma de actualizar la pantalla y sincronizar la salida
- Para reducir el parpadeo, es importante sobrescribir en vez de limpiar la pantalla, escribir de una sola vez en la salida estándar y aprovechar el protocolo Synchronized Output
- La vista
dict.items()de Python y@lru_cacheson útiles para calcular diferencias de renderizado y optimizar funciones pequeñas de alta frecuencia, y la implementación en C de CPython respalda el rendimiento del caché - Los objetos casi inmutables, los diagramas con caracteres de caja Unicode y
fractions.Fractionmejoran respectivamente la precisión en razonamiento, caché y pruebas, comprensión de la documentación y cálculo de layouts - Los Emoji en terminal son difíciles de predecir por las diferencias en la versión de Unicode, el ancho de los caracteres y el renderizado de múltiples code points, así que en la práctica los Emoji simples del nivel de Unicode 9 son más estables
Animaciones en terminal y control del parpadeo
- Los emuladores de terminal modernos funcionan sobre protocolos antiguos, pero muchas implementaciones usan técnicas gráficas que también se emplean en videojuegos
- Los efectos visuales en terminal tienden a sufrir parpadeo o tearing, pero si se cumplen ciertas condiciones se pueden lograr animaciones fluidas
- La primera forma de reducir el parpadeo es no hacer “clear y luego draw”, sino sobrescribir el contenido existente
- Si se borra la pantalla y luego se agrega el contenido nuevo, pueden verse brevemente frames vacíos o parcialmente dibujados
- La segunda forma es escribir la actualización en la salida estándar con una sola llamada de escritura
- Si se llama varias veces a
file.write, el usuario podría ver actualizaciones parciales
- Si se llama varias veces a
- La tercera forma es usar el protocolo Synchronized Output
- Si se le indica a la terminal el inicio y el fin de un frame, la terminal puede usar esa información para hacer actualizaciones sin parpadeo
- Textual toma 60fps como referencia, y por encima de eso la diferencia visible puede no ser muy grande
- No todas las animaciones tienen el mismo valor
- Efectos como una barra lateral que se desliza desde la izquierda se ven bien, pero quizá no aporten una mejora real a la experiencia de usuario
- El desplazamiento suave sí ayuda a mantener la ubicación actual dentro de textos largos
- Por eso, Textual planea incluir un mecanismo para desactivar ciertas animaciones
Encontrar diferencias de renderizado con DictView
keys()eitems()de undictde Python devuelven respectivamenteKeysVieweItemsView, y ofrecen una interfaz parecida a la de los conjuntos- En el proceso de layout de Textual se genera un mapa de renderizado, que es una relación entre Widget y posición en pantalla
- En el enfoque anterior, aunque solo cambiara la posición de un Widget, se refrescaba toda la pantalla de forma innecesaria
- Si se usa la diferencia simétrica entre dos
ItemsView, se pueden obtener los elementos nuevos o modificados- Esta operación se procesa a nivel C
- Textual la usa para encontrar las regiones de pantalla modificadas cuando cambian propiedades CSS y así hacer actualizaciones optimizadas
lru_cache y optimización de funciones pequeñas
@lru_cachede la biblioteca estándarfunctoolses un decorador para almacenar en caché los valores de retorno de una función- Si se define
maxsize, se puede limitar el crecimiento indefinido del caché - La implementación de
lru_cacheen CPython usa C, por lo que es rápida tanto en aciertos como en fallos de caché - En Textual hay funciones pequeñas que no son lentas por sí mismas, pero se llaman con mucha frecuencia, y muchas resultaron buenas candidatas para caché
- En general, establecer
maxsizeentre 1000 y 4000 fue suficiente para que la mayoría de las llamadas terminaran en aciertos de caché - Al usar caché, conviene revisar
cache_info()- Si el caché es efectivo, lo esperable es que
hitscrezca más rápido quemisses
- Si el caché es efectivo, lo esperable es que
- El método
intersection, que calcula la parte superpuesta entre dos áreas rectangulares, no hace un trabajo grande por llamada, pero al ejecutarse miles de veces se beneficia del caché
La simplicidad que aportan los objetos inmutables
- En Python no existen objetos verdaderamente inmutables en sentido estricto, pero con tuple,
NamedTupley frozen dataclass se pueden obtener muchas ventajas - La restricción de no poder modificar un objeto puede parecer una limitación arbitraria, pero en el código real suele ayudar bastante
- En Textual, el código que usa objetos inmutables es más fácil de razonar, y además favorece el caché y las pruebas
- La razón principal es que facilita escribir código sin efectos secundarios
- Cuando se pasa una instancia de clase a una función, es más difícil evitar efectos secundarios de la misma forma
Arte Unicode y documentación
- Cuando un tema técnico es difícil de explicar solo con palabras, los diagramas hechos con caracteres de caja Unicode ayudan mucho a entender la documentación
- En los docstrings de Textual se usan diagramas con caracteres de caja para explicar un método que divide un área en cuatro subáreas
- Estos diagramas no reemplazan un docstring bien escrito, pero usados en conjunto pueden mejorar la comprensión
- Como herramienta para crear diagramas se usa monodraw
- Monodraw es exclusivo de macOS
- Puede haber alternativas en otras plataformas
Reducir errores de layout con fractions.Fraction
- El módulo
fractionsde la biblioteca estándar de Python existe desde Python 2.6 Fractiones otra forma de representar números y, una vez creado, puede usarse en lugar de float- Los números de punto flotante tienen limitaciones bien conocidas
- Por ejemplo,
0.1 + 0.1 + 0.1 == 0.3daFalse
- Por ejemplo,
- En Textual, los errores de redondeo de punto flotante daban problemas en layouts que dividen la pantalla en varias proporciones
- En una situación donde un panel ocupa 1/3 del ancho y luego se vuelve a dividir el 2/3 restante, podía aparecer un espacio vacío de un carácter
- Si se reemplaza float por
Fraction, se evita este tipo de error de redondeo- La suma de tres
Fraction(1, 10)es igual aFraction(3, 10)
- La suma de tres
- En el ejemplo de dividir 24 caracteres en 7 partes, la versión con float se quedaba corta por un carácter, pero la versión con Fraction completaba todo el largo
Las dificultades de los Emoji en terminal
- El soporte de Emoji ha sido un problema constante desde los inicios de Rich, y Textual heredó ese mismo problema
- En terminal, los caracteres normalmente ocupan una celda o dos, y algunos tienen ancho 0
- Los caracteres CJK ocupan el doble de espacio que el alfabeto occidental
- Para hacer formato básico, como centrar texto o dibujar cajas, hay que conocer el ancho real del texto en la terminal
- Por eso,
len(text)no basta para saber cuánto espacio ocupará en terminal
- La base de datos Unicode incluye un mapeo de caracteres de ancho simple y doble
- Rich y Textual consultan esa base de datos para cada carácter que imprimen
- No es una tarea liviana, pero con ingeniería y caché se puede volver lo bastante rápida
- Aunque los Emoji estén incluidos en la base de datos Unicode, cada nueva versión de Unicode agrega nuevos Emoji y eso vuelve impredecibles los resultados
- Un Emoji nuevo puede mostrarse en terminal como de una celda o de dos
- También puede no renderizarse correctamente
- No existe un método estándar y confiable para detectar qué versión de Unicode usa un emulador de terminal
- Tampoco hay una variable de entorno estándar
- Existen heurísticas como imprimir ciertas secuencias y preguntar la posición del cursor, pero en las pruebas, incluso asumiendo conocer la base de datos Unicode, la terminal renderizaba los Emoji de forma impredecible
- Los Emoji compuestos por múltiples code points complican aún más el problema
- Por ejemplo, 👨🏻🦰 está formado por 4 code points
- Algunas terminales pueden renderizarlo como 4 caracteres, 2 caracteres, 1 carácter, una celda o dos, o incluso como 4
?
- En la práctica, usar los Emoji de Unicode 9 resulta relativamente estable en varias plataformas
- Aunque los Emoji nuevos y los compuestos por múltiples code points se vean bien en la terminal actual, conviene evitarlos
Textualize está contratando
- Textualize está contratando para crear un framework TUI capaz de reemplazar parte del rol del navegador
- La información de vacantes está en Jobs
1 comentarios
Opiniones de Hacker News
Es interesante que todos los desarrolladores de TUI terminan chocando con Unicode, y que manejar correctamente caracteres internacionales y emojis se vuelve un proyecto aparte tan grande como el proyecto TUI original, o incluso más
En rivo/tview pasó lo mismo, y el paquete rivo/uniseg que se creó como resultado mostró que los mantenedores de otras bibliotecas TUI también enfrentan el mismo problema
Al final, como el ancho de los caracteres no está estandarizado y las terminales son complejas, cada quien inventa su propia solución. El autor del artículo solo soporta Unicode 9, pero Unicode ya va en 15.1, así que es difícil estar seguro de que sea una buena solución, porque algún día aparecerán quejas de que cierto emoji o carácter internacional se renderiza mal
Mantengo dos bibliotecas TUI que usan esta técnica, y el soporte de emojis ha sido casi excelente. Una de ellas también usa la biblioteca uniseg
https://mitchellh.com/writing/grapheme-clusters-in-terminals
Mi mayor queja sobre textual es que intenta ser como React. Entiendo por qué, ya que es un framework popular con el que mucha gente está familiarizada, pero no lo considero una buena forma de construir interfaces de usuario
El diseño reactivo en sí es un camino probado, y apoyar el diseño del sistema en enfoques que se sabe que funcionan bien es una buena forma de reducir el riesgo del proyecto
Pero sumar a eso una variante de CSS me parece excesivo. El modelo de React, al favorecer componentes estandarizados, ya rompió en muchos aspectos los conceptos de CSS, y que los desarrolladores personalicen componentes con CSS parece más un efecto colateral de la evolución de React que una arquitectura legítima
La última vez que lo usé había que usar CSS, y no había buenos componentes estándar, así que tuve que crearlos yo mismo. En vez de que un componente fuera una clase de Python bien encapsulada, la documentación oficial proponía crear un componente de lista y decorarlo con una hoja de estilos externa
En Python debería haber una forma obvia de hacer algo, y preferiblemente solo una. Al seguir demasiado de cerca a React, parece que también adopta las peores malas prácticas de la comunidad JavaScript
Parece que hace bastante que no revisas lo de que no hay buenos componentes estándar: https://textual.textualize.io/widget_gallery/
Tampoco tuve que crear mis propios componentes, y CSS ya lo conocía. Más bien, algunas diferencias de layout y las partes que se apartan de CSS fueron lo que a veces me resultó confuso
Imagínalo como un componente dentro de una ventana GUI normal que es una ventana de terminal. Me pregunto si existe algo así, o si simplemente debería usar una vista de texto con fuente monoespaciada
Esta TUI es bonita, pero me cuesta imaginar una situación en la que realmente la use y hasta pague por ella. Puede que no esté en el entorno adecuado, pero según mi experiencia, la gente se conforma con herramientas realmente minimalistas o directamente intenta satisfacer a los usuarios con una GUI.
Por ejemplo, el enlace de YouTube del artículo mostraba una tabla que resaltaba celdas, pero no entiendo por qué eso tendría que hacerse como TUI. Si hay que mover la celda activa resaltada, al final se necesitarían muchas otras funciones y probablemente terminaría haciendo falta una GUI en serio.
Dicho eso, no tengo muy claro qué sería una “GUI en serio”. En la terminal hay estándares abiertos ampliamente usados, y esos protocolos son más estándar e interoperables que cualquier framework de GUI, además de estar soportados en todos los sistemas.
Si a esto se le agregan elementos como video y se cubren algunos huecos, también parece razonable competir con el navegador o llevar toda la experiencia de cómputo a la terminal.
Claro que, si la terminal agrega video, en cierto sentido estaría copiando el pipeline de renderizado de una “GUI en serio”, pero sería una mejora de la experiencia de terminal y, si tiene mucho éxito, también podría verse como llevar la simplicidad estilo UNIX a las GUI.
Trabajan de forma muy eficiente con atajos de teclado, y cada vez que alguien intenta reemplazar AS400 por una app web moderna, la productividad cae 100 veces.
Es la misma situación que la diferencia entre un experto en Vim/Emacs y una GUI que se ve pulida pero no tiene atajos de teclado.
Se podría hacer que el usuario entre al servidor por SSH y use la app TUI como si fuera un shell, aunque admito que es un escenario algo forzado.
Personalmente, Textual me resultó un poco raro de usar, aunque fue mejor que ncurses. Aun así, no me dio el resultado que quería. Las viejas aplicaciones de TUI estilo mainframe se sentían extremadamente eficientes.
A veces funciona bien, pero es algo limitado, como cuando se usa X forwarding en una conexión con mucho ancho de banda. En otros casos, la GUI se vuelve completamente torpe.
Una GUI basada en texto parece poder quedarse con lo mejor de ambos mundos.
La idea de “el primer truco es no borrar, sino sobrescribir” es la misma forma en que se escribían los juegos anteriores a DirectX.
Se escribía directamente en el framebuffer y, en vez de borrar todo y volver a dibujar, se redibujaban las partes cambiadas y lo que estaba alrededor o debajo. Era porque no había tiempo para actualizar toda la pantalla a tiempo.
La primera era escribir en otro búfer y, al final del frame, copiar todo el búfer de una sola vez para que cada píxel cambiara solo una vez. Ese búfer normalmente podía estar en RAM común, no en RAM de video.
La segunda era escribir en otro búfer dentro de la RAM de video y cambiar registros del hardware gráfico para que los píxeles mostrados en el monitor se generaran desde ese búfer.
Los compromisos eran distintos. Copiar todo el búfer era caro, y cambiar registros de dirección era barato. Pero los detalles de los registros podían depender del hardware, y tampoco había un framework de drivers gráficos en condiciones.
Para hacer simplemente “buffer flipping”, incluso al renderizar en un búfer fuera de pantalla había que enviar los píxeles a la RAM de video, y según recuerdo el acceso a la RAM de video era más lento que a la RAM normal. En la práctica era una arquitectura NUMA, así que, dependiendo de cuántas veces se repintaran los píxeles, renderizar en RAM normal podía ser más rápido en conjunto incluso contando la copia final.
Aun así, creo que la capacidad de indicarle a la terminal el inicio y el fin de un frame es mucho más potente.
De verdad no entiendo por qué a los ingenieros de software les interesan tanto las TUI. Me gustan los buenos programas de línea de comandos, pero las TUI no me atraen.
Además tiene una estética hacker. El estilo no está fuertemente separado del contenido, y las opciones de estilo ajustables son limitadas, así que las apps TUI suelen converger hacia un solo estilo. Ese estilo recuerda a películas de hackers y a ciencia ficción cool.
Tampoco es un terreno que les encante a los diseñadores corporativos, y las empresas no cierran ventas a compañías o consumidores usando una TUI como argumento. Al no existir esa presión comercial, las TUI las diseñan desarrolladores para desarrolladores.
Por eso el meme de las TUI se refuerza a sí mismo. Los desarrolladores ven y usan TUI, sienten que normalmente son herramientas pensadas para desarrolladores, y eso les da ganas de crear su propia TUI.
Lo multiplataforma sale casi gratis, no hace falta crear una GUI perfecta al píxel —y de entrada es imposible—, y tampoco se necesitan íconos ni gráficos, porque no se pueden mostrar gráficos.
Puedes seguir usando tu emulador de terminal con la fuente que prefieras y, al mismo tiempo, tener una interfaz parecida a una GUI.
Todas las apps TUI que he probado hasta ahora tenían baja latencia. Las apps GUI por lo general también son rápidas, pero todas las apps que me resultaron frustrantemente lentas eran apps GUI.
Como alternativa a Monodraw existe ascii-draw.
https://github.com/Nokse22/ascii-draw
Casi todo lo que vi después de TurboVision estaba inspirado en él, pero en la práctica le faltaba madurez. Cuando probabas bien el toolkit, era simpático, pero se sentía incompleto
Dicho de otra forma, podría decirse que muchos frameworks TUI que vi eran frameworks “muy opinados”
Quizá sea que soy torpe y poco creativo, pero desde que pasé de DOS a Linux en los 90, nunca volví a ser tan productivo como cuando trabajaba con B800
Es posible que me haya tomado mucho tiempo entender la necesidad del doble buffering, la diferencia entre el modo texto directo local y una terminal, e incluso las secuencias de escape. Aun así, cada vez que intentaba hacer algo directamente con ncurses, casi terminaba abandonándolo por una sensación clara de insatisfacción. Era algo completamente distinto de lo que se podía hacer con el ideal simple de B800
No sé si mi visión estaba tapada como una ciudad entera, pero los frameworks GUI posteriores al message pump siempre me resultaron imposibles de entender
En particular, nunca entendí cómo los frameworks orientados a objetos resolvían realmente los problemas de UI multihilo. En Win32 ponía el renderer principal en un thread, mientras renderers secundarios construían el modelo al costado y luego actualizaban solo las diferencias. El código no crecía hasta volverse inmanejable
Si vas a ejecutar kitty, puedes hacer mucho más que eso: https://m.youtube.com/watch?v=ft1Q-DwGWIs
https://notcurses.com/
Ambos pueden mostrar gráficos usando el mismo protocolo. Si realmente necesitas gráficos, una terminal casi nunca es la solución correcta. Aunque a veces sirve para cosas pequeñas, como íconos
Es interesante que estén contratando. Me pregunto cómo piensan ganar dinero con un framework TUI
Como referencia, evalué unas 12 bibliotecas TUI compilables, y FTXUI fue la más fácil de usar y la más estable
https://github.com/ArthurSonzogni/FTXUI
Es una buena herramienta para crear dashboards interactivos con soporte tanto para teclado como para mouse