Overview
Plugins let you add custom functionality to Char as first-class tabs. A plugin ships a JavaScript bundle that Char loads at startup, then your plugin registers one or more tab views.
The current plugin system is intentionally minimal and focused on getting a clean foundation in place.
Plugin Structure
A typical plugin looks like this:
my-plugin/
├── plugin.json # Plugin manifest
├── src/main.tsx # Source file (optional)
├── build.mjs # Local build script (optional)
└── dist/
└── main.js # Built plugin bundle (IIFE)
Manifest
The plugin.json file defines the plugin metadata and entry script:
{
"id": "my-plugin",
"name": "My Plugin",
"version": "0.1.0",
"main": "dist/main.js"
}
Manifest fields:
id: unique plugin identifier (lowercase, hyphenated)name: display name in the UIversion: semantic versionmain: relative path to the built bundle
Lifecycle
Plugins register themselves by calling window.__char_plugins.register(...).
The registered object supports:
onload(ctx): called after the plugin script is loadedonunload(): optional cleanup hook (reserved for future plugin teardown)
PluginContext
onload receives a context object with:
registerView(viewId, factory): register a React view factory for a plugin tabopenTab(pluginId?, state?): open a plugin tab
Example:
const React = window.__char_react;
function MyPluginView() {
const [count, setCount] = React.useState(0);
return (
<div>
<h1>My plugin view</h1>
<button onClick={() => setCount((value) => value + 1)} type="button">
Count: {count}
</button>
</div>
);
}
window.__char_plugins.register({
id: "my-plugin",
onload(ctx) {
ctx.registerView("my-plugin", () => <MyPluginView />);
ctx.openTab("my-plugin");
},
});
Available Globals
Plugins run in the same webview context as the app. Char currently injects:
window.__char_react: shared React instance from the appwindow.__char_plugins.register(...): plugin registration API
Use the shared React instance instead of bundling a separate React copy.
Building
Bundle your plugin entry file to an IIFE script and point plugin.json at it.
Example build.mjs:
import * as esbuild from "esbuild";
await esbuild.build({
bundle: true,
entryPoints: ["src/main.tsx"],
format: "iife",
outfile: "dist/main.js",
platform: "browser",
});
Development Workflow
1. Create plugin directory
mkdir -p examples/plugins/my-plugin
cd examples/plugins/my-plugin
2. Create manifest
Create plugin.json with your plugin metadata.
3. Create plugin source
Create src/main.tsx and register your plugin from that entry file.
4. Build
Bundle src/main.tsx into dist/main.js.
5. Install for development
Install your plugin into Char's plugin directory:
# helper command in this repository
pnpm run plugin:hello-world:install
# or manually (macOS)
cp -r examples/plugins/my-plugin ~/Library/Application\ Support/com.hyprnote.dev/plugins/
# Linux
cp -r examples/plugins/my-plugin ~/.local/share/com.hyprnote.dev/plugins/
# Windows
cp -r examples/plugins/my-plugin %APPDATA%/com.hyprnote.dev/plugins/
6. Test
Launch Char in development mode. Your plugin's onload can open its own tab with ctx.openTab(...).
Example: Hello World
The reference plugin lives at examples/plugins/hello-world and demonstrates:
plugin.jsonmanifestwindow.__char_plugins.register(...)onload(ctx)lifecycle hookregisterView(...)+openTab(...)- React rendering through
window.__char_react
Try it:
pnpm --dir examples/plugins/hello-world build
pnpm run plugin:hello-world:install
ONBOARDING=0 pnpm -F @hypr/desktop tauri:dev