7.8 KiB
Section: Plugins Order: 200
Plugin system
Orbit has a small, operator-controlled plugin system. A deployment lists
plugin scripts in config.json; each is loaded at startup and registers against
a global Orbit object to hook events, read state, send IRC, theme the UI, and
add UI in predefined slots.
Experimental. Orbit is a work in progress and this API may change between releases. Plugins are deployment-controlled — same trust level as the app itself. There is no user-uploaded plugin marketplace, by design.
Enabling plugins
Add script URLs to plugins in config.json:
{ "plugins": ["/app/plugins/orbit-demo.js"] }
They load in order, after the app boots. Host them anywhere the page can reach (same-origin recommended). For a third-party origin, pin the file with Subresource Integrity by giving an object instead of a URL:
{ "plugins": [{ "url": "https://cdn.example/x.js", "integrity": "sha384-…" }] }
(crossorigin defaults to anonymous when an integrity hash is set.)
A quick plugin
A plugin is a plain .js file that calls Orbit.plugin(). UI is authored with
the html tagged template (runtime markup, no build step), or with
orbit.h(...) (React.createElement).
Orbit.plugin('my-plugin', (orbit, log) => {
log('loaded, Orbit v' + orbit.version);
orbit.on('message', (m) => log(m.from, 'said', m.text, 'in', m.target));
orbit.addUi('composer_button', () =>
orbit.html`<button class="composer__emoji" title="Shrug"
onClick=${() => orbit.irc.say('¯\\_(ツ)_/¯')}>🤷</button>`);
});
For anything substantial — real components with state — build a compiled plugin instead. See Compiled plugins.
The Orbit API
Global
| Member | Description |
|---|---|
Orbit.version / Orbit.commit |
app version + git commit (build-time) |
Orbit.apiVersion |
plugin API contract version (bumped on breaking changes) — guard with if (Orbit.apiVersion < N) … |
Orbit.plugin(name, fn) |
register a plugin; fn(orbit, log) |
Orbit.on/once/off/emit(event, …) |
the app event bus |
Orbit.config() |
the resolved runtime config |
Orbit.React / Orbit.ReactDOM / Orbit.jsxRuntime / Orbit.Fragment / Orbit.h / Orbit.html |
render primitives (externalization targets for compiled plugins) |
Inside Orbit.plugin(name, (orbit) => …)
| Member | Description |
|---|---|
orbit.on/once/off/emit |
event bus (see events below) |
orbit.state.active() |
active buffer name |
orbit.state.nick() / account() |
your nick / logged-in account |
orbit.state.buffers() |
open buffer names |
orbit.state.get() |
full store snapshot (read-only) |
orbit.irc.send(line) |
send a raw IRC line |
orbit.irc.msg(target, text) |
PRIVMSG a target |
orbit.irc.say(text) |
send to the active buffer |
orbit.irc.join(chan) / part(chan) |
join / part |
orbit.irc.list() |
request the channel list |
orbit.themes.current()/list()/set(id) |
read/set the theme |
orbit.storage.get(key, def)/set(key, val) |
namespaced persistence |
orbit.config() |
the resolved runtime config (branding, features, …) |
orbit.i18n.language() |
current UI language code (e.g. es, pt-BR) |
orbit.i18n.t(key, opts?) |
translate an app locale key (supports {{interpolation}}) |
orbit.i18n.pick(table) |
pick a string from a { lang: text } table by the current language |
orbit.addUi(slot, render) |
add UI to a slot (returns a remover) |
orbit.addSettingsSection({label, icon?, render}) |
add a whole Settings section |
orbit.addMessageDecorator(m => …) |
inline UI after every message's text; m = {id, nick, text, raw, kind, ts, mine} (text is formatting-stripped, raw keeps mIRC codes) |
orbit.addMessageAction(m => …) |
a button in every message's hover action toolbar (next to reply/react); same m |
orbit.addCommand(name, {run, help?}) |
register a /slash command — run(args, rest); built-ins take priority |
orbit.addShortcut(combo, run) |
register a keyboard shortcut, e.g. "mod+shift+k" (mod = Cmd/Ctrl) |
orbit.addTheme(id, {name, vars, css?, icon?, color?}) |
register a runtime theme; shows in Settings → Appearance |
orbit.notify(title, body?) |
fire a brand-styled desktop notification (asks permission once) |
orbit.modal(render, {title?, wide?}) |
open a modal dialog (core owns the shell); returns a close function |
orbit.h / orbit.html |
render helpers |
log(…) |
namespaced console logger |
Events
ready, connected ({nick}), status (connection status string),
buffer.active (buffer name), message ({from, target, text, self}),
raw (the parsed IRC message).
UI slots
| Slot | Where |
|---|---|
composer_button |
a button in the message composer toolbar |
topbar_item |
an item in the channel topbar action row (next to search / notifications) |
sidebar_item |
an item in the conversation sidebar header (next to the compose button) |
footer_item |
a button in the app footer bar's actions (next to the away / settings buttons) — style it with the appbar__act class to match |
settings_section |
a whole section in Settings (own nav entry + pane) — use orbit.addSettingsSection() |
Two per-message hooks run for every rendered message:
orbit.addMessageDecorator(m => …) appends inline UI after the text, and
orbit.addMessageAction(m => …) adds a button to the hover action toolbar next
to reply/react. Every contributed slot, action and decorator renders inside its
own error boundary, so a crashing plugin renders nothing instead of taking down
the app.
Commands, shortcuts and themes hang off the same api:
orbit.addCommand('roll', {
help: 'Roll a die: /roll [sides]',
run: (args) => orbit.irc.say('rolled ' + (1 + Math.floor(Math.random() * (+args[0] || 6)))),
});
orbit.addShortcut('mod+shift+r', () => orbit.notify('Orbit', 'shortcut fired'));
orbit.addTheme('midnight', {
name: 'Midnight',
color: '#0b0f1a',
vars: { '--bg': '#0b0f1a', '--accent': '#6ea8fe' },
});
A /command you register loses to a built-in of the same name. mod in a
shortcut is Cmd on macOS and Ctrl elsewhere. orbit.notify fires even when the
tab is focused and asks for permission the first time.
Localization (i18n)
The UI ships in 10 languages — keep your plugin in step so it isn't stuck in one. Two ways:
-
Self-contained — carry a
{ lang: text }table and let Orbit pick the current language:const LABEL = { en: 'Copy', fr: 'Copier', de: 'Kopieren' }; orbit.html`<button title=${orbit.i18n.pick(LABEL)}>⧉</button>`; -
Interpolated —
orbit.i18n.t('key', { name })resolves an app locale key with{{placeholders}}, so word order is correct per language. The four bundled plugins (clock, copy, invite, games) use this.
Read the string at render time, not once at load: plugin UI re-renders when the language
changes, so the text follows automatically. orbit.i18n.language() returns the current code
if you need to branch.
orbit.i18nandorbit.config()were added inapiVersion4 — guard withif (orbit.apiVersion >= 4)if your plugin must run on older builds.
Trust & security
Plugins are operator-controlled: a deployment lists them in config.json, so
they run with the same trust as the app itself. There is no user-uploaded plugin
mechanism. Orbit deliberately does not expose internal modules or runtime
component replacement — that would couple plugins to internals that are still
moving. The API above is the stable surface.
For code you don't fully trust — a community or third-party plugin — mark the
entry sandbox: true to run it in an isolated, capability-gated iframe instead.
See The sandbox.