Un curseur et des branches
Le clavier est pour le moment implémenté avec deux mots. L'un qui sera gardé, KBDSCAN et l'autre qui est là en attendant de pouvoir écrire la même chose en Forth, KBDPROCESS. L'objectif premier est de transformer KBDPROCESS en son équivalent Forth que je placerai dans ma boucle principale (pas encore QUIT, qui n'est pas encore prêt).
Mais avant toute chose, j'ai quelque chose à corriger avec le curseur. Pour le moment, j'affiche le curseur avec EMIT, ce qui fait avancer la prochaine position d'affichage de caractère. Ce que je veux, c'est afficher le caractère reçu du clavier avec EMIT puis afficher le caractère du curseur sans faire avancer la position d'affichage. Ainsi, le caractère du curseur sera toujours une position après le dernier caractère affiché.
Pour cela, j'ai un peu remanié le code afin de séparer l'envoi du caractère à afficher et la mise à jour de la position d'affichage. Le code de EMIT appelle les deux fonctions et j'ai ajouté le mot PUTCHR qui affiche le caractère à la position courante sans avancer la position du curseur.
Ainsi, l'affichage d'un caractère reçu depuis le clavier devient :
KEY \ Le caractère reçu du clavier est mis sur la pile
EMIT \ Affiche le caractère présent sur la pile et avance le curseur
CURSOR_CODE @ \ Récupère le code du caractère du curseur
PUTCHR \ Affiche le caractère du curseur sans avancer le curseur
Le fait de mettre le code du curseur dans une variable pointée par CURSOR_CODE permettra de changer la forme du curseur en fonction du mode (pour indiquer si on est en mode KANA ou non par exemple).
Avec un mot KEY bloquant, cela serait suffisant. Ce bout de code serait mis dans la boucle infinie et puis voilà. Seulement, je veux pouvoir opérer d'autres traitements dans la boucle d'attente de caractère. Faire clignoter le curseur par exemple, sans gestion par interruption. Et puis, c'est une bonne occasion d'implémenter une branche conditionnelle.
Le standard Forth actuel définit IF, ELSE et THEN de manière haut niveau, en indiquant le comportement attendu. Dans les Forth plus anciens, comme le FIG-Forth dont je m'inspire, le détail d'implémentation est d'utiliser les mots bas niveau 0BRANCH et BRANCH pour déplacer IP (le pointeur d'instruction).
J'ai déjà BRANCH, c'est le mot qui me permet d'avoir ma boucle infinie, en reculant IP de façon inconditionnelle. QUIT, actuellement, ressemble à ceci :
MAIN_LOOP \ Appel du mot dans lequel je fais mes tests
BRANCH \ Déplacement inconditionnel de IP en ajoutant le mot qui suit en mémoire
$fffc \ le mot qui suit en mémoire : -4. Ce qui ramène IP au niveau de MAIN_LOOP
0BRANCH fonctionne de la même manière, mais ne déplace IP que si la valeur au sommet de la pile est zéro. Dans le cas contraire, l'instruction ne fait rien d'autre que déplacer l'IP pour passer par-dessus le mot qui suit en mémoire, puis donne le contrôle à l'instruction suivante. Dans tous les cas, la valeur sur la pile est retirée de la pile.
Le mot est assez simple à implémenter avec mes briques existantes. Voici le code assembleur :
; 0BRANCH, branches if TOS is zero, else continues
DEFINE_CODE_WORD_WITH_SYMBOL ZERO_BRANCH, "0BRANCH", BRANCH, 0
; Pop TOS into Temp
POP_PARAM_STACK_TO_REG Temp
; If Temp (16 bits) is zero, do the branch, call branch
lda Temp
ora Temp + 1
beq BRANCH_word_pfa
; Not zero, advance IP by 2 (to skip the branch offset)
inc_reg REG_IP
inc_reg REG_IP
END_TO_NEXT
(rappel : TOS signifie "Top Of Stack", le sommet de la pile de données)
Avec 0BRANCH, le traitement et affichage du clavier devient :
KBDSCAN \ Scan du clavier et mise à jour des tampons
KEY? \ Y a-t-il un caractère disponible ?
0BRANCH \ Si non, branchement
$000c \ vers l'instruction après PUTCHR (12 octets plus loin)
KEY \ Récupération du caractère
EMIT \ Affichage du caractère et avancée du curseur
CURSOR_CODE @ \ Récupération du code du curseur
PUTCHR \ Affichage du curseur sans avancer le curseur
\ Suite du code de la boucle principale
Avec ce morceau de code, j'ai un curseur qui fonctionne et je peux l'initialiser dans COLD. J'en profite pour implémenter LIT qui permet de pousser la valeur qui suit le mot en mémoire sur la pile. Cela me permet d'écrire des valeurs immédiates dans le code Forth. Jusqu'à maintenant, j'utilisais des constantes qui contenaient les valeurs initiales.
Il n'est pas impossible que je revienne sur l'initialisation (et le déplacement) du curseur dans le futur pour deux raisons liées. La première est qu'écrire sur les bords de l'écran sur ces machines n'est pas une bonne idée : les télés cathodiques avaient tendance à couper les bords de l'image. Pour le moment, je n'écris ni sur la première ni sur la dernière colonne. Mais en regardant Family BASIC, je vois que ce sont deux colonnes entières qui sont laissées comme marges. Ainsi que les deux premières lignes.
Ce qui m'amène à la seconde raison liée : permettre de changer la géométrie de la fenêtre de texte par l'utilisateur. C'est une commande qui existe sur l'Hector HRX par exemple. Avec ce système, ce mot WINDOW permettrait de définir les marges haut, bas, gauche et droite. À noter pour plus tard... car cela compliquera la gestion du scroll lors de l'arrivée en bas de la zone.
Entrer du texte
Le texte s'affiche donc à l'écran, très bien. Mais il n'est pas encore prêt pour être traité par le système Forth. Pour cela, FIG-Forth utilise un buffer dont l'adresse est maintenue dans la variable adresse TIB (Text Input Buffer), ainsi que sa taille dans la variable adresse #TIB, comme indiqué dans le livre Starting Forth dans le chapitre Under the Hood.
Je ne sais pas encore où placer ce buffer. Je vois plusieurs options possibles :
- Je maintiens un buffer en RAM des caractères entrés et édités, en parallèle de ce qui est envoyé à l'écran. Cela limite l'édition à une ligne logique, pas possible de se balader sur un écran pleine page. Et maintenir en synchro deux systèmes parallèles n'est pas souvent une bonne idée.
- J'utilise l'écran lui-même comme buffer et je limite l'édition à une ligne logique. Le fait que les données à l'écran ne soient pas continues à cause des réservations des premières et dernières colonnes complique un peu les choses. Il faut aussi ramener les informations présentes dans la mémoire vidéo vers la RAM pour traitement, ce qui est assez lent et nécessite d'être fait pendant la VSync.
- J'utilise l'écran lui-même comme buffer et je permets l'édition pleine page. C'est globalement la même chose que l'option précédente, avec en plus un algorithme pour déterminer où commence la ligne logique lorsque l'on appuie sur RETURN.
Dans l'idéal, j'aimerais la troisième option. Cependant si cette fonctionnalité est très pratique en BASIC où on peut revenir sur des lignes précédentes pour les éditer, l'intérêt est moins évident en Forth, où il faut « oublier » les mots avant de les redéfinir.
La première option m'ennuie à cause du double traitement. Je pense que la seconde option sera mon choix de base. Si je vectorise le mot ACCEPT, il sera possible pour un utilisateur avancé de redéfinir le comportement pour faire de l'édition pleine page, en fonction de la RAM disponible sur la cartouche... je m'emballe.
Et les tests ?
L'affichage initial du curseur était testé par mes tests unitaires. Indirectement, le mot LIT que j'utilise à l'initialisation est testé. Par contre, j'ai à présent du texte en entrée et une boucle interactive avec du branchement conditionnel. Et pour ça, je n'ai qu'un test manuel : lancer le programme et vérifier que le clavier fonctionne. Et j'ai encore plus de mots standards que je devrais ajouter à un moment comme DUP, DROP, SWAP, OVER, etc. que je voudrais pouvoir tester depuis Forth.
Il existe un framework de test Forth, extrêmement simple, du nom de ttester, de John Hayes, ainsi que des versions dérivées. J'ai hâte de pouvoir l'utiliser. J'ai deux options. La première est de réécrire le code avec mon système de macros. Jouable, mais un peu long et fastidieux. La seconde est d'attendre d'avoir un interpréteur Forth fonctionnel... mais cela va m'obliger à écrire de nombreux mots sans tests, ce qui me déplaît. Je vais réfléchir à la question.
Pour la partie gestion du clavier, je pense ajouter à mon framework de test une injection de caractères. Il restera le test du clavier à tester physiquement, mais la boucle principale pourra être testée avec des entrées simulées.
La suite !
Mon étape suivante va être de compléter le traitement du clavier avec les majuscules, minuscules et kanas. Puis le traitement des touches de directions, insertions, suppressions, etc. Sur cette partie, je ne pense pas faire d'article, cela va être très mécanique. Cependant, je devrai me diriger vers l'implémentation du mot ACCEPT, qui gère l'entrée clavier et la fournit dans le buffer TIB. Cela m'obligera à choisir une des options évoquées plus haut, et ce sera donc probablement le sujet de l'article suivant.
Ah, et il sera peut-être temps de vérifier si tout cela fonctionne sur une vraie Famicom avec un vrai clavier Family BASIC !
En attendant, voici une capture sur émulateur :
