Changelog
User-facing changes to the Lua API and the mantle CLI. The format follows
Keep a Changelog. Each version is tagged vX.Y.Z
and published on GitHub releases. While the
version is 0.x, a minor release can break the Lua API. A Breaking: entry needs a config edit.
Unreleased
0.13.3 - 2026-11-10
- Fixed:
mantle.lockandmantle.polkitpublish their idle state at startup, socomputedand:mapover them run without waiting for a lock or authentication request. After a restart while locked,mantle.lockstaysniluntil the lock is taken again.
0.13.2 - 2026-11-10
- Fixed:
iconnodes use the icon theme set withgsettings set org.gnome.desktop.interface icon-theme, read through the desktop portal, and redraw when it changes. They used to fall back tohicolorwithout a GTKsettings.ini, and a theme change needed a restart. - Fixed:
mantle.updatesreports the current package and install step counts when pacman runs without a terminal. - Fixed:
mantle.audio:set_default_sinkandset_default_sourcechoices stick. A volume change no longer moves the default back to the highest-priority device.
0.13.1 - 2026-11-09
- Fixed:
process.run’son_exitandcapture’son_donefire as soon as the command exits. A command that closed its output and kept running used to report its exit up to 100 ms late. - Added:
imageandiconnodes load.icoand.bmpfiles, so an app whose only icon is a.ico(/usr/share/pixmaps/<name>.ico) shows it. - Fixed: an
imagenode’s SVG draws its embedded<image>PNG, JPEG, GIF and WebP pictures. They used to draw blank. - Changed:
mantle.keyboard’scaps_lock,num_lockandscroll_lockstay live without theinputgroup, up to 250 ms late. They used to freeze at their startup values.
0.13.0 - 2026-11-06
- Added:
mantle.audio.cardslists each ALSA and Bluetooth sound card with every profile it offers (index,name,description,available) and theactiveone, so a config can switch one card between analog and HDMI. Unplugged profiles stay listed withavailable = false. - Breaking:
mantle.audio:set_bluetooth_profile(device, index)is nowmantle.audio:set_card_profile(device, index), and takes anycards[].device. Rename the call. - Breaking:
mantle.audio.bluetoothis gone; Bluetooth cards are inmantle.audio.cards, with amacto joinmantle.bluetooth. Each profile carries thecodecit names, if any, and unavailable ones stay listed: a codec picker readsprofileswherecodec and available, instead ofcodecs. - Added: Each
mantle.audiosink and source carries its ownvolume,mutedandbalance, thecard(acards[].device;nilfor a virtual device) it comes from, and its reboot-stablenode_name, so a mixer can show and set every output, not only the default, and group outputs by card. - Breaking:
mantle.audio’svolume,muted,balance,source_volumeandsource_mutedare gone: read theactiveentry ofsinksorsources. - Breaking:
mantle.audio:set_volume,set_muted,toggle_muteandset_balancetake asinks[].idorsources[].idfirst:set_volume(sink.id, 50).set_source_volume,set_source_mutedandtoggle_source_muteare gone; pass a source’s id to the same actions.set_sink_channel_volumeandset_source_channel_volumeare now oneset_channel_volume(id, index, volume). - Added:
mantle.audio.cards[].portslists each card’s ports (index,name,description,type,direction,available,profiles), andmantle.audio:set_port(card, index)switches one, such as speakers to headphones. Unplugged ports stay listed withavailable = false. - Added:
mantle.audio.apps[].targetis the sink an app plays into, or the source it records from, andmantle.audio:set_app_target(id, device)moves it;nilreturns it to the default. - Added:
mantle.audio:set_virtual_devices(devices)declares null sinks, combined sinks, loopbacks, echo-cancelled microphones and filter chains such as an equalizer. Each appears insinksorsources, PipeWire’s module arguments pass throughargs, and a reload keeps them until the next call. - Added:
mantle.audio.cards[].ports[].activemarks the port a card routes through now, such as headphones rather than speakers. - Added:
mantle.audio.apps[].pinned_tois the device an app is pinned to withset_app_target, or by another mixer;nilwhen it follows the default or the device it names is gone.targetstays the device it plays through. - Added: Each
mantle.audio.cards[].profiles[]entry carriesoutputsandinputs, how many sinks and sources it makes, so a picker can list output or input profiles without reading their names. - Added:
mantle.audio:set_level_meters(ids)meters those sinks and sources, andmantle.audio_levelscarries their peaks (levels, each{ id, peak }in percent) up to 30 times a second, for VU meters. No call, no meter; an empty list stops them. - Changed:
mantle checkfails when a config reads a capability field that does not exist, such as a field a release removed, and names the nearest one; a running shell still readsnil. It covers reads made while it evaluates and lays out the config;on_click,actionandtimerhandlers do not run. - Changed:
mantle checkfails when a capabilityon_changehandler raises on sample data; before, it only logged a warning. - Fixed: A content-sized node with
animate.heightoranimate.widthgrows with a child’s running animation. A pass between two frames, such as a disclosure’s first open, used to freeze it until the child settled, so it stuttered.
0.12.1 - 2026-11-05
- Added: Packages point to what fingerprint unlock needs:
mantle-binlistsfprintdas optional, the.debsuggestsfprintdandlibpam-fprintd, and the.rpmsuggestsfprintdandfprintd-pam.
0.12.0 - 2026-11-05
- Added: The lock screen unlocks with a fingerprint when fprintd has a finger enrolled. Packages install the
mantle-fingerprintPAM stack.mantle.lock.fingerprintisnilwhile fingerprint is off, else{ messages }with pam_fprintd’s lines. A missed scan never counts as an attempt. - Added:
mantle.polkit.identitieslists the accounts a polkit request accepts,identityis the one it authenticates as (your own when offered), andmantle.polkit:select_identity(uid)switches to another. - Added:
mantle.lockandmantle.polkitgainprompt({ id, text, echo }, PAM’s own question,nilwhile PAM owes nothing) andmessages(PAM’s info and error lines). Two-factor codes, a re-asked password and fingerprint hints now reach the lock screen. Bind the secure field’smaskedtonot prompt.echoto show a visible answer. - Changed: The lock screen starts PAM when the session locks, so
promptis ready before you type. A password typed before PAM asks is held for its first masked prompt, andauthenticatingnow means an answer is with PAM. - Changed: A polkit prompt starts PAM on the first submit, so its
promptisniluntil then. - Changed: The release tarball no longer writes to
/etc, so an upgrade keeps your edits. It puts the PAM stacks, with-debianvariants for Debian and Ubuntu, and the polkit rule in/usr/local/share/mantle/; copy them to/etconce. - Fixed: After a faillock lockout runs out, the first correct password on the lock screen unlocks. It used to fail once, because the lock kept the PAM attempt that had shown the lockout.
- Fixed: A lock screen no longer refuses a correct password once the account’s password has expired. The session is already logged in, as GDM decides.
- Fixed: Polkit prompts in other languages show their real text instead of escape codes, and a visible polkit prompt is no longer answered with an empty line.
- Added:
masked = falseon asecure_submittextfield draws what is typed instead of the mask. Turning it off wipes what was typed under the mask.
0.11.2 - 2026-10-29
- Added: Animated WebP and APNG images (
.apngfiles too) now play and loop inimagenodes, like GIFs, andplayingpauses them. Masks and a shader’simagesstill draw the first frame.source_bluris ignored on them, as on GIFs. Still WebP and PNG files draw as before. - Changed: A long GIF on a large canvas may now loop over fewer frames, and a GIF whose frames never change draws as a still.
- Fixed: On Hyprland 0.57,
mantle.workspaces,mantle.windowsand the special workspace list stayed empty whenever a special workspace existed. Hyprland 0.57 sends noidfor specials.
0.11.1 - 2026-10-27
- Fixed: Images in a fast-scrolled list decode the ones now on screen first, and drop decodes for images no longer shown.
- Fixed: The Renderer settles about 3 MB lower after image-heavy use.
- Fixed: A missing
lsblknow logs a warning instead of leavingmantle.sysinfo.diskssilently empty. - Fixed: Unreadable
/dev/inputdevices log a warning naming theinputgroup, instead ofcaps_lockandnum_lockquietly stopping updates. - Fixed: When screen capture can’t use dma-buf, the warning names the cause, such as a render node the user cannot open.
0.11.0 - 2026-10-23
- Breaking:
mantle.networkdrops its flatscanning,strength,wifi_ipandavailable_networks, which repeated the primary Wi-Fi interface. Read them fromwifi_devices[1](nilwithout Wi-Fi hardware). - Added:
mantle.android_connectlists your phone and other KDE Connect devices with battery, signal and pairing state, and can ring, ping, pair, send files and clipboard, switch plugins and clipboard sync per device, offer commands your phone can run on the PC, and show transfer progress and why browsing failed. It needskdeconnect-servicerunning.
0.10.0 - 2026-10-22
- Breaking:
file.write(path, text, on_done?)is nowfile.write(path, text, opts?, on_done?). Addnil,before an existing callback; a function in theoptsslot raises. - Added:
file.write(..., { in_place = true })rewrites the file in place, so a program watching that one path keeps its watch. - Added:
process.detach(cmd, args, { env = { NAME = "value", OTHER = false } })andmantle.applications:launch(id, { env = ... })set or unset environment variables for the program they start; a string sets,falseunsets. - Added:
:mapandcomputedsignals, and sopersistent_tablekeys, haveon_change(fn(current, previous)), run like a state’s when a source’s write changes the value.delayandpulsestill have none. - Changed:
file.readno longer logs its errors; handleerrinon_done.
0.9.1 - 2026-10-21
- Fixed: a
mantle.mprisFirefox or zen player’spositioncould fall up to a second behind after a play, pause or seek, and step back mid-track. Those browsers reportpositionin whole seconds; Mantle now keeps the precise offset they send with each seek. - Fixed: a
computedreading anothercomputedthat had just changed ran its function a second time on the next unrelated layout pass.
0.9.0 - 2026-10-20
- Added:
mantle.mprisplayers carrypid, the process that owns the player, so a config can match a player to itsmantle.audio.apps[]stream bypidinstead of by name. It isnilwhen the bus does not report one. - Added:
mantle.audio.apps[].media_name, PipeWire’smedia.namefor the stream, such as a browser tab’s"YouTube Music", so a config can tell one app’s streams apart. It updates as the tab navigates. - Breaking:
state(name, initial)raises when one evaluation seeds a name with a table and a scalar, such as{}then-1; rename one. Both calls used to return one signal, so the table’s reader failed only once its node was built, often in a closed panel, andmantle checkpassed.
0.8.1 - 2026-10-19
- Changed: the LuaLS stubs type
mantle.traymenu_typeandtoggle_type,mantle.networkvpns[].kindandmantle.bluetoothdevicecategoryas their literal values, so the editor flags a misspelled comparison. A tray menu entry with an unknownmenu_typenow reads as"standard", and one with an unknowntoggle_typehasnilfor bothtoggle_typeandtoggle_state(the state was-1). - Added:
on_click,on_double_clickandon_pressreport a mouse’s back and forward thumb buttons as"back"and"forward". They used to be ignored. A handler that treats every button but"left"alike now also runs for them.
0.8.0 - 2026-10-17
- Breaking:
effect.shader’su_inputandu_input_blurredmay be larger than their content, sotexture()on them reads past its edge. Read them withmantle_inputandmantle_input_blurred. - Changed: a
listwith bothkeyandscrollbuilds only the rows near its viewport, so a 1000-row list costs about as much as 50. A row scrolled more than a viewport away plays no exit and loses its tweens, keyboard focus and text draft. Keep row state that must survive in a namedstate. - Changed: the
--profileidle report’smscolumns start withhandlers=: due timers, signal wakes, off-thread results and scroll steps. They used to count towardresolve=, which could show 130 ms with no resolve pass. - Changed:
ToplevelHandle:resizetypes its edge as a union of the eight edge names, so LuaLS flags a misspelled edge. - Fixed: a
shapewithcornercommands could cost about 1,000 times a rounded box to draw on every repaint. Mantle skips zero-length curves. - Fixed: an idle shell spent about 1 ms per clock tick in the Lua garbage collector, run after every getter that allocates.
- Fixed: an idle shell showing an image from your home directory (a
~/Pictureswallpaper) redrew the whole surface twice a second. Mantle watches image files with inotify, so an edited, replaced or deleted file repaints at once, shaderimagesincluded. A symlink is still checked twice a second. - Fixed: a
"fill"scroll container under awindow,lockor spanningpanelwith no size of its own grew to its content instead of the surface, so it never scrolled (since 0.4.0). - Fixed: the .deb’s lock screen and polkit agent refused every password. Its PAM stack named
system-auth, which Debian and Ubuntu lack. It includescommon-authandcommon-accountinstead.
0.7.5 - 2026-10-16
- Changed: a getter,
on_changehandler, action or timer no longer fails at 2.5 ms of CPU, which broke configs on slower machines. It fails at 50 ms, as a runaway:exceeded the 50ms CPU limit for one evaluation. Past 2.5 ms the log warns once per signal and names the slowest one. - Added:
mantle-binon the AUR, published from each release’s binaries. - Fixed: on NVIDIA with egl-wayland2’s explicit-sync fix (NVIDIA/egl-wayland2#56), the renderer leaked about 3 MiB an hour. egl-wayland2 1.0.2 holds the same buffer releases where Mantle cannot reach them, so on 1.0.2 the leak remains until #56 ships.
0.7.4 - 2026-10-16
- Fixed: while the session was locked, the renderer could hold one CPU core at 100% until unlock. It showed after a restarted shell took the lock over, and dates from 0.1.1.
- Fixed: with
--profileon the NVIDIA proprietary driver, the supervisor leaked one file descriptor per sample (since 0.7.1). After about 2.5 h at--profile=10it could not spawn processes, so the lock screen could not unlock.
0.7.3 - 2026-10-16
- Changed: a looping
keyframesanimation or adelaycosts no frames while it holds one value. A blink that rests 3.6 s no longer costs a tick per refresh. - Changed:
mantle checkprints the source line under each error that names afile.lua:N, indented after|. - Changed: a touchpad scroll over a
scrollcontainer coasts to a stop after the fingers lift. It used to stop dead. Wheel notches are unchanged. - Added: a
keyframesentry takes"current"as its first frame, to start the run on the value on screen. See Keyframes. - Added:
runon akeyframesentry. A different value starts a new run with the same frames, even mid-run, so a counter replays an animation without removing the entry and adding it back. - Added:
settled = settled("name")on any node gives a read-only signal. It isfalsewhile ananimatetween on that node runs andtrueonce the last one ends. Swap content when a collapse finishes instead of after a guessedtimer. - Added:
effect.shader.input = "desktop"gives a shader on apanelbox the desktop behind it, for lens and refraction effects. It captures through wlr-screencopy. Returnmantle_inputto leave a pixel, as with"backdrop". See Desktop input for how it composites and what it costs. - Added:
shape = { melt = k, parts = { .. } }joins up to 8 rounded boxes with fillets of aboutkpx. The fillets stay tangent throughanimate.shape. Paint, borders, clip, shadows, hit testing and regions follow the joined shape. See Melt. - Added:
mantle checkwarns when its surface’s edge cuts off an outer shadow by 4 px or more, naming the side and the room to add. A side where apanelmeets the output edge is exempt. The exit status is unchanged. - Added: a warning when another node replaces an id-less child with
animate.exitin place, which skips its exit. - Fixed: a
compositor_blurregion that changed while the rest of the surface did not applied only at the next repaint. - Fixed: a rotated or skewed box takes input across its rotated shape, not its upright bounding box. Clicks in the empty corners pass through.
- Fixed:
compositor_blurfollowsrotateand skew instead of blurring the upright bounding box. - Fixed: text in a
rotated, skewed or non-uniformly scaled node drew about 1 px heavier per stroke. - Fixed:
mantle.privacynames apps using the camera through PipeWire (browsers, Snapshot, OBS) instead of listingpipewire.
0.7.2 - 2026-10-13
- Changed: an icon name containing
/no longer resolves through the icon theme. - Changed:
mantle.secretstalks to the Secret Service directly. A locked keyring shows its unlock prompt, and the store waits for it. A missing default keyring is created.timed_outmeans a service call took over 25 s, and it is final. - Fixed: icon lookup finds
.svgztheme icons and icons in~/.icons. - Fixed: a getter, handler or callback could fail with “exceeded the 2.5ms CPU budget” for garbage collection it didn’t cause.
0.7.1 - 2026-10-12
- Changed: a
moveortranslatepaints at whole buffer pixels, so a slide of under one pixel steps instead of gliding. Fractional positions drew glyphs differently on each driver. - Changed:
on_hoverfires on the first pointer motion after layout moves its node under or off a still pointer. The pointer used to have to leave the node and come back. - Changed: Tab on a lone plain
textfieldthat does not hold focus focuses it. It used to reachon_key. - Added:
sizein bytes on eachmantle.foldersentry. A symlink reports its target’s size. - Added:
textelideaccepts"start"(…then the end) and"middle"(start and end around a…, the end taking the odd grapheme). Both cut at grapheme boundaries, andelided(name)is true for either. Underwrap = "word"both act as"end". - Added:
on_key_release(key)on any node hears a key going up whose press was offered toon_key, routed the same way.keyis{ name, text, modifiers }.mantle input keysends the release after the press. See key handlers. - Added:
on_double_click(rect, button, pointer, modifiers)fires afteron_clickon a second click on the same node with the same button, within 400 ms and 4 px. The innermost node withon_double_clicktakes it, so single clicks still reach an ancestor’son_click. See pointer. - Added:
on_dragtakes a fifth argument,velocity = { x, y }, in surface px per second over the last 100 ms of motion. On"end"it is the release velocity, or the last motion’s when the pointer left the surface. Use it for flick scrolling. See pointer. - Added:
image_status(name), bound as an image’sstatus, reports the load of itssource:"empty","loading","ready"with the file’s ownwidthandheight, or"failed". See image_status. - Added:
image.playing, defaulttrue.falseholds an animated GIF on its current frame (the first, if it never played) and stops redrawing it.trueresumes from that frame. Each image node pauses on its own. - Fixed:
elide = "end"on one line keeps a right-to-left paragraph reading right to left when the kept head loses its first strong character ("123 שלום"cut to"123…"). - Fixed: a panel with
exclusive_zone = "ignore"or a fixed zone could map at a transient size first. - Fixed: Tab on the only control, while it holds focus, shows the focus outline and reaches
on_key. It used to be swallowed.
0.7.0 - 2026-10-11
Renamed
Rename each in your config. The old name raises as unknown.
| Old | New | Why |
|---|---|---|
mantle.files, field folders | mantle.folders, field watched | It lists watched folders, and folders.folders read badly |
mantle.system | mantle.clock | It holds the wall and monotonic clocks, nothing else |
mantle.battery capacity | health | It is full charge over design capacity; capacity read as the charge level |
mantle.storage files | contents | The values are each file’s contents, keyed by path |
list source | items | It names the data; source is a file path on image and shader |
list itemfn | each | A plain word |
window on_close | on_close_request | The window stays open until the config hides it |
behind_blur | compositor_blur | The compositor does the blurring |
box outline (type Outline) | shape (type Shape) | outline read as CSS outline, which ring covers |
shader uniforms mantle_outline, mantle_outline_len, mantle_outline_point | mantle_shape, mantle_shape_len, mantle_shape_point | They carry the shape property’s points |
node submit | submits_secure | It sends the armed secure field; submit read like on_submit |
Changed, needs an edit
computed(dependencies, fn, before?)and a capability’s:map(fn, before?)never callfnwith a capability’s pre-pushnil. Until every capability dependency has pushed, the derived signal holdsbefore(defaultnil). Move whatfnreturned for thatnil({},0,"") intobeforeand drop the guard. Withoutbeforethe signal readsnil, which breaks#tor arithmetic downstream.:get()andon_changeare unchanged.persistent_table { path, name }becomespersistent_table { path }, wherepathis the file path, absolute or starting with~/.nameraises as an unknown key. A relative path, one ending in/, or one with an empty,.or..component raises. The engine creates the parent directory.mantle.clock’smonotonicisCLOCK_MONOTONICseconds since boot, as a fractional number. It used to be whole seconds since the capability started.mantle.mpris’sposition_updated_atuses the same clock in microseconds, so divide it by1e6to compare.- The internal
--detachedflag is gone and raises as an unknown argument.
Changed, no edit needed
- A leading
~/means$HOMEin every path a config passes:persistent_table { path },mantle.folders:watch/unwatch,mantle.storage:open/set,mantle.notifications:set_sound,palette.quantize, and theimagesource,iconname,shadersourceandimages,transition.shaderandimages,effect.shaderandmask.sourceproperties. persistent_tablekeys read their declareddefaultsentry untilmantle.storagepushes that file, instead ofnil.set(key, nil)and a key the file lacks also read the declared default. Saves keep a symlink’s link and the file’s mode.mantle.folders:watchon a missing folder, or on one deleted later, waits for it to appear and lists it then.errorholds until it does. It used to stay unwatched until a secondwatch.
Added
file.read(path, on_done)andfile.write(path, text, on_done?)read and write whole files off the Lua thread. A write that would change nothing is skipped and reportschanged = false. Writes land in call order, through a symlink to its target, keeping the file’s mode. Seefileand Colour your apps.process.capture(cmd, args, on_done)runs a program to its end and callson_done(stdout, code)with all of stdout as one string, newlines kept. It keeps the first 1 MiB and drops stderr. A signal, failed spawn or reload givesnil, nil.palette.from_hct{ hue, chroma, tone }andpalette.harmonize(color, source)return"#RRGGBB".mantle.clock:now()reads the monotonic clock at the call, before the first push too.computedaccepts plain strings, numbers, booleans and tables among its dependencies and passes them tofnunchanged. A function or userdata still raises.
0.6.2 - 2026-10-11
- Changed: a HUP, USR1, USR2, ALRM or other terminating signal stops the shell the way SIGTERM does. The error names the signal and the sender’s pid and command line. SIGINT and SIGTERM log their sender too.
- Changed: the renderer’s
connection endedline says whether the supervisor process is still running. - Fixed:
mantle setandtoggleread a joined--pid=Nor--profile=Nafter the state name as an option. It was stored as the value. - Fixed: a notification
image-pathorapp_icongiven as afile://URL with escapes (%20) or alocalhosthost resolves. It was refused. - Fixed: a tray item that leaves the bus while the tray starts is dropped. It could stay listed.
0.6.1 - 2026-10-11
- Fixed: a failed EGL surface, swap or dmabuf image call crashed the renderer. It logs
EGL error 0x…and carries on. - Fixed: the privacy capability could crash the supervisor on an empty camera or microphone device-event batch.
- Fixed: hot reload of a config file that could not be re-watched after a reload stopped silently. It logs a warning.
0.6.0 - 2026-10-10
- Breaking: config error text changed throughout. A shell that matches on
mantle.rescue.error_logtext must update. - Breaking:
package.searchersholds only the Lua file searcher. The C-module searchers could never load in safe mode. - Changed: a bad value, duplicate id or unknown property on a top-level
panel,window,popuporlocknames the line that built it, as errors on child nodes do:shell.lua:2: panel: invalid value for `height`: .... - Changed: an unknown property on any node reports the node, not a top-level return error or a bad
childrenvalue:shell.lua:2: text: no property `contnet`; did you mean `content`?. - Changed: a layout error from
mantle checkor a reload leads with the line that built the failing node:shell.lua:9: text: invalid value for `content`: .... Then come the path to its parent and the surface in parentheses. Achildrenentry readstext[2], counting from 1 like Lua, and a lonechildno longer shows a made-up[0]. - Changed:
mantle checkputs the pass on its own last line and no longer repeatslayout:after the pass name. - Changed: argument and value errors read like Lua’s. An error from a global function, capability action or
mantle checkleads with thefile:lineof the config call that caused it:mantle.audio:set_volume: expects 1 argument, got 2. A bad element inside a table argument is named. A method on an engine object names itself, asmethod reveal. A caughtpcallkeeps the caller’sfile:line, and a rejected property value of the wrong type names its type, asstring "tall"ornil. - Changed: every out-of-range value reads like
timer: ms must be within [1, 86400000], got integer 0, unit in the subject. A float readsnumber 2.5and a whole floatnumber 1.0. This coverstimer,interval,delay,pulse,loops, gradient stops andidle:register_threshold. - Changed: a config handler that raises logs where it was defined (
widgets/bar.lua:12) and folds identical repeats intoraised again (N times). - Changed:
error({...})names the value type. A missingrequirelists config-relative paths without the C-modules line. - Changed: a misspelt table key or choice value (
anchor = { lefft = true }) ends with “did you meanleft?”.
0.5.5 - 2026-10-10
- Added:
mantle.workspaces,mantle.windowsand keyboard layouts on Sway, over$SWAYSOCK. A workspace’sidis its name, and an Xwayland window reports its X11 class asapp_id. Sway ignoresset_maximizedandtoggle_special. - Added: MangoWC support for
mantle.workspaces,mantle.windowsand themantle.keyboardlayout name. Each tag is a workspace with id"<output>:<tag>". - Fixed:
mantle.idlereports a compositor hold taken while the seat is already idle, such as a video resumed from a phone.inhibitedstayed false until input came and went.
0.5.4 - 2026-10-09
- Added:
mantle.mprisplayers carryart_url, the cover URL as the player sent it, so a config can fetch remote artwork such as Spotify’s. - Fixed:
paddingon atextortextfieldinsets its glyphs, caret, links and IME cursor. They drew from the box’s corner, so padded labels sat off-centre.
0.5.3 - 2026-10-09
- Changed: an image file rewritten in place may take up to 0.5 s to show, because Mantle checks each path for edits at most twice a second.
- Fixed: the lock screen ignores a second password submitted during the unlock animation. It used to re-run PAM and flash an error.
0.5.2 - 2026-10-09
- Fixed:
mantle call,setandinputfail at once when no shell is connected. They used to wait 5 s. - Fixed: a
process.runchild printing faster than the shell reads no longer gets the shell restarted. The child waits on its pipe. - Fixed:
mantle.bluetoothdevice names are BlueZ’sAlias. A rename shows, and a nameless device shows its address-style alias instead of an empty name. - Fixed: a Bluetooth device’s
Classchange refreshes its category. - Fixed:
mantle.privacysees a camera plugged in after start or replugged. - Fixed:
mantle.appearancekeeps its last value when the portal call fails. - Fixed:
workspacesandwindowsrecover after niri restarts. They stayed frozen at the last value. - Fixed: a polkit prompt that asks a visible question (OTP, username) fails instead of receiving the password.
0.5.1 - 2026-10-07
- Fixed: an
effectunderanimatekeeps itsbackdrop.maskwhile it tweens. It was dropped from the first frame on.
0.5.0 - 2026-10-06
- Breaking: workspace ids are strings, and
WorkspaceEntry.idxis replaced bynumber. Treatid,active_workspace,focused_workspace,windows[].workspace_idand thefocusandmove_to_workspacearguments as opaque strings to pass back, never to compute with. On Hyprland a numbered workspace’s id is its number ("3"), sofocus(tostring(n))still creates one.numberis what a keybind targets (niri’s position on the output, Hyprland’s number), andnilfor a Hyprland named workspace. - Changed: the engine’s focus outline follows the node’s
radius,corner_smoothingandoutline. - Changed: a rounded
borderhas the box’s radius on its outer edge instead of half its width more. - Changed: a card press released over a field inside it no longer clicks the card.
- Added:
ring = { width, color, offset }on box nodes draws a strokeoffsetpx outside the box (inside when negative) with a clear gap, for Material-style focus rings. It followsradius,corner_smoothingandoutline, takes no layout space and tweens underanimate. - Added: double-click in a plain
textfieldselects a word and triple-click a line. Dragging or Shift+clicking afterwards extends by words or lines. - Added: a right or middle press on a plain
textfieldcalls the field’s ownon_pressand, on release, its ownon_click, never an ancestor’s. Use it for right-click menus. The press focuses the field and keeps a selection it lands in. Left presses andsecure_submitfields are unchanged. - Added: Ctrl+X cuts a field’s selection.
- Added:
focus_target(name)gains:cut(),:copy(),:paste()and:select_all().has_selection(name)is a read-only signal for a field with text selected. - Added:
textfieldtakesmultiline = true. The draft wraps and takes newlines, and the field grows frommin_linestomax_linesrows, then scrolls.submit_key("ctrl+return"or"return") picks the chord that submits.multilinewithsecure_submitis refused. See Multiline. - Added:
focus_targetandautofocuswork on any focusable control, not only a plaintextfield.:set_textstays textfield-only. - Added:
focus_target(name):request()works fromon_keyand from a field’son_changeandon_submit. A request from a key callback shows the focus outline, and one fromon_clickdoes not. - Added:
toplevel(id)takes:set_maximized(on),:set_minimized()and:set_fullscreen(on), which need no press. The result arrives throughstate(). - Added:
textfieldtakesselection = { background, foreground }, the highlight colour and the selected glyphs’ colour. The highlight is drawn as given, so tint it with alpha. Unset, the highlight staysforegroundat 30% alpha and the glyphs keep the text colour. - Added:
mantle.appearance, a read-only capability for the desktop portal’s appearance settings, pushed live:color_scheme,accent,contrastandreduced_motion. Without a portal every field holds its default. - Added:
effect.backdrop.masktakes a nodemask’sgradientorsource(andinvert) and scales the glass’s coverage, for a scroll edge or a feathered panel. Where the mask is clear the ground shows through, and between it crossfades from the ground to the blur. Anodemask is refused. - Added:
on_click,on_press,on_dragandon_wheeltake a trailingmodifiersargument, the{ ctrl, shift, alt, super }tableon_keyreports, for Ctrl-click, Shift-click and Shift-drag. It reads all false while none of the shell’s surfaces has keyboard focus. - Fixed:
on_drag’srectis the node’s current box on every call, not its box at the press.rect.x + pointer.xstays the surface position when the node moves under its own drag (a splitter). - Fixed: Hyprland named workspaces (
workspace = "name:chat") are listed inmantle.workspacesandmantle.windowsafter the numbered ones instead of dropped, andfocusreaches them (issue #1).
0.4.0 - 2026-10-05
- Breaking:
content_blurandbackdrop_blurare replaced by oneeffecttable,{ blur, backdrop = { blur } }, like CSSfilterandbackdrop-filter.content_blur = 4, backdrop_blur = 8becomeseffect = { blur = 4, backdrop = { blur = 8 } }, andanimate = { content_blur = 200 }becomesanimate = { effect = 200 }. Each blur is a sigma in[0, 8192]and bindable. Only box kinds takebackdrop. Unknown keys are refused with the accepted ones listed.animate.effecttweens both blurs, and a key only one side sets tweens from or to0. - Breaking:
textfield.on_navigateis removed. The field’s ownon_keyhears the same keys, and so do its ancestors’.on_navigate = function(k) if k == "down" then ... end endbecomeson_key = function(key) if key.name == "Down" then ... end end. Key names become keysym names:"up","down","left","right","page_up","page_down","tab"and"backtab"become"Up","Down","Left","Right","Page_Up","Page_Down","Tab"and"ISO_Left_Tab". Left and Right still arrive only when the caret cannot move and Shift is up. A key repeats only if anon_keyhandler hears it. - Breaking:
textfield.caret_coloris replaced bycaret = { color, width, height, radius }.caret_color = cbecomescaret = { color = c }. See textfield for each key’s default. - Changed:
shadowstakes up to 16 layers, up from 8. - Added: box kinds take
outline = { commands = { .. } }in place ofradius, any shape as one closed contour ofpathcommands: a popover with its arrow, a speech bubble, a tab joined to its panel. A coordinate is px,"NN%"of the box or{ from = "right", px = -12 }, so the shape follows the box’s size and may reach past it. Fill, border (one width and colour), shadows, inset shadows,clip = "rounded",mask,effect.backdrop,mantle_sdf, hit testing andbehind_blurfollow it.animate.outlinetweens point by point between lists of the same commands, springs included, and snaps otherwise. 0.7.0 renames itshape. - Added:
pathcommands take{ op = "corner", points = { x, y }, radius = r, corner_smoothing = s }, which rounds the turn at one point as a continuous corner. - Added:
effect.shadertakesprogress([-8192, 8192], default0), which becomesu_progressas on ashadernode.animate = { effect = .. }tweens it, keyframes andloops = "infinite"included, so a content or backdrop shader animates without Lua running per frame. The rest of theshadertable snaps to the target. - Added:
focus_visible(name), a read-only signal likefocused(name), bound as a node’sfocus_visible. It istruewhile the node or one inside it holds focus that Tab, Shift+Tab or an assistive-technology action moved there (CSS:focus-visible). A press,autofocusorfocus_target(name):request()makes itfalse.focus_ring = falsedoes not affect it, so a custom ring bound to it stays hidden after a mouse click. - Added:
window.geometry_insetadds a band around a client-decorated window’s frame for its shadow and rounded corners. The compositor sizes, tiles and snaps by the frame alone. Bind it totoplevel(id):state()to drop the band when maximized, fullscreen or tiled. See Client-side decoration. - Added:
shader,effect.shaderandtransition.shadertakeimages = { name = "/abs/path.png" }, up to 8.png,.jpg,.jpegor.webpfiles for the.fragto sample: a normal map, a noise texture or a LUT. Each entry declaresuniform sampler2D nameanduniform vec2 name_size. Changing a file reloads it. See Images for naming rules and sampling. - Added:
toplevel(id):state(), a read-only signal of awindow’s configure state:activated,maximized,fullscreen,resizing,tiled,bounds,capabilitiesanddecoration. It returns to its defaults when the window closes. See Window state. - Added:
window.decorationspicks who draws the frame,"server"(the default, as before) or"client". A reload that changes it asks the compositor again. - Added:
blendsets a CSSmix-blend-mode("multiply","screen","plus_lighter"and more; default"normal") on a node, abackgroundlayer ({ fill = .., blend = .. }) or ashadowslayer. It composites onto what the surface painted under it, never the desktop. A node’s blend applies after everyeffectfilter and takes its shadows with it.blendsnaps underanimate. See Blend modes. - Added:
effect.shadertakesinput = "backdrop"on box kinds. The program reads what the surface painted under the box throughmantle_input, and theeffect.backdropcopy throughmantle_input_blurred. Its output replaces that area before the node paints, mixed with the original by the node’sopacity. Returnmantle_input(uv)to leave a pixel unchanged. See Backdrop input. - Added: a
shadowslayer withinset = trueworks like CSSbox-shadow: inset. It shades the inside of the padding box with the samecolor,blur,offsetandspread, above the background and under the children and border. It needsshadow_mode = "box"and a box kind. See Inset shadows. - Added:
textfield.escapepicks what Escape does:"clear"(the default, as before),"blur"or"pass"."blur"keeps the draft, leaves the field and callson_cancel(false)."pass"keeps the draft and focus, and Escape goes up throughon_key, thenon_escape. Asecure_submitfield ignores it. - Added:
on_key(key)on any node or surface hears key presses and repeats, for custom controls.keyis{ name, text, modifiers = { ctrl, shift, alt, super }, repeat }, withnamean xkb keysym name ("Down","Return","a"). A key goes to the focused node, each ancestor, then the surface, and a handler returningtruestops it. A focusedtextfieldkeeps the keys it edits with and passes the rest up. A named node withon_keytakes keyboard focus. See key handlers. - Added:
pointer(name), a read-only signal of the pointer’s{ x, y }in logical pixels from a node’s top-left corner while the pointer is over the node or its children,nilotherwise. Bind it as a node’spointer. It costs nothing while unread. - Added:
mantle [-c DIR] input <surface> move | press | release | leave | click | drag | wheel | key | typeinjects pointer and keyboard input into one surface of a running config, for testing. Lock surfaces andsecure_submitfields refuse it. See Injecting input. - Added:
backgroundtakes a list of up to 16 layers, first on top, like CSS multiple backgrounds. Each is a colour, a gradient or{ fill = .. }.animate.backgroundtweens colour layers pairwise and fades in one a side lacks. Gradient layers snap. - Added:
textfieldtakesfont,font_weight,italic,letter_spacing,line_heightandfont_variations, withtext’s ranges and defaults. The caret, the selection and IME placement follow the metrics. An unsetheightisfont_size * line_height. - Added:
wrap = trueon aroworcolumnflows children onto new lines, like CSSflex-wrap.line_spacingsets the gap between lines.wrapwithscrollis refused. See Wrapping. - Added:
homogeneous = trueon aroworcolumngives every visible child one equal slot along the main axis, like GTK’shomogeneous. See Equal slots. - Added:
toplevel(id)returns a handle for awindow’s own frame::move(),:resize(edge)and:show_menu(). They work only from anon_pressor anon_drag"start". Anywhere else they warn and send nothing. See Custom title bar. - Added:
on_press(rect, button, pointer)fires on any node at press time for the left, right and middle buttons, beforeon_click. A press on atextfielddoes not call it. - Added:
border_colortakes a gradient table,{ gradient = "linear" | "radial" | "conic", angle, stops }asbackgrounddoes, painted along the whole outline. A gradient per edge is refused, and a gradient border snaps underanimate. - Added:
effect.shader = { source, input, params, padding }runs a fragment shader over a node’s painted subtree, which ashadernode cannot read. The.fragreads the subtree throughmantle_input(uv)and the distance to the node’s outline throughmantle_sdf(p).padding([0, 512]px) lets it read and draw past the box. Saving the file recompiles it, and one that fails to build leaves the node as painted. See Shader effects. - Added:
corner_smoothing,0to1, gives box kinds andimagecontinuous corners like Figma’s corner smoothing.radius = 24, corner_smoothing = 0.6is close to iOS, and0is the circular arc as before.animatetweens it. See Continuous corners. - Added:
effecttakes the CSS colour filterssaturate,brightnessandcontrast, each in[0, 8]with1off, on the node and inbackdrop(effect = { backdrop = { blur = 45, saturate = 2 } }). They apply after the blur, are bindable, andanimate.effecttweens them. - Fixed:
textandtextfieldglyphs that run past a tightline_height(descenders below the line box) are no longer cut flat at it.line_heightsets layout only, and an ancestor’sclipstill cuts the ink.
0.3.0 - 2026-10-05
- Breaking:
clipdefaults to"none", like CSSoverflow: visible. Children, their shadows and their transforms paint and take the pointer past a box that does not set it. A scrollingrow,columnorlist, a surface and a box with amaskstill cut to the box. Addclip = "box"where a box must cut, such as a button holding a ripple or a fixed-widthrectholding a longtext. - Breaking:
shadow_color,shadow_blur,shadow_offsetandshadow_spreadare replaced byshadows, a list of{ color, blur, offset, spread }layers like CSSbox-shadow: a, b. Wrap the old four in one layer:shadow_blur = 8, shadow_offset = { y = 2 }becomesshadows = { { blur = 8, offset = { y = 2 } } }, andanimate = { shadow_blur = 200 }becomesanimate = { shadows = 200 }. The first layer is on top, up to 8. Each field is bindable, andanimate.shadowstweens layer by layer. - Changed:
palette.quantizeuses Material’s Celebi quantizer instead of median cut, so the same image gives different swatches (colours andshares).opts.method = "wu"picks Wu alone. - Changed: a parent’s
clipstays put when a child moves withtranslate,scaleorrotate. A child that transforms past a clipping parent’s box is cut at the box. - Changed: an invalid property value (out of range, wrong type, bad colour, malformed table) no longer freezes its surface at startup or in a live update. The node uses that property’s default and the rest applies. The error is logged and held in
mantle.rescueuntil the value is fixed. A reload andmantle checkstill refuse it. - Changed: a node that fails a live update or the startup apply (a raising getter, a malformed
children) freezes only its own surface, which keeps its last applied tree. The other surfaces keep updating, an error banner drawn frommantle.rescueincluded. The failure is logged as an error, once until its message changes. A reload that fails anywhere is still refused whole. - Changed:
on_clickandon_dragmeasurepointerin the node’s own untransformed box, so a translated, scaled or rotated node reports coordinates within its size. - Changed: each
textfieldkeeps its own draft. Moving focus to another field, with Tab or a click, no longer discards the text of the one you left. A draft goes when its node leaves the tree. - Changed:
textfield.caret_colorcolours the caret, not the selection highlight. It defaults toforeground, as before. - Changed: a
textfieldwithoutheightis one line offont_sizetall instead of 0 px, so its placeholder shows.widthis still required. - Changed: a stacking parent whose size is fixed or capped below its largest child gives every child its content box, so a smaller child no longer centres in the larger one’s width.
- Changed: a
textoriconcapped bymax_widthormax_heightsizes its content-sized parent at the cap instead of its full content, so an elided title in a content-sizedrowstays centred. - Changed:
battery.capacityreports battery health: full charge as a percent of the design capacity,nilwhen UPower does not know it. - Changed: the wheel stops at the end of the content the last layout measured, so a
mapof ascrollsignal runs once per wheel event at an end instead of twice. - Changed: an
iconwithforegroundrecolours every shape of a-symbolicicon that never usescurrentColor, as GTK does, so themes that hard-code symbolic colours (Tela) follow it. Icons that usecurrentColorkeep their accents, and full-colour icons are unchanged. - Changed: images scale down with a bilinear filter instead of box sampling. The result is closer to the source, and a 4K wallpaper scales to 2133x1200 in 8.5 ms instead of 75 ms.
- Added:
path.trim_axis = "x"makestrim_startandtrim_endfractions of the node’s width. The stroke keeps what lies inside that band, cut at its edges withstroke_capon every cut end. - Added:
path.shiftmoves the geometry inside the node before trimming. Animate it to scroll a shape through a fixed window. - Added: a
keyframesentry, or one frame, takesspring = { stiffness, damping }in place ofeasing. The segment springs from rest in real time and lands on its frame when itsdurationends. - Added:
textfield.initial_text(string or signal) seeds a plain field’s draft once, when the field enters the tree, with the caret at the end. Later changes are ignored, and a changedidorkeyis a new field.autofocusresets the draft toinitial_text(""when unset) and callson_changewith it. It is refused withsecure_submit. See textfield. - Added:
scroll(name):scroll_to(offset)and:scroll_by(delta)move a scroll offset from Lua, clamped to the content and eased underanimate.scroll.scroll_byadds to where a run is headed, so repeated arrow clicks add up. A request waits while its container is hidden, and the next pass drops one when no container holds the signal. - Added:
animate.scrollon a scrollingrow,columnorlisteases each mouse-wheel notch toward a target that stops at the ends. Thescrollsignal holds the offset on screen each frame, so its readers reflow continuously.:revealeases the same way. Touchpads, high-resolution wheels andreset_on_closestill move it at once. - Added: a signal may sit inside a property table, such as
margin = { top = sig }, a gradient stop’s colour, ananimateentry or a text run’s flag, and is read again once it is written.children, alist’ssource, structural fields such as a panel’sanchor, tables with a metatable and tables a signal returns still refuse one. - Added:
textfield.disabledkeeps a field drawn but out of keyboard focus (Tab, press,autofocus,focus_target). - Added:
textfield.max_lengthcaps the draft in grapheme clusters for typing, paste, IME andset_text,secure_submitfields included. - Added:
image.radiusrounds the picture’s corners with no clipping wrapper, as a number or a per-corner table likerect.radius. It rounds what shows, sofit = "contain"rounds the fitted picture. - Added:
pathtakesstroke_cap("butt","round","square"),stroke_join("miter","round","bevel"), andtrim_start/trim_end, which stroke only that fraction of the path’s length and animate like any number. - Added:
focus_target(name):set_text(text)prefills or clears the draft of every plain textfield with thatfocus_target, hidden and disabled ones too. It calls noon_changeand never touches asecure_submitfield. - Added:
textfield.placeholder_colorcolours the placeholder. It defaults toforeground, as before. - Added:
texttakesfont_variations, a table of OpenType axis tags to values such as{ FILL = 1, GRAD = 0, opsz = 24 }, for variable fonts like Material Symbols. An explicitwghtoverridesfont_weight. - Added:
on_escape()onpanel,windowandpopupfires on Escape when the surface has the keyboard and no focused field took it, so a dialog or menu without a field can close. A field with text oron_cancelkeeps Escape. - Added:
palette.hct(color)returns a colour’s{ hue, chroma, tone }in HCT, so a config can apply its own seed rule. Eachpalette.scoreseed is one input swatch’s colour. - Added:
palette.scheme(seed, opts?)returns the 49 Material 3 colour roles for a seed, withdark,variantandcontrastoptions. - Added:
palette.score(swatches)rankspalette.quantizeswatches into seed colours as Material 3 does. - Added:
animate.widthandanimate.heightease a content-sized axis. When a label changes, the box grows from the size on screen to the new content size, and siblings follow. Only eased entries animate it. Springs and keyframes snap. - Added:
radiustakes per-corner values,{ top_left, top_right, bottom_right, bottom_left }, whichanimatetweens. Corners too big for a side shrink together, as in CSS. A shadow rounds by the mean corner. - Added:
hittable = falselets the pointer through a node and its descendants to what is underneath. A descendant’shittable = truetakes it back. - Added:
on_clicktakes the pointer as a third argument,{ x, y }in node-local coordinates likeon_drag’s. A keyboard activation passes the node’s centre. - Fixed: a
mapof ascrollsignal kept a stale offset when layout clamped it (a wheel past either end) or a:revealmoved it, so offset-sized children jumped at the ends. It lays out again in the same frame. - Fixed: a
textwhosecontentruns table orfont_variationstable was edited in place and set again kept its old size and glyphs. It re-measures. - Fixed:
audiorecovers when PipeWire restarts or starts after Mantle. While PipeWire is down,audioreads as a machine with no audio hardware andprivacyclears its microphone and screencast users. - Fixed:
networkrebinds its devices when NetworkManager restarts, sowifi_devicesand wired state drop the old daemon’s values. - Fixed: the Bluetooth pairing agent rejects calls from anyone but bluetoothd.
- Fixed:
brightnesspicks up a backlight that appears after startup, such as on a dock or a hybrid GPU.
0.2.0 - 2026-10-04
- Breaking: an omitted
panelwidth or height fills the configured root axis when both opposite edges are anchored. To keep a deliberately narrow root, wrap its content in a sizedchildand move its background there. - Breaking: every engine-defined string enum is lowercase snake_case, and old spellings are refused:
align_h = "center",width = "fill",layer = "top",keyboard_interactivity = "on_demand",easing = "in_out_quad",loops = "infinite",constraint_adjustment = { "flip_y", "slide_x" },exclusive_zone = "ignore", gradients"linear"/"radial"/"conic". Capability values follow the same rule:battery.state("fully_charged","pending_charge", …), mprisplay_state("playing"),loop_statusandset_loop_status("none","track","playlist"). POSIX signal names and SVG path ops keep their case. - Breaking: panel
outputkeywords are"all"and"active"."All"names a connector and logs that none is connected. - Breaking:
tray.items[].statusis"active","passive"or"needs_attention". An item that sends another value, or none, reads"active". - Breaking:
panel’smonitoris renamedoutputandexclusiveis renamedexclusive_zone, with the same values. - Breaking:
marginon awindow,popuporlockroot, andalign_h/align_von any surface root, are refused as unknown properties instead of ignored. Set them on the child.animateon alockrefuseswidth,heightandvisible, which a lock already refuses. - Breaking: node property
blurbecomesbehind_blur.focus(name)becomesfocus_target(name), and the textfield propertyfocusbecomesfocus_target. - Breaking: every volume is a percent.
audio.volume(0 to 150),audio.source_volume,apps[].volumeand mprisplayers[].volumeread100for 100%, andset_volume,set_source_volume,set_app_volumeandmantle.mpris:set_volumetake the same scale. Multiply old fractions by 100. - Breaking:
mantle.mpris:set_loop_statustakes"none"|"track"|"playlist"and raises on anything else.players[].loop_statusisnilinstead of""when unreported, andplay_statereads"stopped"instead of""before its first reading. - Breaking:
mantle.tray:scrollraises on an orientation other than"vertical"or"horizontal". - Breaking: unknown values are
nilinstead of-1:sysinfo.temp_gpu, bluetooth connected-devicebattery, and mprisposition,lengthand track-listlength.network.ethernet_speedisnilinstead of0when unknown or no wired device is active. - Breaking:
keyboard.backlight_pctis renamedbacklight_percent,nilwithout a readable backlight.workspaces.active_clientfieldsclass,is_floatingandis_fullscreenare renamedapp_id,floatingandfullscreen, matchingwindowsentries. - Breaking:
mantle.updates.countand the bluetoothDiscoveredDevice.pairedfield are removed. Use#updates.packages. A discovered device is never paired. - Breaking: list properties such as popup
constraint_adjustmentand textcontentruns reject a list with anilhole or named keys. Entries after a hole used to be dropped. - Breaking:
dofileandloadfileare unavailable in configs, because their synchronous file reads can stall the renderer. Userequirefor Lua modules orprocess.runfor other files. - Breaking: with two or more focusable controls on a surface, Tab moves focus and no longer reaches a textfield’s
on_navigate("tab"). - Breaking: a
pathsubpath is solid regardless of winding, so an inner subpath drawn in the opposite direction no longer cuts a hole. Mark it withhole = trueon its first command. - Changed:
mantle.power:set_profilewarns and sends nothing for a name not inprofiles. - Changed: surfaces paint at their output’s compositor scale, fractional included, so text and images are sharp on HiDPI displays. Sizes stay in logical pixels. A
shader’ssizeuniform stays logical whilegl_FragCoordcounts buffer pixels. - Changed: a resized
imagewithasynckeeps drawing its previous size, scaled, until the new size decodes, instead of blanking. A changedsource_blur, or a changedsourcewithoutretain, still blanks. - Changed: stubs give
panel,window,popupandlockaSurfacereturn type and node constructors aNodeone, so a surface used as a child is a type error. - Added:
network.vpnslists saved NetworkManager VPN and WireGuard profiles withactiveandactivating.connect_vpn(uuid)anddisconnect_vpn(uuid)toggle one, andvpn_errorreports a failed activation. - Added: a NetworkManager secret agent raises
vpn_secretwhen an activation needs secrets. Answer it withsecure_submit = { capability = "network", action = "vpn_secret", name = request.id .. "/" .. field }fields, orcancel_vpn_secret(). - Added:
workspacesandwindowsentries haveurgent. niri reports it as set. Hyprland sets it on anurgentevent and clears it when the window gains focus or closes. It is alwaysfalseon wlr-foreign-toplevel. - Added: a
radiocapability.mantle.radio.radioslists each rfkill kind withsoft_blockedandhard_blocked, andset_blocked(kind, blocked)andset_all_blocked(blocked)write/dev/rfkill. - Added: Bluetooth
pairing_requesthas anidand supports"pin_entry"and"passkey_entry". Answer these withsecure_submit = { capability = "bluetooth", action = "pair", name = request.id .. "/" .. request.mac }. - Added:
network.wifi_deviceslists Wi-Fi interfaces by name.scan_device,connect_deviceanddisconnect_wifi_devicetarget one. Flat join fields describe that join, while other flat Wi-Fi fields and the old actions use the primary interface. - Added: plain
textfieldsupports Ctrl+Z undo and Ctrl+Shift+Z and Ctrl+Y redo, withon_changefor restored drafts. - Added: plain
textfieldcomposes through text-input-v3 where available. Preedit stays local until commit. Secure fields do not use an input method. - Added:
animate.moveeases a matched node from its previous painted position to its new layout position, without moving siblings or running Lua each frame. Give items that can change order a stableidor listkey. - Added:
audio.sinks[]andaudio.sources[]report per-channelindex,positionand percentvolume.set_sink_channel_volumeandset_source_channel_volumeset one channel without changing the others. - Added:
battery.peripheralslists UPower devices outside the system supply, with charge percentage or coarse level when reported. - Added:
json.encode(value)returns compact JSON with sorted keys. It raises on functions, userdata, cycles, NaN, infinity, nesting past 127, over 2^20 values, arrays more than half holes and a decoded array given a named key. - Added:
interval(ms, callback), a repeating timer withtimer’s range,canceland per-evaluation lifetime. - Added:
process.runandprocess.detachacceptnilforargs. - Added:
accessible_namemakes clickable nodes keyboard focusable and names them for screen readers. Tab and Shift+Tab traverse controls, and Enter and Space activate them. Mantle exposes the resolved scene through AT-SPI, with secure field values withheld. - Added: the engine outlines a control only when Tab or an assistive-technology action focused it.
focus_ring = falseturns the outline off.focused(name), bound as a node’sfocused, reports focus within a node for custom styles. - Added:
pathcommandstween point by point underanimatebetween lists with the same ops andholeflags,keyframesincluded, so shapes morph without Lua per frame. Any other change still snaps. - Added:
pathAdraws a circular arc from centre, radius, start and sweep in degrees.hole = trueon a subpath’s first command cuts it out of the fill. - Added: a list of up to 4096 numbers in
shaderandtransitionparamsfills afloatorvec2-vec4uniform array, enough for a 256-bar visualizer. - Added:
idleon_resumereceives"input","activity"or"inhibitor", so a config can wake displays only for input while stopping idle stages for other resumes. - Fixed: input regions, text links and pointer cursors follow ancestor transforms. Canvas
Frame.transformcomposes nested transforms. - Fixed: a per-edge
border_widthorborder_coloron a rounded box, and any border on acorner_shape = "scoop"box, drew as four straight rectangles with square corners. Borders follow the corners, and where two edges meet the colour change sits on the corner in proportion to their widths, as in CSS. - Fixed: Bluetooth switches to a present adapter when the tracked adapter is removed.
0.1.0 - 2026-10-01
First release. Mantle is an engine for Wayland desktop shells declared in Lua. It ships no shell of its own beyond the one-clock starter bar.
- Added: surfaces: panels, popups, windows and lock screens.
- Added: nodes: rects, rows, columns, text, text fields, images, icons, lists, paths, shaders and capture.
- Added: capabilities: 24 desktop services, among them audio, network, Bluetooth, notifications, tray, MPRIS, workspaces, idle, updates and secrets.
- Added: signals update widgets when values change. Saving the config reloads the shell.
- Added:
mantle checkcatches Lua and layout errors before a run.mantle initsets up LuaLS completion and type checking. - Added: packages for Ubuntu 26.04+ (
.deb) and Fedora 44+ (.rpm), and an x86_64 tarball (installation).