Making plugins
A plugin is a folder with a plugin.json and a JavaScript file. Sail loads it into the launcher's window and hands it the Sail API.
A layout is easier, needs no code, and anyone can install it safely.
The folder#
my-plugin/
plugin.json the manifest
index.js your code
... anything else your code uses
plugin.json#
{
"id": "yourname.my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"author": "Your name",
"description": "What it does, in a sentence.",
"main": "index.js",
"platforms": ["win32", "linux"],
"settings": [
{ "type": "heading", "label": "General" },
{ "id": "greeting", "type": "text", "label": "Greeting", "default": "Hello" },
{ "id": "loud", "type": "toggle", "label": "Shout it", "default": false }
]
}
| Field | What it's for |
|---|---|
id | A unique id, yourname.plugin-name in lowercase. Needed for other plugins to depend on yours. Never reuse one for something else. |
name, version, author, description | Shown in the Plugin Manager. Bump version for every update so Sail can offer it. |
main | The JavaScript file Sail runs. |
platforms | Optional. The systems it works on: win32, linux. Sail won't start it anywhere else and says why. |
settings | Optional. Options Sail draws in a settings window (see below). |
requires | Optional. Other plugins yours needs (see below). |
The main script#
Export one function. Sail calls it with the Sail API and your plugin's own context. Return an object with dispose() that undoes everything you did, so your plugin can be switched off and on without restarting Sail.
module.exports = function (SailAPI, context) {
const button = document.createElement('button');
button.textContent = context.settings.get('greeting');
button.addEventListener('click', () => alert(`${context.name} says hi`));
document.body.append(button);
// Settings changed in Sail's settings window arrive here.
const stop = context.settings.onChange((id, value) => {
if (id === 'greeting') button.textContent = value;
});
return {
dispose() {
stop();
button.remove();
}
};
};
Plugins run in the launcher's window with Node.js available, so require('fs') and friends work. Use that power carefully: people trust you with their PC.
The context#
| Member | What it is |
|---|---|
context.name, context.version | Your plugin's name and version. |
context.folder | The name of your plugin's folder. |
context.settings.get(id) | A setting's value (its default until the user changes it). |
context.settings.getAll() | Every setting as an object. |
context.settings.set(id, value), reset(id) | Change a setting, or put it back to its default. |
context.settings.isSet(id) | Whether the user has changed it from the default. |
context.settings.onChange(callback) | Called with (id, value, all) when a setting changes. Returns a function that stops listening. |
context.settings.setChoices(id, options) | Replace a select setting's choices while Sail runs, for example with things found on the PC. |
context.settings.open() | Open your plugin's settings window. |
Settings#
List your options in settings and Sail draws the window, keeps the values on each PC, and tells you when they change.
| Type | Example |
|---|---|
toggle | { "id": "colors", "type": "toggle", "label": "Colours", "description": "…", "default": true } |
select | { "id": "size", "type": "select", "label": "Size", "default": "m", "options": [{ "value": "s", "label": "Small" }, { "value": "m", "label": "Medium" }] } |
number | { "id": "count", "type": "number", "label": "Games shown", "default": 5, "min": 1, "max": 10, "step": 1, "slider": true } |
text | { "id": "name", "type": "text", "label": "Nickname", "default": "", "maxLength": 40 } |
heading | { "type": "heading", "label": "Appearance" } |
Ids start with a letter and use letters, numbers, - and _. A value that doesn't fit its setting falls back to the default.
Depending on another plugin#
"requires": {
"someone.other-plugin": { "version": "^2.0.0", "hub": "<its Sail Hub item id>" }
}
Versions can be exact (2.0.0), ^2.0.0 (2.x from 2.0.0), ~2.0.1 (2.0.x from 2.0.1) or >=2.0.0. With hub, Sail offers to install the plugin from Sail Hub when it's missing. Your plugin starts after the ones it needs, and waits switched off while any of them is missing, off or too old.
Packaging#
Zip the folder. plugin.json can be at the top of the zip or one folder down. Test it with Import Plugin, then upload it to Sail Hub.
When you upload a plugin .zip to Sail Hub, the upload form reads its plugin.json and shows a Requirements section. Search Sail Hub for the plugins yours needs and pick them; the form fills in their ID, a version range and their Sail Hub item for you, and can give your plugin an id too. It writes all of that into plugin.json just before uploading, and leaves every other file untouched. A .rar or .7z can't be edited this way, so add requires by hand before packing those.
Last updated 21 October 2026