Artisan Numérique

/cli/terminal/urxvt/ Créer un plugin URxvt pour sauvegarder VIM automatiquement

Un problème récurrent avec les applications en mode texte est qu'elle n'ont pas conscience du fait que la fenêtre qui les héberge gagne, ou perd le focus. Cette information est pourtant bien utile car elle peut par exemple permettre sous VIM de sauvegarder les buffers modifiés lorsque l'on laisse l'éditeur de côté (perte de focus). Mais aussi de mettre les buffers à jour s'ils ont été modifiés à l'extérieur de VIM lorsque l'on y retourne (gain de focus). Nous allons donc voir comment rendre une application texte consciente de ces deux évènements.

Perte et gain de focus

Lorsqu'une fenêtre a le focus, tout ce qui est tapé au clavier lui est envoyé. La sélection de la fenêtre qui a ce focus est réalisée par le gestionnaire de fenêtre. Certains considèrent que le focus est là où se trouve la souris. C'est ainsi que l'on fonctionne sous Unix depuis des lustres. Du moins jusqu'à ce que la mode windowsienne ne prenne le pas et obligent en plus à cliquer dans la fenêtre à rendre prioritaire.

Le fait de perdre le focus est donc un bon moyen de dire à une application que l'on passe à une autre tâche. On a donc logiquement envie, s'agissant d'un éditeur de texte, d'en profiter pour faire une petite sauvegarde automatique.

Lorsque l'éditeur est une chose graphique (gedit, gVim), il est capable de recevoir directement les messages perte et gain de focus, envoyés par X11. En revanche lorsqu'il s'agit d'une application texte (Vim, Mutt, etc.) c'est plus compliqué car c'est l'émulateur de terminal qui reçoit ces messages. Il nous faut donc faire un pont entre le gestionnaire de fenêtre et l'application en mode texte en utilisant l'émulateur de terminal comme intermédiaire.

Construcion d'un OSC maison

Comme vous le savez déjà, les applications en mode texte conversent avec le terminal sous la forme de séquence d'échappement commençant par le code 27 (aka ESC, aka \033).

Pour s'y retrouver, ces séquences ont des petits noms. Prenons par exemple les séquences ANSI qui permet de cacher le curseur ESC[?25l. La première partie de cette séquence ESC[ défini les commandes dites CSI (Control Sequence Introducer). On résume donc souvent dans les documentation cette commande de masquage de curseur CSI?25l.

Il existe de nombreuses autres classes de séquence (qui commencent cependant toujours par ESC), comme par exemples les OSC (Operating System Control) qui elles sont de la forme ESC] (notez le crochet dans l'autre sens). Ces séquencent permettent généralement de modifier le comportement du terminal comme par exemple changer la couleur du curseur (OSC 12;red BEL). BEL représente le code qui fait beep qui s'écrit aussi \007.

Urxvt permet à ses plugins d'intercepter n'importe quelle séquence OSC nous permettant ainsi de composer la notre pour activer et désactiver la prise en charge du focus par le terminal. En effet, si nous ne faisons pas cela, une console ne gérant pas le focus serait rapidement polluée par les entrées/sorties du curseur de la souris.

  1. Activation du mode mode "focus" par la séquence \033]777;focus;on\007
  2. Désactivation du mode "focus" par la séquence \033]777;focus;off\007

Notez l'usage de 777 qui est une classe spéciale d'OSC qu'urxvt réserve aux plugins.

Pour terminer, lorsque le mode "focus" sera activé, le terminal va lui aussi émettre des séquences que VIM va interpréter. C'est un peu comme des touches "spéciales" :

  1. Focus gagné par la séquence \033UlFocusIn.
  2. Focus perdu par la séquence \033UlFocusOut.

Ces séquences ne suivent aucune normes. C'est un choix totalement arbitraire avec comme seule contrainte de ne pas taper dans une séquence existante.

Construction du plugin URxvt

URxvt dispose d'une API en perl très bien documentée (man urxvtperl) qui fournit une série d'évènements dont le gain et la perte du focus, mais aussi la gestion des séquences OSC custom.

