Requirements and tips for getting your GNOME Shell Extension approved

If you’ve written and submitted a GNOME Shell Extension, you’ve probably run across a reviewer saying that you shouldn’t do something, not even knowing what you shouldn’t have done beforehand. I’m sorry, the Shell team (including myself) hasn’t really done a good job documenting what a good GNOME Shell Extension should do, and we reviewers constantly run across the same mistakes in review.

I’m going to hopefully change this for the better by documenting most of what us reviewers look for when reviewing code. This isn’t meant to be an exhaustive list – even if you do everything in here, we still may reject your extension for other reasons. If it’s a code reason and it’s a common one, I’ll update the list.

This is mostly about GNOME-specific gotchas. Please attempt to write straightforward and clean code. Unravelling spaghetti is not fun for any of the extension reviewer team.

Don’t do anything major in init()

init() is there to have anything that needs to happen at first-run, like binding text domains. Do not make any UI modifications or setup any callbacks or anything in init(). Your extension will break, and I will reject it. Do any and all modifications in enable().

Undo all UI modifications in disable()

If you make any UI modifications in enable(), you need to undo them in disable(). So, if you add an icon to the top bar when your extension is enabled, you need to remove it. Simple. Failure to do so will be an immediate rejection.

Remove all possible callback sources

If you’re writing an extension, you must disconnect all signals and remove all Mainloop sources. Failure to do so will be an immediate rejection. So, if you connect to a signal using global.display.connect('window-created', onWindowCreated);, you need to disconnect it:

let _windowCreatedId;

function enable() {
    _windowCreatedId = global.display.connect('window-created', onWindowCreated);
}

function disable() {
    global.display.disconnect(_windowCreatedId);
}

The same goes for any timeouts or intervals added with the Mainloop module. Use Mainloop.source_remove to make sure your source is removed.

Extensions targeted for GNOME 3.2 cannot use GSettings

It’s unfortuate, but true. Extensions that target GNOME 3.2 cannot use GSettings when uploaded to the repository. It will crash the Shell. There are new APIs in GNOME 3.4 that allow us to work around this. The technique used most often is to have a bunch of const statements at the top of the shell that the user can modify.

A quick aside, a libsoup gotcha

function buttonPressed() {
    // This code will crash the Shell, do not use it!
    let session = new Soup.SessionAsync();
    let message = Soup.form_request_new_for_hash('GET', 'http://example.com', {'beans': 'sauce'});
    session.queue_message(message, function() {
        log('soup is so easy!');
    });
}

While we really shouldn’t be crashing here, it’s a hard issue to fix. The technical reason is that when the session gets garbage collected, it calls back into all callbacks to let them know that the request is cancelled. Because the session is in destruction, this will crash gjs when it tries to find the object to pass to the callback.

A quick fix is to prevent the session from being garbage collected. Move it outside the function to get the correct behaviour.

const session = new Soup.SessionAsync();

// This makes the session work under a proxy. The funky syntax here
// is required because of another libsoup quirk, where there's a gobject
// property called 'add-feature', designed as a construct property for
// C convenience.
Soup.Session.prototype.add_feature.call(session, new Soup.ProxyResolverDefault());

function buttonPressed() {
    let message = Soup.form_request_new_for_hash('GET', 'http://example.com', {'beans': 'sauce'});
    session.queue_message(message, function() {
        log('soup is so easy!');
    });
}

And now, these aren’t required, but it helps me tremendously when reading extensions. Following these tips will likely get your extension approved faster.

Consistent and appropriate indentation

If you indent your code properly and consistently, I don’t have to figure out which brace corresponds to what. Makes it easier for me to read code. It also helps if you follow the GNOME Shell’s indentation and brace style (4-space indents, braces on the same line), but I don’t want to start a flamewar in the comments section. If you like another indentation and brace style, use it. The important thing is that you are consistent.

Removing dead and commented out code

If you know that some code you have is dead, remove it. If you’re using a modern VCS like git, you should be able to get it back at any point. Don’t leave a bunch of commented code in there for me to digest through.

More Extension API breaks/improvements

Since GNOME Shell Extensions launched, we’ve been overwhelmed at the response from the community. You guys are awesome, and I’m constantly impressed at what things you guys are doing with the Shell. I’ve been a bit behind on reviewing extensions and adding requested features to the website (I still need to get started on search and investigating some pretty big bugs), and I apologize. I recently landed a bunch of features to the review system to make it easier for me to review your extensions, such as rewriting the diff system. Hopefully things will go a lot faster now.

When Owen Taylor first suggested the project, we worked out some basic sketches and built an overview for the site, but we didn’t want to change the extension API until we knew what extensions were going to need or want to do. As I’ve said before, I felt that we needed a co-operative live enable/disable system for asthetic reasons: the Shell isn’t the fastest thing at restarting, and a user having to spend ten or fifteen seconds to revert your system to the previous state after deciding that an extension sucks makes for a pretty poor experience. That was the only API break that I inserted into the extension system for 3.2, and it was purely to make sure that your extensions were presented in the best way possible.

Multi-file extensions

The extension system in the shell really wasn’t designed for extensions that had multiple files. The many people that built the system had no idea that this clever hack would be commonplace. While I hate to break API, I’d say it’s for the better. Introducing the “extension object”. Previous to now, we would construct a meta object from your metadata.json, install some random things into it and pass it to you, the extension, through the init method. Over time, we inserted more and more on the metadata object where it was this jumbled bag of things that the extension system added, and things that were in the metadata.json file. While adding a few more keys for the prefs tool below, I finally decided it was worth a redesign.

