Skip to content

Settings and keys of a plugin

Two optional parts of the manifest make a plugin feel like part of the app: settings, which the app draws as rows under your plugin in Settings › Plugins, and shortcuts, which the app lists in its keyboard help and hands to your view.

A plugin with options, such as a diff shown inline or side by side, declares them as a JSON Schema in settings_schema:

manifest.json
{
"settings_schema": {
"type": "object",
"properties": {
"diff": {
"type": "string",
"title": "Diff",
"description": "How a file's changes are laid out",
"oneOf": [
{ "const": "inline", "title": "Inline" },
{ "const": "split", "title": "Side by side" }
],
"default": "inline"
},
"wrap": { "type": "boolean", "title": "Wrap long lines", "default": true },
"context": { "type": "integer", "title": "Context lines", "minimum": 0, "maximum": 20, "default": 3 }
}
}
}

Each property becomes one row, in the order you declare them:

In the schemaIn Settings › Plugins
titleThe row’s label.
descriptionThe line under the label.
"type": "boolean"A switch.
a string with enum, or oneOf of const values with titlesA choice. oneOf lets each value have a readable title.
"type": "integer" or "number" with minimum and maximumA number kept within those bounds.
defaultThe value until the person changes it. Required on every property.

The schema is one level deep: every property is a boolean, string, integer or number, and every property has a default. Like the payload and decision schemas, it can be inline or a $ref to a file in the plugin folder.

The values arrive in init and again whenever they change, whether the person changed them in Settings or another view did. Every key the schema declares is present, with its default under whatever the person set.

const plugin = Pinrail.connect({
onInit() { render(); },
onSettings(settings) { render(); }, // never re-initialises the view or touches its draft
});
const layout = () => plugin.settings.diff; // "inline" or "split"

A control in your view can change a setting directly. The app checks the value against your schema, keeps it, and sends the new values to every open view of the plugin, so a toggle in your view and the row in Settings change the same setting.

splitButton.onclick = () => plugin.setSetting("diff", "split");

change

setSetting

checks against settings_schema

settings

settings

the row in Settings › Plugins

the app

a toolbar in your view

every other open view

change

setSetting

checks against settings_schema

settings

settings

the row in Settings › Plugins

the app

a toolbar in your view

every other open view

One setting, two places to change it

A view can only write its own plugin’s settings. setSetting returns a promise: it resolves with the settings once the app keeps the value, and it rejects when the schema refuses it, with the reasons in the error’s violations.

Settings are stored in the app’s settings.json under plugins.<name>. A script can change one through the local API, and the app validates it the same way:

Terminal window
curl -X PATCH http://127.0.0.1:4747/api/v1/settings \
-H 'content-type: application/json' \
-d '{"plugins": {"review": {"diff": "split"}}}'

A view that answers keys declares them in shortcuts:

manifest.json
{
"shortcuts": [
{ "keys": "j", "does": "Next proposal" },
{ "keys": "k", "does": "Previous proposal" },
{ "keys": "a", "does": "Accept the focused proposal", "group": "Verdicts" },
{ "keys": "x", "does": "Reject the focused proposal", "group": "Verdicts" },
{ "keys": "cmd+shift+f", "does": "Fold every file" }
]
}

Declaring a key does two things for you:

  1. It is listed. The app’s keyboard help, opened with ?, shows your keys under your plugin whenever one of its reviews is open. Your view needs no help overlay of its own.
  2. It is forwarded. When the person presses the key with the app in focus rather than your frame, after clicking the top bar or arriving from the inbox, the app hands it to your view.
FieldMeaning
keysModifiers joined by +, in any order, then one key. The modifiers are cmd, ctrl, alt and shift; command, control and option also work, and cmdorctrl means ⌘ on macOS and Ctrl on Linux. Name the physical key. Write a letter or digit as itself (j, 1), a punctuation key as the character it types without ⇧ (/, [, ,), and any other key as its KeyboardEvent.code in lowercase (enter, escape, arrowdown). Write a shifted character with shift: ? is shift+/.
doesThe one-line label shown in the keyboard help.
groupOptional. Lists the entry under this caption.

A forwarded key arrives as a keydown on your document, exactly like a press inside the frame, so the listener you already have handles both:

document.addEventListener("keydown", (e) => {
if (e.key === "j") focusNext();
if (e.key === "a") accept(focused);
// e.pinrailForwarded is true when the app handed the key over
});

Only declared keys are forwarded. A view that declares none receives none.