Developers

Plugins

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 UI
  • version: semantic version
  • main: 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 loaded
  • onunload(): 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 tab
  • openTab(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 app
  • window.__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.json manifest
  • window.__char_plugins.register(...)
  • onload(ctx) lifecycle hook
  • registerView(...) + 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