Construire un plugin pour URxvt est relativement simple. Il suffit pour cela de créer un script perle focus que l'on place par exemple dans un dossier ~/.urxvt/perl :

#!/usr/bin/perl

# Hook invoqué au démarrage d'urxvt
sub on_start {
  my($term) = @_;
  // le mode "focus" est désactivé par défaut
  $terme->{focus_activated} = 0;
}

# Interception des séquences "OSC 777 xxx BEL"
sub on_osc_seq_perl {
  my ($term, $osc, $resp) = @_;
  return unless $osc =~ s/^focus;//;
  $term->{focus_activated} = $osc eq 'on'?1:0;
}

# Hook invoqué au gain de focus
sub on_focus_in {
  my($term) = @_;

  # Si le mode focus est activé, on envoie la notification
  if ($term->{focus_activated}) {
    $term->tt_write("\033[UlFocusIn");
  }
}

# Hook invoqué à la perte de focus
sub on_focus_out {
  my($term) = @_;

  # Si le mode focus est activé, on envoie la notification
  if ($term->{focus_activated}) {
    $term->tt_write("\033[UlFocusOut");
  }
}
Plugin URxvt - ~/.urxvt/perl/focus

Comme vous le voyez le plugin est assez simple. Les subs définies sont des hooks, c'est à dire des fonctions qui sont appelées par URxvt en réaction à des évènements :

  • on_start est déclenché lorsque le plugin est instancé. On va y régler l'état du mode "focus" par défaut (désactivé)
  • on_focus_in est déclenché lorsqu'URxvt est notifié de la prise de focus. On vérifie alors que le mode focus est activé et si c'est le cas, on écrit la séquence qui va bien dans le terminal.
  • on_focus_out est déclenché lorsqu'URxvt est notifié de la perte de focus.
  • on_osc_seq_perl est déclenché dés qu'URXVT reçoit une séquence OSC. On vérifie juste qu'il s'agit bien de la notre et on active/désactive le mode focus.

Une fois que ce plugin écrit, il nous reste à l'installer dans URxvt. Pour cela rajouter les lignes suivantes à ~.Xdefaults :

# On dit ou se trouvent nos extensions
URxvt.perl-lib : /home/gaston/.urxvt/perl

# Et on active l'extension "focus"
URxvt.perl-ext-common: focus

