Gnome Shell est partie intégrante de l’environnement de bureau par défaut sur Ubuntu. Il gère notamment la zone de notitifcation et on peut le contrôler ou y ajouter des fonctionnalités à l’aide d’extensions. Au cours de la mise à jour agitée de ma version d’Ubuntu, j’ai été confronté à un souci d’extension Gnome Shell qui m’a rendu curieux de leur fonctionnement.
Je me suis renseigné sur le sujet et j’ai mis en pratique sur une extension toute simple pour afficher la température de mon CPU et qui remplacera une extension qui en fait trop pour mon besoin. Ce billet raconte ce que j’ai appris en chemin !
Trouver la documentation de base
Les extensions pour GNOME sont écrites en GNOME Javascript (GJS pour faire court), une variante de JavaScript qui propose notamment des API pour manipuler les différents composants de GNOME, et en particulier GNOME Shell.
La documentation est un peu morcelée, et contrairement à ce à quoi je m’attendais, je n’ai pas pu aller sur le site de GNOME et naviguer simplement du genre GNOME Shell > Développeurs > Extensions GNOME Shell. Mon moteur de recherche favori a été d’un grand secours et j’ai fini par trouver ce que je cherchais.
Le plus important est le guide GJS, qu’on retrouve sur un site dédié1. En particulier, ce site propose toute une section dédié au développement d’extensions avec des guides introductifs et c’est ce que j’ai utilisé pour me mettre un pied à l’étrier. J’ai trouvé tout cela clairement expliqué, bien amené et bien présenté.
Pour les détails d’API, cette documentation2 est utile, bien que pas si pratique quand on ne connaît pas exactement ce qu’on cherche.
Comme on le verra tout à l’heure, les extensions ont à disposition un espace de nom global qui donne directement accès à des éléments de GNOME Shell, mais qui se trouve être mal documenté (ou alors encore une fois, je ne l’ai pas trouvé facilement). La meilleure documentation semble être le code lui-même, côté GNOME Shell.
Composer le squelette
Template d’extension
Concrètement, une extension est composée au minimum de deux fichiers : metadata.json
qui décrit l’extension (un identifiant unique, un nom, une description, etc.) et extension.js
le point d’entrée du code de l’extension.
Je ne m’attarde pas sur les métadonnées qui ont assez peu d’intérêt. Il faut juste remplir cela correctement selon les recommandations.
Intéressons-nous plutôt à extension.js
. Le contenu le plus basique ou presque qu’on puisse imaginer est le code ci-dessous, inspiré de la documentation officielle.
Parmi les deux variantes au choix, j’ai choisi d’utiliser celle avec un objet Extension
. On a alors la fonction init
appelée par le gestionnaire d’extension de GNOME Shell pour… initialiser l’extension, ainsi que les méthodes enable
(pour charger l’extension) et disable
(pour faire le ménage derrière) qui lui sont aussi connues. On a là une interface très simple utilisée par le gestionnaire d’extensions pour faire son job.
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
class Extension {
constructor() {
}
enable() {
log(`enabling ${Me.metadata.name}`);
}
disable() {
log(`disabling ${Me.metadata.name}`);
}
}
function init() {
log(`initializing ${Me.metadata.name}`);
return new Extension();
}
Squelette de l’interface
J’ai envie d’indiquer la température dans le panneau des indicateurs, en haut à droite (là où sont l’indicateur de batterie, connectivité réseau, statut de synchronisation de stockage en ligne, etc). Il s’agit donc :
- de causer avec le panneau des indicateurs (l’API le permet, tant mieux !) ;
- lui ajouter un indicateur ;
- prévoir une zone de texte dans cet indicateur pour afficher le texte qu’on souhaite (quelque chose du genre « 54°C »).
Je n’ai besoin de rien de plus : pas de paramètres, pas de menu, pas d’images, rien qu’un petit texte. Cela donne le code suivant.
const Clutter = imports.gi.Clutter;
const St = imports.gi.St;
const Gio = imports.gi.Gio;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
class Extension {
constructor() {
this._indicator = null;
this._temperatureLabel = null;
}
enable() {
this._temperatureLabel = new St.Label({text : "--- °C", y_align: Clutter.ActorAlign.CENTER});
let indicatorName = `${Me.metadata.name} Indicator`;
this._indicator = new PanelMenu.Button(0.0, indicatorName, false);
this._indicator.add_child(this._temperatureLabel);
Main.panel.addToStatusArea(indicatorName, this._indicator);
}
disable() {
this._indicator.destroy();
this._indicator = null;
}
}
function init() {
return new Extension();
}
Le fonctionnement est très similaire à n’importe quelle bibliothèque graphique : on appelle ou crée des composants, on crée la structure en ajoutant des composants comme enfants, on dispose d’options pour le packing, etc.
Ajouter l'affichage de la température CPU
Maintenant, il ne manque plus que l’essentiel : régulièrement relever la température depuis le capteur adéquat et mettre à jour l’affichage en conséquence.
Le capteur qui m’intéresse est mappé vers le fichier /sys/class/hwmon/hwmon4/temp1_input
1 qu’il s’agit alors de lire. Je me suis inspiré du code de mon extension actuelle : je lis le fichier de manière asynchrone puis mets à jour l’indicateur avec son contenu. Ensuite, un callback appelé toutes les secondes s’occupe de mettre à jour régulièrement.
Le code (dont la propreté n’est pas garantie, je connais très peu JavaScript) n’est pas compliqué. La principale difficulté à l’écriture consiste à identifier les fonctions utiles dans les différentes API.
const Clutter = imports.gi.Clutter;
const St = imports.gi.St;
const Gio = imports.gi.Gio;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
const Mainloop = imports.mainloop;
const delayMs = 1000;
const sensorFilename = "/sys/class/hwmon/hwmon4/temp1_input";
const indicatorName = `${Me.metadata.name} Indicator`;
const defaultText = "--- °C";
class Extension {
constructor() {
this._indicator = null;
this._temperatureLabel = null;
this._callback = null;
}
enable() {
this._temperatureLabel = new St.Label({text : defaultText, y_align: Clutter.ActorAlign.CENTER});
this._indicator = new PanelMenu.Button(0.0, indicatorName, false);
this._indicator.add_child(this._temperatureLabel);
Main.panel.addToStatusArea(indicatorName, this._indicator);
this._callback = Mainloop.timeout_add(delayMs, this._update.bind(this));
}
disable() {
this._indicator.destroy();
this._indicator = null;
Mainloop.source_remove(this._callback);
this._callback = null;
}
_update() {
const sensorFile = Gio.File.new_for_path(sensorFilename);
sensorFile.load_contents_async(null, (source, result) => {
let [success, sensorFileContent, _] = source.load_contents_finish(result);
let labelText = success ? this._format(sensorFileContent) : defaultText;
this._temperatureLabel.set_text(labelText);
});
return true;
}
_format(content) {
const value = content.toString().slice(0, 2);
return `${value} °C`;
}
}
function init() {
return new Extension();
}
- La manière dont le système s’occupe de mapper les capteurs vers le système de fichier est un sujet à part entière que je ne développerai pas dans ce billet.↩
Il n’est pas possible de conclure le billet sans montrer le résultat.
Quoi qu’il en soit, je me retrouve avec une extension qui répond pile-poil à mon besoin, légère et en laquelle j’ai confiance, sans compter les connaissances supplémentaires acquises sur le chemin. Que demander de plus ?
Minitiature du tutoriel : logo de Gnome (source).