7 leçons apprises en construisant un framework TUI moderne
(textualize.io)- Le développement de Textual montre qu’il est possible de créer des animations TUI fluides même sur d’anciens protocoles de terminal, à condition de gérer avec soin le rafraîchissement de l’écran et la synchronisation de la sortie
- Pour réduire le scintillement, il est important d’écraser le contenu à l’écran sans l’effacer, d’écrire sur la sortie standard en une seule fois et d’utiliser le protocole Synchronized Output
- La vue
dict.items()de Python et@lru_cachesont utiles pour calculer les différences de rendu et optimiser de petites fonctions très fréquemment appelées, tandis que l’implémentation C de CPython soutient les performances du cache - Des objets quasi immuables, des schémas en caractères Unicode et
fractions.Fractionaméliorent respectivement la précision du raisonnement, du cache et des tests, la compréhension de la documentation, et les calculs de mise en page - Les Emoji dans le terminal sont difficiles à prévoir à cause des versions d’Unicode, de la largeur des caractères et des différences de rendu pour les séquences à plusieurs code points ; en pratique, les Emoji simples du niveau Unicode 9 sont plus stables
Animation dans le terminal et contrôle du scintillement
- Les émulateurs de terminal modernes fonctionnent sur d’anciens protocoles, mais beaucoup d’implémentations utilisent des techniques graphiques également employées dans le jeu vidéo
- Les effets visuels dans le terminal provoquent facilement du scintillement ou du tearing, mais il est possible d’obtenir des animations fluides en respectant quelques principes
- La première méthode pour réduire le scintillement consiste non pas à faire « clear puis draw », mais à écraser le contenu existant
- Si l’on efface l’écran avant d’ajouter le nouveau contenu, un frame vide ou partiellement dessiné peut brièvement apparaître
- La deuxième méthode consiste à écrire les mises à jour sur la sortie standard en un seul write
- Si l’on appelle
file.writeplusieurs fois, des mises à jour partielles peuvent devenir visibles pour l’utilisateur
- Si l’on appelle
- La troisième méthode consiste à utiliser le protocole Synchronized Output
- En indiquant au terminal le début et la fin d’un frame, celui-ci peut s’en servir pour effectuer une mise à jour sans scintillement
- Textual vise 60 fps, au-delà desquels la différence perceptible n’est pas forcément importante
- Toutes les animations n’ont pas la même valeur
- Un effet comme une barre latérale qui glisse depuis la gauche est agréable visuellement, mais n’apporte pas forcément de bénéfice réel à l’expérience utilisateur
- Un défilement fluide aide à conserver sa position dans de longs textes
- Pour cette raison, Textual prévoit un mécanisme permettant de désactiver certaines animations
Trouver les différences de rendu avec DictView
- Dans Python,
keys()etitems()d’undictrenvoient respectivementKeysViewetItemsView, avec une interface proche des ensembles - Le processus de mise en page de Textual génère une carte de rendu, c’est-à-dire une correspondance entre les widgets et leur position à l’écran
- Dans l’approche précédente, si la position d’un seul widget changeait, l’ensemble de l’écran était rafraîchi inutilement
- En utilisant la différence symétrique entre deux
ItemsView, on peut obtenir les éléments nouveaux ou modifiés- Cette opération est traitée au niveau C
- Textual s’en sert pour repérer les zones de l’écran modifiées quand des propriétés CSS changent, et effectuer des mises à jour optimisées
lru_cache et optimisation des petites fonctions
- Le décorateur
@lru_cachede la bibliothèque standardfunctoolspermet de mettre en cache la valeur de retour d’une fonction - En définissant
maxsize, on évite que le cache ne grossisse indéfiniment - Dans CPython,
lru_cacheest implémenté en C, ce qui le rend rapide aussi bien sur les hits que sur les misses - Textual contient de petites fonctions qui ne sont pas lentes en soi, mais sont appelées très fréquemment, et beaucoup se prêtaient bien à la mise en cache
- En pratique, fixer
maxsizeentre 1000 et 4000 suffisait généralement à faire basculer la majorité des appels en cache hit - Lorsqu’on utilise un cache, il faut vérifier
cache_info()- Si le cache est efficace, on s’attend à voir
hitsaugmenter plus vite quemisses
- Si le cache est efficace, on s’attend à voir
- La méthode
intersection, qui calcule la zone de recouvrement entre deux rectangles, n’est pas très coûteuse en soi, mais comme elle est appelée des milliers de fois, elle bénéficie de la mise en cache
La simplicité apportée par les objets immuables
- Python ne possède pas d’objets véritablement immuables au sens strict, mais les tuples,
NamedTupleet les dataclasses figées offrent déjà de nombreux avantages - La contrainte de ne pas pouvoir modifier un objet peut sembler arbitraire, mais elle s’avère souvent utile dans du vrai code
- Dans Textual, le code qui utilise des objets immuables est plus facile à raisonner, et se prête mieux au cache comme aux tests
- La raison essentielle est qu’il devient plus simple d’écrire du code sans effets de bord
- À l’inverse, lorsqu’on passe une instance de classe à une fonction, il est plus difficile d’éviter les effets de bord de la même manière
Art Unicode et documentation
- Quand un sujet technique est difficile à expliquer uniquement avec des mots, des schémas réalisés avec des caractères de boîte Unicode peuvent grandement aider à la compréhension
- Dans les docstrings de Textual, un schéma en caractères de boîte est utilisé pour expliquer une méthode qui divise une zone en quatre sous-zones
- Ces schémas ne remplacent pas une docstring bien rédigée, mais les deux combinés améliorent la compréhension
- L’outil utilisé pour créer ces schémas est monodraw
- Monodraw est réservé à macOS
- Il existe peut-être des alternatives sur d’autres plateformes
Réduire les erreurs de mise en page avec fractions.Fraction
- Le module
fractionsde la bibliothèque standard Python existe depuis Python 2.6 Fractionest une autre manière de représenter les nombres et peut, une fois créé, être utilisé à la place des floats- Les nombres à virgule flottante ont des limites bien connues
- Par exemple,
0.1 + 0.1 + 0.1 == 0.3renvoieFalse
- Par exemple,
- Dans Textual, les erreurs d’arrondi des floats posaient problème dans les mises en page qui divisent l’écran selon plusieurs proportions
- Dans une situation où un panneau occupe 1/3 de la largeur et où les 2/3 restants sont redécoupés, il pouvait apparaître un espace vide d’un caractère
- Remplacer les floats par
Fractionpermet d’éviter ce type d’erreur d’arrondi- La somme de trois
Fraction(1, 10)est égale àFraction(3, 10)
- La somme de trois
- Dans un exemple où 24 caractères sont répartis en 7 segments, la version en float laissait un caractère manquant, alors que la version en
Fractionremplissait toute la longueur
Les difficultés des Emoji dans le terminal
- La prise en charge des Emoji est un problème constant depuis les débuts de Rich, et Textual en hérite lui aussi
- Dans un terminal, un caractère occupe généralement une ou deux colonnes, et certains ont une largeur de 0
- Les caractères CJK prennent deux fois plus d’espace que l’alphabet latin
- Pour effectuer des opérations de formatage de base comme le centrage ou le dessin de boîtes, il faut connaître la largeur terminal du texte affiché
- Il n’est donc pas possible de connaître cette largeur avec un simple
len(text)
- La base de données Unicode contient des correspondances pour les caractères simple largeur et double largeur
- Rich et Textual la consultent pour chaque caractère affiché
- Cette opération n’est pas légère, mais l’ingénierie et le cache permettent de la rendre suffisamment rapide
- Les Emoji compliquent encore les choses : même lorsqu’ils sont présents dans la base Unicode, chaque nouvelle version d’Unicode en ajoute de nouveaux, ce qui rend le résultat difficile à prévoir
- Un nouvel Emoji affiché dans un terminal peut apparaître sur une ou deux colonnes
- Il peut aussi ne pas être rendu correctement
- Il n’existe pas de méthode standard fiable pour détecter la version d’Unicode utilisée par un émulateur de terminal
- Il n’existe pas non plus de variable d’environnement standard
- Il existe des heuristiques consistant à afficher certaines séquences puis à interroger la position du curseur, mais lors des tests, même en supposant connaître la base Unicode, le terminal rendait les Emoji de façon imprévisible
- Les Emoji composés de plusieurs code points rendent le problème encore plus complexe
- Par exemple, 👨🏻🦰 est composé de 4 code points
- Selon le terminal, il peut être rendu comme 4 caractères, 2 caractères, 1 caractère, sur une ou deux colonnes, ou même comme 4
?
- En pratique, les Emoji d’Unicode 9 sont relativement stables sur plusieurs plateformes
- Mieux vaut éviter les nouveaux Emoji et les caractères à plusieurs code points, même s’ils semblent bien s’afficher dans les terminaux actuels
Recrutement chez Textualize
- Textualize recrute pour construire un framework TUI capable de remplacer certaines fonctions du navigateur
- Les offres sont disponibles sur Jobs
1 commentaires
Commentaires sur Hacker News
Il est amusant de voir que tous les développeurs de TUI finissent par se heurter à Unicode, et que gérer correctement les caractères internationaux et les emojis devient un projet séparé, de taille comparable ou supérieure au projet TUI initial
La même chose s’est produite avec rivo/tview, et le paquet rivo/uniseg créé à cette occasion a aussi permis de constater que les mainteneurs d’autres bibliothèques TUI rencontraient exactement le même problème
Au final, comme la largeur des caractères n’est pas standardisée et que les terminaux sont complexes, chacun invente sa propre solution. L’auteur de l’article ne prend en charge que Unicode 9, alors que la version actuelle d’Unicode est 15.1, donc il est difficile d’être certain que c’est une bonne solution : tôt ou tard, quelqu’un se plaindra qu’un emoji ou un caractère international s’affiche mal
Je maintiens deux bibliothèques TUI qui utilisent cette technique, et la prise en charge des emojis y est presque excellente. L’une des deux utilise aussi la bibliothèque uniseg
https://mitchellh.com/writing/grapheme-clusters-in-terminals
Mon principal reproche à textual, c’est qu’il essaie de devenir React. Je comprends pourquoi, puisque c’est un framework populaire que beaucoup de gens connaissent, mais je ne pense pas que ce soit une bonne manière de construire des interfaces utilisateur
Le design réactif en lui-même est une approche éprouvée, et fonder l’architecture d’un système sur une méthode connue pour bien fonctionner est une bonne façon de réduire les risques d’un projet
Mais aller jusqu’à utiliser une variante de CSS me paraît excessif. Le modèle React a déjà cassé à bien des égards les concepts de CSS en privilégiant des composants standardisés, et le fait que les développeurs personnalisent des composants via CSS ressemble plus à un effet secondaire de l’évolution de React qu’à une architecture réellement justifiée
La dernière fois que je l’ai utilisé, il fallait passer par CSS, et comme il n’y avait pas de bons composants standard, il fallait les créer soi-même. Au lieu d’avoir un composant sous la forme d’une unique classe Python proprement encapsulée, la documentation officielle montrait comment créer un composant de liste et le styliser avec une feuille de style externe
En Python, il devrait y avoir une façon évidente de faire les choses, une seule si possible. À force de trop coller à React, on a l’impression qu’il imite aussi les pires habitudes de la communauté JavaScript
Et cela fait aussi un bon moment qu’il n’est plus vrai qu’il n’y a pas de bons composants standard : https://textual.textualize.io/widget_gallery/
Je n’ai pas eu besoin de créer mes propres composants, et je connaissais déjà CSS. Au contraire, ce sont plutôt les petites différences de layout et les écarts par rapport à CSS qui m’ont parfois dérouté
L’idée serait qu’un des composants d’une fenêtre GUI classique soit une fenêtre de terminal. Je me demande si cela existe, ou s’il faut simplement utiliser une vue texte en police à chasse fixe
Cette TUI est jolie, mais j’ai du mal à imaginer une situation où je l’utiliserais réellement et paierais pour ça. Je ne suis peut-être pas dans le bon contexte, mais d’après mon expérience, les gens se satisfont d’outils vraiment minimalistes ou cherchent directement à satisfaire les utilisateurs avec une GUI
Par exemple, le lien YouTube dans l’article montrait un tableau avec une cellule mise en évidence, mais je ne vois pas pourquoi il faudrait faire ça en TUI. S’il faut déplacer la cellule active mise en évidence, il faudra au final beaucoup d’autres fonctions aussi, et on aura probablement besoin d’une vraie GUI
En revanche, je ne sais pas très bien ce qu’est une « vraie GUI ». Le terminal dispose de standards ouverts largement utilisés, et ces protocoles sont plus standardisés, plus interopérables et mieux pris en charge sur tous les systèmes que n’importe quel framework GUI
Si on y ajoute des éléments comme la vidéo et qu’on comble quelques lacunes, il semble aussi raisonnable d’imaginer concurrencer le navigateur ou de ramener l’expérience informatique complète dans le terminal
Bien sûr, si le terminal ajoute la vidéo, cela revient à recopier le pipeline de rendu d’une « vraie GUI », mais cela améliore l’expérience du terminal et, en cas de grand succès, on peut aussi y voir une manière d’apporter la simplicité à la UNIX à la GUI
Ils travaillent très efficacement avec des raccourcis clavier, et chaque fois que quelqu’un essaie de remplacer l’AS400 par une application web moderne, la productivité chute d’un facteur 100
C’est la même situation que la différence entre un expert de Vim/Emacs et une GUI élégante en apparence mais sans raccourcis clavier
On peut laisser l’utilisateur se connecter au serveur en SSH et utiliser l’application TUI comme un shell, même si j’admets que le scénario paraît un peu forcé
Personnellement, Textual m’a semblé un peu étrange à l’usage, mais c’était mieux que ncurses. Cela dit, ça ne m’a pas donné le résultat que je voulais. Les anciennes applications TUI de style mainframe donnaient une impression d’efficacité remarquable
Parfois cela fonctionne bien, mais dans des cas limités, par exemple avec le X forwarding sur une connexion à forte bande passante. Dans d’autres cas, la GUI devient complètement molle
Une GUI basée sur le texte semble pouvoir combiner les avantages des deux côtés
L’approche « première astuce : ne pas effacer, mais écraser » est la même que celle utilisée pour écrire les jeux d’avant DirectX
On écrivait directement dans le framebuffer, et au lieu de tout effacer puis redessiner, on redessinait seulement les parties modifiées et ce qu’il y avait autour ou en dessous. C’était parce qu’on n’avait pas le temps de rafraîchir tout l’écran à temps
La première consistait à écrire dans un autre buffer, puis, une fois la frame terminée, à copier tout le buffer d’un coup afin que chaque pixel ne change qu’une seule fois. Ce buffer pouvait souvent se trouver en RAM normale plutôt qu’en mémoire vidéo
La seconde consistait à écrire dans un autre buffer situé en mémoire vidéo, puis à modifier les registres du matériel graphique pour que les pixels affichés à l’écran soient générés à partir de ce buffer
Les compromis étaient différents. La copie du buffer entier coûtait cher, alors que le changement de registre d’adresse coûtait peu. Mais les détails des registres pouvaient dépendre du matériel, et il n’existait pas de véritable framework de pilotes graphiques
Pour simplement faire un « buffer flip », il fallait quand même envoyer les pixels en mémoire vidéo lors du rendu dans le buffer hors écran, et il me semble que l’accès à la mémoire vidéo était plus lent que l’accès à la RAM normale. C’était pratiquement une architecture NUMA, donc selon la fréquence à laquelle les pixels étaient redessinés, il pouvait être globalement plus rapide de faire le rendu en RAM normale, même en comptant la copie finale
Cela dit, je pense que la possibilité d’indiquer au terminal le début et la fin d’une frame est bien plus puissante
Je ne comprends vraiment pas pourquoi les ingénieurs logiciel s’intéressent autant aux TUI. J’aime les bons programmes en ligne de commande, mais les TUI ne m’attirent pas
En plus, il y a un côté hacker. Le style n’est pas fortement séparé du contenu, et les options de style ajustables sont limitées, donc les applications TUI convergent en général vers un style unique. Un style qui évoque les films de hackers et la bonne vieille SF
Ce n’est pas non plus le terrain préféré des designers d’entreprise, et les sociétés ne concluent pas des ventes auprès des entreprises ou du grand public sur la base d’une TUI. Faute de cette pression commerciale, les TUI sont conçues par des développeurs pour des développeurs
Du coup, le mème TUI s’auto-renforce. Les développeurs voient et utilisent des TUI en sentant qu’il s’agit généralement d’outils pensés pour d’autres développeurs, puis ils ont envie de créer leur propre TUI
Le cross-platform est presque gratuit, il n’y a pas besoin de créer une GUI parfaite au pixel près, ce qui est de toute façon impossible, et il n’y a pas besoin d’icônes ni de graphismes. Comme on ne peut pas afficher de graphismes
On peut garder son émulateur de terminal avec sa police préférée tout en ayant une interface de type GUI
Toutes les applications TUI que j’ai utilisées jusqu’ici avaient une faible latence. Les applications GUI sont généralement rapides elles aussi, mais toutes celles que j’ai trouvées péniblement lentes étaient des applications GUI
Il existe ascii-draw comme alternative à Monodraw
https://github.com/Nokse22/ascii-draw
Presque tout ce que j’ai vu après TurboVision s’en inspirait, mais manquait en pratique de finition. Quand on utilise vraiment la boîte à outils, c’est mignon, mais on a l’impression que ce n’est pas abouti
Autrement dit, on pourrait dire que beaucoup de frameworks TUI que j’ai vus étaient des frameworks « très prescriptifs »
C’est peut-être juste moi qui suis lent et peu créatif, mais depuis mon passage du DOS à Linux dans les années 90, je n’ai jamais été aussi productif qu’à l’époque où je travaillais avec B800
Il m’a peut-être fallu longtemps pour comprendre la nécessité du double buffering, la différence entre le mode texte local direct et un terminal, jusqu’aux séquences d’échappement. Malgré ça, chaque fois que j’essayais de faire quelque chose moi-même avec ncurses, je finissais presque par abandonner à cause d’un sentiment d’insatisfaction très net. Cela n’avait rien à voir avec ce qu’on pouvait faire au-delà du simple idéal de B800
Je ne sais pas si ma vision était bouchée à ce point, mais tous les frameworks GUI après la boucle de messages m’ont toujours semblé complètement incompréhensibles
En particulier, je n’ai jamais compris comment les frameworks orientés objet résolvaient réellement les problèmes d’UI multithread. Sous Win32, je mettais le renderer principal dans un thread, les renderers secondaires construisaient le modèle à côté, puis ne mettaient à jour que les différences. Le code n’est jamais devenu ingérable
Si on va lancer kitty, on peut faire bien plus que ça : https://m.youtube.com/watch?v=ft1Q-DwGWIs
https://notcurses.com/
Les deux peuvent afficher des graphiques avec le même protocole. Si on a vraiment besoin de graphiques, le terminal n’est presque jamais la bonne solution. Cela dit, pour de petites choses comme des icônes, ça peut parfois être utile
C’est intéressant de voir qu’ils recrutent. Je me demande comment ils comptent gagner de l’argent avec un framework TUI
Pour info, j’ai évalué une douzaine de bibliothèques TUI compilées, et FTXUI était la plus simple à utiliser et la plus stable
https://github.com/ArthurSonzogni/FTXUI
C’est un bon outil pour créer des tableaux de bord interactifs avec prise en charge du clavier et de la souris à la fois