feat: add an own lock screen with the bubble and your wallpaper

This commit is contained in:
Felitendo committed 2026-09-26 19:35:41 +02:00
1 parent c693ca216c
commit c228291520
44 files changed
+3891 -1033

No files matched your search

+37 -2
View File
@@ -26,7 +26,7 @@ AgentSocket::AgentSocket(QObject *parent)
continue;
}
auto buffer = std::make_shared<QByteArray>();
connect(socket, &QLocalSocket::readyRead, this, [this, socket, buffer] {
connect(socket, &QLocalSocket::readyRead, this, [this, socket, buffer, cred] {
*buffer += socket->readAll();
if (buffer->size() > 64 * 1024) {
socket->abort();
@@ -36,8 +36,12 @@ AgentSocket::AgentSocket(QObject *parent)
while ((nl = buffer->indexOf('\n')) >= 0) {
const QJsonObject o = QJsonDocument::fromJson(buffer->left(nl)).object();
buffer->remove(0, nl + 1);
if (o.value(u"event").toString() == u"scan") {
const QString what = o.value(u"event").toString();
if (what == u"scan") {
Q_EMIT scanEvent(o);
} else if (what == u"lock" && cred.uid == ::getuid()) {
m_waiting.append(socket);
Q_EMIT lockRequested();
}
}
});
@@ -58,6 +62,37 @@ bool AgentSocket::running()
return probe.waitForConnected(500);
}
bool AgentSocket::requestLock()
{
QLocalSocket socket;
socket.connectToServer(path());
if (!socket.waitForConnected(500)) {
return false;
}
socket.write(QJsonDocument(QJsonObject{{QStringLiteral("event"), QStringLiteral("lock")}}).toJson(QJsonDocument::Compact) + '\n');
if (!socket.waitForBytesWritten(500)) {
return false;
}
// The answer comes once the compositor has the lock, so that an idle
// daemon that locks before sleep does not suspend an unlocked screen.
QByteArray answer;
while (!answer.contains('\n') && socket.waitForReadyRead(5000)) {
answer += socket.readAll();
}
return QJsonDocument::fromJson(answer.left(answer.indexOf('\n'))).object().value(u"event").toString() == u"locked";
}
void AgentSocket::confirmLock()
{
for (const QPointer<QLocalSocket> &socket : std::as_const(m_waiting)) {
if (socket) {
socket->write(QJsonDocument(QJsonObject{{QStringLiteral("event"), QStringLiteral("locked")}}).toJson(QJsonDocument::Compact) + '\n');
socket->flush();
}
}
m_waiting.clear();
}
bool AgentSocket::listen()
{
const QString p = path();
+11 -1
View File
@@ -7,13 +7,16 @@
// it is idle).
//
// Only root and this user may talk here, and all they can do is make the
// bubble move.
// bubble move, or (this user) ask for face-unlock's own lock screen.
#pragma once
#include <QJsonObject>
#include <QList>
#include <QLocalServer>
#include <QLocalSocket>
#include <QObject>
#include <QPointer>
class AgentSocket : public QObject
{
@@ -26,11 +29,18 @@ public:
// session starts the agent from its own config, and that must not make
// two of them.
static bool running();
// Ask the running agent to lock the screen, and wait until it is. False
// when there is no agent or the lock did not come.
static bool requestLock();
// Tell everybody waiting in requestLock() that the screen is locked.
void confirmLock();
static QString path();
Q_SIGNALS:
void scanEvent(const QJsonObject &event);
void lockRequested();
private:
QLocalServer m_server;
QList<QPointer<QLocalSocket>> m_waiting;
};
+13
View File
@@ -175,6 +175,9 @@ InputWatcher::~InputWatcher() = default;
void InputWatcher::watch(int calmMs)
{
stop();
m_watching = true;
m_calmMs = calmMs;
m_calm.start();
auto *wayland = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>();
if (!m_notifier->isActive() || !wayland || !wayland->seat()) {
if (!m_mutter && QDBusConnection::sessionBus().interface()->isServiceRegistered(MutterService)) {
@@ -205,8 +208,18 @@ void InputWatcher::watch(int calmMs)
});
}
void InputWatcher::noteInput()
{
if (m_watching && m_calm.elapsed() >= m_calmMs) {
m_watching = false;
QMetaObject::invokeMethod(this, &InputWatcher::input, Qt::QueuedConnection);
}
m_calm.restart();
}
void InputWatcher::stop()
{
m_watching = false;
m_notification.reset();
if (m_mutter) {
m_mutter->stop();
+7
View File
@@ -10,6 +10,7 @@
#pragma once
#include <QElapsedTimer>
#include <QObject>
#include <memory>
@@ -29,6 +30,9 @@ public:
// calmMs. 0: the next input. Replaces what was watched before.
void watch(int calmMs);
void stop();
// A key or the pointer on face-unlock's own lock screen, which sees them
// itself. Counts like input the compositor reports.
void noteInput();
Q_SIGNALS:
void input();
@@ -37,4 +41,7 @@ private:
std::unique_ptr<IdleNotifier> m_notifier;
std::unique_ptr<IdleNotification> m_notification;
MutterIdle *m_mutter = nullptr;
bool m_watching = false;
int m_calmMs = 0;
QElapsedTimer m_calm;
};
+14 -2
View File
@@ -4,6 +4,7 @@
#include "bubblecontroller.h"
#include "daemonclient.h"
#include "lockscreencontroller.h"
#include "userconfig.h"
#include <QDBusConnection>
@@ -23,11 +24,18 @@ constexpr int CalmBeforeRetryMs = 2000;
constexpr int ScanAfterWakeMs = 1000;
} // namespace
LockController::LockController(BubbleController *bubble, UserConfig *config, QObject *parent)
LockController::LockController(BubbleController *bubble, UserConfig *config, SessionLock *lock, LockScreenController *screen, QObject *parent)
: QObject(parent)
, m_bubble(bubble)
, m_config(config)
, m_screen(screen)
{
if (lock) {
m_lock.setOwnLock(lock);
}
if (screen) {
connect(screen, &LockScreenController::input, &m_input, &InputWatcher::noteInput);
}
m_armTimer.setSingleShot(true);
connect(&m_armTimer, &QTimer::timeout, this, [this] {
arm(0);
@@ -68,7 +76,11 @@ void LockController::onLockedChanged(bool locked)
return;
}
if (!m_config->lockScreen()) {
const bool face = m_config->enabled() && m_config->lockScreen();
if (m_screen) {
m_screen->setFaceUnlock(face);
}
if (!face) {
return;
}
m_locked = true;
+5 -1
View File
@@ -26,13 +26,16 @@
class BubbleController;
class DaemonRequest;
class LockScreenController;
class SessionLock;
class UserConfig;
class LockController : public QObject
{
Q_OBJECT
public:
LockController(BubbleController *bubble, UserConfig *config, QObject *parent = nullptr);
// lock and screen are face-unlock's own lock screen, where there is one.
LockController(BubbleController *bubble, UserConfig *config, SessionLock *lock, LockScreenController *screen, QObject *parent = nullptr);
bool locked() const
{
@@ -53,6 +56,7 @@ private:
BubbleController *m_bubble;
UserConfig *m_config;
LockScreenController *m_screen;
bool m_locked = false;
bool m_stopped = false;
QElapsedTimer m_lockedFor;
+197
View File
@@ -0,0 +1,197 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "lockscreencontroller.h"
#include <KLocalizedString>
#include <QCoreApplication>
#include <QFile>
#include <QPointer>
#include <security/pam_appl.h>
#include <cstring>
#include <pwd.h>
#include <thread>
#include <unistd.h>
namespace
{
struct Conversation {
QByteArray password;
};
// Answers every hidden question with the password and every other one with
// nothing, as a lock screen that only asks for the password has to.
int converse(int count, const pam_message **messages, pam_response **responses, void *data)
{
auto *conversation = static_cast<Conversation *>(data);
auto *answers = static_cast<pam_response *>(calloc(size_t(count), sizeof(pam_response)));
if (!answers) {
return PAM_BUF_ERR;
}
for (int i = 0; i < count; ++i) {
if (messages[i]->msg_style == PAM_PROMPT_ECHO_OFF) {
answers[i].resp = strdup(conversation->password.constData());
} else if (messages[i]->msg_style == PAM_PROMPT_ECHO_ON) {
answers[i].resp = strdup("");
}
}
*responses = answers;
return PAM_SUCCESS;
}
QByteArray serviceName()
{
static const char *const services[] = {"face-unlock-lock", "password-auth", "common-auth", "login"};
static const char *const dirs[] = {"/etc/pam.d/", "/usr/lib/pam.d/", "/usr/share/pam.d/"};
for (const char *service : services) {
for (const char *dir : dirs) {
if (QFile::exists(QString::fromLatin1(dir) + QString::fromLatin1(service))) {
return service;
}
}
}
return "login";
}
bool checkPassword(const QByteArray &user, const QByteArray &password)
{
Conversation conversation{password};
const pam_conv conv{converse, &conversation};
pam_handle_t *pamh = nullptr;
// For development only, like the daemon's --socket: a directory of PAM
// files of its own, so a test never counts as a wrong password for the
// real account.
const QByteArray confdir = qgetenv("FU_PAM_CONFDIR");
int rc = confdir.isEmpty() ? pam_start(serviceName().constData(), user.constData(), &conv, &pamh)
: pam_start_confdir("face-unlock-lock", user.constData(), &conv, confdir.constData(), &pamh);
if (rc == PAM_SUCCESS) {
rc = pam_authenticate(pamh, 0);
if (rc == PAM_SUCCESS) {
pam_setcred(pamh, PAM_REFRESH_CRED);
}
}
pam_end(pamh, rc);
std::memset(conversation.password.data(), 0, size_t(conversation.password.size()));
return rc == PAM_SUCCESS;
}
passwd *me()
{
return getpwuid(getuid());
}
} // namespace
LockScreenController::LockScreenController(QObject *parent)
: QObject(parent)
{
}
LockScreenController::~LockScreenController()
{
clearPassword();
}
void LockScreenController::setPassword(const QString &password)
{
if (m_password == password) {
return;
}
m_password = password;
Q_EMIT passwordChanged();
if (!m_message.isEmpty()) {
m_message.clear();
Q_EMIT messageChanged();
}
}
void LockScreenController::setFaceUnlock(bool on)
{
if (m_faceUnlock != on) {
m_faceUnlock = on;
Q_EMIT faceUnlockChanged();
}
}
void LockScreenController::setBlur(bool blur)
{
if (m_blur != blur) {
m_blur = blur;
Q_EMIT blurChanged();
}
}
QString LockScreenController::userName() const
{
const passwd *pw = me();
if (!pw) {
return {};
}
// The full name from the comment field, else the login name.
const QString full = QString::fromLocal8Bit(pw->pw_gecos).section(u',', 0, 0).trimmed();
return full.isEmpty() ? QString::fromLocal8Bit(pw->pw_name) : full;
}
void LockScreenController::submit()
{
if (m_busy || m_password.isEmpty()) {
return;
}
const passwd *pw = me();
if (!pw) {
return;
}
m_busy = true;
Q_EMIT busyChanged();
const QByteArray user(pw->pw_name);
QByteArray password = m_password.toUtf8();
QPointer<LockScreenController> self(this);
std::thread([self, user, password]() mutable {
const bool ok = checkPassword(user, password);
std::memset(password.data(), 0, size_t(password.size()));
QMetaObject::invokeMethod(qApp, [self, ok] {
if (self) {
self->finish(ok);
}
});
}).detach();
}
void LockScreenController::finish(bool ok)
{
m_busy = false;
Q_EMIT busyChanged();
clearPassword();
Q_EMIT passwordChanged();
if (ok) {
Q_EMIT authenticated();
return;
}
m_message = i18n("Wrong password");
Q_EMIT messageChanged();
Q_EMIT rejected();
}
void LockScreenController::activity()
{
Q_EMIT input();
}
void LockScreenController::clearPassword()
{
// What is still in memory of it, as far as a QString lets that happen.
m_password.fill(u' ');
m_password.clear();
}
void LockScreenController::reset()
{
clearPassword();
Q_EMIT passwordChanged();
if (!m_message.isEmpty()) {
m_message.clear();
Q_EMIT messageChanged();
}
}
+86
View File
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// The own lock screen's side of things (see SessionLock): the password,
// whether it is being checked, what to say under it.
//
// The password is checked with PAM in a thread of its own, the way swaylock
// and hyprlock do it. The service is face-unlock-lock when an administrator
// has written one, else the distribution's plain password stack:
// password-auth (Fedora), common-auth (Debian, Ubuntu) or login. Not the face
// module: the face has its own way in here, the agent.
#pragma once
#include <QObject>
#include <QString>
class LockScreenController : public QObject
{
Q_OBJECT
Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
// After a wrong password, until the next key.
Q_PROPERTY(QString message READ message NOTIFY messageChanged)
// Whether the face is looked for, which changes what the screen says.
Q_PROPERTY(bool faceUnlock READ faceUnlock NOTIFY faceUnlockChanged)
Q_PROPERTY(QString userName READ userName CONSTANT)
// Whether to blur the picture behind it.
Q_PROPERTY(bool blur READ blur NOTIFY blurChanged)
public:
explicit LockScreenController(QObject *parent = nullptr);
~LockScreenController() override;
QString password() const
{
return m_password;
}
void setPassword(const QString &password);
bool busy() const
{
return m_busy;
}
QString message() const
{
return m_message;
}
bool faceUnlock() const
{
return m_faceUnlock;
}
void setFaceUnlock(bool on);
QString userName() const;
bool blur() const
{
return m_blur;
}
void setBlur(bool blur);
// Check the password typed so far.
Q_INVOKABLE void submit();
// A key or the pointer on the lock screen.
Q_INVOKABLE void activity();
// A fresh lock: no password, nothing said.
void reset();
Q_SIGNALS:
void passwordChanged();
void busyChanged();
void messageChanged();
void faceUnlockChanged();
void blurChanged();
void authenticated();
// A wrong password, for the field to shake.
void rejected();
void input();
private:
void finish(bool ok);
void clearPassword();
QString m_password;
QString m_message;
bool m_busy = false;
bool m_faceUnlock = true;
bool m_blur = false;
};
+13 -2
View File
@@ -3,6 +3,7 @@
#include "lockwatcher.h"
#include "lockers.h"
#include "sessionlock.h"
#include "wayland.h"
#include <QDBusConnection>
@@ -162,14 +163,20 @@ void LockWatcher::pollLockers()
update();
}
void LockWatcher::setOwnLock(SessionLock *lock)
{
m_own = lock;
connect(lock, &SessionLock::lockedChanged, this, &LockWatcher::update);
}
bool LockWatcher::canUnlock() const
{
return !m_ownLockers || m_lockerRunning;
return (m_own && m_own->isLocked()) || !m_ownLockers || m_lockerRunning;
}
void LockWatcher::update()
{
const bool locked = m_screenSaver || m_lockedHint || m_lockerRunning;
const bool locked = m_screenSaver || m_lockedHint || m_lockerRunning || (m_own && m_own->isLocked());
if (locked != m_locked) {
m_locked = locked;
Q_EMIT lockedChanged(locked);
@@ -178,6 +185,10 @@ void LockWatcher::update()
void LockWatcher::unlock()
{
if (m_own && m_own->isLocked()) {
m_own->unlock();
return;
}
// Looked up again rather than taken from the last poll: a second is
// long enough for a pid to belong to something else.
Lockers::signal(SIGUSR1);
+7 -3
View File
@@ -12,9 +12,9 @@
// (ext-session-lock).
//
// hyprlock and swaylock do not listen to logind. They unlock on SIGUSR1.
// 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.
// 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.
//
// None of this lowers the bar: any program running as this user can already
// ask logind to unlock the session, or send its own lock screen a signal. The
@@ -28,6 +28,8 @@
#include <sys/types.h>
class SessionLock;
class LockWatcher : public QObject
{
Q_OBJECT
@@ -42,6 +44,7 @@ public:
// logind unlocks (Plasma, GNOME), only hyprlock and swaylock elsewhere.
bool canUnlock() const;
void unlock();
void setOwnLock(SessionLock *lock);
Q_SIGNALS:
void lockedChanged(bool locked);
@@ -63,6 +66,7 @@ private:
bool m_locked = false;
// The compositor's lock screen is a program of its own.
bool m_ownLockers = false;
SessionLock *m_own = nullptr;
QString m_session;
QTimer m_poll;
};
+155 -2
View File
@@ -5,6 +5,9 @@
// (no arguments) stay in the background: unlock the lock screen by face,
// and show the bubble for every scan
// --enroll open the window that sets up a face
// --lock lock the screen: with face-unlock's own lock screen
// where the lock screen is a program of its own (Hyprland,
// Niri), else through logind with the desktop's
// --demo play the bubble's animations once, for trying out a
// style (--bubble-style minimal) and for screenshots
@@ -14,6 +17,8 @@
#include "bubblewindow.h"
#include "enrollcontroller.h"
#include "lockcontroller.h"
#include "lockscreencontroller.h"
#include "sessionlock.h"
#include "userconfig.h"
#include "wayland.h"
@@ -23,11 +28,17 @@
#include <KLocalizedString>
#include <QCommandLineParser>
#include <QDBusConnection>
#include <QDBusMessage>
#include <QFile>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlEngine>
#include <QTimer>
#include <cstring>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
namespace
@@ -55,6 +66,92 @@ int runEnroll(QGuiApplication &app, const QString &name)
return controller.state() == u"done" ? 0 : code;
}
// Where --lock tells the command that ran it that the screen is locked, when
// it forked to stay behind as the lock screen. -1: it did not.
int readyFd = -1;
void signalReady(bool ok)
{
if (readyFd >= 0) {
if (ok) {
[[maybe_unused]] const ssize_t n = write(readyFd, "1", 1);
}
close(readyFd);
readyFd = -1;
}
}
// Whether an agent listens on its socket. Asked before Qt is up, to decide
// whether to fork.
bool agentListening()
{
const char *runtime = getenv("XDG_RUNTIME_DIR");
if (!runtime) {
return false;
}
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
const int len = snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/face-unlock/agent.socket", runtime);
if (len <= 0 || size_t(len) >= sizeof(addr.sun_path)) {
return false;
}
const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
const bool ok = fd >= 0 && connect(fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) == 0;
if (fd >= 0) {
close(fd);
}
return ok;
}
// With no agent running, --lock stays behind as the lock screen until it is
// unlocked. The command that asked for it returns as soon as the screen is
// locked, the way swaylock -f does: an idle daemon locking before sleep
// waits for that, and not a moment longer.
void forkForLock(int argc, char **argv)
{
bool lock = false;
for (int i = 1; i < argc; ++i) {
lock = lock || std::strcmp(argv[i], "--lock") == 0;
}
int fds[2];
if (!lock || agentListening() || pipe(fds) != 0) {
return;
}
const pid_t pid = fork();
if (pid < 0) {
close(fds[0]);
close(fds[1]);
return;
}
if (pid > 0) {
close(fds[1]);
char c = 0;
const bool locked = read(fds[0], &c, 1) == 1;
_exit(locked ? 0 : 1);
}
close(fds[0]);
setsid();
readyFd = fds[1];
}
// Plasma and GNOME lock with their own lock screen when logind asks them to,
// as `loginctl lock-session` does.
int lockThroughLogind()
{
const QString id = qEnvironmentVariable("XDG_SESSION_ID");
QDBusMessage call = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.login1"),
QStringLiteral("/org/freedesktop/login1"),
QStringLiteral("org.freedesktop.login1.Manager"),
QStringLiteral("LockSession"));
call << (id.isEmpty() ? QStringLiteral("auto") : id);
const QDBusMessage reply = QDBusConnection::systemBus().call(call);
if (reply.type() == QDBusMessage::ErrorMessage) {
qWarning("logind would not lock: %s", qPrintable(reply.errorMessage()));
return 1;
}
return 0;
}
// The whole life of a bubble, twice: a face that is recognised after a blink,
// then one that is not.
void scheduleDemo(BubbleController *bubble)
@@ -77,6 +174,7 @@ void scheduleDemo(BubbleController *bubble)
int main(int argc, char **argv)
{
forkForLock(argc, argv);
QGuiApplication app(argc, argv);
app.setApplicationName(QStringLiteral("face-unlock-agent"));
app.setApplicationVersion(QStringLiteral(FU_VERSION));
@@ -89,10 +187,11 @@ int main(int argc, char **argv)
parser.addVersionOption();
const QCommandLineOption enrollOpt(QStringLiteral("enroll"), i18n("Set up a face."));
const QCommandLineOption nameOpt(QStringLiteral("name"), i18n("What to call the new face."), QStringLiteral("name"));
const QCommandLineOption lockOpt(QStringLiteral("lock"), i18n("Lock the screen."));
const QCommandLineOption demoOpt(QStringLiteral("demo"), i18n("Play the bubble's animations once."));
// Not "--style": QGuiApplication takes that one for itself.
const QCommandLineOption styleOpt(QStringLiteral("bubble-style"), i18n("Bubble style for the demo: full or minimal."), QStringLiteral("style"));
parser.addOptions({enrollOpt, nameOpt, demoOpt, styleOpt});
parser.addOptions({enrollOpt, nameOpt, lockOpt, demoOpt, styleOpt});
parser.process(app);
if (parser.isSet(enrollOpt)) {
@@ -103,6 +202,13 @@ int main(int argc, char **argv)
return runEnroll(app, name);
}
const bool lockNow = parser.isSet(lockOpt);
if (lockNow && !SessionLock::available()) {
const int rc = lockThroughLogind();
signalReady(rc == 0);
return rc;
}
app.setQuitOnLastWindowClosed(false);
QQmlEngine engine;
KLocalization::setupLocalizedContext(&engine);
@@ -131,6 +237,9 @@ int main(int argc, char **argv)
AgentSocket socket;
if (AgentSocket::running()) {
if (lockNow) {
return AgentSocket::requestLock() ? 0 : 1;
}
qInfo("an agent is already running in this session");
return 0;
}
@@ -141,6 +250,50 @@ int main(int argc, char **argv)
service = std::make_unique<BubbleService>(&bubble);
}
LockController lock(&bubble, &config);
LockScreenController screen;
std::unique_ptr<SessionLock> sessionLock;
if (SessionLock::available()) {
sessionLock = std::make_unique<SessionLock>(&engine, &bubble, &screen, &config);
SessionLock *lock = sessionLock.get();
QObject::connect(&socket, &AgentSocket::lockRequested, lock, [lock, &socket] {
lock->lock();
if (lock->isConfirmed()) {
socket.confirmLock();
}
});
QObject::connect(lock, &SessionLock::confirmed, &socket, &AgentSocket::confirmLock);
}
LockController lock(&bubble, &config, sessionLock.get(), &screen);
if (lockNow) {
// No agent was running, so this one is only here for the lock. It
// goes once the lock is gone and the bubble has finished.
QObject::connect(sessionLock.get(), &SessionLock::lockedChanged, &app, [&bubble](bool locked) {
if (locked) {
return;
}
if (!bubble.shown()) {
QCoreApplication::quit();
}
QObject::connect(&bubble, &BubbleController::shownChanged, qApp, [&bubble] {
if (!bubble.shown()) {
QCoreApplication::quit();
}
});
QTimer::singleShot(5000, qApp, &QCoreApplication::quit);
});
QObject::connect(sessionLock.get(), &SessionLock::confirmed, &app, [] {
signalReady(true);
});
sessionLock->lock();
if (!sessionLock->isLocked()) {
signalReady(false);
return 1;
}
} else if (sessionLock && QFile::exists(SessionLock::markerPath())) {
// The last agent went away with the screen locked, and the compositor
// kept it locked. Take the lock back, so it can be opened again.
sessionLock->lock();
}
return app.exec();
}
+204
View File
@@ -0,0 +1,204 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// face-unlock's own lock screen, one per screen (see SessionLock): the time,
// the password, and on the main screen the bubble at the top, the same one
// that shows over the desktop. Behind it the desktop's picture, or the one
// the user set (see Wallpaper).
import QtQuick
import QtQuick.Effects
Item {
id: root
required property var lock
required property var bubble
required property bool primary
required property url wallpaper
property date now: new Date()
Timer {
interval: 1000
running: true
repeat: true
onTriggered: root.now = new Date()
}
Rectangle {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0; color: "#11161D" }
GradientStop { position: 0.6; color: "#000000" }
}
}
// Loaded aside, so a big picture does not hold up the lock, and faded in.
Item {
anchors.fill: parent
opacity: picture.status === Image.Ready ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
Image {
id: picture
// Past the edges when blurred: the blur would pull in the nothing
// beyond them and darken the rim.
anchors.fill: parent
anchors.margins: root.lock.blur ? -64 : 0
source: root.wallpaper
fillMode: Image.PreserveAspectCrop
sourceSize: Qt.size(root.width, root.height)
asynchronous: true
cache: false
layer.enabled: root.lock.blur
layer.effect: MultiEffect {
blurEnabled: true
blur: 1
blurMax: 64
}
}
// Dimmed a little, so the time and the password read on any picture.
Rectangle {
anchors.fill: parent
color: "#000000"
opacity: 0.35
}
}
// Any touch counts as somebody being back, and a click anywhere puts the
// keys into the password.
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.AllButtons
onPositionChanged: root.lock.activity()
onPressed: {
root.lock.activity()
field.forceActiveFocus()
}
}
// Below the open bubble, which hangs from the top edge.
Column {
anchors.horizontalCenter: parent.horizontalCenter
y: Math.max(Theme.topGap + Theme.openHeight + 24, Math.round(parent.height * 0.2))
spacing: 2
Text {
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.textPrimary
font.pixelSize: Math.max(56, Math.min(120, Math.round(root.height * 0.11)))
font.weight: Font.Light
text: Qt.formatTime(root.now, Qt.locale().timeFormat(Locale.ShortFormat))
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.textDetail
font.pixelSize: 18
font.weight: Font.Medium
text: Qt.formatDate(root.now, Qt.locale().dateFormat(Locale.LongFormat))
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
y: Math.round(parent.height * 0.62)
spacing: 12
Text {
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.textPrimary
font.pixelSize: 17
font.weight: Font.DemiBold
text: root.lock.userName
}
Rectangle {
id: pill
anchors.horizontalCenter: parent.horizontalCenter
width: 280
height: 44
radius: height / 2
color: Theme.surfaceRaised
opacity: root.lock.busy ? 0.6 : 1
border.width: field.activeFocus ? 1 : 0
border.color: Qt.rgba(1, 1, 1, 0.18)
property real shake: 0
transform: Translate { x: pill.shake }
SequentialAnimation {
id: shakeAnimation
NumberAnimation { target: pill; property: "shake"; to: -10; duration: 55; easing.type: Easing.OutQuad }
NumberAnimation { target: pill; property: "shake"; to: 9; duration: 70 }
NumberAnimation { target: pill; property: "shake"; to: -6; duration: 65 }
NumberAnimation { target: pill; property: "shake"; to: 4; duration: 60 }
NumberAnimation { target: pill; property: "shake"; to: 0; duration: 55; easing.type: Easing.OutQuad }
}
TextInput {
id: field
anchors.fill: parent
anchors.leftMargin: 20
anchors.rightMargin: 20
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
echoMode: TextInput.Password
passwordCharacter: "●"
color: Theme.textPrimary
selectionColor: Theme.accent
font.pixelSize: 15
clip: true
focus: true
// The dots show how far it is; a blinking bar in the middle
// of the empty field only cuts the word in it in two.
cursorDelegate: Item {}
readOnly: root.lock.busy
text: root.lock.password
onTextEdited: root.lock.password = text
onAccepted: root.lock.submit()
Keys.onPressed: event => {
root.lock.activity()
event.accepted = false
}
Component.onCompleted: forceActiveFocus()
}
Text {
anchors.centerIn: parent
color: Theme.textSecondary
font.pixelSize: 15
text: i18n("Password")
visible: field.text.length === 0
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
width: 360
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
color: root.lock.message.length > 0 ? Theme.failure : Theme.textDetail
font.pixelSize: 13
font.weight: Font.Medium
text: root.lock.message.length > 0 ? root.lock.message
: root.lock.faceUnlock ? i18n("Look at the camera or type your password")
: i18n("Type your password")
}
}
Connections {
target: root.lock
function onRejected() {
shakeAnimation.restart()
}
}
// The bubble, where it shows over the desktop too.
Bubble {
visible: root.primary
anchors.horizontalCenter: parent.horizontalCenter
y: 0
bubble: root.bubble
}
}
+289
View File
@@ -0,0 +1,289 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "sessionlock.h"
#include "bubblecontroller.h"
#include "lockscreencontroller.h"
#include "userconfig.h"
#include "wallpaper.h"
#include "wayland.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QQuickView>
#include <QScreen>
#include <QStandardPaths>
#include <QUrl>
#include <QtWaylandClient/private/qwaylanddisplay_p.h>
#include <QtWaylandClient/private/qwaylandintegration_p.h>
#include <QtWaylandClient/private/qwaylandscreen_p.h>
#include <QtWaylandClient/private/qwaylandshellintegration_p.h>
#include <QtWaylandClient/private/qwaylandshellsurface_p.h>
#include <QtWaylandClient/private/qwaylandsurface_p.h>
#include <QtWaylandClient/private/qwaylandwindow_p.h>
#include "qwayland-ext-session-lock-v1.h"
// The manager, and through it the role every lock window gets.
class LockIntegration : public QtWaylandClient::QWaylandShellIntegrationTemplate<LockIntegration>, public QtWayland::ext_session_lock_manager_v1
{
public:
LockIntegration()
: QWaylandShellIntegrationTemplate<LockIntegration>(1)
{
}
~LockIntegration() override
{
if (object()) {
destroy();
}
}
QtWaylandClient::QWaylandShellSurface *createShellSurface(QtWaylandClient::QWaylandWindow *window) override;
// The lock the surfaces belong to.
::ext_session_lock_v1 *current = nullptr;
};
// One screen's lock surface. Like LayerShellQt's layer surface: the size
// comes from the compositor, and nothing is drawn before it has sent one.
class LockSurface : public QtWaylandClient::QWaylandShellSurface, public QtWayland::ext_session_lock_surface_v1
{
public:
LockSurface(::ext_session_lock_v1 *lock, QtWaylandClient::QWaylandWindow *window)
: QWaylandShellSurface(window)
{
init(ext_session_lock_v1_get_lock_surface(lock, window->waylandSurface()->object(), window->waylandScreen()->output()));
}
~LockSurface() override
{
destroy();
}
bool isExposed() const override
{
return m_configured;
}
// Qt commits a new role at once, as xdg-shell wants. A lock surface must
// not be committed before it has acknowledged its first size.
bool commitSurfaceRole() const override
{
return false;
}
void applyConfigure() override
{
window()->resizeFromApplyConfigure(m_size);
}
protected:
void ext_session_lock_surface_v1_configure(uint32_t serial, uint32_t width, uint32_t height) override
{
ack_configure(serial);
m_size = QSize(int(width), int(height));
if (!m_configured) {
m_configured = true;
applyConfigure();
window()->updateExposure();
} else {
window()->applyConfigureWhenPossible();
}
}
private:
QSize m_size;
bool m_configured = false;
};
QtWaylandClient::QWaylandShellSurface *LockIntegration::createShellSurface(QtWaylandClient::QWaylandWindow *window)
{
return new LockSurface(current, window);
}
class Lock : public QtWayland::ext_session_lock_v1
{
public:
Lock(::ext_session_lock_v1 *object, SessionLock *owner)
: QtWayland::ext_session_lock_v1(object)
, m_owner(owner)
{
}
protected:
// Queued: not from inside the Wayland event, which the answer to it might
// destroy.
void ext_session_lock_v1_locked() override
{
QMetaObject::invokeMethod(m_owner, &SessionLock::onLocked, Qt::QueuedConnection);
}
void ext_session_lock_v1_finished() override
{
QMetaObject::invokeMethod(m_owner, &SessionLock::onFinished, Qt::QueuedConnection);
}
private:
SessionLock *m_owner;
};
SessionLock::SessionLock(QQmlEngine *engine, BubbleController *bubble, LockScreenController *screen, UserConfig *config, QObject *parent)
: QObject(parent)
, m_engine(engine)
, m_bubble(bubble)
, m_screen(screen)
, m_config(config)
{
connect(qGuiApp, &QGuiApplication::screenAdded, this, [this](QScreen *s) {
if (m_lock) {
addScreen(s);
}
});
connect(qGuiApp, &QGuiApplication::screenRemoved, this, &SessionLock::removeScreen);
connect(m_screen, &LockScreenController::authenticated, this, &SessionLock::unlock);
}
SessionLock::~SessionLock()
{
// Leaving with the lock held keeps the session locked, which is the
// point. The windows go, the lock stays.
qDeleteAll(m_views);
delete m_lock;
delete m_integration;
}
bool SessionLock::available()
{
return Wayland::hasGlobal("ext_session_lock_manager_v1");
}
QString SessionLock::markerPath()
{
return QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QStringLiteral("/face-unlock/locked");
}
void SessionLock::lock()
{
if (m_lock) {
return;
}
auto *qpa = QtWaylandClient::QWaylandIntegration::instance();
if (!m_integration && qpa) {
m_integration = new LockIntegration;
if (!m_integration->initialize(qpa->display())) {
delete m_integration;
m_integration = nullptr;
}
}
if (!m_integration) {
qWarning("the compositor has no ext_session_lock_manager_v1, so this cannot lock the screen");
return;
}
m_confirmed = false;
m_unlockWanted = false;
m_lock = new Lock(m_integration->lock(), this);
m_integration->current = m_lock->object();
m_screen->reset();
m_pictures = Wallpaper::forLock(m_config->lockWallpaper());
m_screen->setBlur(m_config->lockBlur());
for (QScreen *s : QGuiApplication::screens()) {
addScreen(s);
}
QDir().mkpath(QFileInfo(markerPath()).absolutePath());
QFile marker(markerPath());
if (!marker.open(QIODevice::WriteOnly)) {
qWarning("cannot write %s", qPrintable(markerPath()));
}
Q_EMIT lockedChanged(true);
}
void SessionLock::addScreen(QScreen *screen)
{
if (m_views.contains(screen) || !dynamic_cast<QtWaylandClient::QWaylandScreen *>(screen->handle())) {
return;
}
auto *view = new QQuickView(m_engine, nullptr);
view->setColor(Qt::black);
view->setScreen(screen);
view->setResizeMode(QQuickView::SizeRootObjectToView);
view->resize(screen->size());
view->setInitialProperties({{QStringLiteral("lock"), QVariant::fromValue(m_screen)},
{QStringLiteral("bubble"), QVariant::fromValue(m_bubble)},
{QStringLiteral("wallpaper"), Wallpaper::pick(m_pictures, screen->name())},
{QStringLiteral("primary"), screen == QGuiApplication::primaryScreen()}});
view->loadFromModule(QStringLiteral("FaceUnlock"), QStringLiteral("LockScreen"));
for (const QQmlError &e : view->errors()) {
qWarning("%s", qPrintable(e.toString()));
}
view->create();
if (auto *wayland = dynamic_cast<QtWaylandClient::QWaylandWindow *>(view->handle())) {
wayland->setShellIntegration(m_integration);
}
view->show();
view->requestActivate();
m_views.insert(screen, view);
}
void SessionLock::removeScreen(QScreen *screen)
{
if (auto view = m_views.take(screen)) {
delete view;
}
}
void SessionLock::onLocked()
{
if (!m_lock) {
return;
}
m_confirmed = true;
Q_EMIT confirmed();
if (m_unlockWanted) {
unlock();
}
}
void SessionLock::onFinished()
{
if (!m_lock) {
return;
}
// Refused (another lock screen is up) or ended by the compositor.
if (m_confirmed) {
m_lock->unlock_and_destroy();
} else {
qWarning("the compositor did not let this lock the screen; is another lock screen running?");
m_lock->destroy();
}
release();
}
void SessionLock::unlock()
{
if (!m_lock) {
return;
}
// Unlocking before the compositor has confirmed the lock is a protocol
// error. It follows as soon as it has.
if (!m_confirmed) {
m_unlockWanted = true;
return;
}
m_lock->unlock_and_destroy();
release();
}
void SessionLock::release()
{
qDeleteAll(m_views);
m_views.clear();
delete m_lock;
m_lock = nullptr;
m_confirmed = false;
m_integration->current = nullptr;
QFile::remove(markerPath());
// Out to the compositor now: an agent started only for this lock quits
// straight after.
Wayland::sync();
m_screen->reset();
Q_EMIT lockedChanged(false);
}
+85
View File
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// face-unlock's own lock screen, for whoever wants it on a compositor whose
// lock screen is a program of its own (ext-session-lock: Hyprland, Niri,
// Sway and others): `face-unlock lock`.
//
// Any other lock screen there covers the bubble, and only opens itself (it
// gets the face through PAM, see src/lib/pam.sh). This one is the lock
// screen, so the bubble shows on it and a face opens it straight away. The
// picture behind it is the user's choice (LockWallpaper).
//
// Qt knows no ext-session-lock, so each screen's window gets the lock surface
// role through Qt's shell integration, the way LayerShellQt gives windows the
// layer-shell role. If this program goes away while locked, the compositor
// keeps the session locked: that is the protocol's promise.
#pragma once
#include <QHash>
#include <QObject>
#include <QPointer>
#include <QUrl>
class BubbleController;
class LockScreenController;
class UserConfig;
class QQmlEngine;
class QQuickView;
class QScreen;
class LockIntegration;
class Lock;
class SessionLock : public QObject
{
Q_OBJECT
public:
SessionLock(QQmlEngine *engine, BubbleController *bubble, LockScreenController *screen, UserConfig *config, QObject *parent = nullptr);
~SessionLock() override;
// Whether the compositor offers ext-session-lock.
static bool available();
// From the lock request until the lock is gone again.
bool isLocked() const
{
return m_lock != nullptr;
}
// The compositor has confirmed the lock: nothing unlocked is on screen.
bool isConfirmed() const
{
return m_confirmed;
}
// A file that says the screen is locked, so that an agent started again
// after a crash locks again (see main.cpp).
static QString markerPath();
public Q_SLOTS:
void lock();
void unlock();
Q_SIGNALS:
void lockedChanged(bool locked);
void confirmed();
private:
friend class Lock;
void onLocked();
void onFinished();
void addScreen(QScreen *screen);
void removeScreen(QScreen *screen);
void release();
QQmlEngine *m_engine;
BubbleController *m_bubble;
LockScreenController *m_screen;
UserConfig *m_config;
LockIntegration *m_integration = nullptr;
Lock *m_lock = nullptr;
bool m_confirmed = false;
bool m_unlockWanted = false;
QHash<QScreen *, QPointer<QQuickView>> m_views;
// The pictures behind it, by output (see Wallpaper).
QHash<QString, QUrl> m_pictures;
};
+3
View File
@@ -29,6 +29,9 @@ QString UserConfig::path()
void UserConfig::load()
{
const KeyValueFile kv = KeyValueFile::load(path());
m_enabled = kv.boolean(QStringLiteral("Enabled"), false);
m_lockWallpaper = kv.value(QStringLiteral("LockWallpaper"));
m_lockBlur = kv.boolean(QStringLiteral("LockBlur"), false);
m_lockScreen = kv.boolean(QStringLiteral("LockScreen"), true);
m_scanOnWake = kv.boolean(QStringLiteral("ScanOnWake"), true);
m_scanOnLock = kv.boolean(QStringLiteral("ScanOnLock"), false);
+19
View File
@@ -17,6 +17,22 @@ class UserConfig : public QObject
public:
explicit UserConfig(QObject *parent = nullptr);
// Face unlock turned on at all. The agent's service only runs when it is,
// but the own lock screen (--lock) runs either way.
bool enabled() const
{
return m_enabled;
}
// The picture behind the own lock screen (see Wallpaper): empty for the
// desktop's, "none", a file, or a folder to take one from at random.
QString lockWallpaper() const
{
return m_lockWallpaper;
}
bool lockBlur() const
{
return m_lockBlur;
}
// Unlock the lock screen by face.
bool lockScreen() const
{
@@ -72,6 +88,9 @@ private:
void load();
QFileSystemWatcher m_watcher;
bool m_enabled = false;
QString m_lockWallpaper;
bool m_lockBlur = false;
bool m_lockScreen = true;
bool m_scanOnWake = true;
bool m_scanOnLock = false;
+180
View File
@@ -0,0 +1,180 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "wallpaper.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QRandomGenerator>
#include <QRegularExpression>
#include <unistd.h>
namespace
{
// A picture, or one at random from a folder.
QUrl picture(const QString &setting)
{
QString path = setting;
if (path.startsWith(u"~/")) {
path = QDir::homePath() + path.mid(1);
}
const QFileInfo info(path);
if (setting.isEmpty() || !info.exists()) {
return {};
}
if (info.isFile()) {
return QUrl::fromLocalFile(info.absoluteFilePath());
}
const QStringList pictures = QDir(path).entryList({QStringLiteral("*.jpg"), QStringLiteral("*.jpeg"), QStringLiteral("*.png"), QStringLiteral("*.webp")},
QDir::Files | QDir::Readable);
if (pictures.isEmpty()) {
return {};
}
return QUrl::fromLocalFile(QDir(path).absoluteFilePath(pictures.at(QRandomGenerator::global()->bounded(int(pictures.size())))));
}
// What a command prints, or nothing when it is not there or hangs.
QString run(const QString &program, const QStringList &args)
{
QProcess process;
process.start(program, args);
if (!process.waitForFinished(1000) || process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
process.kill();
process.waitForFinished(100);
return {};
}
return QString::fromLocal8Bit(process.readAllStandardOutput());
}
// The pictures the running wallpaper programs of this user show.
QHash<QString, QString> desktop()
{
QHash<QString, QString> found;
const uint me = getuid();
const QStringList pids = QDir(QStringLiteral("/proc")).entryList({QStringLiteral("[0-9]*")}, QDir::Dirs);
for (const QString &pid : pids) {
const QString dir = QStringLiteral("/proc/") + pid;
if (QFileInfo(dir).ownerId() != me) {
continue;
}
QFile comm(dir + QStringLiteral("/comm"));
if (!comm.open(QIODevice::ReadOnly)) {
continue;
}
const QByteArray name = comm.readAll().trimmed();
if (name == "swaybg") {
QFile cmdline(dir + QStringLiteral("/cmdline"));
if (cmdline.open(QIODevice::ReadOnly)) {
QStringList args;
for (const QByteArray &arg : cmdline.readAll().split('\0')) {
args << QString::fromLocal8Bit(arg);
}
// Relative to where it was started.
const QDir cwd(QFileInfo(dir + QStringLiteral("/cwd")).symLinkTarget());
QHash<QString, QString> shown = Wallpaper::fromSwaybg(args.mid(1));
for (QString &path : shown) {
path = cwd.absoluteFilePath(path);
}
found.insert(shown);
}
} else if (name == "awww-daemon" || name == "swww-daemon") {
found.insert(Wallpaper::fromAwww(run(QString::fromLatin1(name.left(4)), {QStringLiteral("query")})));
} else if (name == "hyprpaper") {
found.insert(Wallpaper::fromList(run(QStringLiteral("hyprctl"), {QStringLiteral("hyprpaper"), QStringLiteral("listactive")})));
} else if (name == "wpaperd") {
found.insert(Wallpaper::fromList(run(QStringLiteral("wpaperctl"), {QStringLiteral("all-wallpapers")})));
}
}
return found;
}
} // namespace
QHash<QString, QString> Wallpaper::fromSwaybg(const QStringList &args)
{
QHash<QString, QString> found;
QString output;
for (int i = 0; i < args.size(); ++i) {
const QString &arg = args.at(i);
const bool hasNext = i + 1 < args.size();
if ((arg == u"-o" || arg == u"--output") && hasNext) {
output = args.at(++i);
} else if (arg.startsWith(u"--output=")) {
output = arg.mid(9);
} else if ((arg == u"-i" || arg == u"--image") && hasNext) {
found.insert(output == u"*" ? QString() : output, args.at(++i));
} else if (arg.startsWith(u"--image=")) {
found.insert(output == u"*" ? QString() : output, arg.mid(8));
}
}
return found;
}
QHash<QString, QString> Wallpaper::fromAwww(const QString &text)
{
// "eDP-1: 1920x1080, scale: 1, currently displaying: image: /a/b.jpg",
// with a namespace in front on awww. A plain colour is no picture.
static const QRegularExpression line(QStringLiteral("([^\\s:]+): \\d+x\\d+,.*currently displaying: image: (.+)$"), QRegularExpression::MultilineOption);
QHash<QString, QString> found;
auto it = line.globalMatch(text);
while (it.hasNext()) {
const auto match = it.next();
found.insert(match.captured(1), match.captured(2).trimmed());
}
return found;
}
QHash<QString, QString> Wallpaper::fromList(const QString &text)
{
// "eDP-1: /a/b.jpg", on older hyprpaper "eDP-1 = /a/b.jpg".
static const QRegularExpression line(QStringLiteral("^([^\\s:=]+)(?:: | = )(/.+)$"), QRegularExpression::MultilineOption);
QHash<QString, QString> found;
auto it = line.globalMatch(text);
while (it.hasNext()) {
const auto match = it.next();
found.insert(match.captured(1), match.captured(2).trimmed());
}
return found;
}
QHash<QString, QUrl> Wallpaper::forLock(const QString &setting)
{
QHash<QString, QUrl> pictures;
if (setting.compare(u"none", Qt::CaseInsensitive) == 0) {
return pictures;
}
if (!setting.isEmpty()) {
const QUrl url = picture(setting);
if (!url.isEmpty()) {
pictures.insert(QString(), url);
}
return pictures;
}
const QHash<QString, QString> shown = desktop();
for (auto it = shown.cbegin(); it != shown.cend(); ++it) {
const QUrl url = picture(it.value());
if (!url.isEmpty()) {
pictures.insert(it.key(), url);
}
}
return pictures;
}
QUrl Wallpaper::pick(const QHash<QString, QUrl> &pictures, const QString &output)
{
if (pictures.isEmpty()) {
return {};
}
if (pictures.contains(output)) {
return pictures.value(output);
}
if (pictures.contains(QString())) {
return pictures.value(QString());
}
// Named differently than here: some picture of the desktop is still
// better than none.
QStringList outputs = pictures.keys();
outputs.sort();
return pictures.value(outputs.first());
}
+30
View File
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// The picture behind face-unlock's own lock screen. By default the one on
// the desktop. Hyprland, niri and the like leave the wallpaper to a program
// of its own, so it comes from whichever of the common ones runs: swaybg,
// awww (once swww), hyprpaper or wpaperd, each for every output.
#pragma once
#include <QHash>
#include <QString>
#include <QStringList>
#include <QUrl>
namespace Wallpaper
{
// The pictures for a lock by output name, "" for every other output.
// setting is LockWallpaper: empty for the desktop's, "none", a picture, or a
// folder to take one from at random.
QHash<QString, QUrl> forLock(const QString &setting);
// The one for this output.
QUrl pick(const QHash<QString, QUrl> &pictures, const QString &output);
// What the programs tell, by output name. Apart for the tests.
QHash<QString, QString> fromSwaybg(const QStringList &args);
// `awww query` and `swww query`.
QHash<QString, QString> fromAwww(const QString &text);
// `hyprctl hyprpaper listactive` and `wpaperctl all-wallpapers`.
QHash<QString, QString> fromList(const QString &text);
}
+21 -6
View File
@@ -21,6 +21,12 @@ void onGlobalRemove(void *, wl_registry *, uint32_t)
const wl_registry_listener listener = {onGlobal, onGlobalRemove};
wl_display *display()
{
auto *app = qGuiApp ? qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>() : nullptr;
return app ? app->display() : nullptr;
}
const QSet<QByteArray> &globals()
{
static QSet<QByteArray> names;
@@ -30,17 +36,16 @@ const QSet<QByteArray> &globals()
}
asked = true;
auto *app = qGuiApp ? qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>() : nullptr;
wl_display *display = app ? app->display() : nullptr;
if (!display) {
wl_display *d = display();
if (!d) {
return names;
}
wl_event_queue *queue = wl_display_create_queue(display);
auto *wrapped = static_cast<wl_display *>(wl_proxy_create_wrapper(display));
wl_event_queue *queue = wl_display_create_queue(d);
auto *wrapped = static_cast<wl_display *>(wl_proxy_create_wrapper(d));
wl_proxy_set_queue(reinterpret_cast<wl_proxy *>(wrapped), queue);
wl_registry *registry = wl_display_get_registry(wrapped);
wl_registry_add_listener(registry, &listener, &names);
wl_display_roundtrip_queue(display, queue);
wl_display_roundtrip_queue(d, queue);
wl_registry_destroy(registry);
wl_proxy_wrapper_destroy(wrapped);
wl_event_queue_destroy(queue);
@@ -52,3 +57,13 @@ bool Wayland::hasGlobal(const char *interface)
{
return globals().contains(QByteArray(interface));
}
void Wayland::sync()
{
// On a queue of its own, like above: Qt reads the default one.
if (wl_display *d = display()) {
wl_event_queue *queue = wl_display_create_queue(d);
wl_display_roundtrip_queue(d, queue);
wl_event_queue_destroy(queue);
}
}
+3
View File
@@ -12,4 +12,7 @@ namespace Wayland
// "zwlr_layer_shell_v1". Asked once, on an event queue of its own, so Qt's
// own handling of the connection is not disturbed.
bool hasGlobal(const char *interface);
// Wait until the compositor has handled everything sent so far.
void sync();
} // namespace Wayland
+2
View File
@@ -197,6 +197,7 @@ fu_do_help() {
faces $(fu_msg "List the faces")
remove ID $(fu_msg "Delete a face")
test $(fu_msg "Look at the camera and see what it sees")
lock $(fu_msg "Lock the screen")
status $(fu_msg "Show what is on")
-h, --help $(fu_msg "Show this help")
-V, --version $(fu_msg "Show the version")
@@ -236,6 +237,7 @@ main() {
remove|delete) fu_do_remove "$@" ;;
test|try) fu_ensure_service && fu_test ;;
status) fu_do_status ;;
lock) exec "$FU_AGENT" --lock ;;
-h|--help|help) fu_do_help ;;
-V|--version) printf '%s %s\n' "$FU_NAME" "$FU_VERSION" ;;
+3 -1
View File
@@ -31,7 +31,9 @@ FU_SYSCONFIG="${FU_SYSCONFIG:-/etc/${FU_NAME}/config}"
FU_UNIT_SOCKET="face-unlockd.socket"
FU_UNIT_AGENT="face-unlock-agent.service"
# The desktop this runs in: plasma, gnome, hyprland, niri or other.
# The desktop this runs in: plasma, gnome, hyprland, niri or other. All but
# the first two have a lock screen that is a program of its own, and there
# face-unlock's own one (`face-unlock lock`) is on offer.
case ":${XDG_CURRENT_DESKTOP:-}:" in
*:KDE:*) FU_DESKTOP=plasma ;;
*:GNOME:*) FU_DESKTOP=gnome ;;
+39 -3
View File
@@ -277,16 +277,20 @@ fu_ui_status() {
# scope user (this user's file), sys (the system file, through sudo),
# pam (the service of that name, through sudo), or group for a
# heading, with only the label after it
# type bool, choice (steps through the choices) or camera
# type bool, choice (steps through the choices), camera, or path (typed
# in; empty for the desktop's picture)
# needs a bool setting this one does nothing without; it is dimmed while
# that is off
# only lockers: only where the lock screen is a program of its own with a
# PAM file (Hyprland, Niri), see fu_pam_lockers_here
# PAM file (Hyprland, Niri), see fu_pam_lockers_here; ownlock: only
# where face-unlock's own lock screen can be used
FU_SETTINGS=(
"group|Lock screen"
"user|LockScreen|bool|yes|Unlock with your face"
"user|ScanOnWake|bool|yes|Scan when you come back||LockScreen"
"user|ScanOnLock|bool|no|Scan right after locking||LockScreen"
"user|LockWallpaper|path||Wallpaper|||ownlock"
"user|LockBlur|bool|no|Blur the wallpaper|||ownlock"
"group|Password prompts"
"pam|sudo|bool|no|sudo in a terminal"
"pam|polkit-1|bool|no|Admin prompts"
@@ -315,7 +319,7 @@ fu_setting_help() {
return
;;
hyprland:LockScreen|niri:LockScreen)
fu_msg "Scans when you come back to hyprlock or swaylock and opens them. Any other lock screen scans when you press Enter on the empty password field."
fu_msg "Scans when you come back to hyprlock, swaylock or face-unlock's own lock screen (\`face-unlock lock\`, with the bubble), and opens them. Other lock screens scan on Enter."
return
;;
esac
@@ -324,6 +328,8 @@ fu_setting_help() {
ScanOnWake:*) fu_msg "Scans when you press a key or move the mouse on the lock screen, and when the computer wakes up." ;;
ScanOnLock:*) fu_msg "Scans as soon as the screen locks. Off by default: if you lock it yourself, it would unlock again right away." ;;
sudo:*) fu_msg "sudo takes your face instead of the password. No match: you type the password as usual." ;;
LockWallpaper:*) fu_msg "The picture behind face-unlock's own lock screen (\`face-unlock lock\`). Empty: the one on your desktop. Or a file, a folder to take one from at random, or \`none\`. Enter to type it." ;;
LockBlur:*) fu_msg "Blurs the picture behind face-unlock's own lock screen." ;;
lockscreens:*) fu_msg "The lock screen takes your face in its password check. Press Enter on the empty field to scan. Found here: %s." "$(fu_pam_lockers_list)" ;;
polkit-1:*) fu_msg "The password windows of your desktop and apps, for example when you install software. No match: you type the password." ;;
Liveness:heavy) fu_msg "You have to blink or turn your head a little. This also stops printed photos." ;;
@@ -451,6 +457,24 @@ _fu_camera_label() {
printf '%s %s' "$value" "$(fu_msg "(not connected)")"
}
# _fu_path_label <path>
# A path as it fits in the list: with ~ for home, and cut from the left.
# Empty is the desktop's picture.
_fu_path_label() {
local path="$1"
if [[ -z $path ]]; then
fu_msg "same as desktop"
return
fi
if [[ ${path,,} == none ]]; then
fu_msg "none"
return
fi
[[ $path == "$HOME"/* ]] && path="~${path#"$HOME"}"
(( ${#path} > 36 )) && path="…${path: -35}"
printf '%s' "$path"
}
# _fu_setting_change <scope> <Key> <type> <current> <choices> <step>
_fu_setting_change() {
local scope="$1" key="$2" type="$3" current="$4" options="$5" step="$6" next
@@ -459,6 +483,16 @@ _fu_setting_change() {
bool) if fu_is_true "$current"; then next=no; else next=yes; fi ;;
choice) next="$(_fu_next_choice "$current" "$options" "$step")" ;;
camera) next="$(_fu_next_camera "$current" "$step")" ;;
path)
printf '\n %s ' "$(fu_msg "Picture or folder:")"
fu_ui_read_line "$current" || return 0
next="$FU_LINE_RESULT"
# shellcheck disable=SC2088 # typed by the user, expanded here
if [[ -n $next && ${next,,} != none && ! -e ${next/#\~/$HOME} ]]; then
fu_bad "$(fu_msg "Not found: %s" "$next")"
return 0
fi
;;
esac
case "$scope" in
@@ -501,6 +535,7 @@ fu_ui_settings() {
for spec in "${FU_SETTINGS[@]}"; do
IFS='|' read -r scope key type default label choice need only <<< "$spec"
[[ $only == lockers ]] && ! fu_pam_lockers_here && continue
[[ $only == ownlock && ( $FU_DESKTOP == plasma || $FU_DESKTOP == gnome ) ]] && continue
# A heading has its label where the key would be.
[[ $scope == group ]] && label="$key" key=''
scopes+=("$scope"); keys+=("$key"); types+=("$type"); defaults+=("$default")
@@ -568,6 +603,7 @@ fu_ui_settings() {
fi
;;
camera) shown="$(_fu_camera_label "${values[i]}")" ;;
path) shown="$(_fu_path_label "${values[i]}")" ;;
*) shown="$(fu_value_label "${keys[i]}" "${values[i]}")" ;;
esac
# Arrows on the selected choice: left and right step through it.