Skip to content
Vimputer
Esc
↑↓navigate↵open⌘Jpreview
On this page

Plugin runtime

How a plugin is written, loaded, and run.

A plugin is an ES module whose default export is definePlugin(…) from @vimputer/plugin-sdk. It runs in Electron main, trusted, with no sandbox. The host owns persistence, the picker, and how actions are drawn; the plugin supplies data and code.

import { z } from 'zod'
import { defineAction, definePlugin } from '@vimputer/plugin-sdk'

const run = defineAction({
  id: 'run',
  label: 'Example command',
  paramsSchema: z.object({ url: z.string() }),
  async run(ctx, { url }) {
    await ctx.openUrl(url)
  },
  // Pure: recognize an imported action as this one, or pass.
  claim: (action) => (action.kind === 'open_url' && action.url.startsWith('example://') ? { url: action.url } : null),
})

export default definePlugin({
  id: 'example',
  label: 'Example',
  actions: [run],
  async discover(ctx) {
    return { groups: [{ id: 'all', label: 'Commands', items: [/* { id, title, actionId: 'run', actionParams } */] }] }
  },
})

What a plugin can export

Export Does
actions Code that runs when a bound key fires. paramsSchema validates the binding’s params.
discover(ctx) The external inventory as groups of items, each naming an action and its params.
claim on an action Rewrites a matching imported action into this plugin’s action. Must be pure.
views Pages and author dialogs, written in React against ten host elements.
activate(ctx) A long-lived service, such as a socket. Returns { dispose() }.

Discovery is stale-while-revalidate: the last snapshot, persisted to disk, is served at once and refreshed in the background. Saving a plugin instance, or a service calling invalidateDiscovery(), forces a fresh scan.

The action context

Every run, discover, and service gets a ctx:

Member For
exec(file, args) Run a binary with argv, never through a shell
fetch, openUrl, notify HTTP, URLs, notifications
instances Plugin-owned records, such as Soundboard’s boards
secrets Passwords and keys, encrypted with safeStorage
assets Icons and imported files, served over plugin-asset://
dialog, fs, logger File pickers, scoped filesystem reads, logging

Views

A plugin’s React tree is reconciled in main and streamed to the renderer as a snapshot, which draws it with @vimputer/ui. The vocabulary is page, section, list, list.item, text-field, button, audio, drop-zone, empty, and status. Page views appear under Settings → Integrations; author views open inside Add from plugin.

Loading

  • First-party: modules under apps/desktop/plugins/<id>/, imported directly in apps/desktop/src/main/index.ts. No plugin.json.
  • Local: a folder in ~/Library/Application Support/Vimputer/plugins/<id>/ with a plugin.json giving id and entry, the path to a built JS module. There is no install UI.

Services start when the app starts or the plugin is enabled, restart on reload, and are disposed when the plugin is disabled or the app quits.

Layers from plugins

Add from plugin writes a group’s items into the current layer as ordinary plugin_action bindings and records the source in the layer’s pluginStamps. Re-picking a stamped group adds only items whose action and params match no binding in the layer. Nothing is ever removed or overwritten.

Reference implementations: Shottr is the smallest (URL scheme, no service). OBS and Paseo are live services. Soundboard builds its inventory from instances and views.