One of the most popular features of GNOME is that it enables users to build extensions for their shell to increase its basic functionality. You can think of these extensions as add-ons to the GNOME shell. Here’s more on developing GNOME shell extensions.
One of the many choices available to a Linux user is that of the desktop environment. Among the many available, GNOME stands out as one of the most prominent. The GNOME shell has a very user friendly interface, which makes it popular amongst users.
First things first. GNOME extensions are written in Javascript. This is actually Mozilla’s special version of Javascript, which is based on Spidermonkey and is widely known as GJS. A working knowledge of regular Javascript is enough to begin extension development.
Let us have a look at what the components of a GNOME extension are.
Metadata.json: This file contains data about the extension details like the extension name, description, identifier and compatibility information (which versions of the GNOME shell are compatible with the extension).
Extension.js: This is the GNOME-Javascript(GJS) file, which will store the logic that controls the extension. This file must contain the following three functions.
Init (metadata) This is called to initialise the extension. The metadata argument is the data stored in the metadata.json file, along with some additional details like the absolute path to your extension.
enable This is called when the extension is enabled. This function stores the actions to be performed when the extension is enabled, and this generally is the function in which the logic for extensions is placed.
disable This is called when the extension is disabled. It generally contains code, related to clean up activities, which stops whatever actions the extensions perform and restores the system to a state such as when the extension was never there.
There is also an optional stylesheet.css file, which is used to style various attributes of the extension, like buttons or displays.
Creating an extension
When you write your extension, the files related to the extension should be placed in the ~/.local/share/gnome-shell/extensions directory. In the same directory, you will be able to see various other extensions that exist on your system, and it may be a good idea to look at some of them to get a better understanding of how they work. You will also see that the already existing extensions will have the above mentioned files (extension.js, stylesheet.css, metadata.json) .
Now, let us start building our extension. Although it is not mandatory (you can write the extension from scratch), GNOME 3 has a bash command that helps create new extensions. Let’s use the following command to create a draft of our extension:
gnome-shell-extension-tool --create-extension
When you run the above command, you will be prompted to enter the following metadata.
- Name Name of the extension.
- Description Description of the extension.
- Uuid- This is a unique identifier for the extension. It should be in an email address format, like foo@bar.com. In general, as long as the uuid can be used to create a subdirectory, it will be regarded as a valid uuid.
Now, you will be told that your extension has been created and the path to it will also be displayed. You are free to make the required changes to your extension. Next, activate the extension by using the GNOME tweak-tool (if you have not already installed it, you can download and install it from your distro’s software manager). Under the tweak tool, go to shell extensions and turn on your extension there. If you cannot see it, restart your GNOME shell (Alt + F2 and type ‘r’, and then press Enter) to force load all the extensions.
You will see a new icon in the GNOME tray (top right corner of your screen) in the shape of gears. If you click on it, your extension will display ‘Hello world’.
This is the default extension, created by the GNOME shell extension tool. We must modify the files located in ~/.local/share/gnome-shell/extensions, in the folder with the same name as the UUID you entered earlier.
Understanding the code
Let’s look at the code that the GNOME shell extension-tool created, by first examining the extension.js file:
const St = imports.gi.St; const Main = imports.ui.main; const Tweener = imports.ui.tweener; let text, button; function _hideHello() { Main.uiGroup.remove_actor(text); text = null; } function _showHello() { if (!text) { text = new St.Label({ style_class: 'helloworld-label', text: "Hello, world!" }); Main.uiGroup.add_actor(text); } text.opacity = 255; let monitor = Main.layoutManager.primaryMonitor; text.set_position(Math.floor(monitor.width / 2 - text.width / 2), Math.floor(monitor.height / 2 - text.height / 2)); Tweener.addTween(text, { opacity: 0, time: 2, transition: 'easeOutQuad', onComplete: _hideHello }); } function init() { button = new St.Bin({ style_class: 'panel-button', reactive: true, can_focus: true, x_fill: true, y_fill: false, track_hover: true }); let icon = new St.Icon({ icon_name: 'system-run', icon_type: St.IconType.SYMBOLIC, style_class: 'system-status-icon' }); button.set_child(icon); button.connect('button-press-event', _showHello); } function enable() { Main.panel._rightBox.insert_child_at_index(button, 0); } function disable() { Main.panel._rightBox.remove_child(button); }
In the above code, St has been imported because that is the library that allows us to create UI elements. Main is the instance of a class that has all the UI elements, and we add all UI elements to this main instance. Tweener is responsible for handling all the animations that might be performed. Lets look at what each of the functions in the above code does.
Init() – Create a button for the top panel by passing a map of properties from St.bin. In the map, set the button to be reactive to mouse clicks an set the style class of the button. Set it such that the button can be focused by keyboard navigation, and that the button only fills x-space and not y-space. Create an icon, and set the icon as a child of the button. Finally, bind the button press event, to call the function _showHello, which is responsible for displaying the Hello world message.
- Enable() – Add the button we created earlier to the right panel of the top panel.
- Disable() – Remove the button from the right panel.
Next, let us look at the metadata.json file:
{"shell-version": ["3.4.1"], "uuid": "foo@bar", "name": "hello", "description": "OSFY extension"}
As you can see, in the above code, the metadata.json file stores data about your extension. Details stored by default are the shell-version for which the extension was created. (This value will vary depending on your version of the GNOME shell. Also, the syntax for extensions will be different if you are running a GNOME version before 3.2.) Other details stored in the metadata.json file are the name, UUID and description.
And in the end, let’s look at stylesheet.css:
.helloworld-label { font-size: 36px; font-weight: bold; color: #ffffff; background-color: rgba(10,10,10,0.7); border-radius: 5px; padding: .5em; }
You can change various styles associated with your extension by changing what’s needed in this file.
Looking Glass
This is the tool used to debug your GNOME shell extensions and Javascript code. To open Looking Glass, press Alt+F2, type in ‘lg’ and hit Enter. The Looking Glass window will open up.
Looking Glass is divided into tabs: Evaluator, Windows, Memory and Extensions. The Evaluator window is where you can type in random Javascript code. You can also use the picker at the left corner of Looking Glass, which you can use to select various elements of the GNOME shell and find out the element’s name. The Windows tab shows which windows are active, while the Extensions tab shows the various extensions running on your system, along with any errors. If any extension runs into some error, the error will be displayed here.
Customising your extension
Let us now modify your standard ‘Hello world’ extension. Let’s try and modify the type of icons displayed in the right panel. By default, the icons displayed in the right panel are symbolic icons, and your extension will change them to full colour icons. Let us look at the code to do this:
const St = imports.gi.St; const Main = imports.ui.main; const Tweener = imports.ui.tweener; const PopupMenu = imports.ui.popupMenu; function init() { } function disable() { let children = Main.panel._rightBox.get_children(); for (let i = 0; i < children.length; i++) { if (children[i] && children[i]._delegate._iconActor) { children[i]._delegate._iconActor.icon_type = St.IconType.SYMBOLIC; children[i]._delegate._iconActor.style_class = 'system-status-icon'; } } } function enable() { let children = Main.panel._rightBox.get_children(); for (let i = 0; i < children.length; i++) { if (children[i] && children[i]._delegate._iconActor) { children[i]._delegate._iconActor.icon_type = St.IconType.FULLCOLOR; children[i]._delegate._iconActor.style_class = 'color-status-button'; } } }
For the sake of simplicity, I have left the init function empty, but all your initialisations should be done inside that function. In the Enable function, you get all elements in the right panel with the Main.panel._rightBox.get_children() function; then you loop over all children, changing their style class from symbolic icon to full colour. In the Disable function, do the same with one differencechange the icon style from full colour to symbolic.
As we have seen, the GNOME shell allows you to extend its functionality by adding extensions. Building these extensions is easier with tools like the GNOME shell extension-tool, and Looking Glass.
You can learn a lot about building extensions by reading the code of existing extensions. You will find a few of them on GNOME’s GIT under a module called gnome-shell-extensions. Also remember that Looking Glass is your friend. Get familiar with it and use it to solve many a coding error. Further documentation can be found on the GNOME website https://live.gnome.org/GnomeShell/Extensions. You have also seen how easy it is to customise the GNOME shell to suit your needs. GNOME extensions let users take control, making it easier to achieve the perfectly customised system.