Toolbar Plugins тАФ @seelen/fancy-toolbar

This is one concrete example of the generic Plugin mechanism described in plugin guidelines: @seelen/fancy-toolbar is the target widget, and this page documents its schema for plugin, and its rules for parsing and executing that data. None of this is special-cased in Seelen UI's core тАФ the toolbar widget owns all of it.


1. The plugin Payload тАФ ToolbarItem

src/static/widgets/notifications/toolbar-plugin.yml
id: "@seelen/tb-notifications"
target: "@seelen/fancy-toolbar"
plugin:
  scopes:
    - Notifications
  template: >-
    return [
      dndActive ? icon("TbZzz") : null,
      count > 0 ? icon("MdNotificationsActive") : icon("MdOutlineNotifications"),
    ]
  badge: "return count > 0 ? count : null"
  tooltip: 'return [t("placeholder.notifications"), ": ", count]'
  onClickV2: |-
    trigger("@seelen/notifications");

The full field set (scopes, template, render, canvasSize, tooltip, badge, onClick/onClickV2, onWheelUp, onWheelDown, style, remoteData) is shown in the sections below.

Every field except scopes and template is optional. template, tooltip, badge, onClick, onWheelUp, and onWheelDown are all JS function bodies written as plain strings (typically pulled in with !include from a .js file) тАФ not JSON, not a declarative object. The toolbar widget compiles and runs each one in a sandbox.

Legacy alias: onClickV2 is accepted as an alternate key for onClick тАФ several bundled plugins use it, both work identically.


2. scopes тАФ What Data Gets Injected

Each entry in scopes (case-insensitive) makes a set of fields available inside template/tooltip/badge/ onClick as top-level variables тАФ no scope. prefix needed.

Scope Injected variables Backing command(s)
Date date (formatted string) local reactive clock
Notifications count, dndActive GetNotifications, GetNotificationsMode
Media defaultOutputDevice, defaultInputDevice, volume, isMuted, inputVolume, inputIsMuted, mediaSession GetMediaSessions, GetMediaDevices
Network online, interfaces (NetworkAdapter[]), usingInterface GetNetworkInternetConnection, GetNetworkAdapters, GetNetworkDefaultLocalIp
Keyboard activeLang, activeKeyboard, activeLangPrefix, activeKeyboardPrefix, languages, imeState SystemGetLanguages, SystemGetImeState
User user (includes computed displayName) GetUser
Bluetooth devices, getIconNameForBTDevice(device) GetBluetoothDevices
Power power (PowerStatus), powerMode, batteries (Battery[]) GetPowerStatus, GetPowerMode, GetBatteries
FocusedApp focusedApp GetFocusedApp
Workspaces workspaces, activeWorkspace StateGetVirtualDesktops
Disk disks (Disk[]) GetSystemDisks
NetworkStatistics networkStatistics GetSystemNetwork
Memory memory (Memory) GetSystemMemory
Cpu cores (Core[]) GetSystemCores
Tray trayIcons GetSystemTrayIcons
TrashBin trashBinInfo (TrashBinInfo) GetTrashBinInfo
Waveform waveform (AudioWaveform) GetMediaWaveform

Shapes of the more structured values (generated TS, libs/core/gen/types/):

type Memory = { total: number; free: number; swapTotal: number; swapFree: number };
type Core = { name: string; brand: string; usage: number; frequency: number };
type Disk = {
  name: string;
  fileSystem: string;
  totalSpace: number;
  availableSpace: number;
  mountPoint: string;
  isRemovable: boolean;
  readBytes: number;
  writtenBytes: number;
};
type PowerStatus = {
  acLineStatus: string;
  batteryFlag: string;
  batteryLifePercent: number;
  systemStatusFlag: string;
  batteryLifeTime: number;
  batteryFullLifeTime: number;
};
type Battery = {
  vendor: string;
  model: string;
  serialNumber: string;
  technology: string;
  state: string;
  capacity: number;
  temperature: number;
  percentage: number;
  cycleCount: number;
  smartCharging: boolean;
  energy: number;
  energyFull: number;
  energyFullDesign: number;
  energyRate: number;
  voltage: number;
  timeToFull: number;
  timeToEmpty: number;
};
type NetworkAdapter = {
  name: string;
  description: string;
  status: string;
  dnsSuffix: string;
  type: string;
  ipv6: string[];
  ipv4: string[];
  gateway: string | null;
  mac: string | null;
};
type TrashBinInfo = { itemCount: number; sizeInBytes: number };

3. remoteData тАФ Fetching External Data Into Scope

