Lock Screen
Amane can lock your session with a lock screen you design yourself, and check the password through PAM, like your login screen does.
Your compositor must support the ext-session-lock protocol. niri, Hyprland, and Sway do.
A lock screen
use amane::{
App, Center, Color, Column, LayerWindow, Lock, Monitor, Parent, Rectangle, Service, Text,
TextInput, children,
};
fn main() {
App::new().lock(view).run();
}
fn view(_: &Monitor) -> LayerWindow {
let lock = Lock::read();
let status = if lock.checking() {
"checking..."
} else if lock.failed() {
"wrong password"
} else {
"locked"
};
// the compositor sizes lock screens to the monitor, so this size is never used
LayerWindow::new().width(1.0).height(1.0).child(
Rectangle::new()
.width(Parent)
.height(Parent)
.fill("#1e1e2e")
.align_child(Center, Center)
.child(
Column::new(children![
Text::new(status).size(24.0).color(Color::WHITE),
Rectangle::new()
.width(300.0)
.height(40.0)
.fill(Color::WHITE)
.padding(8.0)
.child(
TextInput::new("password")
.placeholder("password")
.password()
.focused()
.on_submit(|password| Lock::unlock(&password)),
),
])
.gap(12.0),
),
)
}
This sets up the lock screen, but doesn’t lock anything yet. Locking the session shows how.
How locking works
App::locktakes a view that’s shown on every monitor while the session is locked. Likewindow_per_monitor, it gets the&Monitorit’s on.- While the session is locked, the compositor shows only the lock screen, sends it all keys, and keeps every other window hidden. The lock screen’s size, anchors, and layer are ignored. It always covers the whole monitor.
Lock::unlock(password)checks the password through PAM (with theloginservice, as your user). That can take a few seconds, so it runs on its own thread. If the password is right, the session unlocks. If not, the screen stays locked.- The
LockService tells the view what’s happening, throughchecking()andfailed().
Locking the session
Lock::start() locks the session. Call it from anywhere: a button, an IPC handler, or a Service’s thread. Calling it while already locked does nothing.
The usual setup is an IPC handler, so a keybind or an idle daemon can lock:
fn main() {
App::new()
.ipc("lock", |_| {
Lock::start();
String::from("locked")
})
.window(bar)
.lock(lock_screen)
.run();
}
Then lock from a keybind, or from swayidle or hypridle:
amane ipc call lock
For niri:
binds {
Mod+Alt+L { spawn "amane" "ipc" "call" "lock"; }
}
Lock reference
| Function | Meaning |
|---|---|
Lock::read().checking() | a password is being checked right now |
Lock::read().failed() | the last password was wrong |
Lock::start() | locks the session |
Lock::unlock(password) | checks a password, and unlocks if it’s right |
Only one password is checked at a time. Calling unlock while a check is running does nothing. Each new lock starts clean, without the last lock’s failed state.
Testing safely
A locked session only opens with the right password. If your lock screen has a bug, like an input that doesn’t take keys, you can’t get back in from that session.
Killing the shell doesn’t help either. If the program that locked the session exits without unlocking, the compositor keeps the session locked. That’s on purpose: otherwise, crashing the lock screen would be a way past it.
Before using a new lock screen for real:
- Save your work in other apps.
- Switch to another TTY (
Ctrl+Alt+F3), log in there, and keep it open. If you get stuck, switch to it and end the locked session withloginctl terminate-session <id>(loginctl list-sessionsshows the id). This closes every app in that session. - Try a wrong password first, then the right one.
A blurred wallpaper background
Rectangle::new()
.width(Parent)
.height(Parent)
.fill(Image::cover("/home/you/Pictures/wall.jpg").thumbnail(64, 36).blurred(4))
See Images for why the small thumbnail makes this cheap.