Dock Plugins тАФ @seelen/weg

This is one concrete example of the generic Plugin mechanism described in plugin guidelines: @seelen/weg (SeelenWeg, the dock/taskbar) 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 dock widget owns all of it.

Note the distinction from pinned dock items: things like AppOrFile and Separator are the dock's built-in, config-driven item kinds тАФ they are not plugins. A dock plugin is a separate, distinct kind of dock item: a reference to an installed Plugin resource targeting @seelen/weg. The bundled Notifications widget (@seelen/dock-notifications, src/static/widgets/notifications/dock-plugin.yml) is a real, shipped example of this extension point тАФ used as the running example throughout this page.


1. The plugin Payload тАФ WegPluginItem

src/static/widgets/notifications/dock-plugin.yml
id: "@seelen/dock-notifications"
target: "@seelen/weg"
plugin:
  scopes:
    - Notifications
  render: !include dock_plugin.js
  badge: "return count > 0 ? count : null"
  tooltip: 'return "Notifications: " + count'
  onClick: |-
    trigger("@seelen/notifications");

scopes follow the same names and shapes documented in Toolbar Plugins тАФ ┬з2; the resolver is shared between the dock and the toolbar. Here Notifications injects count and dndActive into every script below.


2. render

By default render draws onto a canvas тАФ see ┬з2.1. If the plugin sets noCanvas: true, render instead just returns a string naming a custom icon key for the dock to look up, with no drawing involved тАФ that's the whole mode, nothing more to it.

2.1 Drawing onto the canvas (default)

render runs with the resolved scopes data plus isDarkMode, systemTokens/themeTokens (CSS custom-property color values as plain strings), and a canvas object ({ getContext(contextId), width: 256, height: 256 }). It draws onto canvas.getContext("2d") using the standard CanvasRenderingContext2D API тАФ the dock exports the result via canvas.toBlob() and displays it as the item's icon. Abridged for brevity:

src/static/widgets/notifications/dock_plugin.js
const ctx = canvas.getContext("2d");
const color = themeTokens.foregroundColor;

// ...draws the bell shape using ctx.arc/lineTo/stroke/fill with `color`...

if (dndActive) {
  ctx.fillStyle = color;
  ctx.font = `bold ${Math.round(canvas.height * 0.3)}px sans-serif`;
  ctx.textAlign = "center";
  ctx.fillText("zZ", canvas.width / 2, canvas.height * 0.55);
}

3. tooltip / badge

Same base scope as render, expected to return a string (or, for badge, null to hide it).

// tooltip тАФ return "Notifications: " + count
// badge   тАФ return count > 0 ? count : null

4. onClick

Executed with { ...scope, invoke, trigger }.

  • invoke(command, args) is whitelisted to a small allow-list of SeelenCommands for the dock: OpenFile, ShowDesktop, ShowStartMenu. This is a different, narrower allow-list than the toolbar's тАФ each target widget defines its own.
  • trigger(widgetId) pops up another widget anchored at the item тАФ this is how the Notifications dock plugin above opens the @seelen/notifications popup on click.

5. Context Menu Integration

If scopes includes TrashBin (case-insensitive), the dock automatically adds an "Empty Trash" entry to that item's context menu тАФ this is a dock-specific convenience tied to that one scope, not a general plugin feature.


6. Full Example тАФ A Fictitious "Happy Face" Plugin

Everything above in one made-up, self-contained plugin: it draws a smiley on the canvas, shows a fake tooltip and a fake badge count, and opens a folder when clicked. None of this data is real тАФ it's here purely to show how every field from the sections above fits together in one resource.

MyPlugin/
тФЬтФАтФА metadata.yml
тФЬтФАтФА render.js
тФЬтФАтФА tooltip.js
тФФтФАтФА badge.js
MyPlugin/metadata.yml
id: "@yourname/happy-dock"
metadata:
  displayName: Happy Face
  description: Shows a smiley in the dock. Purely decorative.
target: "@seelen/weg"
plugin:
  scopes: []
  render: !include render.js
  tooltip: !include tooltip.js
  badge: !include badge.js
  onClick: |-
    invoke(SeelenCommand.OpenFile, { path: "C:\\Users\\me\\Pictures" });
MyPlugin/render.js
const ctx = canvas.getContext("2d");
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const r = canvas.width * 0.4;

ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = isDarkMode ? "#ffe066" : "#ffb703";
ctx.fillStyle = ctx.strokeStyle;
ctx.lineWidth = 10;

// face outline
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.stroke();

// eyes
ctx.beginPath();
ctx.arc(cx - r * 0.4, cy - r * 0.25, r * 0.08, 0, Math.PI * 2);
ctx.arc(cx + r * 0.4, cy - r * 0.25, r * 0.08, 0, Math.PI * 2);
ctx.fill();

// smile
ctx.beginPath();
ctx.arc(cx, cy, r * 0.5, 0.15 * Math.PI, 0.85 * Math.PI);
ctx.stroke();
MyPlugin/tooltip.js
return "Smile! (this tooltip is fake)";
MyPlugin/badge.js
// pretend there are always 3 unread "smiles"
return 3;

Loading it with the slu CLI

While iterating, load the folder directly into a running Seelen UI instance тАФ no bundling needed:

slu resource load plugin ./MyPlugin

The smiley shows up in the dock immediately. Edit any of the four files and re-run the same command to reload it. When you're done, remove it with:

slu resource unload plugin ./MyPlugin

See resource guidelines тАФ ┬з8 for the full load/unload/bundle workflow shared by every resource kind.