lock
The session lock screen: one ext_session_lock_surface_v1 per connected output, covering it for as
long as the compositor holds the session locked. Declaring a lock does not lock;
mantle.lock:lock() does, and only a correct password typed into its secure field unlocks.
The lock’s state (active, authenticating, attempts, error, unlocking) and actions are on
the lock capability. Rules every role shares are in surfaces.
mantle.lock:set_unlock_animation(250)
local up = mantle.lock:map(function(lock) return lock ~= nil and lock.active and not lock.unlocking end)
local hint = mantle.lock:map(function(lock)
if lock == nil then return "" end
if lock.error ~= "" then return string.format("%s (%d)", lock.error, lock.attempts) end
if lock.authenticating then return "Checking…" end
return lock.active and "Enter your password" or "Locking…"
end)
local lock_screen = lock {
id = "lock",
background = "#11111b",
child = function(output)
return column {
width = "Fill", height = "Fill", align_v = "Center", spacing = 12,
opacity = up:map(function(on) return on and 1 or 0 end),
animate = { opacity = { duration = 200, from = 0 } },
children = {
text { content = output, foreground = "#6c7086", align_h = "Center" },
rect {
width = 320, align_h = "Center", padding = 8, radius = 18, background = "#1e1e2e",
children = {
textfield {
width = "Fill",
height = 20,
placeholder = "Password",
mask_character = "•",
secure_submit = { capability = "lock", action = "authenticate" },
},
},
},
text { content = hint, foreground = "#a6adc8", align_h = "Center" },
},
}
end,
}
action("lock", function() mantle.lock:lock() end)
return { lock_screen }
mantle call lock from a keybind (action) locks the session. Each
output gets its own card; the field is typable as soon as the compositor gives the lock the
keyboard, with no click. After a correct password the lock stays up 250 ms while the card fades
out, then the session unlocks.
Properties
A lock takes id, child and the common and box node properties, minus
the ones the protocol owns.
| Property | Type | Default | Behaviour |
|---|---|---|---|
id | string | Required | The surface’s identity across reloads, unique among surfaces. A panel’s or lock’s per-output instances are "{id}@{output}"; monitor = "Active" keeps the bare id |
child | Node|fun(output: string): Node?|Bound | None | The root’s content. A function runs per output instance with its connector name; nil leaves that instance empty (per-output child) |
width | nil | None | Refused: the lock covers each output |
height | nil | None | Refused, as width |
visible | nil | None | Refused: the session lock decides when it shows |
monitor and anchor are refused too: the protocol owns coverage and lifetime. The root is the
output’s size; give children "Fill" to cover it. A reload that renames id while the session is
locked is refused with a warning in mantle log. A config declares at most one lock; a second
is refused at evaluation.
When a lock is refused
The compositor keeps the session locked if the shell dies, so a lock screen with no way to type a
password leaves only a VT switch. mantle.lock:lock() checks the lock screen before it
asks the compositor. A refusal leaves the session unlocked and puts the reason in mantle.lock’s
error and in mantle.rescue.
| Condition | Result |
|---|---|
The config declares no lock | Refused |
No lock instance holds exactly one shown secure field, with secure_submit = { capability = "lock", action = "authenticate" } | Refused. A hidden field does not count; a second shown secure field of any target refuses too |
| Another client already holds the session lock | The compositor denies it; reported in error and mantle.rescue |
| While locked, a reload leaves no lock instance with that single field | The reload is refused and the lock screen on screen stays |
| The compositor ends a held lock by its own mechanism | The session is unlocked; the reason goes to mantle.rescue only |
The field’s keystrokes go to PAM and never reach Lua.
How do I…
| Task | Answer |
|---|---|
| Lock from a keybind | action("lock", ...) as in the example, then mantle call lock |
| Show “wrong password” | Read error and attempts from mantle.lock, as the example’s hint does |
| Show that PAM is checking | Read authenticating |
| Animate the lock screen out | set_unlock_animation with the animation’s length, and drive opacity or scale from unlocking (lock capability) |
| Animate it in | Drive the same property from active; animate.from covers the first frame |
| Show the desktop wallpaper behind it | An image in the per-output child, keyed by output (per-output content) |
| Put a clock on it | mantle.system:map(function(system) return system and os.date("%H:%M", system.time) or "" end) |
| Add an unlock button beside the field | A button { submit = true } sends the field like Enter (pointer) |
| Lock before suspend | Invoke lock from your idle or suspend handler (idle) |
Gotchas
| Trap | Fix |
|---|---|
mantle call lock does nothing | Read mantle.lock’s error: the config declares no lock, or its tree lacks exactly one shown secure field |
| Two secure fields in one lock tree make the lock refuse | One shown secure field per lock tree; hide the others |
| A reload while locked is ignored | It removed the lock’s password field; the running lock screen stays. Fix the file |
Renaming the lock’s id while locked is refused | Save the rename again after unlocking |
visible, width, height, monitor or anchor on a lock is refused | Remove them; the lock always covers every output |
| The card’s exit animation is cut off | The session unlocks when the set_unlock_animation time ends; make it at least the animation’s length |
See also: lock capability, secure fields, surfaces, animation.
Source: lock spec, at most one lock, session lock, authenticate check, rename while locked.