Skip to main content
Otter Shell v0.13.0 uses native otr applications with optional LuaJIT views and extensions. Start from otter-examples/otr/. The Zig build files and .so plugin ABI are unsupported.

Choose a starter

The native and Lua-UI templates share a demo card and native Wayland shells. Both support an XDG toplevel window and a layer-shell overlay. The standalone otter-lua run app host uses a headless backend; use the Lua-UI starter for a visible Wayland app. For plugins, continue with Writing plugins.

1. Prepare the workspace

You need the otr toolchain on PATH. The examples expect sibling library checkouts with their otr/ trees:
The example’s otr/otr.toml declares local path dependencies on otter_geo, otter_utils, otter_theme, otter_render, otter_wayland, and otter_ui. Preserve that layout while learning. If you move the starter, update its manifest paths. The build wrapper fetches required C dependencies before its steps. The example README describes toolchain and system dependencies. Running a window requires a Wayland session; the layer variant also requires zwlr_layer_shell_v1. Tests and fixtures can run without your desktop session.

2. Build a release app

From the otter-examples repository root:
app builds .build/otter-examples and .build/otter-examples-lua with --release-fast. check checks the package roots and formatting. To try the native window or overlay:
For the Lua view:
The wrapper runs those applications in release-fast mode. The Lua commands select the starter’s own UI directory, so you do not need to install examples.lua into your user data directory first.

3. Keep the shell separate from the view

Choose the lifecycle you need:
  • XDG toplevel for a normal window, such as an editor or Settings.
  • Layer shell for edge-anchored panels, popups, and overlays, when supported by the compositor.
  • Session lock for lock surfaces; use the lock app and shared Wayland lock wrapper as references, not an ordinary overlay.
  • No surface for a service or CLI.
Otter Shell does not ship a default compositor. Do not make a compositor-specific interface a requirement unless your application needs it.

4. Change the UI

Native view

The native starter describes its card through otter-ui Surface Description. Start in otr/ui/demo.otr:
  1. Add a stable id in ids for each new interactive element.
  2. Add its node to the view tree, following neighboring controls’ theme styles.
  3. Handle its press in Demo.on_press.
  4. Return damage for content that changed, or the appropriate existing input/full result.
Keep hit-region capacity large enough for your controls. The shell already handles hover/press damage and pointer cursor changes. Do not build a second hit-test registry. See otter-ui for the native frame boundary.

Lua view

The script returns { frame = function(state), action = function(id) }. It records sd.* nodes; the native LuaView owns replay and routes tagged actions. view.otr’s push_state defines the script’s available state: card geometry, compact mode, title, hover/active ids, and resolved colors and metrics. The starter-specific otter.examples binding exposes native text measurement and baseline placement. Add a hit id, draw its control, then handle the id in action. Script globals reset on reload. A failed candidate script retains the last valid card. See UI scripts for the complete contract, paths, and policy.

5. Apply shared theme and configuration

Use semantic theme colors, dimensions, and control styles rather than checking profile names. Both templates already integrate the shared theme and imported-file watcher. Keep their transactional reload: a broken theme edit must retain the previous valid theme and font. Application configuration belongs under $XDG_CONFIG_HOME/otter-shell/, defaulting to ~/.config/otter-shell/. Use otter-conf for the shared flat key-value format. If Settings should expose a feature, add its configuration and controls there too; study otter-settings/otr/tabs.otr and its editor modules. Lua code belongs in data directories, not config directories:
  • Plugins: $XDG_DATA_HOME/otter-shell/plugins/<id>/.
  • UI scripts: $XDG_DATA_HOME/otter-shell/ui/.
  • Plugin policy: $XDG_CONFIG_HOME/otter-shell/plugins.conf.
  • Native/Lua UI choice: $XDG_CONFIG_HOME/otter-shell/ui.conf.
The default data home is ~/.local/share. Native hosts choose which bindings and capabilities to expose to Lua. See Bindings and capabilities.

6. Verify without touching your desktop

The starter provides static checks, tests, direct-render fixtures, and isolated headless integration tests:
test runs both debug and release-fast tests. Release applications and visual fixtures use --release-fast. The E2E starts private headless Sway sessions with isolated runtime, home, config, and D-Bus state. It compares native and Lua templates and checks input, damage, and reload behavior. Do not point capture tools at your real Wayland session. Review generated visual evidence in otr/out/visual/ and otr/out/headless/. Golden fixtures live in otr/testdata/visual/. Review changed pixels before updating baselines. For native window debugging, pass --inspect or --metrics after window, or press Ctrl+Shift+I to cycle overlays. These overlays intentionally force full-surface repaint on input.

7. Install the application

The starter’s install step writes release-fast binaries under otr/out/bin/ and examples.lua under otr/out/share/otter-shell/ui/:
For your app, add a freedesktop desktop entry and install it with your packaging:
Install the desktop entry into an XDG applications directory and icons under icons/hicolor/. Rename the starter’s binary, application identifiers, UI script, and package metadata consistently before distribution.

Source guides