Lock screen
A lock screen on every monitor with a large clock, the date and a password card that reports
checking and wrong passwords. It fades in when the session locks and out after a correct password.
It locks from a keybind (mantle call lock) and after five minutes idle.
local FADE_MS = 250
mantle.lock:set_unlock_animation(FADE_MS)
mantle.idle:register_threshold(300, function() mantle.lock:lock() end, function() end)
action("lock", function() mantle.lock:lock() end)
-- Up while locked and not yet unlocking: drives the fade both ways.
local up = mantle.lock:map(function(lock) return lock.active and not lock.unlocking end, false)
local time = mantle.clock:map(function(clock) return os.date("%H:%M", clock.time) end, "")
local date = mantle.clock:map(function(clock) return os.date("%A, %d %B", clock.time) end, "")
local hint = mantle.lock:map(function(lock)
local last = lock.messages[#lock.messages]
if lock.authenticating then
return "Checking…"
elseif last then
return last.text
elseif lock.error ~= "" then
return lock.attempts > 1 and string.format("Wrong password (%d tries)", lock.attempts) or "Wrong password"
end
return lock.prompt and lock.prompt.text or "Type your password and press Enter"
end, "")
local failed = mantle.lock:map(function(lock) return lock.error ~= "" and not lock.authenticating end, false)
local lock_screen = lock {
id = "lock",
background = "#11111b",
child = function(output)
return column {
width = "fill",
height = "fill",
align_h = "center",
align_v = "center",
spacing = 16,
background = { gradient = "linear", angle = 160, stops = { { 0, "#1e1e2e" }, { 1, "#11111b" } } },
opacity = up:map(function(on) return on and 1 or 0 end),
animate = { opacity = { duration = FADE_MS, from = 0 } },
children = {
text { content = time, font_size = 96, foreground = "#cdd6f4", align_h = "center" },
text { content = date, font_size = 20, foreground = "#a6adc8", align_h = "center" },
rect { height = 32 },
text { content = os.getenv("USER") or "", font_size = 16, foreground = "#cdd6f4", align_h = "center" },
rect {
width = 320,
align_h = "center",
padding = { left = 16, right = 16 },
radius = 22,
background = "#1e1e2e",
border_width = 2,
border_color = failed:map(function(bad) return bad and "#f38ba8" or "#45475a" end),
animate = { border_color = 150 },
children = {
textfield {
width = "fill",
height = 44,
font_size = 16,
foreground = "#cdd6f4",
text_align = "center",
placeholder = "Password",
masked = mantle.lock:map(function(lock) return not (lock.prompt and lock.prompt.echo) end, true),
secure_submit = { capability = "lock", action = "authenticate" },
},
},
},
text {
content = hint,
foreground = failed:map(function(bad) return bad and "#f38ba8" or "#6c7086" end),
align_h = "center",
},
},
}
end,
}
return { lock_screen }
How it works
- Declaring a
lockdoes not lock;mantle.lock:lock()does, and only a correct password in the one secure field unlocks (lock surface, lock capability). secure_submitsends keystrokes straight to PAM; Lua never sees the password, so the field has noon_changeoron_submit(secure fields).child = function(output)gives every monitor its own copy (per-output child).set_unlock_animationkeeps the lock up forFADE_MSafter success, whileunlockingfades the card out (animation).- PAM starts when the session locks. The hint shows its latest
messagesline (a faillock countdown, say) or else itsprompttext, anderror,attemptsandauthenticatingdrive the rest of the hint and the red border. maskedfollowsprompt.echo, so a two-factor code shows as typed while a password stays masked. Either way it never reaches Lua (lock capability).actionexposesmantle call lockto a keybind, and an idle threshold locks after 300 s without input (action, idle).
Variations
| Change | Edit |
|---|---|
| Wallpaper behind it | Wrap the column in a rect { width = "fill", height = "fill" } whose first child is image { source = "/path/to/wallpaper.jpg", width = "fill", height = "fill" }, and drop the gradient (image) |
| Blurred wallpaper | Add source_blur = 24 to that image (blurs) |
| Unlock button | A clickable node with submits_secure = true beside the field sends it like Enter (pointer) |
| Fingerprint hint | In hint, right after the lock.messages branch, return the last of lock.fingerprint.messages when lock.fingerprint is not nil, so a wrong password’s error does not hide it. A match unlocks like a password (lock capability) |
| Clock on one monitor only | visible = output == "DP-1" on the clock texts |
| Lock before suspend | An action that sets a suspend_pending state and calls mantle.lock:lock(); a mantle.lock:on_change that sees active turn true with it set clears it and runs systemctl suspend through process.detach. Suspending straight away can sleep before the lock draws |