# 0031: Plugins, themes and layouts

> Folder plugins in a sandbox, themes for every toolkit, and layouts as data.

Source: https://wolos.pages.dev/manual/adr-0031

## 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>/` with `plugin.toml` and 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 in `crates/shared/src/plugins.rs`; unknown key = load error with a sentence): header `schema, id, name, version, author, license, description, min_wolos, permissions`; capability tables, each with an `id`: `[[menubar]]` (script, side), `[[dock]]` (name, icon, exec), `[[widget]]` (script, sizes; today's `lua:<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-rebindable `shortcuts::Custom`), `[[webapp]]` (name, url, icon, profile), `[[theme]]` (file), `[[look]]` (file), `[[hypr]]` (file, whitelist of `windowrule, layerrule, bezier, animation`; never `bind, exec, plugin, env, source`). The schema is published as `docs/plugins/plugin.schema.json` and installed at `/usr/share/wolos/plugins.schema.json` so 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's `wol.*` plus the view nodes that exist in `shared::view` but not in the prelude (`image, toggle, slider, list, item, text_field`), `wol.state.get/set` (per-plugin JSON, 64 KB), `wol.run{argv, timeout}` (permission `run`, argv only, through `widgets::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 is `wol.run{"wol", ...}`: it passes `controller::approve` and 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 in `helpers.json` keyed 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 into `hyprprefs` keywords, 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 new `plugins` source in wopkg ([ADR 0021](/manual/adr-0021) recipe model) and by a Store "Plugins" page. A `.wolplug` is a tar.zst of the folder plus `SHA256` and optional `SHA256.sig` (ADR 0011 form). Installs are user level; `wopkg update` updates plugins without touching pacman. Unsigned plugins are marked `unreviewed` like AUR and need the typed `yes`. The index repository (`wolos-plugins`) accepts pull requests validated by `wol plugin check` in 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.json` stays (tokens + `extends`) and gains optional `terminal` (16 colours, derived when absent), `wallpaper`, `iconTheme`, `lock`, `look`. New renderers in `crates/core/src/theme/`: Hyprland colours (hyprbars bar and text, borders, shadow, written to `hypr/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>.tpl` with `{{token}}`, `{{color3}}`, `{{token|strip}}`, `{{token|rgb}}` rendered on every switch. Theme-from-wallpaper generator must pass `check()` (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 in `shell.json`. Now: `Spec::bar(edge)` and `Spec::dock(edge)` take anchors from `Layout`; the bar becomes slots `left/centre/right` of `Vec<BarItem>` (plugin items are `BarItem::Plugin`); `Dock::content` takes `Vec<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 one `BarItem::Tasks` and 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 plugin `org.wolos.stats` that 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](/manual/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.
