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.