Changelog
User-facing changes to the Lua API and the mantle CLI. The format follows
Keep a Changelog. Mantle is pre-release and unversioned,
so everything since the rename from Obelisk sits under Unreleased.
Unreleased
Added
- x86_64 release builds: a
.deb, an.rpmand a tarball, built in Ubuntu 26.04 by thereleaseworkflow on av*tag (install). - This site: guide, one page per node, surface and capability, cookbook, glossary. Every Lua
example on it runs as a test, and the
lua-metastubs link each entry to its page. - Nodes:
shader(a config fragment shader) andcapture(a live output preview, withlivecapped at a frame rate andregionto crop). - Paint: gradient backgrounds,
mask,shadow_*withshadow_mode,content_blur,backdrop_blur,image.source_blur,clip = "None"andz(paint). palette.quantizefor an image’s dominant colours.mantle.screensentries gain position,model,description,fractional_scaleandorientation;mantle.screensandmantle.rescuegainon_change.monitor = "Active"lets the compositor pick a panel’s output.- A plain
textfieldhas a caret, a selection, grapheme-correct deletion, key repeat and Ctrl bindings; Left/Right reachon_navigatewhen the caret cannot move; Escape in asecure_submitfield callson_cancel. - Numbered verbosity (
-v,-vv,-vvv), quiet by default;--profileprints its own reports (CLI). --profilereports each capability’s snapshot pushes sent and deduped beside its size (CLI).- A bare
mantle calllists the running config’s actions, and a baremantle setormantle toggleits states with their values (CLI). mantle.system:configure({ interval = 60 })pushes the clock every minute on the minute instead of every second, or never with0; default1(system).mantle stop [--pid | -c]stops a running shell and waits for it to exit;mantle.pidis the shell’s own pid, so a config can stop itself (CLI).mantle.updatesruns on Fedora throughdnf(dnf5 or dnf4) and on Debian and Ubuntu throughapt-get;package_manageris"dnf"or"apt"(updates).
Changed
mantle.updateschecks throughpacman,pacman-conf,curlandvercmpinstead of linking libalpm, so building no longer needs libalpm and the binary starts off Arch.packagesleaves outIgnorePkgentries, andinstalled_sizerounds to the two decimals pacman prints (updates).- A
geometryrect an animation moved schedules one pass when the animation settles, so a property bound to it catches up instead of waiting for an unrelated write. - A
state:setof the value the state already holds re-resolves nothing, and a:maporcomputedre-resolves its readers only when its result changes. Scalars and plain-data tables compare by value (signals). mantle.systempushes land on the wall-clock second after a resume or clock step too, rather than mid-second until a restart.mantle.keyboardpushes once when it starts, so it is no longernilwithout niri or Hyprland until a lock key or the backlight changes.mantle.sysinforeads on the wall-clock second, first at the next one afterconfigurerather than one interval later, so its pushes can sharemantle.system’s layout pass.mantle.applicationswatches its directories and rescans after a change, so installs and removals appear withoutrefresh(applications).- A reload kills every
process.runchild and calls itsexit_cb(nil)before the new evaluation runs, so a top-level follower restarts instead of doubling (processes). - Lua’s
warnnow logs at warn level likelog.warn, on by default; before, it printed nothing. - A
text.contentrun’skindmust be"text", as a notification text span’s is; any other value fails the pass instead of being ignored. lua-metanode, surface and global types come from the Rust types the engine parses with: a surface’schilddeclares theBoundit always took, a panel’swidth/heightthe[0, 8192]it always enforced. A signal-bound popupparentfails every pass, not only the first.- A callback (
on_*) that is not a function (writecond and fn or nil), asubmitorautofocusthat is not a boolean, and anilorfalseentry inchildren, which dropped every child after it, fail the pass instead of being ignored. - A table property (
padding,anchor,shadow_offset,min_size,anchor_rect, ananimateentry, a text run,secure_submit, …) and thesession_process,persistent_tableandpalette.quantizeoption tables refuse a key they do not take. secure_submittakes onlylock/authenticate,polkit/authenticateandnetwork/connect.- Two surfaces with one
id, or two different scalar seeds for onestatename in one evaluation, fail the evaluation. - A
listwithoutsourcebuilds no items instead of raising. - A
listkeeps its items while nothing its last build read has changed, instead of callingitemfnfor every item on every pass: a write elsewhere on its surface costs about half as much.itemfn,keyand the maps inside items must read time and mutable data through signals, or they show what they read at the last build (when items rebuild). - A node reads each
childrenorchildtable once and keeps it while it holds that table, so a pass’s property reads outsidelistbuilds cost about 40% less. A node table orchildrenarray changed in place is no longer seen; bind a signal or:seta new table (gotchas). - A node keeps the properties it resolved until a signal they read is written, instead of running
every getter on its surface on every write: a clock tick re-reads the clock’s node, not the bar.
A map,
computedor functionchildthat readsos.date()with no time,os.time(), a mutable variable or a file keeps its last answer until a signal it read changes. Derive time frommantle.system:mantle.system:map(function(s) return s and os.date("%H:%M", s.time) or "" end)(what a node reads again). - A write one
listitem read, such as itshover, builds that item alone instead of every item: 4.5 ms instead of 16 ms a pass on 500 rows (when items rebuild). - A wheel over a container whose
scrollsignal no getter,maporlistbuild reads moves its children without a layout pass: 0.06 ms instead of 2.4 ms on 500 rows. - Breaking: each capability action is a method, and
:invokeis gone:mantle.audio:set_volume(0.5)replacesmantle.audio:invoke("set_volume", 0.5). The editor stubs type each action’s own arguments, somantle.audio:set_muted(0.5)is flagged. An unknown name raises, listing the actions the capability has, as does any action on a capability with none; a.call in place of:raises instead of sending a wrong argument (actions). - A state field read off a capability raises
did you mean mantle.audio:get().volume?, and a misspelled action names the one it is close to. - A misspelled node or surface property raises “did you mean
content?” instead of listing every property the kind takes; the list stays for a key close to none. mantle setandmantle togglewait for the shell and exit 1 with its reason when it refuses the write (CLI).- A tween advances on its own surface’s frame callbacks, so a surface animates at its output’s refresh rate instead of the fastest animating output’s (animation).
mantle checklays the config out on stand-in outputs and fails on a layout error, and says when the stubsmantle initwrote are out of date.- A pass that fails names every broken node, one per line, in
mantle check, the log andmantle.rescue, instead of stopping at the first. It lists 20, then counts the rest (what check covers). Achildrenentry that is not a node readschildren[1]: expected a node table, counted from 0 like the rest of the path. mantle checklays out a second time after one sample push per capability, every list one entry long, so an error in a listitemfnor a data-only branch fails the check; each error names its pass (what check covers).- Errors name files relative to the config directory (
widgets/bar.lua:4, not a path Lua cut to...2b41-2949-.../bar.lua:4), includingrequired modules. A layout error’s path names the line that built each node (row[0] (shell.lua:7) > ...), a failing:maporcomputedthe line that created it (signal created at shell.lua:3), and tracebacks drop the engine’s own frames.mantle checkprints the config directory once instead ofshell.lua: shell.lua failed to evaluate(CLI). - The
.luarc.jsonfrommantle initwarns on unused locals and on thetype-check,unbalanced,strictandglobaldiagnostic groups in every file. mantle.rescueis set when a reload, or a live update, fails to apply, and clears only when a scene applies. Errors raised in callbacks, failed spawns and failedmantle calls are warnings, and a missing icon or undecodable image warns once per name.- The editor stubs flag a misspelled property or table key and a percent that is not a whole
"0%"to"100%", and typechildrenas taking a signal, as the engine does. - The editor stubs type a capability’s
:get()asT?, so an unguarded read of a field warns;mantle.screensandmantle.rescuestay non-nil. Ananimatekey the node does not take (orz), and an unknown key in ananimateentry,{ steps = n },session_process,persistent_tableorpalette.quantizeoptions, are flagged. Each node’sanimateis typed by its own alias (RectAnimations,TextAnimations, …); a wrapper that passesanimatethrough types it with that alias. A capability orscroll(...)handle passes where aSignalor ascrollproperty is declared. translate,scale,rotateandorigintweens repaint without relayout.- Hover callbacks fire on pointer entry.
- A
nilor non-signalcomputeddependency raises naming its index,computed() dependency 2 is nil; ...; before, a hole dropped every dependency after it. A named key in the list raises too. timer’smsis anumber:timer(1.5, fn)runs, andtimer(-1, fn)or a NaN raisestimer(-1) is outside 1..=86400000 milliseconds, not mlua’serror converting Lua integer to u64.mantle.idle:register_thresholdoutside1..=4294967seconds raises naming the range; before, a negative one got mlua’s conversion error and a huge one was clamped.cancel_threshold(-1)is a no-op like any unknown handle.- A
nilin the returned surface list raisessurface 2 is nil: ...; before, it dropped every surface after it. A named key in the list (return { bar, cfg = x }) raises instead of being ignored. - An equal capability snapshot is not pushed again, except
trayandnotifications. - A bad
layer,corner_shape,keyboard_interactivity, popupanchororgravity,constraint_adjustmententry or easing name fails with one wording that lists every choice:expected one of `Background`, `Bottom`, `Top`, `Overlay`, got ….