Skip to main content
Build a new Wayland app on the Otter Shell stack.

1. Create the project

Initialize a Zig project and add Otter dependencies in build.zig.zon:
{
    "name": "my-otter-app",
    "version": "0.1.0",
    "dependencies": {
        "otter_ui": { "path": "../otter-ui" },
        "otter_wayland": { "path": "../otter-wayland" },
        "otter_render": { "path": "../otter-render" },
        "otter_theme": { "path": "../otter-theme" },
        "otter_conf": { "path": "../otter-conf" },
        "otter_config_types": { "path": "../otter-config-types" },
        "otter_utils": { "path": "../otter-utils" }
    }
    // Add otter_desktop if you need D-Bus, icons, or .desktop parsing
}
Outside the monorepo, fetch libraries by URL:
zig fetch --save git+https://git.pika-os.com/otter-shell/otter-conf

2. Wire imports in build.zig

const otter_ui = b.dependency("otter_ui", .{ .target = target, .optimize = optimize });
const otter_wayland = b.dependency("otter_wayland", .{ .target = target, .optimize = optimize });
const otter_render = b.dependency("otter_render", .{ .target = target, .optimize = optimize });

exe.root_module.addImport("otter_ui", otter_ui.module("otter_ui"));
exe.root_module.addImport("otter_wayland", otter_wayland.module("otter_wayland"));
exe.root_module.addImport("otter_render", otter_render.module("otter_render"));

3. Choose your app pattern

Layer-shell overlay (launcher, logout, settings style)

  • Connect to Wayland via otter-wayland
  • Create a zwlr_layer_shell_v1 surface
  • Describe UI with Surface Description (otter-ui UiFrame)
  • One-shot: use ArenaAllocator, exit after user action
  • Examples: otter-launcher, otter-logout

Persistent daemon (bar, notifications, wallpaper style)

  • Long-running event loop polling Wayland + D-Bus/inotify FDs
  • Use GeneralPurposeAllocator
  • Hot-reload config via otter-conf.Watcher
  • Examples: otter-bar, otter-notifications

CLI tool (calc, weather, emoji style)

  • Minimal deps: otter-tools-core + otter-conf
  • No Wayland window unless you add a --layer popup mode
  • Examples: otter-calc, otter-weather

4. Build the UI with Surface Description

New app UI should use the bounded UiState frame API:
const ui = @import("otter_ui");

const caps = ui.ui_frame.Capacities{ .elements = 64, .hit_regions = 64, .overlays = 8 };
var state: ui.UiState(caps) = .{};

var frame = state.begin(.{
    .viewport = .{ .x = 0, .y = 0, .width = width, .height = height },
    .scale = surface.scale,
    .font = font,
});

const root = ui.SurfaceNode{
    .id = ui.SurfaceId.namedComptime("root"),
    .kind = .column,
    .layout = .{ .width = .fill, .height = .fill, .gap = 8, .padding = ui.Padding.uniform(12) },
    .children = &children,
};

_ = try frame.render(&root, frame.viewport);
try frame.finish();

// Rasterize via otter-render
render.quad_renderer.rasterize(ui.render.DefaultCommandList, &state.commands, surface, null, null, true);
Use state.hitTest(point) for pointer input. No parallel hit arrays needed.

5. Add configuration

Define a config struct and parse it with otter-conf:
const otter_conf = @import("otter_conf");

const Config = struct {
    font_size: u16 = 14,
    enabled: bool = true,
    label: []const u8 = "Hello",
};

var config = try otter_conf.load(Config, allocator, config_path, .{});
defer otter_conf.freeConfig(Config, allocator, &config);
Store config at ~/.config/otter-shell/my-app.conf. On first run, normalize the file (add missing defaults, strip unknown keys). For typed configs shared with otter-settings, add structs to otter-config-types and register a tab in otter-settings/src/tabs.zig.

6. Apply theming

Load the shared theme:
const theme = try otter_theme.loader.loadTheme(allocator);
defer theme.deinit(allocator);
Widget config fields should use ?Color = null and resolve via config.field orelse theme.token. Precedence: widget config, then theme.conf, then compiled defaults.

7. Install a .desktop file

For launcher integration, install a freedesktop .desktop entry:
[Desktop Entry]
Name=My App
Exec=my-otter-app
Icon=my-otter-app
Type=Application
Categories=Utility;
Place in data/applications/ and install icons under data/icons/hicolor/.

Reference implementations

PatternStudy these sources
Status bar daemonotter-bar/src/main.zig, otter-bar/src/config.zig
One-shot overlayotter-launcher/src/main.zig, otter-launcher/src/draw.zig
Region capture / selectionotter-screenshot/src/main.zig, otter-wayland/src/selection_overlay.zig
Settings GUIotter-settings/src/app.zig, otter-settings/src/draw_form.zig
CLI helperotter-calc/src/main.zig, otter-tools-core/src/calc.zig
Config-only daemonotter-idle/src/main.zig
See the Libraries overview and per-library pages in the sidebar for API details.