Commit 757ad09d authored by Andy Holmes's avatar Andy Holmes
Browse files

Add "extensions" section

This is the initial port of the GNOME Wiki documentation to MarkDown, as
a sub-section of gjs.guide
parent c82833b8
Pipeline #215286 passed with stage
in 2 minutes and 48 seconds
......@@ -25,6 +25,10 @@ module.exports = {
text: 'Showcase',
link: '/showcase/'
},
{
text: 'Extensions',
link: '/extensions/'
},
{
text: 'API References',
link: 'https://gjs-docs.gnome.org'
......
---
title: Extensions
date: 2018-08-20 16:10:11
layout: IndexPage
---
# GNOME Shell Extensions
## Introduction
GNOME Shell's UI and extensions are written in GJS, which is JavaScript bindings for the [GNOME APIs][gnome-api].
JavaScript is a prototype-based language, which means that extensions can modify the UI and behaviour of GNOME Shell *while* it is running. This is what is known as "monkey-patching".
<ShowCaseBox title="Overview" subtitle="The basics of extensions">
<ShowCase link="overview/anatomy.html" title="Anatomy" subtitle="What an extension is made of"/>
<ShowCase link="overview/imports-and-modules.html" title="Imports & Modules" subtitle="How to use imports and modules"/>
<ShowCase link="overview/architecture.html" title="Architecture" subtitle="GNOME Shell Architecture"/>
</ShowCaseBox>
<ShowCaseBox title="Development" subtitle="How to develop an extension">
<ShowCase link="development/creating.html" title="Creating" subtitle="Creating an extension"/>
<ShowCase link="development/preferences.html" title="Preferences" subtitle="Creating a preferences window"/>
<ShowCase link="development/translations.html" title="Translations" subtitle="How add multi-lingual support an extension"/>
<ShowCase link="development/debugging.html" title="Debugging" subtitle="How to debug an extension"/>
</ShowCaseBox>
[gnome-api]: https://gjs-docs.gnome.org
---
title: Creating
---
# Creating
To get clean view of how your extension functions, you should restart GNOME Shell after making changes to the code. For this reason, most extension development happens in Xorg/X11 sessions rather than Wayland, which requires you to logout and login to restart .
To restart GNOME Shell in X11, pressing `Alt`+`F2` to open the *Run Dialog* and enter `restart` (or just `r`).
To run new extensions on Wayland you can run a nested gnome-shell using `dbus-run-session -- gnome-shell --nested --wayland`.
- [GNOME Extensions Tool](#gnome-extensions-tool)
- [Manual Creation](#manual-creation)
- [Enabling the Extension](#enabling-the-extension)
- [A Working Extension](#a-working-extension)
## GNOME Extensions Tool
GNOME Shell ships with a program you can use to create a skeleton extension by running `gnome-extensions create`.
Instead of passing options on the command line, you can start creating an extension interactively:
```sh
$ gnome-extensions create --interactive
```
1. **First choose a name:**
```sh
Name should be a very short (ideally descriptive) string.
Examples are: “Click To Focus”, “Adblock”, “Shell Window Shrinker”
Name: Example Extension
```
2. **Second choose a description:**
```sh
Description is a single-sentence explanation of what your extension does.
Examples are: “Make windows visible on click”, “Block advertisement popups”, “Animate windows shrinking on minimize”
Description: An extension serving as an example
```
3. **The last step is to choose a UUID for your extension:**
```sh
UUID is a globally-unique identifier for your extension.
This should be in the format of an email address (clicktofocus@janedoe.example.com)
UUID: example@shell.gnome.org
```
The whole process looks like this on the command line:
```sh
$ gnome-extensions create --interactive
Name should be a very short (ideally descriptive) string.
Examples are: “Click To Focus”, “Adblock”, “Shell Window Shrinker”
Name: Example Extension
Description is a single-sentence explanation of what your extension does.
Examples are: “Make windows visible on click”, “Block advertisement popups”, “Animate windows shrinking on minimize”
Description: An extension serving as an example
UUID is a globally-unique identifier for your extension.
This should be in the format of an email address (clicktofocus@janedoe.example.com)
UUID: example@shell.gnome.org
```
Once you finish the last step, the extension template will be created and opened in an editor:
<img :src="$withBase('/assets/img/gnome-extensions-create-editor.png')" />
## Manual Creation
Start by creating an extension directory, then open the two required files in `gedit` or another editor:
```sh
$ mkdir -p ~/.local/share/gnome-shell/extensions/example@shell.gnome.org
$ cd ~/.local/share/gnome-shell/extensions/example@shell.gnome.org
$ gedit extension.js metadata.json &
```
Populate `extension.js` and `metadata.json` with the basic requirements, remembering that `uuid` MUST match the directory name of your extension:
### `metadata.json`
```js
{
"uuid": "example@shell.gnome.org",
"name": "Example",
"description": "This extension puts an icon in the panel with a simple dropdown menu.",
"version": 1,
"shell-version": [ "3.36", "3.38" ],
"url": "https://gitlab.gnome.org/World/ShellExtensions/gnome-shell-extension-example"
}
```
### `extension.js`
Notice that in the example below, we are using three top-level functions instead of class like in the example above.
```js
'use strict';
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
function init() {
log(`initializing ${Me.metadata.name}`);
}
function enable() {
log(`enabling ${Me.metadata.name}`);
}
function disable() {
log(`disabling ${Me.metadata.name}`);
}
```
## Enabling the Extension
Firstly, we want to ensure we're watching the journal for any errors or mistakes we might have made. As described in the [Debugging](../development/debugging.html) page, most users can run `journalctl` in a terminal to watch the output of GNOME Shell and extensions:
```sh
$ journalctl -f -o cat /usr/bin/gnome-shell
```
Next we'll enable the extension using `gnome-extensions enable`:
```sh
$ gnome-extensions enable example@shell.gnome.org
```
To get clean view of how your extension functions after making changes, you should either restart GNOME Shell or run a nested session.
- **X11**
In X11 sessions, you can restart GNOME Shell by pressing `Alt`+`F2` to open the *Run Dialog* and then enter `restart` (or just `r`).
- **Wayland**
On Wayland, the easiest way to test the new extension is by running a nested gnome-shell:
```sh
dbus-run-session -- gnome-shell --nested --wayland
```
After this is done you should see something like the following in the log:
```sh
GNOME Shell started at Sat Aug 22 2020 07:14:35 GMT-0800 (PST)
initializing Example Extension version 1
enabling Example Extension version 1
```
## A Working Extension
As a simple example, let's add a panel button to show what a working extension might look like:
```js
const St = imports.gi.St;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
class Extension {
constructor() {
this._indicator = null;
}
enable() {
log(`enabling ${Me.metadata.name}`);
let indicatorName = `${Me.metadata.name} Indicator`;
// Create a panel button
this._indicator = new PanelMenu.Button(0.0, indicatorName, false);
// Add an icon
let icon = new St.Icon({
gicon: new Gio.ThemedIcon({name: 'face-laugh-symbolic'}),
style_class: 'system-status-icon'
});
this._indicator.add_child(icon);
// `Main.panel` is the actual panel you see at the top of the screen,
// not a class constructor.
Main.panel.addToStatusArea(indicatorName, this._indicator);
}
// REMINDER: It's required for extensions to clean up after themselves when
// they are disabled. This is required for approval during review!
disable() {
log(`disabling ${Me.metadata.name}`);
this._indicator.destroy();
this._indicator = null;
}
}
function init() {
log(`initializing ${Me.metadata.name}`);
return new Extension();
}
```
Now save `extension.js` and reload the extension see the button in the panel.
---
title: Debugging
---
# Debugging
## Basic Debugging
> Some distributions may require you to be part of a `systemd` user group to access logs. On systems that are not using `systemd`, logs may be written to `~/.xsession-errors`.
Basic debugging and logging is an important part of developing any software. GJS has a number of built in global functions, although not all of them are useful for extensions.
### Logging
```js
// Log a string, usually to `journalctl`
log('a message');
// Log an Error() with a stack trace and optional prefix
try {
throw new Error('An error occurred');
} catch (e) {
logError(e, 'ExtensionError');
}
// Print a message to stdout
print('a message');
// Print a message to stderr
printerr('An error occured');
```
When writing extensions, `print()` and `printerr()` are not particularly useful since we won't have easy access to `gnome-shell`'s `stdin` and `stderr` pipes. You should generally use `log()` and `logError()` and watch the log in a new terminal with `journalctl`:
```sh
$ journalctl -f -o cat /usr/bin/gnome-shell
```
### GJS Console
Similar to Python, GJS also has a console you can use to test things out. However, you will not be able to access live code running in the `gnome-shell` process or import JS modules from GNOME Shell, since this a separate process.
```sh
$ gjs-console
gjs> log('a message');
Gjs-Message: 06:46:03.487: JS LOG: a message
gjs> try {
.... throw new Error('An error occurred');
.... } catch (e) {
.... logError(e, 'ConsoleError');
.... }
(gjs-console:9133): Gjs-WARNING **: 06:47:06.311: JS ERROR: ConsoleError: Error: An error occurred
@typein:2:16
@<stdin>:1:34
```
### Recovering from Fatal Errors
Despite the fact that extensions are written in JavaScript, the code is executed in the same process as `gnome-shell` so fatal programmer errors can crash GNOME Shell in a few situations. If your extension crashes GNOME Shell as a result of the `init()` or `enable()` hooks being called, this can leave you unable to log into GNOME Shell.
If you find yourself in this situation, you may be able to correct the problem from a TTY:
1. **Switch to a free TTY and log in**
You can do so, for example, by pressing `Ctrl` + `Alt` + `F4`. You may have to cycle through the `F#` keys.
2. **Start `journalctl` as above**
```sh
$ journalctl -f -o cat /usr/bin/gnome-shell
```
3. **Switch back to GDM and log in**
After your log in fails, switch back to the TTY running `journalctl` and see if you can determine the problem in your code. If you can, you may be able to correct the problem using `nano` or `vim` from the command-line.
If you fail to diagnose the problem, or you find it easier to review your code in a GUI editor, you can simply move your extension directory up one directory. This will prevent your extension from being loaded, without losing any of your code:
```sh
$ mv ~/.local/share/gnome-shell/extensions/example@shell.gnome.org ~/.local/share/gnome-shell/
```
---
title: Preferences
---
# Preferences
Our preferences dialog will be written in [Gtk][gtk], which gives us a lot of options for how we present settings to the user. You may consider looking through the GNOME Human Interface Guidelines for ideas or guidance.
- [GSettings](#gsettings)
- [Creating the Schema](#creating-the-schema)
- [Compiling the Schema](#compiling-the-schema)
- [Integrating GSettings](#integrating-gsettings)
- [Preferences Window](#preferences-window)
## GSettings
[GSettings][gsettings] provides a simple, extremely fast API for storing application settings, that can also be used by GNOME Shell extensions.
### Creating the schema
Schema files describe the types and default values of a particular group of settings, using the same type format as [GVariant][gvariant-format]. The first thing to do is create a subdirectory for your settings schema and open the schema file in your editor:
```sh
$ mkdir schemas/
$ gedit schemas/org.gnome.shell.extensions.example.gschema.xml
```
Then using your edit, create a schema describing the settings for your extension:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id="org.gnome.shell.extensions.example" path="/org/gnome/shell/extensions/example/">
<!-- See also: https://developer.gnome.org/glib/stable/gvariant-format-strings.html -->
<key name="show-indicator" type="b">
<default>true</default>
</key>
</schema>
</schemalist>
```
In the case of GSchema IDs, it is convention to use the above `id` and `path` form so that all GSettings for extensions can be found in a common place.
### Compiling the schema
Once you are done defining you schema, you must compile it before it can be used:
```sh
$ glib-compile-schemas schemas/
$ ls schemas
example.gschema.xml gschemas.compiled
```
### Integrating GSettings
Now that our GSettings schema is compiled and ready to be used, we'll integrate it into our extension:
```js
const Gio = imports.gi.Gio;
const St = imports.gi.St;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
class Extension {
constructor() {
this._indicator = null;
this.settings = ExtensionUtils.getSettings(
'org.gnome.shell.extensions.example');
}
enable() {
log(`enabling ${Me.metadata.name}`);
let indicatorName = `${Me.metadata.name} Indicator`;
// Create a panel button
this._indicator = new PanelMenu.Button(0.0, indicatorName, false);
// Add an icon
let icon = new St.Icon({
gicon: new Gio.ThemedIcon({name: 'face-laugh-symbolic'}),
style_class: 'system-status-icon'
});
this._indicator.add_child(icon);
// Bind our indicator visibility to the GSettings value
//
// NOTE: Binding properties only works with GProperties (properties
// registered on a GObject class), not native JavaScript properties
this.settings.bind(
'show-indicator',
this._indicator,
'visible',
Gio.SettingsBindFlags.DEFAULT
);
Main.panel.addToStatusArea(indicatorName, this._indicator);
}
disable() {
log(`disabling ${Me.metadata.name}`);
this._indicator.destroy();
this._indicator = null;
}
}
function init() {
log(`initializing ${Me.metadata.name}`);
return new Extension();
}
```
Now save `extension.js` and restart GNOME Shell to load the changes to your extension.
## Preferences Window
Now that we have GSettings for our extension, we will give the use some control by creating a simple preference dialog. Start by creating the `prefs.js` file and opening it in your text editor:
```sh
$ gedit prefs.js
```
Then we'll create a simple grid with a title, label and button for resetting our saved settings:
```js
'use strict';
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
function init() {
}
function buildPrefsWidget() {
// Copy the same GSettings code from `extension.js`
this.settings = ExtensionUtils.getSettings(
'org.gnome.shell.extensions.example');
// Create a parent widget that we'll return from this function
let prefsWidget = new Gtk.Grid({
margin: 18,
column_spacing: 12,
row_spacing: 12,
visible: true
});
// Add a simple title and add it to the prefsWidget
let title = new Gtk.Label({
label: `<b>${Me.metadata.name} Preferences</b>`,
halign: Gtk.Align.START,
use_markup: true,
visible: true
});
prefsWidget.attach(title, 0, 0, 2, 1);
// Create a label & switch for `show-indicator`
let toggleLabel = new Gtk.Label({
label: 'Show Extension Indicator:',
halign: Gtk.Align.START,
visible: true
});
prefsWidget.attach(toggleLabel, 0, 1, 1, 1);
let toggle = new Gtk.Switch({
active: this.settings.get_boolean ('show-indicator'),
halign: Gtk.Align.END,
visible: true
});
prefsWidget.attach(toggle, 1, 1, 1, 1);
// Bind the switch to the `show-indicator` key
this.settings.bind(
'show-indicator',
toggle,
'active',
Gio.SettingsBindFlags.DEFAULT
);
// Return our widget which will be added to the window
return prefsWidget;
}
```
To test the new preferences dialog, you can launch it directly from the command line:
```sh
$ gnome-extensions prefs example@shell.gnome.org
```
<img :src="$withBase('/assets/img/gnome-extensions-example-prefs.png')" />
The extension should be kept up to date with any changes that happen, because of the binding in `extension.js` watching for changes.
[gsettings]: https://gjs-docs.gnome.org/gio20-settings/
[gvariant-format]: https://developer.gnome.org/glib/stable/gvariant-format-strings.html
[gtk]: https://gjs-docs.gnome.org/gtk30/
---
title: Translations
---
# Translations
[Gettext][gettext] is a localization framework for writing multi-lingual applications that can also be used in GNOME Shell extensions.
Preparing your extension for Gettext allows your users to contribute localized translations. More information and guides to translating can be found on the [GNOME Translation Project](https://wiki.gnome.org/TranslationProject) wiki.
- [Preparing an Extension](#preparing-an-extension)
- [Initializing Translations](#initializing-translations)
- [Marking Strings for Translation](#marking-strings-for-translation)
- [Packing an Extension with Translations](#packing-an-extension-with-translations)
## Preparing an Extension
Start by creating a `po` directory for the translation template and translated languages:
```sh
$ cd ~/.local/share/gnome-shell/extensions
$ mkdir example@shell.gnome.org/po
```
## Initializing Translations
Your extension must be configured to initialize translations when it's loaded. This only has to be done once, so the `init()` function in `extension.js` (and `prefs.js`) is the perfect place to do this:
```js
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
function init() {
// ExtensionUtils has a convenient function for doing this
ExtensionUtils.initTranslations(Me.metadata.uuid);
return new Extension();
}
```
## Marking Strings for Translation
You also need to tell Gettext what strings need to be translated. Gettext functions retrieve the translation during run-time, but also mark strings as translatable for the scanner.
* **`gettext()`**
This function is the most commonly used function, and is passed a single string.
* **`ngettext()`**
This function is the other function you are likely to use, and is meant for strings that may or may not be plural like *"1 Apple"* and *"2 Apples"*.