Not stable yet. WolOS is in active development and the current ISO is a test build: try it in a virtual machine or on a spare disk, not on a computer you depend on.

WolOS
Manual contents

Make it yours

Plugin API reference

The complete manifest, permissions and Lua API for shell plugins.

What it is

This is the full reference for WolOS shell plugins: the plugin.toml manifest, every section it can contain, the permissions, the Lua API and the wol plugin commands. The Plugins page has the short version.

Why

One document for the whole API means an agent or a person can write a working plugin from this page alone. Every TOML and Lua block below is parsed and run by WolOS's own tests.

How

Start with wol plugin new <id>, edit, then wol plugin check <id> until it passes, wol plugin run to see what it draws, and wol plugin enable to put it on your desktop.

Reference

Decision: ADR 0031. Code: crates/shared/src/plugins.rs (manifest contract, parse, the schema), crates/shared/src/layout.rs (bar and Dock slots plugin items land in), crates/wol-lua/src/plugin.rs (Lua API v1 in the widget sandbox), crates/core/src/plugins.rs (folders, check, grants, state, toggles, failure counter, scaffold, pack), crates/controller/src/plugin.rs (wol plugin). Reference plugin: plugins/shell/org.wolos.stats/. Schema: docs/plugins/plugin.schema.json, installed at /usr/share/wolos/plugins.schema.json.

This is the one document for the whole plugin API. Every TOML and Lua block in it is parsed and run by crates/controller/tests/plugin_docs.rs, so what you read here is what the code accepts.

What a plugin is

A plugin is a folder named after its id with a plugin.toml and the files it names. Nothing else: no install hooks, no binaries, no symlinks, at most 1 MiB. The shell looks in ~/.local/share/wolos/plugins/<id>/ (yours, reloaded when it changes) and /usr/share/wolos/plugins/<id>/ (shipped or installed through wopkg). A plugin may contribute, through sections of its manifest: a menu bar item, a Dock entry, a desktop card, a panel, search results, a Control Center tile, a toggle, a hotkey, a web app, a theme, a Looks preset and Hyprland window rules. Code is Lua, run in the sandbox of ADR 0020 (wol-lua: 16 MiB, 5 million instructions per load, render or click, no io, os, require, debug, loaders or bytecode). A plugin cannot leave the HIG: it returns Kit view trees and the shell draws them. It reaches the system only through the calls below and, for anything that changes the system, through wol verbs, which pass the helper policy and the audit log under the plugin's id (Settings > Assistants & Automation).

Ids are reverse-DNS (org.example.stats) or <user>.<name> (alice.stats): lowercase labels of letters, digits and -, joined by dots. org.wolos.* is reserved for plugins WolOS ships.

The manifest, plugin.toml (schema 1)

schema = 1
id = "alice.stats"
name = "Stats"
version = "1.0"
author = "Alice"
license = "MIT"
description = "CPU and memory in the menu bar, with a desktop card."
min_wolos = "0.12"
permissions = ["run"]

[[menubar]]
id = "bar"
script = "bar.lua"
side = "right"

[[widget]]
id = "card"
script = "card.lua"
sizes = ["small", "medium"]

[[hotkey]]
id = "top"
default = "super+shift+t"
exec = ["foot", "-e", "btop"]
group = "Stats"

Header keys, in this order:

KeyRequiredMeaning
schemayes1. A newer number is refused with a sentence.
idyesThe plugin id; also the folder name.
nameyesOne line, up to 40 characters.
versionyesLetters, digits and . _ + ~ -, like "1.0".
authornoOne line, up to 40 characters.
licenseyesAn SPDX id, like "MIT" or "GPL-2.0-or-later".
descriptionnoOne line, up to 120 characters.
min_wolosnoLowest WolOS version with every API the plugin uses, like "0.12".
permissionsnoAny of notifications, run, http, clipboard, open, toggles (below).

Then one [[section]] per contribution. Every entry has an id (1 to 32 lowercase letters, digits, -, _), unique across the whole plugin; the shell refers to it as <plugin id>/<entry id>. Unknown keys and unknown sections are load errors, each a sentence naming the place ([[menubar]] 1: unknown key "colour"; it takes id, script, side.). Files are relative paths inside the folder, no .., no hidden names.

