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.
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.
zwlr_layer_shell_v1 surface for the bar, plugin panels and sidebar,
plus exclusion strips and wallpaper layers. Islands and panels morph
inside that surface with spring physics, and the input region follows
what is drawn so clicks on transparent pixels reach the windows below.mandara studio, app_id = org.mandara.studio) is
the settings and design app, an xdg_toplevel running as its own
process. Every edit is mirrored to the running shell over IPC, so it
previews live, and Studio’s CPU and memory never count against the
shell’s.| 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). |
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:
cargo test -p mandara-i18n fails when a string the
code asks for is missing in any language. After adding strings, run
MANDARA_I18N_DUMP=1 cargo test -p mandara-i18n to refresh
crates/mandara-i18n/locales/keys.txt, then translate the new keys.crates/mandara-ui/tests/no_hardcoded.rs scans
the shell, Studio, greeter and service sources for bare px(<number>),
Duration::from_millis(<number>) and Duration::from_secs(<number>).
The few remaining sites are listed per file with a reason in
crates/mandara-ui/tests/literal_allowlist.tsv, and the counts must
match exactly: it is a ratchet. A new literal fails; a removed one asks
you to lower the count (MANDARA_LITERALS_DUMP=1 rewrites the counts and
keeps the reasons).crates/mandara-ui/tests/startup_budget.rs keeps
registering every built-in plugin a cheap table of commands and views.mandara-plugins’ authoring tests check that
PLUGINS.md states the real time budgets, limits and exports.cargo test -p xtask validates the brand marks, plugin
icons and covers, assets/catalog.json, the links in
assets/README.md and the embedded art.compositor-checkers job installs
sway and river-classic in an Arch container and runs those tests for
real.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:
cargo xtask sandbox up, then
cargo xtask sandbox msg <command>, cargo xtask sandbox pointer click X Y
and cargo xtask sandbox shot out.png. Its buses, runtime dir and home
are private and its system bus is dead, so nothing reaches the real
NetworkManager, logind or BlueZ.mandara install --dry-run,
mandara uninstall --dry-run, mandara setup --check,
mandara update apply --dry-run, cargo xtask bump --dry-run.XDG_CONFIG_HOME at a scratch folder
and set MANDARA_NO_IPC=1 so mandara setup doesn’t talk to a running
shell.These hold for every change. Reviews check them, and several are enforced by tests.
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.)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.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.~/.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).cargo xtask resource-check, mandara msg diagnostics frames)
before claiming a win.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).Mandara is design-first: it should never look like an unstyled toolkit.
crates/mandara-core/src/material.rs.MandaraPalette (surface_*, primary, on_surface,
urgent_coral, success_emerald, warning_amber, …).crates/mandara-theme/themes/, expanded with contrast guarantees. A
Look sets the bar style, desktop cards, palette, typography and shape
scale together, and restoring brings back the user’s own setup. Light
mode is first-class: check both.SpringValue (an analytical damped
oscillator: exact at any frame rate, and an interrupted animation keeps
its velocity), driven by request_animation_frame only while something
moves. Respect the reduce motion setting and the global spring and
speed settings.fonts::tabular_nums) on clocks,
percentages and counters so widths never jitter. Latin and Arabic are
both first-class (Inter, JetBrains Mono, Alexandria, Cairo).Bidi
helpers (crates/mandara-ui/src/bidi.rs): row for reading order,
ltr_row for things that stay left to right (times, key chords, media
transport, charts), and *_start / *_end instead of left and right.Color can
measure it), make everything reachable by keyboard, keep focus inside
open modals and close them on Esc.registry::locked, never silently
ignored or hidden.Things that cost real debugging time:
InputMaskRegistry, which diffs the
union and calls wl_surface.set_input_region only when it changed. A new
interactive surface that forgets to register swallows clicks or lets
them through.mandara_core::query_outputs (zxdg_output_manager_v1). Never request
a layer surface with width or height 0: it triggers a viewport protocol
error. Set inactive_frame_interval: None on layer surfaces, or GPUI
throttles them to 30 fps.crates/mandara-ui/src/backdrop
paints it in software into wl_shm subsurface planes
(mandara_core::shm_canvas) on its own Wayland connection and thread.
Subsurface positions are whole logical pixels, so snap plane rectangles
to shm_canvas::grid_step or text blurs.plugins/external.rs, instance()) so open surfaces switch to the new
build. Don’t capture an instance in a closure once.mandara-services’ shared runtime and talk to the UI only through
channels; file I/O and decoding go to background tasks..clip_rounded() (mandara_ui::style::ClipRounded):
it clips without changing layout, whereas overflow_hidden also lets a
flex item shrink below its content. The shared card helpers already do..occlude().kit::slider or panel_fx::big_slider
(both use crate::slider_model). Their on_change returns the write’s
completion: a service request’s reply receiver, or None for a local value.
That lets the model keep one write in flight per slider, draw the dragged
value instead of the server’s late echoes, and keep following the pointer
outside the track. Read readouts next to a slider through
slider_model::held so they move with the fill. Never fire a service
command for every pointer event: a slow sound server falls behind, and the
bounded queue drops the newest values.pkill -f "mandara studio" (or any pattern
that also matches your own shell’s command line); match the binary path
or use a recorded PID. GUI processes started from a tool shell die with
it; detach them (setsid -f).XDG_RUNTIME_DIR short.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.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.PLUGIN_CATEGORIES in
crates/mandara-studio/src/app/store_page.rs).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.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.
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.
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.
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.
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.
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.
Every user-facing change updates its docs in the same change:
## Unreleased in plain,
friendly words, saying what people will notice (format in
docs/releasing.md). It becomes the release
notes on GitHub, in Studio and in mandara changelog.docs/.docs/ (index): features by
area, install, usage, plugins (table and counts), Studio pages,
commands, configuration; screenshots come from the sandbox.crates/mandara-install/src/packages.rs) when a runtime dependency
changes.fix:, feat:, perf:, refactor:, docs:…, with ! or a
BREAKING CHANGE: footer for breaking changes. The release tooling reads
them to pick each component’s version bump (see
docs/releasing.md).versions.toml by hand: on every push to
main the Release workflow runs cargo xtask bump, commits
chore(release): … with tags, and publishes a release when the shell’s
version changed.cargo check --workspace --all-targets
and cargo test --workspace, and look at cargo clippy --workspace
--all-targets. CI runs the same on x86_64 and aarch64, builds the
example plugins, checks the alternative allocators and dry-runs the
installer.