Passage au gestionnaire de fenêtres i3

Vincent Bernat

J’utilise awesome depuis 10 ans. Il s’agit d’un gestionnaire de fenêtres à tuiles, configurable et extensible avec le langage Lua. L’utilisation d’un langage de programmation polyvalent pour configurer chaque aspect est une arme à double tranchant. Par paresse et à cause de la difficulté apparente d’adapter ma configuration d’environ 3000 lignes aux nouvelles versions, je suis resté coincé avec la version 3.4, dont la dernière version date de 2013.

i3 sur deux écrans
Configuration en double écran faisant tourner i3, Emacs, quelques terminaux, dont une console à la Quake, Firefox, Polybar comme barre de statut et Dunst pour afficher les notifications.

Le gestionnaire de fenêtres n’est qu’une partie d’un environnement de bureau. Il existe plusieurs options pour les autres composants. Je les présente également dans ce billet.

i3 : le gestionnaire de fenêtres#

i3 est un gestionnaire de fenêtres à tuiles (« tiling window manager ») minimaliste. Sa documentation peut être lue du début à la fin en moins d’une heure. i3 organise les fenêtres dans un arbre. Chaque nœud intermédiaire contient une ou plusieurs fenêtres et possède une orientation et une disposition. Ces informations permettent d’arbitrer la position des fenêtres. i3 propose trois dispositions : fractionnée (split), empilée (stacking) et à onglets (tabbed). Elles sont présentées dans la capture d’écran ci-dessous :

Exemple de dispositions
Démonstration des dispositions dans i3. Le conteneur principal est divisé horizontalement. Le premier enfant est divisé verticalement. Le deuxième utilise des onglets. Le dernier est empilé.
Représentation en arbre de la capture d'écran
précédente
Représentation en arbre de la capture d'écran précédente.

La plupart des autres gestionnaires de fenêtres à tuiles, y compris awesome, utilisent des dispositions prédéfinies. Ils présentent généralement une zone importante pour la fenêtre principale et une seconde zone découpée entre les autres fenêtres. Ces dispositions peuvent être légèrement modifiées, mais vous vous en tenez généralement à une ou deux d’entre elles. Lorsqu’une nouvelle fenêtre est ajoutée, le comportement est assez prévisible. De plus, vous pouvez parcourir les différentes fenêtres sans trop réfléchir car elles sont ordonnées.

i3 est plus flexible grâce à sa capacité à construire n’importe quelle disposition à la volée, mais cela peut déconcerter car vous devez visualiser l’arbre dans votre tête. Au début, il n’est pas rare de se retrouver avec une arborescence complexe comportant de nombreux conteneurs imbriqués superflus. De plus, vous devez naviguer dans les fenêtres à l’aide de directions. Il faut un certain temps pour s’y habituer.

J’ai configuré une disposition en deux parties pour Emacs et quelques terminaux, mais la plupart des autres espaces de travail utilisent des onglets. Je n’utilise pas la disposition par empilement. Vous pouvez trouver de nombreux scripts essayant d’émuler d’autres gestionnaires de fenêtres à tuiles, mais j’ai essayé de ne pas me laisser tenter dès le début et de prendre le temps de m’habituer. i3 peut également sauvegarder et restaurer les dispositions, ce qui est une fonctionnalité assez puissante.

Ma configuration est proche de la configuration par défaut avec moins de 200 lignes.

le compagnon i3 : le chaînon manquant#

La philosophie de i3 est de rester minimaliste et de laisser l’utilisateur implémenter les fonctionnalités manquantes en utilisant un protocole de communication (« IPC ») :

Ne pas ajouter de complexité supplémentaire lorsque cela peut être évité. Nous sommes généralement satisfaits de l’ensemble des fonctionnalités de i3 et nous nous concentrons plutôt sur la correction des bogues et la maintenance de la stabilité. De nouvelles fonctionnalités ne seront donc envisagées que si les avantages l’emportent sur la complexité supplémentaire, et nous encourageons les utilisateurs à mettre en œuvre des fonctionnalités en utilisant le protocole IPC chaque fois que cela est possible.

Introduction au gestionnaire de fenêtres i3

Bien que cela ne soit pas aussi versatile qu’un langage intégré, c’est suffisant pour de nombreux cas. De plus, comme les fonctionnalités de haut niveau sont souvent subjectives, les déléguer à de petits morceaux de code faiblement couplés les rend plus faciles à maintenir. Des bibliothèques existent à cet effet dans plusieurs langages. Les utilisateurs ont publié de nombreux scripts pour étendre i3 : mise en page automatique et promotion de fenêtres pour imiter le comportement d’autres gestionnaires de fenêtres, remplacement de fenêtres pour placer une nouvelle application au-dessus du terminal qui la lance et passage d’une fenêtre à l’autre avec Alt+Tab.

