orbit-client-docs/docs/plugins.md

7.5 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.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>`;
    
  • Interpolatedorbit.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.i18n and orbit.config() were added in apiVersion 4 — guard with if (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.