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

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 lock does not lock; mantle.lock:lock() does, and only a correct password in the one secure field unlocks (lock surface, lock capability).
  • secure_submit sends keystrokes straight to PAM; Lua never sees the password, so the field has no on_change or on_submit (secure fields).
  • child = function(output) gives every monitor its own copy (per-output child).
  • set_unlock_animation keeps the lock up for FADE_MS after success, while unlocking fades the card out (animation).
  • PAM starts when the session locks. The hint shows its latest messages line (a faillock countdown, say) or else its prompt text, and error, attempts and authenticating drive the rest of the hint and the red border.
  • masked follows prompt.echo, so a two-factor code shows as typed while a password stays masked. Either way it never reaches Lua (lock capability).
  • action exposes mantle call lock to a keybind, and an idle threshold locks after 300 s without input (action, idle).

Variations

ChangeEdit
Wallpaper behind itWrap 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 wallpaperAdd source_blur = 24 to that image (blurs)
Unlock buttonA clickable node with submits_secure = true beside the field sends it like Enter (pointer)
Fingerprint hintIn 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 onlyvisible = output == "DP-1" on the clock texts
Lock before suspendAn 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