Nous pouvons maintenant relancer URxvt (éventuellement faire un coup de xrdb -load ~/.Xdefaults pour s'assurer que les ressources sont bien à jour) et tester l'activation du mode "focus" :

gastonecho -ne "\033]777;focus;on\007"

Si tout c'est bien passé, en survolant la fenêtre (ou en cliquant dessus selon votre gestionnaire de fenêtres) vous devriez voir des choses apparaître. Idem en perte de focus. Les choses en question sont une tentative échouée d'interprétation de nos séquences par Bash. D'où l'intérêt de pouvoir activer/désactiver ce mode à convenance.

Implémentation du mode focus dans VIM

Maintenant que notre plugin est en place, il ne reste plus qu'à l'intégrer dans VIM :

" Initialisation du mode "focus"
exe 'silent !echo -ne "\033]777;focus;on\007"'

" Gestion de la séquence focusin/focusout
map ^[[UlFocusIn :bufdo checktime<CR>
map ^[[UlFocusOut :wa!<CR>
map! ^[[UlFocusIn <C-O>:bufdo checktime<CR>
map! ^[[UlFocusOut <C-O>:wa!<CR>

" Désactivation du mode focus en partant
autocmd VimLeavePre * exe 'silent !echo -ne "\033]777;focus;off\007"'

Notez bien que l'usage que je fait ici de map est tout sauf efficient. Il est juste là pour simplifier le code. Il serait beaucoup plus efficace d'utiliser la méthode décrite ici.

En relançant votre VIM, la magie devrait fonctionner. En perte du focus on force la sauvegarde de tous les buffers (wa!). Et lorsque l'on regagne le focus, on demande à VIM de passer en revue tous les buffers ouverts (bufdo) pour les recharger s'ils ont été modifiés (checktime).

Approche un peu plus sauvage

Cette technique ne marche pas mal mais elle souffre d'un défaut désagréable. Lorsque l'on passe en mode commande par : (ou recherche par ? ou /) et que l'on a le malheur de perdre le focus, la ligne que l'on saisissait vient se faire pourrir par les messages de notification.

Malheureusement il n'y a pas à ma connaissance de moyen de détourner le problème. VIM ne propose en effet pas d'évènement déclenché à l'entrée et à la sortie des modes commande. La solution est donc d'aller hacker le code de VIM.

Pour cela il faut récupérer le code source. Le mieux est de passer par le gestionnaire de version mercurial comme indiqué ici.

Lorsque le dossier vim est créé, il faut aller modifier le fichier src/os_unix.c et aux environs de la ligne 3342, dans la fonction mch_setmouse, nous allons modifier comme ceci :

# ifdef FEAT_MOUSE_URXVT
    if (ttym_flags == TTYM_URXVT) {
  out_str_nf((char_u *)
       (on
       ? IF_EB("\033[?1015h", ESC_STR "[?1015h")
       : IF_EB("\033[?1015l", ESC_STR "[?1015l")));

        // ----------- LE VILAIN PATCH... {{{
  out_str_nf((char_u *)
       (on
       ? IF_EB("\033]777;focus;on\007",  ESC_STR "]777;focus;on" (char_u *)"\077")
       : IF_EB("\033]777;focus;off\007", ESC_STR "]777;focus;off" (char_u *)"\077")));
       // ------------ FIN DU PATCH}}}

  ison = on;
    }
    # endif

Ceci fait il ne reste plus qu'à configure/compiler/installer. Perso je fais comme ceci, mais la configuration est à adapter selon vos goûts. Je vous conseille si vous vous lancez là dedans de désinstaller proprement VIM avant d'installer ce résultat de compilation !!

make distclean
./configure \
  --prefix=/usr  \
  --with-features=huge \
  --disable-selinux \
  --enable-pythoninterp \
  --enable-xim \
  --enable-float \
  --enable-gui=no \
  --disable-workshop \
  --with-x

  #--enable-perlinterp \
  #--enable-cscope \
make -j 10
sudo make install

Alors avant de lancer, comment ça marche. Le principe est assez simple. VIM au lancement va activer la notification des évènements souris auprès du terminal (set mouse=...). Lorsqu'il va s'arrêter, il va désactiver cette notification. Et lorsqu'il passe en mode commande, il va aussi désactiver. L'astuce est ici donc juste de prendre le train en marche et d'activer/désactiver le mode "focus" en même temps que le mode "souris". Oui je sais c'est stupide, c'est moche, mais ça marche super bien ;-)

Pour que cela fonctionne, il faut supprimer du chapitre précédent tout ce qui touche à l'activation/désactivation du mode "focus" (mais pas la gestion des notifications évidemment) et laisser vim faire...

Conclusion

Voilà en tout cas une manière de replacer avantageusement l'auto-sauvegarde pour VIM que j'avais réalisée précédemment. Car contrairement à la méthode que je proposais, celle-ci fonctionne en toute circonstances et sans aucun délai.

Après cette technique peut être exploitée pour bien d'autres usages. Par exemple un problème classique lorsque l'on bosse en console est de pouvoir copier du texte sur une console distante (un vim en ssh) et de récupérer ce contenu en local. Par cette approche c'est facilement réalisable en définissant une séquence OSC "presse-papier" qu'URxvt récupérerait pour transférer le contenu dans la sélection CLIPBOARD du serveur X11 local. Quelque chose comme :

gastonecho -en "\033]777;CLIPBOARD;ON\077Ceci est mon texte à copier\033]777;CLIPBOARD;OFF\077"

D'ailleurs l'idée est déjà une petit peu mis en place dans un plugin peu connu d'URXVT, clipboard-osc. Sur le même principe peut être utilisé pour implémenter un protocole de transfert de fichier type ZModem/Kermit.

Comme vous le voyez, les usages sont nombreux. Et c'est une fois de plus la preuve qu'URxvt est un fantastiquement outil rentrant pleinement dans la catégories des "ce que je veux, je peux".