Here’s an example of the new system in action:

const Gio = imports.gi.Gio;

let extension = imports.misc.extensionUtils.getCurrentExtension();
let convenience = extension.imports.convenience;

Gio.app_info_launch_default_for_uri(extension.dir.get_uri(), global.create_app_launch_context());

let metadata = extension.metadata;
Gio.app_info_launch_default_for_uri(metadata.url, global.create_app_launch_context());

function init() {}
function enable() {}
function disable() {}

Of course, you shouldn’t do these things outside of any user interaction, but a concise, imperfect example is better than one that misses the point by building an StButton, sticking it somewhere on enable(), and then removing it on disable().

You might notice that you don’t get the extension object passed to you – you go out and grab it. If you’re curious as to how we know what extension you are, there’s some clever trickery involved.

What’s on this object? A bunch of stuff that used to be on the metadata object: state, type, dir, path, etc. When we introduce new APIs specifically designed for the ease of extension, this is that place we will put them. metadata is now a pristine and untampered representation of your metadata.json file, and we’re going to keep it that way.

Preferences

One thing that we don’t really have an area for in the current 3.2 API is preferences. Unfortunately, GSettings was incompatible with extensions installed into your home directory (like extensions installed from GNOME Shell Extensions do). We couldn’t get this fixed in time for 3.2, but Ryan Lortie spent a lot of his personal time fixing this, and I thank him graciously. We now have a usable API for settings from extensions. It requires a bit more code, as you can see in gnome-shell-extensions’s convenience.js.

I spend a lot of my time looking at the code for extensions. Extensions have been shipping their own Python or gjs scripts that stick up a GTK+ dialog. Someitimes they built a configuration UI inside the extension with St. Sometimes they installed a .desktop file so that they would show up in the Applications section of the Shell. Sometimes they would insert a button to launch the script, or maybe they made users right-click on their item in the toolbar.

When Owen and I sat down to discuss this, we both decided we need some consistent way for extensions to be configured. Introducing the “GNOME Shell Extension Preferences” tool. It’s a new entry point to your extension – prefs.js. Here’s a simple prefs.js file adding support to the Alternate Tab extension.

... and here's a screenshot!

I didn’t make it too fancy, and it’s entirely possible that Giovanni may want to make a better UI.

Your entry point is a function labeled buildPrefsWidget(), and it should return some sort of GTK+ widget. Whatever you return from there will get inserted into preferences widget.

The combobox is for switching between extensions, and it’s ours. Otherwise, go crazy… the world below is your canvas. As usual, try to make it useful and pretty, but the reviewers on the extensions repo can’t and won’t discriminate against ugly UIs. There is still a policy for this, though: the widgets that you put in the preferences UI have to be related to preferences (I think I’ll allow version strings and other things though), and you should not walk up the GTK+ widget tree beyond your preferences widget (to adjust the combobox or manipulate any UI that is not yours). I’m looking forward to the neat things that you guys do with this!

“But how do I launch it?”

Oh, right. The tool is marked NoDisplay, so it won’t show up in the Applications menu. This is intentional. I’m currently working on a patch to SweetTooth so you can launch it directly from the website, provided the browser-plugin is installed correctly.

“Something broke, and I don’t think it’s my fault!”

I’m human. I make mistakes. You can let me know I was or am being a moron by filing a bug for GNOME Shell, or filing a bug for SweetTooth. While I try to respond to everyone’s mail and read all blog comments, I regularly check my task list on these two things, so this is a better system for me.

“How do I try it out?”

Providing everything goes well, GNOME Shell 3.3.5 should be released tonight, which should have all this new awesomeness. If you want to test with the new browser-plugin stuff, you’ll need to copy the browser plugin from gnome-shell/browser-plugin/.libs/libgnome-shell-browser-plugin.so to ~/.mozilla/plugins, and then restart your browser.

GJS Improvements

Myself, Giovanni Campagna as well as Colin Walters have all been working hard trying to make GJS somewhat of a competitor to PyGObject, being a full introspection stack for the GNOME Desktop Environment. Rather than give you a bunch of history, let me just give you a quick taste. There’s much more to the landing than this, such as implementing your own signals, properties, as well as implementing interfaces, but it will take me a few days to come up with an exciting example that fully showcases the power that you now have.

const Lang = imports.lang;

const Cogl = imports.gi.Cogl;
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;

const MyClutterActor = new Lang.Class({
    Name: 'MyClutterActor',
    Extends: Clutter.Actor,

    vfunc_get_preferred_width: function(actor, forHeight) {
        return [100, 100];
    },

    vfunc_get_preferred_height: function(actor, forWidth) {
        return [100, 100];
    },

    vfunc_paint: function(actor) {
        let alloc = this.get_allocation_box();
        Cogl.set_source_color4ub(255, 0, 0, 255);
        Cogl.rectangle(alloc.x1, alloc.y1, alloc.x2, alloc.y2);
    }
});

const MyClutterEffect = new Lang.Class({
    Name: 'MyClutterEffect',
    Extends: Clutter.DeformEffect,

    vfunc_deform_vertex: function(effect, width, height, vertex) {
        vertex.x += Math.random() * 20 - 10;
        vertex.y += Math.random() * 20 - 10;
    }
});

let actor = new MyClutterActor();
let stage = new Clutter.Stage();
actor.animatev(Clutter.AnimationMode.EASE_IN_OUT_CUBIC, 2000, ['x', 'y'], [200, 200]);
actor.add_effect(new MyClutterEffect());
stage.add_actor(actor);
stage.show_all();

Clutter.main();