Au lieu de maintenir un script pour chaque fonctionnalité, j’ai tout centralisé dans un seul processus Python, i3-companion, utilisant asyncio et la bibliothèque i3ipc-python. Chaque fonctionnalité est confinée dans une fonction. Il implémente les composants suivants :

Rendre un espace de travail exclusif à une application
Lorsqu’un espace de travail contient Emacs ou Firefox, j’aimerais que les autres applications se placent dans un autre espace de travail, à l’exception du terminal qui est autorisé à « envahir » n’importe quel espace de travail. La fonction workspace_exclusive() surveille les nouvelles fenêtres et les déplace si nécessaire vers un espace de travail vide ou contenant la même application déjà en cours d’exécution.
Implémenter une console Quake
La fonction quake_console() implémente une console déroulante disponible depuis n’importe quel espace de travail. Elle peut être activée avec Mod+`. Elle utilise le mécanisme de scratchpad.
Alterner entre les deux derniers espaces de travail sur un écran
Avec la commande workspace back_and_forth, on peut demander à i3 de passer à l’espace de travail précédent. Cependant, cette fonctionnalité n’est pas limitée à l’écran actuel. Je préfère avoir un raccourci clavier pour passer à l’espace de travail sur l’écran d’à côté et un autre pour passer à l’espace de travail précédent sur le même écran. Ce comportement est mis en place dans la fonction previous_workspace() en conservant un historique par écran.
Créer un nouvel espace de travail vide ou déplacer une fenêtre vers un espace de travail vide
Pour créer un nouvel espace de travail vide ou y déplacer une fenêtre, vous devez trouver un emplacement non utilisé et invoquer le raccourci pour workspace number 4 ou move container to workspace number 4. La fonction new_workspace() trouve un emplacement libre et l’utilise comme espace de travail cible.
Redémarrer certains services lors d’un changement de configuration des écrans
Lors de l’ajout ou de la suppression d’un écran, certaines actions doivent être exécutées : rafraîchir le fond d’écran, redémarrer certains composants incapables d’adapter leur configuration par eux-mêmes, etc. i3 déclenche un événement à cet effet. La fonction output_update() fusionne plusieurs événements consécutifs et vérifie s’il y a un réel changement avec la bibliothèque de bas niveau xcffib.

Je détaillerai les autres fonctionnalités au fur et à mesure de ce billet. Concernant la partie technique, chaque fonction est décorée avec les événements auxquels elle doit réagir :

@on(CommandEvent("previous-workspace"), I3Event.WORKSPACE_FOCUS)
async def previous_workspace(i3, event):
    """Go to previous workspace on the same output."""

La classe d’évènements CommandEvent() correspond à ma façon d’envoyer une commande au compagnon, en utilisant soit i3-msg -t send_tick ou en liant un raccourci à une commande nop. Cette dernière est utilisée pour éviter d’invoquer un shell et un processus i3-msg juste pour envoyer un message. Le compagnon écoute les événements liés aux raccourcis clavier et vérifie si c’est une commande nop.

bindsym $mod+Tab nop "previous-workspace"

Il existe d’autres décorateurs pour éviter la duplication du code : @debounce() pour fusionner plusieurs appels consécutifs, @static() pour définir une variable statique, et @retry() pour relancer une fonction en cas d’échec. Le script entier fait un peu plus de 1000 lignes. Je pense que cela vaut la peine de le lire car je suis assez satisfait du résultat. 🦚

Mise à jour (07.2022)

Daniel Pereira a écrit wmcompanion, un moniteur d’événements pour les utilisateurs de gestionnaires de fenêtres minimaux, inspiré par i3-companion. C’est un moteur générique pour construire votre propre compagnon. Il n’a pas de support intégré pour les événements i3, mais sa conception modulaire permet à un utilisateur motivé de l’ajouter avec quelques lignes de code.

dunst : le démon de notification#

Contrairement à awesome, i3 ne dispose pas d’un système de notification intégré. Dunst est un démon de notification léger. J’utilise une version modifiée avec le support HiDPI pour X11 et la recherche récursive des icônes. Le compagnon i3 a une fonction auxiliaire, notify(), pour envoyer des notifications en utilisant DBus. container_info() et workspace_info() l’utilisent pour afficher des informations sur le conteneur ou l’arbre d’un espace de travail.

Notification montrant l'arbre i3 d'un espace de
travail
Notification montrant l'arbre i3 d'un espace de travail

polybar : la barre de statut#

i3 inclut i3bar, une barre de statut polyvalente, mais j’ai opté pour Polybar. Un script exécute une instance pour chaque écran.

Le premier module est le support intégré pour les espaces de travail i3. Pour ne pas avoir à se rappeler quelle application est en cours d’exécution dans un espace de travail, le compagnon i3 renomme les espaces de travail pour inclure une icône pour chaque application. Cette opération est effectuée par la fonction workspace_rename(). Les icônes proviennent du projet Font Awesome. Je maintiens manuellement la correspondance entre les applications et les icônes. C’est un peu lourd, mais c’est très joli.

Espaces de travail dans Polybar
Les espaces de travail d'i3 dans Polybar

Pour le CPU, la mémoire, la luminosité, la batterie, le disque et le volume sonore, je m’appuie sur les modules intégrés. Le script lançant Polybar génère la liste des systèmes de fichiers à surveiller et ils ne sont affichés que lorsque l’espace disponible est faible. L’icône de la batterie devient rouge et clignote lentement lorsqu’elle est à court d’énergie. Consultez ma configuration de Polybar pour plus de détails.

Divers modules pour Polybar
Polybar affiche diverses informations : utilisation du CPU, utilisation de la mémoire, luminosité de l'écran, état de la batterie, état du bluetooth (avec un casque connecté), état du réseau (connecté à un réseau sans fil et à un VPN), état des notifications et volume sonore.

Pour les statuts du Bluetooh, du réseau et des notifications, j’utilise le module ipc de Polybar : la prochaine version permet de recevoir un texte arbitraire via le canal IPC. Le module est défini avec une seule commande à exécuter au démarrage pour restaurer le dernier statut.

[module/network]
type = custom/ipc
hook-0 = cat $XDG_RUNTIME_DIR/i3/network.txt 2> /dev/null
initial = 1

Il peut être mis à jour avec la commande polybar-msg action "#network.send.XXXX". Dans le compagnon i3, le décorateur @polybar() prend la chaîne retournée par une fonction et pousse la mise à jour à travers la chaussette IPC.

Le compagnon i3 réagit aux signaux DBus pour mettre à jour les icônes Bluetooth et réseau. Le décorateur @on() accepte un objet DBusSignal():

@on(
    StartEvent,
    DBusSignal(
        path="/org/bluez",
        interface="org.freedesktop.DBus.Properties",
        member="PropertiesChanged",
        signature="sa{sv}as",
        onlyif=lambda args: (
            args[0] == "org.bluez.Device1"
            and "Connected" in args[1]
            or args[0] == "org.bluez.Adapter1"
            and "Powered" in args[1]
        ),
    ),
)
@retry(2)
@debounce(0.2)
@polybar("bluetooth")
async def bluetooth_status(i3, event, *args):
    """Update bluetooth status for Polybar."""

Le milieu de la barre est occupé par la date et la météo. Cette dernière utilise également le mécanisme IPC, mais la source est un script Python déclenché régulièrement.

Date et météo dans Polybar
Date actuelle et prévisions météorologiques pour la journée dans Polybar. Les données sont récupérées avec l'API OpenWeather.

Je n’utilise pas la zone de notification intégrée à Polybar. Les icônes sont généralement hideuses et se comportent toutes différemment. Il y a quelques années, Gnome a supprimé la zone de notification. La plupart des problèmes sont résolus par le protocole Status Notifier Item basé sur DBus. Toutefois, Polybar ne prend pas en charge celui-ci. L’implémentation des icônes Bluetooth et réseau, y compris l’affichage des notifications en cas de changement, prend environ 200 lignes dans le compagnon i3. J’ai pu apprendre un peu comment fonctionne DBus et j’obtiens exactement les informations que je veux.

picom : le compositeur#

J’aime avoir des terminaux légèrement transparents et réduire l’opacité des fenêtres inactives. Ces tâches nécessitent un compositeur1. picom est un compositeur léger. Il fonctionne bien dans mon cas, mais il peut nécessiter quelques ajustements en fonction de votre carte graphique2. Contrairement à awesome, i3 ne gère pas la transparence, donc le compositeur doit décider l’opacité de chaque fenêtre. Consultez ma configuration pour plus de détails.

systemd : le gestionnaire de services#

J’utilise systemd pour démarrer i3 et les différents services qui l’entourent. Mon script xsession ne définit que quelques variables d’environnement et laisse systemd s’occuper de tout le reste. Jetez un coup d’œil à cet article de Michał Góral pour en connaître les raisons. Notamment, chaque composant peut être facilement redémarré et leurs sorties ne sont pas mélangées dans le fichier ~/.xsession-errors3.

J’utilise un démarrage en deux étapes : i3.service dépend de xsession.target pour démarrer les services avant i3 :

[Unit]
Description=X session
BindsTo=graphical-session.target
Wants=autorandr.service
Wants=dunst.socket
Wants=inputplug.service
Wants=picom.service
Wants=pulseaudio.socket
Wants=policykit-agent.service
Wants=redshift.service
Wants=spotify-clean.timer
Wants=ssh-agent.service
Wants=weather.service
Wants=weather.timer
Wants=xiccd.service
Wants=xsettingsd.service
Wants=xss-lock.service

Ensuite, i3 active la seconde étape via i3-session.target:

[Unit]
Description=i3 session
BindsTo=graphical-session.target
Wants=wallpaper.service
Wants=wallpaper.timer
Wants=polybar.service
Wants=i3-companion.service
Wants=misc-x.service

Jetez un œil à mes fichiers de configuration pour plus de détails.

rofi : le lanceur d’applications#

Rofi est un lanceur d’applications. Son apparence peut être modifiée en utilisant un langage semblable à CSS et il est livré avec plusieurs thèmes. Regardez ma configuration pour le mien.

Rofi en lanceur d'applications
Rofi en lanceur d'applications

Il peut également faire office de menu. J’ai écrit un script pour contrôler un lecteur multimédia et un autre pour sélectionner le réseau sans fil. C’est une application plutôt flexible.

Rofi pour sélectionner le réseau sans
fil
Rofi pour sélectionner le réseau sans fil

xss-lock et i3lock : le verrouillage de la session#

i3lock permet de verrouiller la session. xss-lock l’invoque de manière fiable en cas d’inactivité ou avant une mise en veille. Pour l’inactivité, il utilise les événements XScreenSaver. Les délai est configuré via la commande xset s. Le verrouillage peut être instantané avec xset s activate. Les applications X11 savent comment le désactiver. J’ai également développé une petite application de fondu au noir qui est exécutée 20 secondes avant le verrouillage pour me donner une chance de bouger la souris si je ne suis pas absent4. Jetez un coup d’oeil à mon script de configuration.

Démonstration accélérée de xss-lock, xss-dimmer et i3lock.

Mise à jour (12.2021)

Je suis passé à XSecureLock en utilisant un script Python pour personnaliser l’apparence de l’écran de veille en y incluant une horloge et la météo. Voir l’article « Économiseur d’écran personnalisé avec XSecureLock ».

Les autres composants#

  • autorandr est un outil qui détecte les écrans connectés, les compare à un ensemble de profils et les configure avec xrandr.

  • inputplug exécute un script pour chaque nouvelle souris et clavier branchés. Regardez ma configuration pour avoir une idée des possibilités.

  • xsettingsd fournit des paramètres aux applications X11, un peu comme xrdb mais il notifie les applications des changements. L’utilisation principale est de configurer les paramètres Gtk et DPI. Lisez mon article sur le support HiDPI sur Linux avec X11 pour les détails.

  • Redshift ajuste la température des couleurs de l’écran en fonction de l’heure de la journée.

  • maim est un utilitaire pour faire des captures d’écran. J’utilise Prt Scn pour déclencher une capture d’écran d’une fenêtre ou d’une zone spécifique et Mod+Prt Scn pour capturer le bureau entier dans un fichier. Consultez le script associé pour plus de détails.

  • J’ai une collection de fonds d’écran que je fais tourner toutes les heures. Un script les sélectionne avec un algorithme neuronal particulièrement élaboré et les assemble sur les configurations multi-écrans. Le fond d’écran sélectionné est réutilisé par i3lock.


  1. Outre le côté esthétique, un compositeur permet également également de fluidifier le rendu vidéo. ↩︎

  2. Ma configuration fonctionne pour les cartes graphiques Intel des générations Haswell (2014) et Whiskey Lake (2018). Elle fonctionne aussi pour les cartes AMD de la génération Polaris (2017) ↩︎

  3. Il n’est pas possible de gérer plusieurs serveurs X de cette façon, par exemple :0 et :1. Dans une première implémentation, j’ai essayé de paramétrer chaque service avec le serveur associé, mais c’est inutile : il n’y a qu’une seule session utilisateur DBus et de nombreux services en dépendent. Par exemple, il n’est pas possible d’exécuter deux démons de notification. ↩︎

  4. Je n’ai découvert que plus tard que XSecureLock livre une telle application avec une implémentation similaire. Mais le mien a un compte à rebours ! Il permet également de faire un fondu vers une image de fond. ↩︎