4.4 KiB
layout | title |
---|---|
post | Writing a GNOME Shell extension |
I could not find a tutorial on how to write a GNOME Shell extension, but I wanted to create one for my SWE GLib library to show the current positions of the planets. So I dug into existing (and working) extensions’ source code and made up something. Comments welcome!
GNOME Shell extensions are written in JavaScript and are interpreted by GJS. Using introspected libraries from JavaScript is not a problem for me (see SWE GLib’s Javascript example; it’s not beautiful, but it’s working), but wrapping your head around the Shell’s concept can take some time.
The Shell is a Clutter stage, and all the buttons (including the top-right “Activities” button) are actors on this stage. You can add practically anything to the Shell panel that you can add to a Clutter stage.
The other thing to remember is the lifecycle of a Shell extension. There
are two methods here: either you use an extension controller, or plain
old Javascript functions enable()
and disable()
; I will go on with
the former method.
Anatomy of an extension
The only thing you actually need is an init()
function:
function init(extensionMeta) {
// Do whatever it takes to initialize your extension,
// like initializing the translations.
return new ExtensionController(extensionMeta);
}
Extension controller
So far so good, but what is this extension controller thing? It is an
object which is capable of managing your GNOME Shell extension. Whenever
the extension is loaded, its enable()
method is called; when the
extension is unloaded, the disable()
method gets called.
function ExtensionController(extensionMeta) {
return {
extensionMeta: extensionMeta,
extension: null,
enable: function() {
this.extension = new PlanetsExtension(this.extensionMeta);
Main.panel.addToStatusArea("planets", this.extension, 0, "right");
},
disable: function() {
this.extension.actor.destroy();
this.extension.destroy();
this.extension = null;
}
}
}
This controller will create a new instance of the PlanetsExtension
class and add it to the panel’s right side when loaded. Upon unloading,
the extension’s actor gets destroyed, together with the extension
itself. Also, for safety measures, the extension is set to null
.
As you will see soon, extension.actor
is not created by us, but behind
the scenes by the Shell.
The extension
The extension is a bit more tricky, as, for convenience reasons, it should extend an existing panel widget type.
function PlanetsExtension(extensionMeta) {
this._init(extensionMeta);
}
PlanetsExtension.prototype = {
__proto__ = PanelMenu.Button.prototype,
_init: function(extensionMeta) {
PanelMenu.Button.prototype._init.call(this, 0.0);
this.extensionMeta = extensionMeta;
this.panelContainer = new St.BoxLayout({style_class: 'panel-box'});
this.actor.add_actor(this.panelContainer);
this.actor.add_style_class_name('panel-status-button');
this.panelLabel = new St.Label({text: 'Loading', y_align: Clutter.ActorAlign.CENTER});
this.panelContainer.add(this.panelLabel);
}
};
The only parameter passed to the parent’s _init
function is
menuAlignment
, with the value 0.0
, which is used to position the
menu arrow. (Note: I cannot find any documentation on this, but it
seems that with the value 0.0
, a menu arrow is not added.)
Loading up the extension
Now with the correct import lines added:
const PanelMenu = imports.ui.panelMenu;
const St = imports.gi.St;
const Clutter = imports.gi.Clutter;
The only thing to create now is the metadata.json
file. It contains
compatibility information and, well, some meta data.
{
"shell-version": ["3.18"],
"uuid": "planets@gergely.polonkai.eu",
"name": "Planets",
"description": "Display current planet positions"
}
As soon as this file is ready, you can restart your Shell (Alt-F2, enter
the command r
), and load the extension with e.g. the GNOME Tweak Tool.
This little label showing the static text “Planets” is pretty boring, so let’s add some content.