Your First Plugin
Scaffold a plugin and run it on your phone.
Go from an empty folder to a plugin running on your phone. You need Bun on your computer and a phone with Unbound installed on the same network.
Quick Start
Scaffold a workspace
ubdasks for anything you leave out, such as your Discord user id.bunx @unbound-app/cli create --kind plugin --name hello-world --shape repository --git-init --install cd hello-worldStart the bridge
Leave it running in its own terminal. It prints a
Devicesaddress, such as192.168.1.20:9090.bunx @unbound-app/debuggerConnect your phone
- In Discord, open Unbound settings, then General, and turn on Developer Mode.
- Open Developer and turn on Enable Debugger.
- Enter the
Devicesaddress in Debugger Address. - Restart Discord.
The bridge prints
Device connected.Push the plugin
pushbuilds, installs and starts the plugin. It greets you with a toast.bunx @unbound-app/cli debug push hello-worldRebuild on save
devrebuilds the plugin and reloads it on the phone each time you save.bun run dev
What You Got
The plugin lives in plugins/hello-world. Plugin Anatomy explains each file.
- hello-world
- manifest.jsonId, name and version
- ADDON.mdThe addon page
- settings.tsSettings; Unbound builds the panel
- i18n
- en-US.jsonEvery string the plugin shows
- assets
- icon.pngThe toast's icon
- mediaAddon page images
- src
- index.tsxStarts each feature
- unbound-i18n.d.tsGenerated. Commit it
- unbound-settings.d.tsGenerated. Commit it
- bunfig.tomlLoads the testing preset
- tests
- index.test.tsRuns with bun test
The entry exports start. It reads settings from @unbound-app/settings, which the build makes from settings.ts. toasts.success takes a message key and its placeholder values, and an imported image is its asset id.
import { settings } from '@unbound-app/settings';
import { toasts } from '@unbound-app/api';
import icon from '../assets/icon.png';
/** Greets the user, unless they turned the greeting off. */
export function start() {
if (!settings.greetOnStart) return;
toasts.success('GREETING_BODY', { name: addon.name }, { icon });
}settings.ts declares one switch. Its title and subtitle are message keys.
import { defineSettings, setting } from '@unbound-app/settings/schema';
export default defineSettings({
greetOnStart: setting.boolean({
default: true,
title: 'GREET_ON_START',
subtitle: 'GREET_ON_START_SUBTITLE',
}),
});The test starts the plugin and checks its toast. Run it with bun test.
import { startPlugin, stopPlugin, toasts } from '@unbound-app/api/testing';
import { settings } from '@unbound-app/settings';
import { describe, expect, test } from 'bun:test';
describe('start', () => {
test('greets with the plugin name', async () => {
await startPlugin();
expect(toasts.shown).toMatchObject([{ key: 'GREETING_BODY', title: 'Hello World is running.' }]);
await stopPlugin();
});
test('stays quiet when greetOnStart is off', async () => {
settings.greetOnStart = false;
await startPlugin();
expect(toasts.shown).toEqual([]);
await stopPlugin();
});
});The scaffold also has a .gitignore and an .oxlintrc.json with the lint rules.
Change Discord
Add a feature that logs every message you send, in its own folder:
import { metro, patcher } from '@unbound-app/api';
export function startMessageLog() {
const Messages = metro.findByProps('sendMessage', 'receiveMessage');
if (!Messages) {
logger.warn('Message log skipped: Discord moved sendMessage.');
return;
}
patcher.before(Messages, 'sendMessage', ({ args }) => {
logger.log('Sending a message to channel', args[0]);
});
}| Call | What it does |
|---|---|
metro.findByProps | Finds the module with both exports. Lazy. undefined when Discord moved it |
patcher.before | Runs your callback before each sendMessage, with its arguments |
logger | A global that prefixes each line with the plugin's name |
Start it from the entry:
import { settings } from '@unbound-app/settings';
import { toasts } from '@unbound-app/api';
import { startMessageLog } from '~/message-log';
import icon from '../assets/icon.png';
/** Starts the message log, and greets the user unless they turned the greeting off. */
export function start() {
startMessageLog();
if (!settings.greetOnStart) return;
toasts.success('GREETING_BODY', { name: addon.name }, { icon });
}Save, then send a message on your phone. The log line appears in the bridge's terminal.