Écrire son propre émulateur de terminal

Vincent Bernat

J’étais un utilisateur heureux de rxvt-unicode jusqu’au jour où j’ai eu un portable avec un écran « haute densité ». Passer d’un écran externe classique à l’écran intégré était alors assez pénible : il fallait soit ajuster la fonte sur chaque terminal, soit redémarrer l’ensemble des terminaux.

VTE est une bibliothèque pour construire un émulateur de terminal utilisant GTK+ qui sait gérer le changement de densité. Elle est utilisée par de nombreux émulateurs tels que GNOME Terminal, evilvte, sakura, termit et ROXTerm. Elle est plutôt simple d’emploi si on se limite aux fonctionnalités prévues.

Un simple émulateur de terminal#

Commençons en se contentant des fonctionnalités par défaut. Nous utiliserons le C. Une autre option bien supportée est le langage Vala.

#include <vte/vte.h>

static void
child_ready(VteTerminal *terminal, GPid pid, GError *error, gpointer user_data)
{
    if (!terminal) return;
    if (pid == -1) gtk_main_quit();
}

int
main(int argc, char *argv[])
{
    GtkWidget *window, *terminal;

    /* Initialise GTK, the window and the terminal */
    gtk_init(&argc, &argv);
    terminal = vte_terminal_new();
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "myterm");

    /* Start a new shell */
    gchar **envp = g_get_environ();
    gchar **command = (gchar *[]){g_strdup(g_environ_getenv(envp, "SHELL")), NULL };
    g_strfreev(envp);
    vte_terminal_spawn_async(VTE_TERMINAL(terminal),
        VTE_PTY_DEFAULT,
        NULL,         /* working directory  */
        command,      /* command */
        NULL,         /* environment */
        0,            /* spawn flags */
        NULL, NULL,   /* child setup */
        NULL,         /* child pid */
        -1,           /* timeout */
        NULL,         /* cancellable */
        child_ready,  /* callback */
        NULL);        /* user_data */

    /* Connect some signals */
    g_signal_connect(window, "delete-event", gtk_main_quit, NULL);
    g_signal_connect(terminal, "child-exited", gtk_main_quit, NULL);

    /* Put widgets together and run the main loop */
    gtk_container_add(GTK_CONTAINER(window), terminal);
    gtk_widget_show_all(window);
    gtk_main();
}

Ce programme se compile avec la commande suivante :

gcc -O2 -Wall $(pkg-config --cflags vte-2.91) term.c -o term $(pkg-config --libs vte-2.91)

En lançant ./term, nous obtenons ceci :

Terminal simple basé sur VTE
Terminal simple basé sur VTE

Fonctionnalités supplémentaires#

L’étape suivante est de regarder la documentation afin d’ajouter quelques fonctionnalités ou de modifier le comportement. Voici trois exemples.

Couleurs#

Il est possible de redéfinir les 16 couleurs de base avec le code suivant :

#define CLR_R(x)   (((x) & 0xff0000) >> 16)
#define CLR_G(x)   (((x) & 0x00ff00) >>  8)
#define CLR_B(x)   (((x) & 0x0000ff) >>  0)
#define CLR_16(x)  ((double)(x) / 0xff)
#define CLR_GDK(x) (const GdkRGBA){ .red = CLR_16(CLR_R(x)), \
                                    .green = CLR_16(CLR_G(x)), \
                                    .blue = CLR_16(CLR_B(x)), \
                                    .alpha = 0 }
vte_terminal_set_colors(VTE_TERMINAL(terminal),
    &CLR_GDK(0xffffff),
    &(GdkRGBA){ .alpha = 0.85 },
    (const GdkRGBA[]){
        CLR_GDK(0x111111),
        CLR_GDK(0xd36265),
        CLR_GDK(0xaece91),
        CLR_GDK(0xe7e18c),
        CLR_GDK(0x5297cf),
        CLR_GDK(0x963c59),
        CLR_GDK(0x5E7175),
        CLR_GDK(0xbebebe),
        CLR_GDK(0x666666),
        CLR_GDK(0xef8171),
        CLR_GDK(0xcfefb3),
        CLR_GDK(0xfff796),
        CLR_GDK(0x74b8ef),
        CLR_GDK(0xb85e7b),
        CLR_GDK(0xA3BABF),
        CLR_GDK(0xffffff)
}, 16);

Bien que cela ne soit pas visible sur la capture d’écran suivante1, le fond est légèrement transparent :

Rendu des couleurs
Rendu des couleurs

Paramètres divers#

VTE dispose de nombreux réglages. Voici quelques exemples :

vte_terminal_set_scrollback_lines(VTE_TERMINAL(terminal), 0);
vte_terminal_set_scroll_on_output(VTE_TERMINAL(terminal), FALSE);
vte_terminal_set_scroll_on_keystroke(VTE_TERMINAL(terminal), TRUE);
vte_terminal_set_mouse_autohide(VTE_TERMINAL(terminal), TRUE);

Respectivement :

  • désactivation de l’historisation des lignes,
  • pas de défilement en cas de nouvelle sortie,
  • défilement en bas s’il y a interaction de l’utilisateur,
  • cacher le curseur de la souris quand le clavier est utilisé.

Titre de la fenêtre#

Une application peut modifier le titre de la fenêtre en utilisant les séquences de contrôle XTerm (par exemple, avec printf "\e]2;${title}\a"). Pour que ce changement impacte la fenêtre GTK, il faut définir cette fonction :

static gboolean
on_title_changed(GtkWidget *terminal, gpointer user_data)
{
    GtkWindow *window = user_data;
    gtk_window_set_title(window,
        vte_terminal_get_window_title(VTE_TERMINAL(terminal))?:"Terminal");
    return TRUE;
}

Il convient ensuite de la connecter au signal approprié depuis la fonction main() :

g_signal_connect(terminal, "window-title-changed",
    G_CALLBACK(on_title_changed), GTK_WINDOW(window));

Conclusion#

Je n’ai guère besoin de plus étant donné que j’utilise tmux à l’intérieur de chaque terminal. J’ai également implémenté une complétion dynamique permettant de compléter le mot sous le curseur à partir des mots présents dans le terminal courant ou un autre. Cela a nécessité de gérer toutes les fenêtres depuis un processus unique, comme avec urxvtcd.

Il est donc très simple de construire un émulateur de terminal de « zéro »2. Cependant, ce n’est pas forcément une bonne idée. Il est facile de compiler evilvte avec les fonctionnalités que l’on désire et d’arriver à un résultat similaire. Considérez d’abord une telle solution. Honnêtement, je ne sais même plus pourquoi je ne l’ai pas fait.

Mise à jour (02.2017)

evilvte n’a pas été mis à jour depuis 2014. Son support GTK+3 est non fonctionnel. Il ne supporte pas les dernières versions de VTE. Il ne s’agit donc pas d’une bonne recommendation.

Un autre élément crucial est que VTE est une bibliothèque de support pour GNOME Terminal uniquement. Une fonctionnalité qui n’est pas utilisée dans GNOME Terminal ne sera pas implémentée. Elle sera même retirée si elle existe déjà.


  1. La transparence est prise en charge par le « compositor » (Compton dans mon cas). ↩︎

  2. Pour une définition assez flexible de zéro : le gros du travail est effectué par la bibliothèque VTE↩︎