Legacy Documentation

This document is an archive of extension documentation for versions before major changes to GNOME Shell. For example, how preferences worked before GTK4 and Adwaita, or how extensions worked before GNOME Shell began using ESModules.

If you are trying to migrate an extension to a newer version of GNOME Shell, please see the available Porting Guides instead.

Imports and Modules

TIP

The following documentation about modules only applies to GNOME 44 and earlier.

Exporting Modules

Larger extensions or extensions with discrete components often separate code into modules, including GNOME Shell. You can put code to be exported into .js files and import them in extension.js, prefs.js or each other.

The basic rules of exporting with GJS's import system are that anything defined with var will be exported, while anything defined with const or let will NOT be exported.

/* eslint-disable func-style */
/* exported: exportedFunction, exportedFunctionWrapper, ExportedClass */

// Any imports this module needs itself must also be imported here
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();


// Variables declared with `let` or `const` are NOT exported
const LOCAL_CONSTANT = 42;
const _PrivateClass = class {};

let localVariable = 'a value';
let _privateFunction = function () {};

// Class declarations are NOT exported
class _PrivateSubClass extends _PrivateClass {}


// Function declarations WILL be exported
function exportedFunction(a, b) {
    return a + b;
}

// Variables declared with `var` WILL be exported
var EXPORTED_VARIABLE = 42;

var exportedFunctionWrapper = function (...args) {
    return exportedFunction(...args);
};

