New curses

Créé le 2010-11-28 et mis à jour le 2010-12-10 par André Gillibert

Introduction: Les terminaux

Les premiers terminaux, aussi connus sous le nom de terminaux stupides, étaient très simples, ne reconnaissant que les caractères de contrôle de base, CR et LF et transmettant directement les codes ASCII des touches enfoncées. Grâce à la miniaturisation et la réduction des coûts de l'électronique, dans les années 70, le marché des terminaux intelligents florit. Ces terminaux supportent des fonctionnalités de déplacement de curseur, de surbrillance ou même de changement du jeu de caractères.

Au delà des fonctions de bases définies par le standard ASCII pour les 33 caractères de contrôles, Les fonctionnalités avancées des terminaux sont obtenues par des séquences d'échappement, toutes débutées par le caractère \033 (ESC) suivi d'un ou plusieurs caractères affichable. De même, les touches supplémentaires, telles que les touches de fléchées, génèrent des séquences d'échappement.

Alors que les caractères CR, LF, TAB et FF sont bien standardisés depuis 1963, les séquences d'échappement n'ont pas été standardisées avant ECMA-48 en 1976. Malheureusement ECMA-48 n'a jamais standardisé les entrées au clavier. Le DEC VT-102, un des premiers terminaux DEC ECMA-48 compatible, devint un standard de fait.

De nos jours, les terminaux physiques ont prèsque disparus, remplacés par des émulateurs de terminaux se vantant d'être compatibles avec le VT-102. Malheureusement, le VT-102 étant pauvre en touches, il n'existe aucun standard de fait pour INSER, SUPPR, ORIGINE, FIN, PAGEPRÉC, PAGESUIV, F5-F24.

Suppr et retour arrière

Le pire de l'histoire tient dans le traitement de SUPPR et RET.ARR (BACKSPACE). Le VT-102 ne disposait que d'une seule touche étiquettée DELETE, et dont l'usage est à rapprocher de celui de RET.ARR. Cette touche générait le caractère ASCII numéro 127 dénommé DEL. Il est donc naturel, pour un émulateur de terminal VT-102 sur un clavier PC moderne, de générer le caractère DEL pour la touche RET.ARR, et de générer une séquence spéciale, telle que ESC [3~, pour SUPPR. Mais, certains créateurs de terminaux virtuels en décidèrent autrement, en faisant correspondre le code ASCII numéro 8 (BS) à la touche RET.ARR. Le tableau ci-dessous illustre les incompatibilités rencontrées:

