opf, discovery, and policy. Widgets and providers share the Lua runtime but expose different functions to their hosts.
A clickable widget
Akind = "widget" plugin returns measure, draw, and an optional event function:
measure(max_w, max_h) returns the desired width and height. draw(w, h) records a complete frame. event(kind, id, x, y) receives pointer events for tagged hit regions.
A widget draws again after input, resizing, or otter.host.redraw(). Use native subscriptions, timers, or file watches to request updates when state changes. An idle widget with no event sources does not need periodic polling or Lua calls.
Surface Description calls
sd is a global supplied by the runtime:
Colors use
0xRRGGBBAA. Alpha below ff blends over content beneath the node. Close every container and the root. Invalid call order, unfinished frames, more than 4096 nodes, or more than 64 KiB of text fail the frame.
The native host replays the recording. You do not allocate a Wayland surface or render pixels from Lua.
Text input
Hosts supporting text-field hits focus a node tagged withsd.hit(id, "text") when clicked. Text events use event(kind, id, text) rather than pointer coordinates:
focusandblur, with empty text.text, with committed field text.preedit, for IME composition.submit, when Enter is pressed.
bar-search plugin for the bar’s field behavior.
Tooltips and menus
Bar, taskbar, and dock hosts support named exports for native tooltips and menus:kind = "separator" for separators. Root items use depth "0"; first-level submenu items use "1". The bar supports nested depths through "8" and calls the optional submenu_open(id) export before opening a submenu. Its menu query uses state = "submenu" while a submenu is open. Other hosts can impose narrower menu limits. screen_w is the output width, or zero when unknown.
Hover shows tooltip text unless the menu is open. Menu activation depends on the host: the bar defaults to left click, while dock tiles use right click. Activating an item calls menu_activate(id); close = "1" dismisses it. Dismissal calls menu_closed(). A redraw while open refreshes menu records.
Launcher provider
Use this manifest:query(text, max), and handle the selected record’s payload in activate(payload):
title and payload are strings; subtitle is optional. Return no more than max records. An optional row(payload, w, h) export draws a custom result row with sd.*.
The launcher reads kind and provides before executing the plugin. The built-in Lua providers calc, emoji, clip, and search default to off to avoid duplicate native results; snippet-provider is also opt-in. Enable those explicitly in plugins.conf only when wanted. This default does not apply to every custom provider.
Test the host behavior
Runopf check and opf check --build after changes. Then exercise your plugin through its real host in an isolated headless session, with temporary HOME, XDG_CONFIG_HOME, XDG_DATA_HOME, and XDG_RUNTIME_DIR.
The otter-shell-plugins/otr/tests/e2e.sh suite covers plugins in the shared host driver, bar, and launcher under headless Sway. Do not use opf check’s inert service substitutes as evidence that process, filesystem, or desktop-service behavior works.
Source contracts: otter-lua/otr/sd.otr, otter-lua/otr/plugins.otr, otter-launcher/otr/providers.otr, and the factory’s otr/templates/.