var ExportedClass = class ExportedClass extends _PrivateClass {
    constructor(params) {
        super();

        Object.assign(this, params);
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

Importing Modules

If placed in example@gjs.guide/exampleLib.js the script above would be available as Me.imports.exampleLib. If it was in a subdirectory, such as example@gjs.guide/modules/exampleLib.js, you would access it as Me.imports.modules.exampleLib.

// GNOME Shell imports
const Main = imports.ui.main;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();


// You can import your modules using the extension object we imported as `Me`.
const ExampleLib = Me.imports.exampleLib;


let myObject = new ExampleLib.ExportedClass();
ExampleLib.exportedFunction(0, ExampleLib.EXPORTED_VARIABLE);
1
2
3
4
5
6
7
8
9
10
11
12

Many of the elements in GNOME Shell like panel buttons, popup menus and notifications are built from reusable classes and functions, found in modules like these:

You can browse around in the js/ui/ folder or any other JavaScript file under js/ for more code to be reused. Notice the path structure in the links above and how they compare to the imports below:

const ExtensionUtils = imports.misc.extensionUtils;
const ModalDialog = imports.ui.modalDialog;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
1
2
3
4

Importing Libraries

Extensions can import libraries from the GNOME platform, or any other library supporting GObject Introspectionopen in new window. There are also a few built-in libraries such as Cairoopen in new window and Gettextopen in new window that are imported differently.

// GJS's Built-in Modules are in the top-level of the import object
const Gettext = imports.gettext;
const Cairo = imports.cairo;


// Introspected libraries are under the `gi` namespace
const Clutter = imports.gi.Clutter;
const Meta = imports.gi.Meta;

// Multiple libraries can be imported with object destructuring
const {GLib, GObject, Gio} = imports.gi;
1
2
3
4
5
6
7
8
9
10
11

Extension

GNOME Shell 44

Below are the types, patterns and examples for extensions written for GNOME 44 and earlier.

ExtensionMeta

Below is the signature of the meta object (i.e. ExtensionMeta) passed to the init() function of extensions:

/**
 * @typedef ExtensionMeta
 * @type {object}
 * @property {object} metadata - the metadata.json file, parsed as JSON
 * @property {string} uuid - the extension UUID
 * @property {number} type - the extension type; `1` for system, `2` for user
 * @property {Gio.File} dir - the extension directory
 * @property {string} path - the extension directory path
 * @property {string} error - an error message or an empty string if no error
 * @property {boolean} hasPrefs - whether the extension has a preferences dialog
 * @property {boolean} hasUpdate - whether the extension has a pending update
 * @property {boolean} canChange - whether the extension can be enabled/disabled
 * @property {string[]} sessionModes - a list of supported session modes
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Class Pattern

Below is the scaffolding for an extension class:

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();


class Extension {
    constructor() {
        console.debug(`constructing ${Me.metadata.name}`);
    }

    /**
     * This function is called when your extension is enabled, which could be
     * done in GNOME Extensions, when you log in or when the screen is unlocked.
     *
     * This is when you should setup any UI for your extension, change existing
     * widgets, connect signals or modify GNOME Shell's behavior.
     */
    enable() {
        console.debug(`enabling ${Me.metadata.name}`);
    }


    /**
     * This function is called when your extension is uninstalled, disabled in
     * GNOME Extensions or when the screen locks.
     *
     * Anything you created, modified or setup in enable() MUST be undone here.
     * Not doing so is the most common reason extensions are rejected in review!
     */
    disable() {
        console.debug(`disabling ${Me.metadata.name}`);
    }
}


/**
 * This function is called once when your extension is loaded, not enabled. This
 * is a good time to setup translations or anything else you only do once.
 *
 * You MUST NOT make any changes to GNOME Shell, connect any signals or add any
 * MainLoop sources here.
 *
 * @param {ExtensionMeta} meta - An extension meta object
 * @returns {object} an object with enable() and disable() methods
 */
function init(meta) {
    console.debug(`initializing ${meta.metadata.name}`);

    return new Extension();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

Module Pattern

Below is the scaffolding for an extension using top-level functions:

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();


/**
 * This function is called once when your extension is loaded, not enabled. This
 * is a good time to setup translations or anything else you only do once.
 *
 * You MUST NOT make any changes to GNOME Shell, connect any signals or add any
 * MainLoop sources here.
 *
 * @param {ExtensionMeta} meta - An extension meta object
 */
function init(meta) {
    console.debug(`initializing ${meta.metadata.name}`);
}


/**
 * This function is called when your extension is enabled, which could be
 * done in GNOME Extensions, when you log in or when the screen is unlocked.
 *
 * This is when you should setup any UI for your extension, change existing
 * widgets, connect signals or modify GNOME Shell's behavior.
 */
function enable() {
    console.debug(`enabling ${Me.metadata.name}`);
}


/**
 * This function is called when your extension is uninstalled, disabled in
 * GNOME Extensions or when the screen locks.
 *
 * Anything you created, modified or setup in enable() MUST be undone here.
 * Not doing so is the most common reason extensions are rejected in review!
 */
function disable() {
    console.debug(`disabling ${Me.metadata.name}`);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

Complete Example

Below is a working example of an extension for GNOME Shell 44 or earlier:

const St = imports.gi.St;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;

const {
    gettext: _,
} = ExtensionUtils;


class Extension {
    enable() {
        // Create a panel button
        this._indicator = new PanelMenu.Button(0.0, Me.metadata.name, false);

        // Add an icon
        const icon = new St.Icon({
            icon_name: 'face-laugh-symbolic',
            style_class: 'system-status-icon',
        });
        this._indicator.add_child(icon);

        // Add the indicator to the panel
        Main.panel.addToStatusArea(Me.metadata.uuid, this._indicator);

        // Add a menu item to open the preferences window
        this._indicator.menu.addAction(_('Preferences'),
            () => ExtensionUtils.openPrefs());

        this._count = 0;
    }

    disable() {
        if (this._indicator) {
            this._indicator.destroy();
            this._indicator = null;
        }
    }
}


function init() {
    ExtensionUtils.initTranslations();

    return new Extension();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

Preferences

GNOME 42

TIP

This documentation covers preferences for GNOME 42 to GNOME 44, since there were no breaking changes between those releases.

Below is the scaffolding for extension preferences using Adwaita and GTK4:

const {Adw, Gtk} = imports.gi;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();


/**
 * Like `extension.js` this is used for any one-time setup like translations.
 *
 * @param {object} metadata - The metadata.json file, parsed as JSON
 */
function init(metadata) {
    console.debug(`initializing ${metadata.name} Preferences`);
}


/**
 * This function is called when the preferences window is first created to build
 * and return a GTK4 widget.
 *
 * The preferences window will be a `Adw.PreferencesWindow`, and the widget
 * returned by this function will be added to an `Adw.PreferencesPage` or
 * `Adw.PreferencesGroup` if necessary.
 *
 * @returns {Gtk.Widget} the preferences widget
 */
function buildPrefsWidget() {
    return new Gtk.Label({
        label: Me.metadata.name,
    });
}

/**
 * This function is called when the preferences window is first created to fill
 * the `Adw.PreferencesWindow`.
 *
 * If this function is defined, `buildPrefsWidget()` will NOT be called.
 *
 * @param {Adw.PreferencesWindow} window - The preferences window
 */
function fillPreferencesWindow(window) {
    const prefsPage = new Adw.PreferencesPage({
        name: 'general',
        title: 'General',
        icon_name: 'dialog-information-symbolic',
    });
    window.add(prefsPage);

    const prefsGroup = new Adw.PreferencesGroup({
        title: 'Appearance',
        description: `Configure the appearance of ${Me.metadata.name}`,
    });
    prefsPage.add(prefsGroup);

    const showIndicatorRow = new Adw.ActionRow({
        title: 'Show Indicator',
        subtitle: 'Whether to show the panel indicator',
    });
    prefsGroup.add(showIndicatorRow);

    const showIndicatorSwitch = new Gtk.Switch();
    showIndicatorRow.add_suffix(showIndicatorSwitch);
    showIndicatorRow.set_activatable_widget(showIndicatorSwitch);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

GNOME 40

Below is the scaffolding for extension preferences using GTK4:

const {Gtk} = imports.gi;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();


/**
 * Like `extension.js`, this is used for any one-time setup like translations.
 *
 * @param {object} metadata - The metadata.json file, parsed as JSON
 */
function init(metadata) {
    console.debug(`initializing ${metadata.name} Preferences`);

    ExtensionUtils.initTranslations();
}

/**
 * This function is called when the preferences window is first created to build
 * and return a GTK4 widget.
 *
 * @returns {Gtk.Widget} the preferences widget
 */
function buildPrefsWidget() {
    return new Gtk.Label({
        label: Me.metadata.name,
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

GNOME 3.x

Below is the scaffolding for extension preferences using GTK3:

const {Gtk} = imports.gi;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();


/**
 * Like `extension.js`, this is used for any one-time setup like translations.
 *
 * @param {object} metadata - The metadata.json file, parsed as JSON
 */
function init(metadata) {
    log(`initializing ${metadata.name} Preferences`);

    ExtensionUtils.initTranslations();
}

/**
 * This function is called when the preferences window is first created to build
 * and return a GTK3 widget.
 *
 * @returns {Gtk.Widget} the preferences widget
 */
function buildPrefsWidget() {
    return new Gtk.Label({
        label: Me.metadata.name,
        visible: true,
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

Translations

TIP

See ExtensionUtils for more complete documentation of translation functions available before GNOME 45.

GNOME 41

TIP

This documentation covers translations for GNOME 41 to GNOME 44, since there were no breaking changes between those releases.

const Gettext = imports.gettext;
const St = imports.gi.St;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;


// Get all the Gettext functions, while aliasing `gettext()` as `_()`
const {
    gettext: _,
    ngettext,
    pgettext,
} = ExtensionUtils;


class Extension {
    enable() {
        // Create a panel button
        this._indicator = new PanelMenu.Button(0.0, Me.metadata.name, false);

        // Add an icon
        const icon = new St.Icon({
            icon_name: 'face-laugh-symbolic',
            style_class: 'system-status-icon',
        });
        this._indicator.add_child(icon);

        // Add the indicator to the panel
        Main.panel.addToStatusArea(Me.metadata.uuid, this._indicator);

        // A string needing more context is marked with `pgettext()`
        this._indicator.menu.addAction(pgettext('menu item', 'Notify'), () => {
            this._count += 1;

            // A regular translatable string is marked with the `_()` function
            const title = _('Notification');

            // A "countable" string is marked with the `ngettext()` function
            const body = ngettext('You have been notified %d time',
                'You have been notified %d times',
                this._count).format(this._count);

            Main.notify(title, body);
        });

        this._count = 0;
    }

    disable() {
        if (this._indicator) {
            this._indicator.destroy();
            this._indicator = null;
        }
    }
}


function init() {
    // If the `gettext-domain` key is not set in `metadata.json`, you must
    // pass the unique Gettext domain for your extension when initializing.
    ExtensionUtils.initTranslations(Me.metadata.uuid);

    return new Extension();
}









 
 
 
 
 
 

















 
 
 
 
 
 
 
 
 
 
 
 
 
 














 
 
 



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

GNOME 40

TIP

This documentation covers translations for GNOME 40 and earlier, but may not be accurate for all previous versions of GNOME Shell.

const Gettext = imports.gettext;
const St = imports.gi.St;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;


// This creates an object with functions for marking strings as translatable.
// You must pass the same domain as `ExtensionUtils.initTranslations()`.
const Domain = Gettext.domain(Me.metadata.uuid);

// Get all the Gettext functions, while aliasing `gettext()` as `_()`
const {
    gettext: _,
    ngettext,
    pgettext,
} = Domain;


class Extension {
    enable() {
        // Create a panel button
        this._indicator = new PanelMenu.Button(0.0, Me.metadata.name, false);

        // Add an icon
        const icon = new St.Icon({
            icon_name: 'face-laugh-symbolic',
            style_class: 'system-status-icon',
        });
        this._indicator.add_child(icon);

        // Add the indicator to the panel
        Main.panel.addToStatusArea(Me.metadata.uuid, this._indicator);

        // A string needing more context is marked with `pgettext()`
        this._indicator.menu.addAction(pgettext('menu item', 'Notify'), () => {
            this._count += 1;

            // A regular translatable string is marked with the `_()` function
            const title = _('Notification');

            // A "countable" string is marked with the `ngettext()` function
            const body = ngettext('You have been notified %d time',
                'You have been notified %d times',
                this._count).format(this._count);

            Main.notify(title, body);
        });

        this._count = 0;
    }

    disable() {
        if (this._indicator) {
            this._indicator.destroy();
            this._indicator = null;
        }
    }
}


function init() {
    // If the `gettext-domain` key is not set in `metadata.json`, you must
    // pass the unique Gettext domain for your extension when initializing.
    ExtensionUtils.initTranslations(Me.metadata.uuid);

    return new Extension();
}









 
 
 
 
 
 
 
 
 
 

















 
 
 
 
 
 
 
 
 
 
 
 
 
 














 
 
 



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

Quick Settings

TIP

This documentation covers Quick Settings for GNOME 43 to GNOME 44, since the changes between these releases were minor.

There are many complete examples of Quick Settings in GNOME Shell, which can be referenced in the js/ui/status/open in new window directory.

Imports

These are the relevant imports and class instances for Quick Settings:

const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
const QuickSettings = imports.ui.quickSettings;

// This are the live instances of the Quick Settings menu and action area
const QuickSettingsMenu = Main.panel.statusArea.quickSettings;
const QuickSettingsActions = QuickSettingsMenu._system._indicator.child;
1
2
3
4
5
6
7

Basic Toggle

WARNING

Note that in GNOME 44, the label property was renamed to title. The label property will continue to work, except as a construct property.

Here is an example of a simple on/off toggle, similar to what the Night Light uses in GNOME Shell:

const ExampleToggle = GObject.registerClass(
class ExampleToggle extends QuickSettings.QuickToggle {
    _init() {
        super._init({
            title: 'Example Name',
            iconName: 'selection-mode-symbolic',
            toggleMode: true,
        });

        // NOTE: In GNOME 44, the `label` property must be set after
        // construction. The newer `title` property can be set at construction.
        this.label = 'Example Name';

        // Binding the toggle to a GSettings key
        this._settings = ExtensionUtils.getSettings();
        this._settings.bind('feature-enabled',
            this, 'checked',
            Gio.SettingsBindFlags.DEFAULT);
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

You may also want your extension to show a panel indicator when the feature is enabled. The QuickSettings.SystemIndicator class is used to display an icon and also manages quick setting items:

const ExampleIndicator = GObject.registerClass(
class ExampleIndicator extends QuickSettings.SystemIndicator {
    _init() {
        super._init();

        // Create the icon for the indicator
        this._indicator = this._addIndicator();
        this._indicator.icon_name = 'selection-mode-symbolic';

        // Showing the indicator when the feature is enabled
        this._settings = ExtensionUtils.getSettings();
        this._settings.bind('feature-enabled',
            this._indicator, 'visible',
            Gio.SettingsBindFlags.DEFAULT);

        // Create the toggle and associate it with the indicator, being sure to
        // destroy it along with the indicator
        this.quickSettingsItems.push(new ExampleToggle());

        this.connect('destroy', () => {
            this.quickSettingsItems.forEach(item => item.destroy());
        });

        // Add the indicator to the panel and the toggle to the menu
        QuickSettingsMenu._indicators.add_child(this);
        QuickSettingsMenu._addItems(this.quickSettingsItems);
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

Since the code for adding the indicator and toggle item is contained in the FeatureIndicator class, the code for the extension is quite simple:

class Extension {
    enable() {
        this._indicator = new ExampleIndicator();
    }

    disable() {
        this._indicator.destroy();
        this._indicator = null;
    }
}

function init() {
    return new Extension();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Toggle Menu

WARNING

Note that in GNOME 44, the label property was renamed to title. The label property will continue to work, except as a construct property.

For features with a few more settings or options, you may want to add a submenu to the toggle. The QuickSettings.QuickMenuToggle includes a built-in Popup Menu, that supports the standard menu functions:

const ExampleMenuToggle = GObject.registerClass(
class ExampleMenuToggle extends QuickSettings.QuickMenuToggle {
    _init() {
        super._init({
            title: 'Example Name',
            iconName: 'selection-mode-symbolic',
            toggleMode: true,
        });

        // This function is unique to this class. It adds a nice header with an
        // icon, title and optional subtitle. It's recommended you do so for
        // consistency with other menus.
        this.menu.setHeader('selection-mode-symbolic', 'Example Header',
            'Optional Subtitle');

        // You may also add sections of items to the menu
        this._itemsSection = new PopupMenu.PopupMenuSection();
        this._itemsSection.addAction('Option 1', () => log('activated'));
        this._itemsSection.addAction('Option 2', () => log('activated'));
        this.menu.addMenuItem(this._itemsSection);

        // Add an entry-point for more settings
        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        const settingsItem = this.menu.addAction('More Settings',
            () => ExtensionUtils.openPrefs());

        // Ensure the settings are unavailable when the screen is locked
        settingsItem.visible = Main.sessionMode.allowSettings;
        this.menu._settingsActions[Extension.uuid] = settingsItem;
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

Slider

The quick settings API also comes with a new class for sliders, for settings like brightness or volume. The QuickSettings.QuickSlider class is fairly straight forward to use:

const ExampleSlider = GObject.registerClass(
class ExampleSlider extends QuickSettings.QuickSlider {
    _init() {
        super._init({
            iconName: 'selection-mode-symbolic',
        });

        this._sliderChangedId = this.slider.connect('notify::value',
            this._onSliderChanged.bind(this));

        // Binding the slider to a GSettings key
        this._settings = ExtensionUtils.getSettings();
        this._settings.connect('changed::feature-range',
            this._onSettingsChanged.bind(this));

        this._onSettingsChanged();

        // Set an accessible name for the slider
        this.slider.accessible_name = 'Example Range';
    }

    _onSettingsChanged() {
        // Prevent the slider from emitting a change signal while being updated
        this.slider.block_signal_handler(this._sliderChangedId);
        this.slider.value = this._settings.get_uint('feature-range') / 100.0;
        this.slider.unblock_signal_handler(this._sliderChangedId);
    }

    _onSliderChanged() {
        // Assuming our GSettings holds values between 0..100, adjust for the
        // slider taking values between 0..1
        const percent = Math.floor(this.slider.value * 100);
        this._settings.set_uint('feature-range', percent);
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

When adding the slider to the menu, you will usually want it to span two columns, like the default volume slider:

// Add the slider to the menu, this time passing `2` as the second
// argument to ensure the slider spans both columns of the menu
QuickSettingsMenu._addItems([new FeatureSlider()], 2);
1
2
3

Action Button

It's also possible to add action buttons to the top of the quick settings, such as the Lock Screen or Settings button. Note that this is a very prominent location in the UI with limited space, so you should consider carefully before adding more buttons here.

const FeatureButton = GObject.registerClass(
class FeatureButton extends QuickSettings.QuickSettingsItem {
    _init() {
        super._init({
            style_class: 'icon-button',
            can_focus: true,
            icon_name: 'selection-mode-symbolic',
            accessible_name: 'Feature',
        });

        this.connect('clicked', () => console.log('activated'));

        // Adding an action button to the Quick Settings menu
        QuickSettingsActions.add_child(this);
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

GNOME Shell 44 features a new Background Apps menu in the quick settings menu, which looks different from the other tiles. If you want your toggle placed above the Background Apps menu (or any other widget), you can move it after adding it with the built-in function:

function addQuickSettingsItems(items) {
    // Add the items with the built-in function
    QuickSettingsMenu._addItems(items);

    // Ensure the tile(s) are above the background apps menu
    for (const item of items) {
        QuickSettingsMenu.menu._grid.set_child_below_sibling(item,
            QuickSettingsMenu._backgroundApps.quickSettingsItems[0]);
    }
}
1
2
3
4
5
6
7
8
9
10

Search Provider

TIP

This documentation covers Quick Settings for GNOME 43 to GNOME 44, since there were no breaking changes between those releases.

A search provider is a mechanism by which an application can expose its search capabilities to GNOME Shell. Text from the search entry is forwarded to all search providers, which may each return a set of search results.

Imports

These are the relevant imports and class instances for Quick Settings:

const {St} = imports.gi;

const Main = imports.ui.main;

// This is the live instance of the Search Results view
const SearchResults = Main.overview._overview._controls._searchController._searchResults;
1
2
3
4
5
6

ResultMeta

The ResultMeta object is a light-weight metadata object, used to represent a search result in the search view. Search providers must return objects of this type when SearchProvider.prototype.getResultMetas() is called.

/**
 * @typedef ResultMeta
 * @type {object}
 * @property {string} id - the unique identifier of the result
 * @property {string} name - the name of the result
 * @property {string} [description] - optional description of the result
 * @property {string} [clipboardText] - optional clipboard content
 * @property {Function} createIcon - creates an icon for the result
 */
1
2
3
4
5
6
7
8
9

The id is the result identifier, as returned by the provider.

The name property holds a name or short description of the result.

The description property is optional, holding a longer description of the result that is only displayed in the list view.

The clipboardText property is optional, holding text that will be copied to the clipboard if the result is activated.

The createIcon property holds a function that takes a size argument and returns a Clutter.Actor, usually an St.Icon:

/**
 * Create an icon for a search result.
 *
 * Implementations may want to take scaling into consideration.
 *
 * @param {number} size - The requested size of the icon
 * @returns {Clutter.Actor} An icon
 */
function createIcon(size) {
    const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);

    return new St.Icon({
        icon_name: 'dialog-question',
        width: size * scaleFactor,
        height: size * scaleFactor,
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Example Provider

GNOME Shell extensions create search providers by creating a class implementing a simple interface. This class is responsible for returning a list of results for a list of search terms.

Results are returned as unique string identifiers, which may be passed back to the search provider to request ResultMeta objects. These are used by GNOME Shell to populate the results displayed to the user.

Search providers are constructed and then registered with GNOME Shell, before they start receiving search requests.

class SearchProvider {
    /**
     * The application of the provider.
     *
     * Applications will return a `Gio.AppInfo` representing themselves.
     * Extensions will usually return `null`.
     *
     * @type {Gio.AppInfo}
     */
    get appInfo() {
        return null;
    }

    /**
     * Whether the provider offers detailed results.
     *
     * Applications will return `true` if they have a way to display more
     * detailed or complete results. Extensions will usually return `false`.
     *
     * @type {boolean}
     */
    get canLaunchSearch() {
        return false;
    }

    /**
     * The unique ID of the provider.
     *
     * Applications will return their application ID. Extensions will usually
     * return their UUID.
     *
     * @type {string}
     */
    get id() {
        return imports.misc.extensionUtils.getCurrentExtension().uuid;
    }

    /**
     * Launch the search result.
     *
     * This method is called when a search provider result is activated.
     *
     * @param {string} result - The result identifier
     * @param {string[]} terms - The search terms
     */
    activateResult(result, terms) {
        console.debug(`activateResult(${result}, [${terms}])`);
    }

    /**
     * Launch the search provider.
     *
     * This method is called when a search provider is activated. A provider can
     * only be activated if the `appInfo` property holds a valid `Gio.AppInfo`
     * and the `canLaunchSearch` property is `true`.
     *
     * Applications will typically open a window to display more detailed or
     * complete results.
     *
     * @param {string[]} terms - The search terms
     */
    launchSearch(terms) {
        console.debug(`launchSearch([${terms}])`);
    }

    /**
     * Create a result object.
     *
     * This method is called to create an actor to represent a search result.
     *
     * Implementations may return any `Clutter.Actor` to serve as the display
     * result, or `null` for the default implementation.
     *
     * @param {ResultMeta} meta - A result metadata object
     * @returns {Clutter.Actor|null} An actor for the result
     */
    createResultObject(meta) {
        console.debug(`createResultObject(${meta.id})`);

        return null;
    }

    /**
     * Get result metadata.
     *
     * This method is called to get a `ResultMeta` for each identifier.
     *
     * If @cancellable is triggered, this method should throw an error.
     *
     * @async
     * @param {string[]} results - The result identifiers
     * @param {Gio.Cancellable} cancellable - A cancellable for the operation
     * @returns {Promise<ResultMeta[]>} A list of result metadata objects
     */
    getResultMetas(results, cancellable) {
        console.debug(`getResultMetas([${results}])`);

        const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);

        return new Promise((resolve, reject) => {
            const cancelledId = cancellable.connect(
                () => reject(Error('Operation Cancelled')));

            const resultMetas = [];

            for (const identifier of results) {
                const meta = {
                    id: identifier,
                    name: 'Result Name',
                    description: 'The result description',
                    clipboardText: 'Content for the clipboard',
                    createIcon: size => {
                        return new St.Icon({
                            icon_name: 'dialog-information',
                            width: size * scaleFactor,
                            height: size * scaleFactor,
                        });
                    },
                };

                resultMetas.push(meta);
            }

            cancellable.disconnect(cancelledId);
            if (!cancellable.is_cancelled())
                resolve(resultMetas);
        });
    }

    /**
     * Initiate a new search.
     *
     * This method is called to start a new search and should return a list of
     * unique identifiers for the results.
     *
     * If @cancellable is triggered, this method should throw an error.
     *
     * @async
     * @param {string[]} terms - The search terms
     * @param {Gio.Cancellable} cancellable - A cancellable for the operation
     * @returns {Promise<string[]>} A list of result identifiers
     */
    getInitialResultSet(terms, cancellable) {
        console.debug(`getInitialResultSet([${terms}])`);

        return new Promise((resolve, reject) => {
            const cancelledId = cancellable.connect(
                () => reject(Error('Search Cancelled')));

            const identifiers = [
                'result-01',
                'result-02',
                'result-03',
            ];

            cancellable.disconnect(cancelledId);
            if (!cancellable.is_cancelled())
                resolve(identifiers);
        });
    }

    /**
     * Refine the current search.
     *
     * This method is called to refine the current search results with
     * expanded terms and should return a subset of the original result set.
     *
     * Implementations may use this method to refine the search results more
     * efficiently than running a new search, or simply pass the terms to the
     * implementation of `getInitialResultSet()`.
     *
     * If @cancellable is triggered, this method should throw an error.
     *
     * @async
     * @param {string[]} results - The original result set
     * @param {string[]} terms - The search terms
     * @param {Gio.Cancellable} cancellable - A cancellable for the operation
     * @returns {Promise<string[]>}
     */
    getSubsearchResultSet(results, terms, cancellable) {
        console.debug(`getSubsearchResultSet([${results}], [${terms}])`);

        if (cancellable.is_cancelled())
            throw Error('Search Cancelled');

        return this.getInitialResultSet(terms, cancellable);
    }

    /**
     * Filter the current search.
     *
     * This method is called to truncate the number of search results.
     *
     * Implementations may use their own criteria for discarding results, or
     * simply return the first n-items.
     *
     * @param {string[]} results - The original result set
     * @param {number} maxResults - The maximum amount of results
     * @returns {string[]} The filtered results
     */
    filterResults(results, maxResults) {
        console.debug(`filterResults([${results}], ${maxResults})`);

        if (results.length <= maxResults)
            return results;

        return results.slice(0, maxResults);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209

Search providers from GNOME Shell extensions must be registered before they become active. Registration should be performed in the enable() function and then later unregistered in disable().

class Extension {
    enable() {
        this._provider = new SearchProvider();
        SearchResults._registerProvider(this._provider);
    }

    disable() {
        if (this._provider) {
            SearchResults._unregisterProvider(this._provider);
            this._provider = null;
        }
    }
}

/** */
function init() {
    return new Extension();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Session Modes

TIP

This documentation covers Session Modes for GNOME 42 to GNOME 44, since there were no breaking changes between those releases.

Session modes are environment states of GNOME Shell. For example, when a user is logged in and using their desktop the Shell is in the user mode.

Since GNOME 42, extensions have the option of operating in other session modes, such as the unlock-dialog mode when the screen is locked. For more details, see the session-modes documentation.

Example Usage

Here is an example of a metadata.json for an extension that can run in the regular user mode and continue running in the unlock-dialog mode, when the screen is locked. Pay attention that the shell may use custom user modes that are not named user, so we need to ensure this by also checking the parent mode.

{
    "uuid": "session-modes@gjs.guide",
    "name": "Session Modes Example",
    "description": "This is an example of using session modes in an extension.",
    "shell-version": [ "42" ],
    "session-modes": ["user", "unlock-dialog"],
    "url": "https://gjs.guide/extensions/"
}
1
2
3
4
5
6
7
8

Like standard extensions, the extension will be enabled when the user logs in and logs out, but won't be disabled when the screen locks.

Extensions that continue running on the lock screen will usually want to disable UI elements when the session is locked, while continuing to operate in the background.

const {GLib, St} = imports.gi;

const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;


class Extension {
    constructor() {
        this._indicator = null;
        this._timeoutId = null;
        this._sessionId = null;
    }

    _addIndicator() {
        if (this._indicator === null) {
            this._indicator = new PanelMenu.Button(0.0, 'Remindicator', false);

            const icon = new St.Icon({
                icon_name: 'preferences-system-time-symbolic',
                style_class: 'system-status-icon',
            });
            this._indicator.add_child(icon);

            Main.panel.addToStatusArea('Remindicator', this._indicator);
        }
    }

    _removeIndicator() {
        if (this._indicator) {
            this._indicator.destroy();
            this._indicator = null;
        }
    }

    // When the session mode changes, we will either add or remove our indicator
    // so it is not visible on the lock screen.
    _onSessionModeChanged(session) {
        if (session.currentMode === 'user' || session.parentMode === 'user')
            this._addIndicator();
        else if (session.currentMode === 'unlock-dialog')
            this._removeIndicator();
    }

    // Our extension will be enabled when the user logs in
    enable() {
        // Watch for changes to the session mode
        this._sessionId = Main.sessionMode.connect('updated',
            this._onSessionModeChanged.bind(this));

        // Show a notification every hour
        this._timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
            60 * 60, () => {
                Main.notify('Reminder', 'An hour has passed!');

                return GLib.SOURCE_CONTINUE;
            });

        this._addIndicator();
    }

    // Our extension will only be disabled when the user logs out
    disable() {
        if (this._timeoutId) {
            GLib.Source.remove(this._timeoutId);
            this._timeoutId = null;
        }

        if (this._sessionId) {
            Main.sessionMode.disconnect(this._sessionId);
            this._sessionId = null;
        }

        this._removeIndicator();
    }
}

/** */
function init() {
    return new Extension();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
Last Updated: 12/3/2023, 4:56:51 PM
Contributors: Andy Holmes