Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Mantle

Introduction

Mantle runs a desktop shell written in Lua on Wayland. The engine evaluates your shell.lua, which returns the surfaces to show (bars, windows, popups, a lock screen). Each surface holds a tree of nodes, and any node property can be a live signal that updates itself when a capability (audio, workspaces, the clock) pushes new state.

Build the first shell below, then find the rest by topic or by task. The glossary defines every term.

Your first shell

1. Create the config

mantle init -c ~/.config/mantle

It writes a starter shell.lua and a .luarc.json that points lua-language-server at the API stubs, so your editor completes and type-checks. The config is a directory, not a file (init).

2. A first bar

shell.lua runs top to bottom and returns one surface or an array of them. This bar shows a launcher button, the workspaces of the first output and a clock. The button and a keybind share one piece of named state, launcher_open, which shows a second panel.

local launcher_open = state("launcher_open", false)

local clock = text {
    content = mantle.system:map(function(system)
        return os.date("%H:%M", system and system.time)
    end),
    foreground = "#cdd6f4",
}

local workspaces = list {
    direction = "Horizontal",
    spacing = 4,
    source = mantle.workspaces:map(function(ws)
        return ws and ws.outputs[1] and ws.outputs[1].workspaces or {}
    end),
    key = function(workspace) return tostring(workspace.id) end,
    itemfn = function(workspace)
        return button {
            padding = { left = 6, right = 6 },
            radius = 4,
            background = workspace.populated and "#45475a" or "#00000000",
            on_click = function()
                mantle.workspaces:focus(workspace.id)
            end,
            children = { text { content = tostring(workspace.idx), foreground = "#cdd6f4" } },
        }
    end,
}

local launcher_button = button {
    padding = { left = 8, right = 8 },
    background = launcher_open:map(function(open) return open and "#89b4fa" or "#313244" end),
    on_click = function() launcher_open:set(not launcher_open:get()) end,
    children = { text { content = "Apps", foreground = "#cdd6f4" } },
}

return {
    panel {
        id = "bar",
        layer = "Top",
        anchor = { top = true, left = true, right = true },
        exclusive = true,
        width = "Fill",
        height = 32,
        background = "#1e1e2ee6",
        child = row {
            width = "Fill",
            align_v = "Center",
            spacing = 8,
            padding = { left = 8, right = 8 },
            children = { launcher_button, workspaces, rect { width = "Fill" }, clock },
        },
    },
    panel {
        id = "launcher",
        layer = "Overlay",
        visible = launcher_open,
        keyboard_interactivity = "OnDemand",
        width = 400,
        height = 300,
        background = "#1e1e2e",
        radius = 12,
        child = text { content = "Launcher", foreground = "#cdd6f4" },
    },
}

LineWhy
mantle.system:map(...)A derived signal; content re-resolves on every push (once a second unless configure says otherwise). :get() would freeze it
system and system.timeCapabilities read nil until their first push, so every map handles nil
list { source, itemfn, key }Rebuilds one button per workspace when the list changes (list)
:focus(id)Fire and forget; the new active workspace arrives in the next push (actions)
width = "Fill" on the panel and the rowThe panel’s root spans the anchored edges only when asked (size); the "Fill" rect then pushes the clock right (alignment)
visible = launcher_openThe launcher panel maps and unmaps with the state

3. Run it

CommandDoes
mantle checkEvaluates and lays out the config with no Wayland, with every capability nil and again with sample data, then exits; 1 on error. Run after every edit. What it misses
mantle -dStarts the shell detached and prints its pid
mantle log -fFollows the running shell’s output, print included
mantleRuns it in the foreground instead

4. Edit it live

Saving any .lua or .frag file under the config directory re-evaluates shell.lua in the same process, and named state keeps its value. A reload whose evaluation raises keeps the previous scene on screen, logs the error and sets mantle.rescue, which a config can draw as an error banner. The next successful reload clears it. What a reload keeps: what survives it.

5. Split into modules

require("widgets.clock") loads widgets/clock.lua from the config directory, and nothing outside it. Bind every require to a local before listing it in a table: it returns the module and its file path. Example and rules: modules.

6. Bind a key

mantle toggle <name> flips a declared boolean state in the running shell, so the launcher above opens from the compositor:

