feat: open gtklock from outside once it can

This commit is contained in:
Felitendo committed 2026-09-26 19:36:17 +02:00
1 parent c228291520
commit 5699158bb1
9 files changed
+61 -19

No files matched your search

+3 -2
View File
@@ -83,8 +83,9 @@ switches it on; right after installing, log out and back in once.
On Hyprland and Niri the lock screen is a program of your choice, and only it
can open itself. So face-unlock goes into its password check, as with sudo:
press Enter on the empty password field to scan. hyprlock and swaylock also
scan by themselves when you come back. Those lock screens cover the bubble,
which shows the tick once they are gone.
scan by themselves when you come back, and so does gtklock after 4.0.0 (the
first to open from outside). Those lock screens cover the bubble, which shows
the tick once they are gone. gtklock shows face unlock's messages.
For the bubble on the lock screen, use face-unlock's own: `face-unlock lock`.
It shows the wallpaper of your desktop (from swaybg, awww, hyprpaper or
+10 -6
View File
@@ -201,8 +201,9 @@ does not start a scan with every key.
When the face matches, the agent unlocks the session the way its lock screen
wants it: through logind, as *loginctl unlock-session* does, on Plasma and
GNOME, by itself on its own lock screen, and with SIGUSR1 for hyprlock and
swaylock. Any other lock screen opens itself, through PAM (see *DESKTOPS*).
GNOME, by itself on its own lock screen, and with SIGUSR1 for hyprlock,
swaylock and gtklock after 4.0.0. Any other lock screen opens itself, through
PAM (see *DESKTOPS*).
Unlocked through logind, Plasma's lock screen cuts off its own password prompt
and counts that as a wrong password. After a face unlock the daemon resets the
@@ -241,10 +242,13 @@ so does the bubble, above the lock screen too.
open itself. hyprlock asks PAM the moment it starts; that first time
is skipped, so a screen locked on purpose does not open again at once.
hyprlock and swaylock can also be opened from outside: the agent finds
them among the user's processes (niri also sets *LockedHint*), scans
when somebody comes back, and opens them with SIGUSR1. Those lock
screens cover the bubble, which shows the tick once they are gone.
hyprlock, swaylock and gtklock (after 4.0.0) can also be opened from
outside: the agent finds them among the user's processes (niri also
sets *LockedHint*), scans when somebody comes back, and opens them with
SIGUSR1. It only does so once the lock screen handles SIGUSR1: before
it has locked, the signal would kill it. Those lock screens cover the
bubble, which shows the tick once they are gone. gtklock shows the
messages of the PAM module.
For the bubble on the lock screen there is face-unlock's own: *lock*.
It shows the time, a password field (PAM service _face-unlock-lock_
+10
View File
@@ -42,6 +42,14 @@ LockController::LockController(BubbleController *bubble, UserConfig *config, Ses
});
connect(&m_input, &InputWatcher::input, this, &LockController::onResume);
connect(&m_lock, &LockWatcher::lockedChanged, this, &LockController::onLockedChanged);
// A lock screen that is only starting cannot be opened yet: the scan
// that was due when it locked follows once it can.
connect(&m_lock, &LockWatcher::unlockable, this, [this] {
if (m_lockScanDue) {
m_lockScanDue = false;
startScan(QStringLiteral("lock"));
}
});
QDBusConnection::systemBus().connect(QStringLiteral("org.freedesktop.login1"),
QStringLiteral("/org/freedesktop/login1"),
@@ -58,6 +66,7 @@ void LockController::onLockedChanged(bool locked)
}
if (!locked) {
m_locked = false;
m_lockScanDue = false;
m_armTimer.stop();
m_input.stop();
if (m_scan) {
@@ -90,6 +99,7 @@ void LockController::onLockedChanged(bool locked)
if (m_config->scanOnLock()) {
QTimer::singleShot(400, this, [this] {
m_lockScanDue = m_locked && !m_lock.canUnlock();
startScan(QStringLiteral("lock"));
});
} else {
+2
View File
@@ -59,6 +59,8 @@ private:
LockScreenController *m_screen;
bool m_locked = false;
bool m_stopped = false;
// Scan right after locking, once the lock screen can be opened.
bool m_lockScanDue = false;
QElapsedTimer m_lockedFor;
QPointer<DaemonRequest> m_scan;
QTimer m_armTimer;
+13 -4
View File
@@ -5,6 +5,7 @@
#include <QFile>
#include <QString>
#include <algorithm>
#include <csignal>
#include <dirent.h>
#include <sys/stat.h>
@@ -14,7 +15,7 @@ namespace
{
bool isLocker(const QByteArray &name)
{
return name == "hyprlock" || name == "swaylock";
return name == "hyprlock" || name == "swaylock" || name == "gtklock";
}
QByteArray readFile(const QString &path)
@@ -33,9 +34,9 @@ pid_t parentOf(pid_t pid)
// The lock screens of this user on this display. Another session of the same
// user has a display of its own, and its lock screen is left alone. A child
// of a lock screen (swaylock checks the password in one) is left out: killed
// by the signal before its parent unlocks, it would take the parent down
// with it, and the screen would stay locked.
// of a lock screen (swaylock and gtklock check the password in one) is left
// out: killed by the signal before its parent unlocks, it would take the
// parent down with it, and the screen would stay locked.
QList<pid_t> findAll()
{
QList<pid_t> found;
@@ -104,6 +105,14 @@ QList<pid_t> Lockers::find(const QByteArray &name)
return found;
}
bool Lockers::ready(int sig)
{
const QList<pid_t> lockers = find();
return std::any_of(lockers.cbegin(), lockers.cend(), [sig](pid_t pid) {
return catches(pid, sig);
});
}
int Lockers::signal(int sig, const QByteArray &name)
{
int waiting = 0;
+4 -1
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// The lock screens that are programs of their own and take signals from
// outside: hyprlock and swaylock open on SIGUSR1.
// outside: hyprlock, swaylock and gtklock open on SIGUSR1.
#pragma once
@@ -19,4 +19,7 @@ QList<pid_t> find(const QByteArray &name = {});
// that, the signal kills it, and the compositor keeps the screen locked with
// nobody to open it.
int signal(int sig, const QByteArray &name = {});
// Whether one of them handles sig: gtklock only opens on SIGUSR1 since 2025,
// and none does before it has locked.
bool ready(int sig);
}
+7 -1
View File
@@ -160,7 +160,13 @@ void LockWatcher::onSessionProperties(const QString &interface, const QVariantMa
void LockWatcher::pollLockers()
{
m_lockerRunning = !Lockers::find().isEmpty();
const bool ready = Lockers::ready(SIGUSR1);
const bool became = ready && !m_lockerReady;
m_lockerReady = ready;
update();
if (became) {
Q_EMIT unlockable();
}
}
void LockWatcher::setOwnLock(SessionLock *lock)
@@ -171,7 +177,7 @@ void LockWatcher::setOwnLock(SessionLock *lock)
bool LockWatcher::canUnlock() const
{
return (m_own && m_own->isLocked()) || !m_ownLockers || m_lockerRunning;
return (m_own && m_own->isLocked()) || !m_ownLockers || m_lockerReady;
}
void LockWatcher::update()
+10 -5
View File
@@ -7,11 +7,12 @@
// GNOME logind's LockedHint. Unlocked through logind.
// Niri logind's LockedHint, which niri sets for any lock screen.
// Hyprland sets no LockedHint, so the lock screen is looked for among this
// user's processes (hyprlock, swaylock) once a second. Only on
// compositors where the lock screen is a program of its own
// (ext-session-lock).
// user's processes (hyprlock, swaylock, gtklock) once a second.
// Only on compositors where the lock screen is a program of its
// own (ext-session-lock).
//
// hyprlock and swaylock do not listen to logind. They unlock on SIGUSR1.
// hyprlock, swaylock and gtklock do not listen to logind. They unlock on
// SIGUSR1.
// face-unlock's own lock screen (SessionLock) is simply told to. Any other
// lock screen of that kind only opens itself: it gets the face through the
// PAM module in its own stack (see src/lib/pam.sh), when Enter is pressed.
@@ -41,13 +42,16 @@ public:
return m_locked;
}
// Whether unlock() can open the lock screen that is up: always where
// logind unlocks (Plasma, GNOME), only hyprlock and swaylock elsewhere.
// logind unlocks (Plasma, GNOME), elsewhere only hyprlock, swaylock and
// gtklock, once they take SIGUSR1.
bool canUnlock() const;
void unlock();
void setOwnLock(SessionLock *lock);
Q_SIGNALS:
void lockedChanged(bool locked);
// canUnlock() came true: the lock screen that is up now takes SIGUSR1.
void unlockable();
private Q_SLOTS:
void onScreenSaver(bool active);
@@ -63,6 +67,7 @@ private:
bool m_screenSaver = false;
bool m_lockedHint = false;
bool m_lockerRunning = false;
bool m_lockerReady = false;
bool m_locked = false;
// The compositor's lock screen is a program of its own.
bool m_ownLockers = false;
+2
View File
@@ -95,6 +95,7 @@ int main(int, char **argv)
pid_t pid = fakeHyprlock(false, &from);
check(Lockers::find("hyprlock") == QList<pid_t>{pid}, "finds the lock screen on this display");
check(Lockers::find("swaylock").isEmpty(), "and only by that name");
check(!Lockers::ready(SIGUSR2), "not ready while it does not handle the signal");
check(Lockers::signal(SIGUSR2, "hyprlock") == 1, "one not ready yet");
usleep(200 * 1000);
check(alive(pid), "and it was left alone, alive");
@@ -103,6 +104,7 @@ int main(int, char **argv)
close(from);
pid = fakeHyprlock(true, &from);
check(Lockers::ready(SIGUSR2), "ready once it does");
check(Lockers::signal(SIGUSR2, "hyprlock") == 0, "a ready one gets it");
check(next(from) == 's', "and handles it");
check(alive(pid), "and lives on");