remoteData:
  weather:
    url: "https://api.example.com/weather"
    requestInit: null
    updateIntervalSeconds: 600

For each key, the toolbar fetch()es url (with the given requestInit, if any), parses the response as JSON or text depending on Content-Type, and injects the result under that key into the script scope тАФ so the example above makes a weather variable available inside template. If updateIntervalSeconds is set, the fetch repeats on that interval.


4. Execution Model тАФ What Each Script Can Do

All scripts run inside a JS sandbox (@nyariv/sandboxjs), not raw eval. There are two categories of script with two different scopes:

Content scripts тАФ template, tooltip, badge

Run with { ...resolvedScopes, ...resolvedRemoteData, t }, where t(key, args) is the i18n lookup function. You must return a value тАФ the return value becomes the rendered content:

  • Strings, numbers, and booleans are stringified directly.
  • Arrays are flattened and each item rendered in sequence.
  • Special tagged objects render as real UI elements, produced by helper functions available in scope:
    • icon(name, size) / Icon(...) тАФ an icon from the bundled icon set
    • AppIcon(...) тАФ an application icon
    • Image(...) тАФ an image element
    • Button(...) тАФ a clickable button element
    • Group(...) тАФ a container grouping other elements
src/static/plugins/tb_cpu_usage/plugin/template.js
const totalUsage = cores.reduce((total, core) => total + core.usage, 0);
const used = totalUsage / cores.length;
return [icon("LuCpu"), " ", used.toFixed(0) + "%"];

Canvas script тАФ render

render is an alternative to template for cases where a plugin needs pixel-level control (custom icons, waveforms, sparklines, etc.) instead of composing icon()/Image()/Button()/Group() elements. If a plugin sets render, it takes priority over template for the item's main content тАФ template is ignored in that case. tooltip, badge, and the action scripts are unaffected and keep working exactly as described elsewhere on this page.

Unlike the dock (see Dock Plugins тАФ ┬з2), toolbar items have no fixed size тАФ width is normally intrinsic to content тАФ so a render-based item needs an explicit width to give the canvas something to draw into:

  • canvasSize (number, optional) тАФ width in pixels of the canvas. If omitted, the item is drawn square (width = height).
  • The canvas height always tracks the toolbar's configured item size (--config-item-size), the same value every other toolbar item's height derives from.

The scope contract for render is otherwise identical to the dock's: it runs with the resolved scopes/remoteData plus isDarkMode, systemTokens/themeTokens (CSS custom-property color values as plain strings), and a canvas object ({ getContext(contextId), width, height }). See Dock Plugins тАФ ┬з2.1 for the full drawing API and an example that draws with ctx.arc/lineTo/stroke/fill.

plugin:
  scopes: [Waveform]
  render: !include render.js
  canvasSize: 40

Action scripts тАФ onClick, onWheelUp, onWheelDown

Run with { ...resolvedScopes, ...resolvedRemoteData, invoke, open, trigger }. Return value is ignored тАФ these are fire-and-forget handlers.

  • invoke(command, args) тАФ whitelisted: only a small allow-list of SeelenCommands can be called this way (e.g. SwitchWorkspace, SetVolumeLevel, OpenFile). This is not the full command surface тАФ it exists so toolbar items can trigger a handful of safe system actions without a full IPC bridge.
  • open(path) тАФ opens a file, URL, or ms-settings: shell URI with the OS default handler.
  • trigger(widgetId) тАФ pops up another widget (typically a Popup-preset widget like @seelen/bluetooth-popup) anchored at this toolbar item's screen position. This is how toolbar items open their associated popups.
src/static/widgets/bluetooth-popup/toolbar-plugin.yml
plugin:
  scopes: [Bluetooth]
  tooltip: |-
    return "Bluetooth";
  template: !include plugin.js
  onClickV2: |-
    trigger("@seelen/bluetooth-popup");

Using open():

src/static/plugins/tb_default_power/metadata.yml
plugin:
  scopes: [Power]
  tooltip: !include plugin/tooltip.js
  template: !include plugin/template.js
  onClickV2: open("ms-settings:powersleep")

5. style

A plain object merged onto the item's container as inline styles, keyed like a React style prop ({ "margin-left": "4px" }, numbers allowed for unitless properties).


6. Referencing an Installed Plugin vs. Writing Inline

In the Fancy Toolbar's own settings, a toolbar item slot accepts either an inline ToolbarItem object (as shown above) or a plain string тАФ the id of an already-installed Plugin resource targeting @seelen/fancy-toolbar. Both forms resolve to the exact same ToolbarItem shape by the time it reaches the execution engine described in section 4.