Terminaux VT-102 Linux console, xterm, XFCE4-Terminal, Eterm, roxterm rxvt, putty, gnome-terminal, aterm, OpenBSD console Minix R3, kterm, hanterm, mlterm, Win95/WinNT telnet, twin, FreeBSD console, OpenSolaris console Hurd console NetBSD console
Retour arrière DEL BS DEL BS DEL DEL
Suppr Indisponible \033[3~ \033[3~ DEL \033[9 DEL

Pour rxvt et xterm, il existe une option pour définir le caractère représentant la touche de retour arrière.

Ainsi, un programme ne sachant pas sur quel type terminal il est exécuté et recevant le code ASCII DEL ne peut savoir s'il correspond à la touche SUPPR ou RET.ARR, ce qui nous amène au problème d'identification des terminaux. Chaque émulateur de terminal assigne son nom à la variable d'environnement TERM avant d'exécuter la tâche principale du terminal, habituellement getty ou sh. Bien sûr pour corser les choses, la majorité des terminaux assignent une valeur mensongère à TERM. Prèsque tous les terminaux graphiques s'identifient comme xterm ou rxvt, indépendamment de leur comportement. Heureusement, certains terminaux assignent une seconde variable d'environnement, COLORTERM, qui reflète de manière moins erronée le nom du terminal, même si certains terminaux persistent à mentir.

Pourquoi un tel mensonge ? Je crois que la gestion des terminfo/termcaps par les programmes et en particuliers les librairies curses est en partie responsable. Tout nouveau terminal souhaite fonctionner correctement avec les systèmes existant, sans modification. Or, la librairies curses, et probablement d'autres programmes, entrent dans un mode très dégradé si jamais la variable TERM est assignée à une valeur qui n'est pas définie par les termcaps/terminfo. Libncurses traite un terminal non reconnu comme un terminal stupide alors que dans la majorité des cas, il s'agit d'un terminal compatible ECMA-48.

Un programme qui souhaite fonctionner correctement dans un maximum de cas, se doit de considérer DEL et BS comme synonymes de BACKSPACE et \033[3~ et \033[9 comme identifiant la touche SUPPR, au coût de ne pas permettre à certains terminaux, certes médiocres, de distinguer les deux touches.

La touche échap

Toutes les touches avancées génèrent des séquences d'échappement débutant par le caractère ESC (ASCII 033). Par exemple, un véritable VT-102 génère \033[D pour la touche fléchée gauche. La touche échap, sur tous les terminaux, génère le caractère ESC tout seul. Ainsi, il est théoriquement impossible de voir la différence entre la séquence de touches (échap), ([), (D) et la touche fléchée gauche. En pratique, quand une touche fléchée est tapée, les caractères arrivent prèsque toujours simultanément à l'application, en une seule opération read(2). Mais, il est néammoins possible que les octets arrivent séparément, parfois avec un intervalle de temps assez important, si par exemple, le caractère est tapé dans un terminal SSH distant et que la séquence soit partagée entre deux paquets IP, soit initialement, soit par section du paquet en deux, au niveau d'un routeur. En cas de paquet perdu, il est renvoyé après un intervalle long, probablement de plusieurs secondes. Certains programmes gèrent néammoins la touche échap, en considérant qu'une séquence d'échappement pour une touche doit arriver entièrement dans un intervalle de temps limité (une ou deux secondes). C'est ce que libncurses fait.

La librairie ncurses

Fonctionnement

La librairie ncurses, qui signifie "new curses" et qui, comme on s'y attend, amène son lot de nouvelles malédictions, est une des librairies de base de GNU.

C'est un petit monstre de 227 kibi-octets pour la version 5.7, dont 125 de code pur, 42 de données et 60 de tables de symboles, relocalisations et autres méta-données si chères aux liens dynamiques. Cette librairie dépend des fichiers binaires de terminfo, version plus moderne des termcaps, situés dans /usr/share/terminfo/* et /etc/terminfo/*, décrivant les capacités et les séquences d'échappement de chaque terminal en fonction de son nom. Ces fichiers sont générés à partir d'un fichier terminfo.src de 927 kibi-octets avec les commentaires ou 489 kibi-octets sans les commentaires et sont à l'origine d'un répertoire /usr/share/terminfo de plus de 3 méga-octets sous forme d'archive tar, mais occupant le double sur un système de fichiers à clusters de 4096 octets.

Son travail est pourtant simpliste: Fournir une interface d'entrée-sortie indépendante du terminal.

Pour les sorties, il fournit des fonctions d'affichage et de positionnement, très rapides, travaillant sur une mémoire tampon, une fonction de synchronisation (refresh), chargée de mettre à jour l'état du terminal en un nombre minimal d'opérations. Ainsi, si l'affichage a été décalé verticalement d'un caractère, ncurses s'en rend compte, en comparant le tampon actuel au tampon précédent et exploite la fonction de défilement du terminal.

Pour les entrées, quelques fonctions permettent d'apprécier la présence de caractères dans le tampon d'entrée ou d'attendre qu'ils arrivent. Les séquences des touches spéciales sont automatiquement convertis depuis leur représentation basique en un code abstrait, indépendant du terminal, tel que KEY_BACKSPACE, KEY_LEFT ou KEY_F3.

Compatibilité

Cette architecture, semblant puissante de prime abord, se révèle être très limitée. En effet, elle base tout sur l'exactitude et la précision de la variable d'environnement TERM, qui est en réalité peu informative puisque prèsque toujours assignée à xterm ou rxvt. L'utilisateur peut éventuellement assigner manuellement la valeur la plus pertinente à TERM, mais cette variable n'indiquera jamais la configuration du terminal. Par exemple, xterm, en fonction de ressources X11 ou de paramètres sur la ligne de commande, peut générer, soit BS, soit DEL, pour la touche RET.ARR, ou se comporter comme un VT-102, un VT-52 ou un VT-220, pourtant, dans tous les cas, TERM indique "xterm".

La variable TERM étant de peu d'utilité, il eût été intelligent d'écrire une fichier terminfo générique, se comportement raisonnablement sur prèsque tous les terminaux compatibles ECMA-48. Après tout, il suffit de ne sortir que des séquences ECMA-48 de base et d'accepter toutes les séquences existantes pour les touches en entrée, à l'exception de SUPPR et RET.ARR pour lesquels ont doit faire un sacrifice. Mais, le langage des terminfos est trop limité pour ça, ne permettant qu'une seule séquence d'échappement par touche spéciale. Ainsi, il n'est pas possible d'accepter à la fois \033[8~ (rxvt) et \033[F (xterm) pour la touche FIN.

Ainsi, les 6.3 méga-octets d'espace disque occupés par /usr/share/terminfo, censés rendre ncurses compatible avec tous les terminaux existant, couvrent mal la compatibilité avec les terminaux qu'un utilisateur moderne emploie quotidiennement tels que putty (ORIGINE, FIN non supportés, RET.ARR interprété comme SUPPR), gnome-terminal (RET.ARR interprété comme SUPPR) ou rxvt (idem). Si l'utilisateur modifie manuellement TERM sous putty, alors, ORIG/FIN/RET.ARR sont correctement supportés, par contre CONTROL+C n'est plus reconnu, ce qui est très handicapant.

Les touches composées de CONTROL, ALT et d'une autre touche sont encore moins bien gérées. Ainsi, pour CONTROL+FLÊCHE, xterm et rxvt sont correctement gérés par ncurses, par contre putty n'est pas géré, même avec TERM=putty. Pour ALT+FLÊCHE, xterm est géré, mais ni rxvt, ni putty ne sont gérés. Il faut dire que ces deux derniers utilisent des séquences ambigües pour ESC+FLÊCHE et ALT+FLÊCHE. Enfin, pour CONTROL+ALT+SHIFT+FLÊCHES, même xterm n'est pas géré !

Le seul terminal que j'utilise fréquemment semblant être totalement géré par ncurses est le VT Linux, quoique, CONTROL+SPACE n'est pas tout à fait interprété comme il le devrait.

Compilation du code

Section à venir.

Conclusion

Le standard ECMA-48 ayant omis les fonctions d'entrées des terminaux, et les terminaux semblant n'avoir même pas tenté de générer des codes compatibles les uns avec les autres, les touches avancées sont très variable d'un terminal à l'autre. Les sorties sont bien standardisées.

La librairie ncurses est donc incapable de remplir sa fonction, bien qu'elle occupe des méga-octets sur le disque. C'est encore un exemple de projet tentant d'être compatible avec les bouliers mais échouant aux tests de compatibilité avec les outils que tout le monde utilise.

Et pourtant, en dehors du problème de SUPPR et RET.ARR, bien que les terminaux génèrent des codes très variable, chaque code n'a qu'une seule signification et il est donc possible d'écrire un programme compatible avec prèsque tous les terminaux modernes.