# Getting started with GNOME Shell extension development

[GNOME Shell](https://wiki.gnome.org/Projects/GnomeShell) is the graphical shell of the **GNOME** desktop environment: the default interface of the popular [Fedora](https://getfedora.org/) Linux distribution, and one of great prominence in the larger ecosystem.

It allows the use of extensions to modify its behavior, which some contest as hacks, saying they monkey-patch the live shell engine, run unchecked by discrete APIs or permissions, and constantly break after updates due to their scopeless nature.

In response, I offer this sage wisdom: Let us not forgo what is for what could be. Instead, let's get hacking 🔧

# First steps

**GNOME Shell** includes a handy tool for scaffolding a new extension. Get to it with haste using the following command:

```bash
gnome-extensions create --interactive
```

You'll be walked through a few prompts to initialize an extension project and the main `extension.js` file should be automatically opened in your editor. If it's not, you can find the extension in the `~/.local/share/gnome-shell/extensions` directory with the UUID given in the interactive creation process.

You can find more information about this process [here](https://gjs.guide/extensions/development/creating.html#gnome-extensions-tool).

# The framework

**GNOME Shell** extensions are written in JavaScript and the `extension.js` file is home to your extension's code.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">As of GNOME Shell 45, extensions are ECMAScript modules. This primarily affects module imports, so the following is only applicable to extensions targeting GNOME Shell 45 or higher.</div>
</div>

The file is populated with class-based scaffolding like this:

```javascript
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';

export default class MyExtension extends Extension {
    constructor(metadata) {
        super(metadata);
    }

    enable() {
        this._settings = this.getSettings();
    }

    disable() {
        this._settings = null;
    }
}
```

This simple framework determines the lifecycle of your extension:

* Extending the `Extension` class allows access to functions such as `this.getSettings()`
    
* The [`constructor`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor) method is a standard JavaScript feature used to initialize an instance of a class
    
    * The parent class, `Extension`, must also be constructed using `super(metadata)`
        

* The `enable` and `disable` class methods are called when your extension is enabled and disabled in the **GNOME Extensions** app, respectively
    
    * `enable` is responsible for integrating into **GNOME Shell** to establish your extension's functionality
        
    * `disable` is responsible for removing this integration so your extension no longer interferes
        

For more information about the changes to module imports made in GNOME Shell 45, see [this guide](https://gjs.guide/extensions/upgrading/gnome-shell-45.html#esm) to porting extensions.

# What's in an extension?

As mentioned before, **GNOME Shell** extensions are essentially scopeless—in other words, they have boundless capabilities. The [Guide to JavaScript for Gnome](https://gjs.guide/) [cites](https://gjs.guide/extensions/overview/architecture.html#extensions) that extensions can:

> * Use Mutter to control displays, workspaces and windows
>     
> * Use Clutter & St to create new UI elements
>     
> * Use JavaScript modules to create new UI elements
>     
> * Use any other library supporting GObject-Introspection
>     
> * Access and modify any internal GNOME Shell code
>     

Good luck with all that! My experience creating extensions is much more limited, having built an extension that only performs basic window management. Let's look into it as a narrow example of extension development.

## An extension case study

My extension is called Fifty Fifty and its code is available [here](https://github.com/jcrd/gnome-extension-fiftyfifty). Its primary function is to resize two windows to fit side-by-side. This is easily accomplished with the following code:

```javascript
function placeWindow(window, r) {
  window.unmaximize(Meta.MaximizeFlags.HORIZONTAL)
  window.unmaximize(Meta.MaximizeFlags.VERTICAL)
  window.move_resize_frame(false, r.x, r.y, r.width, r.height)
}
```

The complexity comes from determining which windows to tile. This depends on the history of focused windows on a workspace, which (as far as I can tell) is not stored in a manner accessible to extensions. The bulk of the extension's code is related to connecting and disconnecting signals to track and record the focus history.

This occurs in the extension's `enable` method, like this:

```javascript
enable() {
    this.globalSignals[0] = global.workspace_manager.connect(
      "workspace-added",
      (_, index) => this._onWorkspaceAdded(index)
    )
    this.globalSignals[1] = global.workspace_manager.connect(
      "workspace-removed",
      (_, index) => this._onWorkspaceRemoved(index)
    )

    for (let i = 0; i < global.workspace_manager.n_workspaces; i++) {
      this._onWorkspaceAdded(i)
    }

    // We'll look at the remaining code later...
}
```

The signal connection IDs must be saved to remove them in the `disable` method.

In essence, this code sets up signals to react to the addition and removal of workspaces, which triggers the connection and disconnection of more signals responsible for reacting to the addition and removal of windows to this workspace.

Here's the `_onWorkspaceAdded` method:

```javascript
_onWorkspaceAdded(index) {
    const workspace = global.workspace_manager
        .get_workspace_by_index(index)
    const data = { focusHistory: [], signals: [] }

    data.signals[0] = 
        workspace.connect("window-added", (_, window) =>
        this._onWindowAdded(window)
    )
    data.signals[1] = 
        workspace.connect("window-removed", (_, window) =>
        this._onWindowRemoved(window)
    )
    this.workspaces.set(workspace, data)

    workspace.list_windows()
        .forEach((window) => this._onWindowAdded(window))
}
```

Again, the signal IDs are stored in the `data.signals` array to be later disconnected in the `_onWorkspaceRemove` method.

Ultimately, it's the `_onWindowAdded` method that establishes the focus history tracking:

```javascript
_onWindowAdded(window) {
    if (!window || window.get_window_type() !== Meta.WindowType.NORMAL)
        return

    this.windowSignals.set(
      window,
      window.connect("focus", () => this.updateFocusHistory(window))
    )

    if (window.has_focus()) this.updateFocusHistory(window)
}
```

Now, all of this orchestration would be useless without some means of activating the fifty-fifty tiling.

This is accomplished by defining a keybinding in the aforementioned `enable` method:

```javascript
Main.wm.addKeybinding("toggle", settings, flag, mode, () => {
    const workspace = global.workspace_manager.get_active_workspace()
    const windows = this.workspaces
      .get(workspace)
      .focusHistory.slice(0, 2)
      .reverse()

    if (windows.length === 1) {
      placeWindow(windows[0], generateRects(windows[0], 0).workspace)
      return
    }

    let cachedRects = []
    let i = 0

    const fullscreen = windows.reduce((state, window) => {
      const r = generateRects(window, i)
      cachedRects[i++] = r
      return state && rectsEqual(window.get_frame_rect(), r.window)
    }, true)

    i = 0
    windows.forEach((window) => {
      const r = cachedRects[i++]
      placeWindow(window, fullscreen ? r.workspace : r.window)
      window.raise()
    })
})
```

The logic here gives this extension some dynamic functionality depending on the tiled and focused state of the two most recently focused windows:

* If at least one of the two most recently focused windows is not tiled, tile both with the focused window on the left.
    
* If the focused window is tiled on the right, swap it with the window on the left.
    
* If the focused window is tiled on the left, maximize the two most recently focused windows.
    
* If only one window has focus history, maximize it.
    

## Schema files

The above keybinding is made accessible to **GNOME Shell** via a GSettings schema file.

Create such a file in the `schemas` directory of your extension project:

```bash
mkdir schemas
touch schemas/org.gnome.shell.extensions.example.gschema.xml
```

In the above filename, `example` should reflect the UUID specified at extension initialization. In the case of my extension, the UUID is `fiftyfifty@jcrd.github.io` so the schema filename is `org.gnome.shell.extensions.fiftyfifty.gschema.xml`.

The schema itself looks like this:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
    <schema id="org.gnome.shell.extensions.fiftyfifty"
            path="/org/gnome/shell/extensions/fiftyfifty/">
        <key type="as" name="toggle">
            <default><![CDATA[['<Super>f']]]></default>
            <summary>Toggle split</summary>
        </key>
    </schema>
</schemalist>
```

As you can see, the `<Super>f` keybinding is assigned to the `toggle` function.

For **GNOME Shell** to use the schema, it must first be compiled with this command:

```bash
glib-compile-schemas schemas/
```

Details about these schemas and changing them via a preferences window can be found [here](https://gjs.guide/extensions/development/preferences.html#gsettings).

## Using extensions

Now, with the extension in a functional state, there are two approaches to testing it:

* Start a nested instance of **GNOME Shell** with this command:
    
    ```bash
    dbus-run-session -- gnome-shell --nested --wayland
    ```
    
* Restart your active instance of **GNOME Shell** by logging out and back in
    

After either of these steps, the extension should be registered by **GNOME Shell** and can be enabled with the following:

```bash
gnome-extensions enable fiftyfifty@jcrd.github.io
```

There we go. Not even the sky's the limit when it comes to extension functionality, so don't be afraid to shoot for the moon and hack away!

# What's next?

The [Extensions](https://gjs.guide/extensions/) section of the [Guide to JavaScript for Gnome](https://gjs.guide/) is an invaluable resource for extension development, containing much of this information and more.

If you build something useful, submit your extension for distribution on [extensions.gnome.org](https://extensions.gnome.org/) after checking the [review guidelines](https://gjs.guide/extensions/review-guidelines/review-guidelines.html#basics).

Explore the [gnome-shell-extension topic on GitHub](https://github.com/topics/gnome-shell-extension) for some inspiration 🍀