SectionKeysNotes
[[menubar]]script, sideA bar item. side is left, centre or right (default). The script returns wol.menubar.
[[dock]]name, icon, execA Dock tile that runs exec (argv, never a shell line).
[[widget]]script, sizesA desktop card (today's lua:<id>). sizes: any of small, medium, large (default all).
[[panel]]script, anchorA sheet the shell shows on wol.show("<id>") or a hotkey. anchor: top (default), bottom, left, right, centre.
[[search]]script, kind, prefixSearch rows. kind = "results" (default) sees every query; kind = "command" sees queries starting with prefix (1 to 8 characters, no spaces).
[[tile]]icon, label, toggleA Control Center tile bound to a [[toggle]] of this plugin.
[[toggle]]on, offTwo argv commands; the state is a flag file in $XDG_STATE_HOME/wolos/toggles/<plugin>.<id>.
[[hotkey]]default, exec, groupA user-rebindable shortcut (shortcuts::Custom). default: modifiers from super, ctrl, alt, shift, then a key (super+shift+t, super+f5, super+space). group is the heading in Settings (default: the plugin name).
[[webapp]]name, url, icon, profileA frameless browser window for an https:// URL, in Search and the Dock. profile: shared (default) or own (isolated cookies).
[[theme]]fileA theme.json (shared::theme tokens, optional extends); checked for contrast.
[[look]]fileA Looks preset JSON (wol_core::looks).
[[hypr]]filekeyword = value lines; only windowrule, layerrule, bezier, animation. Never bind, exec, env, source, plugin, $variables or { } blocks. Parsed into hyprprefs keywords, never sourced raw.

icon is a symbolic icon name or a .png/.svg in the folder.

A plugin with everything:

schema = 1
id = "alice.everything"
name = "Everything"
version = "0.3"
license = "GPL-2.0-or-later"
permissions = ["notifications", "run", "http", "clipboard", "open", "toggles"]

[[menubar]]
id = "bar"
script = "bar.lua"

[[dock]]
id = "launch"
name = "Terminal"
icon = "utilities-terminal-symbolic"
exec = ["foot"]

[[widget]]
id = "card"
script = "card.lua"

[[panel]]
id = "sheet"
script = "sheet.lua"
anchor = "centre"

[[search]]
id = "calc"
script = "calc.lua"
kind = "command"
prefix = "="

[[toggle]]
id = "awake"
on = ["wol", "power", "awake", "on"]
off = ["wol", "power", "awake", "off"]

[[tile]]
id = "awake-tile"
icon = "weather-clear-symbolic"
label = "Stay Awake"
toggle = "awake"

[[hotkey]]
id = "sheet-key"
default = "super+shift+e"
exec = ["wol", "shell", "show", "control-center"]

[[webapp]]
id = "mail"
name = "Mail"
url = "https://mail.example.com/"
icon = "mail-unread-symbolic"
profile = "own"

[[theme]]
id = "night"
file = "night.json"

[[look]]
id = "glass"
file = "glass.json"

[[hypr]]
id = "rules"
file = "rules.conf"

A plugin without code: a Control Center tile bound to a toggle, and a theme.

schema = 1
id = "alice.quiet"
name = "Quiet"
version = "1.0"
license = "MIT"
description = "A Silence tile and a dark theme."

[[toggle]]
id = "silence"
on = ["wol", "focus", "on"]
off = ["wol", "focus", "off"]

[[tile]]
id = "silence-tile"
icon = "notifications-disabled-symbolic"
label = "Silence"
toggle = "silence"

[[theme]]
id = "night"
file = "night.json"

The TOML subset: key = value lines, "strings" with \" \\ \n \t \uXXXX, 'literal strings', whole numbers, true/false, [lists] (may span lines, trailing comma allowed), [[sections]] and # comments. Dotted keys, inline tables, multi-line strings, floats and dates are refused with a sentence.

Permissions

Declared in the header, shown before the plugin is turned on (wol plugin enable, the Store sheet), stored per plugin in $XDG_CONFIG_HOME/wolos/plugins.json and revocable in Settings. A plugin that is on but whose newer manifest asks for more shows "needs a new grant" until it is enabled again. Without the permission the call does not exist (wol.run == nil), so a script can test for it.

PermissionCallShown as
notificationswol.notifyShow notifications
runwol.runRun commands as you
httpwol.http_getRead data from the web (each site asks once)
clipboardwol.clipboard.textRead the clipboard
openwol.openOpen apps, files and links
toggleswol.toggle_setSwitch its own toggles

wol.run { "wol", ... } runs the wol verb as a helper named after the plugin: the helper policy, on-screen approval and the audit log apply exactly as for an AI assistant (docs/modules/automation.md). Other programs run as you, with the limits of widget sources (own process group, CPU limit, nice 10, output cap, never a shell). wol.http_get needs a grant for each host; the first use shows the consent sentence ("Allow this plugin to read data from api.example.com.") and the card asks. No new privilege path exists: a plugin that needs more is a libwol app (ADR 0020).

Lua API v1

A script is one file that ends with return wol.<kind> { ... } matching its section. Loading a script never acts: the service calls are added after the file has run, so only render, popover, query and handlers reach them. Every service call returns ok, message (or value, message) instead of raising.

Scripts

ReturnsFields
wol.menubar { refresh, render, popover }render() is the bar item (a label, an image, a small row); popover() (optional) is shown when the item is clicked.
wol.widget { refresh, render }A desktop card; render() returns the card body.
wol.panel { title, render }A sheet; render() returns its body.
wol.search { query }query(text) returns up to 20 rows { title, subtitle, icon, on_activate }; a command entry gets the text after its prefix.

refresh: seconds between renders, 0 to 86400 (default 60; 0 = only after input). After a handler runs, the shell renders again.

-- bar.lua: CPU and memory from `wol sys stats --json` (permission "run").
local cpu, mem, err = 0, 0, nil
local function read()
  local ok, out = wol.run { "wol", "sys", "stats", "--json", timeout = 5 }
  if not ok then err = out; return end
  local v, e = wol.json.decode(out)
  if not v then err = e; return end
  err = nil
  cpu = math.floor((v.cpu_percent or 0) + 0.5)
  local total, used = (v.memory and v.memory.total) or 0, (v.memory and v.memory.used) or 0
  mem = total > 0 and math.floor(used * 100 / total + 0.5) or 0
end
return wol.menubar {
  refresh = 5,
  render = function()
    read()
    if err then return wol.label { "Stats: -" } end
    return wol.row { spacing = 8, wol.label { "CPU " .. cpu .. "%" }, wol.label { "MEM " .. mem .. "%" } }
  end,
  popover = function()
    return wol.stack { spacing = 8,
      wol.label { "Stats", role = "title" },
      wol.label { err or ("CPU " .. cpu .. "%, memory " .. mem .. "%"), role = "secondary" },
      wol.button { label = "Open Top", on_click = function() wol.run { "foot", "-e", "btop" } end },
    }
  end,
}

Parts (the view tree)

PartDraws
wol.stack { spacing = 8, a, b, ... }Children top to bottom. spacing: 0, 4, 8, 12, 16 or 24.
wol.row { spacing = 8, a, b, ... }Children left to right.
wol.list { a, b, ... }Rows with separators.
wol.item { title = "", subtitle = "", icon = "", size = 24, button = { label = "", on_click = f } }A list row: icon, title over subtitle, optional trailing button.
wol.label { "text", role = "body" }Text. Roles: display, large, title, body, secondary, caption.
wol.button { label = "Go", on_click = function() end }A button; on_click is required.
wol.spacer()Pushes what follows to the far side.
wol.image { "icon.png", size = 24 }A symbolic icon name, or an image file in the plugin folder. Sizes: 16, 24, 32, 48, 64, 96, 128.
wol.toggle { label = "Wi-Fi", on = true, on_change = function(on) end }A switch.
wol.slider { value = 50, min = 0, max = 100, on_change = function(v) end }A slider.
wol.text_field { text = "", placeholder = "Search", on_change = function(text) end }One line of text.

Limits (shared::view): 12 levels deep, 200 parts, text up to 1024 bytes. Interactive parts are numbered in tree order in the printed JSON; wol plugin run ... <n> clicks part n.

-- sheet.lua: every part once (a panel).
local on, level, name = true, 40, ""
return wol.panel {
  title = "Everything",
  render = function()
    return wol.stack { spacing = 12,
      wol.row { spacing = 8, wol.image { "emblem-default-symbolic", size = 32 }, wol.label { "Everything", role = "title" }, wol.spacer(),
        wol.button { label = "Close", on_click = function() end } },
      wol.list {
        wol.item { title = "First", subtitle = "A row", icon = "folder-symbolic" },
        wol.item { title = "Second", button = { label = "Open", on_click = function() wol.show("control-center") end } },
      },
      wol.toggle { label = "Enabled", on = on, on_change = function(v) on = v end },
      wol.slider { value = level, min = 0, max = 100, on_change = function(v) level = v end },
      wol.text_field { text = name, placeholder = "Your name", on_change = function(t) name = t end },
      wol.label { "Caption", role = "caption" },
    }
  end,
}

Services

Always present:

CallReturns
wol.now(){ year, month, day, weekday (1 = Monday), hour, minute, second }
wol.system(){ os_name, os_version, kernel, appearance ("light" or "dark"), accent, accent_rgb }
wol.json.decode(text)value, message: JSON text (up to 256 KB) as Lua tables, or nil and why.
wol.json.encode(value)text, message: strings, numbers, booleans, lists and string-keyed tables.
wol.state.get(key)The saved value (any JSON value) or nil. State is per plugin, a JSON object of at most 64 KB in $XDG_STATE_HOME/wolos/plugins/<id>/state.json, the only place a plugin writes.
wol.state.set(key, value)ok, message. nil removes the key. Saved at once.
wol.toggle_get(id)true when this plugin's toggle id is on.
wol.show(panel)ok, message. A shell panel (control-center, notifications, switcher, session, clipboard, osd, ...) or one of this plugin's [[panel]] ids.

With a permission:

CallPermissionReturns
wol.notify(title, body)notificationsok, message
wol.run { "prog", "arg", ..., timeout = 5 }runok, output_or_message. argv only; timeout 1 to 30 s (default 5); stdout up to 64 KB.
wol.http_get(url)httpbody, message. https:// or http://; the host needs a grant.
wol.open(target)openok, message. A file, folder, link or app id, with the default handler.
wol.toggle_set(id, on)togglesok, message. Flips this plugin's toggle id and runs its on/off command.
wol.clipboard.text()clipboardtext, message
-- card.lua: state, JSON and the web (permissions "http" and "notifications").
-- Loading never acts: wol.state and the other services exist from the first render on.
local n, quote
return wol.widget {
  refresh = 900,
  render = function()
    n = n or wol.state.get("count") or 0
    quote = quote or wol.state.get("quote")
    local body, e = wol.http_get("https://api.example.com/quote")
    if body then
      local v = wol.json.decode(body)
      if v and v.text then quote = v.text; wol.state.set("quote", quote) end
    end
    return wol.stack { spacing = 8,
      wol.label { quote or e or "...", role = "title" },
      wol.row { spacing = 8,
        wol.label { "Seen " .. n .. " times", role = "secondary" },
        wol.spacer(),
        wol.button { label = "+1", on_click = function()
          n = n + 1
          wol.state.set("count", n)
          if n == 10 then wol.notify("Ten!", "You looked ten times.") end
        end },
      },
    }
  end,
}
-- calc.lua: a command search ("=2+2"). The script gets the text after the prefix.
return wol.search {
  query = function(text)
    local expr = text:gsub("[^%d%+%-%*/%.%(%) ]", "")
    if expr == "" then return {} end
    return {
      { title = expr, subtitle = "Copy result", icon = "accessories-calculator-symbolic",
        on_activate = function() wol.state.set("last", expr) end },
    }
  end,
}

Failure and limits

A render or click that errors shows the sentence on the card or bar item, not a crash. Three failures in a row turn the plugin off with a toast saying why (wol_core::plugins::Health). A script that runs too long is stopped ("This widget took too long and was stopped."), as is one that uses too much memory.

wol plugin

CommandDoes
wol plugin new <id> [kinds]Scaffolds ~/.local/share/wolos/plugins/<id>/ with one entry per kind (comma-separated: menubar,widget,panel,search,dock,tile,toggle,hotkey,webapp,theme,look,hypr; default menubar,widget). The result passes check.
wol plugin check <id or folder> [--json]Manifest, folder (files named exist, no symlinks, 1 MiB), every Lua script loaded and rendered once with real services (search: one query), Hyprland whitelist, theme contrast, Looks preset. Exit 0 only when clean.
wol plugin run <id or folder> <menubar|widget|panel|search> [entry] [input]Loads one entry headless and prints JSON: node (and popover), refresh, events (notifications, run, open, show as lines). input is a part number to click before the final render, or the search text. XDG_STATE_HOME=/tmp/x gives a clean state; WOL_PLUGIN_HOSTS=a.com,b.com grants hosts for http_get.
wol plugin list [--json]Installed plugins: on/off, version, entries, problems.
wol plugin enable <id> / disable <id>Turns a plugin on (granting what it declares) or off; the running shell reloads.
wol plugin remove <id>Deletes a user plugin folder and its state. System plugins are only disabled.
wol plugin pack <id or folder> [out]Writes <id>-<version>.wolplug after a clean check.

--json on any command gives machine-readable output; without a TTY it is the default.

How an AI agent writes and tests a plugin in 5 steps

  1. wol plugin new alice.weather menubar,widget makes a folder that already passes check. Read /usr/share/wolos/plugins.schema.json (the manifest schema) and this page.
  2. Edit plugin.toml (name, description, permissions, entries) and the .lua files. Keep scripts small: one read() that fetches data, one render() that returns parts.
  3. wol plugin check alice.weather --json until "valid": true. Every problem is one sentence naming the file or [[section]] n and what to change.
  4. wol plugin run alice.weather menubar --json prints the view tree; wol plugin run alice.weather widget card 1 --json clicks part 1 first and shows the new tree and the events the handler caused. Use XDG_STATE_HOME=$(mktemp -d) for a clean state and WOL_PLUGIN_HOSTS=api.example.com when the plugin reads the web.
  5. wol plugin enable alice.weather to see it on the desktop; wol plugin pack alice.weather to make the .wolplug to share.

Distribution

Three ways in, all user level, none waiting for an OS release (ADR 0031): drop the folder in ~/.local/share/wolos/plugins/; wol plugin add <git-url|path|.wolplug> (stage, check, move; hooks never run); or the signed plugin index through wopkg and the Store's Plugins page. Unsigned plugins are marked unreviewed, like AUR, and need the typed yes.

.wolplug (frozen): a zstd-compressed ustar archive with the members <id>/<file> for every file of the folder, then SHA256 (BSD sha256 lines, one per file, sorted, ADR 0011 form), then optionally SHA256.sig (the raw 64-byte Ed25519 signature of the SHA256 bytes, by the index key in /usr/share/wolos/keys/). wol plugin pack writes it unsigned; the index CI adds the signature. File paths inside are at most 99 characters.

Where things live

WhatWhere
User plugins$XDG_DATA_HOME/wolos/plugins/<id>/ (default ~/.local/share/...)
System plugins/usr/share/wolos/plugins/<id>/
On/off and grants$XDG_CONFIG_HOME/wolos/plugins.json
Plugin state$XDG_STATE_HOME/wolos/plugins/<id>/state.json
Toggle flags$XDG_STATE_HOME/wolos/toggles/<plugin>.<toggle>
Schema/usr/share/wolos/plugins.schema.json
Bar and Dock slotsshared::layout::Layout in shell.json: BarItem::Plugin { plugin, id }, DockItem::Plugin { plugin, id }

Reference synced from wonderlandos/wolos docs/modules/plugins.md.