mandara

Contributing to Mandara Shell

Welcome! This is the guide for everyone who works on Mandara’s code: people and AI coding agents alike. It explains what Mandara is, how the repository is laid out, how to build and check your work, the rules every change follows, and how to add the common kinds of features. If you only want to write a third-party plugin, PLUGINS.md is all you need.

Contents

  1. What Mandara is
  2. Repository map
  3. Build, test and check
  4. Driving and inspecting the shell from the CLI
  5. Hard rules
  6. Design principles
  7. Architecture notes and gotchas
  8. How to add things
  9. Writing a third-party WebAssembly plugin
  10. Keep the docs in sync
  11. Commits and pull requests

What Mandara is

Mandara is a Wayland desktop shell for MangoWM, Niri, Hyprland, Umbriel, Sway and River: the bar, panels, sidebar, dock, launcher, notifications, on-screen displays, wallpaper engine, lock screen, login manager and more, shipped as one native Rust binary (mandara) built on GPUI, Zed’s GPU-accelerated UI framework.

Repository map

Path What lives there
crates/mandara-app The mandara binary: CLI (msg, commands, studio, setup, update, install, …), the restart supervisor, IPC and app bootstrap.
crates/mandara-core GPUI layer-shell surface specs, the analytical spring solver, OKLCH tweening, frame-synced input-region masking, the chromatic-glass material, output geometry and the software (wl_shm) canvas.
crates/mandara-config The TOML schema and defaults, the reactive ConfigStore with its file watcher, XDG paths, and the non-destructive MangoWM / Niri / Hyprland / Umbriel / Sway / River emitters and conflict scanner.
crates/mandara-theme The color engine: Color (OKLCH, gamut mapping, WCAG contrast), MandaraPalette tokens, HCT wallpaper quantization, the theme catalog (themes/*.toml) and app theme templates.
crates/mandara-compositor The CompositorBackend trait, the navigation model, and the MangoWM, Niri, Hyprland, Umbriel, Sway and River drivers (River’s control and status protocols generated from protocols/*.xml).
crates/mandara-services D-Bus and system service actors (MPRIS, notifications, tray, UPower, NetworkManager, polkit, audio, keyrings, …), the marketplace client and the self-updater.
crates/mandara-plugins The plugin runtime: manifests, the registry, CommandSpec and the command catalog, plugin lifecycle, the wasmtime host and the WIT contract (wit/plugin.wit).
crates/mandara-ui The shell’s views: bar, panels, sidebar, launcher, OSDs, lock screen, desktop and backdrop, the UI kit and every first-party plugin (src/plugins/).
crates/mandara-studio Mandara Studio: pages, the declarative settings registry, design tokens and the plugin store.
crates/mandara-greeter Mandara Login, the greetd greeter (see docs/login-manager.md).
crates/mandara-media The pure-Rust video pipeline: WebM / IVF demuxing, AV1 decoding (rav1d), YUV conversion straight into caller buffers, the greeter’s player, the desktop’s frame stream and first / representative frames. No GUI types.
crates/mandara-wallgen The procedural wallpaper engine: twelve resolution-independent styles with generic parameter specs, OKLab palettes, recipes (TOML sidecars), deterministic row-parallel rendering, lossless WebP output through mandara-webp and seamless AV1 video loops through the recorder’s VideoSink. No GUI types. The wallpaper-generator plugin (crates/mandara-ui/src/plugins/wallpaper_generator.rs and its folder: jobs, schedule, CLI) and Studio’s generator (crates/mandara-studio/src/app/wallgen_page.rs) build on it.
crates/mandara-webp A pure-Rust lossless WebP (VP8L) encoder with no dependencies: generated wallpapers (searching their dither period for back references) and the plugin icons embedded in the shell.
crates/mandara-i18n Localization: English-keyed catalogs for twenty languages (locales/*.toml), compiled into memory-mapped language packs (.mlang, src/pack.rs) that ship beside the binary and download on demand (src/packs.rs), and the coverage tests.
crates/mandara-install mandara install / uninstall: per-distribution package tables and the package-manager abstraction.
crates/mandara-version Build identity: component versions, content hashes, git revision, source fingerprint.
vendor/gpui_wgpu GPUI’s wgpu renderer, vendored with Mandara’s patches (GPU memory, fonts, colour emoji, Vulkan first, rounded clipping). Every change is listed in MANDARA-PATCHES.md and marked Mandara patch in the source.
vendor/gpui_linux GPUI’s Linux platform (Wayland client), vendored with Mandara’s touchscreen patch: the seat’s wl_touch feeds GPUI’s own touch events. Every change is listed in MANDARA-PATCHES.md and marked Mandara patch in the source.
vendor/gpui GPUI core, vendored for rounded content masks (a rounded element whose overflow is hidden or clipped clips its children to its rounded shape) and for .hover() on elements without an .id(), which upstream only repaints when something else re-renders the view, and with only the image decoders its ImageFormat names (MANDARA-PATCHES.md).
vendor/pure-onnx-ocr The PaddleOCR engine behind text recognition, vendored with an unchanged src/ and a manifest that stops it turning on every codec of the image crate (MANDARA-PATCHES.md).
xtask cargo xtask: versioning, fingerprinted builds, packaging, artwork, delta updates, the resource budget and the demo sandbox.
tools/mandara-delta The delta-update format and CLI, outside the workspace so plugin authors can cargo install it alone.
plugins/examples Example third-party plugins (moon-phase), built on their own for wasm32-wasip2.
docs Guides: Studio, commands, configuration, login manager, releasing, development, and screenshots/.
assets Brand marks, plugin icons and covers, catalog.json and the artwork index (assets/README.md).
packaging The desktop entry and icon, the PAM file, the Arch PKGBUILD template and AUR recipes.
.github/workflows CI (ci.yml) and the release pipeline (release.yml).

Build, test and check

Mandara builds with stable Rust on Linux. Besides a C toolchain and pkg-config, it needs the development files for xkbcommon, Wayland, Vulkan, fontconfig and freetype (CI installs build-essential pkg-config cmake libxkbcommon-dev libwayland-dev libvulkan-dev libfontconfig-dev libfreetype-dev).

cargo check --workspace --all-targets        # fast type check of everything, tests included
cargo test --workspace                       # unit, integration and doc tests
cargo clippy --workspace --all-targets       # lints (CI reports them)
cargo build --release -p mandara-app         # the shipped binary → target/release/mandara

Release builds use fat LTO and one codegen unit ([profile.release] in Cargo.toml), so they take a while. Aliases in .cargo/config.toml: cargo dist (the same release build), cargo native (tuned for your CPU), cargo profiling (release code with symbols, for perf or samply). Release builds unwind on panic: the plugin guard and the PAM conversation rely on catch_unwind, so never switch them to abort.

cargo xtask runs the project’s tooling (all Rust, no scripts):

Command What it does
cargo xtask status Every component’s version and whether its content changed since.
cargo xtask bump --dry-run What a release would bump (CI does the real bump).
cargo xtask verify Fails if a component changed without a bump.
cargo xtask build A fingerprinted release build, its manifest and the build-fingerprint table in docs/building.md.
cargo xtask plugins Builds the example plugins and packages them in marketplace format.
cargo xtask langpacks --bin PATH --dir DIR Compiles every language pack for a binary’s version (--out DIR: the release assets and their part of update.json).
cargo xtask art [--check] Regenerates (or validates) the plugin artwork embedded in the binary.
cargo xtask resource-check target/release/mandara Runs a release binary in a throw-away headless session and checks its idle memory, threads and CPU against the budget.
cargo xtask sandbox up / shot / msg / studio / down A private headless demo desktop for screenshots and end-to-end tests (docs/development.md).

Checks that guard the codebase and fail cargo test when you break a rule:

Driving and inspecting the shell from the CLI

Mandara is agent-ready: almost everything the UI can do is a documented command on the running shell, so you can drive it, inspect it and verify a change without clicking.

mandara commands                     # every command, its arguments and examples
mandara commands --json              # the same catalog, machine-readable
mandara msg <command> [args]         # run one against the running shell
mandara msg --json <command> [args]  # reply as JSON: {"ok": …, "data": …} or {"ok": false, "error": …}

Useful starting points:

mandara msg ping                          # is the shell up?
mandara msg --json shell state            # what is open: panel, sidebar, launcher, menus
mandara msg config get /bar/height        # read a setting by JSON pointer (no argument: the whole config)
mandara msg config set /bar/height 30     # change one setting live (the value is JSON; bare words are strings)
mandara msg --json plugins list           # every plugin, enabled or not, and third-party load errors
mandara msg panel open media              # open a plugin's panel
mandara msg diagnostics frames            # frame timings, for performance work
mandara studio plugins --plugin weather --settings   # open Studio on a page or a plugin's settings

The catalog is generated from the code (CommandSpecs in the plugin registry), so it is always complete; docs/commands.md is a guided tour by area. Users can also chain commands into named launcher skills, run them from slash commands and attach them to hooks on desktop events, all through the same commands.

Verify safely. Never test against someone’s real session:

Hard rules

These hold for every change. Reviews check them, and several are enforced by tests.

  1. 100 % Rust. No shell, Python or other scripts in the repository: tooling goes in cargo xtask, the installer is the binary itself (mandara install), and plugins are Rust compiled to WebAssembly. One toolchain builds, tests and reviews everything; nothing depends on an interpreter being present or behaving the same on every distribution; and every tool is covered by cargo test. (CI workflow steps are the only shell, and they only call cargo and packaging tools.)
  2. Every user-visible string in every language. Wrap text in mandara_i18n::t (static), tf (with {placeholders}) or tr (runtime keys), or mark it with tk for a later tr. English is the key; add the translation to all nineteen catalogs in crates/mandara-i18n/locales/: ar, bn, de, es, fa, fr, hi, id, ja, ko, mr, pt, ru, sw, te, tr, ur, vi and zh. Keep placeholders intact, and one placeholder per value.
  3. No magic numbers. Anything a user could notice (sizes, spacing, radii, opacity, colours, delays, refresh intervals, thresholds) is a setting with a default and a bounded range. What stays fixed is a named const with a doc comment saying why. Use the design tokens (mandara_ui::kit::tokens and text, crates/mandara-studio/src/tokens.rs) and palette roles, never raw hex colours.
  4. Non-destructive dotfiles. Mandara never rewrites a user’s compositor config. Rules, binds and border colours go to generated drop-ins in ~/.config/mandara/generated/, and the user’s main file gains one include line. Any edit to a user file takes a timestamped backup first, is validated with the compositor’s own checker (mango -p, niri validate, Hyprland --verify-config, umbriel config validate, sway --validate) and rolls back on failure. River is the exception: its config is an executable init program Mandara never edits; its settings are River commands sent over river-control (river_commands), and its tests run every one through a headless River. Binds that collide with the user’s are skipped and reported, never overridden, and “collide” means what the compositor means: MangoWM’s plain bind compares keycodes (Play and Pause are one key), every compositor compares keysyms, not their spellings (crates/mandara-config/src/mango_keys.rs). Every option or syntax an emitter writes comes from the compositor’s documentation and must pass its own checker in a test before it ships (mango_itself_accepts_the_drop_in runs mango -p on the drop-in where MangoWM is installed); never guess one. Every write to a compositor config (the drop-in, the include line, a retire, a fix) goes through the Config doctor (crates/mandara-config/src/doctor/): it runs the compositor’s own checker read-only (private runtime dir, no display, a timeout), compares the result with the check before the write and rolls back when it is worse (doctor::fix::guarded_write, fix::apply, fix::is_worse). Options a compositor version rejects in Mandara’s drop-in go on the per-version unsupported list instead of being emitted again. A change to a checker’s output format needs its parser updated with captured output in crates/mandara-config/tests/fixtures/doctor/ (the golden tests and real_checkers_match_the_golden_files, which runs installed checkers).
  5. Never change a user’s config unless they asked. Defaults apply to new installs; existing settings, files and enabled plugins stay as the user left them.
  6. Resource discipline, but speed wins. Nothing resident while idle: no timers or polling while a surface is hidden, load data when a plugin is first shown (not when it registers), decode images at the size they are drawn, release what is only needed to paint. But never trade speed for memory: an optimization that slows frames, view builds, startup or anything a user triggers is reverted. Measure release builds before and after (cargo xtask resource-check, mandara msg diagnostics frames) before claiming a win.
  7. Honest numbers. Performance and memory figures in the docs come from measured release builds and are written as rounded ceilings (“under 100 MiB”), never guessed, inflated or trimmed.
  8. Screenshots from the demo sandbox only. Docs images come from cargo xtask sandbox with demo data: no other apps behind Mandara, no real notifications, and no personal data (names, networks, IP addresses, locations, project names).
  9. Develop and test in isolation. Debug builds, synthetic input and experiments run only in a sandbox with its own Wayland display, runtime dir, session bus and a dead system bus. Never run a debug build or send synthetic clicks and keys to a user’s real session, and never send commands to their real media player, network or power controls. Tools that act on a sandbox check its runtime dir and marker first.

Design principles

Mandara is design-first: it should never look like an unstyled toolkit.

Architecture notes and gotchas

Things that cost real debugging time:

How to add things

A built-in plugin

  1. Add a module under crates/mandara-ui/src/plugins/ with a pub const ID and a register(r: &mut PluginRegistry) that calls r.register(PluginManifest::builtin(ID, tk("Name"), tk("What it does"), Some(Zone::Right)), |window, cx, panel_cx| PluginInstance { … }). Use .opt_in() for niche or faith-specific plugins.
  2. Call it from register_builtin in crates/mandara-ui/src/plugins/mod.rs. Registration must stay cheap (the startup budget test): load data when the plugin is first shown.
  3. Put it in a store category (PLUGIN_CATEGORIES in crates/mandara-studio/src/app/store_page.rs).
  4. Add its art in assets/plugins/<id>/ (icon.svg, a 256 × 256 icon.png, a 1600 × 900 cover.png), an entry in assets/catalog.json and a row in assets/README.md, then run cargo xtask art and cargo test -p xtask.
  5. Give its options settings (below), its actions commands (below), its strings translations, and add it to the plugin table and count in docs/plugins.md (and the plugin count in the README’s badges and highlights).

A Studio setting

Settings are data in the declarative registry (crates/mandara-studio/src/registry.rs and registry/*.rs): one entry names the section, group, JSON-pointer config path, label, help and control (Toggle, Slider, Choice, Text, …), and one generic engine reads and writes the config. Studio’s settings audit checks that every control is read by the shell. Follow Adding a setting.

A config key

Add the field to the schema in crates/mandara-config/src/ with its default in the nearby defaults module and a named range; read it from the config snapshot where it is used, clamped to the range, so it applies live. Document it in docs/configuration.md, and add a Studio control.

A compositor feature

Implement it for every backend (crates/mandara-compositor/src/ mango.rs, niri.rs, hyprland.rs, umbriel.rs, sway.rs, river.rs, behind CompositorBackend in backend.rs), or lock it in Studio with a reason where a compositor can’t do it. Generated config goes through the emitters in crates/mandara-config/src/compositor.rs (emit_mango, emit_niri, emit_hyprland, emit_umbriel, emit_sway; River’s river_commands), with unit tests and, where the compositor is installed, its own checker (validator_tests.rs). Window-rule options one compositor lacks are gated with rule_option_supported, and include lines follow the installed version (installed_version). Umbriel applies the user’s config.toml after Mandara’s drop-in, so its own values win there.

A command

Register a CommandSpec with the plugin that owns it ("core" for shell verbs). This is how diagnostics frames is registered:

r.command("core", CommandSpec::new("diagnostics frames", mandara_i18n::tk("Recent shell frame timings with p50 / p95 / p99 (build time and frame interval)"), |_, _| Ok(frames_report())).example("diagnostics frames"));

Add arguments with .arg(ArgSpec::required(…)) / ArgSpec::optional(…) and examples with .example(…); replies are JSON. It then shows up in mandara commands, the launcher and plugin host::command. Add it to docs/commands.md in its area.

A translated string

Write the English text in a t, tf, tr or tk call, run MANDARA_I18N_DUMP=1 cargo test -p mandara-i18n, add the translation to each of the nineteen catalogs in crates/mandara-i18n/locales/, and run cargo test -p mandara-i18n until it passes. Debug builds read the catalogs straight from the source tree; a release build only reads language packs, so try a translated release build in the sandbox (cargo xtask sandbox up compiles the packs for it) rather than on its own.

Writing a third-party WebAssembly plugin

Third-party plugins are Rust crates compiled to a WebAssembly component against crates/mandara-plugins/wit/plugin.wit. They return declarative view trees (rows, columns, cards, text, icons, buttons, meters) that Mandara renders natively in every theme, and they run sandboxed: no files, network, environment or subprocesses, a time budget per call, and a trap disables only that plugin.

rustup target add wasm32-wasip2
cargo build --release --target wasm32-wasip2
# copy plugin.toml, locales/, icons and the .wasm (as plugin.wasm) to
# ~/.local/share/mandara/plugins-dev/<id>/ — it hot-reloads

Start from plugins/examples/moon-phase. Studio → Plugins → Develop (mandara studio plugins --tab develop) shows your folders, the contract of your build and a validator. Publishing is a public GitHub repository with the mandara-plugin topic and a release asset named <id>.mandara.tar.gz.

Before you publish: compliance check. Installs and updates run mandara_plugins::vet on every release, and a failing one is refused (the user can still install it anyway; the warning stays on that version only). Run it yourself with mandara msg plugins vet <dir> (JSON verdict). It sorts failures into three classes: Broken (bad manifest, missing or non-component plugin.wasm, a trap or time-budget overrun in init or a render), Malicious (poses as a built-in id or name, imports beyond the plugin world and the empty WASI, or queues commands before any user event) and Placeholder (the unchanged template or example, or views that show nothing). Its tests use the fixture in crates/mandara-plugins/tests/data/vet.

Everything else (the API, views, settings, permissions, translations, releases and updates) is in PLUGINS.md.

Keep the docs in sync

Every user-facing change updates its docs in the same change:

Commits and pull requests