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 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: 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 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, scan by themselves when you come back, and so does gtklock after 4.0.0 (the
which shows the tick once they are gone. 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`. 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 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 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 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 GNOME, by itself on its own lock screen, and with SIGUSR1 for hyprlock,
swaylock. Any other lock screen opens itself, through PAM (see *DESKTOPS*). 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 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 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 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. 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 hyprlock, swaylock and gtklock (after 4.0.0) can also be opened from
them among the user's processes (niri also sets *LockedHint*), scans outside: the agent finds them among the user's processes (niri also
when somebody comes back, and opens them with SIGUSR1. Those lock sets *LockedHint*), scans when somebody comes back, and opens them with
screens cover the bubble, which shows the tick once they are gone. 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*. 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_ 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_input, &InputWatcher::input, this, &LockController::onResume);
connect(&m_lock, &LockWatcher::lockedChanged, this, &LockController::onLockedChanged); 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"), QDBusConnection::systemBus().connect(QStringLiteral("org.freedesktop.login1"),
QStringLiteral("/org/freedesktop/login1"), QStringLiteral("/org/freedesktop/login1"),
@@ -58,6 +66,7 @@ void LockController::onLockedChanged(bool locked)
} }
if (!locked) { if (!locked) {
m_locked = false; m_locked = false;
m_lockScanDue = false;
m_armTimer.stop(); m_armTimer.stop();
m_input.stop(); m_input.stop();
if (m_scan) { if (m_scan) {
@@ -90,6 +99,7 @@ void LockController::onLockedChanged(bool locked)
if (m_config->scanOnLock()) { if (m_config->scanOnLock()) {
QTimer::singleShot(400, this, [this] { QTimer::singleShot(400, this, [this] {
m_lockScanDue = m_locked && !m_lock.canUnlock();
startScan(QStringLiteral("lock")); startScan(QStringLiteral("lock"));
}); });
} else { } else {
+2
View File
@@ -59,6 +59,8 @@ private:
LockScreenController *m_screen; LockScreenController *m_screen;
bool m_locked = false; bool m_locked = false;
bool m_stopped = false; bool m_stopped = false;
// Scan right after locking, once the lock screen can be opened.
bool m_lockScanDue = false;
QElapsedTimer m_lockedFor; QElapsedTimer m_lockedFor;
QPointer<DaemonRequest> m_scan; QPointer<DaemonRequest> m_scan;
QTimer m_armTimer; QTimer m_armTimer;
+13 -4
View File
@@ -5,6 +5,7 @@
#include <QFile> #include <QFile>
#include <QString> #include <QString>
#include <algorithm>
#include <csignal> #include <csignal>
#include <dirent.h> #include <dirent.h>
#include <sys/stat.h> #include <sys/stat.h>
@@ -14,7 +15,7 @@ namespace
{ {
bool isLocker(const QByteArray &name) bool isLocker(const QByteArray &name)
{ {
return name == "hyprlock" || name == "swaylock"; return name == "hyprlock" || name == "swaylock" || name == "gtklock";
} }
QByteArray readFile(const QString &path) 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 // 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 // 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 // of a lock screen (swaylock and gtklock check the password in one) is left
// by the signal before its parent unlocks, it would take the parent down // out: killed by the signal before its parent unlocks, it would take the
// with it, and the screen would stay locked. // parent down with it, and the screen would stay locked.
QList<pid_t> findAll() QList<pid_t> findAll()
{ {
QList<pid_t> found; QList<pid_t> found;
@@ -104,6 +105,14 @@ QList<pid_t> Lockers::find(const QByteArray &name)
return found; 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 Lockers::signal(int sig, const QByteArray &name)
{ {
int waiting = 0; int waiting = 0;
+4 -1
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// //
// The lock screens that are programs of their own and take signals from // 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 #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 // that, the signal kills it, and the compositor keeps the screen locked with
// nobody to open it. // nobody to open it.
int signal(int sig, const QByteArray &name = {}); 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() void LockWatcher::pollLockers()
{ {
m_lockerRunning = !Lockers::find().isEmpty(); m_lockerRunning = !Lockers::find().isEmpty();
const bool ready = Lockers::ready(SIGUSR1);
const bool became = ready && !m_lockerReady;
m_lockerReady = ready;
update(); update();
if (became) {
Q_EMIT unlockable();
}
} }
void LockWatcher::setOwnLock(SessionLock *lock) void LockWatcher::setOwnLock(SessionLock *lock)
@@ -171,7 +177,7 @@ void LockWatcher::setOwnLock(SessionLock *lock)
bool LockWatcher::canUnlock() const 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() void LockWatcher::update()
+10 -5
View File
@@ -7,11 +7,12 @@
// GNOME logind's LockedHint. Unlocked through logind. // GNOME logind's LockedHint. Unlocked through logind.
// Niri logind's LockedHint, which niri sets for any lock screen. // Niri logind's LockedHint, which niri sets for any lock screen.
// Hyprland sets no LockedHint, so the lock screen is looked for among this // Hyprland sets no LockedHint, so the lock screen is looked for among this
// user's processes (hyprlock, swaylock) once a second. Only on // user's processes (hyprlock, swaylock, gtklock) once a second.
// compositors where the lock screen is a program of its own // Only on compositors where the lock screen is a program of its
// (ext-session-lock). // 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 // 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 // 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. // PAM module in its own stack (see src/lib/pam.sh), when Enter is pressed.
@@ -41,13 +42,16 @@ public:
return m_locked; return m_locked;
} }
// Whether unlock() can open the lock screen that is up: always where // 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; bool canUnlock() const;
void unlock(); void unlock();
void setOwnLock(SessionLock *lock); void setOwnLock(SessionLock *lock);
Q_SIGNALS: Q_SIGNALS:
void lockedChanged(bool locked); void lockedChanged(bool locked);
// canUnlock() came true: the lock screen that is up now takes SIGUSR1.
void unlockable();
private Q_SLOTS: private Q_SLOTS:
void onScreenSaver(bool active); void onScreenSaver(bool active);
@@ -63,6 +67,7 @@ private:
bool m_screenSaver = false; bool m_screenSaver = false;
bool m_lockedHint = false; bool m_lockedHint = false;
bool m_lockerRunning = false; bool m_lockerRunning = false;
bool m_lockerReady = false;
bool m_locked = false; bool m_locked = false;
// The compositor's lock screen is a program of its own. // The compositor's lock screen is a program of its own.
bool m_ownLockers = false; bool m_ownLockers = false;
+2
View File
@@ -95,6 +95,7 @@ int main(int, char **argv)
pid_t pid = fakeHyprlock(false, &from); pid_t pid = fakeHyprlock(false, &from);
check(Lockers::find("hyprlock") == QList<pid_t>{pid}, "finds the lock screen on this display"); 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::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"); check(Lockers::signal(SIGUSR2, "hyprlock") == 1, "one not ready yet");
usleep(200 * 1000); usleep(200 * 1000);
check(alive(pid), "and it was left alone, alive"); check(alive(pid), "and it was left alone, alive");
@@ -103,6 +104,7 @@ int main(int, char **argv)
close(from); close(from);
pid = fakeHyprlock(true, &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(Lockers::signal(SIGUSR2, "hyprlock") == 0, "a ready one gets it");
check(next(from) == 's', "and handles it"); check(next(from) == 's', "and handles it");
check(alive(pid), "and lives on"); check(alive(pid), "and lives on");