# Hyprland (hyprland.conf)
bind = SUPER, A, exec, mantle toggle launcher_open
# Hyprland (Lua config)
hl.bind("SUPER + A", hl.dsp.exec_cmd("mantle toggle launcher_open"))
# niri (config.kdl, inside binds {})
Mod+A { spawn "mantle" "toggle" "launcher_open"; }

mantle set writes any value, and mantle call runs an action (commands).

Topic index

The sidebar’s order. Every guide, surface and node page ends with How do I… and Gotchas tables.

PageForSections
installationRequirements, install, autostartRequirements · Install · Set up a config · Run the shell
cliThe mantle binary and keybindsCommands · Flags · Environment variables · Binaries · Which config and which shell · Values and arguments · What check covers · Exit codes
runtimeThe VM, require, reloads, limitsThe VM · Modules and require · Evaluation, reload and generations · What survives a reload · Limits and budgets · Output and logging
signalsReactivity and named stateThe one rule · Reference · Derived signals · Named state · How re-resolution works · Switching views
surfacespanel, window, popup, lockProperties every role takes · Per-output child · Input region
nodesLayout and the node kindsLayout model · Common properties · Identity · Switching
paintHow a box is drawnColours · Box properties · Gradients · Clip · Mask · Shadows · Blurs
animationanimateHow a tween starts · Entry keys · Spring · Keyframes · Exit
inputPointer and keyboardHit testing · Pointer · Hover · Scroll · Text fields · Secure fields
processesRunning other programsWhich one do I use · process.run · process.detach · session_process
scriptingStorage, timers, actions, utilitiespersistent_table · timer · action · json.decode · log · fuzzy · palette.quantize · fonts
capabilitiesmantle.<name> state and actionsReading and acting · Capability list · Renderer members
cookbookComplete widgets to copy
faqA symptom whose cause lives on another pageFirst steps · Nothing shows · A save or a click does nothing · Values are wrong or stale · Errors in the log · Running processes · Capabilities
glossaryTerms; engine-internal ones are in CONTEXT.md
changelogLua API and CLI changes
roadmapGaps, proposed work, non-goals
documentingWriting and testing a page of this book

Editor completion comes from the lua-meta/ stubs that mantle init wires up; mantle.lua is generated from the Rust capability types. DECISIONS.md records why each contract is what it is, cited as ADR-NNNN.

How do I…

TaskAnswer
Install Mantle and start it with the sessionInstall, run the shell
Open UI from a compositor keybindBind a key, drive UI from a keybind
Make a keybind run Lua and print a resultaction
Show a reload error in the barError banner
Find why a reload or budget failedLimits and budgets, output and logging
Show a live value from the systemFirst bar, one rule
Combine two sources into one valueDerive from two capabilities
Debounce a search or hold a valueDebounce a search, delay
Flash a node when a value changespulse
Switch between tabs or viewsSwitching views, with ids
Draw different content per monitorPer-output content, per-output child
Type into a panelKeyboard focus, text fields
Close an overlay or popup on an outside clickClose an overlay, dismissal
Show a dropdown under a buttonAnchor to a node’s geometry, nested menus
Show a tooltip on hoverTooltip
Build a lock screenlock, secure fields, recipe
Centre or space out itemsCentre something, alignment, push items apart
Draw a progress meterrow and column
Show an app’s iconicon
Crossfade a wallpapertransition
Build a list from datalist
Scroll a long listScroll a long list, scroll
Write a shader effectshader
Round and clip contentRound an image’s corners, clip
Blur the desktop behind a barBlurs
Fade or slide a nodeanimation, spring
Show a spinnerKeyframes
Animate a node out before it goesExit
Make a slider or wheel controlPointer
Run a command and read its outputprocess.run
Launch an app that outlives the shellprocess.detach
Keep a daemon running for the sessionsession_process
Remember a setting across restartspersistent_table
Repeat something every few secondstimer
Search a list as you typefuzzy
Theme from the wallpaperpalette.quantize
Handle a capability that has not pushed yetReading and acting
React to a capability change (OSD, sound)on_change, Volume OSD
Copy a complete bar, launcher or lock screenCookbook
Find why something shows nothing or does nothingFAQ

Source: init, starter, CLI, watcher, reload and rescue, require path.