7 lições aprendidas ao construir um framework TUI moderno
(textualize.io)- O processo de desenvolvimento do Textual mostrou que é possível criar animações TUI suaves mesmo sobre protocolos de terminal antigos, mas é preciso tratar com cuidado a forma de atualização da tela e a sincronização da saída
- Para reduzir flicker, é importante sobrescrever sem limpar a tela, escrever na saída padrão de uma só vez e usar o protocolo Synchronized Output
- As views
dict.items()do Python e@lru_cachesão úteis para calcular diferenças de renderização e otimizar pequenas funções chamadas com alta frequência, e a implementação em C do CPython sustenta o desempenho do cache - Objetos quase imutáveis, diagramas com caracteres Unicode de caixa e
fractions.Fractionaumentam, respectivamente, a precisão em raciocínio, caching e testes; compreensão da documentação; e cálculo de layout - Emojis no terminal são difíceis de prever por causa de diferenças em versão do Unicode, largura dos caracteres e renderização de múltiplos code points; na prática, Emojis simples no nível do Unicode 9 são mais estáveis
Animações no terminal e controle de flicker
- Emuladores de terminal modernos funcionam sobre protocolos antigos, mas muitas implementações usam técnicas gráficas também empregadas em videogames
- Efeitos visuais no terminal são propensos a flicker ou tearing, mas é possível implementar animações suaves se algumas condições forem respeitadas
- A primeira forma de reduzir flicker é sobrescrever o conteúdo existente, em vez de “limpar e depois desenhar”
- Se a tela for limpa e o novo conteúdo for adicionado depois, um quadro vazio ou parcialmente desenhado pode aparecer por um instante
- A segunda forma é escrever as atualizações na saída padrão com uma única chamada de write
- Se
file.writefor chamado várias vezes, atualizações parciais podem ficar visíveis para o usuário
- Se
- A terceira forma é usar o protocolo Synchronized Output
- Ao informar ao terminal o início e o fim de um quadro, o terminal pode usar isso para aplicar atualizações sem flicker
- O Textual usa 60 fps como referência; acima disso, a diferença perceptível pode não ser grande
- Nem toda animação tem o mesmo valor
- Um efeito como uma sidebar deslizando a partir da esquerda é visualmente agradável, mas pode não trazer benefício real para a experiência do usuário
- Rolagem suave ajuda a manter a posição atual em textos longos
- Por esse motivo, o Textual pretende incluir um mecanismo para desativar animações específicas
Encontrando diferenças de renderização com DictView
keys()eitems()de umdictem Python retornam, respectivamente,KeysVieweItemsView, que oferecem uma interface semelhante à de conjuntos- O processo de layout do Textual gera um mapa de renderização, que é um mapeamento entre Widgets e posições na tela
- Na abordagem anterior, mesmo que a posição de apenas um Widget mudasse, a tela inteira era atualizada de forma desperdiçada
- Usar a diferença simétrica entre duas
ItemsViewpermite obter itens novos ou alterados- Essa operação é processada no nível de C
- Quando uma propriedade CSS muda, o Textual usa isso para encontrar as áreas alteradas da tela e aplicar atualizações otimizadas
lru_cache e otimização de pequenas funções
@lru_cache, da biblioteca padrãofunctools, é um decorador que faz cache dos valores retornados por uma função- Definir
maxsizepermite limitar uma situação em que o cache cresceria indefinidamente - O
lru_cachedo CPython usa uma implementação em C, sendo rápido tanto em acertos quanto em falhas de cache - O Textual tem pequenas funções que não são lentas, mas são chamadas com muita frequência, e muitas delas eram adequadas para cache
- Em geral, definir
maxsizeentre 1000 e 4000 foi suficiente para fazer a maioria das chamadas resultar em acertos de cache - Ao usar cache, é necessário verificar
cache_info()- Em um cache eficaz, espera-se que
hitscresça mais rapidamente quemisses
- Em um cache eficaz, espera-se que
- O método
intersection, que calcula a parte sobreposta de duas áreas retangulares, não executa muito trabalho por si só, mas é chamado milhares de vezes e se beneficia do caching
A simplicidade trazida por objetos imutáveis
- Python não tem objetos verdadeiramente imutáveis, mas é possível obter muitos benefícios com tuple,
NamedTuplee dataclasses frozen - A restrição de não poder alterar um objeto pode parecer uma limitação arbitrária, mas no código real costuma ajudar em muitos casos
- No Textual, o código que usa objetos imutáveis é mais fácil de raciocinar, além de favorecer caching e testes
- O principal motivo é que fica mais fácil escrever código sem efeitos colaterais
- Ao passar uma instância de classe para uma função, é mais difícil evitar efeitos colaterais da mesma forma
Arte Unicode e documentação
- Quando é difícil explicar um assunto técnico apenas com palavras, diagramas feitos com caracteres Unicode de caixa ajudam muito na compreensão da documentação
- As docstrings do Textual usam diagramas com caracteres de caixa para explicar um método que divide uma área em quatro subáreas
- Esses diagramas não substituem uma docstring bem escrita, mas, usados em conjunto, podem aumentar a compreensão
- A ferramenta monodraw é usada para criar os diagramas
- O Monodraw é exclusivo para macOS
- Pode haver alternativas em outras plataformas
Reduzindo erros de layout com fractions.Fraction
- O módulo
fractionsda biblioteca padrão do Python existe desde o Python 2.6 Fractioné outra forma de representar números e, depois de criado, pode ser usado no lugar de float- Números de ponto flutuante têm limitações bem conhecidas
- Por exemplo,
0.1 + 0.1 + 0.1 == 0.3éFalse
- Por exemplo,
- No Textual, erros de arredondamento de ponto flutuante se tornaram um problema em layouts que dividem a tela em várias proporções
- Em uma situação com um painel ocupando 1/3 da largura da tela e os 2/3 restantes sendo divididos novamente, podia surgir uma lacuna de um caractere
- Trocar float por
Fractionevita esse tipo de erro de arredondamento- A soma de três
Fraction(1, 10)é igual aFraction(3, 10)
- A soma de três
- Em um exemplo que divide 24 caracteres em 7 partes, a versão com float ficava com um caractere a menos, enquanto a versão com Fraction preenchia todo o comprimento
As dificuldades dos Emojis no terminal
- Suporte a Emojis é um problema recorrente desde os primeiros dias do Rich, e o Textual herdou o mesmo problema
- No terminal, caracteres normalmente ocupam uma ou duas células, e alguns caracteres têm largura zero
- Caracteres CJK ocupam o dobro do espaço de alfabetos ocidentais
- Para fazer formatações básicas, como centralização ou desenho de caixas, é preciso saber a largura que o texto impresso ocupará no terminal
- Portanto,
len(text)por si só não informa a largura ocupada no terminal
- O banco de dados Unicode tem mapeamentos entre caracteres de largura simples e dupla
- Rich e Textual consultam esse banco de dados para cada caractere exibido
- Esse trabalho não é leve, mas pode ser rápido o suficiente com engenharia e caching
- Mesmo quando Emojis estão incluídos no banco de dados Unicode, novos Emojis são adicionados a cada nova versão do Unicode, tornando o resultado difícil de prever
- Ao imprimir um Emoji novo no terminal, ele pode aparecer com uma ou duas células de largura
- Também pode não ser renderizado corretamente
- Não há um método padrão para detectar de forma confiável a versão do Unicode usada por um emulador de terminal
- Também não há variável de ambiente padrão
- Existem heurísticas que imprimem uma sequência específica e consultam a posição do cursor, mas nos testes, mesmo quando se acreditava conhecer o banco de dados Unicode, o terminal renderizava Emojis de forma imprevisível
- Emojis com múltiplos code points tornam o problema ainda mais complexo
- Por exemplo, 👨🏻🦰 é composto por 4 code points
- Um terminal pode renderizá-lo como 4 caracteres, 2 caracteres, 1 caractere, uma ou duas células, ou ainda como quatro
?
- Na prática, usar Emojis do Unicode 9 é relativamente estável em várias plataformas
- É melhor evitar Emojis novos e caracteres com múltiplos code points, mesmo que apareçam corretamente no terminal atual
Contratações na Textualize
- A Textualize está contratando para criar um framework TUI que possa substituir parte do papel do navegador
- As vagas podem ser vistas em Jobs
1 comentários
Opiniões do Hacker News
É engraçado como todo desenvolvedor de TUI acaba esbarrando em Unicode, e como lidar corretamente com caracteres internacionais e emojis vira um projeto separado, tão grande quanto ou maior que o projeto TUI original
A mesma coisa aconteceu no rivo/tview, e o pacote rivo/uniseg criado como resultado mostrou que mantenedores de outras bibliotecas de TUI também enfrentam o mesmo problema
No fim, como a largura dos caracteres não é padronizada e terminais são complexos, cada um inventa sua própria solução. O autor do texto só dá suporte ao Unicode 9, mas o Unicode atual é o 15.1, então é difícil ter certeza de que seja uma boa solução, já que em algum momento surgirão reclamações de que certos emojis ou caracteres internacionais são renderizados incorretamente
Mantenho duas bibliotecas de TUI que usam essa técnica, e o suporte a emojis tem sido quase excelente. Uma delas também usa a biblioteca uniseg
https://mitchellh.com/writing/grapheme-clusters-in-terminals
Minha maior reclamação sobre o textual é que ele tenta ser como o React. Entendo o motivo, já que é um framework popular com o qual muita gente está familiarizada, mas não acho que seja uma boa forma de criar interfaces de usuário
O design reativo em si é um caminho comprovado, e basear o desenho do sistema em uma abordagem que se sabe que funciona bem é uma boa maneira de reduzir o risco do projeto
Mas usar também uma variação de CSS parece exagero. O modelo do React, ao favorecer componentes padronizados, já rompeu em vários aspectos com os conceitos do CSS, e desenvolvedores customizando componentes com CSS parece mais um efeito colateral da evolução do React do que uma arquitetura legítima
Da última vez que usei, era preciso usar CSS, e não havia bons componentes padrão, então eu precisava criá-los. Em vez de um componente ser uma única classe Python bem encapsulada, a documentação oficial mostrava algo como criar um componente de lista e estilizá-lo com uma folha de estilos externa
Em Python, deveria haver uma forma óbvia de fazer algo — de preferência apenas uma. Ao seguir o React de perto demais, parece que estão copiando também os piores maus hábitos da comunidade JavaScript
Também parece fazer bastante tempo desde que você verificou a falta de bons componentes padrão: https://textual.textualize.io/widget_gallery/
Eu também não precisei criar componentes próprios, e já conhecia CSS. Na verdade, o que às vezes me confundia eram algumas diferenças de layout e pontos que fugiam do CSS
Imagine um componente, dentro de uma janela GUI comum, que seja uma janela de terminal. Fico me perguntando se isso existe ou se devo simplesmente usar uma visualização de texto com fonte monoespaçada
Esta TUI é bonita, mas é difícil imaginar uma situação em que eu realmente a usaria e até pagaria por ela. Talvez eu simplesmente não esteja no ambiente certo, mas, pela minha experiência, as pessoas ou ficam satisfeitas com ferramentas realmente minimalistas, ou partem direto para uma GUI para satisfazer os usuários
Por exemplo, o link do YouTube no texto mostrava uma tabela que destacava uma célula, mas não sei por que isso precisaria ser feito em uma TUI. Se for preciso mover a célula ativa destacada, no fim você também vai precisar de muitos outros recursos, e provavelmente acabará precisando de uma GUI de verdade
Dito isso, não sei muito bem o que seria uma “GUI de verdade”. Terminais têm padrões abertos amplamente usados, e esses protocolos são mais padronizados e interoperáveis do que qualquer framework de GUI, além de serem compatíveis com todos os sistemas
Se elementos como vídeo forem acrescentados a isso e algumas lacunas forem preenchidas, parece razoável competir com o navegador ou levar toda a experiência de computação para o terminal
Claro que, se o terminal adicionar vídeo, estará copiando o pipeline de renderização de uma “GUI de verdade”, mas isso melhora a experiência do terminal e, se tiver grande sucesso, também pode ser visto como trazer a simplicidade ao estilo UNIX para as GUIs
Eles trabalham com atalhos de teclado e são extremamente eficientes; toda vez que alguém tenta substituir o AS400 por uma aplicação web moderna, a produtividade cai 100 vezes
É a mesma situação da diferença entre usuários avançados de Vim/Emacs e uma GUI visualmente elegante, mas sem atalhos de teclado
Dá para fazer o usuário se conectar ao servidor via SSH e usar o app TUI como se fosse um shell, embora eu admita que é um cenário um tanto forçado
Pessoalmente, achei o Textual um pouco estranho de usar, mas foi melhor que ncurses. Ainda assim, não me entregou o resultado que eu queria. Aplicações TUI antigas no estilo mainframe pareciam extremamente eficientes
Às vezes funciona bem, mas é limitado, como quando se usa encaminhamento X em uma conexão de alta largura de banda. Em outros casos, a GUI fica completamente lenta
Uma GUI baseada em texto parece poder reunir o melhor dos dois mundos
A técnica de “o primeiro truque é sobrescrever em vez de apagar” é a mesma forma como os jogos anteriores ao DirectX eram escritos
Escrevia-se diretamente no frame buffer e, em vez de apagar tudo e redesenhar, redesenhavam-se as partes alteradas e as áreas ao redor/abaixo delas. Isso porque não havia tempo para atualizar a tela inteira a tempo
A primeira era escrever em outro buffer e, ao fim do frame, copiar o buffer inteiro de uma só vez, de modo que cada pixel mudasse apenas uma vez. Esse buffer normalmente podia estar na RAM comum, não na RAM de vídeo
A segunda era escrever em outro buffer dentro da RAM de vídeo e alterar registradores do hardware gráfico para fazer com que os pixels exibidos no monitor fossem gerados a partir desse buffer
Os trade-offs eram diferentes. Copiar o buffer inteiro era caro, enquanto alterar registradores de endereço era barato. Mas os detalhes dos registradores podiam depender do hardware, e não havia um framework de drivers gráficos de verdade
Para fazer simplesmente “buffer flipping”, ao renderizar em um buffer fora da tela ainda era preciso enviar os pixels para a RAM de vídeo, e pelo que me lembro o acesso à RAM de vídeo era mais lento que à RAM comum. Na prática, era uma arquitetura NUMA; dependendo de quantas vezes os pixels eram redesenhados, renderizar na RAM comum podia ser mais rápido no geral, mesmo incluindo a cópia final
Ainda assim, acho muito mais poderosa a capacidade de informar ao terminal o início e o fim de um frame
Eu realmente não entendo por que engenheiros de software têm tanto interesse em TUI. Gosto de bons programas de linha de comando, mas TUI não me atrai
Além disso, tem uma estética hacker. O estilo não é fortemente separado do conteúdo, e as opções de estilo ajustáveis são limitadas, então os apps TUI tendem a convergir para um único estilo. Esse estilo lembra filmes de hackers e ficção científica legal
Também não é uma área de que designers corporativos gostem, e empresas não fecham vendas para corporações ou consumidores com base em TUI. Sem essa pressão comercial, TUI é projetada por desenvolvedores para desenvolvedores
Por isso o meme das TUIs se reforça sozinho. Desenvolvedores veem e usam TUIs, percebem que normalmente são ferramentas pensadas para desenvolvedores, e então querem criar suas próprias TUIs
O suporte multiplataforma vem quase de graça, não é preciso criar uma GUI perfeita em nível de pixel — e, para começo de conversa, isso nem é possível —, e não há necessidade de ícones ou gráficos, porque não dá para exibir gráficos
Você pode continuar usando o emulador de terminal com a fonte que prefere e ainda ter uma interface parecida com GUI
Todos os apps TUI que usei até agora tinham baixa latência. Apps GUI em geral também são rápidos, mas todos os apps que foram irritantemente lentos eram apps GUI
Uma alternativa ao Monodraw é o ascii-draw
https://github.com/Nokse22/ascii-draw
Quase tudo que vi depois do TurboVision era inspirado nele, mas, na prática, faltava acabamento. Quando se usa o toolkit para valer, ele até parece simpático, mas dá a sensação de inacabado
Dito de outra forma, talvez muitos frameworks de TUI que vi fossem frameworks “opinativos”
Talvez eu seja obtuso e pouco criativo, mas desde que migrei do DOS para o Linux nos anos 90, nunca fui tão produtivo quanto quando trabalhava com B800
É possível que eu tenha demorado para entender a necessidade de double buffering, a diferença entre modo texto local direto e terminal, e até as sequências de escape. Mesmo assim, toda vez que tentava fazer algo diretamente com ncurses, a sensação clara de insatisfação quase me fazia desistir. Era completamente diferente do que dava para fazer com a simplicidade ideal do B800
Não sei se minha visão estava bloqueada como uma cidade inteira, mas todos os frameworks GUI posteriores ao message pump eram simplesmente difíceis de entender
Em especial, eu não entendia como os frameworks orientados a objetos realmente resolviam o problema de UI multithread. No Win32, eu colocava o renderizador principal em uma thread, os renderizadores auxiliares montavam o modelo ao lado e depois só atualizavam as diferenças. O código não crescia a ponto de ficar impossível de lidar
Se você vai rodar kitty, dá para fazer muito mais do que isso: https://m.youtube.com/watch?v=ft1Q-DwGWIs
https://notcurses.com/
Ambos podem exibir gráficos usando o mesmo protocolo. Se você realmente precisa de gráficos, o terminal quase nunca é a solução correta. Ainda assim, às vezes é útil para coisas pequenas, como ícones
É interessante que estejam contratando. Fico curioso sobre como pretendem ganhar dinheiro com um framework de TUI
Para referência, avaliei cerca de 12 bibliotecas TUI compiláveis, e FTXUI foi a mais fácil de usar e a mais estável
https://github.com/ArthurSonzogni/FTXUI
É uma boa ferramenta para criar dashboards interativos com suporte tanto a teclado quanto a mouse