TypeScript and LSP
This page will guide you through creating an extension using TypeScript, which will allow autocompletion to work in your editor. The setup presented here is editor-agnostic and will work on any editor that support the Language Server Protocol (LSP) or has some internal equivalent functionality.
Creating the extension
Differently from what was previously done in the Getting Started section, the extension will now be created in a directory outside the installation folder. This is mostly because GNOME Shell does not support TypeScript, so we need a build phase to generate JavaScript from our files, and then the generated files will be copied into the installation folder.
To start, create a folder anywhere on your disk and, inside it, create the following files:
metadata.json
{
"name": "My TypeScript Extension",
"description": "An extension made with TypeScript",
"uuid": "my-extension@example.com",
"url": "https://github.com/example/my-extension",
"settings-schema": "org.gnome.shell.extensions.my-extension",
"shell-version": [
"45"
]
}
This file does not contain anything different when compared to the one created in the Getting Started, but is necessary for the extension to work. Note that the specification has a version
entry which we do not specify, since it is generated automatically by E.G.O (GNOME's online extension management system).
schemas/org.gnome.shell.extensions.my-extension.gschema.xml
Attention: this file goes inside a schemas
folder.
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<schema id="org.gnome.shell.extensions.my-extension" path="/org/gnome/shell/extensions/my-extension/">
<key name="padding-inner" type="i">
<default>8</default>
<summary>Inner padding</summary>
<description>Padding between windows</description>
</key>
<key name="animate" type="b">
<default>true</default>
<summary>Animation</summary>
<description>Whether to animate window movement/resizing</description>
</key>
</schema>
</schemalist>
This is a schema, as described in the Preferences section. It is not necessary for an extension to work, but will be used in the example to show how automate steps and how to generate the final zip for distribution.
TypeScript setup
To use TypeScript we need some setup, installing some dependencies and configuring the project to generate files correctly. In this example both the extensions.js
and prefs.js
will be generated from their TypeScript counterparts. Create the following files:
package.json
{
"name": "my-extension",
"version": "0.0.0",
"description": "A TypeScript GNOME Extension",
"type": "module",
"private": true,
"repository": {
"type": "git",
"url": "git+https://github.com/example/my-extension.git"
},
"author": "Álan Crístoffer e Sousa <acristoffers@startmail.com>",
"license": "LGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/example/my-extension/issues"
},
"homepage": "https://github.com/example/my-extension#readme",
"sideEffects": false
}
In this file, it is important to set "type": "module"
. You can set version
to whatever you want, as it is not used.
Now that you have this file in place, you can run the following to install the dependencies:
npm install --save-dev \
eslint \
eslint-plugin-jsdoc \
typescript
npm install @girs/gjs @girs/gnome-shell
tsconfig.json
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"sourceMap": false,
"strict": true,
"target": "ES2022",
"lib": [
"ES2022"
],
},
"include": [
"ambient.d.ts",
],
"files": [
"extension.ts",
"prefs.ts"
],
}
The TypeScript compiler configuration. Modifying it may break the build process.
ambient.d.ts
import "@girs/gjs";
import "@girs/gjs/dom";
import "@girs/gnome-shell/ambient";
import "@girs/gnome-shell/extensions/global";
This file makes it possible to use the usual import paths in your TypeScript files instead of referencing @girs/*
directly.
The Extension and Preferences files
All the support files are in place, so we can finally write our extension's code. Create the following files:
extension.ts
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
export default class MyExtension extends Extension {
gsettings?: Gio.Settings
animationsEnabled: boolean = true
enable() {
this.gsettings = this.getSettings();
this.animationsEnabled = this.gsettings!.get_value('padding-inner').deepUnpack() ?? 8
}
disable() {
this.gsettings = undefined;
}
}
Thanks to @girs
, the TypeScript code is basically what it would be if it were JavaScript + type information. If you have been following the tutorial, your LSP server should be able to offer information about the types in this file, like auto-complete and go-to-definition.
This is also the very minimum necessary to have a working extension: a default-exported class that extends Extension
containing the methods enable()
and disable()
. You should not create constructors/destructors, and instead initialize your extension in enable()
and finish it in disable()
. This is because your class my be constructed once and reused internally, resulting in many calls to those methods.
prefs.ts
import Gtk from 'gi://Gtk';
import Adw from 'gi://Adw';
import Gio from 'gi://Gio';
import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
export default class GnomeRectanglePreferences extends ExtensionPreferences {
_settings?: Gio.Settings
fillPreferencesWindow(window: Adw.PreferencesWindow) {
this._settings = this.getSettings();
const page = new Adw.PreferencesPage({
title: _('General'),
iconName: 'dialog-information-symbolic',
});
const animationGroup = new Adw.PreferencesGroup({
title: _('Animation'),
description: _('Configure move/resize animation'),
});
page.add(animationGroup);
const animationEnabled = new Adw.SwitchRow({
title: _('Enabled'),
subtitle: _('Wether to animate windows'),
});
animationGroup.add(animationEnabled);
const paddingGroup = new Adw.PreferencesGroup({
title: _('Paddings'),
description: _('Configure the padding between windows'),
});
page.add(paddingGroup);
const paddingInner = new Adw.SpinRow({
title: _('Inner'),
subtitle: _('Padding between windows'),
adjustment: new Gtk.Adjustment({
lower: 0,
upper: 1000,
stepIncrement: 1
})
});
paddingGroup.add(paddingInner);
window.add(page)
this._settings!.bind('animate', animationEnabled, 'active', Gio.SettingsBindFlags.DEFAULT);
this._settings!.bind('padding-inner', paddingInner, 'value', Gio.SettingsBindFlags.DEFAULT);
}
}
This is also a good example of a minimal preference pane: a default-exported class that extends ExtensionPreferences
and implements fillPreferencesWindow(window: Adw.PreferencesWindow)
. There are other methods that can be implemented instead, but this is the easiest to use. It also shows how to populate the window with some widgets which are bound to properties defined in the schema and therefore persisted.
Build and packaging automation
Since we now have a build step, it is better to automate it. Create the following file:
Makefile
NAME=my-extension
DOMAIN=example.com
.PHONY: all pack install clean
all: dist/extension.js
node_modules: package.json
npm install
dist/extension.js dist/prefs.js: node_modules
tsc
schemas/gschemas.compiled: schemas/org.gnome.shell.extensions.$(NAME).gschema.xml
glib-compile-schemas schemas
$(NAME).zip: dist/extension.js dist/prefs.js schemas/gschemas.compiled
@cp -r schemas dist/
@cp metadata.json dist/
@(cd dist && zip ../$(NAME).zip -9r .)
pack: $(NAME).zip
install: $(NAME).zip
@touch ~/.local/share/gnome-shell/extensions/$(NAME)@$(DOMAIN)
@rm -rf ~/.local/share/gnome-shell/extensions/$(NAME)@$(DOMAIN)
@mv dist ~/.local/share/gnome-shell/extensions/$(NAME)@$(DOMAIN)
clean:
@rm -rf dist node_modules $(NAME).zip
You can now run make
to compile your code and generate the files extension.js
and prefs.js
inside the dist
folder. If needed, it will install the dependencies using npm install
.
make pack
will generate a file my-extension.zip
which you can upload for review. It will compile the code and the schema, if needed, and copy the schemas
folder and the metadata.json
file into the dest
folder before zipping it.
make install
will copy the files to the extensions folder. If you logout and back in it should appear in the Extension Manager app.
Finally, make clean
removes all generated files.