Decisions
0031: Plugins, themes and layouts
Folder plugins in a sandbox, themes for every toolkit, and layouts as data.
Status: proposed (main, 2026-11-01; lead asked for it the same day). Builds on 0010 (widget packages), 0020 (view tree, Lua sandbox), 0021 (wopkg), 0027 (one app cannot take the desktop), 0029 (wolos builds nothing for release). Omarchy (MIT) is the reference for format and ecosystem; its palettes are already ported under `crates/core/src/theme/builtin/NOTICE`.
Context
the shell can be extended only by one Lua desktop card (crates/wol-lua), a JSON widget package (ADR 0010) or a theme JSON (crates/shared/src/theme.rs). The menu bar, Dock, Search, Control Center and hotkeys are closed; themes stop at toolkits (Hyprland colours are static in hyprland.conf); the bar and Dock edges are constants in crates/shell/src/kit/surface.rs. Omarchy shows that a plugin ecosystem forms fast (1,700+ repositories in months) when the format is one folder, one manifest and one CLI, and that most plugins are small: a bar item, a panel, a theme, a web app launcher, a toggle. Omarchy runs plugins unsandboxed; WolOS will not. The lead wants: a powerful plugin and theme system integrated everywhere, an app to make themes and plugins, a layout abstraction so a Windows-like layout can come later, an API that AI agents can write plugins against easily, and plugin updates that do not wait for OS releases.
Decision
- A plugin is a folder
<id>/withplugin.tomland the files it names. Search order:~/.local/share/wolos/plugins/<id>/(user, reloaded on change),/usr/share/wolos/plugins/<id>/(packaged). Ids are reverse-DNS or<user>.<name>, lowercase;org.wolos.*is reserved. No symlinks, 1 MiB total, writes only under$XDG_STATE_HOME/wolos/plugins/<id>/. - Manifest
plugin.toml, schema 1 (frozen incrates/shared/src/plugins.rs; unknown key = load error with a sentence): headerschema, id, name, version, author, license, description, min_wolos, permissions; capability tables, each with anid:[[menubar]](script, side),[[dock]](name, icon, exec),[[widget]](script, sizes; today'slua:<id>card),[[panel]](script, anchor),[[search]](script, kind, prefix),[[tile]](icon, label, toggle),[[toggle]](on, off argv; flag file in$XDG_STATE_HOME/wolos/toggles/),[[hotkey]](default, exec, group; becomes a user-rebindableshortcuts::Custom),[[webapp]](name, url, icon, profile),[[theme]](file),[[look]](file),[[hypr]](file, whitelist ofwindowrule, layerrule, bezier, animation; neverbind, exec, plugin, env, source). The schema is published asdocs/plugins/plugin.schema.jsonand installed at/usr/share/wolos/plugins.schema.jsonso agents can validate; the whole API is documented in one file,docs/modules/plugins.md. - Code is Lua in the existing sandbox (
wol-lua: 16 MiB, 5M instructions, no io/os/require). API v1 = today'swol.*plus the view nodes that exist inshared::viewbut not in the prelude (image, toggle, slider, list, item, text_field),wol.state.get/set(per-plugin JSON, 64 KB),wol.run{argv, timeout}(permissionrun, argv only, throughwidgets::run_limited),wol.http_get(per-host grant as ADR 0010),wol.open,wol.toggle_set,wol.clipboard.text(new capability, through wold),wol.show(panel). Anything that changes the system iswol.run{"wol", ...}: it passescontroller::approveand the audit log, so plugins inherit the helper policy and appear in Settings > Assistants & Automation under the plugin id. No new privilege path. Load never acts. - Permissions and limits: declared in the manifest, shown before enabling (Store sheet,
capability_row), stored as grants inhelpers.jsonkeyed by plugin id, revocable in Settings. Each render and click is budgeted; a plugin that fails three renders in a row is disabled with a toast saying why. Hypr fragments are parsed intohyprprefskeywords, never sourced raw. - Distribution, independent of OS releases: (a) folder drop; (b)
wol plugin add <git-url|path|.wolplug>(stage, check, move; hooks never run); (c) a signed plugin index (index.json+ Ed25519 signature, key in/usr/share/wolos/keys/) consumed by a newpluginssource in wopkg (ADR 0021 recipe model) and by a Store "Plugins" page. A.wolplugis a tar.zst of the folder plusSHA256and optionalSHA256.sig(ADR 0011 form). Installs are user level;wopkg updateupdates plugins without touching pacman. Unsigned plugins are markedunreviewedlike AUR and need the typedyes. The index repository (wolos-plugins) accepts pull requests validated bywol plugin checkin CI. - Agent-first CLI:
wol plugin new <id> [--kinds ...](scaffold with a passing check),wol plugin check <dir> [--json](schema, ids, sizes, symlinks, Lua loads, every entry renders with sample state, theme contrast, hypr whitelist),wol plugin run <dir> --kind <k> [--click n --state f](prints the Node tree as JSON, no display needed),list|enable|disable|remove|update|pack|preview. - Themes v2:
theme.jsonstays (tokens +extends) and gains optionalterminal(16 colours, derived when absent),wallpaper,iconTheme,lock,look. New renderers incrates/core/src/theme/: Hyprland colours (hyprbars bar and text, borders, shadow, written tohypr/wolos/theme.conf), lock screen, terminal palettes (foot exists; btop, alacritty, kitty, ghostty opt-in), zsh and PowerShell prompt colours. User templates~/.config/wolos/themed/<name>.tplwith{{token}},{{color3}},{{token|strip}},{{token|rgb}}rendered on every switch. Theme-from-wallpaper generator must passcheck()(contrast). Theme picker panel on ⌘⌃T, background picker on ⌘⌃W. - Layouts:
shared::layout::Layout { bar: Edge, bar_style: Menubar | Taskbar, dock: Option<Edge>, dock_merged_into_bar, workspaces_in_bar, tray_in_bar }saved inshell.json. Now:Spec::bar(edge)andSpec::dock(edge)take anchors fromLayout; the bar becomes slotsleft/centre/rightofVec<BarItem>(plugin items areBarItem::Plugin);Dock::contenttakesVec<DockItem>and an orientation; the hot zone and reveal direction follow the dock edge; Hyprland layer namespaces stay. Later, "Windows-like" =Layout{bar: Bottom, bar_style: Taskbar, dock: None, dock_merged_into_bar: true, workspaces_in_bar: true}plus oneBarItem::Tasksand a start item; no other surface changes. - Plugin Maker / Theme Studio (
wol-plugin-maker, GTK4): editor (TOML/Lua, GtkSourceView) with live preview reusing the widget gallery, a fake bar with the item inserted, Control Center tile preview and the theme preview with token sliders and live contrast; Check (same function as the CLI), Pack, Publish (opens the index pull request with the hash). Reached from Settings > Appearance ("Make a theme...") and Store > Plugins ("Make one"). - Omarchy features ported natively in v0.12 (MIT, rewritten, not loaded as QML): web apps (
wol webapp add, frameless browser window,.desktop, Search and Dock entries), toggles as state with Control Center tiles (Stay Awake, Silence, Bar, Touchpad), theme-from-wallpaper and the two pickers, Hyprland colours from the theme, capture menu additions (colour picker, OCR to clipboard, QR decode kept out of history), clipboard history with images and 1-day expiry that skips verification codes, reminders (wol remind), monitor profiles with auto-apply on hotplug, update badge in the menu bar, and a reference bar pluginorg.wolos.statsthat proves the API and is the documentation example.
Consequences
one manifest, one sandbox, one view tree: plugin UI cannot leave the HIG and a broken plugin shows a sentence, not a crash. Plugins reach system services only through wol verbs, so helper policy, audit and Always-Allow cover them unchanged; a plugin that needs more is a libwol app (ADR 0020). Plugin updates ride wopkg's signed index, not OS releases, so ADR 0029 is untouched. Omarchy plugins are ports, not loads; most are small. The layout refactor is mechanical and must land before plugin bar items, because BarItem and DockItem are the frozen seam. Memory and instruction limits of wol-lua now protect the menu bar, so the existing bomb tests are extended to every capability.
Reference synced from wonderlandos/wolos docs/adr/0031-shell-plugins-themes-layouts.md.